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}