001    /**
002     * Copyright 2005-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
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,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.uif.util;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.krad.uif.field.DataField;
020    import org.kuali.rice.krad.uif.view.View;
021    import org.springframework.beans.PropertyValues;
022    import org.springframework.beans.factory.config.TypedStringValue;
023    
024    import java.util.Collection;
025    import java.util.Map;
026    
027    /**
028     * Provides methods for getting property values, types, and paths within the
029     * context of a <code>View</code>
030     *
031     * <p>
032     * The view provides a special map named 'abstractTypeClasses' that indicates
033     * concrete classes that should be used in place of abstract property types that
034     * are encountered on the object graph. This classes takes into account that map
035     * while dealing with properties. e.g. suppose we have propertyPath
036     * 'document.name' on the form, with the type of the document property set to
037     * the interface Document. Using class introspection we would get back the
038     * interface type for document and this would not be able to get the property
039     * type for name. Using the view map, we can replace document with a concrete
040     * class and then use it to get the name property
041     * </p>
042     *
043     * @author Kuali Rice Team (rice.collab@kuali.org)
044     */
045    public class ViewModelUtils {
046    
047        /**
048         * Determines the associated type for the property within the View context
049         *
050         * <p>
051         * Property path is full path to property from the View Form class. The abstract type classes
052         * map configured on the View will be consulted for any entries that match the property path. If the
053         * property path given contains a partial match to an abstract class (somewhere on path is an abstract
054         * class), the property type will be retrieved based on the given concrete class to use and the part
055         * of the path remaining. If no matching entry is found, standard reflection is used to get the type
056         * </p>
057         *
058         * @param view - view instance providing the context (abstract map)
059         * @param propertyPath - full path to property to retrieve type for (relative to the form class)
060         * @return Class<?> type of property in model, or Null if type could not be determined
061         * @see org.kuali.rice.krad.uif.view.View#getObjectPathToConcreteClassMapping()
062         */
063        public static Class<?> getPropertyTypeByClassAndView(View view, String propertyPath) {
064            Class<?> propertyType = null;
065    
066            if (StringUtils.isBlank(propertyPath)) {
067                return propertyType;
068            }
069    
070            // in case of partial match, holds the class that matched and the
071            // property so we can get by reflection
072            Class<?> modelClass = view.getFormClass();
073            String modelProperty = propertyPath;
074    
075            int bestMatchLength = 0;
076    
077            // removed collection indexes from path for matching
078            String flattenedPropertyPath = propertyPath.replaceAll("\\[.+\\]", "");
079    
080            // check if property path matches one of the modelClass entries
081            Map<String, Class<?>> modelClasses = view.getObjectPathToConcreteClassMapping();
082            for (String path : modelClasses.keySet()) {
083                // full match
084                if (StringUtils.equals(path, flattenedPropertyPath)) {
085                    propertyType = modelClasses.get(path);
086                    break;
087                }
088    
089                // partial match
090                if (flattenedPropertyPath.startsWith(path) && (path.length() > bestMatchLength)) {
091                    bestMatchLength = path.length();
092    
093                    modelClass = modelClasses.get(path);
094                    modelProperty = StringUtils.removeStart(flattenedPropertyPath, path);
095                    modelProperty = StringUtils.removeStart(modelProperty, ".");
096                }
097            }
098    
099            // if full match not found, get type based on reflection
100            if (propertyType == null) {
101                propertyType = ObjectPropertyUtils.getPropertyType(modelClass, modelProperty);
102            }
103    
104            return propertyType;
105        }
106    
107        /**
108         * Gets the parent object path of the data field
109         *
110         * @param field
111         * @return String parent object path
112         */
113        public static String getParentObjectPath(DataField field) {
114            String parentObjectPath = "";
115    
116            String objectPath = field.getBindingInfo().getBindingObjectPath();
117            String propertyPrefix = field.getBindingInfo().getBindByNamePrefix();
118    
119            if (!field.getBindingInfo().isBindToForm() && StringUtils.isNotBlank(objectPath)) {
120                parentObjectPath = objectPath;
121            }
122    
123            if (StringUtils.isNotBlank(propertyPrefix)) {
124                if (StringUtils.isNotBlank(parentObjectPath)) {
125                    parentObjectPath += ".";
126                }
127    
128                parentObjectPath += propertyPrefix;
129            }
130    
131            return parentObjectPath;
132        }
133    
134        /**
135         * Determines the associated type for the property within the View context
136         *
137         * <p>
138         * The abstract type classes map configured on the View will be consulted for any entries that match
139         * the property path. If the parent object path for the given field contains a partial match to an
140         * abstract class (somewhere on path is an abstract class), the property type will be retrieved based
141         * on the given concrete class to use and the part of the path remaining. If no matching entry is found,
142         * standard reflection is used to get the type
143         * </p>
144         *
145         * @param view - view instance providing the context (abstract map)
146         * @param field - field to retrieve type for
147         * @return Class<?> type of property in model, or Null if type could not be determined
148         * @see org.kuali.rice.krad.uif.view.View#getObjectPathToConcreteClassMapping()
149         */
150        public static Class<?> getParentObjectClassForMetadata(View view, DataField field) {
151            String parentObjectPath = getParentObjectPath(field);
152    
153            return getPropertyTypeByClassAndView(view, parentObjectPath);
154        }
155    
156        /**
157         * Determines the associated type for the property within the View context
158         *
159         * <p>
160         * If the parent object instance is not null, get the class through it.  Otherwise, use the following logic:
161         * The abstract type classes map configured on the View will be consulted for any entries that match
162         * the property path. If the parent object path for the given field contains a partial match to an
163         * abstract class (somewhere on path is an abstract class), the property type will be retrieved based
164         * on the given concrete class to use and the part of the path remaining. If no matching entry is found,
165         * standard reflection is used to get the type
166         * </p>
167         *
168         * @param view - view instance providing the context (abstract map)
169         * @param model - object model
170         * @param field - field to retrieve type for
171         * @return Class<?> the class of the object instance if not null or the type of property in model, or Null if type could not be determined
172         * @see org.kuali.rice.krad.uif.view.View#getObjectPathToConcreteClassMapping()
173         */
174        public static Class<?> getParentObjectClassForMetadata(View view, Object model, DataField field) {
175            String parentObjectPath = getParentObjectPath(field);
176    
177            return getObjectClassForMetadata(view, model, parentObjectPath);
178        }
179    
180        /**
181         * Determines the associated type for the property within the View context
182         *
183         * <p>
184         * If the parent object instance is not null, get the class through it.  Otherwise, use the following logic:
185         * The abstract type classes map configured on the View will be consulted for any entries that match
186         * the property path. If the parent object path for the given field contains a partial match to an
187         * abstract class (somewhere on path is an abstract class), the property type will be retrieved based
188         * on the given concrete class to use and the part of the path remaining. If no matching entry is found,
189         * standard reflection is used to get the type
190         * </p>
191         *
192         * @param view - view instance providing the context (abstract map)
193         * @param model - object model
194         * @param propertyPath - full path to property to retrieve type for (relative to the form class)
195         * @return Class<?> the class of the object instance if not null or the type of property in model, or Null if type could not be determined
196         * @see org.kuali.rice.krad.uif.view.View#getObjectPathToConcreteClassMapping()
197         */
198        public static Class<?> getObjectClassForMetadata(View view, Object model, String propertyPath) {
199            // get class by object instance if not null
200            Object parentObject = ObjectPropertyUtils.getPropertyValue(model, propertyPath);
201            if (parentObject != null) {
202                return parentObject.getClass();
203            }
204    
205            // get class by property type with abstract map check
206            return getPropertyTypeByClassAndView(view, propertyPath);
207        }
208    
209        /**
210         * Retrieves the parent object if it exists or attempts to create a new instance
211         *
212         * @param view - view instance providing the context (abstract map)
213         * @param model - object model
214         * @param field - field to retrieve type for
215         * @return Class<?> the class of the object instance if not null or the type of property in model, or Null if type could not be determined
216         * @see org.kuali.rice.krad.uif.view.View#getObjectPathToConcreteClassMapping()
217         */
218        public static Object getParentObjectForMetadata(View view, Object model, DataField field) {
219            // default to model as parent
220            Object parentObject = model;
221    
222            String parentObjectPath = getParentObjectPath(field);
223            if (StringUtils.isNotBlank(parentObjectPath)) {
224                parentObject = ObjectPropertyUtils.getPropertyValue(model, parentObjectPath);
225    
226                // attempt to create new instance if parent is null or is a
227                // collection or map
228                if ((parentObject == null) || Collection.class.isAssignableFrom(parentObject.getClass()) ||
229                        Map.class.isAssignableFrom(parentObject.getClass())) {
230                    try {
231                        Class<?> parentObjectClass = getPropertyTypeByClassAndView(view, parentObjectPath);
232                        if (parentObjectClass != null) {
233                            parentObject = parentObjectClass.newInstance();
234                        }
235                    } catch (InstantiationException e) {
236                        // swallow exception and let null be returned
237                    } catch (IllegalAccessException e) {
238                        // swallow exception and let null be returned
239                    }
240                }
241            }
242    
243            return parentObject;
244        }
245    
246        /**
247         * Helper method for getting the string value of a property from a {@link PropertyValues}
248         *
249         * @param propertyValues - property values instance to pull from
250         * @param propertyName - name of property whose value should be retrieved
251         * @return String value for property or null if property was not found
252         */
253        public static String getStringValFromPVs(PropertyValues propertyValues, String propertyName) {
254            String propertyValue = null;
255    
256            if ((propertyValues != null) && propertyValues.contains(propertyName)) {
257                Object pvValue = propertyValues.getPropertyValue(propertyName).getValue();
258                if (pvValue instanceof TypedStringValue) {
259                    TypedStringValue typedStringValue = (TypedStringValue) pvValue;
260                    propertyValue = typedStringValue.getValue();
261                } else if (pvValue instanceof String) {
262                    propertyValue = (String) pvValue;
263                }
264            }
265    
266            return propertyValue;
267        }
268    }