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