    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
    015package org.apache.tapestry5.internal.services;
    016
    017import org.apache.tapestry5.ComponentResources;
    018import org.apache.tapestry5.Field;
    019import org.apache.tapestry5.FieldValidator;
    020import org.apache.tapestry5.Validator;
    021import org.apache.tapestry5.commons.MessageFormatter;
    022import org.apache.tapestry5.commons.Messages;
    023import org.apache.tapestry5.commons.services.TypeCoercer;
    024import org.apache.tapestry5.commons.util.CollectionFactory;
    025import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    026import org.apache.tapestry5.runtime.Component;
    027import org.apache.tapestry5.services.FieldValidatorSource;
    028import org.apache.tapestry5.services.FormSupport;
    029import org.apache.tapestry5.validator.ValidatorMacro;
    030
    031import static org.apache.tapestry5.commons.util.CollectionFactory.newList;
    032
    033import java.util.List;
    034import java.util.Locale;
    035import java.util.Map;
    036
    037@SuppressWarnings("all")
    038public 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(String.format("Unknown validator type '%s'. Configured validators are %s.", validatorType, InternalUtils.join(InternalUtils.sortedKeys(validators))));
    106
    107        // I just have this thing about always treating parameters as finals, so
    108        // we introduce a second variable to treat a mutable.
    109
    110        String formValidationid = formSupport.getFormValidationId();
    111
    112        Object coercedConstraintValue = computeConstraintValue(validatorType, validator, spec.getConstraintValue(),
    113                formValidationid, overrideId, overrideMessages);
    114
    115        MessageFormatter formatter = findMessageFormatter(formValidationid, overrideId, overrideMessages, validatorType,
    116                validator);
    117
    118        return new FieldValidatorImpl(field, coercedConstraintValue, formatter, validator, formSupport);
    119    }
    120
    121    private Object computeConstraintValue(String validatorType, Validator validator, String constraintValue,
    122                                          String formId, String overrideId, Messages overrideMessages)
    123    {
    124        Class constraintType = validator.getConstraintType();
    125
    126        String constraintText = findConstraintValue(validatorType, constraintType, constraintValue, formId, overrideId,
    127                overrideMessages);
    128
    129        if (constraintText == null)
    130            return null;
    131
    132        return typeCoercer.coerce(constraintText, constraintType);
    133    }
    134
    135    private String findConstraintValue(String validatorType, Class constraintType, String constraintValue,
    136                                       String formValidationId, String overrideId, Messages overrideMessages)
    137    {
    138        if (constraintValue != null)
    139            return constraintValue;
    140
    141        if (constraintType == null)
    142            return null;
    143
    144        // If no constraint was provided, check to see if it is available via a localized message
    145        // key. This is really handy for complex validations such as patterns.
    146
    147        String perFormKey = formValidationId + "-" + overrideId + "-" + validatorType;
    148
    149        if (overrideMessages.contains(perFormKey))
    150            return overrideMessages.get(perFormKey);
    151
    152        String generalKey = overrideId + "-" + validatorType;
    153
    154        if (overrideMessages.contains(generalKey))
    155            return overrideMessages.get(generalKey);
    156
    157        throw new IllegalArgumentException(String.format("Validator '%s' requires a validation constraint (of type %s) but none was provided. The constraint may be provided inside the @Validator annotation on the property, or in the associated component message catalog as key '%s' or key '%s'.", validatorType, constraintType.getName(), perFormKey,
    158                generalKey));
    159    }
    160
    161    private MessageFormatter findMessageFormatter(String formId, String overrideId, Messages overrideMessages,
    162                                                  String validatorType, Validator validator)
    163    {
    164
    165        String overrideKey = formId + "-" + overrideId + "-" + validatorType + "-message";
    166
    167        if (overrideMessages.contains(overrideKey))
    168            return overrideMessages.getFormatter(overrideKey);
    169
    170        overrideKey = overrideId + "-" + validatorType + "-message";
    171
    172        if (overrideMessages.contains(overrideKey))
    173            return overrideMessages.getFormatter(overrideKey);
    174
    175        String key = validator.getMessageKey();
    176
    177        return globalMessages.getFormatter(key);
    178    }
    179
    180    public FieldValidator createValidators(Field field, String specification)
    181    {
    182        List<ValidatorSpecification> specs = toValidatorSpecifications(specification);
    183
    184        List<FieldValidator> fieldValidators = CollectionFactory.newList();
    185
    186        for (ValidatorSpecification spec : specs)
    187        {
    188            fieldValidators.add(createValidator(field, spec.getValidatorType(), spec.getConstraintValue()));
    189        }
    190
    191        if (fieldValidators.size() == 1)
    192            return fieldValidators.get(0);
    193
    194        return new CompositeFieldValidator(fieldValidators);
    195    }
    196
    197    List<ValidatorSpecification> toValidatorSpecifications(String specification)
    198    {
    199        return expandMacros(parse(specification));
    200    }
    201
    202    private List<ValidatorSpecification> expandMacros(List<ValidatorSpecification> specs)
    203    {
    204        Map<String, Boolean> expandedMacros = CollectionFactory.newCaseInsensitiveMap();
    205        List<ValidatorSpecification> queue = CollectionFactory.newList(specs);
    206        List<ValidatorSpecification> result = CollectionFactory.newList();
    207
    208        while (!queue.isEmpty())
    209        {
    210            ValidatorSpecification head = queue.remove(0);
    211
    212            String validatorType = head.getValidatorType();
    213
    214            String expanded = validatorMacro.valueForMacro(validatorType);
    215            if (expanded != null)
    216            {
    217                if (head.getConstraintValue() != null)
    218                    throw new RuntimeException(String.format(
    219                            "'%s' is a validator macro, not a validator, and can not have a constraint value.",
    220                            validatorType));
    221
    222                if (expandedMacros.containsKey(validatorType))
    223                    throw new RuntimeException(String.format("Validator macro '%s' appears more than once.",
    224                            validatorType));
    225
    226                expandedMacros.put(validatorType, true);
    227
    228                List<ValidatorSpecification> parsed = parse(expanded);
    229
    230                // Add the new validator specifications to the front of the queue, replacing the validator macro
    231
    232                for (int i = 0; i < parsed.size(); i++)
    233                {
    234                    queue.add(i, parsed.get(i));
    235                }
    236            } else
    237            {
    238                result.add(head);
    239            }
    240        }
    241
    242        return result;
    243    }
    244
    245    /**
    246     * A code defining what the parser is looking for.
    247     */
    248    enum State
    249    {
    250
    251        /**
    252         * The start of a validator type.
    253         */
    254        TYPE_START,
    255        /**
    256         * The end of a validator type.
    257         */
    258        TYPE_END,
    259        /**
    260         * Equals sign after a validator type, or a comma.
    261         */
    262        EQUALS_OR_COMMA,
    263        /**
    264         * The start of a constraint value.
    265         */
    266        VALUE_START,
    267        /**
    268         * The end of the constraint value.
    269         */
    270        VALUE_END,
    271        /**
    272         * The comma after a constraint value.
    273         */
    274        COMMA
    275    }
    276
    277    static List<ValidatorSpecification> parse(String specification)
    278    {
    279        List<ValidatorSpecification> result = newList();
    280
    281        char[] input = specification.toCharArray();
    282
    283        int cursor = 0;
    284        int start = -1;
    285
    286        String type = null;
    287        boolean skipWhitespace = true;
    288        State state = State.TYPE_START;
    289
    290        while (cursor < input.length)
    291        {
    292            char ch = input[cursor];
    293
    294            if (skipWhitespace && Character.isWhitespace(ch))
    295            {
    296                cursor++;
    297                continue;
    298            }
    299
    300            skipWhitespace = false;
    301
    302            switch (state)
    303            {
    304
    305                case TYPE_START:
    306
    307                    if (Character.isLetter(ch))
    308                    {
    309                        start = cursor;
    310                        state = State.TYPE_END;
    311                        break;
    312                    }
    313
    314                    parseError(cursor, specification);
    315
    316                case TYPE_END:
    317
    318                    if (Character.isLetter(ch))
    319                    {
    320                        break;
    321                    }
    322
    323                    type = specification.substring(start, cursor);
    324
    325                    skipWhitespace = true;
    326                    state = State.EQUALS_OR_COMMA;
    327                    continue;
    328
    329                case EQUALS_OR_COMMA:
    330
    331                    if (ch == '=')
    332                    {
    333                        skipWhitespace = true;
    334                        state = State.VALUE_START;
    335                        break;
    336                    }
    337
    338                    if (ch == ',')
    339                    {
    340                        result.add(new ValidatorSpecification(type));
    341                        type = null;
    342                        state = State.COMMA;
    343                        continue;
    344                    }
    345
    346                    parseError(cursor, specification);
    347
    348                case VALUE_START:
    349
    350                    start = cursor;
    351                    state = State.VALUE_END;
    352                    break;
    353
    354                case VALUE_END:
    355
    356                    // The value ends when we hit whitespace or a comma
    357
    358                    if (Character.isWhitespace(ch) || ch == ',')
    359                    {
    360                        String value = specification.substring(start, cursor);
    361
    362                        result.add(new ValidatorSpecification(type, value));
    363                        type = null;
    364
    365                        skipWhitespace = true;
    366                        state = State.COMMA;
    367                        continue;
    368                    }
    369
    370                    break;
    371
    372                case COMMA:
    373
    374                    if (ch == ',')
    375                    {
    376                        skipWhitespace = true;
    377                        state = State.TYPE_START;
    378                        break;
    379                    }
    380
    381                    parseError(cursor, specification);
    382            } // case
    383
    384            cursor++;
    385        } // while
    386
    387        // cursor is now one character past end of string.
    388        // Cleanup whatever state we were in the middle of.
    389
    390        switch (state)
    391        {
    392            case TYPE_END:
    393
    394                type = specification.substring(start);
    395
    396            case EQUALS_OR_COMMA:
    397
    398                result.add(new ValidatorSpecification(type));
    399                break;
    400
    401            // Case when the specification ends with an equals sign.
    402
    403            case VALUE_START:
    404                result.add(new ValidatorSpecification(type, ""));
    405                break;
    406
    407            case VALUE_END:
    408
    409                result.add(new ValidatorSpecification(type, specification.substring(start)));
    410                break;
    411
    412            // For better or worse, ending the string with a comma is valid.
    413
    414            default:
    415        }
    416
    417        return result;
    418    }
    419
    420    private static void parseError(int cursor, String specification)
    421    {
    422        throw new RuntimeException(String.format("Unexpected character '%s' at position %d of input string: %s", specification.charAt(cursor), cursor + 1,
    423                specification));
    424    }
    425}