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    }