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