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.widget;
17  
18  import java.security.GeneralSecurityException;
19  import java.util.HashMap;
20  import java.util.Map;
21  import java.util.Map.Entry;
22  import java.util.Properties;
23  
24  import org.apache.commons.lang.StringUtils;
25  import org.kuali.rice.core.api.CoreApiServiceLocator;
26  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
27  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
28  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
29  import org.kuali.rice.krad.service.ModuleService;
30  import org.kuali.rice.krad.uif.UifConstants;
31  import org.kuali.rice.krad.uif.UifParameters;
32  import org.kuali.rice.krad.uif.component.BindingInfo;
33  import org.kuali.rice.krad.uif.component.Component;
34  import org.kuali.rice.krad.uif.element.Action;
35  import org.kuali.rice.krad.uif.element.Link;
36  import org.kuali.rice.krad.uif.field.DataField;
37  import org.kuali.rice.krad.uif.field.InputField;
38  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
39  import org.kuali.rice.krad.uif.util.LifecycleElement;
40  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
41  import org.kuali.rice.krad.uif.util.ViewModelUtils;
42  import org.kuali.rice.krad.uif.view.View;
43  import org.kuali.rice.krad.util.KRADUtils;
44  import org.kuali.rice.krad.util.UrlFactory;
45  import org.kuali.rice.krad.web.form.InquiryForm;
46  
47  /**
48   * Widget for rendering an Inquiry link or DirectInquiry action field
49   *
50   * <p>
51   * The inquiry widget will render a button for the field value when
52   * that field is editable. When read only the widget will create a link on the display value.
53   * It points to the associated inquiry view for the field. The inquiry can be configured to point to a certain
54   * {@code InquiryView}, or the framework will attempt to associate the field with a inquiry based on
55   * its metadata (in particular its relationships in the model).
56   * </p>
57   *
58   * @author Kuali Rice Team (rice.collab@kuali.org)
59   */
60  @BeanTag(name = "inquiry-bean", parent = "Uif-Inquiry")
61  public class Inquiry extends WidgetBase {
62      private static final long serialVersionUID = -2154388007867302901L;
63      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(Inquiry.class);
64  
65      public static final String INQUIRY_TITLE_PREFIX = "title.inquiry.url.actiontext";
66      public static final String INQUIRY_TITLE_POSTFIX = "title.inquiry.url.value.prependtext";
67  
68      private String baseInquiryUrl;
69  
70      private String dataObjectClassName;
71      private String viewName;
72  
73      private Map<String, String> inquiryParameters;
74  
75      private Link inquiryLink;
76  
77      private Action directInquiryAction;
78      private boolean enableDirectInquiry;
79  
80      private boolean adjustInquiryParameters;
81      private BindingInfo fieldBindingInfo;
82  
83      private boolean parentReadOnly;
84  
85      public Inquiry() {
86          super();
87  
88          inquiryParameters = new HashMap<String, String>();
89      }
90  
91      /**
92       * {@inheritDoc}
93       */
94      @Override
95      public void performFinalize(Object model, LifecycleElement parent) {
96          super.performFinalize(model, parent);
97  
98          if (!isRender()) {
99              return;
100         }
101 
102         // set render to false until we find an inquiry class
103         setRender(false);
104 
105         // used to determine whether a normal or direct inquiry should be enabled
106         if (parent instanceof Component) {
107             setParentReadOnly(((Component) parent).isReadOnly());
108         }
109 
110         // Do checks for inquiry when read only
111         if (isParentReadOnly()) {
112             if (StringUtils.isBlank(((DataField) parent).getBindingInfo().getBindingPath()) || ((DataField) parent)
113                     .getBindingInfo().getBindingPath().equals("null")) {
114                 return;
115             }
116 
117             // check if field value is null, if so no inquiry
118             try {
119                 Object propertyValue = ObjectPropertyUtils.getPropertyValue(model,
120                         ((DataField) parent).getBindingInfo().getBindingPath());
121 
122                 if ((propertyValue == null) || StringUtils.isBlank(propertyValue.toString())) {
123                     return;
124                 }
125             } catch (Exception e) {
126                 // if we can't get the value just swallow the exception and don't set an inquiry
127                 return;
128             }
129 
130             View view = ViewLifecycle.getActiveLifecycle().getView();
131             // skips creating inquiry link if same as parent
132             if (view.getViewTypeName() == UifConstants.ViewType.INQUIRY) {
133                 DataField dataField = (DataField)parent;
134                 InquiryForm inquiryForm = (InquiryForm)model;
135 
136                 // value of field
137                 Object fieldValue = ObjectPropertyUtils.getPropertyValue(ViewModelUtils.getParentObjectForMetadata(
138                         view, model, dataField), dataField.getPropertyName());
139 
140                 // value of field in request parameter
141                 Object parameterValue = inquiryForm.getInitialRequestParameters().get(dataField.getPropertyName());
142 
143                 // if data classes and field values are equal
144                 if (inquiryForm.getDataObjectClassName().equals(dataField.getDictionaryObjectEntry())
145                         && parameterValue != null && fieldValue.equals(parameterValue))  {
146                     return ;
147                 }
148             }
149         }
150 
151         // Do checks for direct inquiry when editable
152         if (!isParentReadOnly() && parent instanceof InputField) {
153             if (!enableDirectInquiry) {
154                 return;
155             }
156 
157             // determine whether inquiry parameters will need adjusted
158             if (StringUtils.isBlank(getDataObjectClassName())
159                     || (getInquiryParameters() == null)
160                     || getInquiryParameters().isEmpty()) {
161                 // if inquiry parameters not given, they will not be adjusted by super
162                 adjustInquiryParameters = true;
163                 fieldBindingInfo = ((InputField) parent).getBindingInfo();
164             }
165         }
166 
167         setupLink(model, (DataField) parent);
168     }
169 
170     /**
171      * Get parent object and field name and build the inquiry link
172      *
173      * <p>
174      * This was moved from the performFinalize because overlapping and to be used
175      * by DirectInquiry.
176      * </p>
177      *
178      * @param model model
179      * @param field The parent Attribute field
180      */
181     private void setupLink(Object model, DataField field) {
182         String propertyName = field.getBindingInfo().getBindingName();
183 
184         // if class and parameters configured, build link from those
185         if (StringUtils.isNotBlank(getDataObjectClassName()) && (getInquiryParameters() != null) &&
186                 !getInquiryParameters().isEmpty()) {
187             Class<?> inquiryObjectClass;
188             try {
189                 inquiryObjectClass = Class.forName(getDataObjectClassName());
190             } catch (ClassNotFoundException e) {
191                 LOG.error("Unable to get class for: " + getDataObjectClassName());
192                 throw new RuntimeException(e);
193             }
194 
195             updateInquiryParameters(field.getBindingInfo());
196 
197             buildInquiryLink(model, propertyName, inquiryObjectClass, getInquiryParameters());
198         }
199         // get inquiry class and parameters from view helper
200         else {
201             // get parent object for inquiry metadata
202             ViewLifecycle viewLifecycle = ViewLifecycle.getActiveLifecycle();
203             Object parentObject = ViewModelUtils.getParentObjectForMetadata(viewLifecycle.getView(), model, field);
204             viewLifecycle.getHelper().buildInquiryLink(parentObject, propertyName, this);
205         }
206     }
207 
208     /**
209      * Adjusts the path on the inquiry parameter property to match the binding
210      * path prefix of the given {@code BindingInfo}
211      *
212      * @param bindingInfo binding info instance to copy binding path prefix from
213      */
214     public void updateInquiryParameters(BindingInfo bindingInfo) {
215         Map<String, String> adjustedInquiryParameters = new HashMap<String, String>();
216         for (Entry<String, String> stringEntry : inquiryParameters.entrySet()) {
217             String toField = stringEntry.getValue();
218             String adjustedFromFieldPath = bindingInfo.getPropertyAdjustedBindingPath(stringEntry.getKey());
219 
220             adjustedInquiryParameters.put(adjustedFromFieldPath, toField);
221         }
222 
223         this.inquiryParameters = adjustedInquiryParameters;
224     }
225 
226     /**
227      * Builds the inquiry link based on the given inquiry class and parameters
228      *
229      * @param dataObject parent object that contains the data (used to pull inquiry
230      * parameters)
231      * @param propertyName name of the property the inquiry is set on
232      * @param inquiryObjectClass class of the object the inquiry should point to
233      * @param inquiryParams map of key field mappings for the inquiry
234      */
235     @SuppressWarnings("deprecation")
236     public void buildInquiryLink(Object dataObject, String propertyName, Class<?> inquiryObjectClass,
237             Map<String, String> inquiryParams) {
238 
239         Properties urlParameters = new Properties();
240         Map<String,String> inquiryKeyValues = new HashMap<String, String>();
241 
242         urlParameters.setProperty(UifParameters.DATA_OBJECT_CLASS_NAME, inquiryObjectClass.getName());
243         urlParameters.setProperty(UifParameters.METHOD_TO_CALL, UifConstants.MethodToCallNames.START);
244         if (StringUtils.isNotBlank(this.viewName)) {
245             urlParameters.setProperty(UifParameters.VIEW_NAME, this.viewName);
246         }
247 
248         // add inquiry specific parms to url
249         if (getInquiryLink().getLightBox() != null) {
250             getInquiryLink().getLightBox().setAddAppParms(true);
251         }
252 
253         // configure inquiry when read only
254         if (isParentReadOnly()) {
255             for (Entry<String, String> inquiryParameter : inquiryParams.entrySet()) {
256                 String parameterName = inquiryParameter.getKey();
257 
258                 Object parameterValue = ObjectPropertyUtils.getPropertyValue(dataObject, parameterName);
259 
260                 // TODO: need general format util that uses spring
261                 if (parameterValue == null) {
262                     parameterValue = "";
263                 } else if (parameterValue instanceof java.sql.Date) {
264                     if (org.kuali.rice.core.web.format.Formatter.findFormatter(parameterValue.getClass()) != null) {
265                         org.kuali.rice.core.web.format.Formatter formatter =
266                                 org.kuali.rice.core.web.format.Formatter.getFormatter(parameterValue.getClass());
267                         parameterValue = formatter.format(parameterValue);
268                     }
269                 } else {
270                     parameterValue = parameterValue.toString();
271                 }
272 
273                 // Encrypt value if it is a field that has restriction that prevents a value from being shown to
274                 // user, because we don't want the browser history to store the restricted attributes value in the URL
275                 if (KRADServiceLocatorWeb.getDataObjectAuthorizationService()
276                         .attributeValueNeedsToBeEncryptedOnFormsAndLinks(inquiryObjectClass,
277                                 inquiryParameter.getValue())) {
278                     try {
279                         parameterValue = CoreApiServiceLocator.getEncryptionService().encrypt(parameterValue);
280                     } catch (GeneralSecurityException e) {
281                         throw new RuntimeException("Exception while trying to encrypted value for inquiry framework.",
282                                 e);
283                     }
284                 }
285 
286                 // add inquiry parameter to URL
287                 urlParameters.put(inquiryParameter.getValue(), parameterValue);
288 
289                 inquiryKeyValues.put(inquiryParameter.getValue(), parameterValue.toString());
290             }
291 
292             /* build inquiry URL */
293             String inquiryUrl;
294 
295             // check for EBOs for an alternate inquiry URL
296             ModuleService responsibleModuleService =
297                     KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(inquiryObjectClass);
298             if (responsibleModuleService != null && responsibleModuleService.isExternalizable(inquiryObjectClass)) {
299                 inquiryUrl = responsibleModuleService.getExternalizableDataObjectInquiryUrl(inquiryObjectClass,
300                         urlParameters);
301             } else {
302                 inquiryUrl = UrlFactory.parameterizeUrl(getBaseInquiryUrl(), urlParameters);
303             }
304 
305             getInquiryLink().setHref(inquiryUrl);
306 
307             // set inquiry title
308             getInquiryLink().setTitle(createTitleText(inquiryObjectClass, inquiryKeyValues));
309 
310             setRender(true);
311         }
312         // configure direct inquiry when editable
313         else {
314             // Direct inquiry
315             String inquiryUrl = UrlFactory.parameterizeUrl(getBaseInquiryUrl(), urlParameters);
316 
317             StringBuilder paramMapStringBuilder = new StringBuilder();
318 
319             // Build parameter string using the actual names of the fields as on the html page
320             for (Entry<String, String> inquiryParameter : inquiryParams.entrySet()) {
321                 String inquiryParameterFrom = inquiryParameter.getKey();
322 
323                 if (adjustInquiryParameters && (fieldBindingInfo != null)) {
324                     inquiryParameterFrom = fieldBindingInfo.getPropertyAdjustedBindingPath(inquiryParameterFrom);
325                 }
326 
327                 paramMapStringBuilder.append(inquiryParameterFrom);
328                 paramMapStringBuilder.append(":");
329                 paramMapStringBuilder.append(inquiryParameter.getValue());
330                 paramMapStringBuilder.append(",");
331             }
332             String paramMapString = StringUtils.removeEnd(paramMapStringBuilder.toString(), ",");
333 
334             // Check if lightbox is set. Get lightbox options.
335             String lightBoxOptions = "";
336             boolean lightBoxShow = (getInquiryLink().getLightBox() != null);
337             if (lightBoxShow) {
338                 lightBoxOptions = getInquiryLink().getLightBox().getTemplateOptionsJSString();
339             }
340 
341             // Create onlick script to open the inquiry window on the click event
342             // of the direct inquiry
343             StringBuilder onClickScript = new StringBuilder("showDirectInquiry(\"");
344             onClickScript.append(inquiryUrl);
345             onClickScript.append("\", \"");
346             onClickScript.append(paramMapString);
347             onClickScript.append("\", ");
348             onClickScript.append(lightBoxShow);
349             onClickScript.append(", ");
350             onClickScript.append(lightBoxOptions);
351             onClickScript.append(");");
352 
353             directInquiryAction.setPerformDirtyValidation(false);
354             directInquiryAction.setActionScript(onClickScript.toString());
355 
356             setRender(true);
357         }
358     }
359 
360     /**
361      * Gets text to prepend to the inquiry link title
362      *
363      * @param dataObjectClass data object class being inquired into
364      * @return inquiry link title
365      */
366     public String createTitleText(Class<?> dataObjectClass, Map<String,String> inquiryKeyValues) {
367         // use manually configured title if exists
368         if (StringUtils.isNotBlank(getTitle())) {
369             return getTitle();
370         }
371 
372         String titleText = "";
373 
374         String titlePrefix = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
375                 INQUIRY_TITLE_PREFIX);
376         if (StringUtils.isNotBlank(titlePrefix)) {
377             titleText += titlePrefix + " ";
378         }
379 
380         String objectLabel = KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getDataObjectEntry(
381                 dataObjectClass.getName()).getObjectLabel();
382         if (StringUtils.isNotBlank(objectLabel)) {
383             titleText += objectLabel + " ";
384         }
385 
386         if (StringUtils.isNotBlank(titleText)){
387             String titlePostfix = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
388                     INQUIRY_TITLE_POSTFIX);
389             if (StringUtils.isNotBlank(titlePostfix)) {
390                 titleText += titlePostfix + " ";
391             }
392         }
393 
394         return KRADUtils.buildAttributeTitleString(titleText, dataObjectClass, inquiryKeyValues);
395    }
396 
397     /**
398      * Returns the URL for the inquiry for which parameters will be added
399      *
400      * <p>
401      * The base URL includes the domain, context, and controller mapping for the inquiry invocation. Parameters are
402      * then added based on configuration to complete the URL. This is generally defaulted to the application URL and
403      * internal KRAD servlet mapping, but can be changed to invoke another application such as the Rice standalone
404      * server
405      * </p>
406      *
407      * @return inquiry base URL
408      */
409     @BeanTagAttribute(name = "baseInquiryUrl")
410     public String getBaseInquiryUrl() {
411         return this.baseInquiryUrl;
412     }
413 
414     /**
415      * Setter for the inquiry base url (domain, context, and controller)
416      *
417      * @param baseInquiryUrl
418      */
419     public void setBaseInquiryUrl(String baseInquiryUrl) {
420         this.baseInquiryUrl = baseInquiryUrl;
421     }
422 
423     /**
424      * Full class name the inquiry should be provided for
425      *
426      * <p>
427      * This is passed on to the inquiry request for the data object the lookup should be rendered for. This is then
428      * used by the inquiry framework to select the lookup view (if more than one inquiry view exists for the same
429      * data object class name, the {@link #getViewName()} property should be specified to select the view to render).
430      * </p>
431      *
432      * @return inquiry class name
433      */
434     @BeanTagAttribute(name = "dataObjectClassName")
435     public String getDataObjectClassName() {
436         return this.dataObjectClassName;
437     }
438 
439     /**
440      * Setter for the class name that inquiry should be provided for
441      *
442      * @param dataObjectClassName
443      */
444     public void setDataObjectClassName(String dataObjectClassName) {
445         this.dataObjectClassName = dataObjectClassName;
446     }
447 
448     /**
449      * When multiple target inquiry views exists for the same data object class, the view name can be set to
450      * determine which one to use
451      *
452      * <p>
453      * When creating multiple inquiry views for the same data object class, the view name can be specified for the
454      * different versions (for example 'simple' and 'advanced'). When multiple inquiry views exist the view name must
455      * be sent with the data object class for the request. Note the view id can be alternatively used to uniquely
456      * identify the inquiry view
457      * </p>
458      * @return view name
459      */
460     @BeanTagAttribute(name = "viewName")
461     public String getViewName() {
462         return this.viewName;
463     }
464 
465     /**
466      * Setter for the view name configured on the inquiry view that should be invoked by the inquiry widget
467      *
468      * @param viewName
469      */
470     public void setViewName(String viewName) {
471         this.viewName = viewName;
472     }
473 
474     /**
475      * Map that determines what properties from a calling view will be sent to properties on the inquiry data object
476      *
477      * <p>
478      * When invoking an inquiry view, a query is done against the inquiries configured data object and the resulting
479      * record is display. The values for the properties configured within the inquiry parameters Map will be
480      * pulled and passed along as values for the inquiry data object properties (thus they form the criteria for
481      * the inquiry)
482      * </p>
483      *
484      * @return mapping of calling view properties to inquiry data object properties
485      */
486     @BeanTagAttribute(name = "inquiryParameters", type = BeanTagAttribute.AttributeType.MAPVALUE)
487     public Map<String, String> getInquiryParameters() {
488         return this.inquiryParameters;
489     }
490 
491     /**
492      * Setter for the map that determines what property values on the calling view will be sent to properties on the
493      * inquiry data object
494      *
495      * @param inquiryParameters
496      */
497     public void setInquiryParameters(Map<String, String> inquiryParameters) {
498         this.inquiryParameters = inquiryParameters;
499     }
500 
501     /**
502      * {@code Link} that will be rendered for an inquiry
503      *
504      * @return the inquiry link
505      */
506     @BeanTagAttribute(name = "inquiryLink", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
507     public Link getInquiryLink() {
508         return this.inquiryLink;
509     }
510 
511     /**
512      * Setter for the inquiry {@code Link}
513      *
514      * @param inquiryLink the inquiry {@link Link} object
515      */
516     public void setInquiryLink(Link inquiryLink) {
517         this.inquiryLink = inquiryLink;
518     }
519 
520     /**
521      * {@code Action} that will be rendered next to the field for a direct inquiry
522      *
523      * @return the directInquiryAction
524      */
525     @BeanTagAttribute(name = "directInquiryAction", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
526     public Action getDirectInquiryAction() {
527         return this.directInquiryAction;
528     }
529 
530     /**
531      * Setter for the direct inquiry {@code Action}
532      *
533      * @param directInquiryAction the direct inquiry {@link Action}
534      */
535     public void setDirectInquiryAction(Action directInquiryAction) {
536         this.directInquiryAction = directInquiryAction;
537     }
538 
539     /**
540      * Indicates that the direct inquiry will not be rendered
541      *
542      * @return true if the direct inquiry should be rendered, false if not
543      */
544     @BeanTagAttribute(name = "enableDirectInquiry")
545     public boolean isEnableDirectInquiry() {
546         return enableDirectInquiry;
547     }
548 
549     /**
550      * Setter for the hideDirectInquiry flag
551      *
552      * @param enableDirectInquiry
553      */
554     public void setEnableDirectInquiry(boolean enableDirectInquiry) {
555         this.enableDirectInquiry = enableDirectInquiry;
556     }
557 
558     /**
559      * Determines whether a normal or direct inquiry should be enabled
560      *
561      * @return true if parent component is read only, false otherwise
562      */
563     protected boolean isParentReadOnly() {
564         return parentReadOnly;
565     }
566 
567     /**
568      * Determines whether a normal or direct inquiry should be enabled
569      *
570      * <p>
571      * Used by unit tests and internally
572      * </p>
573      *
574      * @param parentReadOnly true if parent component is read only, false otherwise
575      */
576     protected void setParentReadOnly(boolean parentReadOnly) {
577         this.parentReadOnly = parentReadOnly;
578     }
579 
580     /**
581      * Determines whether inquiry parameters adjusted
582      *
583      * @return true if adjusted
584      */
585     public boolean isAdjustInquiryParameters() {
586         return adjustInquiryParameters;
587     }
588 
589     /**
590      * Determines whether inquiry parameters adjusted
591      *
592      * <p>
593      * Used internally
594      * </p>
595      *
596      * @param adjustInquiryParameters
597      */
598     protected void setAdjustInquiryParameters(boolean adjustInquiryParameters) {
599         this.adjustInquiryParameters = adjustInquiryParameters;
600     }
601 
602     /**
603      * Sets the field binding information
604      *
605      * <p>
606      * Sets the field binding information
607      * </p>
608      *
609      * @param fieldBindingInfo
610      */
611     protected void setFieldBindingInfo(BindingInfo fieldBindingInfo) {
612         this.fieldBindingInfo = fieldBindingInfo;
613     }
614 }