Coverage Report - net.sf.beanform.BeanForm
 
Classes in this File Line Coverage Branch Coverage Complexity
BeanForm
98% 
100% 
0
 
 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  46
 public abstract class BeanForm extends BeanFormComponent implements PageDetachListener, IDirect {
 59  
 
 60  1
     public final static String BEAN_FORM_ATTRIBUTE = BeanForm.class.getName();
 61  
 
 62  1
     private final static Log LOG = LogFactory.getLog( BeanForm.class );
 63  1
     private final static Pattern PROPERTIES_PATTERN = Pattern.compile( "\\s*(#?[\\w\\.]+)\\s*(?:=\\s*([\\w]+)\\s*)?(?:\\{\\s*([^\\}]+)\\s*\\}\\s*)?,?" );
 64  1
     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  4
         if( this.customized == false && element instanceof IComponent) {
 99  3
             this.customized = this.isOrHasBeanFormComponent( (IComponent) element );
 100  
         }
 101  4
         super.addBody( element );
 102  4
     }
 103  
 
 104  
     @SuppressWarnings( "unchecked" )
 105  
     private boolean isOrHasBeanFormComponent( IComponent component ) {
 106  5
         if( component instanceof BeanFormComponent ) return true;
 107  3
         Map<String, IComponent> children = component.getComponents();
 108  3
         for( IComponent child : children.values() ) {
 109  2
             if( this.isOrHasBeanFormComponent( child ) ) return true;
 110  0
         }
 111  1
         return false;
 112  
     }
 113  
 
 114  
     public void pageDetached( PageEvent event ) {
 115  2
         if( this.getCacheProperties() == false ) this.cleanup();
 116  2
     }
 117  
 
 118  
     public boolean getIsInsideAForm() {
 119  2
         return this.getPage().getRequestCycle().getAttribute( TapestryUtils.FORM_ATTRIBUTE ) != null;
 120  
     }
 121  
 
 122  
     public boolean getIsNotCustomized() {
 123  6
         return ! this.customized;
 124  
     }
 125  
 
 126  
     public Object getBeanSafely() {
 127  22
         Object bean = this.getBean();
 128  22
         if( bean != null ) return bean;
 129  1
         else throw new ApplicationRuntimeException( BeanFormMessages.nullBean() );
 130  
     }
 131  
 
 132  
     public List<BeanProperty> getBeanProperties() {
 133  10
         this.init();
 134  8
         return this.properties;
 135  
     }
 136  
 
 137  
     public Map<String, IBinding> getFieldBindingsFor( BeanProperty property ) {
 138  6
         this.init();
 139  6
         return this.fieldBindings.get( property );
 140  
     }
 141  
 
 142  
     @SuppressWarnings( "unchecked" )
 143  
     public Map<String, IBinding> extractBindingOverrides( String prefix ) {
 144  158
         Map<String, IBinding> bindings = new HashMap<String, IBinding>();
 145  158
         Map<String, IBinding> allBindings = this.getBindings();
 146  158
         for( Entry<String, IBinding> entry : allBindings.entrySet() ) {
 147  90
             String name = entry.getKey();
 148  90
             if( name.startsWith( prefix ) ) {
 149  11
                 String newName = name.substring( prefix.length() );
 150  11
                 IBinding binding = entry.getValue();
 151  11
                 bindings.put( newName, binding );
 152  
             }
 153  90
         }
 154  158
         return bindings;
 155  
     }
 156  
 
 157  
     /**
 158  
      * Obvious shortcut.
 159  
      *
 160  
      * @see BeanFormComponent#getBeanForm()
 161  
      */
 162  
     @Override
 163  
     protected BeanForm getBeanForm() {
 164  39
         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  1
         IRequestCycle cycle = this.getPage().getRequestCycle();
 175  1
         BeanFormRows rows = (BeanFormRows) cycle.getAttribute( BeanFormRows.BEAN_FORM_ROWS_ATTRIBUTE );
 176  1
         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  1
         Object old = cycle.getAttribute( BEAN_FORM_ATTRIBUTE );
 188  1
         cycle.setAttribute( BEAN_FORM_ATTRIBUTE, this );
 189  1
         super.renderComponent( writer, cycle );
 190  1
         cycle.setAttribute( BEAN_FORM_ATTRIBUTE, old );
 191  1
     }
 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  1
         Object old = cycle.getAttribute( BEAN_FORM_ATTRIBUTE );
 202  1
         cycle.setAttribute( BEAN_FORM_ATTRIBUTE, this );
 203  1
         IForm form = (IForm) this.getComponent( INNER_FORM_COMPONENT_ID );
 204  1
         cycle.rewindForm( form );
 205  1
         cycle.setAttribute( BEAN_FORM_ATTRIBUTE, old );
 206  1
     }
 207  
 
 208  
     /* -------------------------------------------------------------------------------------------------------- */
 209  
     /* ------------------------------- cached state initialization and cleanup -------------------------------- */
 210  
     /* -------------------------------------------------------------------------------------------------------- */
 211  
 
 212  
     protected synchronized void init() {
 213  19
         this.initProperties();
 214  17
         this.initFieldBindings();
 215  17
     }
 216  
 
 217  
     protected synchronized void cleanup() {
 218  1
         this.properties = null;
 219  1
         this.fieldBindings = null;
 220  1
     }
 221  
 
 222  
     private void initProperties() {
 223  
 
 224  19
         if( this.properties != null ) return;
 225  
 
 226  15
         this.properties = new ArrayList<BeanProperty>();
 227  15
         Class clazz = this.getBeanSafely().getClass();
 228  15
         String props = this.getProperties();
 229  15
         String exclude = this.getExclude();
 230  
 
 231  15
         List<String> exclusions = new ArrayList<String>();
 232  15
         if( exclude != null ) {
 233  1
             Matcher m = EXCLUDE_PATTERN.matcher( exclude );
 234  5
             while( m.find() ) {
 235  4
                 String name = m.group( 1 );
 236  4
                 exclusions.add( name );
 237  4
             }
 238  1
             if( LOG.isDebugEnabled() ) {
 239  1
                 LOG.debug( "Excluding properties: " + exclusions );
 240  
             }
 241  
         }
 242  
 
 243  15
         if( props == null ) {
 244  
             // No properties were specified explicitly; use bean introspection.
 245  4
             BeanInfo info = this.getBeanInfo();
 246  4
             PropertyDescriptor[] descriptors = info.getPropertyDescriptors();
 247  72
             for( PropertyDescriptor descriptor : descriptors ) {
 248  68
                 String name = descriptor.getName();
 249  68
                 BeanProperty property = new BeanProperty( clazz, name, null, null );
 250  68
                 if( this.shouldIncludeProperty( property, exclusions ) ) {
 251  56
                     this.properties.add( property );
 252  
                 }
 253  
             }
 254  4
             if( LOG.isDebugEnabled() ) {
 255  4
                 LOG.debug( "No properties specified; defaulting to: " + this.properties );
 256  
             }
 257  4
         }
 258  
         else {
 259  
             // Included properties were specified explicitly.
 260  11
             Matcher m = PROPERTIES_PATTERN.matcher( props );
 261  32
             while( m.find() ) {
 262  23
                 String name = m.group( 1 );
 263  23
                 String input = m.group( 2 );
 264  23
                 String validators = m.group( 3 );
 265  23
                 if( validators != null ) validators = validators.trim();
 266  
                 BeanProperty property;
 267  23
                 if( name.startsWith( PSEUDO_PROPERTY_PREFIX ) ) {
 268  3
                     name = name.substring( PSEUDO_PROPERTY_PREFIX.length() );
 269  3
                     property = new PseudoProperty( clazz, name, validators, input );
 270  3
                     if( this.hasCustomField( property ) == false ) {
 271  1
                         String fieldName = this.getCustomFieldName( property );
 272  1
                         String blockName = this.getCustomFieldBlockName( property );
 273  1
                         String msg = BeanFormMessages.pseudoPropMissingField( property, fieldName, blockName );
 274  1
                         throw new ApplicationRuntimeException( msg );
 275  
                     }
 276  
                 }
 277  
                 else {
 278  20
                     property = new BeanProperty( clazz, name, validators, input );
 279  20
                     if( this.shouldIncludeProperty( property, exclusions ) == false ) {
 280  1
                         String msg = BeanFormMessages.unmodifiableExplicitProperty( property );
 281  1
                         throw new ApplicationRuntimeException( msg );
 282  
                     }
 283  
                 }
 284  21
                 this.properties.add( property );
 285  21
             }
 286  9
             if( LOG.isDebugEnabled() ) {
 287  9
                 LOG.debug( "Using specified properties: " + this.properties );
 288  
             }
 289  
         }
 290  13
     }
 291  
 
 292  
     private BeanInfo getBeanInfo() {
 293  4
         Object bean = this.getBeanSafely();
 294  
         BeanInfo info;
 295  
         try {
 296  4
             info = Introspector.getBeanInfo( bean.getClass() );
 297  
         }
 298  0
         catch( IntrospectionException e ) {
 299  0
             throw new ApplicationRuntimeException( e );
 300  4
         }
 301  4
         return info;
 302  
     }
 303  
 
 304  
     private boolean shouldIncludeProperty( BeanProperty property, List<String> exclusions ) {
 305  88
         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  17
         if( this.fieldBindings != null ) return;
 318  13
         this.fieldBindings = new HashMap<BeanProperty, Map<String, IBinding>>( this.properties.size() );
 319  13
         for( BeanProperty prop : this.properties ) {
 320  77
             Map<String, IBinding> bindings = new HashMap<String, IBinding>();
 321  
             // Add user-defined binding overrides.
 322  77
             String prefix1 = prop.getName() + BINDING_OVERRIDE_SEPARATOR;
 323  77
             String prefix2 = BINDING_OVERRIDE_SEPARATOR;
 324  77
             bindings.putAll( this.extractBindingOverrides( prefix1 ) );
 325  77
             bindings.putAll( this.extractBindingOverrides( prefix2 ) );
 326  
             // Add implicit enum IPropertySelectionModel bindings if the user didn't provide them explicitly.
 327  77
             if( prop.isEnum() ) {
 328  3
                 if( bindings.containsKey( MODEL ) == false ) {
 329  2
                     String desc = "enum model for " + prop.getOwnerClass().getName() + "#" + prop.getName();
 330  2
                     ValueConverter converter = new ValueConverterImpl();
 331  2
                     Location location = null;
 332  2
                     IPropertySelectionModel psm = new EnumPropertySelectionModel( prop.getType(), prop.isNullable(), this.getPage().getMessages() );
 333  2
                     IBinding binding = new ObjectBinding( desc, converter, location, psm );
 334  2
                     bindings.put( MODEL, binding );
 335  
                 }
 336  
             }
 337  77
             this.fieldBindings.put( prop, bindings );
 338  77
         }
 339  13
     }
 340  
 
 341  
 }