001/** 002 * Copyright 2005-2016 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 org.apache.commons.lang.StringUtils; 019import org.kuali.rice.krad.uif.field.DataField; 020import org.kuali.rice.krad.uif.view.View; 021import org.springframework.beans.PropertyValues; 022import org.springframework.beans.factory.config.TypedStringValue; 023 024import java.util.Collection; 025import 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 */ 045public 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#getAbstractTypeClasses() 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.getAbstractTypeClasses(); 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 public static String getParentObjectPath(DataField field) { 108 String parentObjectPath = ""; 109 110 String objectPath = field.getBindingInfo().getBindingObjectPath(); 111 String propertyPrefix = field.getBindingInfo().getBindByNamePrefix(); 112 113 if (!field.getBindingInfo().isBindToForm() && StringUtils.isNotBlank(objectPath)) { 114 parentObjectPath = objectPath; 115 } 116 117 if (StringUtils.isNotBlank(propertyPrefix)) { 118 if (StringUtils.isNotBlank(parentObjectPath)) { 119 parentObjectPath += "."; 120 } 121 122 parentObjectPath += propertyPrefix; 123 } 124 125 return parentObjectPath; 126 } 127 128 public static Class<?> getParentObjectClassForMetadata(View view, DataField field) { 129 String parentObjectPath = getParentObjectPath(field); 130 131 return getPropertyTypeByClassAndView(view, parentObjectPath); 132 } 133 134 public static Class<?> getParentObjectClassForMetadata(View view, Object model, DataField field) { 135 String parentObjectPath = getParentObjectPath(field); 136 137 return getObjectClassForMetadata(view, model, parentObjectPath); 138 } 139 140 public static Class<?> getObjectClassForMetadata(View view, Object model, String propertyPath) { 141 // get class by object instance if not null 142 Object parentObject = ObjectPropertyUtils.getPropertyValue(model, propertyPath); 143 if (parentObject != null) { 144 return parentObject.getClass(); 145 } 146 147 // get class by property type with abstract map check 148 return getPropertyTypeByClassAndView(view, propertyPath); 149 } 150 151 public static Object getParentObjectForMetadata(View view, Object model, DataField field) { 152 // default to model as parent 153 Object parentObject = model; 154 155 String parentObjectPath = getParentObjectPath(field); 156 if (StringUtils.isNotBlank(parentObjectPath)) { 157 parentObject = ObjectPropertyUtils.getPropertyValue(model, parentObjectPath); 158 159 // attempt to create new instance if parent is null or is a 160 // collection or map 161 if ((parentObject == null) || Collection.class.isAssignableFrom(parentObject.getClass()) || 162 Map.class.isAssignableFrom(parentObject.getClass())) { 163 try { 164 Class<?> parentObjectClass = getPropertyTypeByClassAndView(view, parentObjectPath); 165 if (parentObjectClass != null) { 166 parentObject = parentObjectClass.newInstance(); 167 } 168 } catch (InstantiationException e) { 169 // swallow exception and let null be returned 170 } catch (IllegalAccessException e) { 171 // swallow exception and let null be returned 172 } 173 } 174 } 175 176 return parentObject; 177 } 178 179 /** 180 * Helper method for getting the string value of a property from a {@link PropertyValues} 181 * 182 * @param propertyValues - property values instance to pull from 183 * @param propertyName - name of property whose value should be retrieved 184 * @return String value for property or null if property was not found 185 */ 186 public static String getStringValFromPVs(PropertyValues propertyValues, String propertyName) { 187 String propertyValue = null; 188 189 if ((propertyValues != null) && propertyValues.contains(propertyName)) { 190 Object pvValue = propertyValues.getPropertyValue(propertyName).getValue(); 191 if (pvValue instanceof TypedStringValue) { 192 TypedStringValue typedStringValue = (TypedStringValue) pvValue; 193 propertyValue = typedStringValue.getValue(); 194 } else if (pvValue instanceof String) { 195 propertyValue = (String) pvValue; 196 } 197 } 198 199 return propertyValue; 200 } 201}