001    /**
002     * Copyright 2005-2014 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.datadictionary;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.krad.datadictionary.exception.DuplicateEntryException;
020    import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
021    import org.kuali.rice.krad.datadictionary.state.StateMapping;
022    import org.kuali.rice.krad.datadictionary.validator.ValidationTrace;
023    import org.kuali.rice.krad.exception.ValidationException;
024    import org.springframework.beans.BeanUtils;
025    import org.springframework.beans.factory.InitializingBean;
026    
027    import java.io.Serializable;
028    import java.util.ArrayList;
029    import java.util.LinkedHashMap;
030    import java.util.List;
031    import java.util.Map;
032    import java.util.Set;
033    
034    /**
035     * Contains common properties and methods for data dictionary entries
036     *
037     * @author Kuali Rice Team (rice.collab@kuali.org)
038     */
039    abstract public class DataDictionaryEntryBase extends DictionaryBeanBase implements DataDictionaryEntry, Serializable, InitializingBean {
040        protected List<AttributeDefinition> attributes;
041        protected List<ComplexAttributeDefinition> complexAttributes;
042        protected List<CollectionDefinition> collections;
043        protected List<RelationshipDefinition> relationships;
044        protected Map<String, AttributeDefinition> attributeMap;
045        protected Map<String, ComplexAttributeDefinition> complexAttributeMap;
046        protected Map<String, CollectionDefinition> collectionMap;
047        protected Map<String, RelationshipDefinition> relationshipMap;
048    
049        protected StateMapping stateMapping;
050    
051        public DataDictionaryEntryBase() {
052            this.attributes = new ArrayList<AttributeDefinition>();
053            this.complexAttributes = new ArrayList<ComplexAttributeDefinition>();
054            this.collections = new ArrayList<CollectionDefinition>();
055            this.relationships = new ArrayList<RelationshipDefinition>();
056            this.attributeMap = new LinkedHashMap<String, AttributeDefinition>();
057            this.complexAttributeMap = new LinkedHashMap<String, ComplexAttributeDefinition>();
058            this.collectionMap = new LinkedHashMap<String, CollectionDefinition>();
059            this.relationshipMap = new LinkedHashMap<String, RelationshipDefinition>();
060        }
061    
062        /* Returns the given entry class (bo class or document class) */
063        public abstract Class<?> getEntryClass();
064    
065        /**
066         * @param attributeName
067         * @return AttributeDefinition with the given name, or null if none with that name exists
068         */
069        public AttributeDefinition getAttributeDefinition(String attributeName) {
070            if (StringUtils.isBlank(attributeName)) {
071                throw new IllegalArgumentException("invalid (blank) attributeName");
072            }
073            return attributeMap.get(attributeName);
074        }
075    
076        /**
077         * @return a Map containing all AttributeDefinitions associated with this BusinessObjectEntry, indexed by
078         *         attributeName
079         */
080        @BeanTagAttribute(name = "attributes", type = BeanTagAttribute.AttributeType.LISTBEAN)
081        public List<AttributeDefinition> getAttributes() {
082            return this.attributes;
083        }
084    
085        /**
086         * @return the complexAttributes
087         */
088        public List<ComplexAttributeDefinition> getComplexAttributes() {
089            return this.complexAttributes;
090        }
091    
092        /**
093         * @param complexAttributes the complexAttributes to set
094         */
095        public void setComplexAttributes(List<ComplexAttributeDefinition> complexAttributes) {
096            complexAttributeMap.clear();
097            for (ComplexAttributeDefinition complexAttribute : complexAttributes) {
098                if (complexAttribute == null) {
099                    throw new IllegalArgumentException("invalid (null) complexAttributeDefinition");
100                }
101                String complexAttributeName = complexAttribute.getName();
102                if (StringUtils.isBlank(complexAttributeName)) {
103                    throw new ValidationException("invalid (blank) collectionName");
104                }
105    
106                if (complexAttributeMap.containsKey(complexAttribute)) {
107                    throw new DuplicateEntryException("complex attribute '"
108                            + complexAttribute
109                            + "' already defined as an complex attribute for class '"
110                            + getEntryClass().getName()
111                            + "'");
112                } else if (collectionMap.containsKey(complexAttributeName)) {
113                    throw new DuplicateEntryException("complex attribute '"
114                            + complexAttributeName
115                            + "' already defined as a Collection for class '"
116                            + getEntryClass().getName()
117                            + "'");
118                } else if (attributeMap.containsKey(complexAttributeName)) {
119                    throw new DuplicateEntryException("complex attribute '"
120                            + complexAttributeName
121                            + "' already defined as an Attribute for class '"
122                            + getEntryClass().getName()
123                            + "'");
124                }
125    
126                complexAttributeMap.put(complexAttributeName, complexAttribute);
127    
128            }
129    
130            this.complexAttributes = complexAttributes;
131        }
132    
133        /**
134         * @param collectionName
135         * @return CollectionDefinition with the given name, or null if none with that name exists
136         */
137        public CollectionDefinition getCollectionDefinition(String collectionName) {
138            if (StringUtils.isBlank(collectionName)) {
139                throw new IllegalArgumentException("invalid (blank) collectionName");
140            }
141            return collectionMap.get(collectionName);
142        }
143    
144        /**
145         * @return a Map containing all CollectionDefinitions associated with this BusinessObjectEntry, indexed by
146         *         collectionName
147         */
148        @BeanTagAttribute(name = "collections", type = BeanTagAttribute.AttributeType.LISTBEAN)
149        public List<CollectionDefinition> getCollections() {
150            return this.collections;
151        }
152    
153        /**
154         * @param relationshipName
155         * @return RelationshipDefinition with the given name, or null if none with that name exists
156         */
157        public RelationshipDefinition getRelationshipDefinition(String relationshipName) {
158            if (StringUtils.isBlank(relationshipName)) {
159                throw new IllegalArgumentException("invalid (blank) relationshipName");
160            }
161            return relationshipMap.get(relationshipName);
162        }
163    
164        /**
165         * @return a Map containing all RelationshipDefinitions associated with this BusinessObjectEntry, indexed by
166         *         relationshipName
167         */
168        @BeanTagAttribute(name = "relationships", type = BeanTagAttribute.AttributeType.LISTBEAN)
169        public List<RelationshipDefinition> getRelationships() {
170            return this.relationships;
171        }
172    
173        /**
174         * Directly validate simple fields, call completeValidation on Definition fields.
175         */
176        public void completeValidation() {
177    
178            for (AttributeDefinition attributeDefinition : attributes) {
179                attributeDefinition.completeValidation(getEntryClass(), null);
180            }
181    
182            for (CollectionDefinition collectionDefinition : collections) {
183                collectionDefinition.completeValidation(getEntryClass(), null);
184            }
185    
186            for (RelationshipDefinition relationshipDefinition : relationships) {
187                relationshipDefinition.completeValidation(getEntryClass(), null);
188            }
189        }
190    
191        /**
192         * Directly validate simple fields, call completeValidation on Definition
193         * fields.
194         *
195         * @see org.kuali.rice.krad.datadictionary.DataDictionaryEntry#completeValidation(org.kuali.rice.krad.datadictionary.validator.ValidationTrace)
196         */
197        public void completeValidation(ValidationTrace tracer) {
198            for (AttributeDefinition definition : getAttributes()) {
199                definition.completeValidation(getEntryClass(), null, tracer.getCopy());
200            }
201            for (CollectionDefinition definition : getCollections()) {
202                definition.completeValidation(getEntryClass(), null, tracer.getCopy());
203            }
204            for (RelationshipDefinition definition : getRelationships()) {
205                definition.completeValidation(getEntryClass(), null, tracer.getCopy());
206            }
207        }
208    
209        /**
210         * The attributes element contains attribute
211         * elements.  These define the specifications for business object fields.
212         *
213         * JSTL: attributes is a Map which is accessed by a key of "attributes".
214         * This map contains entries with the following keys:
215         * attributeName of first attribute
216         * attributeName of second attribute
217         * etc.
218         *
219         * The corresponding value for each entry is an attribute ExportMap.
220         * By the time the JSTL export happens, all attributeReferences will be
221         * indistinguishable from attributes.
222         *
223         * See AttributesMapBuilder.java
224         *
225         * The attribute element specifies the way in which a business object
226         * field appears on a screen for data entry or display purposes.  These
227         * specifications include the following:
228         * The title and formatting of the field
229         * Descriptive information about the field
230         * The edits used at time of data-entry
231         *
232         * DD: See AttributeDefinition.java
233         *
234         * JSTL: attribute is a Map which is accessed using a key which is the attributeName
235         * of an attribute.  Each entry contains the following keys:
236         * name (String)
237         * forceUppercase (boolean String)
238         * label (String)
239         * shortLabel (String, copied from label if not present)
240         * maxLength (String)
241         * exclusiveMin (bigdecimal String)
242         * exclusiveMax (bigdecimal String)
243         * validationPattern (Map, optional)
244         * required (boolean String)
245         * control (Map)
246         * summary (String)
247         * description (String)
248         * formatterClass (String, optional)
249         * fullClassName (String)
250         * displayWorkgroup(String, optional)
251         * displayMaskClass(String, optional)
252         *
253         * See AttributesMapBuilder.java
254         * Note: exclusiveMax is mapped from the inclusiveMax element!
255         * The validation logic seems to be assuming inclusiveMax.
256         */
257        public void setAttributes(List<AttributeDefinition> attributes) {
258            attributeMap.clear();
259            for (AttributeDefinition attribute : attributes) {
260                if (attribute == null) {
261                    throw new IllegalArgumentException("invalid (null) attributeDefinition");
262                }
263                String attributeName = attribute.getName();
264                if (StringUtils.isBlank(attributeName)) {
265                    throw new ValidationException("invalid (blank) attributeName");
266                }
267    
268                if (attributeMap.containsKey(attributeName)) {
269                    throw new DuplicateEntryException("attribute '"
270                            + attributeName
271                            + "' already defined as an Attribute for class '"
272                            + getEntryClass().getName()
273                            + "'");
274                } else if (collectionMap.containsKey(attributeName)) {
275                    throw new DuplicateEntryException("attribute '"
276                            + attributeName
277                            + "' already defined as a Collection for class '"
278                            + getEntryClass().getName()
279                            + "'");
280                } else if (complexAttributeMap.containsKey(attributeName)) {
281                    throw new DuplicateEntryException("attribute '"
282                            + attributeName
283                            + "' already defined as an Complex Attribute for class '"
284                            + getEntryClass().getName()
285                            + "'");
286                }
287                attributeMap.put(attributeName, attribute);
288            }
289            this.attributes = attributes;
290        }
291    
292        /**
293         * The collections element contains collection elements.  These define
294         * the lists of other business objects which are related to and
295         * defined in the business objects.
296         *
297         * JSTL: collections is a Map which is accessed by a key of "collections".
298         * This map contains entries with the following keys:
299         * name of first collection
300         * name of second collection
301         * etc.
302         * The corresponding value for each entry is a collection ExportMap.
303         *
304         * The collection element defines the name and description a
305         * list of objects related to the business object.
306         *
307         * DD: See CollectionDefinition.java.
308         *
309         * JSTL: collection is a Map which is accessed using a key which is the
310         * name of the collection.  Each entry contains the following keys:
311         * name (String)
312         * label (String)
313         * shortLabel (String, copied from label if missing)
314         * elementLabel (String, copied from contained class if missing)
315         * summary (String)
316         * description (String)
317         *
318         * See CollectionsMapBuilder.java.
319         */
320        public void setCollections(List<CollectionDefinition> collections) {
321            collectionMap.clear();
322            for (CollectionDefinition collection : collections) {
323                if (collection == null) {
324                    throw new IllegalArgumentException("invalid (null) collectionDefinition");
325                }
326                String collectionName = collection.getName();
327                if (StringUtils.isBlank(collectionName)) {
328                    throw new ValidationException("invalid (blank) collectionName");
329                }
330    
331                if (collectionMap.containsKey(collectionName)) {
332                    throw new DuplicateEntryException("collection '"
333                            + collectionName
334                            + "' already defined for class '"
335                            + getEntryClass().getName()
336                            + "'");
337                } else if (attributeMap.containsKey(collectionName)) {
338                    throw new DuplicateEntryException("collection '"
339                            + collectionName
340                            + "' already defined as an Attribute for class '"
341                            + getEntryClass().getName()
342                            + "'");
343                } else if (complexAttributeMap.containsKey(collectionName)) {
344                    throw new DuplicateEntryException("collection '"
345                            + collectionName
346                            + "' already defined as Complex Attribute for class '"
347                            + getEntryClass().getName()
348                            + "'");
349                }
350    
351                collectionMap.put(collectionName, collection);
352    
353            }
354            this.collections = collections;
355        }
356    
357        /**
358         * The relationships element contains relationship elements.
359         * These are used to map attribute names to fields in a reference object.
360         *
361         * JSTL: relationships is a Map which is accessed by a key of "relationships".
362         * This map contains entries with the following keys:
363         * objectAttributeName of first relationship
364         * objectAttributeName of second relationship
365         * etc.
366         * The corresponding value for each entry is a relationship ExportMap.
367         *
368         * The relationship element defines how primitive attributes of this
369         * class can be used to retrieve an instance of some related Object instance
370         * DD: See RelationshipDefinition.java.
371         *
372         * JSTL: relationship is a Map which is accessed using a key which is the
373         * objectAttributeName of a relationship.  The map contains a single entry
374         * with a key of "primitiveAttributes" and value which is an attributesMap ExportMap.
375         *
376         * The attributesMap ExportMap contains the following keys:
377         * 0   (for first primitiveAttribute)
378         * 1   (for second primitiveAttribute)
379         * etc.
380         * The corresponding value for each entry is an primitiveAttribute ExportMap
381         * which contains the following keys:
382         * "sourceName"
383         * "targetName"
384         *
385         * See RelationshipsMapBuilder.java.
386         */
387        public void setRelationships(List<RelationshipDefinition> relationships) {
388            this.relationships = relationships;
389        }
390    
391        public Set<String> getCollectionNames() {
392            return collectionMap.keySet();
393        }
394    
395        public Set<String> getAttributeNames() {
396            return attributeMap.keySet();
397        }
398    
399        public Set<String> getRelationshipNames() {
400            return relationshipMap.keySet();
401        }
402    
403        /**
404         * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
405         */
406        public void afterPropertiesSet() throws Exception {
407            if (relationships != null) {
408                relationshipMap.clear();
409                for (RelationshipDefinition relationship : relationships) {
410                    if (relationship == null) {
411                        throw new IllegalArgumentException("invalid (null) relationshipDefinition");
412                    }
413                    String relationshipName = relationship.getObjectAttributeName();
414                    if (StringUtils.isBlank(relationshipName)) {
415                        throw new ValidationException("invalid (blank) relationshipName");
416                    }
417                    relationship.setSourceClass(getEntryClass());
418                    relationshipMap.put(relationshipName, relationship);
419                }
420            }
421    
422            //Populate attributes with nested attribute definitions
423            if (complexAttributes != null) {
424                for (ComplexAttributeDefinition complexAttribute : complexAttributes) {
425                    addNestedAttributes(complexAttribute, complexAttribute.getName());
426                }
427            }
428        }
429    
430        /**
431         * recursively add complex attributes
432         *
433         * @param complexAttribute - the complex attribute to add recursively
434         * @param attrPath - a string representation of specifically which attribute (at some depth) is being accessed
435         */
436        private void addNestedAttributes(ComplexAttributeDefinition complexAttribute, String attrPath) {
437            DataDictionaryEntryBase dataDictionaryEntry = (DataDictionaryEntryBase) complexAttribute.getDataObjectEntry();
438    
439            //Add attributes for the complex attibutes
440            for (AttributeDefinition attribute : dataDictionaryEntry.getAttributes()) {
441                String nestedAttributeName = attrPath + "." + attribute.getName();
442                AttributeDefinition nestedAttribute = copyAttributeDefinition(attribute);
443                nestedAttribute.setName(nestedAttributeName);
444    
445                if (!attributeMap.containsKey(nestedAttributeName)) {
446                    this.attributes.add(nestedAttribute);
447                    this.attributeMap.put(nestedAttributeName, nestedAttribute);
448                }
449            }
450    
451            //Recursively add complex attributes
452            List<ComplexAttributeDefinition> nestedComplexAttributes = dataDictionaryEntry.getComplexAttributes();
453            if (nestedComplexAttributes != null) {
454                for (ComplexAttributeDefinition nestedComplexAttribute : nestedComplexAttributes) {
455                    addNestedAttributes(nestedComplexAttribute, attrPath + "." + nestedComplexAttribute.getName());
456                }
457            }
458        }
459    
460        /**
461         * copy an attribute definition
462         *
463         * @param attrDefToCopy - the attribute to create a copy of
464         * @return a copy of the attribute
465         */
466        private AttributeDefinition copyAttributeDefinition(AttributeDefinition attrDefToCopy) {
467            AttributeDefinition attrDefCopy = new AttributeDefinition();
468    
469            try {
470                BeanUtils.copyProperties(attrDefToCopy, attrDefCopy, new String[]{"formatterClass"});
471    
472                //BeanUtils doesn't copy properties w/o "get" read methods, manually copy those here
473                attrDefCopy.setRequired(attrDefToCopy.isRequired());
474    
475            } catch (Exception e) {
476                e.printStackTrace();
477            }
478    
479            return attrDefCopy;
480        }
481    
482        /**
483         * @see org.kuali.rice.krad.datadictionary.DataDictionaryEntry#getStateMapping()
484         */
485        @BeanTagAttribute(name = "stateMapping", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
486        public StateMapping getStateMapping() {
487            return stateMapping;
488        }
489    
490        /**
491         * @see DataDictionaryEntry#setStateMapping(org.kuali.rice.krad.datadictionary.state.StateMapping)
492         */
493        public void setStateMapping(StateMapping stateMapping) {
494            this.stateMapping = stateMapping;
495        }
496    }