View Javadoc
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 }