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 }