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.kns.lookup;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.core.api.CoreApiServiceLocator;
021import org.kuali.rice.core.api.encryption.EncryptionService;
022import org.kuali.rice.core.api.search.SearchOperator;
023import org.kuali.rice.krad.bo.BusinessObject;
024import org.kuali.rice.krad.bo.ExternalizableBusinessObject;
025import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
026import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
027import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
028import org.kuali.rice.krad.service.ModuleService;
029import org.kuali.rice.krad.util.BeanPropertyComparator;
030import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
031import org.kuali.rice.krad.util.KRADConstants;
032import org.kuali.rice.krad.util.ObjectUtils;
033import org.springframework.transaction.annotation.Transactional;
034
035import java.security.GeneralSecurityException;
036import java.util.ArrayList;
037import java.util.Collections;
038import java.util.HashMap;
039import java.util.HashSet;
040import java.util.Iterator;
041import java.util.List;
042import java.util.Map;
043import java.util.Set;
044
045/**
046 * @deprecated Use {@link org.kuali.rice.krad.lookup.LookupableImpl}.
047 */
048@Deprecated
049@Transactional
050public class KualiLookupableHelperServiceImpl extends AbstractLookupableHelperServiceImpl {
051
052    protected static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(KualiLookupableHelperServiceImpl.class);
053    protected boolean searchUsingOnlyPrimaryKeyValues = false;
054
055
056    /**
057     * Uses Lookup Service to provide a basic search.
058     *
059     * @param fieldValues - Map containing prop name keys and search values
060     *
061     * @return List found business objects
062     * @see LookupableHelperService#getSearchResults(java.util.Map)
063     */
064    public List<? extends BusinessObject> getSearchResults(Map<String, String> fieldValues) {
065        return getSearchResultsHelper(
066                org.kuali.rice.krad.lookup.LookupUtils.forceUppercase(getBusinessObjectClass(), fieldValues), false);
067    }
068
069
070    /**
071     * Uses Lookup Service to provide a basic unbounded search.
072     *
073     * @param fieldValues - Map containing prop name keys and search values
074     *
075     * @return List found business objects
076     * @see LookupableHelperService#getSearchResultsUnbounded(java.util.Map)
077     */
078    public List<? extends BusinessObject> getSearchResultsUnbounded(Map<String, String> fieldValues) {
079        return getSearchResultsHelper(
080                org.kuali.rice.krad.lookup.LookupUtils.forceUppercase(getBusinessObjectClass(), fieldValues), true);
081    }
082
083    // TODO: Fix? - this does not handle nested properties within the EBO.
084
085    /**
086     * Check whether the given property represents a property within an EBO starting
087     * with the sampleBo object given.  This is used to determine if a criteria needs
088     * to be applied to the EBO first, before sending to the normal lookup DAO.
089     */
090    protected boolean isExternalBusinessObjectProperty(Object sampleBo, String propertyName) {
091        try {
092                if ( propertyName.indexOf( "." ) > 0 && !StringUtils.contains( propertyName, "add." ) ) {
093                        Class propertyClass = PropertyUtils.getPropertyType(
094                                                sampleBo, StringUtils.substringBeforeLast( propertyName, "." ) );
095                        if ( propertyClass != null ) {
096                                return ExternalizableBusinessObjectUtils.isExternalizableBusinessObjectInterface( propertyClass );
097                        } else {
098                                if ( LOG.isDebugEnabled() ) {
099                                        LOG.debug( "unable to get class for " + StringUtils.substringBeforeLast( propertyName, "." ) + " on " + sampleBo.getClass().getName() );
100                                }
101                        }
102                }
103        } catch (Exception e) {
104                LOG.debug("Unable to determine type of property for " + sampleBo.getClass().getName() + "/" + propertyName, e );
105        }
106        return false;
107    }
108
109    /**
110     * Get the name of the property which represents the ExternalizableBusinessObject for the given property.
111     *
112     * This method can not handle nested properties within the EBO.
113     *
114     * Returns null if the property is not a nested property or is part of an add line.
115     */
116    protected String getExternalBusinessObjectProperty(Object sampleBo, String propertyName) {
117        if ( propertyName.indexOf( "." ) > 0 && !StringUtils.contains( propertyName, "add." ) ) {
118                return StringUtils.substringBeforeLast( propertyName, "." );
119        }
120        return null;
121    }
122
123    /**
124     * Checks whether any of the fieldValues being passed refer to a property within an ExternalizableBusinessObject.
125     */
126    protected boolean hasExternalBusinessObjectProperty(Class boClass, Map<String,String> fieldValues ) {
127        try {
128                Object sampleBo = boClass.newInstance();
129                for ( String key : fieldValues.keySet() ) {
130                        if ( isExternalBusinessObjectProperty( sampleBo, key )) {
131                                return true;
132                        }
133                }
134        } catch ( Exception ex ) {
135                LOG.debug("Unable to check " + boClass + " for EBO properties.", ex );
136        }
137        return false;
138    }
139
140    /**
141     * Returns a map stripped of any properties which refer to ExternalizableBusinessObjects.  These values may not be passed into the
142     * lookup service, since the objects they refer to are not in the local database.
143     */
144    protected Map<String,String> removeExternalizableBusinessObjectFieldValues(Class boClass, Map<String,String> fieldValues ) {
145        Map<String,String> eboFieldValues = new HashMap<String,String>();
146        try {
147                Object sampleBo = boClass.newInstance();
148                for ( String key : fieldValues.keySet() ) {
149                        if ( !isExternalBusinessObjectProperty( sampleBo, key )) {
150                                eboFieldValues.put( key, fieldValues.get( key ) );
151                        }
152                }
153        } catch ( Exception ex ) {
154                LOG.debug("Unable to check " + boClass + " for EBO properties.", ex );
155        }
156        return eboFieldValues;
157    }
158
159    /**
160     * Return the EBO fieldValue entries explicitly for the given eboPropertyName.  (I.e., any properties with the given
161     * property name as a prefix.
162     */
163    protected Map<String,String> getExternalizableBusinessObjectFieldValues(String eboPropertyName, Map<String,String> fieldValues ) {
164        Map<String,String> eboFieldValues = new HashMap<String,String>();
165        for ( String key : fieldValues.keySet() ) {
166                if ( key.startsWith( eboPropertyName + "." ) ) {
167                        eboFieldValues.put( StringUtils.substringAfterLast( key, "." ), fieldValues.get( key ) );
168                }
169        }
170        return eboFieldValues;
171    }
172
173    /**
174     * Get the complete list of all properties referenced in the fieldValues that are ExternalizableBusinessObjects.
175     *
176     * This is a list of the EBO object references themselves, not of the properties within them.
177     */
178    protected List<String> getExternalizableBusinessObjectProperties(Class boClass, Map<String,String> fieldValues ) {
179        Set<String> eboPropertyNames = new HashSet<String>();
180        try {
181                Object sampleBo = boClass.newInstance();
182                for ( String key : fieldValues.keySet() ) {
183                        if ( isExternalBusinessObjectProperty( sampleBo, key )) {
184                                eboPropertyNames.add( StringUtils.substringBeforeLast( key, "." ) );
185                        }
186                }
187        } catch ( Exception ex ) {
188                LOG.debug("Unable to check " + boClass + " for EBO properties.", ex );
189        }
190        return new ArrayList<String>(eboPropertyNames);
191    }
192
193    /**
194     * Given an property on the main BO class, return the defined type of the ExternalizableBusinessObject.  This will be used
195     * by other code to determine the correct module service to call for the lookup.
196     *
197     * @param boClass
198     * @param propertyName
199     * @return
200     */
201    protected Class<? extends ExternalizableBusinessObject> getExternalizableBusinessObjectClass(Class boClass, String propertyName) {
202        try {
203                return (Class<? extends ExternalizableBusinessObject>)PropertyUtils.getPropertyType(
204                                        boClass.newInstance(), StringUtils.substringBeforeLast( propertyName, "." ) );
205        } catch (Exception e) {
206                LOG.debug("Unable to determine type of property for " + boClass.getName() + "/" + propertyName, e );
207        }
208        return null;
209    }
210
211    /**
212     *
213     * This method does the actual search, with the parameters specified, and returns the result.
214     *
215     * NOTE that it will not do any upper-casing based on the DD forceUppercase. That is handled through an external call to
216     * LookupUtils.forceUppercase().
217     *
218     * @param fieldValues A Map of the fieldNames and fieldValues to be searched on.
219     * @param unbounded Whether the results should be bounded or not to a certain max size.
220     * @return A List of search results.
221     *
222     */
223    protected List<? extends BusinessObject> getSearchResultsHelper(Map<String, String> fieldValues, boolean unbounded) {
224        // remove hidden fields
225        LookupUtils.removeHiddenCriteriaFields(getBusinessObjectClass(), fieldValues);
226
227        searchUsingOnlyPrimaryKeyValues = getLookupService().allPrimaryKeyValuesPresentAndNotWildcard(getBusinessObjectClass(), fieldValues);
228
229        setBackLocation(fieldValues.get(KRADConstants.BACK_LOCATION));
230        setDocFormKey(fieldValues.get(KRADConstants.DOC_FORM_KEY));
231        setReferencesToRefresh(fieldValues.get(KRADConstants.REFERENCES_TO_REFRESH));
232        List searchResults;
233        Map<String,String> nonBlankFieldValues = new HashMap<String, String>();
234        for (String fieldName : fieldValues.keySet()) {
235                String fieldValue = fieldValues.get(fieldName);
236                if (StringUtils.isNotBlank(fieldValue) ) {
237                        if (fieldValue.endsWith(EncryptionService.ENCRYPTION_POST_PREFIX)) {
238                                String encryptedValue = StringUtils.removeEnd(fieldValue, EncryptionService.ENCRYPTION_POST_PREFIX);
239                                try {
240                        if(CoreApiServiceLocator.getEncryptionService().isEnabled()) {
241                                            fieldValue = getEncryptionService().decrypt(encryptedValue);
242                        }
243                                }
244                                catch (GeneralSecurityException e) {
245                                LOG.error("Error decrypting value for business object " + getBusinessObjectService() + " attribute " + fieldName, e);
246                                throw new RuntimeException("Error decrypting value for business object " + getBusinessObjectService() + " attribute " + fieldName, e);
247                        }
248                        }
249                        nonBlankFieldValues.put(fieldName, fieldValue);
250                }
251        }
252
253        // If this class is an EBO, just call the module service to get the results
254        if ( ExternalizableBusinessObjectUtils.isExternalizableBusinessObject( getBusinessObjectClass() ) ) {
255                ModuleService eboModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService( getBusinessObjectClass() );
256                BusinessObjectEntry ddEntry = eboModuleService.getExternalizableBusinessObjectDictionaryEntry(getBusinessObjectClass());
257                Map<String,String> filteredFieldValues = new HashMap<String, String>();
258                for (String fieldName : nonBlankFieldValues.keySet()) {
259                        if (ddEntry.getAttributeNames().contains(fieldName)) {
260                                filteredFieldValues.put(fieldName, nonBlankFieldValues.get(fieldName));
261                        }
262                }
263                searchResults = eboModuleService.getExternalizableBusinessObjectsListForLookup(getBusinessObjectClass(), (Map)filteredFieldValues, unbounded);
264        // if any of the properties refer to an embedded EBO, call the EBO lookups first and apply to the local lookup
265        } else if ( hasExternalBusinessObjectProperty( getBusinessObjectClass(), nonBlankFieldValues ) ) {
266                if ( LOG.isDebugEnabled() ) {
267                        LOG.debug( "has EBO reference: " + getBusinessObjectClass() );
268                        LOG.debug( "properties: " + nonBlankFieldValues );
269                }
270                // remove the EBO criteria
271                Map<String,String> nonEboFieldValues = removeExternalizableBusinessObjectFieldValues( getBusinessObjectClass(), nonBlankFieldValues );
272                if ( LOG.isDebugEnabled() ) {
273                        LOG.debug( "Non EBO properties removed: " + nonEboFieldValues );
274                }
275                // get the list of EBO properties attached to this object
276                List<String> eboPropertyNames = getExternalizableBusinessObjectProperties( getBusinessObjectClass(), nonBlankFieldValues );
277                if ( LOG.isDebugEnabled() ) {
278                        LOG.debug( "EBO properties: " + eboPropertyNames );
279                }
280                // loop over those properties
281                for ( String eboPropertyName : eboPropertyNames ) {
282                        // extract the properties as known to the EBO
283                        Map<String,String> eboFieldValues = getExternalizableBusinessObjectFieldValues( eboPropertyName, nonBlankFieldValues );
284                if ( LOG.isDebugEnabled() ) {
285                        LOG.debug( "EBO properties for master EBO property: " + eboPropertyName );
286                        LOG.debug( "properties: " + eboFieldValues );
287                }
288                // run search against attached EBO's module service
289                ModuleService eboModuleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService( getExternalizableBusinessObjectClass( getBusinessObjectClass(), eboPropertyName) );
290                // KULRICE-4401 made eboResults an empty list and only filled if service is found.
291                        List eboResults = Collections.emptyList();
292                        if (eboModuleService != null) 
293                        {
294                                eboResults = eboModuleService.getExternalizableBusinessObjectsListForLookup( getExternalizableBusinessObjectClass( getBusinessObjectClass(), eboPropertyName), (Map)eboFieldValues, unbounded);
295                        }
296                                else
297                        {
298                                LOG.debug( "EBO ModuleService is null: " + eboPropertyName );
299                        }
300                        // get the mapping/relationship between the EBO object and it's parent object
301                        // use that to adjust the fieldValues
302
303                        // get the parent property type
304                        Class eboParentClass;
305                        String eboParentPropertyName;
306                        if ( ObjectUtils.isNestedAttribute( eboPropertyName ) ) {
307                                eboParentPropertyName = StringUtils.substringBeforeLast( eboPropertyName, "." );
308                                try {
309                                        eboParentClass = PropertyUtils.getPropertyType( getBusinessObjectClass().newInstance(), eboParentPropertyName );
310                                } catch ( Exception ex ) {
311                                        throw new RuntimeException( "Unable to create an instance of the business object class: " + getBusinessObjectClass().getName(), ex );
312                                }
313                        } else {
314                                eboParentClass = getBusinessObjectClass();
315                                eboParentPropertyName = null;
316                        }
317                        if ( LOG.isDebugEnabled() ) {
318                                LOG.debug( "determined EBO parent class/property name: " + eboParentClass + "/" + eboParentPropertyName );
319                        }
320                        // look that up in the DD (BOMDS)
321                        // find the appropriate relationship
322                        // CHECK THIS: what if eboPropertyName is a nested attribute - need to strip off the eboParentPropertyName if not null
323                        RelationshipDefinition rd = getBusinessObjectMetaDataService().getBusinessObjectRelationshipDefinition( eboParentClass, eboPropertyName );
324                        if ( LOG.isDebugEnabled() ) {
325                                LOG.debug( "Obtained RelationshipDefinition for " + eboPropertyName );
326                                LOG.debug( rd );
327                        }
328
329                        // copy the needed properties (primary only) to the field values
330                        // KULRICE-4446 do so only if the relationship definition exists
331                        // NOTE: this will work only for single-field PK unless the ORM layer is directly involved
332                        // (can't make (field1,field2) in ( (v1,v2),(v3,v4) ) style queries in the lookup framework
333                        if ( ObjectUtils.isNotNull(rd)) {
334                                if ( rd.getPrimitiveAttributes().size() > 1 ) {
335                                        throw new RuntimeException( "EBO Links don't work for relationships with multiple-field primary keys." );
336                                }
337                                String boProperty = rd.getPrimitiveAttributes().get( 0 ).getSourceName();
338                                String eboProperty = rd.getPrimitiveAttributes().get( 0 ).getTargetName();
339                                StringBuffer boPropertyValue = new StringBuffer();
340                                // loop over the results, making a string that the lookup DAO will convert into an
341                                // SQL "IN" clause
342                                for ( Object ebo : eboResults ) {
343                                        if ( boPropertyValue.length() != 0 ) {
344                                                boPropertyValue.append( SearchOperator.OR.op() );
345                                        }
346                                        try {
347                                                boPropertyValue.append( PropertyUtils.getProperty( ebo, eboProperty ).toString() );
348                                        } catch ( Exception ex ) {
349                                                LOG.warn( "Unable to get value for " + eboProperty + " on " + ebo );
350                                        }
351                                }
352                                if ( eboParentPropertyName == null ) {
353                                        // non-nested property containing the EBO
354                                        nonEboFieldValues.put( boProperty, boPropertyValue.toString() );
355                                } else {
356                                        // property nested within the main searched-for BO that contains the EBO
357                                        nonEboFieldValues.put( eboParentPropertyName + "." + boProperty, boPropertyValue.toString() );
358                                }
359                        }
360                }
361                if ( LOG.isDebugEnabled() ) {
362                        LOG.debug( "Passing these results into the lookup service: " + nonEboFieldValues );
363                }
364                // add those results as criteria
365                // run the normal search (but with the EBO critieria added)
366                searchResults = (List) getLookupService().findCollectionBySearchHelper(getBusinessObjectClass(), nonEboFieldValues, unbounded);
367        } else {
368            searchResults = (List) getLookupService().findCollectionBySearchHelper(getBusinessObjectClass(), nonBlankFieldValues, unbounded);
369        }
370        
371        if (searchResults == null) {
372                searchResults = new ArrayList();
373        }
374
375        // sort list if default sort column given
376        List defaultSortColumns = getDefaultSortColumns();
377        if (defaultSortColumns.size() > 0) {
378            Collections.sort(searchResults, new BeanPropertyComparator(defaultSortColumns, true));
379        }
380        return searchResults;
381    }
382
383
384    /**
385     * @see LookupableHelperService#isSearchUsingOnlyPrimaryKeyValues()
386     */
387    @Override
388    public boolean isSearchUsingOnlyPrimaryKeyValues() {
389        return searchUsingOnlyPrimaryKeyValues;
390}
391
392
393    /**
394     * Returns a comma delimited list of primary key field labels, to be used on the UI to tell the user which fields were used to search
395     *
396     * These labels are generated from the DD definitions for the lookup fields
397     *
398     * @return a comma separated list of field attribute names.  If no fields found, returns "N/A"
399     * @see LookupableHelperService#isSearchUsingOnlyPrimaryKeyValues()
400     * @see LookupableHelperService#getPrimaryKeyFieldLabels()
401     */
402    @Override
403    public String getPrimaryKeyFieldLabels() {
404        StringBuilder buf = new StringBuilder();
405        List<String> primaryKeyFieldNames = KRADServiceLocatorWeb.getLegacyDataAdapter().listPrimaryKeyFieldNames(getBusinessObjectClass());
406        Iterator<String> pkIter = primaryKeyFieldNames.iterator();
407        while (pkIter.hasNext()) {
408            String pkFieldName = (String) pkIter.next();
409            buf.append(getDataDictionaryService().getAttributeLabel(getBusinessObjectClass(), pkFieldName));
410            if (pkIter.hasNext()) {
411                buf.append(", ");
412            }
413        }
414        return buf.length() == 0 ? KRADConstants.NOT_AVAILABLE_STRING : buf.toString();
415    }
416
417
418}
419