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.service.impl;
017
018import java.beans.PropertyDescriptor;
019import java.lang.reflect.InvocationTargetException;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Date;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028import java.util.TreeMap;
029
030import org.apache.commons.beanutils.PropertyUtils;
031import org.apache.commons.lang.StringUtils;
032import org.kuali.rice.core.api.config.property.ConfigurationService;
033import org.kuali.rice.core.api.criteria.OrderByField;
034import org.kuali.rice.core.api.criteria.OrderDirection;
035import org.kuali.rice.core.api.criteria.QueryByCriteria;
036import org.kuali.rice.core.api.criteria.QueryResults;
037import org.kuali.rice.core.api.mo.common.Versioned;
038import org.kuali.rice.core.api.search.SearchOperator;
039import org.kuali.rice.core.api.uif.RemotableQuickFinder;
040import org.kuali.rice.core.api.util.RiceKeyConstants;
041import org.kuali.rice.core.framework.persistence.ojb.conversion.OjbCharBooleanConversion;
042import org.kuali.rice.krad.bo.BusinessObject;
043import org.kuali.rice.krad.bo.InactivatableFromTo;
044import org.kuali.rice.krad.bo.PersistableBusinessObjectExtension;
045import org.kuali.rice.krad.data.CompoundKey;
046import org.kuali.rice.krad.data.DataObjectService;
047import org.kuali.rice.krad.data.DataObjectWrapper;
048import org.kuali.rice.krad.data.KradDataServiceLocator;
049import org.kuali.rice.krad.data.PersistenceOption;
050import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship;
051import org.kuali.rice.krad.data.metadata.DataObjectCollection;
052import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
053import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
054import org.kuali.rice.krad.data.provider.annotation.ExtensionFor;
055import org.kuali.rice.krad.datadictionary.DataDictionaryEntry;
056import org.kuali.rice.krad.datadictionary.DataObjectEntry;
057import org.kuali.rice.krad.datadictionary.PrimitiveAttributeDefinition;
058import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
059import org.kuali.rice.krad.datadictionary.SupportAttributeDefinition;
060import org.kuali.rice.krad.document.Document;
061import org.kuali.rice.krad.exception.ValidationException;
062import org.kuali.rice.krad.lookup.LookupUtils;
063import org.kuali.rice.krad.service.DataDictionaryService;
064import org.kuali.rice.krad.service.DocumentAdHocService;
065import org.kuali.rice.krad.service.KRADServiceLocator;
066import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
067import org.kuali.rice.krad.service.KualiModuleService;
068import org.kuali.rice.krad.service.LegacyDataAdapter;
069import org.kuali.rice.krad.service.ModuleService;
070import org.kuali.rice.krad.uif.service.ViewDictionaryService;
071import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
072import org.kuali.rice.krad.util.ForeignKeyFieldsPopulationState;
073import org.kuali.rice.krad.util.GlobalVariables;
074import org.kuali.rice.krad.util.KRADConstants;
075import org.kuali.rice.krad.util.KRADPropertyConstants;
076import org.kuali.rice.krad.util.KRADUtils;
077import org.kuali.rice.krad.util.LegacyUtils;
078import org.springframework.beans.PropertyAccessorUtils;
079import org.springframework.beans.factory.annotation.Required;
080import org.springframework.dao.IncorrectResultSizeDataAccessException;
081
082/**
083 *
084 */
085public class KRADLegacyDataAdapterImpl implements LegacyDataAdapter {
086    private DataObjectService dataObjectService;
087    private LookupCriteriaGenerator lookupCriteriaGenerator;
088
089    private ConfigurationService kualiConfigurationService;
090    private KualiModuleService kualiModuleService;
091    private DataDictionaryService dataDictionaryService;
092    private ViewDictionaryService viewDictionaryService;
093
094    @Override
095    public <T> T save(T dataObject) {
096        if (dataObject instanceof Collection) {
097            Collection<Object> newList = new ArrayList<Object>(((Collection) dataObject).size());
098            for (Object obj : (Collection<?>) dataObject) {
099                newList.add(save(obj));
100            }
101            return (T) newList;
102        } else {
103            return dataObjectService.save(dataObject);
104        }
105    }
106
107    @Override
108    public <T> T linkAndSave(T dataObject) {
109        // This method is only used from MaintainableImpl
110        return dataObjectService.save(dataObject, PersistenceOption.LINK_KEYS);
111    }
112
113    @Override
114    public <T> T saveDocument(T document) {
115        return dataObjectService.save(document, PersistenceOption.LINK_KEYS, PersistenceOption.FLUSH);
116    }
117
118    @Override
119    public <T> T findByPrimaryKey(Class<T> clazz, Map<String, ?> primaryKeys) {
120        return dataObjectService.find(clazz, new CompoundKey(primaryKeys));
121    }
122
123    @Override
124    public <T> T findBySinglePrimaryKey(Class<T> clazz, Object primaryKey) {
125        return dataObjectService.find(clazz, primaryKey);
126    }
127
128    @Override
129    public void delete(Object dataObject) {
130        if (dataObject instanceof Collection) {
131            for (Object dobj : (Collection) dataObject) {
132                delete(dobj);
133            }
134        } else {
135            dataObjectService.delete(dataObject);
136        }
137    }
138
139    @Override
140    public void deleteMatching(Class<?> type, Map<String, ?> fieldValues) {
141        dataObjectService.deleteMatching(type, QueryByCriteria.Builder.andAttributes(fieldValues).build());
142    }
143
144    @Override
145    public <T> T retrieve(T dataObject) {
146        Object id = null;
147        Map<String, Object> primaryKeyValues = dataObjectService.wrap(dataObject).getPrimaryKeyValues();
148        if (primaryKeyValues.isEmpty()) {
149            throw new IllegalArgumentException("Given data object has no primary key!");
150        }
151        if (primaryKeyValues.size() == 1) {
152            id = primaryKeyValues.values().iterator().next();
153        } else {
154            id = new CompoundKey(primaryKeyValues);
155        }
156        return dataObjectService.find((Class<T>) dataObject.getClass(), id);
157    }
158
159    @Override
160    public <T> Collection<T> findAll(Class<T> clazz) {
161        // just find all objects of given type without any attribute criteria
162        return findMatching(clazz, Collections.<String, Object>emptyMap());
163    }
164
165    @Override
166    public <T> Collection<T> findMatching(Class<T> clazz, Map<String, ?> fieldValues) {
167        QueryResults<T> result = dataObjectService.findMatching(clazz, QueryByCriteria.Builder.andAttributes(
168                fieldValues).build());
169        return result.getResults();
170    }
171
172    @Override
173    public <T> Collection<T> findMatchingOrderBy(Class<T> clazz, Map<String, ?> fieldValues, String sortField,
174            boolean sortAscending) {
175        OrderDirection direction = sortAscending ? OrderDirection.ASCENDING : OrderDirection.DESCENDING;
176        OrderByField orderBy = OrderByField.Builder.create(sortField, direction).build();
177        QueryResults<T> result = dataObjectService.findMatching(clazz, QueryByCriteria.Builder.andAttributes(
178                fieldValues).setOrderByFields(orderBy).build());
179        return result.getResults();
180    }
181
182    @Override
183    public Map<String, ?> getPrimaryKeyFieldValues(Object dataObject) {
184        return dataObjectService.wrap(dataObject).getPrimaryKeyValues();
185    }
186
187    @Override
188    public void retrieveNonKeyFields(Object persistableObject) {
189        List<DataObjectRelationship> relationships = dataObjectService.getMetadataRepository().getMetadata(
190                persistableObject.getClass()).getRelationships();
191        for (DataObjectRelationship relationship : relationships) {
192            retrieveReferenceObject(persistableObject, relationship.getName());
193        }
194    }
195
196    @Override
197    public void retrieveReferenceObject(Object persistableObject, String referenceObjectName) {
198        dataObjectService.wrap(persistableObject).fetchRelationship(referenceObjectName);
199    }
200
201    @Override
202    public void refreshAllNonUpdatingReferences(Object persistableObject) {
203        List<DataObjectRelationship> nonUpdateableRelationships = findNonUpdateableRelationships(persistableObject);
204        for (DataObjectRelationship relationship : nonUpdateableRelationships) {
205            retrieveReferenceObject(persistableObject, relationship.getName());
206        }
207    }
208
209    protected List<DataObjectRelationship> findNonUpdateableRelationships(Object persistableObject) {
210        List<DataObjectRelationship> nonUpdateableRelationships = new ArrayList<DataObjectRelationship>();
211        DataObjectMetadata dataObjectMetadata = dataObjectService.getMetadataRepository().
212                getMetadata(persistableObject.getClass());
213        if (dataObjectMetadata != null) {
214            List<DataObjectRelationship> relationships = dataObjectMetadata.getRelationships();
215            for (DataObjectRelationship relationship : relationships) {
216                if (!relationship.isSavedWithParent()) {
217                    nonUpdateableRelationships.add(relationship);
218                }
219            }
220        }
221        return nonUpdateableRelationships;
222    }
223
224    @Override
225    public boolean isProxied(Object object) {
226        // KRAD data adapter does nothing
227        return false;
228    }
229
230    @Override
231    public Object resolveProxy(Object o) {
232        // KRAD data adapter does nothing
233        return o;
234    }
235
236    // Lookup methods
237
238    @Override
239    public <T> Collection<T> findCollectionBySearchHelper(Class<T> dataObjectClass, Map<String, String> formProperties,
240            boolean unbounded, boolean allPrimaryKeyValuesPresentAndNotWildcard, Integer searchResultsLimit) {
241        return performDataObjectServiceLookup(dataObjectClass, formProperties, unbounded,
242                allPrimaryKeyValuesPresentAndNotWildcard, searchResultsLimit);
243    }
244
245    @Override
246    public <T> Collection<T> findCollectionBySearchHelper(Class<T> dataObjectClass, Map<String, String> formProperties,
247            List<String> wildcardAsLiteralPropertyNames, boolean unbounded,
248            boolean allPrimaryKeyValuesPresentAndNotWildcard, Integer searchResultsLimit) {
249        return performDataObjectServiceLookup(dataObjectClass, formProperties, wildcardAsLiteralPropertyNames,
250                unbounded, allPrimaryKeyValuesPresentAndNotWildcard, searchResultsLimit);
251    }
252
253    /**
254     * Our new DataObjectService-based lookup implementation
255     *
256     * @param dataObjectClass the dataobject class
257     * @param formProperties the incoming lookup form properties
258     * @param unbounded whether the search is unbounded
259     * @param searchResultsLimit the searchResultsLimit; null implies use of default KNS value if set for the class
260     * @param <T> the data object type
261     * @return collection of lookup results
262     */
263    protected <T> Collection<T> performDataObjectServiceLookup(Class<T> dataObjectClass,
264            Map<String, String> formProperties, boolean unbounded, boolean allPrimaryKeyValuesPresentAndNotWildcard,
265            Integer searchResultsLimit) {
266        if (!unbounded && searchResultsLimit == null) {
267            // use KRAD LookupUtils.getSearchResultsLimit instead of KNS version. we have no LookupForm, so pass null, only the class will be used
268            //searchResultsLimit = LookupUtils.getSearchResultsLimit(example, null);
269            searchResultsLimit = org.kuali.rice.kns.lookup.LookupUtils.getSearchResultsLimit(dataObjectClass);
270        }
271        QueryByCriteria.Builder query = lookupCriteriaGenerator.generateCriteria(dataObjectClass, formProperties,
272                allPrimaryKeyValuesPresentAndNotWildcard);
273        if (!unbounded && searchResultsLimit != null) {
274            query.setMaxResults(searchResultsLimit);
275        }
276
277        Collection<T> results = dataObjectService.findMatching(dataObjectClass, query.build()).getResults();
278        return filterCurrentDataObjects(dataObjectClass, results, formProperties);
279    }
280
281    /**
282     * Our newer DataObjectService-based lookup implementation
283     *
284     * @param dataObjectClass the dataobject class
285     * @param formProperties the incoming lookup form properties
286     * @param wildcardAsLiteralPropertyNames list of the lookup properties with wildcard characters disabled
287     * @param unbounded whether the search is unbounded
288     * @param searchResultsLimit the searchResultsLimit; null implies use of default KNS value if set for the class
289     * @param <T> the data object type
290     * @return collection of lookup results
291     */
292    protected <T> Collection<T> performDataObjectServiceLookup(Class<T> dataObjectClass,
293            Map<String, String> formProperties, List<String> wildcardAsLiteralPropertyNames, boolean unbounded,
294            boolean allPrimaryKeyValuesPresentAndNotWildcard, Integer searchResultsLimit) {
295        if (!unbounded && searchResultsLimit == null) {
296            // use KRAD LookupUtils.getSearchResultsLimit instead of KNS version. we have no LookupForm, so pass null, only the class will be used
297            //searchResultsLimit = LookupUtils.getSearchResultsLimit(example, null);
298            searchResultsLimit = org.kuali.rice.kns.lookup.LookupUtils.getSearchResultsLimit(dataObjectClass);
299        }
300
301        QueryByCriteria.Builder query = lookupCriteriaGenerator.generateCriteria(dataObjectClass, formProperties,
302                wildcardAsLiteralPropertyNames, allPrimaryKeyValuesPresentAndNotWildcard);
303        if (!unbounded && searchResultsLimit != null) {
304            query.setMaxResults(searchResultsLimit);
305        }
306
307        Collection<T> results = dataObjectService.findMatching(dataObjectClass, query.build()).getResults();
308        return filterCurrentDataObjects(dataObjectClass, results, formProperties);
309    }
310
311    protected <T> Collection<T> filterCurrentDataObjects(Class<T> dataObjectClass, Collection<T> unfiltered,
312            Map<String, String> formProps) {
313        if (InactivatableFromTo.class.isAssignableFrom(dataObjectClass)) {
314            Boolean currentSpecifier = lookupCriteriaCurrentSpecifier(formProps);
315            if (currentSpecifier != null) {
316                List<InactivatableFromTo> onlyCurrent =
317                        KRADServiceLocator.getInactivateableFromToService().filterOutNonCurrent(new ArrayList(
318                                unfiltered), new Date(LookupUtils.getActiveDateTimestampForCriteria(formProps)
319                                .getTime()));
320                if (currentSpecifier) {
321                    return (Collection<T>) onlyCurrent;
322                } else {
323                    unfiltered.removeAll(onlyCurrent);
324                    return unfiltered;
325                }
326            }
327        }
328        return unfiltered;
329    }
330
331    protected Boolean lookupCriteriaCurrentSpecifier(Map<String, String> formProps) {
332        String value = formProps.get(KRADPropertyConstants.CURRENT);
333        if (StringUtils.isNotBlank(value)) {
334            // FIXME: use something more portable than this direct OJB converter
335            String currentBooleanStr = (String) new OjbCharBooleanConversion().javaToSql(value);
336            if (OjbCharBooleanConversion.DATABASE_BOOLEAN_TRUE_STRING_REPRESENTATION.equals(currentBooleanStr)) {
337                return Boolean.TRUE;
338            } else if (OjbCharBooleanConversion.DATABASE_BOOLEAN_FALSE_STRING_REPRESENTATION.equals(
339                    currentBooleanStr)) {
340                return Boolean.FALSE;
341            }
342        }
343        return null;
344    }
345
346    @Override
347    public <T> T findObjectBySearch(Class<T> type, Map<String, String> formProps) {
348        // This is the strictly Lookup-compatible way of constructing the criteria
349        // from a map of properties, which performs some minor logic such as only including
350        // non-blank and "writable" properties, as well as minor type coercions of string values
351        QueryByCriteria.Builder queryByCriteria = lookupCriteriaGenerator.createObjectCriteriaFromMap(type, formProps);
352        List<T> results = dataObjectService.findMatching(type, queryByCriteria.build()).getResults();
353        if (results.isEmpty()) {
354            return null;
355        }
356        if (results.size() != 1) {
357            // this behavior is different from the legacy OJB behavior in that it throws an exception if more than
358            // one result from such a single object query
359            throw new IncorrectResultSizeDataAccessException("Incorrect number of results returned when finding object",
360                    1, results.size());
361        }
362        return results.get(0);
363    }
364
365    /**
366     * Returns whether all primary key values are specified in the lookup  map and do not contain any wildcards
367     *
368     * @param boClass the bo class to lookup
369     * @param formProps the incoming form/lookup properties
370     * @return whether all primary key values are specified in the lookup  map and do not contain any wildcards
371     */
372    @Override
373    public boolean allPrimaryKeyValuesPresentAndNotWildcard(Class<?> boClass, Map<String, String> formProps) {
374        List<String> pkFields = listPrimaryKeyFieldNames(boClass);
375        Iterator<String> pkIter = pkFields.iterator();
376        boolean returnVal = true;
377        while (returnVal && pkIter.hasNext()) {
378            String pkName = pkIter.next();
379            String pkValue = formProps.get(pkName);
380
381            if (StringUtils.isBlank(pkValue)) {
382                returnVal = false;
383            } else {
384                for (SearchOperator op : SearchOperator.QUERY_CHARACTERS) {
385                    if (pkValue.contains(op.op())) {
386                        returnVal = false;
387                        break;
388                    }
389                }
390            }
391        }
392        return returnVal;
393    }
394
395    //    @Override
396    //    public Attachment getAttachmentByNoteId(Long noteId) {
397    //        // noteIdentifier is the PK of Attachment, so just look up by PK
398    //        return dataObjectService.find(Attachment.class, noteId);
399    //    }
400
401    @Override
402    public List<String> listPrimaryKeyFieldNames(Class<?> type) {
403        List<String> keys = Collections.emptyList();
404        if (dataObjectService.getMetadataRepository().contains(type)) {
405            keys = dataObjectService.getMetadataRepository().getMetadata(type).getPrimaryKeyAttributeNames();
406        } else {
407            // check the Data Dictionary for PK's of non-persisted objects
408            DataObjectEntry dataObjectEntry = dataDictionaryService.getDataDictionary().getDataObjectEntry(
409                    type.getName());
410            if (dataObjectEntry != null) {
411                List<String> pks = dataObjectEntry.getPrimaryKeys();
412                if (pks != null) {
413                    keys = pks;
414                }
415            } else {
416                ModuleService responsibleModuleService = kualiModuleService.getResponsibleModuleService(type);
417                if (responsibleModuleService != null && responsibleModuleService.isExternalizable(type)) {
418                    keys = responsibleModuleService.listPrimaryKeyFieldNames(type);
419                }
420            }
421        }
422        return keys;
423    }
424
425    /**
426     * LookupServiceImpl calls BusinessObjectMetaDataService to listPrimaryKeyFieldNames.
427     * The BusinessObjectMetaDataService goes beyond the PersistenceStructureService to consult
428     * the associated ModuleService in determining the primary key field names.
429     * TODO: Do we need both listPrimaryKeyFieldNames/persistenceStructureService and
430     * listPrimaryKeyFieldNamesConsultingAllServices/businesObjectMetaDataService or
431     * can the latter superset be used for the former?
432     *
433     * @param type the data object class
434     * @return list of primary key field names, consulting persistence structure service, module service and
435     * datadictionary
436     */
437    protected List<String> listPrimaryKeyFieldNamesConsultingAllServices(Class<?> type) {
438        List<String> keys = new ArrayList<String>();
439        if (dataObjectService.getMetadataRepository().contains(type)) {
440            keys = dataObjectService.getMetadataRepository().getMetadata(type).getPrimaryKeyAttributeNames();
441        }
442        return keys;
443    }
444
445    @Override
446    public Class<?> determineCollectionObjectType(Class<?> containingType, String collectionPropertyName) {
447        final Class<?> collectionObjectType;
448        if (dataObjectService.getMetadataRepository().contains(containingType)) {
449            DataObjectMetadata metadata = dataObjectService.getMetadataRepository().getMetadata(containingType);
450            DataObjectCollection collection = metadata.getCollection(collectionPropertyName);
451            if (collection == null) {
452                throw new IllegalArgumentException(
453                        "Failed to locate a collection property with the given name: " + collectionPropertyName);
454            }
455            collectionObjectType = collection.getRelatedType();
456        } else {
457            throw new IllegalArgumentException(
458                    "Given containing class is not a valid data object, no metadata could be located for "
459                            + containingType.getName());
460        }
461        return collectionObjectType;
462
463    }
464
465    @Override
466    public boolean hasReference(Class<?> boClass, String referenceName) {
467        throw new UnsupportedOperationException("hasReference not valid for KRAD data operation");
468    }
469
470    @Override
471    public boolean hasCollection(Class<?> boClass, String collectionName) {
472        throw new UnsupportedOperationException("hasCollection not valid for KRAD data operation");
473    }
474
475    @Override
476    public boolean isExtensionAttribute(Class<?> boClass, String attributePropertyName, Class<?> propertyType) {
477        DataObjectMetadata metadata = dataObjectService.getMetadataRepository().getMetadata(boClass);
478        if (metadata != null) {
479            DataObjectRelationship relationship = metadata.getRelationship(attributePropertyName);
480            if (relationship != null) {
481                Class<?> relatedType = relationship.getRelatedType();
482                // right now, the only way to tell if an attribute is an extension is to check this annotation, the
483                // metadata repository does not currently store any such info that we can glom onto
484                ExtensionFor annotation = relatedType.getAnnotation(ExtensionFor.class);
485                if (annotation != null) {
486                    return annotation.value().equals(boClass);
487                }
488            }
489        }
490        return false;
491    }
492
493    @Override
494    public Class<?> getExtensionAttributeClass(Class<?> boClass, String attributePropertyName) {
495        DataObjectMetadata metadata = dataObjectService.getMetadataRepository().getMetadata(boClass);
496        if (metadata != null) {
497            DataObjectRelationship relationship = metadata.getRelationship(attributePropertyName);
498            if (relationship != null) {
499                return relationship.getRelatedType();
500            }
501        }
502        return null;
503    }
504
505    @Override
506    public Map<String, ?> getPrimaryKeyFieldValuesDOMDS(Object dataObject) {
507        return dataObjectService.wrap(dataObject).getPrimaryKeyValues();
508    }
509
510    @Override
511    public boolean equalsByPrimaryKeys(Object do1, Object do2) {
512        return dataObjectService.wrap(do1).equalsByPrimaryKey(do2);
513    }
514
515//    @Override
516//    public PersistableBusinessObject toPersistableBusinessObject(Object object) {
517//        throw new UnsupportedOperationException("toPersistableBusinessObject not valid for KRAD data operation");
518//    }
519
520    @Override
521    public void materializeAllSubObjects(Object object) {
522        DataObjectWrapper<?> wrappedObject = dataObjectService.wrap(object);
523        
524        // Using 3 as that is what the KNS version of this method did
525        wrappedObject.materializeReferencedObjectsToDepth(3);
526    }
527
528    @Override
529    /**
530     * Recursively calls getPropertyTypeChild if nested property to allow it to properly look it up
531     */
532    public Class<?> getPropertyType(Object object, String propertyName) {
533        DataObjectWrapper<?> wrappedObject = dataObjectService.wrap(object);
534        if (PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName)) {
535            return wrappedObject.getPropertyTypeNullSafe(wrappedObject.getWrappedClass(), propertyName);
536        }
537        return wrappedObject.getPropertyType(propertyName);
538    }
539
540    @Override
541    public Object getExtension(
542            Class<?> businessObjectClass) throws InstantiationException, IllegalAccessException {
543        DataObjectMetadata metadata = dataObjectService.getMetadataRepository().getMetadata(businessObjectClass);
544        DataObjectRelationship extensionRelationship = metadata.getRelationship("extension");
545        if (extensionRelationship != null) {
546            Class<?> extensionType = extensionRelationship.getRelatedType();
547            return extensionType.newInstance();
548        }
549        return null;
550    }
551
552    @Override
553    public void refreshReferenceObject(Object businessObject, String referenceObjectName) {
554        dataObjectService.wrap(businessObject).fetchRelationship(referenceObjectName);
555    }
556
557    @Override
558    public boolean isLockable(Object object) {
559        return isPersistable(object.getClass());
560    }
561
562    @Override
563    public void verifyVersionNumber(Object dataObject) {
564        DataObjectMetadata metadata = dataObjectService.getMetadataRepository().getMetadata(dataObject.getClass());
565        if (metadata == null) {
566            return;
567        }
568
569        if (metadata.isSupportsOptimisticLocking()) {
570            if (dataObject instanceof Versioned) {
571                Map<String, ?> keyPropertyValues = dataObjectService.wrap(dataObject).getPrimaryKeyValues();
572                CompoundKey key = new CompoundKey(keyPropertyValues);
573                Object persistableDataObject = null;
574                if (!key.hasNullKeyValues()) {
575                    persistableDataObject = dataObjectService.find(dataObject.getClass(), key);
576                }
577                // if it's null that means that this is an insert, not an update
578                if (persistableDataObject != null) {
579                    Long databaseVersionNumber = ((Versioned) persistableDataObject).getVersionNumber();
580                    Long documentVersionNumber = ((Versioned) dataObject).getVersionNumber();
581                    if (databaseVersionNumber != null && !(databaseVersionNumber.equals(documentVersionNumber))) {
582                        GlobalVariables.getMessageMap().putError(KRADConstants.GLOBAL_ERRORS,
583                                RiceKeyConstants.ERROR_VERSION_MISMATCH);
584                        throw new ValidationException(
585                                "Version mismatch between the local business object and the database business object");
586                    }
587                }
588            }
589        }
590    }
591
592    @Override
593    public RemotableQuickFinder.Builder createQuickFinder(Class<?> containingClass, String attributeName) {
594        return createQuickFinderNew(containingClass, attributeName);
595    }
596
597    /**
598     * New implementation of createQuickFinder which uses the new dataObjectService.getMetadataRepository().
599     */
600    protected RemotableQuickFinder.Builder createQuickFinderNew(Class<?> containingClass, String attributeName) {
601        if (dataObjectService.getMetadataRepository().contains(containingClass)) {
602
603            String lookupClassName = null;
604            Map<String, String> fieldConversions = new HashMap<String, String>();
605            Map<String, String> lookupParameters = new HashMap<String, String>();
606
607            DataObjectMetadata metadata = dataObjectService.getMetadataRepository().getMetadata(containingClass);
608            DataObjectRelationship relationship = metadata.getRelationshipByLastAttributeInRelationship(attributeName);
609            if (relationship != null) {
610                DataObjectMetadata lookupClassMetadata = dataObjectService.getMetadataRepository().getMetadata(
611                        relationship.getRelatedType());
612                lookupClassName = lookupClassMetadata.getClass().getName();
613                for (DataObjectAttributeRelationship attributeRelationship : relationship.getAttributeRelationships()) {
614
615                    // for field conversions, we map from the child attribute name to the parent attribute name because
616                    // whenever the value is returned from the object being looked up (child in this case) we want to
617                    // map the result back to the corresponding attributes on the "parent" object
618                    fieldConversions.put(attributeRelationship.getChildAttributeName(),
619                            attributeRelationship.getParentAttributeName());
620
621                    // for lookup parameters, we need to map the other direction since we are passing parameters *from* our parent
622                    // object *to* the child object
623                    lookupParameters.put(attributeRelationship.getParentAttributeName(),
624                            attributeRelationship.getChildAttributeName());
625                }
626                // in the legacy implementation of this, if there was a "userVisibleIdentifierKey" defined on
627                // the relationship, it would only add the lookup parameter for that key
628                //
629                // In krad-data, we recognize that related objects have business keys and we use that information
630                // to alter the lookup parameters (only) to pass the business key field(s) to the lookup
631                if (lookupClassMetadata.hasDistinctBusinessKey()) {
632                    lookupParameters.clear();
633                    for (String businessKeyAttributeName : lookupClassMetadata.getBusinessKeyAttributeNames()) {
634                        lookupParameters.put(relationship.getName() + "." + businessKeyAttributeName,
635                                businessKeyAttributeName);
636                    }
637                }
638            } else {
639                // check for primary display attribute attribute and if match build lookup to target class using primary key fields
640                String primaryDisplayAttributeName = metadata.getPrimaryDisplayAttributeName();
641                if (StringUtils.equals(primaryDisplayAttributeName, attributeName)) {
642                    lookupClassName = containingClass.getName();
643                    List<String> primaryKeyAttributes = metadata.getPrimaryKeyAttributeNames();
644                    for (String primaryKeyAttribute : primaryKeyAttributes) {
645                        fieldConversions.put(primaryKeyAttribute, primaryKeyAttribute);
646                        if (!StringUtils.equals(primaryKeyAttribute, attributeName)) {
647                            lookupParameters.put(primaryKeyAttribute, primaryKeyAttribute);
648                        }
649                    }
650                }
651            }
652
653            if (StringUtils.isNotBlank(lookupClassName)) {
654                String baseUrl = kualiConfigurationService.getPropertyValueAsString(KRADConstants.KRAD_LOOKUP_URL_KEY);
655                RemotableQuickFinder.Builder builder = RemotableQuickFinder.Builder.create(baseUrl, lookupClassName);
656                builder.setLookupParameters(lookupParameters);
657                builder.setFieldConversions(fieldConversions);
658                return builder;
659            }
660
661        }
662        return null;
663    }
664
665    @Override
666    public boolean isReferenceUpdatable(Class<?> type, String referenceName) {
667        if (dataObjectService.getMetadataRepository().contains(type)) {
668            DataObjectRelationship relationship = dataObjectService.getMetadataRepository().getMetadata(type)
669                    .getRelationship(referenceName);
670            if (relationship != null) {
671                return relationship.isSavedWithParent();
672            }
673        }
674        return false;
675    }
676
677    @SuppressWarnings("rawtypes")
678    @Override
679    public Map<String, Class> listReferenceObjectFields(Class<?> type) {
680        Map<String, Class> referenceNameToTypeMap = new HashMap<String, Class>();
681        if (dataObjectService.getMetadataRepository().contains(type)) {
682            List<DataObjectRelationship> relationships = dataObjectService.getMetadataRepository().getMetadata(type)
683                    .getRelationships();
684            for (DataObjectRelationship rel : relationships) {
685                referenceNameToTypeMap.put(rel.getName(), rel.getRelatedType());
686            }
687        }
688        return referenceNameToTypeMap;
689    }
690
691    @Override
692    public boolean isCollectionUpdatable(Class<?> type, String collectionName) {
693        if (dataObjectService.getMetadataRepository().contains(type)) {
694            DataObjectCollection collection = dataObjectService.getMetadataRepository().getMetadata(type).getCollection(
695                    collectionName);
696            if (collection != null) {
697                return collection.isSavedWithParent();
698            }
699        }
700        return false;
701    }
702
703    @Override
704    public Map<String, Class> listCollectionObjectTypes(Class<?> type) {
705        Map<String, Class> collectionNameToTypeMap = new HashMap<String, Class>();
706        if (dataObjectService.getMetadataRepository().contains(type)) {
707            List<DataObjectCollection> collections = dataObjectService.getMetadataRepository().getMetadata(type)
708                    .getCollections();
709            for (DataObjectCollection coll : collections) {
710                collectionNameToTypeMap.put(coll.getName(), coll.getRelatedType());
711            }
712        }
713        return collectionNameToTypeMap;
714    }
715
716    @Override
717    public Object getReferenceIfExists(Object bo, String referenceName) {
718        // fetches relationship if key is set and return populated value or null
719        DataObjectWrapper<Object> dataObjectWrapper = dataObjectService.wrap(bo);
720        dataObjectWrapper.fetchRelationship(referenceName);
721        return dataObjectWrapper.getPropertyValueNullSafe(referenceName);
722    }
723
724    @Override
725    public boolean allForeignKeyValuesPopulatedForReference(Object bo, String referenceName) {
726        Map<String, String> fkReferences = getForeignKeysForReference(bo.getClass(), referenceName);
727        if (fkReferences.size() > 0) {
728            DataObjectWrapper<Object> dataObjectWrapper = dataObjectService.wrap(bo);
729
730            for (String fkFieldName : fkReferences.keySet()) {
731                Object fkFieldValue = dataObjectWrapper.getForeignKeyAttributeValue(fkFieldName);
732                if (fkFieldValue == null) {
733                    return false;
734                } else if (fkFieldValue instanceof CompoundKey) {
735                    return !((CompoundKey) fkFieldValue).hasNullKeyValues();
736                } else if (String.class.isAssignableFrom(fkFieldValue.getClass())) {
737                    if (StringUtils.isBlank((String) fkFieldValue)) {
738                        return false;
739                    }
740                }
741            }
742        }
743
744        return true;
745    }
746
747    /**
748     * gets the relationship that the attribute represents on the class
749     *
750     * @param c - the class to which the attribute belongs
751     * @param attributeName - property name for the attribute
752     * @return a relationship definition for the attribute
753     */
754    @Override
755    public RelationshipDefinition getDictionaryRelationship(Class<?> c, String attributeName) {
756        DataDictionaryEntry entryBase =
757                KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary().getDictionaryObjectEntry(
758                        c.getName());
759        if (entryBase == null) {
760            return null;
761        }
762
763        RelationshipDefinition relationship = null;
764
765        List<RelationshipDefinition> ddRelationships = entryBase.getRelationships();
766
767        int minKeys = Integer.MAX_VALUE;
768        for (RelationshipDefinition def : ddRelationships) {
769            // favor key sizes of 1 first
770            if (def.getPrimitiveAttributes().size() == 1) {
771                for (PrimitiveAttributeDefinition primitive : def.getPrimitiveAttributes()) {
772                    if (primitive.getSourceName().equals(attributeName) || def.getObjectAttributeName().equals(
773                            attributeName)) {
774                        relationship = def;
775                        minKeys = 1;
776                        break;
777                    }
778                }
779            } else if (def.getPrimitiveAttributes().size() < minKeys) {
780                for (PrimitiveAttributeDefinition primitive : def.getPrimitiveAttributes()) {
781                    if (primitive.getSourceName().equals(attributeName) || def.getObjectAttributeName().equals(
782                            attributeName)) {
783                        relationship = def;
784                        minKeys = def.getPrimitiveAttributes().size();
785                        break;
786                    }
787                }
788            }
789        }
790
791        // check the support attributes
792        if (relationship == null) {
793            for (RelationshipDefinition def : ddRelationships) {
794                if (def.hasIdentifier()) {
795                    if (def.getIdentifier().getSourceName().equals(attributeName)) {
796                        relationship = def;
797                    }
798                }
799            }
800        }
801
802        return relationship;
803    }
804
805    /**
806     * @see org.kuali.rice.krad.service.LegacyDataAdapter
807     */
808    @Override
809    public String getTitleAttribute(Class<?> dataObjectClass) {
810        String titleAttribute = null;
811        DataObjectEntry entry = getDataObjectEntry(dataObjectClass);
812        if (entry != null) {
813            titleAttribute = entry.getTitleAttribute();
814        }
815        return titleAttribute;
816    }
817
818    /**
819     * @param dataObjectClass
820     * @return DataObjectEntry for the given dataObjectClass, or null if
821     * there is none
822     * @throws IllegalArgumentException if the given Class is null
823     */
824    protected DataObjectEntry getDataObjectEntry(Class<?> dataObjectClass) {
825        if (dataObjectClass == null) {
826            throw new IllegalArgumentException("invalid (null) dataObjectClass");
827        }
828
829        DataObjectEntry entry = dataDictionaryService.getDataDictionary().getDataObjectEntry(dataObjectClass.getName());
830
831        return entry;
832    }
833
834    /**
835     * @see org.kuali.rice.krad.service.LegacyDataAdapter#areNotesSupported(java.lang.Class)
836     */
837    @Override
838    public boolean areNotesSupported(Class<?> dataObjectClass) {
839        boolean hasNotesSupport = false;
840
841        DataObjectEntry entry = getDataObjectEntry(dataObjectClass);
842        if (entry != null) {
843            hasNotesSupport = entry.isBoNotesEnabled();
844        }
845
846        return hasNotesSupport;
847    }
848
849    /**
850     * Grabs primary key fields and sorts them if sort field names is true
851     *
852     * @param dataObject
853     * @param sortFieldNames
854     * @return Map of sorted primary key field values
855     */
856    public Map<String, ?> getPrimaryKeyFieldValues(Object dataObject, boolean sortFieldNames) {
857        Map<String, Object> keyFieldValues = (Map<String, Object>) getPrimaryKeyFieldValues(dataObject);
858        if (sortFieldNames) {
859            Map<String, Object> sortedKeyFieldValues = new TreeMap<String, Object>();
860            sortedKeyFieldValues.putAll(keyFieldValues);
861            return sortedKeyFieldValues;
862        }
863        return keyFieldValues;
864    }
865
866    /**
867     * @see org.kuali.rice.krad.service.DataObjectMetaDataService#getDataObjectIdentifierString
868     */
869    @Override
870    public String getDataObjectIdentifierString(Object dataObject) {
871        String identifierString = "";
872
873        if (dataObject == null) {
874            identifierString = "Null";
875            return identifierString;
876        }
877
878        Class<?> dataObjectClass = dataObject.getClass();
879        // build identifier string from primary key values
880        Map<String, ?> primaryKeyFieldValues = getPrimaryKeyFieldValues(dataObject, true);
881        for (Map.Entry<String, ?> primaryKeyValue : primaryKeyFieldValues.entrySet()) {
882            if (primaryKeyValue.getValue() == null) {
883                identifierString += "Null";
884            } else {
885                identifierString += primaryKeyValue.getValue();
886            }
887            identifierString += ":";
888        }
889        return StringUtils.removeEnd(identifierString, ":");
890    }
891
892    @Override
893    public Class<?> getInquiryObjectClassIfNotTitle(Object dataObject, String propertyName) {
894        DataObjectMetadata objectMetadata =
895                KRADServiceLocator.getDataObjectService().getMetadataRepository().getMetadata(dataObject.getClass());
896        if (objectMetadata != null) {
897            org.kuali.rice.krad.data.metadata.DataObjectRelationship dataObjectRelationship =
898                    objectMetadata.getRelationship(propertyName);
899            if (dataObjectRelationship != null) {
900                return dataObjectRelationship.getRelatedType();
901            }
902        }
903        return null;
904    }
905
906    @Override
907    public Map<String, String> getInquiryParameters(Object dataObject, List<String> keys, String propertyName) {
908        Map<String, String> inquiryParameters = new HashMap<String, String>();
909        org.kuali.rice.krad.data.metadata.DataObjectRelationship dataObjectRelationship = null;
910
911        DataObjectMetadata objectMetadata =
912                KRADServiceLocator.getDataObjectService().getMetadataRepository().getMetadata(dataObject.getClass());
913
914        if (objectMetadata != null) {
915            dataObjectRelationship = objectMetadata.getRelationshipByLastAttributeInRelationship(propertyName);
916        }
917
918        for (String keyName : keys) {
919            String keyConversion = keyName;
920            if (dataObjectRelationship != null) {
921                keyConversion = dataObjectRelationship.getParentAttributeNameRelatedToChildAttributeName(keyName);
922            } else if (PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName)) {
923                String nestedAttributePrefix = KRADUtils.getNestedAttributePrefix(propertyName);
924                keyConversion = nestedAttributePrefix + "." + keyName;
925            }
926            inquiryParameters.put(keyConversion, keyName);
927        }
928        return inquiryParameters;
929    }
930
931    @Override
932    public boolean hasLocalLookup(Class<?> dataObjectClass) {
933        return viewDictionaryService.isLookupable(dataObjectClass);
934    }
935
936    @Override
937    public boolean hasLocalInquiry(Class<?> dataObjectClass) {
938        return viewDictionaryService.isInquirable(dataObjectClass);
939    }
940
941    @Override
942    public org.kuali.rice.krad.bo.DataObjectRelationship getDataObjectRelationship(Object dataObject,
943            Class<?> dataObjectClass, String attributeName, String attributePrefix, boolean keysOnly,
944            boolean supportsLookup, boolean supportsInquiry) {
945        RelationshipDefinition ddReference = getDictionaryRelationship(dataObjectClass, attributeName);
946
947        org.kuali.rice.krad.bo.DataObjectRelationship relationship = null;
948        DataObjectAttributeRelationship rel = null;
949        if (PropertyAccessorUtils.isNestedOrIndexedProperty(attributeName)) {
950            if (ddReference != null) {
951                if (classHasSupportedFeatures(ddReference.getTargetClass(), supportsLookup, supportsInquiry)) {
952                    relationship = populateRelationshipFromDictionaryReference(dataObjectClass, ddReference,
953                            attributePrefix, keysOnly);
954
955                    return relationship;
956                }
957            }
958
959            if (dataObject == null) {
960                try {
961                    dataObject = KRADUtils.createNewObjectFromClass(dataObjectClass);
962                } catch (RuntimeException e) {
963                    // found interface or abstract class, just swallow exception and return a null relationship
964                    return null;
965                }
966            }
967
968            // recurse down to the next object to find the relationship
969            int nextObjectIndex = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(attributeName);
970            if (nextObjectIndex == StringUtils.INDEX_NOT_FOUND) {
971                nextObjectIndex = attributeName.length();
972            }
973            String localPrefix = StringUtils.substring(attributeName, 0, nextObjectIndex);
974            String localAttributeName = StringUtils.substring(attributeName, nextObjectIndex + 1);
975            Object nestedObject = ObjectPropertyUtils.getPropertyValue(dataObject, localPrefix);
976            Class<?> nestedClass = null;
977            if (nestedObject == null) {
978                nestedClass = ObjectPropertyUtils.getPropertyType(dataObject, localPrefix);
979            } else {
980                nestedClass = nestedObject.getClass();
981            }
982
983            String fullPrefix = localPrefix;
984            if (StringUtils.isNotBlank(attributePrefix)) {
985                fullPrefix = attributePrefix + "." + localPrefix;
986            }
987
988            relationship = getDataObjectRelationship(nestedObject, nestedClass, localAttributeName, fullPrefix,
989                    keysOnly, supportsLookup, supportsInquiry);
990
991            return relationship;
992        }
993
994        // non-nested reference, get persistence relationships first
995        int maxSize = Integer.MAX_VALUE;
996
997        if (isPersistable(dataObjectClass)) {
998            DataObjectMetadata metadata = dataObjectService.getMetadataRepository().getMetadata(dataObjectClass);
999            DataObjectRelationship dataObjectRelationship = metadata.getRelationship(attributeName);
1000
1001            if (dataObjectRelationship != null) {
1002                List<DataObjectAttributeRelationship> attributeRelationships =
1003                        dataObjectRelationship.getAttributeRelationships();
1004                for (DataObjectAttributeRelationship dataObjectAttributeRelationship : attributeRelationships) {
1005                    if (classHasSupportedFeatures(dataObjectRelationship.getRelatedType(), supportsLookup,
1006                            supportsInquiry)) {
1007                        maxSize = attributeRelationships.size();
1008                        relationship = transformToDeprecatedDataObjectRelationship(dataObjectClass, attributeName,
1009                                attributePrefix, dataObjectRelationship.getRelatedType(),
1010                                dataObjectAttributeRelationship);
1011
1012                        break;
1013                    }
1014                }
1015            }
1016
1017        } else {
1018            ModuleService moduleService = kualiModuleService.getResponsibleModuleService(dataObjectClass);
1019            if (moduleService != null && moduleService.isExternalizable(dataObjectClass)) {
1020                relationship = getRelationshipMetadata(dataObjectClass, attributeName, attributePrefix);
1021                if ((relationship != null) && classHasSupportedFeatures(relationship.getRelatedClass(), supportsLookup,
1022                        supportsInquiry)) {
1023                    return relationship;
1024                } else {
1025                    return null;
1026                }
1027            }
1028        }
1029
1030        if (ddReference != null && ddReference.getPrimitiveAttributes().size() < maxSize) {
1031            if (classHasSupportedFeatures(ddReference.getTargetClass(), supportsLookup, supportsInquiry)) {
1032                relationship = populateRelationshipFromDictionaryReference(dataObjectClass, ddReference, null,
1033                        keysOnly);
1034            }
1035        }
1036        return relationship;
1037    }
1038
1039    protected org.kuali.rice.krad.bo.DataObjectRelationship transformToDeprecatedDataObjectRelationship(
1040            Class<?> dataObjectClass, String attributeName, String attributePrefix, Class<?> relatedObjectClass,
1041            DataObjectAttributeRelationship relationship) {
1042        org.kuali.rice.krad.bo.DataObjectRelationship rel = new org.kuali.rice.krad.bo.DataObjectRelationship(
1043                dataObjectClass, attributeName, relatedObjectClass);
1044        if (StringUtils.isBlank(attributePrefix)) {
1045            rel.getParentToChildReferences().put(relationship.getParentAttributeName(),
1046                    relationship.getChildAttributeName());
1047        } else {
1048            rel.getParentToChildReferences().put(attributePrefix + "." + relationship.getParentAttributeName(),
1049                    relationship.getChildAttributeName());
1050        }
1051
1052        return rel;
1053    }
1054
1055    protected org.kuali.rice.krad.bo.DataObjectRelationship populateRelationshipFromDictionaryReference(
1056            Class<?> dataObjectClass, RelationshipDefinition ddReference, String attributePrefix, boolean keysOnly) {
1057        org.kuali.rice.krad.bo.DataObjectRelationship relationship = new org.kuali.rice.krad.bo.DataObjectRelationship(
1058                dataObjectClass, ddReference.getObjectAttributeName(), ddReference.getTargetClass());
1059
1060        for (PrimitiveAttributeDefinition def : ddReference.getPrimitiveAttributes()) {
1061            if (StringUtils.isNotBlank(attributePrefix)) {
1062                relationship.getParentToChildReferences().put(attributePrefix + "." + def.getSourceName(),
1063                        def.getTargetName());
1064            } else {
1065                relationship.getParentToChildReferences().put(def.getSourceName(), def.getTargetName());
1066            }
1067        }
1068
1069        if (!keysOnly) {
1070            for (SupportAttributeDefinition def : ddReference.getSupportAttributes()) {
1071                if (StringUtils.isNotBlank(attributePrefix)) {
1072                    relationship.getParentToChildReferences().put(attributePrefix + "." + def.getSourceName(),
1073                            def.getTargetName());
1074                    if (def.isIdentifier()) {
1075                        relationship.setUserVisibleIdentifierKey(attributePrefix + "." + def.getSourceName());
1076                    }
1077                } else {
1078                    relationship.getParentToChildReferences().put(def.getSourceName(), def.getTargetName());
1079                    if (def.isIdentifier()) {
1080                        relationship.setUserVisibleIdentifierKey(def.getSourceName());
1081                    }
1082                }
1083            }
1084        }
1085
1086        return relationship;
1087    }
1088
1089    @Override
1090    public boolean isPersistable(Class<?> dataObjectClass) {
1091        return dataObjectService.getMetadataRepository().contains(dataObjectClass);
1092    }
1093
1094    protected org.kuali.rice.krad.bo.DataObjectRelationship getRelationshipMetadata(Class<?> dataObjectClass,
1095            String attributeName, String attributePrefix) {
1096
1097        RelationshipDefinition relationshipDefinition = getDictionaryRelationship(dataObjectClass, attributeName);
1098        if (relationshipDefinition == null) {
1099            return null;
1100        }
1101
1102        org.kuali.rice.krad.bo.DataObjectRelationship dataObjectRelationship =
1103                new org.kuali.rice.krad.bo.DataObjectRelationship(relationshipDefinition.getSourceClass(),
1104                        relationshipDefinition.getObjectAttributeName(), relationshipDefinition.getTargetClass());
1105
1106        if (!StringUtils.isEmpty(attributePrefix)) {
1107            attributePrefix += ".";
1108        }
1109
1110        List<PrimitiveAttributeDefinition> primitives = relationshipDefinition.getPrimitiveAttributes();
1111        for (PrimitiveAttributeDefinition primitiveAttributeDefinition : primitives) {
1112            dataObjectRelationship.getParentToChildReferences().put(
1113                    attributePrefix + primitiveAttributeDefinition.getSourceName(),
1114                    primitiveAttributeDefinition.getTargetName());
1115        }
1116
1117        return dataObjectRelationship;
1118    }
1119
1120    protected boolean classHasSupportedFeatures(Class relationshipClass, boolean supportsLookup,
1121            boolean supportsInquiry) {
1122        boolean hasSupportedFeatures = true;
1123        if (supportsLookup && !getViewDictionaryService().isLookupable(relationshipClass)) {
1124            hasSupportedFeatures = false;
1125        }
1126        if (supportsInquiry && !getViewDictionaryService().isInquirable(relationshipClass)) {
1127            hasSupportedFeatures = false;
1128        }
1129
1130        return hasSupportedFeatures;
1131    }
1132
1133    @Override
1134    public ForeignKeyFieldsPopulationState getForeignKeyFieldsPopulationState(Object dataObject, String referenceName) {
1135        DataObjectWrapper<Object> dataObjectWrapper = dataObjectService.wrap(dataObject);
1136        return new ForeignKeyFieldsPopulationState(dataObjectWrapper.areAllPrimaryKeyAttributesPopulated(),
1137                dataObjectWrapper.areAnyPrimaryKeyAttributesPopulated(),
1138                dataObjectWrapper.getUnpopulatedPrimaryKeyAttributeNames());
1139    }
1140
1141    @Override
1142    public Map<String, String> getForeignKeysForReference(Class<?> clazz, String attributeName) {
1143        if (dataObjectService.getMetadataRepository().contains(clazz)) {
1144            DataObjectRelationship relationship = dataObjectService.getMetadataRepository().getMetadata(clazz)
1145                    .getRelationship(attributeName);
1146            List<DataObjectAttributeRelationship> attributeRelationships = relationship.getAttributeRelationships();
1147            Map<String, String> parentChildKeyRelationships = new HashMap<String, String>(
1148                    attributeRelationships.size());
1149            for (DataObjectAttributeRelationship doar : attributeRelationships) {
1150                parentChildKeyRelationships.put(doar.getParentAttributeName(), doar.getChildAttributeName());
1151            }
1152            return parentChildKeyRelationships;
1153        }
1154        return Collections.emptyMap();
1155    }
1156
1157    @Override
1158    public void setObjectPropertyDeep(Object bo, String propertyName, Class type,
1159            Object propertyValue) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1160        DataObjectWrapper<Object> dataObjectWrapper = dataObjectService.wrap(bo);
1161        // Base return cases to avoid null pointers & infinite loops
1162        if (KRADUtils.isNull(bo) || !PropertyUtils.isReadable(bo, propertyName) || (propertyValue != null
1163                && propertyValue.equals(dataObjectWrapper.getPropertyValueNullSafe(propertyName))) || (type != null
1164                && !type.equals(KRADUtils.easyGetPropertyType(bo, propertyName)))) {
1165            return;
1166        }
1167        // Set the property in the BO
1168        KRADUtils.setObjectProperty(bo, propertyName, type, propertyValue);
1169
1170        // Now drill down and check nested BOs and BO lists
1171        PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(bo.getClass());
1172        for (int i = 0; i < propertyDescriptors.length; i++) {
1173
1174            PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
1175
1176            // Business Objects
1177            if (propertyDescriptor.getPropertyType() != null && (BusinessObject.class).isAssignableFrom(
1178                    propertyDescriptor.getPropertyType()) && PropertyUtils.isReadable(bo,
1179                    propertyDescriptor.getName())) {
1180                Object nestedBo = dataObjectWrapper.getPropertyValueNullSafe(propertyDescriptor.getName());
1181                if (nestedBo instanceof BusinessObject) {
1182                    setObjectPropertyDeep(nestedBo, propertyName, type, propertyValue);
1183                }
1184            }
1185
1186            // Lists
1187            else if (propertyDescriptor.getPropertyType() != null && (List.class).isAssignableFrom(
1188                    propertyDescriptor.getPropertyType()) && dataObjectWrapper.getPropertyValueNullSafe(
1189                    propertyDescriptor.getName()) != null) {
1190
1191                List propertyList = (List) dataObjectWrapper.getPropertyValueNullSafe(propertyDescriptor.getName());
1192                for (Object listedBo : propertyList) {
1193                    if (listedBo != null && listedBo instanceof BusinessObject) {
1194                        setObjectPropertyDeep(listedBo, propertyName, type, propertyValue);
1195                    }
1196                } // end for
1197            }
1198        } // end for
1199    }
1200
1201    @Override
1202    public boolean hasPrimaryKeyFieldValues(Object dataObject) {
1203        DataObjectWrapper<Object> dataObjectWrapper = dataObjectService.wrap(dataObject);
1204        return dataObjectWrapper.areAllPrimaryKeyAttributesPopulated();
1205    }
1206
1207    @Override
1208    public Class materializeClassForProxiedObject(Object object) {
1209        if (object == null) {
1210            return null;
1211        }
1212        if (LegacyUtils.isKradDataManaged(object.getClass())) {
1213            Object o = resolveProxy(object);
1214            if (o != null) {
1215                return o.getClass();
1216            }
1217        }
1218        return object.getClass();
1219    }
1220
1221    @Override
1222    public Object getNestedValue(Object bo, String fieldName) {
1223        return KradDataServiceLocator.getDataObjectService().wrap(bo).getPropertyValueNullSafe(fieldName);
1224    }
1225
1226    @Override
1227    public Object createNewObjectFromClass(Class clazz) {
1228        if (clazz == null) {
1229            throw new IllegalArgumentException("Class was passed in as null");
1230        }
1231
1232        Object object = null;
1233
1234        try {
1235            object = clazz.newInstance();
1236        } catch (InstantiationException e) {
1237            throw new RuntimeException(e);
1238        } catch (IllegalAccessException e) {
1239            throw new RuntimeException(e);
1240        }
1241
1242        return object;
1243    }
1244
1245    @Override
1246    public boolean isNull(Object object) {
1247        return object == null;
1248    }
1249
1250    @Override
1251    public void setObjectProperty(Object bo, String propertyName, Class propertyType,
1252            Object propertyValue) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1253        PropertyUtils.setNestedProperty(bo, propertyName, propertyValue);
1254    }
1255
1256    @Override
1257    public <T extends Document> T findByDocumentHeaderId(Class<T> documentClass, String id) {
1258        T document = KRADServiceLocator.getDataObjectService().find(documentClass, id);
1259        // original KNS code always did this addAdHocs nonsense, so we'll do the same to preserve behavior
1260        ((DocumentAdHocService) KRADServiceLocatorWeb.getService("documentAdHocService")).addAdHocs(document);
1261        return document;
1262    }
1263
1264    @Override
1265    public <T extends Document> List<T> findByDocumentHeaderIds(Class<T> documentClass, List<String> ids) {
1266        List<T> documents = new ArrayList<T>();
1267
1268        for (String id : ids) {
1269            T document = findByDocumentHeaderId(documentClass, id);
1270
1271            if (document != null) {
1272                documents.add(document);
1273            }
1274        }
1275
1276        return documents;
1277    }
1278
1279    @Required
1280    public void setDataObjectService(DataObjectService dataObjectService) {
1281        this.dataObjectService = dataObjectService;
1282    }
1283
1284    @Required
1285    public void setLookupCriteriaGenerator(LookupCriteriaGenerator lookupCriteriaGenerator) {
1286        this.lookupCriteriaGenerator = lookupCriteriaGenerator;
1287    }
1288
1289    @Required
1290    public void setKualiConfigurationService(ConfigurationService kualiConfigurationService) {
1291        this.kualiConfigurationService = kualiConfigurationService;
1292    }
1293
1294    @Required
1295    public void setKualiModuleService(KualiModuleService kualiModuleService) {
1296        this.kualiModuleService = kualiModuleService;
1297    }
1298
1299    @Required
1300    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
1301        this.dataDictionaryService = dataDictionaryService;
1302    }
1303
1304    public ViewDictionaryService getViewDictionaryService() {
1305        return viewDictionaryService;
1306    }
1307
1308    public void setViewDictionaryService(ViewDictionaryService viewDictionaryService) {
1309        this.viewDictionaryService = viewDictionaryService;
1310    }
1311
1312}