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.web.bind;
17  
18  import org.apache.commons.lang.ArrayUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.apache.commons.lang3.reflect.FieldUtils;
21  import org.kuali.rice.krad.data.DataObjectService;
22  import org.kuali.rice.krad.data.DataObjectWrapper;
23  import org.kuali.rice.krad.data.KradDataServiceLocator;
24  import org.kuali.rice.krad.data.util.Link;
25  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
26  import org.kuali.rice.krad.uif.UifConstants.ViewType;
27  import org.kuali.rice.krad.uif.UifParameters;
28  import org.kuali.rice.krad.uif.service.ViewService;
29  import org.kuali.rice.krad.uif.view.View;
30  import org.kuali.rice.krad.util.KRADUtils;
31  import org.kuali.rice.krad.web.form.UifFormBase;
32  import org.springframework.core.annotation.AnnotationUtils;
33  import org.springframework.core.convert.ConversionService;
34  import org.springframework.util.Assert;
35  import org.springframework.validation.AbstractPropertyBindingResult;
36  import org.springframework.web.bind.ServletRequestDataBinder;
37  
38  import javax.servlet.ServletRequest;
39  import javax.servlet.http.HttpServletRequest;
40  import java.lang.reflect.Field;
41  import java.util.ArrayList;
42  import java.util.Collections;
43  import java.util.HashSet;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.Set;
47  
48  /**
49   * Override of ServletRequestDataBinder in order to hook in the UifBeanPropertyBindingResult
50   * which instantiates a custom BeanWrapperImpl, and to initialize the view.
51   *
52   * @author Kuali Rice Team (rice.collab@kuali.org)
53   */
54  public class UifServletRequestDataBinder extends ServletRequestDataBinder {
55  
56      protected static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
57              UifServletRequestDataBinder.class);
58  
59      private UifBeanPropertyBindingResult bindingResult;
60      private ConversionService conversionService;
61      private DataObjectService dataObjectService;
62      private boolean changeTracking = false;
63      private boolean autoLinking = true;
64  
65      public UifServletRequestDataBinder(Object target) {
66          super(target);
67          this.changeTracking = determineChangeTracking(target);
68          setBindingErrorProcessor(new UifBindingErrorProcessor());
69      }
70  
71      public UifServletRequestDataBinder(Object target, String name) {
72          super(target, name);
73          this.changeTracking = determineChangeTracking(target);
74          setBindingErrorProcessor(new UifBindingErrorProcessor());
75      }
76  
77      /**
78       * Return true if the target of this data binder has change tracking enabled.
79       */
80      private static boolean determineChangeTracking(Object target) {
81          ChangeTracking changeTracking = AnnotationUtils.findAnnotation(target.getClass(), ChangeTracking.class);
82          if (changeTracking != null && changeTracking.enabled()) {
83              return true;
84          }
85          return false;
86      }
87  
88      /**
89       * Allows for a custom binding result class.
90       *
91       * @see org.springframework.validation.DataBinder#initBeanPropertyAccess()
92       */
93      @Override
94      public void initBeanPropertyAccess() {
95          Assert.state(this.bindingResult == null,
96                  "DataBinder is already initialized - call initBeanPropertyAccess before other configuration methods");
97  
98          this.bindingResult = new UifBeanPropertyBindingResult(getTarget(), getObjectName(), isAutoGrowNestedPaths(),
99                  getAutoGrowCollectionLimit());
100         this.bindingResult.setChangeTracking(this.changeTracking);
101 
102         if (this.conversionService != null) {
103             this.bindingResult.initConversion(this.conversionService);
104         }
105 
106         if (this.dataObjectService == null) {
107             this.dataObjectService = KradDataServiceLocator.getDataObjectService();
108         }
109     }
110 
111     /**
112      * Allows for the setting attributes to use to find the data dictionary data from Kuali
113      *
114      * @see org.springframework.validation.DataBinder#getInternalBindingResult()
115      */
116     @Override
117     protected AbstractPropertyBindingResult getInternalBindingResult() {
118         if (this.bindingResult == null) {
119             initBeanPropertyAccess();
120         }
121 
122         return this.bindingResult;
123     }
124 
125     /**
126      * Disallows direct field access for Kuali
127      *
128      * @see org.springframework.validation.DataBinder#initDirectFieldAccess()
129      */
130     @Override
131     public void initDirectFieldAccess() {
132         LOG.error("Direct Field access is not allowed in UifServletRequestDataBinder.");
133         throw new RuntimeException("Direct Field access is not allowed in Kuali");
134     }
135 
136     /**
137      * Helper method to facilitate calling super.bind() from {@link #bind(ServletRequest)}.
138      */
139     private void _bind(ServletRequest request) {
140         super.bind(request);
141     }
142 
143     /**
144      * Calls {@link org.kuali.rice.krad.web.form.UifFormBase#preBind(HttpServletRequest)}, Performs data binding
145      * from servlet request parameters to the form, initializes view object, then calls
146      * {@link org.kuali.rice.krad.web.form.UifFormBase#postBind(javax.servlet.http.HttpServletRequest)}
147      *
148      * <p>
149      * The view is initialized by first looking for the {@code viewId} parameter in the request. If found, the view is
150      * retrieved based on this id. If the id is not present, then an attempt is made to find a view by type. In order
151      * to retrieve a view based on type, the view request parameter {@code viewTypeName} must be present. If all else
152      * fails and the viewId is populated on the form (could be populated from a previous request), this is used to
153      * retrieve the view.
154      * </p>
155      *
156      * @param request - HTTP Servlet Request instance
157      */
158     @Override
159     public void bind(ServletRequest request) {
160         UifFormBase form = (UifFormBase) UifServletRequestDataBinder.this.getTarget();
161 
162         form.preBind((HttpServletRequest) request);
163 
164         _bind(request);
165 
166         executeAutomaticLinking(request, form);
167         
168         if (!form.isUpdateNoneRequest()) {
169             // attempt to retrieve a view by unique identifier first, either as request attribute or parameter
170             String viewId = (String) request.getAttribute(UifParameters.VIEW_ID);
171             if (StringUtils.isBlank(viewId)) {
172                 viewId = request.getParameter(UifParameters.VIEW_ID);
173             }
174 
175             View view = null;
176             if (StringUtils.isNotBlank(viewId)) {
177                 view = getViewService().getViewById(viewId);
178             }
179 
180             // attempt to get view instance by type parameters
181             if (view == null) {
182                 view = getViewByType(request, form);
183             }
184 
185             // if view not found attempt to find one based on the cached form
186             if (view == null) {
187                 view = getViewFromPreviousModel(form);
188 
189                 if (view != null) {
190                     LOG.warn("Obtained viewId from cached form, this may not be safe!");
191                 }
192             }
193 
194             if (view != null) {
195                 form.setViewId(view.getId());
196 
197             } else {
198                 form.setViewId(null);
199             }
200 
201             form.setView(view);
202         }
203 
204         // invoke form callback for custom binding
205         form.postBind((HttpServletRequest) request);
206     }
207 
208     /**
209      * Performs automatic reference linking of the given form based on the properties on the form for which linking
210      * is enabled.
211      *
212      * <p>Linking will only be performed if change tracking and auto linking are enabled on this data binder.</p>
213      *
214      * @param request request instance
215      * @param form form instance against which to perform automatic linking
216      */
217     protected void executeAutomaticLinking(ServletRequest request, UifFormBase form) {
218         if (!changeTracking) {
219             LOG.info("Skip automatic linking because change tracking not enabled for this form.");
220             return;
221         }
222         if (!autoLinking) {
223             LOG.info("Skip automatic linking because it has been disabled for this form");
224             return;
225         }
226         Set<String> autoLinkingPaths = determineRootAutoLinkingPaths(form.getClass(), null, new HashSet<Class<?>>());
227         List<AutoLinkTarget> targets = extractAutoLinkTargets(autoLinkingPaths);
228         // perform linking for each target
229         for (AutoLinkTarget target : targets) {
230             if (!dataObjectService.supports(target.getTarget().getClass())) {
231                 LOG.warn("Encountered an auto linking target that is not a valid data object: "
232                         + target.getTarget().getClass());
233             } else {
234                 DataObjectWrapper<?> wrapped = dataObjectService.wrap(target.getTarget());
235                 wrapped.linkChanges(target.getModifiedPropertyPaths());
236             }
237         }
238     }
239 
240     /**
241      * Determines the root property paths relative to the given root object type against which to perform automatic
242      * linking.
243      *
244      * <p>This will be determined based on the presence of {@link Link} annotations on the given root object type.
245      * This method is invoked recursively as it walks the class structure looking for Link annotations. It uses the path
246      * and scanned arguments to keep track of how deep into the structure the scanning is and to prevent infinite
247      * recursion.</p>
248      *
249      * @param rootObjectType the root object type from which to perform the scan for auto-linking paths
250      * @param path the current property path relative to the original root object type at which the scan began, if null
251      *             then we are scanning from the root-most object type. Each recursive call of this method will append
252      *             a new property to this path
253      * @param scanned used to track classes that have already been scanned and prevent infinite recursion
254      *
255      * @return a set of property paths that should be auto linked
256      */
257     protected Set<String> determineRootAutoLinkingPaths(Class<?> rootObjectType, String path, Set<Class<?>> scanned) {
258         Set<String> autoLinkingPaths = new HashSet<String>();
259         if (scanned.contains(rootObjectType)) {
260             return autoLinkingPaths;
261         } else {
262             scanned.add(rootObjectType);
263         }
264         Link autoLink = AnnotationUtils.findAnnotation(rootObjectType, Link.class);
265         if (autoLink != null && autoLink.cascade()) {
266             autoLinkingPaths.addAll(assembleAutoLinkingPaths(path, autoLink));
267         } else if (autoLink == null) {
268             Field[] fields = FieldUtils.getAllFields(rootObjectType);
269             for (Field field : fields) {
270                 autoLink = field.getAnnotation(Link.class);
271                 if (autoLink != null) {
272                     if (autoLink.cascade()) {
273                         String fieldPath = appendToPath(path, field.getName());
274                         autoLinkingPaths.addAll(assembleAutoLinkingPaths(fieldPath, autoLink));
275                     }
276                 } else {
277                     autoLinkingPaths.addAll(determineRootAutoLinkingPaths(field.getType(), appendToPath(path,
278                             field.getName()), scanned));
279                 }
280             }
281         }
282         return autoLinkingPaths;
283     }
284 
285     /**
286      * A helper method which simply assembles a set of property paths for the given {@link Link} annotation which should
287      * be auto linked.
288      *
289      * @param path the property path from the top-most root class to where the Link annotation was found during the scan
290      * @param autoLink the Link annotation which is being processed
291      *
292      * @return a Set of auto linking paths based on the given path parameter, plus the path(s) defined on the
293      * {@link Link} annotation
294      */
295     protected Set<String> assembleAutoLinkingPaths(String path, Link autoLink) {
296         Set<String> autoLinkingPaths = new HashSet<String>();
297         if (ArrayUtils.isEmpty(autoLink.path())) {
298             autoLinkingPaths.add(path);
299         } else {
300             for (String autoLinkingPath : autoLink.path()) {
301                 autoLinkingPaths.add(appendToPath(path, autoLinkingPath));
302             }
303         }
304         return autoLinkingPaths;
305     }
306 
307     /**
308      * Uses the binding result on this data binder to determine the targets on the form that automatic linking should be
309      * performed against.
310      *
311      * <p>Only those property paths for which auto linking is enabled and which were actually modified during the
312      * execution of this data binding will be returned from this method.</p>
313      *
314      * @param autoLinkingPaths a set of paths relative to the form class for which auto-linking has been enabled
315      *
316      * @return a list of {@link AutoLinkTarget} objects which contain an object to be linked and which properties on
317      * that object were modified during this data binding execution
318      */
319     protected List<AutoLinkTarget> extractAutoLinkTargets(Set<String> autoLinkingPaths) {
320         List<AutoLinkTarget> targets = new ArrayList<AutoLinkTarget>();
321         for (String autoLinkingPath : autoLinkingPaths) {
322             Set<String> modifiedAutoLinkingPaths = new HashSet<String>();
323             Set<String> modifiedPaths = ((UifBeanPropertyBindingResult)getInternalBindingResult()).getModifiedPaths();
324             for (String modifiedPath : modifiedPaths) {
325                 if (modifiedPath.startsWith(autoLinkingPath)) {
326                     modifiedAutoLinkingPaths.add(modifiedPath.substring(autoLinkingPath.length() + 1));
327                 }
328             }
329             Object targetObject = getInternalBindingResult().getPropertyAccessor().getPropertyValue(autoLinkingPath);
330             if (targetObject != null) {
331                 targets.add(new AutoLinkTarget(targetObject, modifiedAutoLinkingPaths));
332             }
333         }
334         return targets;
335     }
336 
337     /**
338      * A utility method which appends two property paths together to create a new nested property path.
339      *
340      * <p>Handles null values for either the path or pathElement. The general output will be path.pathElement
341      * except in situations where either of the two given values are empty or null, in which case only the non-null
342      * value will be returned.</p>
343      *
344      * @param path the prefix of the property path
345      * @param pathElement the suffix of the property path to append to the given path
346      *
347      * @return an appended path, appended with a "." between the given path and pathElement (unless one of these is null)
348      */
349     private String appendToPath(String path, String pathElement) {
350         if (StringUtils.isEmpty(path)) {
351             return pathElement;
352         } else if (StringUtils.isEmpty(pathElement)) {
353             return path;
354         }
355         return path + "." + pathElement;
356     }
357 
358     /**
359      * Attempts to get a view instance by looking for a view type name in the request or the form and querying
360      * that view type with the request parameters
361      *
362      * @param request request instance to pull parameters from
363      * @param form form instance to pull values from
364      * @return View instance if found or null
365      */
366     protected View getViewByType(ServletRequest request, UifFormBase form) {
367         View view = null;
368 
369         String viewTypeName = request.getParameter(UifParameters.VIEW_TYPE_NAME);
370         ViewType viewType = StringUtils.isBlank(viewTypeName) ? form.getViewTypeName() : ViewType.valueOf(viewTypeName);
371 
372         if (viewType != null) {
373             Map<String, String> parameterMap = KRADUtils.translateRequestParameterMap(request.getParameterMap());
374             view = getViewService().getViewByType(viewType, parameterMap);
375         }
376 
377         return view;
378     }
379 
380     /**
381      * Attempts to get a view instance based on the view id stored on the form (which might not be populated
382      * from the request but remaining from session)
383      *
384      * @param form form instance to pull view id from
385      * @return View instance associated with form's view id or null if id or view not found
386      */
387     protected View getViewFromPreviousModel(UifFormBase form) {
388         // maybe we have a view id from the session form
389         if (form.getViewId() != null) {
390             return getViewService().getViewById(form.getViewId());
391         }
392 
393         return null;
394     }
395 
396     public boolean isChangeTracking() {
397         return changeTracking;
398     }
399 
400     public boolean isAutoLinking() {
401         return autoLinking;
402     }
403 
404     public void setAutoLinking(boolean autoLinking) {
405         this.autoLinking = autoLinking;
406     }
407 
408     public ViewService getViewService() {
409         return KRADServiceLocatorWeb.getViewService();
410     }
411 
412     public DataObjectService getDataObjectService() {
413         return this.dataObjectService;
414     }
415 
416     public void setDataObjectService(DataObjectService dataObjectService) {
417         this.dataObjectService = dataObjectService;
418     }
419 
420     /**
421      * Holds an object that will have auto-linking executed against it.
422      *
423      * <p>Also contains a set of property paths (relative to the object) that were modified during the data binding
424      * execution.</p>
425      */
426     private static final class AutoLinkTarget {
427 
428         private final Object target;
429         private final Set<String> modifiedPropertyPaths;
430 
431         AutoLinkTarget(Object target, Set<String> modifiedPropertyPaths) {
432             this.target = target;
433             this.modifiedPropertyPaths = modifiedPropertyPaths;
434         }
435 
436         Object getTarget() {
437             return target;
438         }
439 
440         Set<String> getModifiedPropertyPaths() {
441             return Collections.unmodifiableSet(modifiedPropertyPaths);
442         }
443 
444     }
445 
446 }
447 
448