    001    // Copyright 2006, 2007, 2008, 2010, 2011 The Apache Software Foundation
    002    //
    003    // Licensed under the Apache License, Version 2.0 (the "License");
    004    // you may not use this file except in compliance with the License.
    005    // You may obtain a copy of the License at
    006    //
    007    // http://www.apache.org/licenses/LICENSE-2.0
    008    //
    009    // Unless required by applicable law or agreed to in writing, software
    010    // distributed under the License is distributed on an "AS IS" BASIS,
    011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012    // See the License for the specific language governing permissions and
    013    // limitations under the License.
    014
    015    package org.apache.tapestry5.internal.services;
    016
    017    import org.apache.tapestry5.ComponentResources;
    018    import org.apache.tapestry5.Field;
    019    import org.apache.tapestry5.FieldValidator;
    020    import org.apache.tapestry5.Validator;
    021    import org.apache.tapestry5.ioc.MessageFormatter;
    022    import org.apache.tapestry5.ioc.Messages;
    023    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    024    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    025    import org.apache.tapestry5.ioc.services.TypeCoercer;
    026    import org.apache.tapestry5.runtime.Component;
    027    import org.apache.tapestry5.services.FieldValidatorSource;
    028    import org.apache.tapestry5.services.FormSupport;
    029    import org.apache.tapestry5.validator.ValidatorMacro;
    030
    031    import java.util.List;
    032    import java.util.Locale;
    033    import java.util.Map;
    034
    035    import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newList;
    036
    037    @SuppressWarnings("all")
    038    public class FieldValidatorSourceImpl implements FieldValidatorSource
    039    {
    040        private final Messages globalMessages;
    041
    042        private final Map<String, Validator> validators;
    043
    044        private final TypeCoercer typeCoercer;
    045
    046        private final FormSupport formSupport;
    047
    048        private final ValidatorMacro validatorMacro;
    049
    050        public FieldValidatorSourceImpl(Messages globalMessages, TypeCoercer typeCoercer,
    051                                        FormSupport formSupport, Map<String, Validator> validators, ValidatorMacro validatorMacro)
    052        {
    053            this.globalMessages = globalMessages;
    054            this.typeCoercer = typeCoercer;
    055            this.formSupport = formSupport;
    056            this.validators = validators;
    057            this.validatorMacro = validatorMacro;
    058        }
    059
    060        public FieldValidator createValidator(Field field, String validatorType, String constraintValue)
    061        {
    062            Component component = (Component) field;
    063            assert InternalUtils.isNonBlank(validatorType);
    064            ComponentResources componentResources = component.getComponentResources();
    065            String overrideId = componentResources.getId();
    066
    067            // So, if you use a TextField on your EditUser page, we want to search the messages
    068            // of the EditUser page (the container), not the TextField (which will always be the same).
    069
    070            Messages overrideMessages = componentResources.getContainerMessages();
    071
    072            return createValidator(field, validatorType, constraintValue, overrideId, overrideMessages, null);
    073        }
    074
    075        public FieldValidator createValidator(Field field, String validatorType, String constraintValue, String overrideId,
    076                                              Messages overrideMessages, Locale locale)
    077        {
    078
    079            ValidatorSpecification originalSpec = new ValidatorSpecification(validatorType, constraintValue);
    080
    081            List<ValidatorSpecification> org = CollectionFactory.newList(originalSpec);
    082
    083            List<ValidatorSpecification> specs = expandMacros(org);
    084
    085            List<FieldValidator> fieldValidators = CollectionFactory.<FieldValidator>newList();
    086
    087            for (ValidatorSpecification spec : specs)
    088            {
    089                fieldValidators.add(createValidator(field, spec, overrideId, overrideMessages));
    090            }
    091
    092            return new CompositeFieldValidator(fieldValidators);
    093        }
    094
    095        private FieldValidator createValidator(Field field, ValidatorSpecification spec, String overrideId,
    096                                               Messages overrideMessages)
    097        {
    098
    099            String validatorType = spec.getValidatorType();
    100
    101            assert InternalUtils.isNonBlank(validatorType);
    102            Validator validator = validators.get(validatorType);
    103
    104            if (validator == null)
    105                throw new IllegalArgumentException(ServicesMessages.unknownValidatorType(validatorType,
    106                        InternalUtils.sortedKeys(validators)));
    107
    108            // I just have this thing about always treating parameters as finals, so
    109            // we introduce a second variable to treat a mutable.
    110
    111            String formValidationid = formSupport.getFormValidationId();
    112
    113            Object coercedConstraintValue = computeConstraintValue(validatorType, validator, spec.getConstraintValue(),
    114                    formValidationid, overrideId, overrideMessages);
    115
    116            MessageFormatter formatter = findMessageFormatter(formValidationid, overrideId, overrideMessages, validatorType,
    117                    validator);
    118
    119            return new FieldValidatorImpl(field, coercedConstraintValue, formatter, validator, formSupport);
    120        }
    121
    122        private Object computeConstraintValue(String validatorType, Validator validator, String constraintValue,
    123                                              String formId, String overrideId, Messages overrideMessages)
    124        {
    125            Class constraintType = validator.getConstraintType();
    126
    127            String constraintText = findConstraintValue(validatorType, constraintType, constraintValue, formId, overrideId,
    128                    overrideMessages);
    129
    130            if (constraintText == null)
    131                return null;
    132
    133            return typeCoercer.coerce(constraintText, constraintType);
    134        }
    135
    136        private String findConstraintValue(String validatorType, Class constraintType, String constraintValue,
    137                                           String formValidationId, String overrideId, Messages overrideMessages)
    138        {
    139            if (constraintValue != null)
    140                return constraintValue;
    141
    142            if (constraintType == null)
    143                return null;
    144
    145            // If no constraint was provided, check to see if it is available via a localized message
    146            // key. This is really handy for complex validations such as patterns.
    147
    148            String perFormKey = formValidationId + "-" + overrideId + "-" + validatorType;
    149
    150            if (overrideMessages.contains(perFormKey))
    151                return overrideMessages.get(perFormKey);
    152
    153            String generalKey = overrideId + "-" + validatorType;
    154
    155            if (overrideMessages.contains(generalKey))
    156                return overrideMessages.get(generalKey);
    157
    158            throw new IllegalArgumentException(ServicesMessages.missingValidatorConstraint(validatorType, constraintType,
    159                    perFormKey, generalKey));
    160        }
    161
    162        private MessageFormatter findMessageFormatter(String formId, String overrideId, Messages overrideMessages,
    163                                                      String validatorType, Validator validator)
    164        {
    165
    166            String overrideKey = formId + "-" + overrideId + "-" + validatorType + "-message";
    167
    168            if (overrideMessages.contains(overrideKey))
    169                return overrideMessages.getFormatter(overrideKey);
    170
    171            overrideKey = overrideId + "-" + validatorType + "-message";
    172
    173            if (overrideMessages.contains(overrideKey))
    174                return overrideMessages.getFormatter(overrideKey);
    175
    176            String key = validator.getMessageKey();
    177
    178            return globalMessages.getFormatter(key);
    179        }
    180
    181        public FieldValidator createValidators(Field field, String specification)
    182        {
    183            List<ValidatorSpecification> specs = toValidatorSpecifications(specification);
    184
    185            List<FieldValidator> fieldValidators = CollectionFactory.newList();
    186
    187            for (ValidatorSpecification spec : specs)
    188            {
    189                fieldValidators.add(createValidator(field, spec.getValidatorType(), spec.getConstraintValue()));
    190            }
    191
    192            if (fieldValidators.size() == 1)
    193                return fieldValidators.get(0);
    194
    195            return new CompositeFieldValidator(fieldValidators);
    196        }
    197
    198        List<ValidatorSpecification> toValidatorSpecifications(String specification)
    199        {
    200            return expandMacros(parse(specification));
    201        }
    202
    203        private List<ValidatorSpecification> expandMacros(List<ValidatorSpecification> specs)
    204        {
    205            Map<String, Boolean> expandedMacros = CollectionFactory.newCaseInsensitiveMap();
    206            List<ValidatorSpecification> queue = CollectionFactory.newList(specs);
    207            List<ValidatorSpecification> result = CollectionFactory.newList();
    208
    209            while (!queue.isEmpty())
    210            {
    211                ValidatorSpecification head = queue.remove(0);
    212
    213                String validatorType = head.getValidatorType();
    214
    215                String expanded = validatorMacro.valueForMacro(validatorType);
    216                if (expanded != null)
    217                {
    218                    if (head.getConstraintValue() != null)
    219                        throw new RuntimeException(String.format(
    220                                "'%s' is a validator macro, not a validator, and can not have a constraint value.",
    221                                validatorType));
    222
    223                    if (expandedMacros.containsKey(validatorType))
    224                        throw new RuntimeException(String.format("Validator macro '%s' appears more than once.",
    225                                validatorType));
    226
    227                    expandedMacros.put(validatorType, true);
    228
    229                    List<ValidatorSpecification> parsed = parse(expanded);
    230
    231                    // Add the new validator specifications to the front of the queue, replacing the validator macro
    232
    233                    for (int i = 0; i < parsed.size(); i++)
    234                    {
    235                        queue.add(i, parsed.get(i));
    236                    }
    237                } else
    238                {
    239                    result.add(head);
    240                }
    241            }
    242
    243            return result;
    244        }
    245
    246        /**
    247         * A code defining what the parser is looking for.
    248         */
    249        enum State
    250        {
    251
    252            /**
    253             * The start of a validator type.
    254             */
    255            TYPE_START,
    256            /**
    257             * The end of a validator type.
    258             */
    259            TYPE_END,
    260            /**
    261             * Equals sign after a validator type, or a comma.
    262             */
    263            EQUALS_OR_COMMA,
    264            /**
    265             * The start of a constraint value.
    266             */
    267            VALUE_START,
    268            /**
    269             * The end of the constraint value.
    270             */
    271            VALUE_END,
    272            /**
    273             * The comma after a constraint value.
    274             */
    275            COMMA
    276        }
    277
    278        static List<ValidatorSpecification> parse(String specification)
    279        {
    280            List<ValidatorSpecification> result = newList();
    281
    282            char[] input = specification.toCharArray();
    283
    284            int cursor = 0;
    285            int start = -1;
    286
    287            String type = null;
    288            boolean skipWhitespace = true;
    289            State state = State.TYPE_START;
    290
    291            while (cursor < input.length)
    292            {
    293                char ch = input[cursor];
    294
    295                if (skipWhitespace && Character.isWhitespace(ch))
    296                {
    297                    cursor++;
    298                    continue;
    299                }
    300
    301                skipWhitespace = false;
    302
    303                switch (state)
    304                {
    305
    306                    case TYPE_START:
    307
    308                        if (Character.isLetter(ch))
    309                        {
    310                            start = cursor;
    311                            state = State.TYPE_END;
    312                            break;
    313                        }
    314
    315                        parseError(cursor, specification);
    316
    317                    case TYPE_END:
    318
    319                        if (Character.isLetter(ch))
    320                        {
    321                            break;
    322                        }
    323
    324                        type = specification.substring(start, cursor);
    325
    326                        skipWhitespace = true;
    327                        state = State.EQUALS_OR_COMMA;
    328                        continue;
    329
    330                    case EQUALS_OR_COMMA:
    331
    332                        if (ch == '=')
    333                        {
    334                            skipWhitespace = true;
    335                            state = State.VALUE_START;
    336                            break;
    337                        }
    338
    339                        if (ch == ',')
    340                        {
    341                            result.add(new ValidatorSpecification(type));
    342                            type = null;
    343                            state = State.COMMA;
    344                            continue;
    345                        }
    346
    347                        parseError(cursor, specification);
    348
    349                    case VALUE_START:
    350
    351                        start = cursor;
    352                        state = State.VALUE_END;
    353                        break;
    354
    355                    case VALUE_END:
    356
    357                        // The value ends when we hit whitespace or a comma
    358
    359                        if (Character.isWhitespace(ch) || ch == ',')
    360                        {
    361                            String value = specification.substring(start, cursor);
    362
    363                            result.add(new ValidatorSpecification(type, value));
    364                            type = null;
    365
    366                            skipWhitespace = true;
    367                            state = State.COMMA;
    368                            continue;
    369                        }
    370
    371                        break;
    372
    373                    case COMMA:
    374
    375                        if (ch == ',')
    376                        {
    377                            skipWhitespace = true;
    378                            state = State.TYPE_START;
    379                            break;
    380                        }
    381
    382                        parseError(cursor, specification);
    383                } // case
    384
    385                cursor++;
    386            } // while
    387
    388            // cursor is now one character past end of string.
    389            // Cleanup whatever state we were in the middle of.
    390
    391            switch (state)
    392            {
    393                case TYPE_END:
    394
    395                    type = specification.substring(start);
    396
    397                case EQUALS_OR_COMMA:
    398
    399                    result.add(new ValidatorSpecification(type));
    400                    break;
    401
    402                // Case when the specification ends with an equals sign.
    403
    404                case VALUE_START:
    405                    result.add(new ValidatorSpecification(type, ""));
    406                    break;
    407
    408                case VALUE_END:
    409
    410                    result.add(new ValidatorSpecification(type, specification.substring(start)));
    411                    break;
    412
    413                // For better or worse, ending the string with a comma is valid.
    414
    415                default:
    416            }
    417
    418            return result;
    419        }
    420
    421        private static void parseError(int cursor, String specification)
    422        {
    423            throw new RuntimeException(ServicesMessages.validatorSpecificationParseError(cursor, specification));
    424        }
    425    }