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.field;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.core.api.uif.RemotableAttributeField;
020    import org.kuali.rice.krad.datadictionary.parse.BeanTag;
021    import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
022    import org.kuali.rice.krad.uif.component.BindingInfo;
023    import org.kuali.rice.krad.uif.component.ComponentBase;
024    import org.kuali.rice.krad.uif.component.DataBinding;
025    import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
026    import org.kuali.rice.krad.uif.container.Container;
027    import org.kuali.rice.krad.uif.util.CloneUtils;
028    import org.kuali.rice.krad.uif.util.ComponentFactory;
029    import org.kuali.rice.krad.uif.view.View;
030    
031    import java.util.ArrayList;
032    import java.util.List;
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 = "remotableFieldsPlaceholderConfig-bean", parent = "Uif-RemotableFieldsPlaceholderConfig")
080    public 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 view view instance the container belongs to, sent to the fetching method
099         * @param model object containing the view data, sent to the fetching method
100         * @param parent container instance that holder is configured for, sent to the fetching method
101         * @return list of attribute fields that should be placed into container, if no remotable
102         *         fields were returned from the fetching method the list will be empty
103         */
104        public List<InputField> fetchAndTranslateRemoteFields(View view, Object model, Container parent) {
105            if (StringUtils.isBlank(fetchingMethodToCall) && (fetchingMethodInvoker == null)) {
106                throw new RuntimeException("");
107            }
108    
109            if (fetchingMethodInvoker == null) {
110                fetchingMethodInvoker = new MethodInvokerConfig();
111            }
112    
113            // if method not set on invoker, use fetchingMethodToCall, note staticMethod could be set(don't know since
114            // there is not a getter), if so it will override the target method in prepare
115            if (StringUtils.isBlank(fetchingMethodInvoker.getTargetMethod())) {
116                fetchingMethodInvoker.setTargetMethod(fetchingMethodToCall);
117            }
118    
119            // if target class or object not set, use view helper service
120            if ((fetchingMethodInvoker.getTargetClass() == null) && (fetchingMethodInvoker.getTargetObject() == null)) {
121                fetchingMethodInvoker.setTargetObject(view.getViewHelperService());
122            }
123    
124            Object[] arguments = new Object[3];
125            arguments[0] = view;
126            arguments[1] = model;
127            arguments[2] = parent;
128            fetchingMethodInvoker.setArguments(arguments);
129    
130            // invoke method
131            List<RemotableAttributeField> remotableFields = null;
132            try {
133                LOG.debug("Invoking fetching method: " + fetchingMethodInvoker.getTargetMethod());
134                fetchingMethodInvoker.prepare();
135    
136                remotableFields = (List<RemotableAttributeField>) fetchingMethodInvoker.invoke();
137    
138            } catch (Exception e) {
139                LOG.error("Error invoking fetching method", e);
140                throw new RuntimeException("Error invoking fetching method", e);
141            }
142    
143            // do translation
144            List<InputField> attributeFields = new ArrayList<InputField>();
145            if ((remotableFields != null) && !remotableFields.isEmpty()) {
146                attributeFields = ComponentFactory.translateRemotableFields(remotableFields);
147            }
148    
149            // set binding info on the translated fields
150            if (bindingInfo == null) {
151                bindingInfo = new BindingInfo();
152            }
153    
154            // property name should point to a Map that holds attribute name/value pairs
155            bindingInfo.addToBindByNamePrefix(propertyName);
156            bindingInfo.setBindToMap(true);
157    
158            for (InputField field : attributeFields) {
159                BindingInfo fieldBindingInfo = CloneUtils.deepClone(bindingInfo);
160                fieldBindingInfo.setDefaults(view, field.getPropertyName());
161                field.setBindingInfo(fieldBindingInfo);
162    
163                view.assignComponentIds(field);
164            }
165    
166            return attributeFields;
167        }
168    
169        @Override
170        public String getComponentTypeName() {
171            return "RemoteFieldsHolder";
172        }
173    
174        /**
175         * Path to the Map property that the translated fields bind to
176         *
177         * <p>
178         * It is assumed this property points to a Map where the property names on the returned remotable fields
179         * are keys in that map, with the corresponding map value giving the model value for the field
180         * </p>
181         *
182         * @return path to property on model
183         */
184        @BeanTagAttribute(name = "propertyName")
185        public String getPropertyName() {
186            return propertyName;
187        }
188    
189        /**
190         * Setter for the property name that points to the binding Map
191         *
192         * @param propertyName
193         */
194        public void setPropertyName(String propertyName) {
195            this.propertyName = propertyName;
196        }
197    
198        /**
199         * Can be used to for more complex binding paths
200         *
201         * <p>
202         * Generally not necessary to set on a field level, any default object path or binding prefixes set
203         * on the view or container will be inherited
204         * </p>
205         *
206         * @return BindingInfo instance containing binding information for the Map property
207         */
208        @BeanTagAttribute(name = "bindingInfo", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
209        public BindingInfo getBindingInfo() {
210            return bindingInfo;
211        }
212    
213        /**
214         * Setter for the Map property binding info instance
215         *
216         * @param bindingInfo
217         */
218        public void setBindingInfo(BindingInfo bindingInfo) {
219            this.bindingInfo = bindingInfo;
220        }
221    
222        /**
223         * Name of the method to invoke for retrieving the list of remotable fields
224         *
225         * <p>
226         * When only the fetching method to call is configured it is assumed to be a valid method on the view
227         * helper service for the containing view. The method name must accept the view, model object, and parent
228         * container as arguments, and return a list of {@link RemotableAttributeField} instances.
229         * </p>
230         *
231         * <p>
232         * For invoking the method on classes other than the view helper service, see {@link #getFetchingMethodInvoker()}
233         * </p>
234         *
235         * @return name of method to invoke for fetching remote fields
236         */
237        @BeanTagAttribute(name = "fetchingMethodToCall")
238        public String getFetchingMethodToCall() {
239            return fetchingMethodToCall;
240        }
241    
242        /**
243         * Setter for the fetching method to call
244         *
245         * @param fetchingMethodToCall
246         */
247        public void setFetchingMethodToCall(String fetchingMethodToCall) {
248            this.fetchingMethodToCall = fetchingMethodToCall;
249        }
250    
251        /**
252         * Configuration for the method to invoke for retrieving the list of remotable fields
253         *
254         * <p>
255         * Through the method invoker config, a service or static class can be configured along with the
256         * method name that will be invoked. The method name must accept the view, model object, and parent
257         * container as arguments, and return a list of {@link RemotableAttributeField} instances.
258         * </p>
259         *
260         * <p>
261         * Note the {@link org.kuali.rice.krad.uif.component.MethodInvokerConfig#getTargetMethod()} property can
262         * be configured, or the {@link #getFetchingMethodToCall()}. In the case of both configurations, the target
263         * method on the method invoker config will be used
264         * </p>
265         *
266         * @return MethodInvokerConfig instance containing method configuration
267         */
268        @BeanTagAttribute(name = "fetchingMethodInvoker", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
269        public MethodInvokerConfig getFetchingMethodInvoker() {
270            return fetchingMethodInvoker;
271        }
272    
273        /**
274         * Setter for the fetching method to invoke configuration
275         *
276         * @param fetchingMethodInvoker
277         */
278        public void setFetchingMethodInvoker(MethodInvokerConfig fetchingMethodInvoker) {
279            this.fetchingMethodInvoker = fetchingMethodInvoker;
280        }
281    
282        /**
283         * @see org.kuali.rice.krad.uif.component.ComponentBase#copy()
284         */
285        @Override
286        protected <T> void copyProperties(T component) {
287            super.copyProperties(component);
288            RemoteFieldsHolder remoteFieldsHolderCopy = (RemoteFieldsHolder) component;
289            remoteFieldsHolderCopy.setPropertyName(this.getPropertyName());
290    
291            if(this.bindingInfo != null) {
292                remoteFieldsHolderCopy.setBindingInfo((BindingInfo)this.getBindingInfo().copy());
293            }
294    
295            remoteFieldsHolderCopy.setFetchingMethodToCall(this.getFetchingMethodToCall());
296    
297            if(this.fetchingMethodInvoker != null) {
298                this.setFetchingMethodInvoker((MethodInvokerConfig)this.getFetchingMethodInvoker().copy());
299            }
300        }
301    
302    }