001    /**
002     * Copyright 2005-2013 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     */
016    package org.kuali.rice.krad.uif.widget;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.core.api.CoreApiServiceLocator;
020    import org.kuali.rice.core.web.format.Formatter;
021    import org.kuali.rice.krad.datadictionary.parse.BeanTag;
022    import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
023    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
024    import org.kuali.rice.krad.service.ModuleService;
025    import org.kuali.rice.krad.uif.UifConstants;
026    import org.kuali.rice.krad.uif.UifParameters;
027    import org.kuali.rice.krad.uif.component.BindingInfo;
028    import org.kuali.rice.krad.uif.component.Component;
029    import org.kuali.rice.krad.uif.element.Action;
030    import org.kuali.rice.krad.uif.element.Link;
031    import org.kuali.rice.krad.uif.field.DataField;
032    import org.kuali.rice.krad.uif.field.InputField;
033    import org.kuali.rice.krad.uif.util.LookupInquiryUtils;
034    import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
035    import org.kuali.rice.krad.uif.util.ViewModelUtils;
036    import org.kuali.rice.krad.uif.view.View;
037    import org.kuali.rice.krad.util.UrlFactory;
038    
039    import java.security.GeneralSecurityException;
040    import java.util.HashMap;
041    import java.util.List;
042    import java.util.Map;
043    import java.util.Map.Entry;
044    import java.util.Properties;
045    
046    /**
047     * Widget for rendering an Inquiry link or DirectInquiry action field
048     *
049     * <p>
050     * The inquiry widget will render a button for the field value when
051     * that field is editable. When read only the widget will create a link on the display value.
052     * It points to the associated inquiry view for the field. The inquiry can be configured to point to a certain
053     * {@code InquiryView}, or the framework will attempt to associate the
054     * field with a inquiry based on its metadata (in particular its
055     * relationships in the model).
056     * </p>
057     *
058     * @author Kuali Rice Team (rice.collab@kuali.org)
059     */
060    @BeanTag(name = "inquiry-bean", parent = "Uif-Inquiry")
061    public class Inquiry extends WidgetBase {
062        private static final long serialVersionUID = -2154388007867302901L;
063        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(Inquiry.class);
064    
065        public static final String INQUIRY_TITLE_PREFIX = "title.inquiry.url.value.prependtext";
066    
067        private String baseInquiryUrl;
068    
069        private String dataObjectClassName;
070        private String viewName;
071    
072        private Map<String, String> inquiryParameters;
073    
074        private Link inquiryLink;
075    
076        private Action directInquiryAction;
077        private boolean enableDirectInquiry;
078    
079        private boolean adjustInquiryParameters;
080        private BindingInfo fieldBindingInfo;
081    
082        private boolean parentReadOnly;
083    
084        public Inquiry() {
085            super();
086    
087            inquiryParameters = new HashMap<String, String>();
088        }
089    
090        /**
091         * @see org.kuali.rice.krad.uif.widget.WidgetBase#performFinalize(org.kuali.rice.krad.uif.view.View,
092         *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
093         */
094        @Override
095        public void performFinalize(View view, Object model, Component parent) {
096            super.performFinalize(view, model, parent);
097    
098            if (!isRender()) {
099                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            setParentReadOnly(parent.isReadOnly());
107    
108            // Do checks for inquiry when read only
109            if (isParentReadOnly()) {
110                if (StringUtils.isBlank(((DataField) parent).getBindingInfo().getBindingPath())
111                        || ((DataField) parent).getBindingInfo().getBindingPath().equals("null")) {
112                    return;
113                }
114    
115                // check if field value is null, if so no inquiry
116                try {
117                    Object propertyValue = ObjectPropertyUtils.getPropertyValue(model,
118                            ((DataField) parent).getBindingInfo().getBindingPath());
119    
120                    if ((propertyValue == null) || StringUtils.isBlank(propertyValue.toString())) {
121                        return;
122                    }
123                } catch (Exception e) {
124                    // if we can't get the value just swallow the exception and don't set an inquiry
125                    return;
126                }
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            setupLink(view, model, (DataField) parent);
146        }
147    
148        /**
149         * Get parent object and field name and build the inquiry link
150         *
151         * <p>
152         * This was moved from the performFinalize because overlapping and to be used
153         * by DirectInquiry.
154         * </p>
155         *
156         * @param view Container View
157         * @param model model
158         * @param field The parent Attribute field
159         */
160        public void setupLink(View view, Object model, DataField field) {
161            String propertyName = field.getBindingInfo().getBindingName();
162    
163            // if class and parameters configured, build link from those
164            if (StringUtils.isNotBlank(getDataObjectClassName()) && (getInquiryParameters() != null) &&
165                    !getInquiryParameters().isEmpty()) {
166                Class<?> inquiryObjectClass;
167                try {
168                    inquiryObjectClass = Class.forName(getDataObjectClassName());
169                } catch (ClassNotFoundException e) {
170                    LOG.error("Unable to get class for: " + getDataObjectClassName());
171                    throw new RuntimeException(e);
172                }
173    
174                updateInquiryParameters(field.getBindingInfo());
175    
176                buildInquiryLink(model, propertyName, inquiryObjectClass, getInquiryParameters());
177            }
178            // get inquiry class and parameters from view helper
179            else {
180                // get parent object for inquiry metadata
181                Object parentObject = ViewModelUtils.getParentObjectForMetadata(view, model, field);
182                view.getViewHelperService().buildInquiryLink(parentObject, propertyName, this);
183            }
184        }
185    
186        /**
187         * Adjusts the path on the inquiry parameter property to match the binding
188         * path prefix of the given {@code BindingInfo}
189         *
190         * @param bindingInfo binding info instance to copy binding path prefix from
191         */
192        public void updateInquiryParameters(BindingInfo bindingInfo) {
193            Map<String, String> adjustedInquiryParameters = new HashMap<String, String>();
194            for (Entry<String, String> stringEntry : inquiryParameters.entrySet()) {
195                String toField = stringEntry.getValue();
196                String adjustedFromFieldPath = bindingInfo.getPropertyAdjustedBindingPath(stringEntry.getKey());
197    
198                adjustedInquiryParameters.put(adjustedFromFieldPath, toField);
199            }
200    
201            this.inquiryParameters = adjustedInquiryParameters;
202        }
203    
204        /**
205         * Builds the inquiry link based on the given inquiry class and parameters
206         *
207         * @param dataObject parent object that contains the data (used to pull inquiry
208         * parameters)
209         * @param propertyName name of the property the inquiry is set on
210         * @param inquiryObjectClass class of the object the inquiry should point to
211         * @param inquiryParams map of key field mappings for the inquiry
212         */
213        public void buildInquiryLink(Object dataObject, String propertyName, Class<?> inquiryObjectClass,
214                Map<String, String> inquiryParams) {
215    
216            Properties urlParameters = new Properties();
217    
218            urlParameters.setProperty(UifParameters.DATA_OBJECT_CLASS_NAME, inquiryObjectClass.getName());
219            urlParameters.setProperty(UifParameters.METHOD_TO_CALL, UifConstants.MethodToCallNames.START);
220            if(StringUtils.isNotBlank(this.viewName)){
221              urlParameters.setProperty(UifParameters.VIEW_NAME, this.viewName);
222            }
223            // add inquiry specific parms to url
224            if (getInquiryLink().getLightBox() != null) {
225                getInquiryLink().getLightBox().setAddAppParms(true);
226            }
227    
228            // configure inquiry when read only
229            if (isParentReadOnly()) {
230                for (Entry<String, String> inquiryParameter : inquiryParams.entrySet()) {
231                    String parameterName = inquiryParameter.getKey();
232    
233                    Object parameterValue = ObjectPropertyUtils.getPropertyValue(dataObject, parameterName);
234    
235                    // TODO: need general format util that uses spring
236                    if (parameterValue == null) {
237                        parameterValue = "";
238                    } else if (parameterValue instanceof java.sql.Date) {
239                        if (Formatter.findFormatter(parameterValue.getClass()) != null) {
240                            Formatter formatter = Formatter.getFormatter(parameterValue.getClass());
241                            parameterValue = formatter.format(parameterValue);
242                        }
243                    } else {
244                        parameterValue = parameterValue.toString();
245                    }
246    
247                    // Encrypt value if it is a field that has restriction that prevents a value from being shown to
248                    // user, because we don't want the browser history to store the restricted attributes value in the URL
249                    if (KRADServiceLocatorWeb.getDataObjectAuthorizationService()
250                            .attributeValueNeedsToBeEncryptedOnFormsAndLinks(inquiryObjectClass,
251                                    inquiryParameter.getValue())) {
252                        try {
253                            parameterValue = CoreApiServiceLocator.getEncryptionService().encrypt(parameterValue);
254                        } catch (GeneralSecurityException e) {
255                            throw new RuntimeException("Exception while trying to encrypted value for inquiry framework.",
256                                    e);
257                        }
258                    }
259    
260                    // add inquiry parameter to URL
261                    urlParameters.put(inquiryParameter.getValue(), parameterValue);
262                }
263    
264                /* build inquiry URL */
265                String inquiryUrl;
266    
267                // check for EBOs for an alternate inquiry URL
268                ModuleService responsibleModuleService =
269                        KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(inquiryObjectClass);
270                if (responsibleModuleService != null && responsibleModuleService.isExternalizable(inquiryObjectClass)) {
271                    inquiryUrl = responsibleModuleService.getExternalizableDataObjectInquiryUrl(inquiryObjectClass,
272                            urlParameters);
273                } else {
274                    inquiryUrl = UrlFactory.parameterizeUrl(getBaseInquiryUrl(), urlParameters);
275                }
276    
277                getInquiryLink().setHref(inquiryUrl);
278    
279                // set inquiry title
280                String linkTitle = createTitleText(inquiryObjectClass);
281                linkTitle = LookupInquiryUtils.getLinkTitleText(linkTitle, inquiryObjectClass, getInquiryParameters());
282                getInquiryLink().setTitle(linkTitle);
283    
284                setRender(true);
285            }
286            // configure direct inquiry when editable
287            else {
288                // Direct inquiry
289                String inquiryUrl = UrlFactory.parameterizeUrl(getBaseInquiryUrl(), urlParameters);
290    
291                StringBuilder paramMapString = new StringBuilder();
292    
293                // Build parameter string using the actual names of the fields as on the html page
294                for (Entry<String, String> inquiryParameter : inquiryParams.entrySet()) {
295                    String inquiryParameterFrom = inquiryParameter.getKey();
296    
297                    if (adjustInquiryParameters && (fieldBindingInfo != null)) {
298                        inquiryParameterFrom = fieldBindingInfo.getPropertyAdjustedBindingPath(inquiryParameterFrom);
299                    }
300    
301                    paramMapString.append(inquiryParameterFrom);
302                    paramMapString.append(":");
303                    paramMapString.append(inquiryParameter.getValue());
304                    paramMapString.append(",");
305                }
306                paramMapString.deleteCharAt(paramMapString.length() - 1);
307    
308                // Check if lightbox is set. Get lightbox options.
309                String lightBoxOptions = "";
310                boolean lightBoxShow = (getInquiryLink().getLightBox() != null);
311                if (lightBoxShow) {
312                    lightBoxOptions = getInquiryLink().getLightBox().getTemplateOptionsJSString();
313                }
314    
315                // Create onlick script to open the inquiry window on the click event
316                // of the direct inquiry
317                StringBuilder onClickScript = new StringBuilder("showDirectInquiry(\"");
318                onClickScript.append(inquiryUrl);
319                onClickScript.append("\", \"");
320                onClickScript.append(paramMapString);
321                onClickScript.append("\", ");
322                onClickScript.append(lightBoxShow);
323                onClickScript.append(", ");
324                onClickScript.append(lightBoxOptions);
325                onClickScript.append(");");
326    
327                directInquiryAction.setPerformDirtyValidation(false);
328                directInquiryAction.setActionScript(onClickScript.toString());
329    
330                setRender(true);
331            }
332        }
333    
334        /**
335         * Gets text to prepend to the inquiry link title
336         *
337         * @param dataObjectClass data object class being inquired into
338         * @return title prepend text
339         */
340        public String createTitleText(Class<?> dataObjectClass) {
341            String titleText = "";
342    
343            String titlePrefixProp = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
344                    INQUIRY_TITLE_PREFIX);
345            if (StringUtils.isNotBlank(titlePrefixProp)) {
346                titleText += titlePrefixProp + " ";
347            }
348    
349            String objectLabel = KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getDataObjectEntry(
350                    dataObjectClass.getName()).getObjectLabel();
351            if (StringUtils.isNotBlank(objectLabel)) {
352                titleText += objectLabel + " ";
353            }
354    
355            return titleText;
356        }
357    
358        /**
359         * @see org.kuali.rice.krad.uif.component.ComponentBase#getComponentsForLifecycle()
360         */
361        @Override
362        public List<Component> getComponentsForLifecycle() {
363            List<Component> components = super.getComponentsForLifecycle();
364    
365            components.add(getInquiryLink());
366            components.add(getDirectInquiryAction());
367    
368            return components;
369        }
370    
371        /**
372         * Returns the URL for the inquiry for which parameters will be added
373         *
374         * <p>
375         * The base URL includes the domain, context, and controller mapping for the inquiry invocation. Parameters are
376         * then added based on configuration to complete the URL. This is generally defaulted to the application URL and
377         * internal KRAD servlet mapping, but can be changed to invoke another application such as the Rice standalone
378         * server
379         * </p>
380         *
381         * @return inquiry base URL
382         */
383        @BeanTagAttribute(name="baseInquiryUrl")
384        public String getBaseInquiryUrl() {
385            return this.baseInquiryUrl;
386        }
387    
388        /**
389         * Setter for the inquiry base url (domain, context, and controller)
390         *
391         * @param baseInquiryUrl
392         */
393        public void setBaseInquiryUrl(String baseInquiryUrl) {
394            this.baseInquiryUrl = baseInquiryUrl;
395        }
396    
397        /**
398         * Full class name the inquiry should be provided for
399         *
400         * <p>
401         * This is passed on to the inquiry request for the data object the lookup should be rendered for. This is then
402         * used by the inquiry framework to select the lookup view (if more than one inquiry view exists for the same
403         * data object class name, the {@link #getViewName()} property should be specified to select the view to render).
404         * </p>
405         *
406         * @return inquiry class name
407         */
408        @BeanTagAttribute(name="dataObjectClassName")
409        public String getDataObjectClassName() {
410            return this.dataObjectClassName;
411        }
412    
413        /**
414         * Setter for the class name that inquiry should be provided for
415         *
416         * @param dataObjectClassName
417         */
418        public void setDataObjectClassName(String dataObjectClassName) {
419            this.dataObjectClassName = dataObjectClassName;
420        }
421    
422        /**
423         * When multiple target inquiry views exists for the same data object class, the view name can be set to
424         * determine which one to use
425         *
426         * <p>
427         * When creating multiple inquiry views for the same data object class, the view name can be specified for the
428         * different versions (for example 'simple' and 'advanced'). When multiple inquiry views exist the view name must
429         * be sent with the data object class for the request. Note the view id can be alternatively used to uniquely
430         * identify the inquiry view
431         * </p>
432         */
433        @BeanTagAttribute(name="viewName")
434        public String getViewName() {
435            return this.viewName;
436        }
437    
438        /**
439         * Setter for the view name configured on the inquiry view that should be invoked by the inquiry widget
440         *
441         * @param viewName
442         */
443        public void setViewName(String viewName) {
444            this.viewName = viewName;
445        }
446    
447        /**
448         * Map that determines what properties from a calling view will be sent to properties on the inquiry data object
449         *
450         * <p>
451         * When invoking an inquiry view, a query is done against the inquiries configured data object and the resulting
452         * record is display. The values for the properties configured within the inquiry parameters Map will be
453         * pulled and passed along as values for the inquiry data object properties (thus they form the criteria for
454         * the inquiry)
455         * </p>
456         *
457         * @return mapping of calling view properties to inquiry data object properties
458         */
459        @BeanTagAttribute(name="inquiryParameters",type= BeanTagAttribute.AttributeType.MAPVALUE)
460        public Map<String, String> getInquiryParameters() {
461            return this.inquiryParameters;
462        }
463    
464        /**
465         * Setter for the map that determines what property values on the calling view will be sent to properties on the
466         * inquiry data object
467         *
468         * @param inquiryParameters
469         */
470        public void setInquiryParameters(Map<String, String> inquiryParameters) {
471            this.inquiryParameters = inquiryParameters;
472        }
473    
474        /**
475         * {@code Link} that will be rendered for an inquiry
476         *
477         * @return the inquiry link
478         */
479        @BeanTagAttribute(name="inquiryLink",type= BeanTagAttribute.AttributeType.SINGLEBEAN)
480        public Link getInquiryLink() {
481            return this.inquiryLink;
482        }
483    
484        /**
485         * Setter for the inquiry {@code Link}
486         *
487         * @param inquiryLink the inquiry {@link Link} object
488         */
489        public void setInquiryLink(Link inquiryLink) {
490            this.inquiryLink = inquiryLink;
491        }
492    
493        /**
494         * {@code Action} that will be rendered next to the field for a direct inquiry
495         *
496         * @return the directInquiryAction
497         */
498        @BeanTagAttribute(name="directInquiryAction",type= BeanTagAttribute.AttributeType.SINGLEBEAN)
499        public Action getDirectInquiryAction() {
500            return this.directInquiryAction;
501        }
502    
503        /**
504         * Setter for the direct inquiry {@code Action}
505         *
506         * @param directInquiryAction the direct inquiry {@link Action}
507         */
508        public void setDirectInquiryAction(Action directInquiryAction) {
509            this.directInquiryAction = directInquiryAction;
510        }
511    
512        /**
513         * Indicates that the direct inquiry will not be rendered
514         *
515         * @return true if the direct inquiry should be rendered, false if not
516         */
517        @BeanTagAttribute(name="enableDirectInquiry")
518        public boolean isEnableDirectInquiry() {
519            return enableDirectInquiry;
520        }
521    
522        /**
523         * Setter for the hideDirectInquiry flag
524         *
525         * @param enableDirectInquiry
526         */
527        public void setEnableDirectInquiry(boolean enableDirectInquiry) {
528            this.enableDirectInquiry = enableDirectInquiry;
529        }
530    
531        /**
532         *  Determines whether a normal or direct inquiry should be enabled
533         *
534         * @return true if parent component is read only, false otherwise
535         */
536        protected boolean isParentReadOnly() {
537            return parentReadOnly;
538        }
539    
540        /**
541         * Determines whether a normal or direct inquiry should be enabled
542         *
543         * <p>
544         * Used by unit tests and internally
545         * </p>
546         *
547         * @param parentReadOnly true if parent component is read only, false otherwise
548         */
549        protected void setParentReadOnly(boolean parentReadOnly) {
550            this.parentReadOnly = parentReadOnly;
551        }
552    }