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 */
016/*
017* Copyright 2006-2012 The Kuali Foundation
018*
019* Licensed under the Educational Community License, Version 2.0 (the "License");
020* you may not use this file except in compliance with the License.
021* You may obtain a copy of the License at
022*
023* http://www.opensource.org/licenses/ecl2.php
024*
025* Unless required by applicable law or agreed to in writing, software
026* distributed under the License is distributed on an "AS IS" BASIS,
027* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
028* See the License for the specific language governing permissions and
029* limitations under the License.
030*/
031package org.kuali.rice.krad.datadictionary.validation;
032
033import org.apache.commons.beanutils.PropertyUtils;
034import org.apache.commons.collections.CollectionUtils;
035import org.apache.commons.lang.StringUtils;
036import org.kuali.rice.core.api.CoreApiServiceLocator;
037import org.kuali.rice.core.api.config.property.ConfigurationService;
038import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
039import org.kuali.rice.core.api.uif.RemotableAttributeError;
040import org.kuali.rice.core.api.uif.RemotableAttributeField;
041import org.kuali.rice.core.api.util.RiceKeyConstants;
042import org.kuali.rice.core.api.util.Truth;
043import org.kuali.rice.core.api.util.type.TypeUtils;
044import org.kuali.rice.core.web.format.Formatter;
045import org.kuali.rice.krad.bo.BusinessObject;
046import org.kuali.rice.krad.data.DataObjectWrapper;
047import org.kuali.rice.krad.data.KradDataServiceLocator;
048import org.kuali.rice.krad.datadictionary.PrimitiveAttributeDefinition;
049import org.kuali.rice.krad.datadictionary.RelationshipDefinition;
050import org.kuali.rice.krad.service.DataDictionaryRemoteFieldService;
051import org.kuali.rice.krad.service.DataDictionaryService;
052import org.kuali.rice.krad.service.DictionaryValidationService;
053import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
054import org.kuali.rice.krad.util.ErrorMessage;
055import org.kuali.rice.krad.util.GlobalVariables;
056import org.kuali.rice.krad.util.KRADUtils;
057
058import java.beans.PropertyDescriptor;
059import java.text.MessageFormat;
060import java.util.ArrayList;
061import java.util.Collections;
062import java.util.HashMap;
063import java.util.List;
064import java.util.Map;
065import java.util.regex.Pattern;
066
067/**
068 * <p>An abstract base class for type service implementations which provides default validation of attributes from the Data
069 * Dictionary.  It attempts to remain module independent by requiring the translation of the attribute definitions to a
070 * generic format that includes the required {@link RemotableAttributeField}s as an unimplemented template method,
071 * see{@link #getTypeAttributeDefinitions(String)}.
072 * </p>
073 * <p>Note that any {@link RemotableAttributeError}s returned from here should be fully resolved to the messages to be
074 * displayed to the user (in other words, they should not contain error keys).  <b>The same approach should be taken by
075 * subclasses since the message resources may not be present on the remote server that is invoking this service</b>.
076 * There is a {@link #createErrorString(String, String...)} utility method that can be used to resolve
077 * errorKeys and format them appropriately.</p>
078 *
079 * @author Kuali Rice Team (rice.collab@kuali.org)
080 */
081public abstract class AttributeValidatingTypeServiceBase {
082
083        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AttributeValidatingTypeServiceBase.class);
084    private static final String ANY_CHAR_PATTERN_S = ".*";
085    private static final Pattern ANY_CHAR_PATTERN = Pattern.compile(ANY_CHAR_PATTERN_S);
086
087        private DictionaryValidationService dictionaryValidationService;
088        private DataDictionaryService dataDictionaryService;
089    private DataDictionaryRemoteFieldService dataDictionaryRemoteFieldService;
090
091    /**
092     * Retrieves active type attribute definitions and translates them into a module-independent representation.  Note
093     * that they should be returned in the order desired for display.
094     *
095     * @param typeId the identifier for the type
096     * @return a correctly ordered List of active, module-independent type attribute definitions
097     */
098    protected abstract List<TypeAttributeDefinition> getTypeAttributeDefinitions(String typeId);
099
100    /**
101     * Validates an attribute that is *not* mapped to a data dictionary component via
102     * {@link TypeAttributeDefinition#componentName} and {@link TypeAttributeDefinition#name}.
103     *
104     * @param attr the RemotableAttributeField for which to validate.
105     * @param key the attribute name
106     * @param value the attribute value
107     * @return a List of {@link RemotableAttributeError}s with fully resolved error messages (not error keys).  May
108     * return null or an empty List if no errors are encountered.
109     */
110    protected abstract List<RemotableAttributeError>
111    validateNonDataDictionaryAttribute(RemotableAttributeField attr, String key, String value);
112
113
114
115    /**
116     * <p>This is the default implementation.  It calls into the service for each attribute to
117     * validate it there.  No combination validation is done.  That should be done
118     * by overriding this method.</p>
119     * <p>This implementation calls {@link #getTypeAttributeDefinitions(String)} to retrieve module-agnostic 
120     * representations.  It then iterates through the entry set of attributes, and calls 
121     * {@link #validateNonDataDictionaryAttribute(org.kuali.rice.core.api.uif.RemotableAttributeField, String, String)} 
122     * or {@link #validateDataDictionaryAttribute(AttributeValidatingTypeServiceBase.TypeAttributeDefinition, String, String)}
123     * as appropriate.  Lastly it calls {@link #validateReferencesExistAndActive(java.util.Map, java.util.Map, java.util.List)}.
124     * </p>
125     *
126     * @param typeId the identifier for the type
127     * @param attributes the Map of attribute names to values
128     * @return the List of errors ({@link RemotableAttributeError}s) encountered during validation.
129     */
130    public List<RemotableAttributeError> validateAttributes(String typeId, Map<String, String> attributes) {
131
132        if (StringUtils.isBlank(typeId)) {
133            throw new RiceIllegalArgumentException("typeId was null or blank");
134        }
135
136        if (attributes == null) {
137            throw new RiceIllegalArgumentException("attributes was null or blank");
138        }
139
140        List<TypeAttributeDefinition> definitions = getTypeAttributeDefinitions(typeId);
141        Map<String, TypeAttributeDefinition> typeAttributeDefinitionMap =
142                buildTypeAttributeDefinitionMapByName(definitions);
143
144        final List<RemotableAttributeError> validationErrors = new ArrayList<RemotableAttributeError>();
145
146        for ( Map.Entry<String, String> entry : attributes.entrySet() ) {
147
148            TypeAttributeDefinition typeAttributeDefinition = typeAttributeDefinitionMap.get(entry.getKey());
149
150            final List<RemotableAttributeError> attributeErrors;
151            if (typeAttributeDefinition != null) {
152                if (typeAttributeDefinition.getComponentName() == null) {
153                    attributeErrors = validateNonDataDictionaryAttribute(typeAttributeDefinition.getField(), entry.getKey(), entry.getValue());
154                } else {
155                    attributeErrors = validateDataDictionaryAttribute(typeAttributeDefinition, entry.getKey(), entry.getValue());
156                }
157
158                if ( attributeErrors != null ) {
159                    validationErrors.addAll(attributeErrors);
160                }
161            }
162        }
163
164
165        final List<RemotableAttributeError> referenceCheckErrors = validateReferencesExistAndActive(typeAttributeDefinitionMap, attributes, validationErrors);
166        validationErrors.addAll(referenceCheckErrors);
167
168        return Collections.unmodifiableList(validationErrors);
169    }
170
171    private Map<String, TypeAttributeDefinition> buildTypeAttributeDefinitionMapByName(
172            List<TypeAttributeDefinition> definitions) {// throw them into a map by name
173        Map<String, TypeAttributeDefinition> typeAttributeDefinitionMap;
174        if (definitions == null || definitions.size() == 0) {
175            typeAttributeDefinitionMap = Collections.<String, TypeAttributeDefinition>emptyMap();
176        } else {
177            typeAttributeDefinitionMap = new HashMap<String, TypeAttributeDefinition>();
178
179            for (TypeAttributeDefinition definition : definitions) {
180                typeAttributeDefinitionMap.put(definition.getName(), definition);
181            }
182        }
183        return typeAttributeDefinitionMap;
184    }
185
186    /**
187     * <p>Cross-validates referenced components amongst attributes to ensure they refer to existing and active
188     * business objects.</p>
189     * <p>This implementation instantiates any components mapped by attributes, populates them as best it can, and then
190     * uses the {@link DataDictionaryService} to get relationship information.  Then, through the
191     * {@link DictionaryValidationService} it attempts to ensure that any referenced business objects mapped by other
192     * attributes exist and are active.  It pulls any errors encountered out of the global error map via calls to 
193     * {@link #extractErrorsFromGlobalVariablesErrorMap(String)}</p>
194     * <p>TODO: who can explain this? :-)</p>
195     *
196     * @param typeAttributeDefinitionMap a Map from attribute name to {@link TypeAttributeDefinition} containing all of
197     * the attribute definitions for this type.
198     * @param attributes the Map of attribute names to values
199     * @param previousValidationErrors a List of previously encountered errors used to short circuit testing on
200     * attributes that are already known to have errors.
201     * @return the List of errors encountered. Cannot return null.
202     */
203        protected List<RemotableAttributeError> validateReferencesExistAndActive( Map<String, TypeAttributeDefinition> typeAttributeDefinitionMap, Map<String, String> attributes, List<RemotableAttributeError> previousValidationErrors) {
204        //
205        // Here there be dragons -- adapted from DataDictionaryTypeServiceBase, please excuse X-.
206        //
207
208                Map<String, BusinessObject> componentClassInstances = new HashMap<String, BusinessObject>();
209                List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
210
211        // Create an instance of each component and shove it into the componentClassInstances
212                for ( String attributeName : attributes.keySet() ) {
213                        TypeAttributeDefinition attr = typeAttributeDefinitionMap.get(attributeName);
214
215                        if ((attr != null) && StringUtils.isNotBlank(attr.getComponentName())) {
216                                if (!componentClassInstances.containsKey(attr.getComponentName())) {
217                                        try {
218                                                Class<?> componentClass = Class.forName(attr.getComponentName());
219                                                if (!BusinessObject.class.isAssignableFrom(componentClass)) {
220                                                        LOG.warn("Class " + componentClass.getName() + " does not implement BusinessObject.  Unable to perform reference existence and active validation");
221                                                        continue;
222                                                }
223                                                BusinessObject componentInstance = (BusinessObject) componentClass.newInstance();
224                                                componentClassInstances.put(attr.getComponentName(), componentInstance);
225                                        } catch (Exception e) {
226                                                LOG.error("Unable to instantiate class for attribute: " + attributeName, e);
227                                        }
228                                }
229                        }
230                }
231
232                // now that we have instances for each component class, try to populate them with any attribute we can,
233                // assuming there were no other validation errors associated with it
234                for ( Map.Entry<String, String> entry : attributes.entrySet() ) {
235                        if (!RemotableAttributeError.containsAttribute(entry.getKey(), previousValidationErrors)) {
236                                for (Object componentInstance : componentClassInstances.values()) {
237                                        try {
238                        DataObjectWrapper wrapper = KradDataServiceLocator.getDataObjectService().wrap(componentInstance);
239                        wrapper.setPropertyValues(Collections.singletonMap(entry.getKey(), entry.getValue()));
240                                        } catch (Exception e) {
241                                                LOG.error("Unable to set object property class: " + componentInstance.getClass().getName() + " property: " + entry.getKey(), e);
242                                        }
243                                }
244                        }
245                }
246
247                for (Map.Entry<String, BusinessObject> entry : componentClassInstances.entrySet()) {
248                        List<RelationshipDefinition> relationships = getDataDictionaryService().getDataDictionary().getBusinessObjectEntry(entry.getKey()).getRelationships();
249                        if (relationships == null) {
250                                continue;
251                        }
252
253                        for (RelationshipDefinition relationshipDefinition : relationships) {
254                                List<PrimitiveAttributeDefinition> primitiveAttributes = relationshipDefinition.getPrimitiveAttributes();
255
256                                // this code assumes that the last defined primitiveAttribute is the attributeToHighlightOnFail
257                                String attributeToHighlightOnFail = primitiveAttributes.get(primitiveAttributes.size() - 1).getSourceName();
258
259                                // TODO: will this work for user ID attributes?
260
261                if (attributes.containsKey(attributeToHighlightOnFail)) {
262
263                    TypeAttributeDefinition attr = typeAttributeDefinitionMap.get(attributeToHighlightOnFail);
264                    if (attr != null) {
265                        final String attributeDisplayLabel;
266                        if (StringUtils.isNotBlank(attr.getComponentName())) {
267                            attributeDisplayLabel = getDataDictionaryService().getAttributeLabel(attr.getComponentName(), attributeToHighlightOnFail);
268                        } else {
269                            attributeDisplayLabel = attr.getLabel();
270                        }
271
272                        getDictionaryValidationService().validateReferenceExistsAndIsActive(entry.getValue(), relationshipDefinition.getObjectAttributeName(),
273                                attributeToHighlightOnFail, attributeDisplayLabel);
274                    }
275                    List<String> extractedErrors = extractErrorsFromGlobalVariablesErrorMap(attributeToHighlightOnFail);
276                    if (CollectionUtils.isNotEmpty(extractedErrors)) {
277                        errors.add(RemotableAttributeError.Builder.create(attributeToHighlightOnFail, extractedErrors).build());
278                    }
279                }
280                        }
281                }
282                return errors;
283        }
284
285    /**
286     * <p>Returns a String suitable for use in error messages to represent the given attribute.</p>
287     * <p>This implementation returns a String of the format "longLabel (shortLabel)" where those fields are pulled
288     * from the passed in definition.</p>
289     *
290     * @param definition the definition for which to create an error label.
291     * @return the error label String.
292     */
293    protected static String getAttributeErrorLabel(RemotableAttributeField definition) {
294        String longAttributeLabel = definition.getLongLabel();
295        String shortAttributeLabel = definition.getShortLabel();
296
297        return longAttributeLabel + " (" + shortAttributeLabel + ")";
298    }
299
300    /**
301     * <p>creates an error String from the given errorKey and parameters.</p>
302     * <p>This implementation will attempt to resolve the errorKey using the {@link ConfigurationService}, and format it
303     * with the provided params using {@link MessageFormat#format(String, Object...)}.  If the errorKey can't be
304     * resolved, it will return a string like the following: errorKey:param1;param2;param3;
305     * </p>
306     *
307     * @param errorKey the errorKey
308     * @param params the error params
309     * @return error string
310     */
311    protected String createErrorString(String errorKey, String... params) {
312
313        String errorString = getConfigurationService().getPropertyValueAsString(errorKey);
314        if (StringUtils.isEmpty(errorString)) {
315            final StringBuilder s = new StringBuilder(errorKey).append(':');
316            if (params != null) {
317                for (String p : params) {
318                    if (p != null) {
319                        s.append(p);
320                        s.append(';');
321                    }
322                }
323            }
324            errorString = s.toString();
325        } else {
326            errorString = MessageFormat.format(errorString, params);
327        }
328        return errorString;
329    }
330
331    /**
332     * <p>Validates a data dictionary mapped attribute for a primitive property.</p>
333     * <p>This implementation checks that the attribute is defined using the {@link DataDictionaryService} if it is
334     * from a specific set of types defined in TypeUtils.  Then, if the value is not blank, it checks for errors by
335     * calling
336     * {@link #validateAttributeFormat(org.kuali.rice.core.api.uif.RemotableAttributeField, String, String, String, String)}.
337     * If it is blank, it checks for errors by calling
338     * {@link #validateAttributeRequired(org.kuali.rice.core.api.uif.RemotableAttributeField, String, String, Object, String)}
339     * .</p>
340     *
341     * @param typeAttributeDefinition the definition for the attribute
342     * @param componentName the data dictionary component name
343     * @param object the instance of the component
344     * @param propertyDescriptor the descriptor for the property that the attribute maps to
345     * @return a List of errors ({@link RemotableAttributeError}s) encountered during validation.  Cannot return null.
346     */
347    protected List<RemotableAttributeError> validatePrimitiveAttributeFromDescriptor (
348            TypeAttributeDefinition typeAttributeDefinition, String componentName, Object object,
349            PropertyDescriptor propertyDescriptor) {
350
351        List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
352        // validate the primitive attributes if defined in the dictionary
353        if (null != propertyDescriptor
354                && getDataDictionaryService().isAttributeDefined(componentName, propertyDescriptor.getName())) {
355
356            DataObjectWrapper wrapper = KradDataServiceLocator.getDataObjectService().wrap(object);
357            Object value = wrapper.getPropertyValue(propertyDescriptor.getName());
358            Class<?> propertyType = propertyDescriptor.getPropertyType();
359
360            if (TypeUtils.isStringClass(propertyType)
361                    || TypeUtils.isIntegralClass(propertyType)
362                    || TypeUtils.isDecimalClass(propertyType)
363                    || TypeUtils.isTemporalClass(propertyType)) {
364
365                // check value format against dictionary
366                if (value != null && StringUtils.isNotBlank(value.toString())) {
367                    if (!TypeUtils.isTemporalClass(propertyType)) {
368                        errors.addAll(validateAttributeFormat(typeAttributeDefinition.getField(), componentName,
369                                propertyDescriptor.getName(), value.toString(), propertyDescriptor.getName()));
370                    }
371                } else {
372                        // if it's blank, then we check whether the attribute should be required
373                    errors.addAll(validateAttributeRequired(typeAttributeDefinition.getField(), componentName,
374                            propertyDescriptor.getName(), value, propertyDescriptor.getName()));
375                }
376            }
377        }
378        return errors;
379    }
380
381
382    /**
383     * <p>Validates required-ness of an attribute against its corresponding value</p>
384     * <p>This implementation checks if an attribute value is null or blank, and if so checks if the
385     * {@link RemotableAttributeField} is required.  If it is, a {@link RemotableAttributeError} is created
386     * with the message populated by a call to {@link #createErrorString(String, String...)}.</p>
387     *
388     * @param field the field for the attribute being tested
389     * @param objectClassName the class name for the component
390     * @param attributeName the name of the attribute
391     * @param attributeValue the value of the attribute
392     * @param errorKey the errorKey used to identify the field
393     * @return the List of errors ({@link RemotableAttributeError}s) encountered during validation.  Cannot return null.
394     */
395    protected List<RemotableAttributeError> validateAttributeRequired(RemotableAttributeField field,
396            String objectClassName, String attributeName, Object attributeValue, String errorKey) {
397        List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
398        // check if field is a required field for the business object
399
400        if (attributeValue == null
401                || (attributeValue instanceof String && StringUtils.isBlank((String) attributeValue))) {
402
403            boolean required = field.isRequired();
404            if (required) {
405                // get label of attribute for message
406                String errorLabel = getAttributeErrorLabel(field);
407                errors.add(RemotableAttributeError.Builder.create(errorKey,
408                        createErrorString(RiceKeyConstants.ERROR_REQUIRED, errorLabel)).build());
409            }
410        }
411
412        return errors;
413    }
414
415    /**
416     * <p>Gets the validation {@link Pattern} for the given {@link RemotableAttributeField}.</p>
417     * <p>This implementation checks if there is a regexConstraint set on the field, and if so
418     * it compiles a Pattern (with no special flags) using it.  Otherwise, it returns a pattern that
419     * always matches.</p>
420     * 
421     * @param field the field for which to return a validation {@link Pattern}.
422     * @return the compiled {@link Pattern} to use in validation the given field.
423     */
424    protected Pattern getAttributeValidatingExpression(RemotableAttributeField field) {
425        if (field == null || StringUtils.isBlank(field.getRegexConstraint())) {
426            return ANY_CHAR_PATTERN;
427        }
428
429        return Pattern.compile(field.getRegexConstraint());
430    }
431
432    /**
433     * <p>Gets a {@link Formatter} appropriate for the data type of the given field.</p>
434     * <p>This implementation returns null if {@link org.kuali.rice.core.api.uif.RemotableAttributeField#getDataType()} 
435     * returns null.  Otherwise, it returns the result of calling {@link Formatter#getFormatter(Class)} on the
436     * {@link org.kuali.rice.core.api.data.DataType}'s type</p>
437     *
438     * @param field the field for which to provide a {@link Formatter}.
439     * @return an applicable {@link Formatter}, or null if one can't be found.
440     */
441        protected Formatter getAttributeFormatter(RemotableAttributeField field) {
442        if (field.getDataType() == null) {
443            return null;
444        }
445
446        return Formatter.getFormatter(field.getDataType().getType());
447    }
448
449    /**
450     * <p>Validates the format of the value for the given attribute field.</p>
451     * <p>This implementation checks if the attribute value is not blank, in which case it checks (as applicable) the
452     * max length, min length, min value, max value, and format (using the {@link Pattern} returned by
453     * {@link #getAttributeValidatingExpression(org.kuali.rice.core.api.uif.RemotableAttributeField)}).  If that doesn't
454     * match, it will use the Formatter returned by
455     * {@link #getAttributeFormatter(org.kuali.rice.core.api.uif.RemotableAttributeField)} to format the value and try
456     * matching against it again.  For each format error that is found,
457     * {@link #createErrorString(String, String...)} is called to prepare the text for the
458     * {@link RemotableAttributeError} that is generated.
459     *
460     * @param field the field for the attribute whose value we are validating
461     * @param objectClassName the name of the class to which the attribute belongs
462     * @param attributeName the name of the attribute
463     * @param attributeValue the String value whose format we are validating
464     * @param errorKey the name of the property on the object class that this attribute maps to
465     * @return a List containing any errors ({@link RemotableAttributeError}s) that are detected.
466     */
467    protected List<RemotableAttributeError> validateAttributeFormat(RemotableAttributeField field, String objectClassName, String attributeName, String attributeValue, String errorKey) {
468        List<RemotableAttributeError> errors = new ArrayList<RemotableAttributeError>();
469
470        String errorLabel = getAttributeErrorLabel(field);
471
472        if ( LOG.isDebugEnabled() ) {
473                LOG.debug("(bo, attributeName, attributeValue) = (" + objectClassName + "," + attributeName + "," + attributeValue + ")");
474        }
475
476        if (StringUtils.isNotBlank(attributeValue)) {
477            Integer maxLength = field.getMaxLength();
478            if ((maxLength != null) && (maxLength.intValue() < attributeValue.length())) {
479                errors.add(RemotableAttributeError.Builder.create(errorKey,
480                        createErrorString(RiceKeyConstants.ERROR_MAX_LENGTH, errorLabel, maxLength.toString())).build());
481                return errors;
482            }
483            Integer minLength = field.getMinLength();
484            if ((minLength != null) && (minLength.intValue() > attributeValue.length())) {
485                errors.add(RemotableAttributeError.Builder.create(errorKey,
486                        createErrorString(RiceKeyConstants.ERROR_MIN_LENGTH, errorLabel, minLength.toString())).build());
487                return errors;
488            }
489            Pattern validationExpression = getAttributeValidatingExpression(field);
490            if (!ANY_CHAR_PATTERN_S.equals(validationExpression.pattern())) {
491                if ( LOG.isDebugEnabled() ) {
492                        LOG.debug("(bo, attributeName, validationExpression) = (" + objectClassName + "," + attributeName + "," + validationExpression + ")");
493                }
494
495                if (!validationExpression.matcher(attributeValue).matches()) {
496                    boolean isError=true;
497                    final Formatter formatter = getAttributeFormatter(field);
498                    if (formatter != null) {
499                        Object o = formatter.format(attributeValue);
500                        isError = !validationExpression.matcher(String.valueOf(o)).matches();
501                    }
502                    if (isError) {
503                        errors.add(RemotableAttributeError.Builder.create(errorKey, createErrorString(field.getRegexContraintMsg(), errorLabel))
504                                .build());
505                    }
506                    return errors;
507                }
508            }
509            Double min = field.getMinValue();
510            if (min != null) {
511                try {
512                    if (Double.parseDouble(attributeValue) < min) {
513                        errors.add(RemotableAttributeError.Builder.create(errorKey, createErrorString(
514                                RiceKeyConstants.ERROR_INCLUSIVE_MIN, errorLabel, min.toString())).build());
515                        return errors;
516                    }
517                }
518                catch (NumberFormatException e) {
519                    // quash; this indicates that the DD contained a min for a non-numeric attribute
520                }
521            }
522            Double max = field.getMaxValue();
523            if (max != null) {
524                try {
525
526                    if (Double.parseDouble(attributeValue) > max) {
527                        errors.add(RemotableAttributeError.Builder.create(errorKey, createErrorString(
528                                RiceKeyConstants.ERROR_INCLUSIVE_MAX, errorLabel, max.toString())).build());
529                        return errors;
530                    }
531                }
532                catch (NumberFormatException e) {
533                    // quash; this indicates that the DD contained a max for a non-numeric attribute
534                }
535            }
536        }
537        return errors;
538    }
539
540
541    /**
542     * <p>Removes all errors for the given attributeName from the global error map, transforms them as appropriate and
543     * returns them as a List of Strings.</p>
544     * <p>This implementation iterates through any errors found in the error map, transforms them by calling
545     * {@link #createErrorString(String, String...)} and adds them to the List that is then returned</p>
546     *
547     * @param attributeName the attribute name for which to extract errors from the global error map.
548     * @return a List of error Strings
549     */
550        protected List<String> extractErrorsFromGlobalVariablesErrorMap(String attributeName) {
551                Object results = GlobalVariables.getMessageMap().getErrorMessagesForProperty(attributeName);
552                List<String> errors = new ArrayList<String>();
553        if (results instanceof String) {
554                errors.add(createErrorString((String) results));
555        } else if ( results != null) {
556                if (results instanceof List) {
557                        List<?> errorList = (List<?>)results;
558                        for (Object msg : errorList) {
559                                ErrorMessage errorMessage = (ErrorMessage)msg;
560                                errors.add(createErrorString(errorMessage.getErrorKey(), errorMessage.getMessageParameters()));
561                                }
562                } else {
563                        String [] temp = (String []) results;
564                        for (String string : temp) {
565                                        errors.add(createErrorString(string));
566                                }
567                }
568        }
569        GlobalVariables.getMessageMap().removeAllErrorMessagesForProperty(attributeName);
570        return errors;
571        }
572
573    /**
574     * <p>Validates the attribute value for the given {@link TypeAttributeDefinition} having a componentName.</p>
575     * <p>This implementation instantiates a component object using reflection on the class name specified in the
576     * {@link TypeAttributeDefinition}s componentName, gets a {@link PropertyDescriptor} for the attribute of the
577     * component object, hydrates the attribute's value from it's String form, sets that value on the component object,
578     * and then delegates to
579     * {@link #validatePrimitiveAttributeFromDescriptor(AttributeValidatingTypeServiceBase.TypeAttributeDefinition, String, Object, java.beans.PropertyDescriptor)}.
580     * </p>
581     *
582     * @param typeAttributeDefinition
583     * @param attributeName
584     * @param value
585     * @return
586     */
587    protected List<RemotableAttributeError> validateDataDictionaryAttribute(TypeAttributeDefinition typeAttributeDefinition, String attributeName, String value) {
588                try {
589            // create an object of the proper type per the component
590            Object componentObject = Class.forName( typeAttributeDefinition.getComponentName() ).newInstance();
591
592            if ( attributeName != null ) {
593                // get the bean utils descriptor for accessing the attribute on that object
594                PropertyDescriptor propertyDescriptor = PropertyUtils.getPropertyDescriptor(componentObject, attributeName);
595                if ( propertyDescriptor != null ) {
596                    // set the value on the object so that it can be checked
597                    Object attributeValue = getAttributeValue(propertyDescriptor, value);
598                    propertyDescriptor.getWriteMethod().invoke( componentObject, attributeValue);
599                    return validatePrimitiveAttributeFromDescriptor(typeAttributeDefinition,
600                            typeAttributeDefinition.getComponentName(), componentObject, propertyDescriptor);
601                }
602            }
603        } catch (Exception e) {
604            throw new TypeAttributeValidationException(e);
605        }
606        return Collections.emptyList();
607        }
608
609    private Object getAttributeValue(PropertyDescriptor propertyDescriptor, String attributeValue){
610        Object attributeValueObject = null;
611        if (propertyDescriptor!=null && attributeValue!=null) {
612            Class<?> propertyType = propertyDescriptor.getPropertyType();
613            if (String.class.equals(propertyType)) {
614                // it's already a String
615                attributeValueObject = attributeValue;
616            } // KULRICE-6808: Kim Role Maintenance - Custom boolean role qualifier values are not being converted properly
617            else if (Boolean.class.equals(propertyType) || Boolean.TYPE.equals(propertyType)) {
618                attributeValueObject = Truth.strToBooleanIgnoreCase(attributeValue);
619            } else {
620                // try to create one with KRADUtils for other misc data types
621                attributeValueObject = KRADUtils.createObject(propertyType, new Class[]{String.class}, new Object[]{attributeValue});
622                // if that didn't work, we'll get a null back
623                if (attributeValueObject == null ) {
624                    // this doesn't seem like a great option, since we know the property isn't of type String
625                    attributeValueObject = attributeValue;
626                }
627            }
628        }
629        return attributeValueObject;
630    }
631
632
633        protected DictionaryValidationService getDictionaryValidationService() {
634        if (dictionaryValidationService == null) {
635            dictionaryValidationService = KRADServiceLocatorWeb.getDictionaryValidationService();
636        }
637        return dictionaryValidationService;
638        }
639
640    // lazy initialization holder class
641    private static class DataDictionaryServiceHolder {
642        public static DataDictionaryService dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService();
643    }
644
645        protected DataDictionaryService getDataDictionaryService() {
646                return DataDictionaryServiceHolder.dataDictionaryService;
647        }
648
649    // lazy initialization holder class
650    private static class DataDictionaryRemoteFieldServiceHolder {
651        public static DataDictionaryRemoteFieldService dataDictionaryRemoteFieldService =
652                KRADServiceLocatorWeb.getDataDictionaryRemoteFieldService();
653    }
654
655    protected DataDictionaryRemoteFieldService getDataDictionaryRemoteFieldService() {
656        return DataDictionaryRemoteFieldServiceHolder.dataDictionaryRemoteFieldService;
657    }
658
659    // lazy initialization holder class
660    private static class ConfigurationServiceHolder {
661        public static ConfigurationService configurationService = CoreApiServiceLocator.getKualiConfigurationService();
662    }
663    
664    protected ConfigurationService getConfigurationService() {
665        return ConfigurationServiceHolder.configurationService;
666    }
667
668
669    protected static class TypeAttributeValidationException extends RuntimeException {
670
671        protected TypeAttributeValidationException(String message) {
672            super( message );
673        }
674
675        protected TypeAttributeValidationException(Throwable cause) {
676            super( cause );
677        }
678
679        private static final long serialVersionUID = 8220618846321607801L;
680
681    }
682
683
684    /**
685     * A module-independent representation of a type attribute containing all the information that we need
686     * in order to validate data dictionary-based attributes.
687     */
688    protected static class TypeAttributeDefinition {
689
690        private final RemotableAttributeField field;
691        private final String name;
692        private final String componentName;
693        private final String label;
694        private final Map<String, String> properties;
695
696        /**
697         * Constructs a {@link TypeAttributeDefinition}
698         * @param field the RemotableAttributeField corresponding to this definition.  Must not be null.
699         * @param name the name for this attribute.  Must not be empty or null.
700         * @param componentName The name of a data dictionary component that this field refers to. May be null.
701         * @param label The label to use for this attribute.  May be null.
702         * @param properties a catch all for properties important to a module's type attrbute definitions
703         * that aren't directly supported by {@link TypeAttributeDefinition}.
704         */
705        public TypeAttributeDefinition(RemotableAttributeField field, String name, String componentName, String label, Map<String, String> properties) {
706            if (field == null) throw new RiceIllegalArgumentException("field must not be null");
707            if (StringUtils.isEmpty(name)) throw new RiceIllegalArgumentException("name must not be empty or null");
708            this.field = field;
709            this.name = name;
710            this.componentName = componentName;
711            this.label = label;
712
713            if (properties == null || properties.isEmpty()) {
714                this.properties = Collections.emptyMap();
715            } else {
716                // make our local variable into a copy of the passed in Map
717                properties = new HashMap<String, String>(properties);
718                // assign in in immutable form to our class member variable
719                this.properties = Collections.unmodifiableMap(properties);
720            }
721        }
722
723        public RemotableAttributeField getField() {
724            return field;
725        }
726
727        public String getName() {
728            return name;
729        }
730
731        public String getComponentName() {
732            return componentName;
733        }
734
735        public String getLabel() {
736            return label;
737        }
738
739        /**
740         * @return an unmodifiable map of properties for this attribute.  Will never be null.
741         */
742        public Map<String, String> getProperties() {
743            return properties;
744        }
745    }
746}