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 }