001/**
002 * Copyright 2005-2016 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 java.io.Serializable;
019import java.util.List;
020
021import org.apache.commons.lang.StringUtils;
022import org.kuali.rice.krad.datadictionary.parse.BeanTag;
023import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
024import org.kuali.rice.krad.uif.component.BindingInfo;
025import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
026import org.kuali.rice.krad.uif.field.AttributeQuery;
027import org.kuali.rice.krad.uif.field.InputField;
028import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
029import org.kuali.rice.krad.uif.util.LifecycleElement;
030import org.kuali.rice.krad.uif.util.ScriptUtils;
031import org.kuali.rice.krad.uif.view.View;
032
033/**
034 * Widget that provides dynamic select options to the user as they are entering the value (also known
035 * as auto-complete).
036 *
037 * <p>Widget is backed by an {@link org.kuali.rice.krad.uif.field.AttributeQuery} that provides the configuration
038 * for executing a query server side that will retrieve the valid option values.</p>
039 *
040 * @author Kuali Rice Team (rice.collab@kuali.org)
041 */
042@BeanTag(name = "suggest", parent = "Uif-Suggest")
043public class Suggest extends WidgetBase {
044    private static final long serialVersionUID = 7373706855319347225L;
045
046    private AttributeQuery suggestQuery;
047
048    private String valuePropertyName;
049    private String labelPropertyName;
050    private List<String> additionalPropertiesToReturn;
051
052    private boolean returnFullQueryObject;
053
054    private boolean retrieveAllSuggestions;
055    private List<Object> suggestOptions;
056
057    private String suggestOptionsJsString;
058
059    public Suggest() {
060        super();
061    }
062
063    /**
064     * The following updates are done here:
065     *
066     * <ul>
067     * <li>Invoke expression evaluation on the suggestQuery</li>
068     * </ul>
069     *
070     * {@inheritDoc}
071     */
072    public void performApplyModel(Object model, LifecycleElement parent) {
073        super.performApplyModel(model, parent);
074
075        if (suggestQuery != null) {
076            ViewLifecycle.getExpressionEvaluator().evaluateExpressionsOnConfigurable(ViewLifecycle.getView(),
077                    suggestQuery, getContext());
078        }
079    }
080
081    /**
082     * The following actions are performed:
083     *
084     * <ul>
085     * <li>Adjusts the query field mappings on the query based on the binding configuration of the field</li>
086     * <li>TODO: determine query if render is true and query is not set</li>
087     * </ul>
088     *
089     * {@inheritDoc}
090     */
091    @Override
092    public void performFinalize(Object model, LifecycleElement parent) {
093        super.performFinalize(model, parent);
094
095        // check for necessary configuration
096        if (!isSuggestConfigured()) {
097            setRender(false);
098        }
099
100        if (!isRender()) {
101            return;
102        }
103
104        if (retrieveAllSuggestions) {
105            if (suggestOptions == null || suggestOptions.isEmpty()) {
106                // execute query method to retrieve up front suggestions
107                if (suggestQuery.hasConfiguredMethod()) {
108                    retrieveSuggestOptions(ViewLifecycle.getView());
109                }
110            } else {
111                suggestOptionsJsString = ScriptUtils.translateValue(suggestOptions);
112            }
113        } else {
114            // adjust from side on query field mapping to match parent fields path
115            InputField field = (InputField) parent;
116
117            BindingInfo bindingInfo = field.getBindingInfo();
118            suggestQuery.updateQueryFieldMapping(bindingInfo);
119
120            if (suggestQuery != null) {
121                suggestQuery.defaultQueryTarget(ViewLifecycle.getHelper());
122            }
123        }
124    }
125
126    /**
127     * Indicates whether the suggest widget has the necessary configuration to render
128     *
129     * @return true if the necessary configuration is present, false if not
130     */
131    public boolean isSuggestConfigured() {
132        if (StringUtils.isNotBlank(valuePropertyName) || suggestQuery.hasConfiguredMethod() ||
133                (suggestOptions != null && !suggestOptions.isEmpty())) {
134            return true;
135        }
136
137        return false;
138    }
139
140    /**
141     * Invokes the configured query method and sets the returned method value as the suggest options or
142     * suggest options JS string
143     *
144     * @param view view instance the suggest belongs to, used to get the view helper service if needed
145     */
146    protected void retrieveSuggestOptions(View view) {
147        String queryMethodToCall = suggestQuery.getQueryMethodToCall();
148        MethodInvokerConfig queryMethodInvoker = suggestQuery.getQueryMethodInvokerConfig();
149
150        if (queryMethodInvoker == null) {
151            queryMethodInvoker = new MethodInvokerConfig();
152        }
153
154        // if method not set on invoker, use queryMethodToCall, note staticMethod could be set(don't know since
155        // there is not a getter), if so it will override the target method in prepare
156        if (StringUtils.isBlank(queryMethodInvoker.getTargetMethod())) {
157            queryMethodInvoker.setTargetMethod(queryMethodToCall);
158        }
159
160        // if target class or object not set, use view helper service
161        if ((queryMethodInvoker.getTargetClass() == null) && (queryMethodInvoker.getTargetObject() == null)) {
162            queryMethodInvoker.setTargetObject(view.getViewHelperService());
163        }
164
165        try {
166            queryMethodInvoker.prepare();
167
168            Object methodResult = queryMethodInvoker.invoke();
169            if (methodResult instanceof String) {
170                suggestOptionsJsString = (String) methodResult;
171            } else if (methodResult instanceof List) {
172                suggestOptions = (List<Object>) methodResult;
173                suggestOptionsJsString = ScriptUtils.translateValue(suggestOptions);
174            } else {
175                throw new RuntimeException("Suggest query method did not return List<String> for suggestions");
176            }
177        } catch (Exception e) {
178            throw new RuntimeException("Unable to invoke query method: " + queryMethodInvoker.getTargetMethod(), e);
179        }
180    }
181
182    /**
183     * Returns object containing post data to store for the suggest request.
184     *
185     * @return suggest post data instance
186     */
187    public SuggestPostData getPostData() {
188        return new SuggestPostData(this);
189    }
190
191    /**
192     * Attribute query instance the will be executed to provide
193     * the suggest options
194     *
195     * @return AttributeQuery
196     */
197    @BeanTagAttribute(type = BeanTagAttribute.AttributeType.DIRECTORBYTYPE)
198    public AttributeQuery getSuggestQuery() {
199        return suggestQuery;
200    }
201
202    /**
203     * Setter for the suggest attribute query
204     *
205     * @param suggestQuery
206     */
207    public void setSuggestQuery(AttributeQuery suggestQuery) {
208        this.suggestQuery = suggestQuery;
209    }
210
211    /**
212     * Name of the property on the query result object that provides
213     * the options for the suggest, values from this field will be
214     * collected and sent back on the result to provide as suggest options.
215     *
216     * <p>If a labelPropertyName is also set,
217     * the property specified by it will be used as the label the user selects (the suggestion), but the value will
218     * be the value retrieved by this property.  If only one of labelPropertyName or valuePropertyName is set,
219     * the property's value on the object will be used for both the value inserted on selection and the suggestion
220     * text (most default cases only a valuePropertyName would be set).</p>
221     *
222     * @return source property name
223     */
224    @BeanTagAttribute
225    public String getValuePropertyName() {
226        return valuePropertyName;
227    }
228
229    /**
230     * Setter for the value property name
231     *
232     * @param valuePropertyName
233     */
234    public void setValuePropertyName(String valuePropertyName) {
235        this.valuePropertyName = valuePropertyName;
236    }
237
238    /**
239     * Name of the property on the query result object that provides the label for the suggestion.
240     *
241     * <p>This should
242     * be set when the label that the user selects is different from the value that is inserted when a user selects a
243     * suggestion. If only one of labelPropertyName or valuePropertyName is set,
244     * the property's value on the object will be used for both the value inserted on selection and the suggestion
245     * text (most default cases only a valuePropertyName would be set).</p>
246     *
247     * @return labelPropertyName representing the property to use for the suggestion label of the item
248     */
249    @BeanTagAttribute
250    public String getLabelPropertyName() {
251        return labelPropertyName;
252    }
253
254    /**
255     * Set the labelPropertyName
256     *
257     * @param labelPropertyName
258     */
259    public void setLabelPropertyName(String labelPropertyName) {
260        this.labelPropertyName = labelPropertyName;
261    }
262
263    /**
264     * List of additional properties to return in the result objects to the plugin's success callback.
265     *
266     * <p>In most cases, this should not be set.  The main use case
267     * of setting this list is to use additional properties in the select function on the plugin's options, so
268     * it is only recommended that this property be set when doing heavy customization to the select function.
269     * This list is not used if the full result object is already being returned.</p>
270     *
271     * @return the list of additional properties to send back
272     */
273    @BeanTagAttribute
274    public List<String> getAdditionalPropertiesToReturn() {
275        return additionalPropertiesToReturn;
276    }
277
278    /**
279     * Set the list of additional properties to return to the plugin success callback results
280     *
281     * @param additionalPropertiesToReturn
282     */
283    public void setAdditionalPropertiesToReturn(List<String> additionalPropertiesToReturn) {
284        this.additionalPropertiesToReturn = additionalPropertiesToReturn;
285    }
286
287    /**
288     * When set to true the results of a query method will be sent back as-is (in translated form) with all properties
289     * intact.
290     *
291     * <p>
292     * Note this is not supported for highly complex objects (ie, most auto-query objects - will throw exception).
293     * Intended usage of this flag is with custom query methods which return simple data objects.
294     * The query method can return a list of Strings which will be used for the suggestions, a list of objects
295     * with 'label' and 'value' properties, or a custom object.  In the case of using a customObject
296     * labelPropertyName or valuePropertyName MUST be specified (or both) OR the custom object must contain a
297     * property named "label" or "value" (or both) for the suggestions to appear.  In cases where this is not used,
298     * the data sent back represents a slim subset of the properties on the object.
299     * </p>
300     *
301     * @return true if the query method results should be used as the suggestions, false to assume
302     * objects are returned and suggestions are formed using the source property name
303     */
304    @BeanTagAttribute
305    public boolean isReturnFullQueryObject() {
306        return returnFullQueryObject;
307    }
308
309    /**
310     * Setter for the for returning the full object of the query
311     *
312     * @param returnFullQueryObject
313     */
314    public void setReturnFullQueryObject(boolean returnFullQueryObject) {
315        this.returnFullQueryObject = returnFullQueryObject;
316    }
317
318    /**
319     * Indicates whether all suggest options should be retrieved up front and provide to the suggest
320     * widget as options locally
321     *
322     * <p>
323     * Use this for a small list of options to improve performance. The query will be performed on the client
324     * to filter the provider options based on the users input instead of doing a query each time
325     * </p>
326     *
327     * <p>
328     * When a query method is configured and this option set to true the method will be invoked to set the
329     * options. The query method should not take any arguments and should return the suggestion options
330     * List or the JS String as a result. If a query method is not configured the suggest options can be
331     * set through configuration or a view helper method (for example a component finalize method)
332     * </p>
333     *
334     * @return true to provide the suggest options initially, false to use ajax retrieval based on the
335     * user's input
336     */
337    @BeanTagAttribute
338    public boolean isRetrieveAllSuggestions() {
339        return retrieveAllSuggestions;
340    }
341
342    /**
343     * Setter for the retrieve all suggestions indicator
344     *
345     * @param retrieveAllSuggestions
346     */
347    public void setRetrieveAllSuggestions(boolean retrieveAllSuggestions) {
348        this.retrieveAllSuggestions = retrieveAllSuggestions;
349    }
350
351    /**
352     * When {@link #isRetrieveAllSuggestions()} is true, this list provides the full list of suggestions
353     *
354     * <p>
355     * If a query method is configured that method will be invoked to populate this list, otherwise the
356     * list should be populated through configuration or the view helper
357     * </p>
358     *
359     * <p>
360     * The suggest options can either be a list of Strings, in which case the strings will be the suggested
361     * values. Or a list of objects. If the object does not have 'label' and 'value' properties, a custom render
362     * and select method must be provided
363     * </p>
364     *
365     * @return list of suggest options
366     */
367    @BeanTagAttribute
368    public List<Object> getSuggestOptions() {
369        return suggestOptions;
370    }
371
372    /**
373     * Setter for the list of suggest options
374     *
375     * @param suggestOptions
376     */
377    public void setSuggestOptions(List<Object> suggestOptions) {
378        this.suggestOptions = suggestOptions;
379    }
380
381    /**
382     * Returns the suggest options as a JS String (set by the framework from method invocation)
383     *
384     * @return suggest options JS string
385     */
386    public String getSuggestOptionsJsString() {
387        if (StringUtils.isNotBlank(suggestOptionsJsString)) {
388            return this.suggestOptionsJsString;
389        }
390
391        return "null";
392    }
393
394    /**
395     * Sets suggest options javascript string
396     *
397     * @param suggestOptionsJsString
398     */
399    public void setSuggestOptionsJsString(String suggestOptionsJsString) {
400        this.suggestOptionsJsString = suggestOptionsJsString;
401    }
402
403    /**
404     * Holds post data for the suggest component.
405     */
406    public static class SuggestPostData implements Serializable {
407        private static final long serialVersionUID = 997780560864981128L;
408
409        private String id;
410
411        private AttributeQuery suggestQuery;
412
413        private String valuePropertyName;
414        private String labelPropertyName;
415        private List<String> additionalPropertiesToReturn;
416        private boolean returnFullQueryObject;
417        private boolean retrieveAllSuggestions;
418
419        /**
420         * Constructor taking suggest widget to pull post data from.
421         *
422         * @param suggest component instance to pull data
423         */
424        public SuggestPostData(Suggest suggest) {
425            this.id = suggest.getId();
426            this.suggestQuery = suggest.getSuggestQuery();
427            this.valuePropertyName = suggest.getValuePropertyName();
428            this.labelPropertyName = suggest.getLabelPropertyName();
429            this.additionalPropertiesToReturn = suggest.getAdditionalPropertiesToReturn();
430            this.returnFullQueryObject = suggest.isReturnFullQueryObject();
431            this.retrieveAllSuggestions = suggest.isRetrieveAllSuggestions();
432        }
433
434        /**
435         * @see org.kuali.rice.krad.uif.util.LifecycleElement#getId()
436         */
437        public String getId() {
438            return id;
439        }
440
441        /**
442         * @see Suggest#getSuggestQuery()
443         */
444        public AttributeQuery getSuggestQuery() {
445            return suggestQuery;
446        }
447
448        /**
449         * @see Suggest#getValuePropertyName()
450         */
451        public String getValuePropertyName() {
452            return valuePropertyName;
453        }
454
455        /**
456         * @see Suggest#getLabelPropertyName()
457         */
458        public String getLabelPropertyName() {
459            return labelPropertyName;
460        }
461
462        /**
463         * @see Suggest#getAdditionalPropertiesToReturn()
464         */
465        public List<String> getAdditionalPropertiesToReturn() {
466            return additionalPropertiesToReturn;
467        }
468
469        /**
470         * @see Suggest#isReturnFullQueryObject()
471         */
472        public boolean isReturnFullQueryObject() {
473            return returnFullQueryObject;
474        }
475
476        /**
477         * @see Suggest#isRetrieveAllSuggestions()
478         */
479        public boolean isRetrieveAllSuggestions() {
480            return retrieveAllSuggestions;
481        }
482    }
483}