001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.krad.uif.util;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.krad.uif.field.DataField;
020import org.kuali.rice.krad.uif.view.View;
021import org.springframework.beans.PropertyValues;
022import org.springframework.beans.factory.config.TypedStringValue;
023
024import java.util.Collection;
025import 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 */
045public 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#getAbstractTypeClasses()
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.getAbstractTypeClasses();
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    public static String getParentObjectPath(DataField field) {
108        String parentObjectPath = "";
109
110        String objectPath = field.getBindingInfo().getBindingObjectPath();
111        String propertyPrefix = field.getBindingInfo().getBindByNamePrefix();
112
113        if (!field.getBindingInfo().isBindToForm() && StringUtils.isNotBlank(objectPath)) {
114            parentObjectPath = objectPath;
115        }
116
117        if (StringUtils.isNotBlank(propertyPrefix)) {
118            if (StringUtils.isNotBlank(parentObjectPath)) {
119                parentObjectPath += ".";
120            }
121
122            parentObjectPath += propertyPrefix;
123        }
124
125        return parentObjectPath;
126    }
127
128    public static Class<?> getParentObjectClassForMetadata(View view, DataField field) {
129        String parentObjectPath = getParentObjectPath(field);
130
131        return getPropertyTypeByClassAndView(view, parentObjectPath);
132    }
133
134    public static Class<?> getParentObjectClassForMetadata(View view, Object model, DataField field) {
135        String parentObjectPath = getParentObjectPath(field);
136
137        return getObjectClassForMetadata(view, model, parentObjectPath);
138    }
139
140    public static Class<?> getObjectClassForMetadata(View view, Object model, String propertyPath) {
141        // get class by object instance if not null
142        Object parentObject = ObjectPropertyUtils.getPropertyValue(model, propertyPath);
143        if (parentObject != null) {
144            return parentObject.getClass();
145        }
146
147        // get class by property type with abstract map check
148        return getPropertyTypeByClassAndView(view, propertyPath);
149    }
150
151    public static Object getParentObjectForMetadata(View view, Object model, DataField field) {
152        // default to model as parent
153        Object parentObject = model;
154
155        String parentObjectPath = getParentObjectPath(field);
156        if (StringUtils.isNotBlank(parentObjectPath)) {
157            parentObject = ObjectPropertyUtils.getPropertyValue(model, parentObjectPath);
158
159            // attempt to create new instance if parent is null or is a
160            // collection or map
161            if ((parentObject == null) || Collection.class.isAssignableFrom(parentObject.getClass()) ||
162                    Map.class.isAssignableFrom(parentObject.getClass())) {
163                try {
164                    Class<?> parentObjectClass = getPropertyTypeByClassAndView(view, parentObjectPath);
165                    if (parentObjectClass != null) {
166                        parentObject = parentObjectClass.newInstance();
167                    }
168                } catch (InstantiationException e) {
169                    // swallow exception and let null be returned
170                } catch (IllegalAccessException e) {
171                    // swallow exception and let null be returned
172                }
173            }
174        }
175
176        return parentObject;
177    }
178
179    /**
180     * Helper method for getting the string value of a property from a {@link PropertyValues}
181     *
182     * @param propertyValues - property values instance to pull from
183     * @param propertyName - name of property whose value should be retrieved
184     * @return String value for property or null if property was not found
185     */
186    public static String getStringValFromPVs(PropertyValues propertyValues, String propertyName) {
187        String propertyValue = null;
188
189        if ((propertyValues != null) && propertyValues.contains(propertyName)) {
190            Object pvValue = propertyValues.getPropertyValue(propertyName).getValue();
191            if (pvValue instanceof TypedStringValue) {
192                TypedStringValue typedStringValue = (TypedStringValue) pvValue;
193                propertyValue = typedStringValue.getValue();
194            } else if (pvValue instanceof String) {
195                propertyValue = (String) pvValue;
196            }
197        }
198
199        return propertyValue;
200    }
201}