View Javadoc

1   // Copyright 2006 Daniel Gredler
2   //
3   // Licensed under the Apache License, Version 2.0 (the "License");
4   // you may not use this file except in compliance with the License.
5   // You may obtain a copy of the License at
6   //
7   //     http://www.apache.org/licenses/LICENSE-2.0
8   //
9   // Unless required by applicable law or agreed to in writing, software
10  // distributed under the License is distributed on an "AS IS" BASIS,
11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  // See the License for the specific language governing permissions and
13  // limitations under the License.
14  
15  package net.sf.beanform;
16  
17  import java.beans.BeanInfo;
18  import java.beans.IntrospectionException;
19  import java.beans.Introspector;
20  import java.beans.PropertyDescriptor;
21  import java.util.ArrayList;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Map.Entry;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import net.sf.beanform.binding.ObjectBinding;
30  import net.sf.beanform.prop.BeanProperty;
31  import net.sf.beanform.prop.PseudoProperty;
32  import net.sf.beanform.util.EnumPropertySelectionModel;
33  
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  import org.apache.hivemind.ApplicationRuntimeException;
37  import org.apache.hivemind.Location;
38  import org.apache.tapestry.IActionListener;
39  import org.apache.tapestry.IBinding;
40  import org.apache.tapestry.IComponent;
41  import org.apache.tapestry.IDirect;
42  import org.apache.tapestry.IForm;
43  import org.apache.tapestry.IMarkupWriter;
44  import org.apache.tapestry.IRender;
45  import org.apache.tapestry.IRequestCycle;
46  import org.apache.tapestry.TapestryUtils;
47  import org.apache.tapestry.coerce.ValueConverter;
48  import org.apache.tapestry.coerce.ValueConverterImpl;
49  import org.apache.tapestry.event.PageDetachListener;
50  import org.apache.tapestry.event.PageEvent;
51  import org.apache.tapestry.form.IPropertySelectionModel;
52  
53  /***
54   * A form that provides edit capabilities for a Java Bean.
55   *
56   * @author Daniel Gredler
57   */
58  public abstract class BeanForm extends BeanFormComponent implements PageDetachListener, IDirect {
59  
60      public final static String BEAN_FORM_ATTRIBUTE = BeanForm.class.getName();
61  
62      private final static Log LOG = LogFactory.getLog( BeanForm.class );
63      private final static Pattern PROPERTIES_PATTERN = Pattern.compile( "//s*(#?[//w//.]+)//s*(?:=//s*([//w]+)//s*)?(?://{//s*([^//}]+)//s*//}//s*)?,?" );
64      private final static Pattern EXCLUDE_PATTERN = Pattern.compile( "//s*([//w//.]+)//s*,?" );
65      private final static String INNER_FORM_COMPONENT_ID = "form";
66      private final static String PSEUDO_PROPERTY_PREFIX = "#";
67  
68      private boolean customized;
69      private List<BeanProperty> properties;
70      private Map<BeanProperty, Map<String, IBinding>> fieldBindings;
71  
72      public abstract Object getBean();
73      public abstract void setBean( Object bean );
74  
75      public abstract String getProperties();
76      public abstract void setProperties( String properties );
77  
78      public abstract String getExclude();
79      public abstract void setExclude( String exclude );
80  
81      public abstract boolean getCacheProperties();
82      public abstract void setCacheProperties( boolean cacheProperties );
83  
84      public abstract IActionListener getSave();
85      public abstract void setSave( IActionListener save );
86  
87      public abstract IActionListener getCancel();
88      public abstract void setCancel( IActionListener cancel );
89  
90      public abstract IActionListener getRefresh();
91      public abstract void setRefresh( IActionListener refresh );
92  
93      public abstract IActionListener getDelete();
94      public abstract void setDelete( IActionListener delete );
95  
96      @Override
97      public void addBody( IRender element ) {
98          if( this.customized == false && element instanceof IComponent) {
99              this.customized = this.isOrHasBeanFormComponent( (IComponent) element );
100         }
101         super.addBody( element );
102     }
103 
104     @SuppressWarnings( "unchecked" )
105     private boolean isOrHasBeanFormComponent( IComponent component ) {
106         if( component instanceof BeanFormComponent ) return true;
107         Map<String, IComponent> children = component.getComponents();
108         for( IComponent child : children.values() ) {
109             if( this.isOrHasBeanFormComponent( child ) ) return true;
110         }
111         return false;
112     }
113 
114     public void pageDetached( PageEvent event ) {
115         if( this.getCacheProperties() == false ) this.cleanup();
116     }
117 
118     public boolean getIsInsideAForm() {
119         return this.getPage().getRequestCycle().getAttribute( TapestryUtils.FORM_ATTRIBUTE ) != null;
120     }
121 
122     public boolean getIsNotCustomized() {
123         return ! this.customized;
124     }
125 
126     public Object getBeanSafely() {
127         Object bean = this.getBean();
128         if( bean != null ) return bean;
129         else throw new ApplicationRuntimeException( BeanFormMessages.nullBean() );
130     }
131 
132     public List<BeanProperty> getBeanProperties() {
133         this.init();
134         return this.properties;
135     }
136 
137     public Map<String, IBinding> getFieldBindingsFor( BeanProperty property ) {
138         this.init();
139         return this.fieldBindings.get( property );
140     }
141 
142     @SuppressWarnings( "unchecked" )
143     public Map<String, IBinding> extractBindingOverrides( String prefix ) {
144         Map<String, IBinding> bindings = new HashMap<String, IBinding>();
145         Map<String, IBinding> allBindings = this.getBindings();
146         for( Entry<String, IBinding> entry : allBindings.entrySet() ) {
147             String name = entry.getKey();
148             if( name.startsWith( prefix ) ) {
149                 String newName = name.substring( prefix.length() );
150                 IBinding binding = entry.getValue();
151                 bindings.put( newName, binding );
152             }
153         }
154         return bindings;
155     }
156 
157     /***
158      * Obvious shortcut.
159      *
160      * @see BeanFormComponent#getBeanForm()
161      */
162     @Override
163     protected BeanForm getBeanForm() {
164         return this;
165     }
166 
167     /***
168      * This method exists only for the convenience of users who wish to reference the current
169      * property from within OGNL binding overrides that are applied to all property input
170      * fields. It could be done without this method, but the user would have to know the ID of
171      * the contained {@link BeanFormRows} component.
172      */
173     public BeanProperty getProperty() {
174         IRequestCycle cycle = this.getPage().getRequestCycle();
175         BeanFormRows rows = (BeanFormRows) cycle.getAttribute( BeanFormRows.BEAN_FORM_ROWS_ATTRIBUTE );
176         return rows.getProperty();
177     }
178 
179     /***
180      * All low level BeanForm components expect to be able to retrieve
181      * their containing BeanForm during the render phase.
182      *
183      * @see BeanFormComponent#getBeanForm()
184      */
185     @Override
186     protected void renderComponent( IMarkupWriter writer, IRequestCycle cycle ) {
187         Object old = cycle.getAttribute( BEAN_FORM_ATTRIBUTE );
188         cycle.setAttribute( BEAN_FORM_ATTRIBUTE, this );
189         super.renderComponent( writer, cycle );
190         cycle.setAttribute( BEAN_FORM_ATTRIBUTE, old );
191     }
192 
193     /***
194      * All low level BeanForm components expect to be able to retrieve
195      * their containing BeanForm during the rewind phase.
196      *
197      * @see BeanFormComponent#getBeanForm()
198      * @see IDirect#trigger(IRequestCycle)
199      */
200     public void trigger( IRequestCycle cycle ) {
201         Object old = cycle.getAttribute( BEAN_FORM_ATTRIBUTE );
202         cycle.setAttribute( BEAN_FORM_ATTRIBUTE, this );
203         IForm form = (IForm) this.getComponent( INNER_FORM_COMPONENT_ID );
204         cycle.rewindForm( form );
205         cycle.setAttribute( BEAN_FORM_ATTRIBUTE, old );
206     }
207 
208     /* -------------------------------------------------------------------------------------------------------- */
209     /* ------------------------------- cached state initialization and cleanup -------------------------------- */
210     /* -------------------------------------------------------------------------------------------------------- */
211 
212     protected synchronized void init() {
213         this.initProperties();
214         this.initFieldBindings();
215     }
216 
217     protected synchronized void cleanup() {
218         this.properties = null;
219         this.fieldBindings = null;
220     }
221 
222     private void initProperties() {
223 
224         if( this.properties != null ) return;
225 
226         this.properties = new ArrayList<BeanProperty>();
227         Class clazz = this.getBeanSafely().getClass();
228         String props = this.getProperties();
229         String exclude = this.getExclude();
230 
231         List<String> exclusions = new ArrayList<String>();
232         if( exclude != null ) {
233             Matcher m = EXCLUDE_PATTERN.matcher( exclude );
234             while( m.find() ) {
235                 String name = m.group( 1 );
236                 exclusions.add( name );
237             }
238             if( LOG.isDebugEnabled() ) {
239                 LOG.debug( "Excluding properties: " + exclusions );
240             }
241         }
242 
243         if( props == null ) {
244             // No properties were specified explicitly; use bean introspection.
245             BeanInfo info = this.getBeanInfo();
246             PropertyDescriptor[] descriptors = info.getPropertyDescriptors();
247             for( PropertyDescriptor descriptor : descriptors ) {
248                 String name = descriptor.getName();
249                 BeanProperty property = new BeanProperty( clazz, name, null, null );
250                 if( this.shouldIncludeProperty( property, exclusions ) ) {
251                     this.properties.add( property );
252                 }
253             }
254             if( LOG.isDebugEnabled() ) {
255                 LOG.debug( "No properties specified; defaulting to: " + this.properties );
256             }
257         }
258         else {
259             // Included properties were specified explicitly.
260             Matcher m = PROPERTIES_PATTERN.matcher( props );
261             while( m.find() ) {
262                 String name = m.group( 1 );
263                 String input = m.group( 2 );
264                 String validators = m.group( 3 );
265                 if( validators != null ) validators = validators.trim();
266                 BeanProperty property;
267                 if( name.startsWith( PSEUDO_PROPERTY_PREFIX ) ) {
268                     name = name.substring( PSEUDO_PROPERTY_PREFIX.length() );
269                     property = new PseudoProperty( clazz, name, validators, input );
270                     if( this.hasCustomField( property ) == false ) {
271                         String fieldName = this.getCustomFieldName( property );
272                         String blockName = this.getCustomFieldBlockName( property );
273                         String msg = BeanFormMessages.pseudoPropMissingField( property, fieldName, blockName );
274                         throw new ApplicationRuntimeException( msg );
275                     }
276                 }
277                 else {
278                     property = new BeanProperty( clazz, name, validators, input );
279                     if( this.shouldIncludeProperty( property, exclusions ) == false ) {
280                         String msg = BeanFormMessages.unmodifiableExplicitProperty( property );
281                         throw new ApplicationRuntimeException( msg );
282                     }
283                 }
284                 this.properties.add( property );
285             }
286             if( LOG.isDebugEnabled() ) {
287                 LOG.debug( "Using specified properties: " + this.properties );
288             }
289         }
290     }
291 
292     private BeanInfo getBeanInfo() {
293         Object bean = this.getBeanSafely();
294         BeanInfo info;
295         try {
296             info = Introspector.getBeanInfo( bean.getClass() );
297         }
298         catch( IntrospectionException e ) {
299             throw new ApplicationRuntimeException( e );
300         }
301         return info;
302     }
303 
304     private boolean shouldIncludeProperty( BeanProperty property, List<String> exclusions ) {
305         return
306             exclusions.contains( property.getName() ) == false &&     // Not excluded by the user.
307             (
308                 property.isEditableType() ||                          // It's an editable type.
309                 property.isEnum() ||                                  // It's not an editable type, but we're going to add an implicit IPropertySelectionModel.
310                 this.hasPropertySelectionModel( property, false ) ||  // It's not an editable type, but the user provided an IPropertySelectionModel.
311                 this.hasCustomField( property )                       // It's not an editable type, but the user provided an input override.
312             );
313     }
314 
315     @SuppressWarnings( "unchecked" )
316     private void initFieldBindings() {
317         if( this.fieldBindings != null ) return;
318         this.fieldBindings = new HashMap<BeanProperty, Map<String, IBinding>>( this.properties.size() );
319         for( BeanProperty prop : this.properties ) {
320             Map<String, IBinding> bindings = new HashMap<String, IBinding>();
321             // Add user-defined binding overrides.
322             String prefix1 = prop.getName() + BINDING_OVERRIDE_SEPARATOR;
323             String prefix2 = BINDING_OVERRIDE_SEPARATOR;
324             bindings.putAll( this.extractBindingOverrides( prefix1 ) );
325             bindings.putAll( this.extractBindingOverrides( prefix2 ) );
326             // Add implicit enum IPropertySelectionModel bindings if the user didn't provide them explicitly.
327             if( prop.isEnum() ) {
328                 if( bindings.containsKey( MODEL ) == false ) {
329                     String desc = "enum model for " + prop.getOwnerClass().getName() + "#" + prop.getName();
330                     ValueConverter converter = new ValueConverterImpl();
331                     Location location = null;
332                     IPropertySelectionModel psm = new EnumPropertySelectionModel( prop.getType(), prop.isNullable(), this.getPage().getMessages() );
333                     IBinding binding = new ObjectBinding( desc, converter, location, psm );
334                     bindings.put( MODEL, binding );
335                 }
336             }
337             this.fieldBindings.put( prop, bindings );
338         }
339     }
340 
341 }