001    /**
002     * Copyright 2005-2013 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.kns.kim.type;
017    
018    import com.google.common.base.Function;
019    import com.google.common.collect.Lists;
020    import org.apache.commons.beanutils.PropertyUtils;
021    import org.apache.commons.collections.CollectionUtils;
022    import org.apache.commons.lang.StringUtils;
023    import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
024    import org.kuali.rice.core.api.uif.RemotableAbstractWidget;
025    import org.kuali.rice.core.api.uif.RemotableAttributeError;
026    import org.kuali.rice.core.api.uif.RemotableAttributeField;
027    import org.kuali.rice.core.api.uif.RemotableQuickFinder;
028    import org.kuali.rice.core.api.util.RiceKeyConstants;
029    import org.kuali.rice.core.api.util.type.TypeUtils;
030    import org.kuali.rice.core.web.format.Formatter;
031    import org.kuali.rice.kew.api.KewApiServiceLocator;
032    import org.kuali.rice.kew.api.doctype.DocumentType;
033    import org.kuali.rice.kew.api.doctype.DocumentTypeService;
034    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
035    import org.kuali.rice.kim.api.type.KimAttributeField;
036    import org.kuali.rice.kim.api.type.KimType;
037    import org.kuali.rice.kim.api.type.KimTypeAttribute;
038    import org.kuali.rice.kim.api.type.KimTypeInfoService;
039    import org.kuali.rice.kim.framework.type.KimTypeService;
040    import org.kuali.rice.kns.lookup.LookupUtils;
041    import org.kuali.rice.kns.service.KNSServiceLocator;
042    import org.kuali.rice.kns.util.FieldUtils;
043    import org.kuali.rice.kns.web.ui.Field;
044    import org.kuali.rice.krad.bo.BusinessObject;
045    import org.kuali.rice.krad.comparator.StringValueComparator;
046    import org.kuali.rice.krad.datadictionary.AttributeDefinition;
047    import org.kuali.rice.krad.datadictionary.PrimitiveAttributeDefinition;
048    import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
049    import org.kuali.rice.krad.service.BusinessObjectService;
050    import org.kuali.rice.krad.service.DataDictionaryService;
051    import org.kuali.rice.kns.service.DictionaryValidationService;
052    import org.kuali.rice.krad.service.KRADServiceLocator;
053    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
054    import org.kuali.rice.krad.service.ModuleService;
055    import org.kuali.rice.krad.util.ErrorMessage;
056    import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
057    import org.kuali.rice.krad.util.GlobalVariables;
058    import org.kuali.rice.krad.util.KRADUtils;
059    import org.kuali.rice.krad.util.ObjectUtils;
060    
061    import java.beans.PropertyDescriptor;
062    import java.util.AbstractMap;
063    import java.util.ArrayList;
064    import java.util.Collections;
065    import java.util.Comparator;
066    import java.util.HashMap;
067    import java.util.Iterator;
068    import java.util.List;
069    import java.util.Map;
070    import java.util.Set;
071    import java.util.regex.Pattern;
072    
073    /**
074     * A base class for {@code KimTypeService} implementations which read attribute-related information from the Data
075     * Dictionary. This implementation is currently written against the KNS apis for Data Dictionary. Additionally, it
076     * supports the ability to read non-Data Dictionary attribute information from the {@link KimTypeInfoService}.
077     *
078     * @author Kuali Rice Team (rice.collab@kuali.org)
079     */
080    public class DataDictionaryTypeServiceBase implements KimTypeService {
081    
082            private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DataDictionaryTypeServiceBase.class);
083        private static final String ANY_CHAR_PATTERN_S = ".*";
084        private static final Pattern ANY_CHAR_PATTERN = Pattern.compile(ANY_CHAR_PATTERN_S);
085    
086            private BusinessObjectService businessObjectService;
087            private DictionaryValidationService dictionaryValidationService;
088            private DataDictionaryService dataDictionaryService;
089            private KimTypeInfoService typeInfoService;
090        private DocumentTypeService documentTypeService;
091    
092            @Override
093            public String getWorkflowDocumentTypeName() {
094                    return null;
095            }
096    
097            @Override
098            public List<String> getWorkflowRoutingAttributes(String routeLevel) {
099                    if (StringUtils.isBlank(routeLevel)) {
100                throw new RiceIllegalArgumentException("routeLevel was blank or null");
101            }
102    
103            return Collections.emptyList();
104            }
105    
106        @Override
107            public List<KimAttributeField> getAttributeDefinitions(String kimTypeId) {
108            final List<String> uniqueAttributes = getUniqueAttributes(kimTypeId);
109    
110            //using map.entry as a 2-item tuple
111            final List<Map.Entry<String,KimAttributeField>> definitions = new ArrayList<Map.Entry<String,KimAttributeField>>();
112            final KimType kimType = getTypeInfoService().getKimType(kimTypeId);
113            final String nsCode = kimType.getNamespaceCode();
114    
115            for (KimTypeAttribute typeAttribute : kimType.getAttributeDefinitions()) {
116                final KimAttributeField definition;
117                if (typeAttribute.getKimAttribute().getComponentName() == null) {
118                    definition = getNonDataDictionaryAttributeDefinition(nsCode,kimTypeId,typeAttribute, uniqueAttributes);
119                } else {
120                    definition = getDataDictionaryAttributeDefinition(nsCode,kimTypeId,typeAttribute, uniqueAttributes);
121                }
122    
123                if (definition != null) {
124                    definitions.add(new AbstractMap.SimpleEntry<String,KimAttributeField>(typeAttribute.getSortCode() != null ? typeAttribute.getSortCode() : "", definition));
125                }
126            }
127    
128            //sort by sortCode
129            Collections.sort(definitions, new Comparator<Map.Entry<String, KimAttributeField>>() {
130                @Override
131                public int compare(Map.Entry<String, KimAttributeField> o1, Map.Entry<String, KimAttributeField> o2) {
132                    return o1.getKey().compareTo(o2.getKey());
133                }
134            });
135    
136            //transform removing sortCode
137                    return Collections.unmodifiableList(Lists.transform(definitions, new Function<Map.Entry<String, KimAttributeField>, KimAttributeField>() {
138                @Override
139                public KimAttributeField apply(Map.Entry<String, KimAttributeField> v) {
140                    return v.getValue();
141                }
142            }));
143            }
144    
145        /**
146             * This is the default implementation.  It calls into the service for each attribute to
147             * validate it there.  No combination validation is done.  That should be done
148             * by overriding this method.
149             */
150            @Override
151            public List<RemotableAttributeError> validateAttributes(String kimTypeId, Map<String, String> attributes) {
152                    if (StringUtils.isBlank(kimTypeId)) {
153                throw new RiceIllegalArgumentException("kimTypeId was null or blank");
154            }
155    
156            if (attributes == null) {
157                throw new RiceIllegalArgumentException("attributes was null or blank");
158            }
159    
160            final List<RemotableAttributeError> validationErrors = new ArrayList<RemotableAttributeError>();
161                    KimType kimType = getTypeInfoService().getKimType(kimTypeId);
162    
163                    for ( Map.Entry<String, String> entry : attributes.entrySet() ) {
164                KimTypeAttribute attr = kimType.getAttributeDefinitionByName(entry.getKey());
165                            final List<RemotableAttributeError> attributeErrors;
166                if ( attr.getKimAttribute().getComponentName() == null) {
167                    attributeErrors = validateNonDataDictionaryAttribute(attr, entry.getKey(), entry.getValue());
168                } else {
169                    attributeErrors = validateDataDictionaryAttribute(attr, entry.getKey(), entry.getValue());
170                }
171    
172                            if ( attributeErrors != null ) {
173                    validationErrors.addAll(attributeErrors);
174                            }
175                    }
176    
177    
178                    final List<RemotableAttributeError> referenceCheckErrors = validateReferencesExistAndActive(kimType, attributes, validationErrors);
179            validationErrors.addAll(referenceCheckErrors);
180    
181                    return Collections.unmodifiableList(validationErrors);
182            }
183    
184        @Override
185            public List<RemotableAttributeError> validateAttributesAgainstExisting(String kimTypeId, Map<String, String> newAttributes, Map<String, String> oldAttributes){
186            if (StringUtils.isBlank(kimTypeId)) {
187                throw new RiceIllegalArgumentException("kimTypeId was null or blank");
188            }
189    
190            if (newAttributes == null) {
191                throw new RiceIllegalArgumentException("newAttributes was null or blank");
192            }
193    
194            if (oldAttributes == null) {
195                throw new RiceIllegalArgumentException("oldAttributes was null or blank");
196            }
197            return Collections.emptyList();
198            //final List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
199            //errors.addAll(validateUniqueAttributes(kimTypeId, newAttributes, oldAttributes));
200            //return Collections.unmodifiableList(errors);
201    
202            }
203    
204            /**
205             *
206             * This method matches input attribute set entries and standard attribute set entries using literal string match.
207             *
208             */
209            protected boolean performMatch(Map<String, String> inputAttributes, Map<String, String> storedAttributes) {
210                    if ( storedAttributes == null || inputAttributes == null ) {
211                            return true;
212                    }
213                    for ( Map.Entry<String, String> entry : storedAttributes.entrySet() ) {
214                            if (inputAttributes.containsKey(entry.getKey()) && !StringUtils.equals(inputAttributes.get(entry.getKey()), entry.getValue()) ) {
215                                    return false;
216                            }
217                    }
218                    return true;
219            }
220    
221            protected Map<String, String> translateInputAttributes(Map<String, String> qualification){
222                    return qualification;
223            }
224    
225            protected List<RemotableAttributeError> validateReferencesExistAndActive( KimType kimType, Map<String, String> attributes, List<RemotableAttributeError> previousValidationErrors) {
226                    Map<String, BusinessObject> componentClassInstances = new HashMap<String, BusinessObject>();
227                    List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
228                    
229                    for ( String attributeName : attributes.keySet() ) {
230                            KimTypeAttribute attr = kimType.getAttributeDefinitionByName(attributeName);
231                            
232                            if (StringUtils.isNotBlank(attr.getKimAttribute().getComponentName())) {
233                                    if (!componentClassInstances.containsKey(attr.getKimAttribute().getComponentName())) {
234                                            try {
235                                                    Class<?> componentClass = Class.forName( attr.getKimAttribute().getComponentName() );
236                                                    if (!BusinessObject.class.isAssignableFrom(componentClass)) {
237                                                            LOG.warn("Class " + componentClass.getName() + " does not implement BusinessObject.  Unable to perform reference existence and active validation");
238                                                            continue;
239                                                    }
240                                                    BusinessObject componentInstance = (BusinessObject) componentClass.newInstance();
241                                                    componentClassInstances.put(attr.getKimAttribute().getComponentName(), componentInstance);
242                                            } catch (Exception e) {
243                                                    LOG.error("Unable to instantiate class for attribute: " + attributeName, e);
244                                            }
245                                    }
246                            }
247                    }
248                    
249                    // now that we have instances for each component class, try to populate them with any attribute we can, assuming there were no other validation errors associated with it
250                    for ( Map.Entry<String, String> entry : attributes.entrySet() ) {
251                            if (!RemotableAttributeError.containsAttribute(entry.getKey(), previousValidationErrors)) {
252                                    for (Object componentInstance : componentClassInstances.values()) {
253                                            try {
254                                                    ObjectUtils.setObjectProperty(componentInstance, entry.getKey(), entry.getValue());
255                                            } catch (NoSuchMethodException e) {
256                                                    // this is expected since not all attributes will be in all components
257                                            } catch (Exception e) {
258                                                    LOG.error("Unable to set object property class: " + componentInstance.getClass().getName() + " property: " + entry.getKey(), e);
259                                            }
260                                    }
261                            }
262                    }
263                    
264                    for (Map.Entry<String, BusinessObject> entry : componentClassInstances.entrySet()) {
265                            List<RelationshipDefinition> relationships = getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(entry.getKey()).getRelationships();
266                            if (relationships == null) {
267                                    continue;
268                            }
269                            
270                            for (RelationshipDefinition relationshipDefinition : relationships) {
271                                    List<PrimitiveAttributeDefinition> primitiveAttributes = relationshipDefinition.getPrimitiveAttributes();
272                                    
273                                    // this code assumes that the last defined primitiveAttribute is the attributeToHighlightOnFail
274                                    String attributeToHighlightOnFail = primitiveAttributes.get(primitiveAttributes.size() - 1).getSourceName();
275                                    
276                                    // TODO: will this work for user ID attributes?
277                                    
278                                    if (!attributes.containsKey(attributeToHighlightOnFail)) {
279                                            // if the attribute to highlight wasn't passed in, don't bother validating
280                                            continue;
281                                    }
282                                    
283    
284                                    KimTypeAttribute attr = kimType.getAttributeDefinitionByName(attributeToHighlightOnFail);
285                                    if (attr != null) {
286                                            final String attributeDisplayLabel;
287                        if (StringUtils.isNotBlank(attr.getKimAttribute().getComponentName())) {
288                                                    attributeDisplayLabel = getDataDictionaryService().getAttributeLabel(attr.getKimAttribute().getComponentName(), attributeToHighlightOnFail);
289                                            } else {
290                                                    attributeDisplayLabel = attr.getKimAttribute().getAttributeLabel();
291                                            }
292    
293                                            getDictionaryValidationService().validateReferenceExistsAndIsActive(entry.getValue(), relationshipDefinition.getObjectAttributeName(),
294                                                            attributeToHighlightOnFail, attributeDisplayLabel);
295                                    }
296                    List<String> extractedErrors = extractErrorsFromGlobalVariablesErrorMap(attributeToHighlightOnFail);
297                    if (CollectionUtils.isNotEmpty(extractedErrors)) {
298                                        errors.add(RemotableAttributeError.Builder.create(attributeToHighlightOnFail, extractedErrors).build());
299                    }
300                            }
301                    }
302                    return errors;
303            }
304            
305        protected List<RemotableAttributeError> validateAttributeRequired(String kimTypeId, String objectClassName, String attributeName, Object attributeValue, String errorKey) {
306            List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
307            // check if field is a required field for the business object
308            if (attributeValue == null || (attributeValue instanceof String && StringUtils.isBlank((String) attributeValue))) {
309                    List<KimAttributeField> map = getAttributeDefinitions(kimTypeId);
310                    KimAttributeField definition = DataDictionaryTypeServiceHelper.findAttributeField(attributeName, map);
311                    
312                boolean required = definition.getAttributeField().isRequired();
313                if (required) {
314                    // get label of attribute for message
315                    String errorLabel = DataDictionaryTypeServiceHelper.getAttributeErrorLabel(definition);
316                    errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper
317                            .createErrorString(RiceKeyConstants.ERROR_REQUIRED, errorLabel)).build());
318                }
319            }
320            return errors;
321        }
322        
323            protected List<RemotableAttributeError> validateDataDictionaryAttribute(String kimTypeId, String entryName, Object object, PropertyDescriptor propertyDescriptor) {
324                    return validatePrimitiveFromDescriptor(kimTypeId, entryName, object, propertyDescriptor);
325            }
326    
327        protected List<RemotableAttributeError> validatePrimitiveFromDescriptor(String kimTypeId, String entryName, Object object, PropertyDescriptor propertyDescriptor) {
328            List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
329            // validate the primitive attributes if defined in the dictionary
330            if (null != propertyDescriptor && getDataDictionaryService().isAttributeDefined(entryName, propertyDescriptor.getName())) {
331                Object value = ObjectUtils.getPropertyValue(object, propertyDescriptor.getName());
332                Class<?> propertyType = propertyDescriptor.getPropertyType();
333    
334                if (TypeUtils.isStringClass(propertyType) || TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType) || TypeUtils.isTemporalClass(propertyType)) {
335    
336                    // check value format against dictionary
337                    if (value != null && StringUtils.isNotBlank(value.toString())) {
338                        if (!TypeUtils.isTemporalClass(propertyType)) {
339                            errors.addAll(validateAttributeFormat(kimTypeId, entryName, propertyDescriptor.getName(), value.toString(), propertyDescriptor.getName()));
340                        }
341                    }
342                    else {
343                            // if it's blank, then we check whether the attribute should be required
344                        errors.addAll(validateAttributeRequired(kimTypeId, entryName, propertyDescriptor.getName(), value, propertyDescriptor.getName()));
345                    }
346                }
347            }
348            return errors;
349        }
350        
351        protected Pattern getAttributeValidatingExpression(KimAttributeField definition) {
352            if (definition == null || StringUtils.isBlank(definition.getAttributeField().getRegexConstraint())) {
353                return ANY_CHAR_PATTERN;
354            }
355    
356            return Pattern.compile(definition.getAttributeField().getRegexConstraint());
357         }
358        
359            protected Formatter getAttributeFormatter(KimAttributeField definition) {
360            if (definition.getAttributeField().getDataType() == null) {
361                return null;
362            }
363    
364            return Formatter.getFormatter(definition.getAttributeField().getDataType().getType());
365        }
366        
367    
368        
369            protected Double getAttributeMinValue(KimAttributeField definition) {
370            return definition == null ? null : definition.getAttributeField().getMinValue();
371        }
372    
373            protected Double getAttributeMaxValue(KimAttributeField definition) {
374            return definition == null ? null : definition.getAttributeField().getMaxValue();
375        }
376            
377        protected List<RemotableAttributeError> validateAttributeFormat(String kimTypeId, String objectClassName, String attributeName, String attributeValue, String errorKey) {
378            List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
379    
380            List<KimAttributeField> attributeDefinitions = getAttributeDefinitions(kimTypeId);
381            KimAttributeField definition = DataDictionaryTypeServiceHelper.findAttributeField(attributeName,
382                    attributeDefinitions);
383            
384            String errorLabel = DataDictionaryTypeServiceHelper.getAttributeErrorLabel(definition);
385    
386            if ( LOG.isDebugEnabled() ) {
387                    LOG.debug("(bo, attributeName, attributeValue) = (" + objectClassName + "," + attributeName + "," + attributeValue + ")");
388            }
389    
390            if (StringUtils.isNotBlank(attributeValue)) {
391                Integer maxLength = definition.getAttributeField().getMaxLength();
392                if ((maxLength != null) && (maxLength.intValue() < attributeValue.length())) {
393                    errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper
394                            .createErrorString(RiceKeyConstants.ERROR_MAX_LENGTH, errorLabel, maxLength.toString())).build());
395                    return errors;
396                }
397                Pattern validationExpression = getAttributeValidatingExpression(definition);
398                if (!ANY_CHAR_PATTERN_S.equals(validationExpression.pattern())) {
399                    if ( LOG.isDebugEnabled() ) {
400                            LOG.debug("(bo, attributeName, validationExpression) = (" + objectClassName + "," + attributeName + "," + validationExpression + ")");
401                    }
402    
403                    if (!validationExpression.matcher(attributeValue).matches()) {
404                        boolean isError=true;
405                        final Formatter formatter = getAttributeFormatter(definition);
406                        if (formatter != null) {
407                            Object o = formatter.format(attributeValue);
408                            isError = !validationExpression.matcher(String.valueOf(o)).matches();
409                        }
410                        if (isError) {
411                            errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper
412                                    .createErrorString(definition)).build());
413                        }
414                        return errors;
415                    }
416                }
417                Double min = getAttributeMinValue(definition);
418                if (min != null) {
419                    try {
420                        if (Double.parseDouble(attributeValue) < min) {
421                            errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper
422                                    .createErrorString(RiceKeyConstants.ERROR_INCLUSIVE_MIN, errorLabel, min.toString())).build());
423                            return errors;
424                        }
425                    }
426                    catch (NumberFormatException e) {
427                        // quash; this indicates that the DD contained a min for a non-numeric attribute
428                    }
429                }
430                Double max = getAttributeMaxValue(definition);
431                if (max != null) {
432                    try {
433    
434                        if (Double.parseDouble(attributeValue) > max) {
435                            errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper
436                                    .createErrorString(RiceKeyConstants.ERROR_INCLUSIVE_MAX, errorLabel, max.toString())).build());
437                            return errors;
438                        }
439                    }
440                    catch (NumberFormatException e) {
441                        // quash; this indicates that the DD contained a max for a non-numeric attribute
442                    }
443                }
444            }
445            return errors;
446        }
447    
448    
449    
450        /*
451         * will create a list of errors in the following format:
452         *
453         *
454         * error_key:param1;param2;param3;
455         */
456            protected List<String> extractErrorsFromGlobalVariablesErrorMap(String attributeName) {
457                    Object results = GlobalVariables.getMessageMap().getErrorMessagesForProperty(attributeName);
458                    List<String> errors = new ArrayList<String>();
459            if (results instanceof String) {
460                    errors.add((String)results);
461            } else if ( results != null) {
462                    if (results instanceof List) {
463                            List<?> errorList = (List<?>)results;
464                            for (Object msg : errorList) {
465                                    ErrorMessage errorMessage = (ErrorMessage)msg;
466                                    errors.add(DataDictionaryTypeServiceHelper.createErrorString(errorMessage.getErrorKey(),
467                                errorMessage.getMessageParameters()));
468                                    }
469                    } else {
470                            String [] temp = (String []) results;
471                            for (String string : temp) {
472                                            errors.add(string);
473                                    }
474                    }
475            }
476            GlobalVariables.getMessageMap().removeAllErrorMessagesForProperty(attributeName);
477            return errors;
478            }
479    
480            protected List<RemotableAttributeError> validateNonDataDictionaryAttribute(KimTypeAttribute attr, String key, String value) {
481                    return Collections.emptyList();
482            }
483    
484        protected List<RemotableAttributeError> validateDataDictionaryAttribute(KimTypeAttribute attr, String key, String value) {
485                    try {
486                // create an object of the proper type per the component
487                Object componentObject = Class.forName( attr.getKimAttribute().getComponentName() ).newInstance();
488    
489                if ( attr.getKimAttribute().getAttributeName() != null ) {
490                    // get the bean utils descriptor for accessing the attribute on that object
491                    PropertyDescriptor propertyDescriptor = PropertyUtils.getPropertyDescriptor(componentObject, attr.getKimAttribute().getAttributeName());
492                    if ( propertyDescriptor != null ) {
493                        // set the value on the object so that it can be checked
494                        Object attributeValue = KRADUtils.hydrateAttributeValue(propertyDescriptor.getPropertyType(), value);
495                        if (attributeValue == null) {
496                            attributeValue = value; // not a super-awesome fallback strategy, but...
497                        }
498                        propertyDescriptor.getWriteMethod().invoke( componentObject, attributeValue);
499                        return validateDataDictionaryAttribute(attr.getKimTypeId(), attr.getKimAttribute().getComponentName(), componentObject, propertyDescriptor);
500                    }
501                }
502            } catch (Exception e) {
503                throw new KimTypeAttributeValidationException(e);
504            }
505            return Collections.emptyList();
506            }
507    
508    
509            /**
510             * @param namespaceCode
511             * @param typeAttribute
512             * @return an AttributeDefinition for the given KimTypeAttribute, or null no base AttributeDefinition 
513             * matches the typeAttribute parameter's attributeName.
514             */
515            protected KimAttributeField getDataDictionaryAttributeDefinition( String namespaceCode, String kimTypeId, KimTypeAttribute typeAttribute, List<String> uniqueAttributes) {
516    
517                    final String componentClassName = typeAttribute.getKimAttribute().getComponentName();
518                    final String attributeName = typeAttribute.getKimAttribute().getAttributeName();
519            final Class<? extends BusinessObject> componentClass;
520            final AttributeDefinition baseDefinition;
521    
522                    // try to resolve the component name - if not possible - try to pull the definition from the app mediation service
523                    try {
524                if (StringUtils.isNotBlank(componentClassName)) {
525                    componentClass = (Class<? extends BusinessObject>) Class.forName(componentClassName);
526                    baseDefinition = getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(componentClassName).getAttributeDefinition(attributeName);
527                } else {
528                    baseDefinition = null;
529                    componentClass = null;
530                }
531            } catch (ClassNotFoundException ex) {
532                throw new KimTypeAttributeException(ex);
533                    }
534    
535            if (baseDefinition == null) {
536                return null;
537            }
538            final RemotableAttributeField.Builder definition = RemotableAttributeField.Builder.create(baseDefinition.getName());
539    
540            definition.setLongLabel(baseDefinition.getLabel());
541            definition.setShortLabel(baseDefinition.getShortLabel());
542            definition.setMaxLength(baseDefinition.getMaxLength());
543            definition.setRequired(baseDefinition.isRequired());
544            definition.setForceUpperCase(baseDefinition.getForceUppercase());
545            definition.setControl(DataDictionaryTypeServiceHelper.toRemotableAbstractControlBuilder(
546                    baseDefinition));
547            final RemotableQuickFinder.Builder qf = createQuickFinder(componentClass, attributeName);
548            if (qf != null) {
549                definition.setWidgets(Collections.<RemotableAbstractWidget.Builder>singletonList(qf));
550            }
551            final KimAttributeField.Builder kimField = KimAttributeField.Builder.create(definition, typeAttribute.getKimAttribute().getId());
552    
553            if(uniqueAttributes!=null && uniqueAttributes.contains(definition.getName())){
554                kimField.setUnique(true);
555            }
556    
557                    return kimField.build();
558            }
559    
560        private RemotableQuickFinder.Builder createQuickFinder(Class<? extends BusinessObject> componentClass, String attributeName) {
561    
562            Field field = FieldUtils.getPropertyField(componentClass, attributeName, false);
563            if ( field != null ) {
564                final BusinessObject sampleComponent;
565                try {
566                    sampleComponent = componentClass.newInstance();
567                } catch(InstantiationException e) {
568                    throw new KimTypeAttributeException(e);
569                } catch (IllegalAccessException e) {
570                    throw new KimTypeAttributeException(e);
571                }
572    
573                field = LookupUtils.setFieldQuickfinder( sampleComponent, attributeName, field, Collections.singletonList(attributeName) );
574                if ( StringUtils.isNotBlank( field.getQuickFinderClassNameImpl() ) ) {
575                    final Class<? extends BusinessObject> lookupClass;
576                    try {
577                        lookupClass = (Class<? extends BusinessObject>) Class.forName( field.getQuickFinderClassNameImpl() );
578                    } catch (ClassNotFoundException e) {
579                        throw new KimTypeAttributeException(e);
580                    }
581    
582                    String baseLookupUrl = LookupUtils.getBaseLookupUrl(false) + "?methodToCall=start";
583    
584                    if (ExternalizableBusinessObjectUtils.isExternalizableBusinessObject(lookupClass)) {
585                        ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(lookupClass);
586                        if (moduleService.isExternalizableBusinessObjectLookupable(lookupClass)) {
587                            baseLookupUrl = moduleService.getExternalizableBusinessObjectLookupUrl(lookupClass, Collections.<String,String>emptyMap());
588                            // XXX: I'm not proud of this:
589                            baseLookupUrl = baseLookupUrl.substring(0,baseLookupUrl.indexOf("?")) + "?methodToCall=start";
590                        }
591                    }
592    
593                    final RemotableQuickFinder.Builder builder =
594                            RemotableQuickFinder.Builder.create(baseLookupUrl, lookupClass.getName());
595                    builder.setLookupParameters(toMap(field.getLookupParameters()));
596                    builder.setFieldConversions(toMap(field.getFieldConversions()));
597                    return builder;
598                }
599            }
600            return null;
601        }
602    
603        private static Map<String, String> toMap(String s) {
604            if (StringUtils.isBlank(s)) {
605                return Collections.emptyMap();
606            }
607            final Map<String, String> map = new HashMap<String, String>();
608            for (String string : s.split(",")) {
609                String [] keyVal = string.split(":");
610                map.put(keyVal[0], keyVal[1]);
611            }
612            return Collections.unmodifiableMap(map);
613        }
614    
615            protected KimAttributeField getNonDataDictionaryAttributeDefinition(String namespaceCode, String kimTypeId, KimTypeAttribute typeAttribute, List<String> uniqueAttributes) {
616                    RemotableAttributeField.Builder field = RemotableAttributeField.Builder.create(typeAttribute.getKimAttribute().getAttributeName());
617                    field.setLongLabel(typeAttribute.getKimAttribute().getAttributeLabel());
618    
619            KimAttributeField.Builder definition = KimAttributeField.Builder.create(field, typeAttribute.getKimAttribute().getId());
620    
621            if(uniqueAttributes!=null && uniqueAttributes.contains(typeAttribute.getKimAttribute().getAttributeName())){
622                definition.setUnique(true);
623            }
624                    return definition.build();
625            }
626    
627            protected static final String COMMA_SEPARATOR = ", ";
628    
629            protected void validateRequiredAttributesAgainstReceived(Map<String, String> receivedAttributes){
630                    // abort if type does not want the qualifiers to be checked
631                    if ( !isCheckRequiredAttributes() ) {
632                            return;
633                    }
634                    // abort if the list is empty, no attributes need to be checked
635                    if ( getRequiredAttributes() == null || getRequiredAttributes().isEmpty() ) {
636                            return;
637                    }
638                    List<String> missingAttributes = new ArrayList<String>();
639                    // if attributes are null or empty, they're all missing
640                    if ( receivedAttributes == null || receivedAttributes.isEmpty() ) {
641                            return;         
642                    } else {
643                            for( String requiredAttribute : getRequiredAttributes() ) {
644                                    if( !receivedAttributes.containsKey(requiredAttribute) ) {
645                                            missingAttributes.add(requiredAttribute);
646                                    }
647                            }
648                    }
649            if(!missingAttributes.isEmpty()) {
650                    StringBuilder errorMessage = new StringBuilder();
651                    Iterator<String> attribIter = missingAttributes.iterator();
652                    while ( attribIter.hasNext() ) {
653                            errorMessage.append( attribIter.next() );
654                            if( attribIter.hasNext() ) {
655                                    errorMessage.append( COMMA_SEPARATOR );
656                            }
657                    }
658                    errorMessage.append( " not found in required attributes for this type." );
659                throw new KimTypeAttributeValidationException(errorMessage.toString());
660            }
661            }
662    
663    
664            @Override
665            public List<RemotableAttributeError> validateUniqueAttributes(String kimTypeId, Map<String, String> newAttributes, Map<String, String> oldAttributes) {
666            if (StringUtils.isBlank(kimTypeId)) {
667                throw new RiceIllegalArgumentException("kimTypeId was null or blank");
668            }
669    
670            if (newAttributes == null) {
671                throw new RiceIllegalArgumentException("newAttributes was null or blank");
672            }
673    
674            if (oldAttributes == null) {
675                throw new RiceIllegalArgumentException("oldAttributes was null or blank");
676            }
677            List<String> uniqueAttributes = getUniqueAttributes(kimTypeId);
678                    if(uniqueAttributes==null || uniqueAttributes.isEmpty()){
679                            return Collections.emptyList();
680                    } else{
681                            List<RemotableAttributeError> m = new ArrayList<RemotableAttributeError>();
682                if(areAttributesEqual(uniqueAttributes, newAttributes, oldAttributes)){
683                                    //add all unique attrs to error map
684                    for (String a : uniqueAttributes) {
685                        m.add(RemotableAttributeError.Builder.create(a, RiceKeyConstants.ERROR_DUPLICATE_ENTRY).build());
686                    }
687    
688                    return m;
689                            }
690                    }
691                    return Collections.emptyList();
692            }
693            
694            protected boolean areAttributesEqual(List<String> uniqueAttributeNames, Map<String, String> aSet1, Map<String, String> aSet2){
695                    StringValueComparator comparator = StringValueComparator.getInstance();
696                    for(String uniqueAttributeName: uniqueAttributeNames){
697                            String attrVal1 = getAttributeValue(aSet1, uniqueAttributeName);
698                            String attrVal2 = getAttributeValue(aSet2, uniqueAttributeName);
699                            if(comparator.compare(attrVal1, attrVal2)!=0){
700                                    return false;
701                            }
702                    }
703                    return true;
704            }
705    
706            protected String getAttributeValue(Map<String, String> aSet, String attributeName){
707                    if(StringUtils.isEmpty(attributeName)) {
708                            return null;
709                    }
710                    for(Map.Entry<String, String> entry : aSet.entrySet()){
711                            if(attributeName.equals(entry.getKey())) {
712                                    return entry.getValue();
713                            }
714                    }
715                    return null;
716            }
717    
718            protected List<String> getUniqueAttributes(String kimTypeId){
719                    KimType kimType = getTypeInfoService().getKimType(kimTypeId);
720            List<String> uniqueAttributes = new ArrayList<String>();
721            if ( kimType != null ) {
722                    for(KimTypeAttribute attributeDefinition: kimType.getAttributeDefinitions()){
723                            uniqueAttributes.add(attributeDefinition.getKimAttribute().getAttributeName());
724                    }
725            } else {
726                    LOG.error("Unable to retrieve a KimTypeInfo for a null kimTypeId in getUniqueAttributes()");
727            }
728            return Collections.unmodifiableList(uniqueAttributes);
729            }
730    
731        @Override
732            public List<RemotableAttributeError> validateUnmodifiableAttributes(String kimTypeId, Map<String, String> originalAttributes, Map<String, String> newAttributes){
733            if (StringUtils.isBlank(kimTypeId)) {
734                throw new RiceIllegalArgumentException("kimTypeId was null or blank");
735            }
736    
737            if (newAttributes == null) {
738                throw new RiceIllegalArgumentException("newAttributes was null or blank");
739            }
740    
741            if (originalAttributes == null) {
742                throw new RiceIllegalArgumentException("oldAttributes was null or blank");
743            }
744            List<RemotableAttributeError> validationErrors = new ArrayList<RemotableAttributeError>();
745                    KimType kimType = getTypeInfoService().getKimType(kimTypeId);
746                    List<String> uniqueAttributes = getUniqueAttributes(kimTypeId);
747                    for(String attributeNameKey: uniqueAttributes){
748                            KimTypeAttribute attr = kimType.getAttributeDefinitionByName(attributeNameKey);
749                            String mainAttributeValue = getAttributeValue(originalAttributes, attributeNameKey);
750                            String delegationAttributeValue = getAttributeValue(newAttributes, attributeNameKey);
751    
752                            if(!StringUtils.equals(mainAttributeValue, delegationAttributeValue)){
753                                    validationErrors.add(RemotableAttributeError.Builder.create(attributeNameKey, DataDictionaryTypeServiceHelper
754                            .createErrorString(RiceKeyConstants.ERROR_CANT_BE_MODIFIED,
755                                    dataDictionaryService.getAttributeLabel(attr.getKimAttribute().getComponentName(),
756                                            attributeNameKey))).build());
757                            }
758                    }
759                    return validationErrors;
760            }
761    
762        protected List<String> getRequiredAttributes() {
763            return Collections.emptyList();
764        }
765    
766            protected boolean isCheckRequiredAttributes() {
767                    return false;
768            }
769    
770            protected String getClosestParentDocumentTypeName(
771                            DocumentType documentType,
772                            Set<String> potentialParentDocumentTypeNames) {
773                    if ( potentialParentDocumentTypeNames == null || documentType == null ) {
774                            return null;
775                    }
776                    if (potentialParentDocumentTypeNames.contains(documentType.getName())) {
777                            return documentType.getName();
778                    } 
779                    if ((documentType.getParentId() == null)
780                                    || documentType.getParentId().equals(
781                                                    documentType.getId())) {
782                            return null;
783                    } 
784                    return getClosestParentDocumentTypeName(getDocumentTypeService().getDocumentTypeById(documentType
785                                    .getParentId()), potentialParentDocumentTypeNames);
786            }
787    
788        protected static class KimTypeAttributeValidationException extends RuntimeException {
789    
790            protected KimTypeAttributeValidationException(String message) {
791                super( message );
792            }
793    
794            protected KimTypeAttributeValidationException(Throwable cause) {
795                super( cause );
796            }
797    
798            private static final long serialVersionUID = 8220618846321607801L;
799    
800        }
801    
802        protected static class KimTypeAttributeException extends RuntimeException {
803    
804            protected KimTypeAttributeException(String message) {
805                super( message );
806            }
807    
808            protected KimTypeAttributeException(Throwable cause) {
809                super( cause );
810            }
811    
812            private static final long serialVersionUID = 8220618846321607801L;
813    
814        }
815    
816        protected KimTypeInfoService getTypeInfoService() {
817                    if ( typeInfoService == null ) {
818                            typeInfoService = KimApiServiceLocator.getKimTypeInfoService();
819                    }
820                    return typeInfoService;
821            }
822    
823            protected BusinessObjectService getBusinessObjectService() {
824                    if ( businessObjectService == null ) {
825                            businessObjectService = KRADServiceLocator.getBusinessObjectService();
826                    }
827                    return businessObjectService;
828            }
829    
830            protected DictionaryValidationService getDictionaryValidationService() {
831                    if ( dictionaryValidationService == null ) {
832                            dictionaryValidationService = KNSServiceLocator.getKNSDictionaryValidationService();
833                    }
834                    return dictionaryValidationService;
835            }
836    
837            protected DataDictionaryService getDataDictionaryService() {
838                    if ( dataDictionaryService == null ) {
839                            dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
840                    }
841                    return this.dataDictionaryService;
842            }
843    
844    
845            protected DocumentTypeService getDocumentTypeService() {
846                    if ( documentTypeService == null ) {
847                            documentTypeService = KewApiServiceLocator.getDocumentTypeService();
848                    }
849                    return this.documentTypeService;
850            }
851    }