001 /** 002 * Copyright 2005-2012 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 */ 016 package org.kuali.rice.krad.uif.util; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.kuali.rice.krad.uif.field.DataField; 020 import org.kuali.rice.krad.uif.view.View; 021 import org.springframework.beans.PropertyValues; 022 import org.springframework.beans.factory.config.TypedStringValue; 023 024 import java.util.Collection; 025 import java.util.Map; 026 027 /** 028 * Provides methods for getting property values, types, and paths within the 029 * context of a <code>View</code> 030 * 031 * <p> 032 * The view provides a special map named 'abstractTypeClasses' that indicates 033 * concrete classes that should be used in place of abstract property types that 034 * are encountered on the object graph. This classes takes into account that map 035 * while dealing with properties. e.g. suppose we have propertyPath 036 * 'document.name' on the form, with the type of the document property set to 037 * the interface Document. Using class introspection we would get back the 038 * interface type for document and this would not be able to get the property 039 * type for name. Using the view map, we can replace document with a concrete 040 * class and then use it to get the name property 041 * </p> 042 * 043 * @author Kuali Rice Team (rice.collab@kuali.org) 044 */ 045 public class ViewModelUtils { 046 047 /** 048 * Determines the associated type for the property within the View context 049 * 050 * <p> 051 * Property path is full path to property from the View Form class. The abstract type classes 052 * map configured on the View will be consulted for any entries that match the property path. If the 053 * property path given contains a partial match to an abstract class (somewhere on path is an abstract 054 * class), the property type will be retrieved based on the given concrete class to use and the part 055 * of the path remaining. If no matching entry is found, standard reflection is used to get the type 056 * </p> 057 * 058 * @param view - view instance providing the context (abstract map) 059 * @param propertyPath - full path to property to retrieve type for (relative to the form class) 060 * @return Class<?> type of property in model, or Null if type could not be determined 061 * @see org.kuali.rice.krad.uif.view.View#getObjectPathToConcreteClassMapping() 062 */ 063 public static Class<?> getPropertyTypeByClassAndView(View view, String propertyPath) { 064 Class<?> propertyType = null; 065 066 if (StringUtils.isBlank(propertyPath)) { 067 return propertyType; 068 } 069 070 // in case of partial match, holds the class that matched and the 071 // property so we can get by reflection 072 Class<?> modelClass = view.getFormClass(); 073 String modelProperty = propertyPath; 074 075 int bestMatchLength = 0; 076 077 // removed collection indexes from path for matching 078 String flattenedPropertyPath = propertyPath.replaceAll("\\[.+\\]", ""); 079 080 // check if property path matches one of the modelClass entries 081 Map<String, Class<?>> modelClasses = view.getObjectPathToConcreteClassMapping(); 082 for (String path : modelClasses.keySet()) { 083 // full match 084 if (StringUtils.equals(path, flattenedPropertyPath)) { 085 propertyType = modelClasses.get(path); 086 break; 087 } 088 089 // partial match 090 if (flattenedPropertyPath.startsWith(path) && (path.length() > bestMatchLength)) { 091 bestMatchLength = path.length(); 092 093 modelClass = modelClasses.get(path); 094 modelProperty = StringUtils.removeStart(flattenedPropertyPath, path); 095 modelProperty = StringUtils.removeStart(modelProperty, "."); 096 } 097 } 098 099 // 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 }