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.inquiry; 017 018import org.apache.commons.lang.StringUtils; 019import org.kuali.rice.core.api.CoreApiServiceLocator; 020import org.kuali.rice.core.api.encryption.EncryptionService; 021import org.kuali.rice.krad.bo.BusinessObject; 022import org.kuali.rice.krad.bo.DocumentHeader; 023import org.kuali.rice.krad.bo.ExternalizableBusinessObject; 024import org.kuali.rice.krad.data.CompoundKey; 025import org.kuali.rice.krad.data.KradDataServiceLocator; 026import org.kuali.rice.krad.datadictionary.exception.UnknownBusinessClassAttributeException; 027import org.kuali.rice.krad.service.DataDictionaryService; 028import org.kuali.rice.krad.service.DataObjectAuthorizationService; 029import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 030import org.kuali.rice.krad.service.KualiModuleService; 031import org.kuali.rice.krad.service.LegacyDataAdapter; 032import org.kuali.rice.krad.service.ModuleService; 033import org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl; 034import org.kuali.rice.krad.uif.widget.Inquiry; 035import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils; 036import org.kuali.rice.krad.util.KRADConstants; 037import org.kuali.rice.krad.util.KRADUtils; 038import org.springframework.beans.PropertyAccessorUtils; 039 040import java.security.GeneralSecurityException; 041import java.util.ArrayList; 042import java.util.Collections; 043import java.util.HashMap; 044import java.util.List; 045import java.util.Map; 046 047/** 048 * Implementation of the <code>Inquirable</code> interface that uses metadata 049 * from the data dictionary and performs a query against the database to retrieve 050 * the data object for inquiry 051 * 052 * <p> 053 * More advanced lookup operations or alternate ways of retrieving metadata can 054 * be implemented by extending this base implementation and configuring 055 * </p> 056 * 057 * @author Kuali Rice Team (rice.collab@kuali.org) 058 */ 059public class InquirableImpl extends ViewHelperServiceImpl implements Inquirable { 060 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(InquirableImpl.class); 061 062 protected Class<?> dataObjectClass; 063 064 /** 065 * A list that can be used to define classes that are superclasses or 066 * superinterfaces of kuali objects where those objects' inquiry URLs need 067 * to use the name of the superclass or superinterface as the business 068 * object class attribute 069 */ 070 public static List<Class<?>> SUPER_CLASS_TRANSLATOR_LIST = new ArrayList<Class<?>>(); 071 072 /** 073 * Finds primary and alternate key sets configured for the configured data object class and 074 * then attempts to find a set with matching key/value pairs from the request, if a set is 075 * found then calls the module service (for EBOs) or business object service to retrieve 076 * the data object 077 * 078 * <p> 079 * Note at this point on business objects are supported by the default implementation 080 * </p> 081 * 082 * {@inheritDoc} 083 */ 084 @Override 085 public Object retrieveDataObject(Map<String, String> parameters) { 086 if (dataObjectClass == null) { 087 LOG.error("Data object class must be set in inquirable before retrieving the object"); 088 throw new RuntimeException("Data object class must be set in inquirable before retrieving the object"); 089 } 090 091 // build list of key values from the map parameters 092 List<String> pkPropertyNames = getLegacyDataAdapter().listPrimaryKeyFieldNames(dataObjectClass); 093 094 // some classes might have alternate keys defined for retrieving 095 List<List<String>> alternateKeyNameSets = getAlternateKeysForClass(dataObjectClass); 096 097 // add pk set as beginning so it will be checked first for match 098 alternateKeyNameSets.add(0, pkPropertyNames); 099 100 List<String> dataObjectKeySet = retrieveKeySetFromMap(alternateKeyNameSets, parameters); 101 if ((dataObjectKeySet == null) || dataObjectKeySet.isEmpty()) { 102 LOG.warn("Matching key set not found in request for class: " + getDataObjectClass()); 103 104 return null; 105 } 106 107 // found key set, now build map of key values pairs we can use to retrieve the object 108 Map<String, String> keyPropertyValues = new HashMap<String, String>(); 109 for (String keyPropertyName : dataObjectKeySet) { 110 String keyPropertyValue = parameters.get(keyPropertyName); 111 112 // uppercase value if needed 113 Boolean forceUppercase = Boolean.FALSE; 114 try { 115 forceUppercase = getDataDictionaryService().getAttributeForceUppercase(dataObjectClass, 116 keyPropertyName); 117 } catch (UnknownBusinessClassAttributeException ex) { 118 // swallowing exception because this check for ForceUppercase would 119 // require a DD entry for the attribute, and we will just set force uppercase to false 120 LOG.warn("Data object class " 121 + dataObjectClass 122 + " property " 123 + keyPropertyName 124 + " should probably have a DD definition.", ex); 125 } 126 127 if (forceUppercase.booleanValue() && (keyPropertyValue != null)) { 128 keyPropertyValue = keyPropertyValue.toUpperCase(); 129 } 130 131 // check security on field 132 boolean isSecure = KRADUtils.isSecure(keyPropertyName, dataObjectClass); 133 134 if (StringUtils.endsWith(keyPropertyValue, EncryptionService.ENCRYPTION_POST_PREFIX)) { 135 keyPropertyValue = StringUtils.removeEnd(keyPropertyValue, EncryptionService.ENCRYPTION_POST_PREFIX); 136 isSecure = true; 137 } 138 139 // decrypt if the value is secure 140 if (isSecure) { 141 try { 142 if (CoreApiServiceLocator.getEncryptionService().isEnabled()) { 143 keyPropertyValue = getEncryptionService().decrypt(keyPropertyValue); 144 } 145 } catch (GeneralSecurityException e) { 146 String message = "Data object class " + dataObjectClass + " property " + keyPropertyName 147 + " should have been encrypted, but there was a problem decrypting it."; 148 LOG.error(message, e); 149 150 throw new RuntimeException(message, e); 151 } 152 } 153 154 keyPropertyValues.put(keyPropertyName, keyPropertyValue); 155 } 156 157 // now retrieve the object based on the key set 158 Object dataObject = null; 159 160 Map<String, Object> translatedValues = KRADUtils.coerceRequestParameterTypes( 161 (Class<? extends ExternalizableBusinessObject>) getDataObjectClass(), keyPropertyValues); 162 163 ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService( 164 getDataObjectClass()); 165 if (moduleService != null && moduleService.isExternalizable(getDataObjectClass())) { 166 dataObject = moduleService.getExternalizableBusinessObject(getDataObjectClass().asSubclass( 167 ExternalizableBusinessObject.class), translatedValues); 168 } else if ( KradDataServiceLocator.getDataObjectService().supports(getDataObjectClass())) { 169 dataObject = KradDataServiceLocator.getDataObjectService().find(getDataObjectClass(), new CompoundKey(translatedValues)); 170 } else if (BusinessObject.class.isAssignableFrom(getDataObjectClass())) { 171 dataObject = getLegacyDataAdapter().findByPrimaryKey(getDataObjectClass().asSubclass( 172 BusinessObject.class), translatedValues); 173 } else { 174 throw new IllegalArgumentException( "ERROR: Unsupported object type passed to inquiry: " + getDataObjectClass() + " / keys=" + keyPropertyValues ); 175 } 176 return dataObject; 177 } 178 179 180 /** 181 * Iterates through the list of key sets looking for a set where the given map of parameters has 182 * all the key names and values are non-blank, first matched set is returned 183 * 184 * @param potentialKeySets - List of key sets to check for match 185 * @param parameters - map of parameter name/value pairs for matching key set 186 * @return List<String> key set that was matched, or null if none were matched 187 */ 188 protected List<String> retrieveKeySetFromMap(List<List<String>> potentialKeySets, Map<String, String> parameters) { 189 List<String> foundKeySet = null; 190 191 for (List<String> potentialKeySet : potentialKeySets) { 192 boolean keySetMatch = true; 193 for (String keyName : potentialKeySet) { 194 if (!parameters.containsKey(keyName) || StringUtils.isBlank(parameters.get(keyName))) { 195 keySetMatch = false; 196 } 197 } 198 199 if (keySetMatch) { 200 foundKeySet = potentialKeySet; 201 break; 202 } 203 } 204 205 return foundKeySet; 206 } 207 208 /** 209 * Invokes the module service to retrieve any alternate keys that have been 210 * defined for the given class 211 * 212 * @param clazz - class to find alternate keys for 213 * @return List<List<String>> list of alternate key sets, or empty list if none are found 214 */ 215 protected List<List<String>> getAlternateKeysForClass(Class<?> clazz) { 216 KualiModuleService kualiModuleService = getKualiModuleService(); 217 ModuleService moduleService = kualiModuleService.getResponsibleModuleService(clazz); 218 219 List<List<String>> altKeys = null; 220 if (moduleService != null) { 221 altKeys = moduleService.listAlternatePrimaryKeyFieldNames(clazz); 222 } 223 224 return altKeys != null ? altKeys : new ArrayList<List<String>>(); 225 } 226 227 /** 228 * @see Inquirable#buildInquirableLink(java.lang.Object, 229 * java.lang.String, org.kuali.rice.krad.uif.widget.Inquiry) 230 */ 231 @Override 232 public void buildInquirableLink(Object dataObject, String propertyName, Inquiry inquiry) { 233 Class<?> inquiryObjectClass = null; 234 235 // inquiry into data object class if property is title attribute 236 Class<?> objectClass = KRADUtils.materializeClassForProxiedObject(dataObject); 237 if (propertyName.equals(KRADServiceLocatorWeb.getLegacyDataAdapter().getTitleAttribute(objectClass))) { 238 inquiryObjectClass = objectClass; 239 } else if (PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName)) { 240 String nestedPropertyName = KRADUtils.getNestedAttributePrefix(propertyName); 241 Object nestedPropertyObject = KRADUtils.getNestedValue(dataObject, nestedPropertyName); 242 243 if (KRADUtils.isNotNull(nestedPropertyObject)) { 244 String nestedPropertyPrimitive = KRADUtils.getNestedAttributePrimitive(propertyName); 245 Class<?> nestedPropertyObjectClass = KRADUtils.materializeClassForProxiedObject(nestedPropertyObject); 246 247 if (nestedPropertyPrimitive.equals(KRADServiceLocatorWeb.getLegacyDataAdapter().getTitleAttribute( 248 nestedPropertyObjectClass))) { 249 inquiryObjectClass = nestedPropertyObjectClass; 250 } 251 } 252 } 253 254 // if not title, then get primary relationship 255 if (inquiryObjectClass == null) { 256 inquiryObjectClass = getLegacyDataAdapter().getInquiryObjectClassIfNotTitle(dataObject,propertyName); 257 } 258 259 // if haven't found inquiry class, then no inquiry can be rendered 260 if (inquiryObjectClass == null) { 261 inquiry.setRender(false); 262 263 return; 264 } 265 266 if (DocumentHeader.class.isAssignableFrom(inquiryObjectClass)) { 267 String documentNumber = (String) KradDataServiceLocator.getDataObjectService().wrap(dataObject).getPropertyValueNullSafe(propertyName); 268 if (StringUtils.isNotBlank(documentNumber)) { 269 inquiry.getInquiryLink().setHref(getConfigurationService().getPropertyValueAsString( 270 KRADConstants.WORKFLOW_URL_KEY) 271 + KRADConstants.DOCHANDLER_DO_URL 272 + documentNumber 273 + KRADConstants.DOCHANDLER_URL_CHUNK); 274 inquiry.getInquiryLink().setLinkText(documentNumber); 275 inquiry.setRender(true); 276 } 277 278 return; 279 } 280 281 synchronized (SUPER_CLASS_TRANSLATOR_LIST) { 282 for (Class<?> clazz : SUPER_CLASS_TRANSLATOR_LIST) { 283 if (clazz.isAssignableFrom(inquiryObjectClass)) { 284 inquiryObjectClass = clazz; 285 break; 286 } 287 } 288 } 289 290 if (!inquiryObjectClass.isInterface() && ExternalizableBusinessObject.class.isAssignableFrom( 291 inquiryObjectClass)) { 292 inquiryObjectClass = ExternalizableBusinessObjectUtils.determineExternalizableBusinessObjectSubInterface( 293 inquiryObjectClass); 294 } 295 296 // listPrimaryKeyFieldNames returns an unmodifiable list. So a copy is necessary. 297 List<String> keys = new ArrayList<String>(getLegacyDataAdapter().listPrimaryKeyFieldNames( 298 inquiryObjectClass)); 299 300 if (keys == null) { 301 keys = Collections.emptyList(); 302 } 303 304 // build inquiry parameter mappings 305 Map<String, String> inquiryParameters = getLegacyDataAdapter().getInquiryParameters(dataObject,keys,propertyName); 306 307 inquiry.buildInquiryLink(dataObject, propertyName, inquiryObjectClass, inquiryParameters); 308 } 309 310 /** 311 * {@inheritDoc} 312 */ 313 @Override 314 public Class<?> getDataObjectClass() { 315 return this.dataObjectClass; 316 } 317 318 /** 319 * {@inheritDoc} 320 */ 321 @Override 322 public void setDataObjectClass(Class<?> dataObjectClass) { 323 this.dataObjectClass = dataObjectClass; 324 } 325 326 protected LegacyDataAdapter getLegacyDataAdapter() { 327 return KRADServiceLocatorWeb.getLegacyDataAdapter(); 328 } 329 330 protected KualiModuleService getKualiModuleService() { 331 return KRADServiceLocatorWeb.getKualiModuleService(); 332 } 333 334 @Override 335 public DataDictionaryService getDataDictionaryService() { 336 return KRADServiceLocatorWeb.getDataDictionaryService(); 337 } 338 339 protected DataObjectAuthorizationService getDataObjectAuthorizationService() { 340 return KRADServiceLocatorWeb.getDataObjectAuthorizationService(); 341 } 342 343 protected EncryptionService getEncryptionService() { 344 return CoreApiServiceLocator.getEncryptionService(); 345 } 346 347}