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.util.ArrayList;
019import java.util.HashMap;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023import java.util.TreeMap;
024import java.util.UUID;
025
026import org.apache.commons.lang.StringUtils;
027import org.kuali.rice.krad.bo.BusinessObject;
028import org.kuali.rice.krad.bo.DataObjectRelationship;
029import org.kuali.rice.krad.bo.PersistableBusinessObject;
030import org.kuali.rice.krad.datadictionary.BusinessObjectEntry;
031import org.kuali.rice.krad.datadictionary.DataDictionaryEntry;
032import org.kuali.rice.krad.datadictionary.DataObjectEntry;
033import org.kuali.rice.krad.datadictionary.PrimitiveAttributeDefinition;
034import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
035import org.kuali.rice.krad.datadictionary.SupportAttributeDefinition;
036import org.kuali.rice.krad.service.DataDictionaryService;
037import org.kuali.rice.krad.service.DataObjectMetaDataService;
038import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
039import org.kuali.rice.krad.service.KualiModuleService;
040import org.kuali.rice.krad.service.ModuleService;
041import org.kuali.rice.krad.service.PersistenceStructureService;
042import org.kuali.rice.krad.uif.UifPropertyPaths;
043import org.kuali.rice.krad.uif.service.ViewDictionaryService;
044import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
045import org.kuali.rice.krad.util.ObjectUtils;
046import org.springframework.beans.BeanWrapper;
047
048/**
049 * @author Kuali Rice Team (rice.collab@kuali.org)
050 */
051public class DataObjectMetaDataServiceImpl implements DataObjectMetaDataService {
052
053    private DataDictionaryService dataDictionaryService;
054    private KualiModuleService kualiModuleService;
055    private PersistenceStructureService persistenceStructureService;
056    private ViewDictionaryService viewDictionaryService;
057
058    /**
059     * @see org.kuali.rice.krad.service.DataObjectMetaDataService#listPrimaryKeyFieldNames(java.lang.Class)
060     */
061    @Override
062    public List<String> listPrimaryKeyFieldNames(Class<?> clazz) {
063        if (persistenceStructureService.isPersistable(clazz)) {
064            return persistenceStructureService.listPrimaryKeyFieldNames(clazz);
065        }
066
067        ModuleService responsibleModuleService = getKualiModuleService().getResponsibleModuleService(clazz);
068        if (responsibleModuleService != null && responsibleModuleService.isExternalizable(clazz)) {
069            return responsibleModuleService.listPrimaryKeyFieldNames(clazz);
070        }
071
072        // check the Data Dictionary for PK's of non PBO/EBO
073        List<String> pks = dataDictionaryService.getDataDictionary().getDataObjectEntry(clazz.getName())
074                .getPrimaryKeys();
075        if (pks != null && !pks.isEmpty()) {
076            return pks;
077        }
078
079        return new ArrayList<String>();
080    }
081
082    /**
083     * @see org.kuali.rice.krad.service.DataObjectMetaDataService#getPrimaryKeyFieldValues(java.lang.Object)
084     */
085    @Override
086    public Map<String, ?> getPrimaryKeyFieldValues(Object dataObject) {
087        return getPrimaryKeyFieldValues(dataObject, false);
088    }
089
090    /**
091     * @see org.kuali.rice.krad.service.DataObjectMetaDataService#getPrimaryKeyFieldValues(java.lang.Object,
092     *      boolean)
093     */
094    @Override
095    public Map<String, ?> getPrimaryKeyFieldValues(Object dataObject, boolean sortFieldNames) {
096        Map<String, Object> keyValueMap;
097
098        if (sortFieldNames) {
099            keyValueMap = new TreeMap<String, Object>();
100        } else {
101            keyValueMap = new HashMap<String, Object>();
102        }
103
104        BeanWrapper wrapper = ObjectPropertyUtils.wrapObject(dataObject);
105
106        List<String> fields = listPrimaryKeyFieldNames(dataObject.getClass());
107        for (String fieldName : fields) {
108            keyValueMap.put(fieldName, wrapper.getPropertyValue(fieldName));
109        }
110
111        return keyValueMap;
112    }
113
114    /**
115     * @see org.kuali.rice.krad.service.DataObjectMetaDataService#equalsByPrimaryKeys(java.lang.Object,
116     *      java.lang.Object)
117     */
118    @Override
119    public boolean equalsByPrimaryKeys(Object do1, Object do2) {
120        boolean equal = true;
121
122        if (do1 == null && do2 == null) {
123            equal = true;
124        } else if (do1 == null || do2 == null) {
125            equal = false;
126        } else if (!do1.getClass().getName().equals(do2.getClass().getName())) {
127            equal = false;
128        } else {
129            Map<String, ?> do1Keys = getPrimaryKeyFieldValues(do1);
130            Map<String, ?> do2Keys = getPrimaryKeyFieldValues(do2);
131            for (Iterator<String> iter = do1Keys.keySet().iterator(); iter.hasNext(); ) {
132                String keyName = iter.next();
133                if (do1Keys.get(keyName) != null && do2Keys.get(keyName) != null) {
134                    if (!do1Keys.get(keyName).toString().equals(do2Keys.get(keyName).toString())) {
135                        equal = false;
136                    }
137                } else {
138                    equal = false;
139                }
140            }
141        }
142
143        return equal;
144    }
145
146    /**
147     * @see org.kuali.rice.kns.service.BusinessObjectMetaDataService#getDataObjectRelationship(java.lang.Object,
148     *      java.lang.Class, java.lang.String, java.lang.String, boolean,
149     *      boolean, boolean)
150     */
151    public DataObjectRelationship getDataObjectRelationship(Object dataObject, Class<?> dataObjectClass,
152            String attributeName, String attributePrefix, boolean keysOnly, boolean supportsLookup,
153            boolean supportsInquiry) {
154        RelationshipDefinition ddReference = getDictionaryRelationship(dataObjectClass, attributeName);
155
156        return getDataObjectRelationship(ddReference, dataObject, dataObjectClass, attributeName, attributePrefix,
157                keysOnly, supportsLookup, supportsInquiry);
158    }
159
160    protected DataObjectRelationship getDataObjectRelationship(RelationshipDefinition ddReference, Object dataObject,
161            Class<?> dataObjectClass, String attributeName, String attributePrefix, boolean keysOnly,
162            boolean supportsLookup, boolean supportsInquiry) {
163        DataObjectRelationship relationship = null;
164
165        // if it is nested then replace the data object and attributeName with the
166        // sub-refs
167        if (ObjectUtils.isNestedAttribute(attributeName)) {
168            if (ddReference != null) {
169                if (classHasSupportedFeatures(ddReference.getTargetClass(), supportsLookup, supportsInquiry)) {
170                    relationship = populateRelationshipFromDictionaryReference(dataObjectClass, ddReference,
171                            attributePrefix, keysOnly);
172
173                    return relationship;
174                }
175            }
176
177            // recurse down to the next object to find the relationship
178            String localPrefix = StringUtils.substringBefore(attributeName, ".");
179            String localAttributeName = StringUtils.substringAfter(attributeName, ".");
180            if (dataObject == null) {
181                dataObject = ObjectUtils.createNewObjectFromClass(dataObjectClass);
182            }
183
184            Object nestedObject = ObjectPropertyUtils.getPropertyValue(dataObject, localPrefix);
185            Class<?> nestedClass = null;
186            if (nestedObject == null) {
187                nestedClass = ObjectPropertyUtils.getPropertyType(dataObject, localPrefix);
188            } else {
189                nestedClass = nestedObject.getClass();
190            }
191
192            String fullPrefix = localPrefix;
193            if (StringUtils.isNotBlank(attributePrefix)) {
194                fullPrefix = attributePrefix + "." + localPrefix;
195            }
196
197            relationship = getDataObjectRelationship(nestedObject, nestedClass, localAttributeName, fullPrefix,
198                    keysOnly, supportsLookup, supportsInquiry);
199
200            return relationship;
201        }
202
203        // non-nested reference, get persistence relationships first
204        int maxSize = Integer.MAX_VALUE;
205
206        // try persistable reference first
207        if (getPersistenceStructureService().isPersistable(dataObjectClass)) {
208            Map<String, DataObjectRelationship> rels = getPersistenceStructureService().getRelationshipMetadata(
209                    dataObjectClass, attributeName, attributePrefix);
210            if (rels.size() > 0) {
211                for (DataObjectRelationship rel : rels.values()) {
212                    if (rel.getParentToChildReferences().size() < maxSize) {
213                        if (classHasSupportedFeatures(rel.getRelatedClass(), supportsLookup, supportsInquiry)) {
214                            maxSize = rel.getParentToChildReferences().size();
215                            relationship = rel;
216                        }
217                    }
218                }
219            }
220        } else {
221            ModuleService moduleService = getKualiModuleService().getResponsibleModuleService(dataObjectClass);
222            if (moduleService != null && moduleService.isExternalizable(dataObjectClass)) {
223                relationship = getRelationshipMetadata(dataObjectClass, attributeName, attributePrefix);
224                if ((relationship != null) && classHasSupportedFeatures(relationship.getRelatedClass(), supportsLookup,
225                        supportsInquiry)) {
226                    return relationship;
227                } else {
228                    return null;
229                }
230            }
231        }
232
233        if (ddReference != null && ddReference.getPrimitiveAttributes().size() < maxSize) {
234            if (classHasSupportedFeatures(ddReference.getTargetClass(), supportsLookup, supportsInquiry)) {
235                relationship = populateRelationshipFromDictionaryReference(dataObjectClass, ddReference, null,
236                        keysOnly);
237            }
238        }
239
240        return relationship;
241    }
242
243    protected boolean classHasSupportedFeatures(Class relationshipClass, boolean supportsLookup,
244            boolean supportsInquiry) {
245        boolean hasSupportedFeatures = true;
246        if (supportsLookup && !getViewDictionaryService().isLookupable(relationshipClass)) {
247            hasSupportedFeatures = false;
248        }
249        if (supportsInquiry && !getViewDictionaryService().isInquirable(relationshipClass)) {
250            hasSupportedFeatures = false;
251        }
252
253        return hasSupportedFeatures;
254    }
255
256    public RelationshipDefinition getDictionaryRelationship(Class<?> c, String attributeName) {
257        DataDictionaryEntry entryBase = getDataDictionaryService().getDataDictionary().getDictionaryObjectEntry(
258                c.getName());
259        if (entryBase == null) {
260            return null;
261        }
262
263        RelationshipDefinition relationship = null;
264
265        List<RelationshipDefinition> ddRelationships = entryBase.getRelationships();
266
267        int minKeys = Integer.MAX_VALUE;
268        for (RelationshipDefinition def : ddRelationships) {
269            // favor key sizes of 1 first
270            if (def.getPrimitiveAttributes().size() == 1) {
271                for (PrimitiveAttributeDefinition primitive : def.getPrimitiveAttributes()) {
272                    if (primitive.getSourceName().equals(attributeName) || def.getObjectAttributeName().equals(
273                            attributeName)) {
274                        relationship = def;
275                        minKeys = 1;
276                        break;
277                    }
278                }
279            } else if (def.getPrimitiveAttributes().size() < minKeys) {
280                for (PrimitiveAttributeDefinition primitive : def.getPrimitiveAttributes()) {
281                    if (primitive.getSourceName().equals(attributeName) || def.getObjectAttributeName().equals(
282                            attributeName)) {
283                        relationship = def;
284                        minKeys = def.getPrimitiveAttributes().size();
285                        break;
286                    }
287                }
288            }
289        }
290
291        // check the support attributes
292        if (relationship == null) {
293            for (RelationshipDefinition def : ddRelationships) {
294                if (def.hasIdentifier()) {
295                    if (def.getIdentifier().getSourceName().equals(attributeName)) {
296                        relationship = def;
297                    }
298                }
299            }
300        }
301
302        return relationship;
303    }
304
305    protected DataObjectRelationship populateRelationshipFromDictionaryReference(Class<?> dataObjectClass,
306            RelationshipDefinition ddReference, String attributePrefix, boolean keysOnly) {
307        DataObjectRelationship relationship = new DataObjectRelationship(dataObjectClass,
308                ddReference.getObjectAttributeName(), ddReference.getTargetClass());
309
310        for (PrimitiveAttributeDefinition def : ddReference.getPrimitiveAttributes()) {
311            if (StringUtils.isNotBlank(attributePrefix)) {
312                relationship.getParentToChildReferences().put(attributePrefix + "." + def.getSourceName(),
313                        def.getTargetName());
314            } else {
315                relationship.getParentToChildReferences().put(def.getSourceName(), def.getTargetName());
316            }
317        }
318
319        if (!keysOnly) {
320            for (SupportAttributeDefinition def : ddReference.getSupportAttributes()) {
321                if (StringUtils.isNotBlank(attributePrefix)) {
322                    relationship.getParentToChildReferences().put(attributePrefix + "." + def.getSourceName(),
323                            def.getTargetName());
324                    if (def.isIdentifier()) {
325                        relationship.setUserVisibleIdentifierKey(attributePrefix + "." + def.getSourceName());
326                    }
327                } else {
328                    relationship.getParentToChildReferences().put(def.getSourceName(), def.getTargetName());
329                    if (def.isIdentifier()) {
330                        relationship.setUserVisibleIdentifierKey(def.getSourceName());
331                    }
332                }
333            }
334        }
335
336        return relationship;
337    }
338
339    protected DataObjectRelationship getRelationshipMetadata(Class<?> dataObjectClass, String attributeName,
340            String attributePrefix) {
341
342        RelationshipDefinition relationshipDefinition = getDictionaryRelationship(dataObjectClass, attributeName);
343        if (relationshipDefinition == null) {
344            return null;
345        }
346
347        DataObjectRelationship dataObjectRelationship = new DataObjectRelationship(
348                relationshipDefinition.getSourceClass(), relationshipDefinition.getObjectAttributeName(),
349                relationshipDefinition.getTargetClass());
350
351        if (!StringUtils.isEmpty(attributePrefix)) {
352            attributePrefix += ".";
353        }
354
355        List<PrimitiveAttributeDefinition> primitives = relationshipDefinition.getPrimitiveAttributes();
356        for (PrimitiveAttributeDefinition primitiveAttributeDefinition : primitives) {
357            dataObjectRelationship.getParentToChildReferences().put(
358                    attributePrefix + primitiveAttributeDefinition.getSourceName(),
359                    primitiveAttributeDefinition.getTargetName());
360        }
361
362        return dataObjectRelationship;
363    }
364
365    /**
366     * @see org.kuali.rice.krad.service.DataObjectMetaDataService#getTitleAttribute(java.lang.Class)
367     */
368    public String getTitleAttribute(Class<?> dataObjectClass) {
369        String titleAttribute = null;
370
371        DataObjectEntry entry = getDataObjectEntry(dataObjectClass);
372        if (entry != null) {
373            titleAttribute = entry.getTitleAttribute();
374        }
375
376        return titleAttribute;
377    }
378
379    /**
380     * @see org.kuali.rice.krad.service.DataObjectMetaDataService#areNotesSupported(java.lang.Class)
381     */
382    @Override
383    public boolean areNotesSupported(Class<?> dataObjectClass) {
384        boolean hasNotesSupport = false;
385
386        DataObjectEntry entry = getDataObjectEntry(dataObjectClass);
387        if (entry != null) {
388            hasNotesSupport = entry.isBoNotesEnabled();
389        }
390
391        return hasNotesSupport;
392    }
393
394    /**
395     * @see org.kuali.rice.krad.service.DataObjectMetaDataService#getDataObjectIdentifierString
396     */
397    public String getDataObjectIdentifierString(Object dataObject) {
398        String identifierString = "";
399
400        if (dataObject == null) {
401            identifierString = "Null";
402            return identifierString;
403        }
404
405        Class<?> dataObjectClass = dataObject.getClass();
406
407        // if PBO, use object id property
408        if (PersistableBusinessObject.class.isAssignableFrom(dataObjectClass)) {
409            String objectId = ObjectPropertyUtils.getPropertyValue(dataObject, UifPropertyPaths.OBJECT_ID);
410            if (StringUtils.isBlank(objectId)) {
411                objectId = UUID.randomUUID().toString();
412                ObjectPropertyUtils.setPropertyValue(dataObject, UifPropertyPaths.OBJECT_ID, objectId);
413            }
414
415            identifierString = objectId;
416        } else {
417            // build identifier string from primary key values
418            Map<String, ?> primaryKeyFieldValues = getPrimaryKeyFieldValues(dataObject, true);
419            for (Map.Entry<String, ?> primaryKeyValue : primaryKeyFieldValues.entrySet()) {
420                if (primaryKeyValue.getValue() == null) {
421                    identifierString += "Null";
422                } else {
423                    identifierString += primaryKeyValue.getValue();
424                }
425                identifierString += ":";
426            }
427            identifierString = StringUtils.removeEnd(identifierString, ":");
428        }
429
430        return identifierString;
431    }
432
433    /**
434     * @param dataObjectClass
435     * @return DataObjectEntry for the given dataObjectClass, or null if
436     *         there is none
437     * @throws IllegalArgumentException if the given Class is null
438     */
439    protected DataObjectEntry getDataObjectEntry(Class<?> dataObjectClass) {
440        if (dataObjectClass == null) {
441            throw new IllegalArgumentException("invalid (null) dataObjectClass");
442        }
443
444        DataObjectEntry entry = getDataDictionaryService().getDataDictionary().getDataObjectEntry(
445                dataObjectClass.getName());
446
447        return entry;
448    }
449
450    public List<DataObjectRelationship> getDataObjectRelationships(Class<?> dataObjectClass) {
451                if (dataObjectClass == null) {
452                        return null;
453                }
454
455                Map<String, Class> referenceClasses = null;
456                if (PersistableBusinessObject.class.isAssignableFrom(dataObjectClass)
457                                && getPersistenceStructureService().isPersistable(dataObjectClass)) {
458                        referenceClasses = getPersistenceStructureService().listReferenceObjectFields(dataObjectClass);
459                }
460                DataDictionaryEntry ddEntry = getDataDictionaryService().getDataDictionary().getDictionaryObjectEntry(
461                                dataObjectClass.getName());
462                List<RelationshipDefinition> ddRelationships = (ddEntry == null ? new ArrayList<RelationshipDefinition>()
463                                : ddEntry.getRelationships());
464                List<DataObjectRelationship> relationships = new ArrayList<DataObjectRelationship>();
465
466                // loop over all relationships
467                if (referenceClasses != null) {
468                        for (Map.Entry<String, Class> entry : referenceClasses.entrySet()) {
469                if (classHasSupportedFeatures(entry.getValue(), true, false)) {
470                                        Map<String, String> fkToPkRefs = getPersistenceStructureService().getForeignKeysForReference(dataObjectClass,
471                                                        entry.getKey());
472                                        DataObjectRelationship rel = new DataObjectRelationship(dataObjectClass, entry.getKey(),
473                                                        entry.getValue());
474                                        for (Map.Entry<String, String> ref : fkToPkRefs.entrySet()) {
475                                                rel.getParentToChildReferences().put(ref.getKey(), ref.getValue());
476                                        }
477                                        relationships.add(rel);
478                                }
479                        }
480                }
481
482                for (RelationshipDefinition rd : ddRelationships) {
483                        if (classHasSupportedFeatures(rd.getTargetClass(), true, false)) {
484                                DataObjectRelationship rel = new DataObjectRelationship(dataObjectClass, rd.getObjectAttributeName(),
485                                                rd.getTargetClass());
486                                for (PrimitiveAttributeDefinition def : rd.getPrimitiveAttributes()) {
487                                        rel.getParentToChildReferences().put(def.getSourceName(), def.getTargetName());
488                                }
489                                relationships.add(rel);
490                        }
491                }
492
493                return relationships;
494        }
495
496    /**
497     * @param businessObjectClass - class of business object to return entry for
498     * @return BusinessObjectEntry for the given dataObjectClass, or null if
499     *         there is none
500     */
501    protected BusinessObjectEntry getBusinessObjectEntry(Class businessObjectClass) {
502        validateBusinessObjectClass(businessObjectClass);
503
504        BusinessObjectEntry entry = getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(
505                businessObjectClass.getName());
506        return entry;
507    }
508
509    /**
510     * @param businessObjectClass
511     * @throws IllegalArgumentException if the given Class is null or is not a BusinessObject class
512     */
513    protected void validateBusinessObjectClass(Class businessObjectClass) {
514        if (businessObjectClass == null) {
515            throw new IllegalArgumentException("invalid (null) dataObjectClass");
516        }
517        if (!BusinessObject.class.isAssignableFrom(businessObjectClass)) {
518            throw new IllegalArgumentException(
519                    "class '" + businessObjectClass.getName() + "' is not a descendant of BusinessObject");
520        }
521    }
522
523    /**
524     * Protected method to allow subclasses to access the dataDictionaryService.
525     *
526     * @return Returns the dataDictionaryService.
527     */
528    protected DataDictionaryService getDataDictionaryService() {
529        return this.dataDictionaryService;
530    }
531
532    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
533        this.dataDictionaryService = dataDictionaryService;
534    }
535
536    /**
537     * Protected method to allow subclasses to access the kualiModuleService.
538     *
539     * @return Returns the persistenceStructureService.
540     */
541    protected KualiModuleService getKualiModuleService() {
542        return this.kualiModuleService;
543    }
544
545    public void setKualiModuleService(KualiModuleService kualiModuleService) {
546        this.kualiModuleService = kualiModuleService;
547    }
548
549    /**
550     * Protected method to allow subclasses to access the
551     * persistenceStructureService.
552     *
553     * @return Returns the persistenceStructureService.
554     */
555    protected PersistenceStructureService getPersistenceStructureService() {
556        return this.persistenceStructureService;
557    }
558
559    public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
560        this.persistenceStructureService = persistenceStructureService;
561    }
562
563    protected ViewDictionaryService getViewDictionaryService() {
564        if (this.viewDictionaryService == null) {
565            this.viewDictionaryService = KRADServiceLocatorWeb.getViewDictionaryService();
566        }
567        return this.viewDictionaryService;
568    }
569
570    public void setViewDictionaryService(ViewDictionaryService viewDictionaryService) {
571        this.viewDictionaryService = viewDictionaryService;
572    }
573
574}