001    /*
002     * Copyright 2002-2004 the original author or authors.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.form.binding.support;
017    
018    import java.util.ArrayList;
019    import java.util.HashMap;
020    import java.util.Iterator;
021    import java.util.List;
022    import java.util.Map;
023    import java.util.Properties;
024    
025    import org.springframework.beans.factory.InitializingBean;
026    import org.springframework.beans.propertyeditors.ClassEditor;
027    import org.springframework.binding.form.FormModel;
028    import org.springframework.context.ApplicationContext;
029    import org.springframework.context.ApplicationContextAware;
030    import org.springframework.core.enums.LabeledEnum;
031    import org.springframework.richclient.form.binding.Binder;
032    import org.springframework.richclient.form.binding.BinderSelectionStrategy;
033    import org.springframework.richclient.util.ClassUtils;
034    import org.springframework.util.Assert;
035    
036    /**
037     * Default implementation of <code>BinderSelectionStrategy</code>. Provides for 
038     * registering of binders by control type, property type and property name.  
039     * 
040     * @author Oliver Hutchison
041     * @author Jim Moore
042     */
043    public abstract class AbstractBinderSelectionStrategy implements BinderSelectionStrategy, ApplicationContextAware, InitializingBean{
044    
045        private final Class defaultControlType;
046    
047        private final ClassEditor classEditor = new ClassEditor();
048        
049        private final Map controlTypeBinders = new HashMap();
050    
051        private final Map propertyTypeBinders = new HashMap();
052    
053        private final Map propertyNameBinders = new HashMap();
054        
055        private List bindersForPropertyNames = new ArrayList();
056        
057        private ApplicationContext applicationContext;
058    
059        public AbstractBinderSelectionStrategy(Class defaultControlType) {
060            this.defaultControlType = defaultControlType;
061            registerDefaultBinders();
062        }
063    
064        public Binder selectBinder(FormModel formModel, String propertyName) {
065            // first try and find a binder for the specific property name
066            Binder binder = findBinderByPropertyName(formModel.getFormObject().getClass(), propertyName);
067            if (binder == null) {
068                // next try and find a binder for the specific property type
069                binder = findBinderByPropertyType(getPropertyType(formModel, propertyName));
070            }
071            if (binder == null) {
072                // just find a binder for the default control type
073                binder = selectBinder(defaultControlType, formModel, propertyName);
074            }
075            if (binder != null) {
076                return binder;
077            }
078            throw new UnsupportedOperationException("Unable to select a binder for form model [" + formModel
079                    + "] property [" + propertyName + "]");
080        }
081    
082        public Binder selectBinder(Class controlType, FormModel formModel, String propertyName) {
083            Binder binder = findBinderByControlType(controlType);
084            if (binder == null) {
085                binder = selectBinder(formModel, propertyName);
086            }
087            if (binder != null) {
088                return binder;
089            }
090            throw new UnsupportedOperationException("Unable to select a binder for form model [" + formModel
091                    + "] property [" + propertyName + "]");
092        }
093    
094        /**
095         * Register the default set of binders. This method is called on construction.
096         * 
097         * @see #registerBinderForPropertyName(Class, String, Binder)
098         * @see #registerBinderForPropertyType(Class, Binder)
099         * @see #registerBinderForControlType(Class, Binder)
100         */
101        protected abstract void registerDefaultBinders();
102    
103        /**
104         * Try to find a binder for the provided parentObjectType and propertyName. If no 
105         * direct match found try to find binder for any superclass of the provided 
106         * objectType which also has the same propertyName.
107         */
108        protected Binder findBinderByPropertyName(Class parentObjectType, String propertyName) {
109            PropertyNameKey key = new PropertyNameKey(parentObjectType, propertyName);
110            Binder binder = (Binder)propertyNameBinders.get(key);
111            if (binder == null) {
112                // if no direct match was found try to find a match in any super classes
113                final Map potentialMatchingBinders = new HashMap();
114                for (Iterator i = propertyNameBinders.entrySet().iterator(); i.hasNext();) {
115                    Map.Entry entry = (Map.Entry)i.next();
116                    if (((PropertyNameKey)entry.getKey()).getPropertyName().equals(propertyName)) {
117                        potentialMatchingBinders.put(((PropertyNameKey)entry.getKey()).getParentObjectType(),
118                                entry.getValue());
119                    }
120                }
121                binder = (Binder)ClassUtils.getValueFromMapForClass(parentObjectType, potentialMatchingBinders);
122                if (binder != null) {
123                    // remember the lookup so it doesn't have to be discovered again 
124                    registerBinderForPropertyName(parentObjectType, propertyName, binder);
125                }
126            }
127            return binder;
128        }
129    
130        /**
131         * Try to find a binder for the provided propertyType. If no direct match found,
132         * try to find binder for closest superclass of the given control type.
133         */
134        protected Binder findBinderByPropertyType(Class propertyType) {
135            return (Binder)ClassUtils.getValueFromMapForClass(propertyType, propertyTypeBinders);
136        }
137    
138        /**
139         * Try to find a binder for the provided controlType. If no direct match found,
140         * try to find binder for closest superclass of the given control type.
141         */
142        protected Binder findBinderByControlType(Class controlType) {
143            return (Binder)ClassUtils.getValueFromMapForClass(controlType, controlTypeBinders);
144        }
145    
146        protected void registerBinderForPropertyName(Class parentObjectType, String propertyName, Binder binder) {
147            propertyNameBinders.put(new PropertyNameKey(parentObjectType, propertyName), binder);
148        }
149        
150        /**
151         * Add a list of binders that are bound to propertyNames. Each element in the list should
152         * be a Properties element describing the binder and propertyName. For more information
153         * about the structure of the properties see {@link #setBinderForPropertyName(Properties)}.
154    
155         * <br><br>
156         *  &lt;list&gt;<br>
157         *   &lt;props&gt;<br>
158         *     &lt;prop key="..."&gt;...&lt;/prop&gt;<br>
159         *     &lt;!-- More info in docs of setBinderForPropertyName(Properties)--&gt;<br>
160         *   &lt;/props&gt;<br>
161         *  &lt;/list&gt;<br>
162         *  
163         * @param binders List of <code>Properties</code> elements
164         * @see #setBinderForPropertyName(Properties)
165         */
166        public void setBindersForPropertyNames(List binders)
167        {
168            bindersForPropertyNames = binders;
169        }
170        
171        /**
172         * Create/link a <code>Binder</code> to a propertyName from the given <code>Properties</code>.
173         * <p> 
174         * The used keys are:
175         * <ul>
176         * <li><b>objectClass</b>: The bean which has the property.
177         * <li><b>propertyName</b>: The property that will need the binder.
178         * <li><b>binder</b>: The Fully Qualified ClassName that will be used to instantiate the <code>Binder</code>.
179         * <li><b>binderRef</b>: The beanId that identifies the <code>Binder</code> which is defined elsewhere.
180         * </ul>
181         * <p>
182         * The first two keys are mandatory in combination with one of the two latter (binder or binderRef)
183         * The following two cases can be used to define a binder/propertyName combination:
184         * <br><br>
185         *  &lt;props&gt;<br>
186         *    &lt;prop key="objectClass"&gt;mypackage.MyBean&lt;/prop&gt;<br>
187         *    &lt;prop key="propertyName"&gt;myProperty&lt;/prop&gt;<br>
188         *    &lt;prop key="binder"&gt;mypackage.MyBinder&lt;/prop&gt;<br>
189         *  &lt;/props&gt;<br>
190         *  <br>
191         *  &lt;props&gt;<br>
192         *    &lt;prop key="objectClass"&gt;mypackage.MyBean&lt;/prop&gt;<br>
193         *    &lt;prop key="propertyName"&gt;myProperty&lt;/prop&gt;<br>
194         *    &lt;prop key="binderRef"&gt;myBinderBeanId&lt;/prop&gt;<br>
195         *    &lt;!-- myBinderBeanId identifies a bean defined elsewhere--&gt;<br>
196         *  &lt;/props&gt;<br>
197         *   
198         * @param binder The <code>Properties</code> object containing the correct keys.
199         */
200        public void setBinderForPropertyName(Properties binder)
201        {
202            String objectClassName = (String) binder.get("objectClass");
203            if (objectClassName == null)
204                throw new IllegalArgumentException("objectClass is required");
205    
206            classEditor.setAsText(objectClassName);
207            Class objectClass = (Class) classEditor.getValue();
208            
209            String propertyName = (String) binder.get("propertyName");
210            if (propertyName == null)
211                throw new IllegalArgumentException("propertyName is required");
212            
213            if (binder.containsKey("binder"))
214            {
215                Object binderParameter = binder.get("binder");
216                classEditor.setAsText((String) binderParameter);
217                Class binderClass = (Class) classEditor.getValue();
218                try
219                {
220                    registerBinderForPropertyName(objectClass, propertyName, (Binder) binderClass.newInstance());
221                }
222                catch (Exception e)
223                {
224                    throw new IllegalArgumentException(
225                            "Could not instantiate new binder with default constructor: " + binderParameter);
226                }
227            }
228            else if (binder.containsKey("binderRef"))
229            {
230                String binderID = (String) binder.get("binderRef");
231                Binder binderBean = (Binder) getApplicationContext().getBean(binderID);
232                registerBinderForPropertyName(objectClass, propertyName, binderBean);
233            }
234            else
235                throw new IllegalArgumentException("binder or binderRef is required");
236        }
237    
238        protected void registerBinderForPropertyType(Class propertyType, Binder binder) {
239            propertyTypeBinders.put(propertyType, binder);
240        }
241    
242        /**
243         * Registers property type binders by extracting the key and value from each entry 
244         * in the provided map using the key to specify the property type and the value 
245         * to specify the binder.
246         * 
247         * <p>Binders specified in the provided map will override any binders previously 
248         * registered for the same property type.
249         * @param binders the map containing the entries to register; keys must be of type 
250         * <code>Class</code> and values of type <code>Binder</code>.
251         */
252        public void setBindersForPropertyTypes(Map binders) {
253            for (Iterator i = binders.entrySet().iterator(); i.hasNext();) {
254                Map.Entry entry = (Map.Entry)i.next();
255                registerBinderForPropertyType((Class)entry.getKey(), (Binder)entry.getValue());
256            }
257        }
258    
259        protected void registerBinderForControlType(Class controlType, Binder binder) {
260            controlTypeBinders.put(controlType, binder);
261        }
262    
263        /**
264         * Registers control type binders by extracting the key and value from each entry 
265         * in the provided map using the key to specify the property type and the value 
266         * to specify the binder.
267         * 
268         * <p>Binders specified in the provided map will override any binders previously 
269         * registered for the same control type.
270         * @param binders the map containing the entries to register; keys must be of type 
271         * <code>Class</code> and values of type <code>Binder</code>.
272         */
273        public void setBindersForControlTypes(Map binders) {
274            for (Iterator i = binders.entrySet().iterator(); i.hasNext();) {
275                Map.Entry entry = (Map.Entry)i.next();
276                registerBinderForControlType((Class)entry.getKey(), (Binder)entry.getValue());
277            }
278        }
279    
280        protected Class getPropertyType(FormModel formModel, String formPropertyPath) {
281            return formModel.getFieldMetadata(formPropertyPath).getPropertyType();
282        }
283    
284        protected boolean isEnumeration(FormModel formModel, String formPropertyPath) {
285            return LabeledEnum.class.isAssignableFrom(getPropertyType(formModel, formPropertyPath));
286        }
287    
288        private static class PropertyNameKey {
289            private final Class parentObjectType;
290    
291            private final String propertyName;
292    
293            public PropertyNameKey(Class parentObjectType, String propertyName) {
294                Assert.notNull(parentObjectType, "parentObjectType must not be null.");
295                Assert.notNull(propertyName, "propertyName must not be null.");
296                this.parentObjectType = parentObjectType;
297                this.propertyName = propertyName;
298            }
299    
300            public String getPropertyName() {
301                return propertyName;
302            }
303    
304            public Class getParentObjectType() {
305                return parentObjectType;
306            }
307    
308            public boolean equals(Object o) {
309                if (this == o) {
310                    return true;
311                }
312                if (!(o instanceof PropertyNameKey)) {
313                    return false;
314                }
315                final PropertyNameKey propertyNameKey = (PropertyNameKey)o;
316                return propertyName.equals(propertyNameKey.propertyName)
317                        && parentObjectType.equals(propertyNameKey.parentObjectType);
318            }
319    
320            public int hashCode() {
321                return (propertyName.hashCode() * 29) + parentObjectType.hashCode();
322            }
323        }
324    
325        public void setApplicationContext(ApplicationContext applicationContext)
326        {
327            this.applicationContext = applicationContext;
328        }
329        
330        protected ApplicationContext getApplicationContext()
331        {
332            return applicationContext;
333        }
334        
335        public void afterPropertiesSet() throws Exception
336        {
337            for (Iterator i = bindersForPropertyNames.iterator(); i.hasNext();)
338            {
339                setBinderForPropertyName((Properties) i.next());
340            }
341        }
342    }