View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.inquiry;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.core.api.CoreApiServiceLocator;
20  import org.kuali.rice.core.api.encryption.EncryptionService;
21  import org.kuali.rice.krad.bo.BusinessObject;
22  import org.kuali.rice.krad.bo.DocumentHeader;
23  import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
24  import org.kuali.rice.krad.data.CompoundKey;
25  import org.kuali.rice.krad.data.KradDataServiceLocator;
26  import org.kuali.rice.krad.datadictionary.exception.UnknownBusinessClassAttributeException;
27  import org.kuali.rice.krad.service.DataDictionaryService;
28  import org.kuali.rice.krad.service.DataObjectAuthorizationService;
29  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
30  import org.kuali.rice.krad.service.KualiModuleService;
31  import org.kuali.rice.krad.service.LegacyDataAdapter;
32  import org.kuali.rice.krad.service.ModuleService;
33  import org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl;
34  import org.kuali.rice.krad.uif.widget.Inquiry;
35  import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
36  import org.kuali.rice.krad.util.KRADConstants;
37  import org.kuali.rice.krad.util.KRADUtils;
38  import org.springframework.beans.PropertyAccessorUtils;
39  
40  import java.security.GeneralSecurityException;
41  import java.util.ArrayList;
42  import java.util.Collections;
43  import java.util.HashMap;
44  import java.util.List;
45  import java.util.Map;
46  
47  /**
48   * Implementation of the <code>Inquirable</code> interface that uses metadata
49   * from the data dictionary and performs a query against the database to retrieve
50   * the data object for inquiry
51   *
52   * <p>
53   * More advanced lookup operations or alternate ways of retrieving metadata can
54   * be implemented by extending this base implementation and configuring
55   * </p>
56   *
57   * @author Kuali Rice Team (rice.collab@kuali.org)
58   */
59  public class InquirableImpl extends ViewHelperServiceImpl implements Inquirable {
60      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(InquirableImpl.class);
61  
62      protected Class<?> dataObjectClass;
63  
64      /**
65       * A list that can be used to define classes that are superclasses or
66       * superinterfaces of kuali objects where those objects' inquiry URLs need
67       * to use the name of the superclass or superinterface as the business
68       * object class attribute
69       */
70      public static List<Class<?>> SUPER_CLASS_TRANSLATOR_LIST = new ArrayList<Class<?>>();
71  
72      /**
73       * Finds primary and alternate key sets configured for the configured data object class and
74       * then attempts to find a set with matching key/value pairs from the request, if a set is
75       * found then calls the module service (for EBOs) or business object service to retrieve
76       * the data object
77       *
78       * <p>
79       * Note at this point on business objects are supported by the default implementation
80       * </p>
81       *
82       * {@inheritDoc}
83       */
84      @Override
85      public Object retrieveDataObject(Map<String, String> parameters) {
86          if (dataObjectClass == null) {
87              LOG.error("Data object class must be set in inquirable before retrieving the object");
88              throw new RuntimeException("Data object class must be set in inquirable before retrieving the object");
89          }
90  
91          // build list of key values from the map parameters
92          List<String> pkPropertyNames = getLegacyDataAdapter().listPrimaryKeyFieldNames(dataObjectClass);
93  
94          // some classes might have alternate keys defined for retrieving
95          List<List<String>> alternateKeyNameSets = getAlternateKeysForClass(dataObjectClass);
96  
97          // add pk set as beginning so it will be checked first for match
98          alternateKeyNameSets.add(0, pkPropertyNames);
99  
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 key field
132             if (getDataObjectAuthorizationService().attributeValueNeedsToBeEncryptedOnFormsAndLinks(dataObjectClass,
133                     keyPropertyName)) {
134                 try {
135                     if(CoreApiServiceLocator.getEncryptionService().isEnabled()) {
136                         keyPropertyValue = getEncryptionService().decrypt(keyPropertyValue);
137                     }
138                 } catch (GeneralSecurityException e) {
139                     LOG.error("Data object class "
140                             + dataObjectClass
141                             + " property "
142                             + keyPropertyName
143                             + " should have been encrypted, but there was a problem decrypting it.", e);
144                     throw new RuntimeException("Data object class "
145                             + dataObjectClass
146                             + " property "
147                             + keyPropertyName
148                             + " should have been encrypted, but there was a problem decrypting it.", e);
149                 }
150             }
151 
152             keyPropertyValues.put(keyPropertyName, keyPropertyValue);
153         }
154 
155         // now retrieve the object based on the key set
156         Object dataObject = null;
157 
158         Map<String, Object> translatedValues  = KRADUtils.coerceRequestParameterTypes(
159                 (Class<? extends ExternalizableBusinessObject>) getDataObjectClass(), keyPropertyValues);
160 
161         ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(
162                 getDataObjectClass());
163         if (moduleService != null && moduleService.isExternalizable(getDataObjectClass())) {
164             dataObject = moduleService.getExternalizableBusinessObject(getDataObjectClass().asSubclass(
165                     ExternalizableBusinessObject.class), translatedValues);
166         } else if ( KradDataServiceLocator.getDataObjectService().supports(getDataObjectClass())) {
167             dataObject = KradDataServiceLocator.getDataObjectService().find(getDataObjectClass(), new CompoundKey(translatedValues));
168         } else if (BusinessObject.class.isAssignableFrom(getDataObjectClass())) {
169             dataObject = getLegacyDataAdapter().findByPrimaryKey(getDataObjectClass().asSubclass(
170                     BusinessObject.class), translatedValues);
171         } else {
172             throw new IllegalArgumentException( "ERROR: Unsupported object type passed to inquiry: " + getDataObjectClass() + " / keys=" + keyPropertyValues );
173         }
174         return dataObject;
175     }
176 
177 
178     /**
179      * Iterates through the list of key sets looking for a set where the given map of parameters has
180      * all the key names and values are non-blank, first matched set is returned
181      *
182      * @param potentialKeySets - List of key sets to check for match
183      * @param parameters - map of parameter name/value pairs for matching key set
184      * @return List<String> key set that was matched, or null if none were matched
185      */
186     protected List<String> retrieveKeySetFromMap(List<List<String>> potentialKeySets, Map<String, String> parameters) {
187         List<String> foundKeySet = null;
188 
189         for (List<String> potentialKeySet : potentialKeySets) {
190             boolean keySetMatch = true;
191             for (String keyName : potentialKeySet) {
192                 if (!parameters.containsKey(keyName) || StringUtils.isBlank(parameters.get(keyName))) {
193                     keySetMatch = false;
194                 }
195             }
196 
197             if (keySetMatch) {
198                 foundKeySet = potentialKeySet;
199                 break;
200             }
201         }
202 
203         return foundKeySet;
204     }
205 
206     /**
207      * Invokes the module service to retrieve any alternate keys that have been
208      * defined for the given class
209      *
210      * @param clazz - class to find alternate keys for
211      * @return List<List<String>> list of alternate key sets, or empty list if none are found
212      */
213     protected List<List<String>> getAlternateKeysForClass(Class<?> clazz) {
214         KualiModuleService kualiModuleService = getKualiModuleService();
215         ModuleService moduleService = kualiModuleService.getResponsibleModuleService(clazz);
216 
217         List<List<String>> altKeys = null;
218         if (moduleService != null) {
219             altKeys = moduleService.listAlternatePrimaryKeyFieldNames(clazz);
220         }
221 
222         return altKeys != null ? altKeys : new ArrayList<List<String>>();
223     }
224 
225     /**
226      * @see Inquirable#buildInquirableLink(java.lang.Object,
227      *      java.lang.String, org.kuali.rice.krad.uif.widget.Inquiry)
228      */
229     @Override
230     public void buildInquirableLink(Object dataObject, String propertyName, Inquiry inquiry) {
231         Class<?> inquiryObjectClass = null;
232 
233         // inquiry into data object class if property is title attribute
234         Class<?> objectClass = KRADUtils.materializeClassForProxiedObject(dataObject);
235         if (propertyName.equals(KRADServiceLocatorWeb.getLegacyDataAdapter().getTitleAttribute(objectClass))) {
236             inquiryObjectClass = objectClass;
237         } else if (PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName)) {
238             String nestedPropertyName = KRADUtils.getNestedAttributePrefix(propertyName);
239             Object nestedPropertyObject = KRADUtils.getNestedValue(dataObject, nestedPropertyName);
240 
241             if (KRADUtils.isNotNull(nestedPropertyObject)) {
242                 String nestedPropertyPrimitive = KRADUtils.getNestedAttributePrimitive(propertyName);
243                 Class<?> nestedPropertyObjectClass = KRADUtils.materializeClassForProxiedObject(nestedPropertyObject);
244 
245                 if (nestedPropertyPrimitive.equals(KRADServiceLocatorWeb.getLegacyDataAdapter().getTitleAttribute(
246                         nestedPropertyObjectClass))) {
247                     inquiryObjectClass = nestedPropertyObjectClass;
248                 }
249             }
250         }
251 
252         // if not title, then get primary relationship
253         if (inquiryObjectClass == null) {
254             inquiryObjectClass = getLegacyDataAdapter().getInquiryObjectClassIfNotTitle(dataObject,propertyName);
255         }
256 
257         // if haven't found inquiry class, then no inquiry can be rendered
258         if (inquiryObjectClass == null) {
259             inquiry.setRender(false);
260 
261             return;
262         }
263 
264         if (DocumentHeader.class.isAssignableFrom(inquiryObjectClass)) {
265             String documentNumber = (String) KradDataServiceLocator.getDataObjectService().wrap(dataObject).getPropertyValueNullSafe(propertyName);
266             if (StringUtils.isNotBlank(documentNumber)) {
267                 inquiry.getInquiryLink().setHref(getConfigurationService().getPropertyValueAsString(
268                         KRADConstants.WORKFLOW_URL_KEY)
269                         + KRADConstants.DOCHANDLER_DO_URL
270                         + documentNumber
271                         + KRADConstants.DOCHANDLER_URL_CHUNK);
272                 inquiry.getInquiryLink().setLinkText(documentNumber);
273                 inquiry.setRender(true);
274             }
275 
276             return;
277         }
278 
279         synchronized (SUPER_CLASS_TRANSLATOR_LIST) {
280             for (Class<?> clazz : SUPER_CLASS_TRANSLATOR_LIST) {
281                 if (clazz.isAssignableFrom(inquiryObjectClass)) {
282                     inquiryObjectClass = clazz;
283                     break;
284                 }
285             }
286         }
287 
288         if (!inquiryObjectClass.isInterface() && ExternalizableBusinessObject.class.isAssignableFrom(
289                 inquiryObjectClass)) {
290             inquiryObjectClass = ExternalizableBusinessObjectUtils.determineExternalizableBusinessObjectSubInterface(
291                     inquiryObjectClass);
292         }
293 
294         // listPrimaryKeyFieldNames returns an unmodifiable list. So a copy is necessary.
295         List<String> keys = new ArrayList<String>(getLegacyDataAdapter().listPrimaryKeyFieldNames(
296                 inquiryObjectClass));
297 
298         if (keys == null) {
299             keys = Collections.emptyList();
300         }
301 
302         // build inquiry parameter mappings
303         Map<String, String> inquiryParameters = getLegacyDataAdapter().getInquiryParameters(dataObject,keys,propertyName);
304 
305         inquiry.buildInquiryLink(dataObject, propertyName, inquiryObjectClass, inquiryParameters);
306     }
307 
308     /**
309      * {@inheritDoc}
310      */
311     @Override
312     public Class<?> getDataObjectClass() {
313         return this.dataObjectClass;
314     }
315 
316     /**
317      * {@inheritDoc}
318      */
319     @Override
320     public void setDataObjectClass(Class<?> dataObjectClass) {
321         this.dataObjectClass = dataObjectClass;
322     }
323 
324     protected LegacyDataAdapter getLegacyDataAdapter() {
325         return KRADServiceLocatorWeb.getLegacyDataAdapter();
326     }
327 
328     protected KualiModuleService getKualiModuleService() {
329         return KRADServiceLocatorWeb.getKualiModuleService();
330     }
331 
332     @Override
333     public DataDictionaryService getDataDictionaryService() {
334         return KRADServiceLocatorWeb.getDataDictionaryService();
335     }
336 
337     protected DataObjectAuthorizationService getDataObjectAuthorizationService() {
338         return KRADServiceLocatorWeb.getDataObjectAuthorizationService();
339     }
340 
341     protected EncryptionService getEncryptionService() {
342         return CoreApiServiceLocator.getEncryptionService();
343     }
344 
345     /**
346      * Creates a copy of this {@code InquirableImpl}.
347      *
348      * @return a copy of this {@code InquirableImpl}
349      */
350     public InquirableImpl copy() {
351         InquirableImpl inquirableImplCopy = KRADUtils.createNewObjectFromClass(getClass());
352 
353         if (this.getDataObjectClass() != null) {
354             inquirableImplCopy.setDataObjectClass(this.getDataObjectClass());
355         }
356 
357         if (this.getConfigurationService() != null) {
358             inquirableImplCopy.setConfigurationService(this.getConfigurationService());
359         }
360 
361         if (this.getDataDictionaryService() != null) {
362             inquirableImplCopy.setDataDictionaryService(this.getDataDictionaryService());
363         }
364 
365         if (this.getLegacyDataAdapter() != null) {
366             inquirableImplCopy.setLegacyDataAdapter(this.getLegacyDataAdapter());
367         }
368 
369         if (this.getDataObjectService() != null) {
370             inquirableImplCopy.setDataObjectService(this.getDataObjectService());
371         }
372 
373         if (this.getViewDictionaryService() != null) {
374             inquirableImplCopy.setViewDictionaryService(this.getViewDictionaryService());
375         }
376 
377         if (this.getExpressionEvaluatorFactory() != null) {
378             inquirableImplCopy.setExpressionEvaluatorFactory(this.getExpressionEvaluatorFactory());
379         }
380 
381         return inquirableImplCopy;
382     }
383 }