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