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.ioc.MessageFormatter;
022import org.apache.tapestry5.ioc.Messages;
023import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
024import org.apache.tapestry5.ioc.internal.util.InternalUtils;
025import org.apache.tapestry5.ioc.services.TypeCoercer;
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 java.util.List;
032import java.util.Locale;
033import java.util.Map;
034
035import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newList;
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(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}