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