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 org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.krad.uif.field.DataField;
20  import org.kuali.rice.krad.uif.view.View;
21  import org.springframework.beans.PropertyValues;
22  import org.springframework.beans.factory.config.TypedStringValue;
23  
24  import java.util.Collection;
25  import java.util.Map;
26  
27  /**
28   * Provides methods for getting property values, types, and paths within the
29   * context of a <code>View</code>
30   *
31   * <p>
32   * The view provides a special map named 'abstractTypeClasses' that indicates
33   * concrete classes that should be used in place of abstract property types that
34   * are encountered on the object graph. This classes takes into account that map
35   * while dealing with properties. e.g. suppose we have propertyPath
36   * 'document.name' on the form, with the type of the document property set to
37   * the interface Document. Using class introspection we would get back the
38   * interface type for document and this would not be able to get the property
39   * type for name. Using the view map, we can replace document with a concrete
40   * class and then use it to get the name property
41   * </p>
42   *
43   * @author Kuali Rice Team (rice.collab@kuali.org)
44   */
45  public class ViewModelUtils {
46  
47      /**
48       * Determines the associated type for the property within the View context
49       *
50       * <p>
51       * Property path is full path to property from the View Form class. The abstract type classes
52       * map configured on the View will be consulted for any entries that match the property path. If the
53       * property path given contains a partial match to an abstract class (somewhere on path is an abstract
54       * class), the property type will be retrieved based on the given concrete class to use and the part
55       * of the path remaining. If no matching entry is found, standard reflection is used to get the type
56       * </p>
57       *
58       * @param view view instance providing the context (abstract map)
59       * @param propertyPath full path to property to retrieve type for (relative to the form class)
60       * @return Class<?> type of property in model, or Null if type could not be determined
61       * @see org.kuali.rice.krad.uif.view.View#getObjectPathToConcreteClassMapping()
62       */
63      public static Class<?> getPropertyTypeByClassAndView(View view, String propertyPath) {
64          Class<?> propertyType = null;
65  
66          if (StringUtils.isBlank(propertyPath)) {
67              return propertyType;
68          }
69  
70          // in case of partial match, holds the class that matched and the
71          // property so we can get by reflection
72          Class<?> modelClass = view.getFormClass();
73          String modelProperty = propertyPath;
74  
75          int bestMatchLength = 0;
76  
77          // removed collection indexes from path for matching
78          String flattenedPropertyPath = propertyPath.replaceAll("\\[.+\\]", "");
79  
80          // check if property path matches one of the modelClass entries
81          Map<String, Class<?>> modelClasses = view.getObjectPathToConcreteClassMapping();
82          for (String path : modelClasses.keySet()) {
83              // full match
84              if (StringUtils.equals(path, flattenedPropertyPath)) {
85                  propertyType = modelClasses.get(path);
86                  break;
87              }
88  
89              // partial match
90              if (flattenedPropertyPath.startsWith(path) && (path.length() > bestMatchLength)) {
91                  bestMatchLength = path.length();
92  
93                  modelClass = modelClasses.get(path);
94                  modelProperty = StringUtils.removeStart(flattenedPropertyPath, path);
95                  modelProperty = StringUtils.removeStart(modelProperty, ".");
96              }
97          }
98  
99          // 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 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 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 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 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 }