001/**
002 * Copyright 2005-2018 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.lifecycle.initialize;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.krad.datadictionary.AttributeDefinition;
020import org.kuali.rice.krad.service.DataDictionaryService;
021import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
022import org.kuali.rice.krad.uif.UifConstants;
023import org.kuali.rice.krad.uif.UifPropertyPaths;
024import org.kuali.rice.krad.uif.component.BindingInfo;
025import org.kuali.rice.krad.uif.field.DataField;
026import org.kuali.rice.krad.uif.field.InputField;
027import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
028import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleTaskBase;
029import org.kuali.rice.krad.uif.util.ComponentFactory;
030import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
031import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
032import org.kuali.rice.krad.uif.view.View;
033import org.kuali.rice.krad.util.KRADConstants;
034
035import java.util.List;
036import java.util.Map;
037import java.util.Map.Entry;
038
039/**
040 * Performs initialization on data fields based on attributes found in the data dictionary.
041 * 
042 * @author Kuali Rice Team (rice.collab@kuali.org)
043 */
044public class InitializeDataFieldFromDictionaryTask extends ViewLifecycleTaskBase<DataField> {
045
046    /**
047     * Constructor.
048     * 
049     * @param phase The initialize phase for the data field.
050     */
051    public InitializeDataFieldFromDictionaryTask() {
052        super(DataField.class);
053    }
054
055    /**
056     * Sets properties of the <code>InputField</code> (if blank) to the corresponding attribute
057     * entry in the data dictionary
058     *
059     * {@inheritDoc}
060     */
061    @Override
062    protected void performLifecycleTask() {
063        DataField field = (DataField) getElementState().getElement();
064
065        AttributeDefinition attributeDefinition = null;
066
067        String dictionaryAttributeName = field.getDictionaryAttributeName();
068        String dictionaryObjectEntry = field.getDictionaryObjectEntry();
069
070        Map<String, String> propertyExpressions = field.getPropertyExpressions();
071
072        if (dictionaryAttributeName == null && propertyExpressions.containsKey(UifPropertyPaths.DICTIONARY_ATTR_NAME)) {
073            ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator();
074            dictionaryAttributeName = propertyExpressions.get(UifPropertyPaths.DICTIONARY_ATTR_NAME);
075
076            if (dictionaryAttributeName.contains(UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX)) {
077                dictionaryAttributeName = dictionaryAttributeName.replace(UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
078                        "");
079            } else if (dictionaryAttributeName.contains(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX)) {
080                // It is not possible to add both the collection and index to the context at this time.  Until
081                // the dictionaryAttributeName expression can be properly evaluated, get the attribute definition for
082                // the field by looking at the first item in the collection if an item exists.
083
084                List<Object> collection = ObjectPropertyUtils.getPropertyValue(ViewLifecycle.getModel(),
085                       field.getBindingInfo().getCollectionPath());
086
087                if (!collection.isEmpty()) {
088                    dictionaryAttributeName = dictionaryAttributeName.replace(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
089                            field.getBindingInfo().getCollectionPath() + "[0].");
090                } else {
091                    dictionaryAttributeName = null;
092                }
093            }
094
095            dictionaryAttributeName = (String) expressionEvaluator.evaluateExpression(field.getContext(),
096                    dictionaryAttributeName);
097        }
098
099        if (!(dictionaryAttributeName == null && propertyExpressions.containsKey(UifPropertyPaths.DICTIONARY_ATTR_NAME))) {
100
101            // if entry given but not attribute name, use field name as attribute name
102            if (StringUtils.isNotBlank(dictionaryObjectEntry) && StringUtils.isBlank(dictionaryAttributeName)) {
103                dictionaryAttributeName = field.getPropertyName();
104            }
105
106            // if dictionary entry and attribute set, attempt to find definition
107            if (StringUtils.isNotBlank(dictionaryAttributeName) && StringUtils.isNotBlank(dictionaryObjectEntry)) {
108                attributeDefinition = KRADServiceLocatorWeb.getDataDictionaryService().getAttributeDefinition(
109                        dictionaryObjectEntry, dictionaryAttributeName);
110            }
111
112            // if definition not found, recurse through path
113            if (attributeDefinition == null) {
114                BindingInfo fieldBindingInfo = field.getBindingInfo();
115                String collectionPath = fieldBindingInfo.getCollectionPath();
116
117                String propertyPath;
118                if (StringUtils.isNotBlank(collectionPath)) {
119                    StringBuilder propertyPathBuilder = new StringBuilder();
120
121                    String bindingObjectPath = fieldBindingInfo.getBindingObjectPath();
122                    if (StringUtils.isNotBlank(bindingObjectPath)) {
123                        propertyPathBuilder.append(bindingObjectPath).append('.');
124                    }
125
126                    propertyPathBuilder.append(collectionPath).append('.');
127
128                    String bindByNamePrefix = fieldBindingInfo.getBindByNamePrefix();
129                    if (StringUtils.isNotBlank(bindByNamePrefix)) {
130
131                        // fix for both collectionPath and bindByNamePrefix being set,
132                        // wherein the bindByNamePrefix contains the collectionPath
133                        if (!bindByNamePrefix.startsWith(collectionPath)) {
134                            propertyPathBuilder.append(bindByNamePrefix).append('.');
135                        }
136                    }
137
138                    propertyPathBuilder.append(fieldBindingInfo.getBindingName());
139                    propertyPath = propertyPathBuilder.toString();
140                } else {
141                    propertyPath = field.getBindingInfo().getBindingPath();
142                }
143
144                attributeDefinition = findNestedDictionaryAttribute(propertyPath);
145            }
146
147            // if a definition was found, initialize field from definition
148            if (attributeDefinition != null) {
149                field.copyFromAttributeDefinition(attributeDefinition);
150            }
151        }
152
153        // if control still null, assign default
154        if (field instanceof InputField) {
155            InputField inputField = (InputField) field;
156            if (inputField.getControl() == null) {
157                inputField.setControl(ComponentFactory.getTextControl());
158            }
159        }
160    }
161
162    /**
163     * Determines the name of a data dictionary entry based on the portion of the path leading up to
164     * the attribute name.
165     * 
166     * <p>
167     * The property path passed in is checked first against
168     * {@link View#getObjectPathToConcreteClassMapping()} for a full or partial match. If no match
169     * is found then property type relative to the model involved in the current view lifecycle is
170     * returned, where applicable.
171     * </p>
172     * 
173     * @param dictionaryEntryPrefix Portion of a property path referring to the entry that has the
174     *        attribute.
175     * @return The name of the dictionary entry indicated by the property path.
176     */
177    private String getDictionaryEntryName(String dictionaryEntryPrefix) {
178        if (StringUtils.isEmpty(dictionaryEntryPrefix)) {
179            return dictionaryEntryPrefix;
180        }
181        
182        Map<String, Class<?>> modelClasses = ViewLifecycle.getView().getObjectPathToConcreteClassMapping();
183        Class<?> dictionaryModelClass = modelClasses.get(dictionaryEntryPrefix);
184
185        // full match
186        if (dictionaryModelClass != null) {
187            return dictionaryModelClass.getName();
188        }
189
190        // in case of partial match, holds the class that matched and the
191        // property so we can get by reflection
192        Class<?> modelClass = null;
193        String modelProperty = dictionaryEntryPrefix;
194
195        int bestMatchLength = 0;
196        int modelClassPathLength = dictionaryEntryPrefix.length();
197
198        // check if property path matches one of the modelClass entries
199        synchronized (modelClasses) {
200            // synchronizing on modelClasses prevents ConcurrentModificationException during
201            // asynchronous lifecycle processing
202            for (Entry<String, Class<?>> modelClassEntry : modelClasses.entrySet()) {
203                String path = modelClassEntry.getKey();
204                int pathlen = path.length();
205
206                if (dictionaryEntryPrefix.startsWith(path) && pathlen > bestMatchLength
207                        && modelClassPathLength > pathlen && dictionaryEntryPrefix.charAt(pathlen) == '.') {
208                    bestMatchLength = pathlen;
209                    modelClass = modelClassEntry.getValue();
210                    modelProperty = dictionaryEntryPrefix.substring(pathlen + 1);
211                }
212            }
213        }
214
215        if (modelClass != null) {
216            // if a partial match was found, look up the property type based on matched model class
217            dictionaryModelClass = ObjectPropertyUtils.getPropertyType(modelClass, modelProperty);
218        }
219
220        if (dictionaryModelClass == null) {
221            // If no full or partial match, look up based on the model directly
222            dictionaryModelClass = ObjectPropertyUtils.getPropertyType(ViewLifecycle.getModel(), dictionaryEntryPrefix);
223        }
224
225        return dictionaryModelClass == null ? null : dictionaryModelClass.getName();
226    }
227
228    /**
229     * Recursively drills down the property path (if nested) to find an AttributeDefinition, the
230     * first attribute definition found will be returned
231     * 
232     * <p>
233     * e.g. suppose parentPath is 'document' and propertyPath is 'account.subAccount.name', first
234     * the property type for document will be retrieved using the view metadata and used as the
235     * dictionary entry, with the propertyPath as the dictionary attribute, if an attribute
236     * definition exists it will be returned. Else, the first part of the property path is added to
237     * the parent, making the parentPath 'document.account' and the propertyPath 'subAccount.name',
238     * the method is then called again to perform the process with those parameters. The recursion
239     * continues until an attribute field is found, or the propertyPath is no longer nested
240     * </p>
241     * 
242     * @param propertyPath path of the property to use as dictionary attribute and to drill down on
243     * @return AttributeDefinition if found, or Null
244     */
245    protected AttributeDefinition findNestedDictionaryAttribute(String propertyPath) {
246        DataField field = (DataField) getElementState().getElement();
247
248        String fieldBindingPrefix = null;
249        String dictionaryAttributePath = propertyPath;
250
251        if (field.getBindingInfo().isBindToMap()) {
252            fieldBindingPrefix = "";
253            if (!field.getBindingInfo().isBindToForm() && StringUtils.isNotBlank(
254                    field.getBindingInfo().getBindingObjectPath())) {
255                fieldBindingPrefix = field.getBindingInfo().getBindingObjectPath();
256            }
257            if (StringUtils.isNotBlank(field.getBindingInfo().getBindByNamePrefix())) {
258                if (StringUtils.isNotBlank(fieldBindingPrefix)) {
259                    fieldBindingPrefix += "." + field.getBindingInfo().getBindByNamePrefix();
260                } else {
261                    fieldBindingPrefix = field.getBindingInfo().getBindByNamePrefix();
262                }
263            }
264
265            dictionaryAttributePath = field.getBindingInfo().getBindingName();
266        }
267
268        if (StringUtils.isEmpty(dictionaryAttributePath)) {
269            return null;
270        }
271
272        if (StringUtils.startsWith(dictionaryAttributePath, KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX)) {
273            dictionaryAttributePath = StringUtils.substringAfter(dictionaryAttributePath, KRADConstants.LOOKUP_RANGE_LOWER_BOUND_PROPERTY_PREFIX);
274        }
275        
276        DataDictionaryService ddService = KRADServiceLocatorWeb.getDataDictionaryService();
277        
278        String dictionaryAttributeName = ObjectPropertyUtils.getCanonicalPath(dictionaryAttributePath);
279        String dictionaryEntryPrefix = fieldBindingPrefix;
280        
281        AttributeDefinition attribute = null;
282        String dictionaryEntryName = null;
283        
284        int i = dictionaryAttributeName.indexOf('.');
285        while (attribute == null && i != -1) {
286            
287            if (dictionaryEntryPrefix != null) {
288                dictionaryEntryName = getDictionaryEntryName(dictionaryEntryPrefix);
289                dictionaryEntryPrefix += '.' + dictionaryAttributeName.substring(0, i);
290            } else {
291                dictionaryEntryName = null;
292                dictionaryEntryPrefix = dictionaryAttributeName.substring(0, i);
293            }
294
295            if (dictionaryEntryName != null) {
296                attribute = ddService.getAttributeDefinition(dictionaryEntryName, dictionaryAttributeName);
297            }
298            
299            if (attribute == null) {
300                dictionaryAttributeName = dictionaryAttributeName.substring(i+1);
301                i = dictionaryAttributeName.indexOf('.');
302            }
303        }
304        
305        if (attribute == null && dictionaryEntryPrefix != null) {
306            dictionaryEntryName = getDictionaryEntryName(dictionaryEntryPrefix);
307            
308            if (dictionaryEntryName != null) {
309                attribute = ddService.getAttributeDefinition(dictionaryEntryName, dictionaryAttributeName);
310            }
311        }
312        
313        // if a definition was found, update the fields dictionary properties
314        if (attribute != null) {
315            field.setDictionaryObjectEntry(dictionaryEntryName);
316            field.setDictionaryAttributeName(dictionaryAttributeName);
317        }
318
319        return attribute;
320    }
321
322}