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}