/** * Copyright (C) 2015 Valkyrie RCP * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.valkyriercp.form.binding.support; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.ClassEditor; import org.springframework.context.ApplicationContext; import org.springframework.util.Assert; import org.valkyriercp.binding.form.FormModel; import org.valkyriercp.form.binding.Binder; import org.valkyriercp.form.binding.BinderSelectionStrategy; import org.valkyriercp.util.ClassUtils; import javax.annotation.PostConstruct; import java.util.*; /** * Default implementation of <code>BinderSelectionStrategy</code>. Provides for * registering of binders by control type, property type and property name. * * @author Oliver Hutchison * @author Jim Moore */ public abstract class AbstractBinderSelectionStrategy implements BinderSelectionStrategy { private final Class defaultControlType; private final ClassEditor classEditor = new ClassEditor(); private final Map controlTypeBinders = new HashMap(); private final Map propertyTypeBinders = new HashMap(); private final Map propertyNameBinders = new HashMap(); private List bindersForPropertyNames = new ArrayList(); @Autowired private ApplicationContext applicationContext; public AbstractBinderSelectionStrategy(Class defaultControlType) { this.defaultControlType = defaultControlType; } public Binder selectBinder(FormModel formModel, String propertyName) { // first try and find a binder for the specific property name Binder binder = findBinderByPropertyName(formModel.getFormObject().getClass(), propertyName); if (binder == null) { // next try and find a binder for the specific property type binder = findBinderByPropertyType(getPropertyType(formModel, propertyName)); } if (binder == null) { // just find a binder for the default control type binder = selectBinder(defaultControlType, formModel, propertyName); } if (binder != null) { return binder; } throw new UnsupportedOperationException("Unable to select a binder for form model [" + formModel + "] property [" + propertyName + "]"); } public Binder selectBinder(Class controlType, FormModel formModel, String propertyName) { Binder binder = findBinderByControlType(controlType); if (binder == null) { binder = selectBinder(formModel, propertyName); } if (binder != null) { return binder; } throw new UnsupportedOperationException("Unable to select a binder for form model [" + formModel + "] property [" + propertyName + "]"); } /** * Try to find a binder for the provided parentObjectType and propertyName. If no * direct match found try to find binder for any superclass of the provided * objectType which also has the same propertyName. */ protected Binder findBinderByPropertyName(Class parentObjectType, String propertyName) { PropertyNameKey key = new PropertyNameKey(parentObjectType, propertyName); Binder binder = (Binder)propertyNameBinders.get(key); if (binder == null) { // if no direct match was found try to find a match in any super classes final Map potentialMatchingBinders = new HashMap(); for (Iterator i = propertyNameBinders.entrySet().iterator(); i.hasNext();) { Map.Entry entry = (Map.Entry)i.next(); if (((PropertyNameKey)entry.getKey()).getPropertyName().equals(propertyName)) { potentialMatchingBinders.put(((PropertyNameKey)entry.getKey()).getParentObjectType(), entry.getValue()); } } binder = (Binder) ClassUtils.getValueFromMapForClass(parentObjectType, potentialMatchingBinders); if (binder != null) { // remember the lookup so it doesn't have to be discovered again registerBinderForPropertyName(parentObjectType, propertyName, binder); } } return binder; } /** * Try to find a binder for the provided propertyType. If no direct match found, * try to find binder for closest superclass of the given control type. */ protected Binder findBinderByPropertyType(Class propertyType) { return (Binder)ClassUtils.getValueFromMapForClass(propertyType, propertyTypeBinders); } /** * Try to find a binder for the provided controlType. If no direct match found, * try to find binder for closest superclass of the given control type. */ protected Binder findBinderByControlType(Class controlType) { return (Binder)ClassUtils.getValueFromMapForClass(controlType, controlTypeBinders); } @Override public void registerBinderForPropertyName(Class parentObjectType, String propertyName, Binder binder) { propertyNameBinders.put(new PropertyNameKey(parentObjectType, propertyName), binder); } /** * Add a list of binders that are bound to propertyNames. Each element in the list should * be a Properties element describing the binder and propertyName. For more information * about the structure of the properties see {@link #setBinderForPropertyName(Properties)}. * <br><br> * <list><br> * <props><br> * <prop key="...">...</prop><br> * <!-- More info in docs of setBinderForPropertyName(Properties)--><br> * </props><br> * </list><br> * * @param binders List of <code>Properties</code> elements * @see #setBinderForPropertyName(Properties) */ public void setBindersForPropertyNames(List binders) { bindersForPropertyNames = binders; } /** * Create/link a <code>Binder</code> to a propertyName from the given <code>Properties</code>. * <p> * The used keys are: * <ul> * <li><b>objectClass</b>: The bean which has the property. * <li><b>propertyName</b>: The property that will need the binder. * <li><b>binder</b>: The Fully Qualified ClassName that will be used to instantiate the <code>Binder</code>. * <li><b>binderRef</b>: The beanId that identifies the <code>Binder</code> which is defined elsewhere. * </ul> * <p> * The first two keys are mandatory in combination with one of the two latter (binder or binderRef) * The following two cases can be used to define a binder/propertyName combination: * <br><br> * <props><br> * <prop key="objectClass">mypackage.MyBean</prop><br> * <prop key="propertyName">myProperty</prop><br> * <prop key="binder">mypackage.MyBinder</prop><br> * </props><br> * <br> * <props><br> * <prop key="objectClass">mypackage.MyBean</prop><br> * <prop key="propertyName">myProperty</prop><br> * <prop key="binderRef">myBinderBeanId</prop><br> * <!-- myBinderBeanId identifies a bean defined elsewhere--><br> * </props><br> * * @param binder The <code>Properties</code> object containing the correct keys. */ public void setBinderForPropertyName(Properties binder) { String objectClassName = (String) binder.get("objectClass"); if (objectClassName == null) throw new IllegalArgumentException("objectClass is required"); classEditor.setAsText(objectClassName); Class objectClass = (Class) classEditor.getValue(); String propertyName = (String) binder.get("propertyName"); if (propertyName == null) throw new IllegalArgumentException("propertyName is required"); if (binder.containsKey("binder")) { Object binderParameter = binder.get("binder"); classEditor.setAsText((String) binderParameter); Class binderClass = (Class) classEditor.getValue(); try { registerBinderForPropertyName(objectClass, propertyName, (Binder) binderClass.newInstance()); } catch (Exception e) { throw new IllegalArgumentException( "Could not instantiate new binder with default constructor: " + binderParameter); } } else if (binder.containsKey("binderRef")) { String binderID = (String) binder.get("binderRef"); Binder binderBean = (Binder) getApplicationContext().getBean(binderID); registerBinderForPropertyName(objectClass, propertyName, binderBean); } else throw new IllegalArgumentException("binder or binderRef is required"); } @Override public void registerBinderForPropertyType(Class propertyType, Binder binder) { propertyTypeBinders.put(propertyType, binder); } /** * Registers property type binders by extracting the key and value from each entry * in the provided map using the key to specify the property type and the value * to specify the binder. * * <p>Binders specified in the provided map will override any binders previously * registered for the same property type. * @param binders the map containing the entries to register; keys must be of type * <code>Class</code> and values of type <code>Binder</code>. */ public void setBindersForPropertyTypes(Map binders) { for (Iterator i = binders.entrySet().iterator(); i.hasNext();) { Map.Entry entry = (Map.Entry)i.next(); registerBinderForPropertyType((Class)entry.getKey(), (Binder)entry.getValue()); } } @Override public void registerBinderForControlType(Class controlType, Binder binder) { controlTypeBinders.put(controlType, binder); } /** * Registers control type binders by extracting the key and value from each entry * in the provided map using the key to specify the property type and the value * to specify the binder. * * <p>Binders specified in the provided map will override any binders previously * registered for the same control type. * @param binders the map containing the entries to register; keys must be of type * <code>Class</code> and values of type <code>Binder</code>. */ public void setBindersForControlTypes(Map binders) { for (Iterator i = binders.entrySet().iterator(); i.hasNext();) { Map.Entry entry = (Map.Entry)i.next(); registerBinderForControlType((Class)entry.getKey(), (Binder)entry.getValue()); } } protected Class getPropertyType(FormModel formModel, String formPropertyPath) { return formModel.getFieldMetadata(formPropertyPath).getPropertyType(); } private static class PropertyNameKey { private final Class parentObjectType; private final String propertyName; public PropertyNameKey(Class parentObjectType, String propertyName) { Assert.notNull(parentObjectType, "parentObjectType must not be null."); Assert.notNull(propertyName, "propertyName must not be null."); this.parentObjectType = parentObjectType; this.propertyName = propertyName; } public String getPropertyName() { return propertyName; } public Class getParentObjectType() { return parentObjectType; } public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof PropertyNameKey)) { return false; } final PropertyNameKey propertyNameKey = (PropertyNameKey)o; return propertyName.equals(propertyNameKey.propertyName) && parentObjectType.equals(propertyNameKey.parentObjectType); } public int hashCode() { return (propertyName.hashCode() * 29) + parentObjectType.hashCode(); } } public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } protected ApplicationContext getApplicationContext() { return applicationContext; } @PostConstruct public void afterPropertiesSet() throws Exception { for (Iterator i = bindersForPropertyNames.iterator(); i.hasNext();) { setBinderForPropertyName((Properties) i.next()); } } }