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