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