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 java.util.Collection;
19 import java.util.Map;
20
21 import org.apache.commons.lang.StringUtils;
22 import org.kuali.rice.krad.uif.component.BindingInfo;
23 import org.kuali.rice.krad.uif.field.DataField;
24 import org.kuali.rice.krad.uif.view.View;
25 import org.springframework.beans.PropertyValues;
26 import org.springframework.beans.factory.config.TypedStringValue;
27
28 /**
29 * Provides methods for getting property values, types, and paths within the
30 * context of a <code>View</code>
31 *
32 * <p>
33 * The view provides a special map named 'abstractTypeClasses' that indicates
34 * concrete classes that should be used in place of abstract property types that
35 * are encountered on the object graph. This classes takes into account that map
36 * while dealing with properties. e.g. suppose we have propertyPath
37 * 'document.name' on the form, with the type of the document property set to
38 * the interface Document. Using class introspection we would get back the
39 * interface type for document and this would not be able to get the property
40 * type for name. Using the view map, we can replace document with a concrete
41 * class and then use it to get the name property
42 * </p>
43 *
44 * @author Kuali Rice Team (rice.collab@kuali.org)
45 */
46 public class ViewModelUtils {
47
48 /**
49 * Determines the associated type for the property within the View context
50 *
51 * <p>
52 * Property path is full path to property from the View Form class. The abstract type classes
53 * map configured on the View will be consulted for any entries that match the property path. If the
54 * property path given contains a partial match to an abstract class (somewhere on path is an abstract
55 * class), the property type will be retrieved based on the given concrete class to use and the part
56 * of the path remaining. If no matching entry is found, standard reflection is used to get the type
57 * </p>
58 *
59 * @param view view instance providing the context (abstract map)
60 * @param model the model
61 * @param propertyPath full path to property to retrieve type for (relative to the form class)
62 * @return Class<?> type of property in model, or Null if type could not be determined
63 * @see org.kuali.rice.krad.uif.view.View#getObjectPathToConcreteClassMapping()
64 */
65 public static Class<?> getPropertyTypeByClassAndView(View view, Object model, String propertyPath) {
66 if (StringUtils.isBlank(propertyPath)) {
67 return null;
68 }
69
70 Class<?> propertyType = null;
71
72 // in case of partial match, holds the class that matched and the
73 // property so we can get by reflection
74 Class<?> modelClass = null;
75 String modelProperty = propertyPath;
76
77 int bestMatchLength = 0;
78
79 // removed collection indexes from path for matching
80 String flattenedPropertyPath = propertyPath.replaceAll("\\[.+\\]", "");
81
82 // check if property path matches one of the modelClass entries
83 Map<String, Class<?>> modelClasses = view.getObjectPathToConcreteClassMapping();
84 for (String path : modelClasses.keySet()) {
85 // full match
86 if (StringUtils.equals(path, flattenedPropertyPath)) {
87 propertyType = modelClasses.get(path);
88 break;
89 }
90
91 // partial match
92 if (flattenedPropertyPath.startsWith(path) && (path.length() > bestMatchLength)) {
93 bestMatchLength = path.length();
94
95 modelClass = modelClasses.get(path);
96 modelProperty = StringUtils.removeStart(flattenedPropertyPath, path);
97 modelProperty = StringUtils.removeStart(modelProperty, ".");
98 }
99 }
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 }