1
2
3
4
5
6
7
8
9
10
11
12
13
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
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
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
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 &&
307 (
308 property.isEditableType() ||
309 property.isEnum() ||
310 this.hasPropertySelectionModel( property, false ) ||
311 this.hasCustomField( property )
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
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
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 }