1 /**
2 * Copyright 2005-2014 The Kuali Foundation
3 *
4 * Licensed under the Educational Community License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.opensource.org/licenses/ecl2.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.kuali.rice.krad.uif.util;
17
18 import org.apache.commons.lang.StringUtils;
19 import org.kuali.rice.krad.uif.field.DataField;
20 import org.kuali.rice.krad.uif.view.View;
21 import org.springframework.beans.PropertyValues;
22 import org.springframework.beans.factory.config.TypedStringValue;
23
24 import java.util.Collection;
25 import java.util.Map;
26
27 /**
28 * Provides methods for getting property values, types, and paths within the
29 * context of a <code>View</code>
30 *
31 * <p>
32 * The view provides a special map named 'abstractTypeClasses' that indicates
33 * concrete classes that should be used in place of abstract property types that
34 * are encountered on the object graph. This classes takes into account that map
35 * while dealing with properties. e.g. suppose we have propertyPath
36 * 'document.name' on the form, with the type of the document property set to
37 * the interface Document. Using class introspection we would get back the
38 * interface type for document and this would not be able to get the property
39 * type for name. Using the view map, we can replace document with a concrete
40 * class and then use it to get the name property
41 * </p>
42 *
43 * @author Kuali Rice Team (rice.collab@kuali.org)
44 */
45 public class ViewModelUtils {
46
47 /**
48 * Determines the associated type for the property within the View context
49 *
50 * <p>
51 * Property path is full path to property from the View Form class. The abstract type classes
52 * map configured on the View will be consulted for any entries that match the property path. If the
53 * property path given contains a partial match to an abstract class (somewhere on path is an abstract
54 * class), the property type will be retrieved based on the given concrete class to use and the part
55 * of the path remaining. If no matching entry is found, standard reflection is used to get the type
56 * </p>
57 *
58 * @param view view instance providing the context (abstract map)
59 * @param propertyPath full path to property to retrieve type for (relative to the form class)
60 * @return Class<?> type of property in model, or Null if type could not be determined
61 * @see org.kuali.rice.krad.uif.view.View#getObjectPathToConcreteClassMapping()
62 */
63 public static Class<?> getPropertyTypeByClassAndView(View view, String propertyPath) {
64 Class<?> propertyType = null;
65
66 if (StringUtils.isBlank(propertyPath)) {
67 return propertyType;
68 }
69
70 // in case of partial match, holds the class that matched and the
71 // property so we can get by reflection
72 Class<?> modelClass = view.getFormClass();
73 String modelProperty = propertyPath;
74
75 int bestMatchLength = 0;
76
77 // removed collection indexes from path for matching
78 String flattenedPropertyPath = propertyPath.replaceAll("\\[.+\\]", "");
79
80 // check if property path matches one of the modelClass entries
81 Map<String, Class<?>> modelClasses = view.getObjectPathToConcreteClassMapping();
82 for (String path : modelClasses.keySet()) {
83 // full match
84 if (StringUtils.equals(path, flattenedPropertyPath)) {
85 propertyType = modelClasses.get(path);
86 break;
87 }
88
89 // partial match
90 if (flattenedPropertyPath.startsWith(path) && (path.length() > bestMatchLength)) {
91 bestMatchLength = path.length();
92
93 modelClass = modelClasses.get(path);
94 modelProperty = StringUtils.removeStart(flattenedPropertyPath, path);
95 modelProperty = StringUtils.removeStart(modelProperty, ".");
96 }
97 }
98
99 // 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 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 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 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 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 }