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.field;
017
018import java.util.ArrayList;
019import java.util.List;
020
021import org.apache.commons.lang.StringUtils;
022import org.kuali.rice.core.api.uif.RemotableAttributeField;
023import org.kuali.rice.krad.datadictionary.parse.BeanTag;
024import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
025import org.kuali.rice.krad.uif.component.BindingInfo;
026import org.kuali.rice.krad.uif.component.ComponentBase;
027import org.kuali.rice.krad.uif.component.DataBinding;
028import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
029import org.kuali.rice.krad.uif.container.Container;
030import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
031import org.kuali.rice.krad.uif.util.ComponentFactory;
032import org.kuali.rice.krad.uif.util.CopyUtils;
033
034/**
035 * A placeholder in the configuration for a <code>Container</code> list of items that will be invoked to
036 * retrieve a list of {@link RemotableAttributeField} instances which will then be inserted into the containers
037 * list at the position of the holder
038 *
039 * <p>
040 * Since remotable fields are dynamic by nature, the individual fields cannot be configured initially with the
041 * container. Further more the properties for the field are constructed with code. This gives the ability to specify
042 * where that list of fields should be placed, along with configured on how to retrieve the remote fields.
043 * </p>
044 *
045 * <p>
046 * The fetching properties are used to configure what method to invoke that will return the list of remotable fields.
047 * Specifying the {@link #getFetchingMethodToCall()} only assumes the method is on the view helper service for the
048 * contained view. For invoking other classes, such as services or static classes, use {@link
049 * #getFetchingMethodInvoker()}
050 * </p>
051 *
052 * <p>
053 * The list of remotable fields should bind to a Map property on the model. The {@link #getPropertyName()} and
054 * {@link #getBindingInfo()} properties specify the path to this property. The property names configured on the
055 * returned fields are assumed to be keys in that above configured map, with the corresponding map value giving the
056 * actual model value for the remote field.
057 * </p>
058 *
059 * <p>
060 * e.g. configuration
061 * {@code
062 * <property name="items">
063 * <list>
064 * <bean parent="RemoteFieldsHolder" p:propertyName="remoteFieldValuesMap"
065 * p:fetchingMethodToCall="retrieveRemoteFields"/>
066 * ...
067 * }
068 *
069 * This example will invoke a method named 'retrieveRemoteFields' on the view helper service, which should return
070 * a list of {@link RemotableAttributeField} instances. The view, model instance, and parent container will be sent
071 * to the method as arguments.
072 *
073 * The returned fields will be translated to {@link InputField} instances that bind to a map property named
074 * 'remoteFieldValuesMap' on the model.
075 * </p>
076 *
077 * @author Kuali Rice Team (rice.collab@kuali.org)
078 */
079@BeanTag(name = "remotableFieldsPlaceholder", parent = "Uif-RemotableFieldsPlaceholderConfig")
080public class RemoteFieldsHolder extends ComponentBase implements DataBinding {
081    private static final long serialVersionUID = -8493923312021633727L;
082    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(RemoteFieldsHolder.class);
083
084    private String propertyName;
085    private BindingInfo bindingInfo;
086
087    private String fetchingMethodToCall;
088    private MethodInvokerConfig fetchingMethodInvoker;
089
090    public RemoteFieldsHolder() {
091        super();
092    }
093
094    /**
095     * Invokes the configured fetching method to retrieve a list of remotable fields, then invoked the
096     * {@code ComponentFactory} to translate the fields, and finally sets up the binding for the attribute fields
097     *
098     * @param parent container instance that holder is configured for, sent to the fetching method
099     * @return list of attribute fields that should be placed into container, if no remotable
100     *         fields were returned from the fetching method the list will be empty
101     */
102    public List<InputField> fetchAndTranslateRemoteFields(Container parent) {
103        if (StringUtils.isBlank(fetchingMethodToCall) && (fetchingMethodInvoker == null)) {
104            throw new RuntimeException("");
105        }
106
107        if (fetchingMethodInvoker == null) {
108            fetchingMethodInvoker = new MethodInvokerConfig();
109        }
110
111        // if method not set on invoker, use fetchingMethodToCall, note staticMethod could be set(don't know since
112        // there is not a getter), if so it will override the target method in prepare
113        if (StringUtils.isBlank(fetchingMethodInvoker.getTargetMethod())) {
114            fetchingMethodInvoker.setTargetMethod(fetchingMethodToCall);
115        }
116
117        // if target class or object not set, use view helper service
118        if ((fetchingMethodInvoker.getTargetClass() == null) && (fetchingMethodInvoker.getTargetObject() == null)) {
119            fetchingMethodInvoker.setTargetObject(ViewLifecycle.getHelper());
120        }
121
122        Object[] arguments = new Object[3];
123        arguments[0] = ViewLifecycle.getView();
124        arguments[1] = ViewLifecycle.getModel();
125        arguments[2] = parent;
126        fetchingMethodInvoker.setArguments(arguments);
127
128        // invoke method
129        List<RemotableAttributeField> remotableFields = null;
130        try {
131            LOG.debug("Invoking fetching method: " + fetchingMethodInvoker.getTargetMethod());
132            fetchingMethodInvoker.prepare();
133
134            remotableFields = (List<RemotableAttributeField>) fetchingMethodInvoker.invoke();
135
136        } catch (Exception e) {
137            LOG.error("Error invoking fetching method", e);
138            throw new RuntimeException("Error invoking fetching method", e);
139        }
140
141        // do translation
142        List<InputField> attributeFields = new ArrayList<InputField>();
143        if ((remotableFields != null) && !remotableFields.isEmpty()) {
144            attributeFields = ComponentFactory.translateRemotableFields(remotableFields);
145        }
146
147        // set binding info on the translated fields
148        if (bindingInfo == null) {
149            bindingInfo = new BindingInfo();
150        }
151
152        // property name should point to a Map that holds attribute name/value pairs
153        bindingInfo.addToBindByNamePrefix(propertyName);
154        bindingInfo.setBindToMap(true);
155
156        for (InputField field : attributeFields) {
157            BindingInfo fieldBindingInfo = CopyUtils.copy(bindingInfo);
158            fieldBindingInfo.setDefaults(ViewLifecycle.getView(), field.getPropertyName());
159            field.setBindingInfo(fieldBindingInfo);
160        }
161
162        return attributeFields;
163    }
164
165    @Override
166    public String getComponentTypeName() {
167        return "RemoteFieldsHolder";
168    }
169
170    /**
171     * Path to the Map property that the translated fields bind to
172     *
173     * <p>
174     * It is assumed this property points to a Map where the property names on the returned remotable fields
175     * are keys in that map, with the corresponding map value giving the model value for the field
176     * </p>
177     *
178     * @return path to property on model
179     */
180    @BeanTagAttribute
181    public String getPropertyName() {
182        return propertyName;
183    }
184
185    /**
186     * Setter for the property name that points to the binding Map
187     *
188     * @param propertyName
189     */
190    public void setPropertyName(String propertyName) {
191        this.propertyName = propertyName;
192    }
193
194    /**
195     * Can be used to for more complex binding paths
196     *
197     * <p>
198     * Generally not necessary to set on a field level, any default object path or binding prefixes set
199     * on the view or container will be inherited
200     * </p>
201     *
202     * @return BindingInfo instance containing binding information for the Map property
203     */
204    @BeanTagAttribute
205    public BindingInfo getBindingInfo() {
206        return bindingInfo;
207    }
208
209    /**
210     * Setter for the Map property binding info instance
211     *
212     * @param bindingInfo
213     */
214    public void setBindingInfo(BindingInfo bindingInfo) {
215        this.bindingInfo = bindingInfo;
216    }
217
218    /**
219     * Name of the method to invoke for retrieving the list of remotable fields
220     *
221     * <p>
222     * When only the fetching method to call is configured it is assumed to be a valid method on the view
223     * helper service for the containing view. The method name must accept the view, model object, and parent
224     * container as arguments, and return a list of {@link RemotableAttributeField} instances.
225     * </p>
226     *
227     * <p>
228     * For invoking the method on classes other than the view helper service, see {@link #getFetchingMethodInvoker()}
229     * </p>
230     *
231     * @return name of method to invoke for fetching remote fields
232     */
233    @BeanTagAttribute
234    public String getFetchingMethodToCall() {
235        return fetchingMethodToCall;
236    }
237
238    /**
239     * Setter for the fetching method to call
240     *
241     * @param fetchingMethodToCall
242     */
243    public void setFetchingMethodToCall(String fetchingMethodToCall) {
244        this.fetchingMethodToCall = fetchingMethodToCall;
245    }
246
247    /**
248     * Configuration for the method to invoke for retrieving the list of remotable fields
249     *
250     * <p>
251     * Through the method invoker config, a service or static class can be configured along with the
252     * method name that will be invoked. The method name must accept the view, model object, and parent
253     * container as arguments, and return a list of {@link RemotableAttributeField} instances.
254     * </p>
255     *
256     * <p>
257     * Note the {@link org.kuali.rice.krad.uif.component.MethodInvokerConfig#getTargetMethod()} property can
258     * be configured, or the {@link #getFetchingMethodToCall()}. In the case of both configurations, the target
259     * method on the method invoker config will be used
260     * </p>
261     *
262     * @return MethodInvokerConfig instance containing method configuration
263     */
264    @BeanTagAttribute(type = BeanTagAttribute.AttributeType.SINGLEBEAN)
265    public MethodInvokerConfig getFetchingMethodInvoker() {
266        return fetchingMethodInvoker;
267    }
268
269    /**
270     * Setter for the fetching method to invoke configuration
271     *
272     * @param fetchingMethodInvoker
273     */
274    public void setFetchingMethodInvoker(MethodInvokerConfig fetchingMethodInvoker) {
275        this.fetchingMethodInvoker = fetchingMethodInvoker;
276    }
277
278}