001    // Copyright 2006-2007 Daniel Gredler
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package net.sf.beanform;
016    
017    import java.beans.BeanInfo;
018    import java.beans.IntrospectionException;
019    import java.beans.Introspector;
020    import java.beans.PropertyDescriptor;
021    import java.util.ArrayList;
022    import java.util.HashMap;
023    import java.util.List;
024    import java.util.Map;
025    import java.util.Map.Entry;
026    import java.util.regex.Matcher;
027    import java.util.regex.Pattern;
028    
029    import net.sf.beanform.binding.ObjectBinding;
030    import net.sf.beanform.prop.BeanProperty;
031    import net.sf.beanform.prop.PseudoProperty;
032    import net.sf.beanform.util.EnumPropertySelectionModel;
033    
034    import org.apache.commons.logging.Log;
035    import org.apache.commons.logging.LogFactory;
036    import org.apache.hivemind.ApplicationRuntimeException;
037    import org.apache.hivemind.Location;
038    import org.apache.tapestry.IActionListener;
039    import org.apache.tapestry.IBinding;
040    import org.apache.tapestry.IComponent;
041    import org.apache.tapestry.IDirect;
042    import org.apache.tapestry.IForm;
043    import org.apache.tapestry.IMarkupWriter;
044    import org.apache.tapestry.IRender;
045    import org.apache.tapestry.IRequestCycle;
046    import org.apache.tapestry.TapestryUtils;
047    import org.apache.tapestry.coerce.ValueConverter;
048    import org.apache.tapestry.coerce.ValueConverterImpl;
049    import org.apache.tapestry.event.PageDetachListener;
050    import org.apache.tapestry.event.PageEvent;
051    import org.apache.tapestry.form.IPropertySelectionModel;
052    
053    /**
054     * A form that provides edit capabilities for a Java Bean.
055     *
056     * @author Daniel Gredler
057     */
058    public abstract class BeanForm extends BeanFormComponent implements PageDetachListener, IDirect {
059    
060        public final static String BEAN_FORM_ATTRIBUTE = BeanForm.class.getName();
061    
062        private final static Log LOG = LogFactory.getLog( BeanForm.class );
063        private final static Pattern PROPERTIES_PATTERN = Pattern.compile( "\\s*(#?[\\w\\.]+)\\s*(?:=\\s*([\\w]+)\\s*)?(?:\\{\\s*([^\\}]+)\\s*\\}\\s*)?,?" );
064        private final static Pattern EXCLUDE_PATTERN = Pattern.compile( "\\s*([\\w\\.]+)\\s*,?" );
065        private final static String INNER_FORM_COMPONENT_ID = "form";
066        private final static String PSEUDO_PROPERTY_PREFIX = "#";
067    
068        private boolean customized;
069        private List<BeanProperty> properties;
070        private Map<BeanProperty, Map<String, IBinding>> fieldBindings;
071    
072        public abstract Object getBean();
073        public abstract void setBean( Object bean );
074    
075        public abstract String getProperties();
076        public abstract void setProperties( String properties );
077    
078        public abstract String getExclude();
079        public abstract void setExclude( String exclude );
080    
081        public abstract boolean getCacheProperties();
082        public abstract void setCacheProperties( boolean cacheProperties );
083    
084        public abstract IActionListener getSave();
085        public abstract void setSave( IActionListener save );
086    
087        public abstract IActionListener getCancel();
088        public abstract void setCancel( IActionListener cancel );
089    
090        public abstract IActionListener getRefresh();
091        public abstract void setRefresh( IActionListener refresh );
092    
093        public abstract IActionListener getDelete();
094        public abstract void setDelete( IActionListener delete );
095        
096        public abstract List getUpdateComponents();
097    
098        @Override
099        public void addBody( IRender element ) {
100            if( this.customized == false && element instanceof IComponent) {
101                this.customized = BeanForm.isOrHasBeanFormComponent( (IComponent) element );
102            }
103            super.addBody( element );
104        }
105    
106        @SuppressWarnings( "unchecked" )
107        private static boolean isOrHasBeanFormComponent( IComponent component ) {
108            if( component instanceof BeanFormComponent ) return true;
109            Map<String, IComponent> children = component.getComponents();
110            for( IComponent child : children.values() ) {
111                if( BeanForm.isOrHasBeanFormComponent( child ) ) return true;
112            }
113            return false;
114        }
115    
116        public void pageDetached( PageEvent event ) {
117            if( this.getCacheProperties() == false ) this.cleanup();
118        }
119    
120        public boolean getIsInsideAForm() {
121            return this.getPage().getRequestCycle().getAttribute( TapestryUtils.FORM_ATTRIBUTE ) != null;
122        }
123    
124        public boolean getIsNotCustomized() {
125            return ! this.customized;
126        }
127    
128        public Object getBeanSafely() {
129            Object bean = this.getBean();
130            if( bean != null ) return bean;
131            else throw new ApplicationRuntimeException( BeanFormMessages.nullBean() );
132        }
133    
134        public List<BeanProperty> getBeanProperties() {
135            this.init();
136            return this.properties;
137        }
138    
139        public Map<String, IBinding> getFieldBindingsFor( BeanProperty property ) {
140            this.init();
141            return this.fieldBindings.get( property );
142        }
143    
144        @SuppressWarnings( "unchecked" )
145        public Map<String, IBinding> extractBindingOverrides( String prefix ) {
146            Map<String, IBinding> bindings = new HashMap<String, IBinding>();
147            Map<String, IBinding> allBindings = this.getBindings();
148            for( Entry<String, IBinding> entry : allBindings.entrySet() ) {
149                String name = entry.getKey();
150                if( name.startsWith( prefix ) ) {
151                    String newName = name.substring( prefix.length() );
152                    IBinding binding = entry.getValue();
153                    bindings.put( newName, binding );
154                }
155            }
156            return bindings;
157        }
158    
159        /**
160         * Obvious shortcut.
161         *
162         * @see BeanFormComponent#getBeanForm()
163         */
164        @Override
165        protected BeanForm getBeanForm() {
166            return this;
167        }
168    
169        /**
170         * This method exists only for the convenience of users who wish to reference the current
171         * property from within OGNL binding overrides that are applied to all property input
172         * fields. It could be done without this method, but the user would have to know the ID of
173         * the contained {@link BeanFormRows} component.
174         */
175        public BeanProperty getProperty() {
176            IRequestCycle cycle = this.getPage().getRequestCycle();
177            BeanFormRows rows = (BeanFormRows) cycle.getAttribute( BeanFormRows.BEAN_FORM_ROWS_ATTRIBUTE );
178            return rows.getProperty();
179        }
180    
181        /**
182         * All low level BeanForm components expect to be able to retrieve
183         * their containing BeanForm during the render phase.
184         *
185         * @see BeanFormComponent#getBeanForm()
186         */
187        @Override
188        protected void renderComponent( IMarkupWriter writer, IRequestCycle cycle ) {
189            Object old = cycle.getAttribute( BEAN_FORM_ATTRIBUTE );
190            cycle.setAttribute( BEAN_FORM_ATTRIBUTE, this );
191            super.renderComponent( writer, cycle );
192            cycle.setAttribute( BEAN_FORM_ATTRIBUTE, old );
193        }
194    
195        /**
196         * All low level BeanForm components expect to be able to retrieve
197         * their containing BeanForm during the rewind phase.
198         *
199         * @see BeanFormComponent#getBeanForm()
200         * @see IDirect#trigger(IRequestCycle)
201         */
202        public void trigger( IRequestCycle cycle ) {
203            Object old = cycle.getAttribute( BEAN_FORM_ATTRIBUTE );
204            cycle.setAttribute( BEAN_FORM_ATTRIBUTE, this );
205            IForm form = (IForm) this.getComponent( INNER_FORM_COMPONENT_ID );
206            cycle.rewindForm( form );
207            cycle.setAttribute( BEAN_FORM_ATTRIBUTE, old );
208        }
209    
210        /* -------------------------------------------------------------------------------------------------------- */
211        /* ------------------------------- cached state initialization and cleanup -------------------------------- */
212        /* -------------------------------------------------------------------------------------------------------- */
213    
214        protected synchronized void init() {
215            this.initProperties();
216            this.initFieldBindings();
217        }
218    
219        protected synchronized void cleanup() {
220            this.properties = null;
221            this.fieldBindings = null;
222        }
223    
224        private void initProperties() {
225    
226            if( this.properties != null ) return;
227    
228            this.properties = new ArrayList<BeanProperty>();
229            Class clazz = this.getBeanSafely().getClass();
230            String props = this.getProperties();
231            String exclude = this.getExclude();
232    
233            List<String> exclusions = new ArrayList<String>();
234            if( exclude != null ) {
235                Matcher m = EXCLUDE_PATTERN.matcher( exclude );
236                while( m.find() ) {
237                    String name = m.group( 1 );
238                    exclusions.add( name );
239                }
240                if( LOG.isDebugEnabled() ) {
241                    LOG.debug( "Excluding properties: " + exclusions );
242                }
243            }
244    
245            if( props == null ) {
246                // No properties were specified explicitly; use bean introspection.
247                BeanInfo info = this.getBeanInfo();
248                PropertyDescriptor[] descriptors = info.getPropertyDescriptors();
249                for( PropertyDescriptor descriptor : descriptors ) {
250                    String name = descriptor.getName();
251                    BeanProperty property = new BeanProperty( clazz, name, null, null );
252                    if( this.shouldIncludeProperty( property, exclusions ) ) {
253                        this.properties.add( property );
254                    }
255                }
256                if( LOG.isDebugEnabled() ) {
257                    LOG.debug( "No properties specified; defaulting to: " + this.properties );
258                }
259            }
260            else {
261                // Included properties were specified explicitly.
262                Matcher m = PROPERTIES_PATTERN.matcher( props );
263                while( m.find() ) {
264                    String name = m.group( 1 );
265                    String input = m.group( 2 );
266                    String validators = m.group( 3 );
267                    if( validators != null ) validators = validators.trim();
268                    BeanProperty property;
269                    if( name.startsWith( PSEUDO_PROPERTY_PREFIX ) ) {
270                        name = name.substring( PSEUDO_PROPERTY_PREFIX.length() );
271                        property = new PseudoProperty( clazz, name, validators, input );
272                        if( this.hasCustomField( property ) == false ) {
273                            String fieldName = this.getCustomFieldName( property );
274                            String blockName = this.getCustomFieldBlockName( property );
275                            String msg = BeanFormMessages.pseudoPropMissingField( property, fieldName, blockName );
276                            throw new ApplicationRuntimeException( msg );
277                        }
278                    }
279                    else {
280                        property = new BeanProperty( clazz, name, validators, input );
281                        if( this.shouldIncludeProperty( property, exclusions ) == false ) {
282                            String msg = BeanFormMessages.unmodifiableExplicitProperty( property );
283                            throw new ApplicationRuntimeException( msg );
284                        }
285                    }
286                    this.properties.add( property );
287                }
288                if( LOG.isDebugEnabled() ) {
289                    LOG.debug( "Using specified properties: " + this.properties );
290                }
291            }
292        }
293    
294        private BeanInfo getBeanInfo() {
295            Object bean = this.getBeanSafely();
296            BeanInfo info;
297            try {
298                info = Introspector.getBeanInfo( bean.getClass() );
299            }
300            catch( IntrospectionException e ) {
301                throw new ApplicationRuntimeException( e );
302            }
303            return info;
304        }
305    
306        private boolean shouldIncludeProperty( BeanProperty property, List<String> exclusions ) {
307            return
308                exclusions.contains( property.getName() ) == false &&     // Not excluded by the user.
309                (
310                    property.isEditableType() ||                          // It's an editable type.
311                    property.isEnum() ||                                  // It's not an editable type, but we're going to add an implicit IPropertySelectionModel.
312                    this.hasPropertySelectionModel( property, false ) ||  // It's not an editable type, but the user provided an IPropertySelectionModel.
313                    this.hasCustomField( property )                       // It's not an editable type, but the user provided an input override.
314                );
315        }
316    
317        @SuppressWarnings( "unchecked" )
318        private void initFieldBindings() {
319            if( this.fieldBindings != null ) return;
320            this.fieldBindings = new HashMap<BeanProperty, Map<String, IBinding>>( this.properties.size() );
321            for( BeanProperty prop : this.properties ) {
322                Map<String, IBinding> bindings = new HashMap<String, IBinding>();
323                // Add user-defined binding overrides.
324                String prefix1 = prop.getName() + BINDING_OVERRIDE_SEPARATOR;
325                String prefix2 = BINDING_OVERRIDE_SEPARATOR;
326                bindings.putAll( this.extractBindingOverrides( prefix1 ) );
327                bindings.putAll( this.extractBindingOverrides( prefix2 ) );
328                // Add implicit enum IPropertySelectionModel bindings if the user didn't provide them explicitly.
329                if( prop.isEnum() ) {
330                    if( bindings.containsKey( MODEL ) == false ) {
331                        String desc = "enum model for " + prop.getOwnerClass().getName() + "#" + prop.getName();
332                        ValueConverter converter = new ValueConverterImpl();
333                        Location location = null;
334                        IPropertySelectionModel psm = new EnumPropertySelectionModel( prop.getType(), prop.isNullable(), this.getPage().getMessages() );
335                        IBinding binding = new ObjectBinding( desc, converter, location, psm );
336                        bindings.put( MODEL, binding );
337                    }
338                }
339                this.fieldBindings.put( prop, bindings );
340            }
341        }
342    
343    }