001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kns.kim.type;
017
018import com.google.common.base.Function;
019import com.google.common.collect.Lists;
020import org.apache.commons.beanutils.PropertyUtils;
021import org.apache.commons.collections.CollectionUtils;
022import org.apache.commons.lang.StringUtils;
023import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
024import org.kuali.rice.core.api.uif.RemotableAbstractWidget;
025import org.kuali.rice.core.api.uif.RemotableAttributeError;
026import org.kuali.rice.core.api.uif.RemotableAttributeField;
027import org.kuali.rice.core.api.uif.RemotableQuickFinder;
028import org.kuali.rice.core.api.util.RiceKeyConstants;
029import org.kuali.rice.core.api.util.type.TypeUtils;
030import org.kuali.rice.core.web.format.Formatter;
031import org.kuali.rice.kew.api.KewApiServiceLocator;
032import org.kuali.rice.kew.api.doctype.DocumentType;
033import org.kuali.rice.kew.api.doctype.DocumentTypeService;
034import org.kuali.rice.kim.api.services.KimApiServiceLocator;
035import org.kuali.rice.kim.api.type.KimAttributeField;
036import org.kuali.rice.kim.api.type.KimType;
037import org.kuali.rice.kim.api.type.KimTypeAttribute;
038import org.kuali.rice.kim.api.type.KimTypeInfoService;
039import org.kuali.rice.kim.framework.type.KimTypeService;
040import org.kuali.rice.kns.lookup.LookupUtils;
041import org.kuali.rice.kns.service.KNSServiceLocator;
042import org.kuali.rice.kns.util.FieldUtils;
043import org.kuali.rice.kns.web.ui.Field;
044import org.kuali.rice.krad.bo.BusinessObject;
045import org.kuali.rice.krad.comparator.StringValueComparator;
046import org.kuali.rice.krad.datadictionary.AttributeDefinition;
047import org.kuali.rice.krad.datadictionary.PrimitiveAttributeDefinition;
048import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
049import org.kuali.rice.krad.service.BusinessObjectService;
050import org.kuali.rice.krad.service.DataDictionaryService;
051import org.kuali.rice.kns.service.DictionaryValidationService;
052import org.kuali.rice.krad.service.KRADServiceLocator;
053import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
054import org.kuali.rice.krad.service.ModuleService;
055import org.kuali.rice.krad.util.ErrorMessage;
056import org.kuali.rice.krad.util.ExternalizableBusinessObjectUtils;
057import org.kuali.rice.krad.util.GlobalVariables;
058import org.kuali.rice.krad.util.KRADUtils;
059import org.kuali.rice.krad.util.ObjectUtils;
060
061import java.beans.PropertyDescriptor;
062import java.util.AbstractMap;
063import java.util.ArrayList;
064import java.util.Collections;
065import java.util.Comparator;
066import java.util.HashMap;
067import java.util.Iterator;
068import java.util.List;
069import java.util.Map;
070import java.util.Set;
071import 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 */
080public 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                relationships = getDataDictionaryService().getDataDictionary().getDataObjectEntry(entry.getKey()).getRelationships();
268                if (relationships == null) {
269                    continue;
270                }
271            }
272                        
273                        for (RelationshipDefinition relationshipDefinition : relationships) {
274                                List<PrimitiveAttributeDefinition> primitiveAttributes = relationshipDefinition.getPrimitiveAttributes();
275                                
276                                // this code assumes that the last defined primitiveAttribute is the attributeToHighlightOnFail
277                                String attributeToHighlightOnFail = primitiveAttributes.get(primitiveAttributes.size() - 1).getSourceName();
278                                
279                                // TODO: will this work for user ID attributes?
280                                
281                                if (!attributes.containsKey(attributeToHighlightOnFail)) {
282                                        // if the attribute to highlight wasn't passed in, don't bother validating
283                                        continue;
284                                }
285                                
286
287                                KimTypeAttribute attr = kimType.getAttributeDefinitionByName(attributeToHighlightOnFail);
288                                if (attr != null) {
289                                        final String attributeDisplayLabel;
290                    if (StringUtils.isNotBlank(attr.getKimAttribute().getComponentName())) {
291                                                attributeDisplayLabel = getDataDictionaryService().getAttributeLabel(attr.getKimAttribute().getComponentName(), attributeToHighlightOnFail);
292                                        } else {
293                                                attributeDisplayLabel = attr.getKimAttribute().getAttributeLabel();
294                                        }
295
296                                        getDictionaryValidationService().validateReferenceExistsAndIsActive(entry.getValue(), relationshipDefinition.getObjectAttributeName(),
297                                                        attributeToHighlightOnFail, attributeDisplayLabel);
298                                }
299                List<String> extractedErrors = extractErrorsFromGlobalVariablesErrorMap(attributeToHighlightOnFail);
300                if (CollectionUtils.isNotEmpty(extractedErrors)) {
301                                    errors.add(RemotableAttributeError.Builder.create(attributeToHighlightOnFail, extractedErrors).build());
302                }
303                        }
304                }
305                return errors;
306        }
307        
308    protected List<RemotableAttributeError> validateAttributeRequired(String kimTypeId, String objectClassName, String attributeName, Object attributeValue, String errorKey) {
309        List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
310        // check if field is a required field for the business object
311        if (attributeValue == null || (attributeValue instanceof String && StringUtils.isBlank((String) attributeValue))) {
312                List<KimAttributeField> map = getAttributeDefinitions(kimTypeId);
313                KimAttributeField definition = DataDictionaryTypeServiceHelper.findAttributeField(attributeName, map);
314                
315            boolean required = definition.getAttributeField().isRequired();
316            if (required) {
317                // get label of attribute for message
318                String errorLabel = DataDictionaryTypeServiceHelper.getAttributeErrorLabel(definition);
319                errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper
320                        .createErrorString(RiceKeyConstants.ERROR_REQUIRED, errorLabel)).build());
321            }
322        }
323        return errors;
324    }
325    
326        protected List<RemotableAttributeError> validateDataDictionaryAttribute(String kimTypeId, String entryName, Object object, PropertyDescriptor propertyDescriptor) {
327                return validatePrimitiveFromDescriptor(kimTypeId, entryName, object, propertyDescriptor);
328        }
329
330    protected List<RemotableAttributeError> validatePrimitiveFromDescriptor(String kimTypeId, String entryName, Object object, PropertyDescriptor propertyDescriptor) {
331        List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
332        // validate the primitive attributes if defined in the dictionary
333        if (null != propertyDescriptor && getDataDictionaryService().isAttributeDefined(entryName, propertyDescriptor.getName())) {
334            Object value = ObjectUtils.getPropertyValue(object, propertyDescriptor.getName());
335            Class<?> propertyType = propertyDescriptor.getPropertyType();
336
337            if (TypeUtils.isStringClass(propertyType) || TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType) || TypeUtils.isTemporalClass(propertyType)) {
338
339                // check value format against dictionary
340                if (value != null && StringUtils.isNotBlank(value.toString())) {
341                    if (!TypeUtils.isTemporalClass(propertyType)) {
342                        errors.addAll(validateAttributeFormat(kimTypeId, entryName, propertyDescriptor.getName(), value.toString(), propertyDescriptor.getName()));
343                    }
344                }
345                else {
346                        // if it's blank, then we check whether the attribute should be required
347                    errors.addAll(validateAttributeRequired(kimTypeId, entryName, propertyDescriptor.getName(), value, propertyDescriptor.getName()));
348                }
349            }
350        }
351        return errors;
352    }
353    
354    protected Pattern getAttributeValidatingExpression(KimAttributeField definition) {
355        if (definition == null || StringUtils.isBlank(definition.getAttributeField().getRegexConstraint())) {
356            return ANY_CHAR_PATTERN;
357        }
358
359        return Pattern.compile(definition.getAttributeField().getRegexConstraint());
360     }
361    
362        protected Formatter getAttributeFormatter(KimAttributeField definition) {
363        if (definition.getAttributeField().getDataType() == null) {
364            return null;
365        }
366
367        return Formatter.getFormatter(definition.getAttributeField().getDataType().getType());
368    }
369    
370
371    
372        protected Double getAttributeMinValue(KimAttributeField definition) {
373        return definition == null ? null : definition.getAttributeField().getMinValue();
374    }
375
376        protected Double getAttributeMaxValue(KimAttributeField definition) {
377        return definition == null ? null : definition.getAttributeField().getMaxValue();
378    }
379        
380    protected List<RemotableAttributeError> validateAttributeFormat(String kimTypeId, String objectClassName, String attributeName, String attributeValue, String errorKey) {
381        List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
382
383        List<KimAttributeField> attributeDefinitions = getAttributeDefinitions(kimTypeId);
384        KimAttributeField definition = DataDictionaryTypeServiceHelper.findAttributeField(attributeName,
385                attributeDefinitions);
386        
387        String errorLabel = DataDictionaryTypeServiceHelper.getAttributeErrorLabel(definition);
388
389        if ( LOG.isDebugEnabled() ) {
390                LOG.debug("(bo, attributeName, attributeValue) = (" + objectClassName + "," + attributeName + "," + attributeValue + ")");
391        }
392
393        if (StringUtils.isNotBlank(attributeValue)) {
394            Integer maxLength = definition.getAttributeField().getMaxLength();
395            if ((maxLength != null) && (maxLength.intValue() < attributeValue.length())) {
396                errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper
397                        .createErrorString(RiceKeyConstants.ERROR_MAX_LENGTH, errorLabel, maxLength.toString())).build());
398                return errors;
399            }
400            Pattern validationExpression = getAttributeValidatingExpression(definition);
401            if (!ANY_CHAR_PATTERN_S.equals(validationExpression.pattern())) {
402                if ( LOG.isDebugEnabled() ) {
403                        LOG.debug("(bo, attributeName, validationExpression) = (" + objectClassName + "," + attributeName + "," + validationExpression + ")");
404                }
405
406                if (!validationExpression.matcher(attributeValue).matches()) {
407                    boolean isError=true;
408                    final Formatter formatter = getAttributeFormatter(definition);
409                    if (formatter != null) {
410                        Object o = formatter.format(attributeValue);
411                        isError = !validationExpression.matcher(String.valueOf(o)).matches();
412                    }
413                    if (isError) {
414                        errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper
415                                .createErrorString(definition)).build());
416                    }
417                    return errors;
418                }
419            }
420            Double min = getAttributeMinValue(definition);
421            if (min != null) {
422                try {
423                    if (Double.parseDouble(attributeValue) < min) {
424                        errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper
425                                .createErrorString(RiceKeyConstants.ERROR_INCLUSIVE_MIN, errorLabel, min.toString())).build());
426                        return errors;
427                    }
428                }
429                catch (NumberFormatException e) {
430                    // quash; this indicates that the DD contained a min for a non-numeric attribute
431                }
432            }
433            Double max = getAttributeMaxValue(definition);
434            if (max != null) {
435                try {
436
437                    if (Double.parseDouble(attributeValue) > max) {
438                        errors.add(RemotableAttributeError.Builder.create(errorKey, DataDictionaryTypeServiceHelper
439                                .createErrorString(RiceKeyConstants.ERROR_INCLUSIVE_MAX, errorLabel, max.toString())).build());
440                        return errors;
441                    }
442                }
443                catch (NumberFormatException e) {
444                    // quash; this indicates that the DD contained a max for a non-numeric attribute
445                }
446            }
447        }
448        return errors;
449    }
450
451
452
453    /*
454     * will create a list of errors in the following format:
455     *
456     *
457     * error_key:param1;param2;param3;
458     */
459        protected List<String> extractErrorsFromGlobalVariablesErrorMap(String attributeName) {
460                Object results = GlobalVariables.getMessageMap().getErrorMessagesForProperty(attributeName);
461                List<String> errors = new ArrayList<String>();
462        if (results instanceof String) {
463                errors.add((String)results);
464        } else if ( results != null) {
465                if (results instanceof List) {
466                        List<?> errorList = (List<?>)results;
467                        for (Object msg : errorList) {
468                                ErrorMessage errorMessage = (ErrorMessage)msg;
469                                errors.add(DataDictionaryTypeServiceHelper.createErrorString(errorMessage.getErrorKey(),
470                            errorMessage.getMessageParameters()));
471                                }
472                } else {
473                        String [] temp = (String []) results;
474                        for (String string : temp) {
475                                        errors.add(string);
476                                }
477                }
478        }
479        GlobalVariables.getMessageMap().removeAllErrorMessagesForProperty(attributeName);
480        return errors;
481        }
482
483        protected List<RemotableAttributeError> validateNonDataDictionaryAttribute(KimTypeAttribute attr, String key, String value) {
484                return Collections.emptyList();
485        }
486
487    protected List<RemotableAttributeError> validateDataDictionaryAttribute(KimTypeAttribute attr, String key, String value) {
488                try {
489            // create an object of the proper type per the component
490            Object componentObject = Class.forName( attr.getKimAttribute().getComponentName() ).newInstance();
491
492            if ( attr.getKimAttribute().getAttributeName() != null ) {
493                // get the bean utils descriptor for accessing the attribute on that object
494                PropertyDescriptor propertyDescriptor = PropertyUtils.getPropertyDescriptor(componentObject, attr.getKimAttribute().getAttributeName());
495                if ( propertyDescriptor != null ) {
496                    // set the value on the object so that it can be checked
497                    Object attributeValue = KRADUtils.hydrateAttributeValue(propertyDescriptor.getPropertyType(), value);
498                    if (attributeValue == null) {
499                        attributeValue = value; // not a super-awesome fallback strategy, but...
500                    }
501                    propertyDescriptor.getWriteMethod().invoke( componentObject, attributeValue);
502                    return validateDataDictionaryAttribute(attr.getKimTypeId(), attr.getKimAttribute().getComponentName(), componentObject, propertyDescriptor);
503                }
504            }
505        } catch (Exception e) {
506            throw new KimTypeAttributeValidationException(e);
507        }
508        return Collections.emptyList();
509        }
510
511
512        /**
513         * @param namespaceCode
514         * @param typeAttribute
515         * @return an AttributeDefinition for the given KimTypeAttribute, or null no base AttributeDefinition 
516         * matches the typeAttribute parameter's attributeName.
517         */
518        protected KimAttributeField getDataDictionaryAttributeDefinition( String namespaceCode, String kimTypeId, KimTypeAttribute typeAttribute, List<String> uniqueAttributes) {
519
520                final String componentClassName = typeAttribute.getKimAttribute().getComponentName();
521                final String attributeName = typeAttribute.getKimAttribute().getAttributeName();
522        final Class<? extends BusinessObject> componentClass;
523        final AttributeDefinition baseDefinition;
524
525                // try to resolve the component name - if not possible - try to pull the definition from the app mediation service
526                try {
527            if (StringUtils.isNotBlank(componentClassName)) {
528                componentClass = (Class<? extends BusinessObject>) Class.forName(componentClassName);
529                AttributeDefinition baseDefinitionTemp =
530                        getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(componentClassName)
531                                .getAttributeDefinition(attributeName);
532                if (baseDefinitionTemp == null) {
533                    baseDefinition = getDataDictionaryService().getDataDictionary().getDataObjectEntry(
534                            componentClassName).getAttributeDefinition(attributeName);
535                } else {
536                    baseDefinition = baseDefinitionTemp;
537                }
538            } else {
539                baseDefinition = null;
540                componentClass = null;
541            }
542        } catch (ClassNotFoundException ex) {
543            throw new KimTypeAttributeException(ex);
544                }
545
546        if (baseDefinition == null) {
547            return null;
548        }
549        final RemotableAttributeField.Builder definition = RemotableAttributeField.Builder.create(baseDefinition.getName());
550
551        definition.setLongLabel(baseDefinition.getLabel());
552        definition.setShortLabel(baseDefinition.getShortLabel());
553        definition.setMaxLength(baseDefinition.getMaxLength());
554
555        if (baseDefinition.isRequired() != null) {
556            definition.setRequired(baseDefinition.isRequired());
557        } else {
558            definition.setRequired(false);
559        }
560
561        if (baseDefinition.getForceUppercase() != null) {
562            definition.setForceUpperCase(baseDefinition.getForceUppercase());
563        }
564        definition.setControl(DataDictionaryTypeServiceHelper.toRemotableAbstractControlBuilder(
565                baseDefinition));
566        final RemotableQuickFinder.Builder qf = createQuickFinder(componentClass, attributeName);
567        if (qf != null) {
568            definition.setWidgets(Collections.<RemotableAbstractWidget.Builder>singletonList(qf));
569        }
570        final KimAttributeField.Builder kimField = KimAttributeField.Builder.create(definition, typeAttribute.getKimAttribute().getId());
571
572        if(uniqueAttributes!=null && uniqueAttributes.contains(definition.getName())){
573            kimField.setUnique(true);
574        }
575
576                return kimField.build();
577        }
578
579    private RemotableQuickFinder.Builder createQuickFinder(Class<? extends BusinessObject> componentClass, String attributeName) {
580
581        Field field = FieldUtils.getPropertyField(componentClass, attributeName, false);
582        if ( field != null ) {
583            final BusinessObject sampleComponent;
584            try {
585                sampleComponent = componentClass.newInstance();
586            } catch(InstantiationException e) {
587                throw new KimTypeAttributeException(e);
588            } catch (IllegalAccessException e) {
589                throw new KimTypeAttributeException(e);
590            }
591
592            field = LookupUtils.setFieldQuickfinder( sampleComponent, attributeName, field, Collections.singletonList(attributeName) );
593            if ( StringUtils.isNotBlank( field.getQuickFinderClassNameImpl() ) ) {
594                final Class<? extends BusinessObject> lookupClass;
595                try {
596                    lookupClass = (Class<? extends BusinessObject>) Class.forName( field.getQuickFinderClassNameImpl() );
597                } catch (ClassNotFoundException e) {
598                    throw new KimTypeAttributeException(e);
599                }
600
601                String baseLookupUrl = LookupUtils.getBaseLookupUrl(false) + "?methodToCall=start";
602
603                if (ExternalizableBusinessObjectUtils.isExternalizableBusinessObject(lookupClass)) {
604                    ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getResponsibleModuleService(lookupClass);
605                    if (moduleService.isExternalizableBusinessObjectLookupable(lookupClass)) {
606                        baseLookupUrl = moduleService.getExternalizableBusinessObjectLookupUrl(lookupClass, Collections.<String,String>emptyMap());
607                        // XXX: I'm not proud of this:
608                        baseLookupUrl = baseLookupUrl.substring(0,baseLookupUrl.indexOf("?")) + "?methodToCall=start";
609                    }
610                }
611
612                final RemotableQuickFinder.Builder builder =
613                        RemotableQuickFinder.Builder.create(baseLookupUrl, lookupClass.getName());
614                builder.setLookupParameters(toMap(field.getLookupParameters()));
615                builder.setFieldConversions(toMap(field.getFieldConversions()));
616                return builder;
617            }
618        }
619        return null;
620    }
621
622    private static Map<String, String> toMap(String s) {
623        if (StringUtils.isBlank(s)) {
624            return Collections.emptyMap();
625        }
626        final Map<String, String> map = new HashMap<String, String>();
627        for (String string : s.split(",")) {
628            String [] keyVal = string.split(":");
629            map.put(keyVal[0], keyVal[1]);
630        }
631        return Collections.unmodifiableMap(map);
632    }
633
634        protected KimAttributeField getNonDataDictionaryAttributeDefinition(String namespaceCode, String kimTypeId, KimTypeAttribute typeAttribute, List<String> uniqueAttributes) {
635                RemotableAttributeField.Builder field = RemotableAttributeField.Builder.create(typeAttribute.getKimAttribute().getAttributeName());
636                field.setLongLabel(typeAttribute.getKimAttribute().getAttributeLabel());
637
638        //KULRICE-9143 shortLabel must be set for KIM to render attribute
639        field.setShortLabel(typeAttribute.getKimAttribute().getAttributeLabel());
640
641        KimAttributeField.Builder definition = KimAttributeField.Builder.create(field, typeAttribute.getKimAttribute().getId());
642
643        if(uniqueAttributes!=null && uniqueAttributes.contains(typeAttribute.getKimAttribute().getAttributeName())){
644            definition.setUnique(true);
645        }
646                return definition.build();
647        }
648
649        protected static final String COMMA_SEPARATOR = ", ";
650
651        protected void validateRequiredAttributesAgainstReceived(Map<String, String> receivedAttributes){
652                // abort if type does not want the qualifiers to be checked
653                if ( !isCheckRequiredAttributes() ) {
654                        return;
655                }
656                // abort if the list is empty, no attributes need to be checked
657                if ( getRequiredAttributes() == null || getRequiredAttributes().isEmpty() ) {
658                        return;
659                }
660                List<String> missingAttributes = new ArrayList<String>();
661                // if attributes are null or empty, they're all missing
662                if ( receivedAttributes == null || receivedAttributes.isEmpty() ) {
663                        return;         
664                } else {
665                        for( String requiredAttribute : getRequiredAttributes() ) {
666                                if( !receivedAttributes.containsKey(requiredAttribute) ) {
667                                        missingAttributes.add(requiredAttribute);
668                                }
669                        }
670                }
671        if(!missingAttributes.isEmpty()) {
672                StringBuilder errorMessage = new StringBuilder();
673                Iterator<String> attribIter = missingAttributes.iterator();
674                while ( attribIter.hasNext() ) {
675                        errorMessage.append( attribIter.next() );
676                        if( attribIter.hasNext() ) {
677                                errorMessage.append( COMMA_SEPARATOR );
678                        }
679                }
680                errorMessage.append( " not found in required attributes for this type." );
681            throw new KimTypeAttributeValidationException(errorMessage.toString());
682        }
683        }
684
685
686        @Override
687        public List<RemotableAttributeError> validateUniqueAttributes(String kimTypeId, Map<String, String> newAttributes, Map<String, String> oldAttributes) {
688        if (StringUtils.isBlank(kimTypeId)) {
689            throw new RiceIllegalArgumentException("kimTypeId was null or blank");
690        }
691
692        if (newAttributes == null) {
693            throw new RiceIllegalArgumentException("newAttributes was null or blank");
694        }
695
696        if (oldAttributes == null) {
697            throw new RiceIllegalArgumentException("oldAttributes was null or blank");
698        }
699        List<String> uniqueAttributes = getUniqueAttributes(kimTypeId);
700                if(uniqueAttributes==null || uniqueAttributes.isEmpty()){
701                        return Collections.emptyList();
702                } else{
703                        List<RemotableAttributeError> m = new ArrayList<RemotableAttributeError>();
704            if(areAttributesEqual(uniqueAttributes, newAttributes, oldAttributes)){
705                                //add all unique attrs to error map
706                for (String a : uniqueAttributes) {
707                    m.add(RemotableAttributeError.Builder.create(a, RiceKeyConstants.ERROR_DUPLICATE_ENTRY).build());
708                }
709
710                return m;
711                        }
712                }
713                return Collections.emptyList();
714        }
715        
716        protected boolean areAttributesEqual(List<String> uniqueAttributeNames, Map<String, String> aSet1, Map<String, String> aSet2){
717                StringValueComparator comparator = StringValueComparator.getInstance();
718                for(String uniqueAttributeName: uniqueAttributeNames){
719                        String attrVal1 = getAttributeValue(aSet1, uniqueAttributeName);
720                        String attrVal2 = getAttributeValue(aSet2, uniqueAttributeName);
721                        if(comparator.compare(attrVal1, attrVal2)!=0){
722                                return false;
723                        }
724                }
725                return true;
726        }
727
728        protected String getAttributeValue(Map<String, String> aSet, String attributeName){
729                if(StringUtils.isEmpty(attributeName)) {
730                        return null;
731                }
732                for(Map.Entry<String, String> entry : aSet.entrySet()){
733                        if(attributeName.equals(entry.getKey())) {
734                                return entry.getValue();
735                        }
736                }
737                return null;
738        }
739
740        protected List<String> getUniqueAttributes(String kimTypeId){
741                KimType kimType = getTypeInfoService().getKimType(kimTypeId);
742        List<String> uniqueAttributes = new ArrayList<String>();
743        if ( kimType != null ) {
744                for(KimTypeAttribute attributeDefinition: kimType.getAttributeDefinitions()){
745                        uniqueAttributes.add(attributeDefinition.getKimAttribute().getAttributeName());
746                }
747        } else {
748                LOG.error("Unable to retrieve a KimTypeInfo for a null kimTypeId in getUniqueAttributes()");
749        }
750        return Collections.unmodifiableList(uniqueAttributes);
751        }
752
753    @Override
754        public List<RemotableAttributeError> validateUnmodifiableAttributes(String kimTypeId, Map<String, String> originalAttributes, Map<String, String> newAttributes){
755        if (StringUtils.isBlank(kimTypeId)) {
756            throw new RiceIllegalArgumentException("kimTypeId was null or blank");
757        }
758
759        if (newAttributes == null) {
760            throw new RiceIllegalArgumentException("newAttributes was null or blank");
761        }
762
763        if (originalAttributes == null) {
764            throw new RiceIllegalArgumentException("oldAttributes was null or blank");
765        }
766        List<RemotableAttributeError> validationErrors = new ArrayList<RemotableAttributeError>();
767                KimType kimType = getTypeInfoService().getKimType(kimTypeId);
768                List<String> uniqueAttributes = getUniqueAttributes(kimTypeId);
769                for(String attributeNameKey: uniqueAttributes){
770                        KimTypeAttribute attr = kimType.getAttributeDefinitionByName(attributeNameKey);
771                        String mainAttributeValue = getAttributeValue(originalAttributes, attributeNameKey);
772                        String delegationAttributeValue = getAttributeValue(newAttributes, attributeNameKey);
773
774                        if(!StringUtils.equals(mainAttributeValue, delegationAttributeValue)){
775                                validationErrors.add(RemotableAttributeError.Builder.create(attributeNameKey, DataDictionaryTypeServiceHelper
776                        .createErrorString(RiceKeyConstants.ERROR_CANT_BE_MODIFIED,
777                                dataDictionaryService.getAttributeLabel(attr.getKimAttribute().getComponentName(),
778                                        attributeNameKey))).build());
779                        }
780                }
781                return validationErrors;
782        }
783
784    protected List<String> getRequiredAttributes() {
785        return Collections.emptyList();
786    }
787
788        protected boolean isCheckRequiredAttributes() {
789                return false;
790        }
791
792        protected String getClosestParentDocumentTypeName(
793                        DocumentType documentType,
794                        Set<String> potentialParentDocumentTypeNames) {
795                if ( potentialParentDocumentTypeNames == null || documentType == null ) {
796                        return null;
797                }
798                if (potentialParentDocumentTypeNames.contains(documentType.getName())) {
799                        return documentType.getName();
800                } 
801                if ((documentType.getParentId() == null)
802                                || documentType.getParentId().equals(
803                                                documentType.getId())) {
804                        return null;
805                } 
806                return getClosestParentDocumentTypeName(getDocumentTypeService().getDocumentTypeById(documentType
807                                .getParentId()), potentialParentDocumentTypeNames);
808        }
809
810    protected static class KimTypeAttributeValidationException extends RuntimeException {
811
812        protected KimTypeAttributeValidationException(String message) {
813            super( message );
814        }
815
816        protected KimTypeAttributeValidationException(Throwable cause) {
817            super( cause );
818        }
819
820        private static final long serialVersionUID = 8220618846321607801L;
821
822    }
823
824    protected static class KimTypeAttributeException extends RuntimeException {
825
826        protected KimTypeAttributeException(String message) {
827            super( message );
828        }
829
830        protected KimTypeAttributeException(Throwable cause) {
831            super( cause );
832        }
833
834        private static final long serialVersionUID = 8220618846321607801L;
835
836    }
837
838    protected KimTypeInfoService getTypeInfoService() {
839                if ( typeInfoService == null ) {
840                        typeInfoService = KimApiServiceLocator.getKimTypeInfoService();
841                }
842                return typeInfoService;
843        }
844
845        protected BusinessObjectService getBusinessObjectService() {
846                if ( businessObjectService == null ) {
847                        businessObjectService = KRADServiceLocator.getBusinessObjectService();
848                }
849                return businessObjectService;
850        }
851
852        protected DictionaryValidationService getDictionaryValidationService() {
853                if ( dictionaryValidationService == null ) {
854                        dictionaryValidationService = KNSServiceLocator.getKNSDictionaryValidationService();
855                }
856                return dictionaryValidationService;
857        }
858
859        protected DataDictionaryService getDataDictionaryService() {
860                if ( dataDictionaryService == null ) {
861                        dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
862                }
863                return this.dataDictionaryService;
864        }
865
866
867        protected DocumentTypeService getDocumentTypeService() {
868                if ( documentTypeService == null ) {
869                        documentTypeService = KewApiServiceLocator.getDocumentTypeService();
870                }
871                return this.documentTypeService;
872        }
873}