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}