001/**
002 * Copyright 2005-2015 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.uif.widget;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.CoreApiServiceLocator;
020import org.kuali.rice.core.api.encryption.EncryptionService;
021import org.kuali.rice.krad.datadictionary.DataObjectEntry;
022import org.kuali.rice.krad.datadictionary.parse.BeanTag;
023import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
024import org.kuali.rice.krad.messages.MessageService;
025import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
026import org.kuali.rice.krad.service.ModuleService;
027import org.kuali.rice.krad.uif.UifConstants;
028import org.kuali.rice.krad.uif.UifParameters;
029import org.kuali.rice.krad.uif.component.BindingInfo;
030import org.kuali.rice.krad.uif.component.Component;
031import org.kuali.rice.krad.uif.element.Action;
032import org.kuali.rice.krad.uif.element.Link;
033import org.kuali.rice.krad.uif.field.DataField;
034import org.kuali.rice.krad.uif.field.InputField;
035import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
036import org.kuali.rice.krad.uif.util.LifecycleElement;
037import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
038import org.kuali.rice.krad.uif.util.ViewModelUtils;
039import org.kuali.rice.krad.util.KRADConstants;
040import org.kuali.rice.krad.util.KRADUtils;
041import org.kuali.rice.krad.util.UrlFactory;
042
043import java.security.GeneralSecurityException;
044import java.util.ArrayList;
045import java.util.HashMap;
046import java.util.List;
047import java.util.Map;
048import java.util.Map.Entry;
049import java.util.Properties;
050
051/**
052 * Widget for rendering an Inquiry link or DirectInquiry action field
053 *
054 * <p>
055 * The inquiry widget will render a button for the field value when
056 * that field is editable. When read only the widget will create a link on the display value.
057 * It points to the associated inquiry view for the field. The inquiry can be configured to point to a certain
058 * {@code InquiryView}, or the framework will attempt to associate the field with a inquiry based on
059 * its metadata (in particular its relationships in the model).
060 * </p>
061 *
062 * @author Kuali Rice Team (rice.collab@kuali.org)
063 */
064@BeanTag(name = "inquiry", parent = "Uif-Inquiry")
065public class Inquiry extends WidgetBase {
066    private static final long serialVersionUID = -2154388007867302901L;
067    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(Inquiry.class);
068
069    public static final String INQUIRY_TITLE_PREFIX = "title.inquiry.url.actiontext";
070    public static final String INQUIRY_TITLE_POSTFIX = "title.inquiry.url.value.prependtext";
071
072    private String baseInquiryUrl;
073
074    private String dataObjectClassName;
075    private String viewName;
076
077    private Map<String, String> inquiryParameters;
078
079    private Link inquiryLink;
080
081    private Action directInquiryAction;
082    private boolean enableDirectInquiry;
083
084    private boolean adjustInquiryParameters;
085    private BindingInfo fieldBindingInfo;
086
087    private boolean parentReadOnly;
088
089    public Inquiry() {
090        super();
091
092        inquiryParameters = new HashMap<String, String>();
093    }
094
095    /**
096     * Inherits readOnly from parent if not explicitly populated.
097     * 
098     * {@inheritDoc}
099     */
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}