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 }