1 /** 2 * Copyright 2005-2013 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 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 }