001    /**
002     * Copyright 2005-2011 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     */
016    package org.kuali.rice.krad.inquiry;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.core.api.CoreApiServiceLocator;
020    import org.kuali.rice.core.api.config.property.ConfigurationService;
021    import org.kuali.rice.core.api.encryption.EncryptionService;
022    import org.kuali.rice.krad.bo.BusinessObject;
023    import org.kuali.rice.krad.bo.DataObjectRelationship;
024    import org.kuali.rice.krad.bo.DocumentHeader;
025    import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
026    import org.kuali.rice.krad.datadictionary.exception.UnknownBusinessClassAttributeException;
027    import org.kuali.rice.krad.service.BusinessObjectService;
028    import org.kuali.rice.krad.service.DataDictionaryService;
029    import org.kuali.rice.krad.service.DataObjectAuthorizationService;
030    import org.kuali.rice.krad.service.DataObjectMetaDataService;
031    import org.kuali.rice.krad.service.KRADServiceLocator;
032    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
033    import org.kuali.rice.krad.service.KualiModuleService;
034    import org.kuali.rice.krad.service.ModuleService;
035    import org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl;
036    import org.kuali.rice.krad.uif.widget.Inquiry;
037    import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
038    import org.kuali.rice.krad.util.KRADConstants;
039    import org.kuali.rice.krad.util.ObjectUtils;
040    
041    import java.security.GeneralSecurityException;
042    import java.util.ArrayList;
043    import java.util.Collections;
044    import java.util.HashMap;
045    import java.util.List;
046    import 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     */
060    public 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                        keyPropertyValue = getEncryptionService().decrypt(keyPropertyValue);
137                    } catch (GeneralSecurityException e) {
138                        LOG.error("Data object class "
139                                + dataObjectClass
140                                + " property "
141                                + keyPropertyName
142                                + " should have been encrypted, but there was a problem decrypting it.", e);
143                        throw new RuntimeException("Data object class "
144                                + dataObjectClass
145                                + " property "
146                                + keyPropertyName
147                                + " should have been encrypted, but there was a problem decrypting it.", e);
148                    }
149                }
150    
151                keyPropertyValues.put(keyPropertyName, keyPropertyValue);
152            }
153    
154            // now retrieve the object based on the key set
155            Object dataObject = null;
156    
157            ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(
158                    getDataObjectClass());
159            if (moduleService != null && moduleService.isExternalizable(getDataObjectClass())) {
160                dataObject = moduleService.getExternalizableBusinessObject(getDataObjectClass().asSubclass(
161                        ExternalizableBusinessObject.class), keyPropertyValues);
162            } else if (BusinessObject.class.isAssignableFrom(getDataObjectClass())) {
163                dataObject = getBusinessObjectService().findByPrimaryKey(getDataObjectClass().asSubclass(
164                        BusinessObject.class), keyPropertyValues);
165            }
166    
167            return dataObject;
168        }
169    
170        /**
171         * Iterates through the list of key sets looking for a set where the given map of parameters has
172         * all the key names and values are non-blank, first matched set is returned
173         *
174         * @param potentialKeySets - List of key sets to check for match
175         * @param parameters - map of parameter name/value pairs for matching key set
176         * @return List<String> key set that was matched, or null if none were matched
177         */
178        protected List<String> retrieveKeySetFromMap(List<List<String>> potentialKeySets, Map<String, String> parameters) {
179            List<String> foundKeySet = null;
180    
181            for (List<String> potentialKeySet : potentialKeySets) {
182                boolean keySetMatch = true;
183                for (String keyName : potentialKeySet) {
184                    if (!parameters.containsKey(keyName) || StringUtils.isBlank(parameters.get(keyName))) {
185                        keySetMatch = false;
186                    }
187                }
188    
189                if (keySetMatch) {
190                    foundKeySet = potentialKeySet;
191                    break;
192                }
193            }
194    
195            return foundKeySet;
196        }
197    
198        /**
199         * Invokes the module service to retrieve any alternate keys that have been
200         * defined for the given class
201         *
202         * @param clazz - class to find alternate keys for
203         * @return List<List<String>> list of alternate key sets, or empty list if none are found
204         */
205        protected List<List<String>> getAlternateKeysForClass(Class<?> clazz) {
206            KualiModuleService kualiModuleService = getKualiModuleService();
207            ModuleService moduleService = kualiModuleService.getResponsibleModuleService(clazz);
208    
209            List<List<String>> altKeys = null;
210            if (moduleService != null) {
211                altKeys = moduleService.listAlternatePrimaryKeyFieldNames(clazz);
212            }
213    
214            return altKeys != null ? altKeys : new ArrayList<List<String>>();
215        }
216    
217        /**
218         * @see Inquirable#buildInquirableLink(java.lang.Object,
219         *      java.lang.String, org.kuali.rice.krad.uif.widget.Inquiry)
220         */
221        @Override
222        public void buildInquirableLink(Object dataObject, String propertyName, Inquiry inquiry) {
223            Class<?> inquiryObjectClass = null;
224    
225            // inquiry into data object class if property is title attribute
226            Class<?> objectClass = ObjectUtils.materializeClassForProxiedObject(dataObject);
227            if (propertyName.equals(getDataObjectMetaDataService().getTitleAttribute(objectClass))) {
228                inquiryObjectClass = objectClass;
229            } else if (ObjectUtils.isNestedAttribute(propertyName)) {
230                String nestedPropertyName = ObjectUtils.getNestedAttributePrefix(propertyName);
231                Object nestedPropertyObject = ObjectUtils.getNestedValue(dataObject, nestedPropertyName);
232    
233                if (ObjectUtils.isNotNull(nestedPropertyObject)) {
234                    String nestedPropertyPrimitive = ObjectUtils.getNestedAttributePrimitive(propertyName);
235                    Class<?> nestedPropertyObjectClass = ObjectUtils.materializeClassForProxiedObject(nestedPropertyObject);
236    
237                    if (nestedPropertyPrimitive.equals(getDataObjectMetaDataService().getTitleAttribute(
238                            nestedPropertyObjectClass))) {
239                        inquiryObjectClass = nestedPropertyObjectClass;
240                    }
241                }
242            }
243    
244            // if not title, then get primary relationship
245            DataObjectRelationship relationship = null;
246            if (inquiryObjectClass == null) {
247                relationship = getDataObjectMetaDataService().getDataObjectRelationship(dataObject, objectClass,
248                        propertyName, "", true, false, true);
249                if (relationship != null) {
250                    inquiryObjectClass = relationship.getRelatedClass();
251                }
252            }
253    
254            // if haven't found inquiry class, then no inquiry can be rendered
255            if (inquiryObjectClass == null) {
256                inquiry.setRender(false);
257    
258                return;
259            }
260    
261            if (DocumentHeader.class.isAssignableFrom(inquiryObjectClass)) {
262                String documentNumber = (String) ObjectUtils.getPropertyValue(dataObject, propertyName);
263                if (StringUtils.isNotBlank(documentNumber)) {
264                    inquiry.getInquiryLinkField().setHrefText(getConfigurationService().getPropertyValueAsString(
265                            KRADConstants.WORKFLOW_URL_KEY)
266                            + KRADConstants.DOCHANDLER_DO_URL
267                            + documentNumber
268                            + KRADConstants.DOCHANDLER_URL_CHUNK);
269                    inquiry.getInquiryLinkField().setLinkLabel(documentNumber);
270                    inquiry.setRender(true);
271                }
272    
273                return;
274            }
275    
276            synchronized (SUPER_CLASS_TRANSLATOR_LIST) {
277                for (Class<?> clazz : SUPER_CLASS_TRANSLATOR_LIST) {
278                    if (clazz.isAssignableFrom(inquiryObjectClass)) {
279                        inquiryObjectClass = clazz;
280                        break;
281                    }
282                }
283            }
284    
285            if (!inquiryObjectClass.isInterface() && ExternalizableBusinessObject.class.isAssignableFrom(
286                    inquiryObjectClass)) {
287                inquiryObjectClass = ExternalizableBusinessObjectUtils.determineExternalizableBusinessObjectSubInterface(
288                        inquiryObjectClass);
289            }
290    
291            // listPrimaryKeyFieldNames returns an unmodifiable list. So a copy is necessary.
292            List<String> keys = new ArrayList<String>(getDataObjectMetaDataService().listPrimaryKeyFieldNames(
293                    inquiryObjectClass));
294    
295            if (keys == null) {
296                keys = Collections.emptyList();
297            }
298    
299            // build inquiry parameter mappings
300            Map<String, String> inquiryParameters = new HashMap<String, String>();
301            for (String keyName : keys) {
302                String keyConversion = keyName;
303                if (relationship != null) {
304                    keyConversion = relationship.getParentAttributeForChildAttribute(keyName);
305                } else if (ObjectUtils.isNestedAttribute(propertyName)) {
306                    String nestedAttributePrefix = ObjectUtils.getNestedAttributePrefix(propertyName);
307                    keyConversion = nestedAttributePrefix + "." + keyName;
308                }
309    
310                inquiryParameters.put(keyConversion, keyName);
311            }
312    
313            inquiry.buildInquiryLink(dataObject, propertyName, inquiryObjectClass, inquiryParameters);
314        }
315    
316        /**
317         * @see Inquirable#setDataObjectClass(java.lang.Class)
318         */
319        @Override
320        public void setDataObjectClass(Class<?> dataObjectClass) {
321            this.dataObjectClass = dataObjectClass;
322        }
323    
324        /**
325         * Retrieves the data object class configured for this inquirable
326         *
327         * @return Class<?> of configured data object, or null if data object class not configured
328         */
329        protected Class<?> getDataObjectClass() {
330            return this.dataObjectClass;
331        }
332    
333        protected ConfigurationService getConfigurationService() {
334            return KRADServiceLocator.getKualiConfigurationService();
335        }
336    
337        protected DataObjectMetaDataService getDataObjectMetaDataService() {
338            return KRADServiceLocatorWeb.getDataObjectMetaDataService();
339        }
340    
341        protected KualiModuleService getKualiModuleService() {
342            return KRADServiceLocatorWeb.getKualiModuleService();
343        }
344    
345        protected DataDictionaryService getDataDictionaryService() {
346            return KRADServiceLocatorWeb.getDataDictionaryService();
347        }
348    
349        protected DataObjectAuthorizationService getDataObjectAuthorizationService() {
350            return KRADServiceLocatorWeb.getDataObjectAuthorizationService();
351        }
352    
353        protected EncryptionService getEncryptionService() {
354            return CoreApiServiceLocator.getEncryptionService();
355        }
356    
357        protected BusinessObjectService getBusinessObjectService() {
358            return KRADServiceLocator.getBusinessObjectService();
359        }
360    
361    }