001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.bean.validation;
018
019import java.lang.annotation.Annotation;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.List;
023import java.util.Map;
024import java.util.concurrent.ConcurrentHashMap;
025import java.util.concurrent.CopyOnWriteArrayList;
026import java.util.function.Supplier;
027
028import javax.validation.Validator;
029import javax.validation.constraints.NotEmpty;
030import javax.validation.constraints.NotNull;
031import javax.validation.constraints.Size;
032import javax.validation.metadata.ConstraintDescriptor;
033
034import org.apache.wicket.Application;
035import org.apache.wicket.MetaDataKey;
036import org.apache.wicket.markup.html.form.FormComponent;
037import org.apache.wicket.util.lang.Args;
038
039/**
040 * Configures bean validation and integrates it with Wicket
041 * 
042 * @author igor
043 * 
044 */
045public class BeanValidationConfiguration implements BeanValidationContext
046{
047        private static final MetaDataKey<BeanValidationConfiguration> KEY = new MetaDataKey<>()
048        {
049        };
050
051        /**
052         * Default list of annotations that make a component required.
053         */
054        static final List<Class<? extends Annotation>> REQUIRED_ANNOTATIONS;
055        static
056        {
057                List<Class<? extends Annotation>> tmp = new ArrayList<>();
058                tmp.add(NotNull.class);
059                try
060                {
061                        tmp.add(Class.forName("javax.validation.constraints.NotBlank")
062                                .asSubclass(Annotation.class));
063                        tmp.add(Class.forName("javax.validation.constraints.NotEmpty")
064                                .asSubclass(Annotation.class));
065                }
066                catch (ClassNotFoundException e)
067                {
068                        // ignore exception, we are using bean validation 1.1
069                }
070                REQUIRED_ANNOTATIONS = Collections.unmodifiableList(tmp);
071        }
072
073        private Supplier<Validator> validatorProvider = new DefaultValidatorProvider();
074
075        private IViolationTranslator violationTranslator = new DefaultViolationTranslator();
076
077        private List<IPropertyResolver> propertyResolvers = new CopyOnWriteArrayList<>();
078
079        private Map<Class<?>, ITagModifier<? extends Annotation>> tagModifiers = new ConcurrentHashMap<>();
080
081        public BeanValidationConfiguration()
082        {
083                add(new DefaultPropertyResolver());
084                register(Size.class, new SizeTagModifier());
085        }
086
087        /**
088         * Registers a tag modifier for a specific constraint annotation.
089         * <p>
090         * By default {@link Size} constraints are automatically mapped to <code>maxlength</code> of text inputs,
091         * this can be disabled by registering a {@link ITagModifier#NO_OP} instead:
092         * <code>
093         * configuration.register(Size.class, ITagModifier.NO_OP});
094         * </code>
095         * 
096         * @param annotationType
097         *            constraint annotation such as {@link Size}
098         * @param modifier
099         *            tag modifier to use
100         * @return {@code this}
101         */
102        public <T extends Annotation> BeanValidationConfiguration register(Class<T> annotationType,
103                ITagModifier<T> modifier)
104        {
105                Args.notNull(annotationType, "annotationType");
106                Args.notNull(modifier, "modifier");
107
108                tagModifiers.put(annotationType, modifier);
109
110                return this;
111        }
112
113        /**
114         * Get the registered modifier for the given annotation.
115         * 
116         * @see #register(Class, ITagModifier)
117         */
118        @Override
119        @SuppressWarnings("unchecked")
120        public <T extends Annotation> ITagModifier<T> getTagModifier(Class<T> annotationType)
121        {
122                Args.notNull(annotationType, "annotationType");
123
124                return (ITagModifier<T>)tagModifiers.get(annotationType);
125        }
126
127        /**
128         * Adds a property resolver to the configuration. Property resolvers registered last are the
129         * first to be allowed to resolve the property.
130         * 
131         * @param resolver
132         * @return {@code this}
133         */
134        public BeanValidationConfiguration add(IPropertyResolver resolver)
135        {
136                Args.notNull(resolver, "resolver");
137
138                // put newly added resolvers in the beginning so its possible to override previously added
139                // resolvers with newer ones
140                propertyResolvers.add(0, resolver);
141
142                return this;
143        }
144
145        /**
146         * @return validator
147         */
148        @Override
149        public Validator getValidator()
150        {
151                return validatorProvider.get();
152        }
153
154        /**
155         * Sets the provider used to retrieve {@link Validator} instances
156         * 
157         * @param validatorProvider
158         */
159        public void setValidatorProvider(Supplier<Validator> validatorProvider)
160        {
161                Args.notNull(validatorProvider, "validatorProvider");
162
163                this.validatorProvider = validatorProvider;
164        }
165
166        /**
167         * Binds this configuration to the application instance
168         * 
169         * @param application
170         */
171        public void configure(Application application)
172        {
173                application.setMetaData(KEY, this);
174        }
175
176        /** @return registered violation translator */
177        @Override
178        public IViolationTranslator getViolationTranslator()
179        {
180                return violationTranslator;
181        }
182
183        /**
184         * Registers a violation translator
185         *
186         * @param violationTranslator
187         *            A violation translator that will convert {@link javax.validation.ConstraintViolation}s into Wicket's
188         *            {@link org.apache.wicket.validation.ValidationError}s
189         */
190        public void setViolationTranslator(IViolationTranslator violationTranslator)
191        {
192                Args.notNull(violationTranslator, "violationTranslator");
193
194                this.violationTranslator = violationTranslator;
195        }
196
197        /**
198         * Retrieves the validation context (read only version of the configuration). This is how
199         * components retrieve the configuration.
200         * 
201         * @return validation context
202         */
203        public static BeanValidationContext get()
204        {
205                BeanValidationConfiguration config = Application.get().getMetaData(KEY);
206                if (config == null)
207                {
208                        throw new IllegalStateException(
209                                "Application instance has not yet been configured for bean validation. See BeanValidationConfiguration#configure(Application)");
210                }
211                return config;
212        }
213
214        @Override
215        public Property resolveProperty(FormComponent<?> component)
216        {
217                for (IPropertyResolver resolver : propertyResolvers)
218                {
219                        Property property = resolver.resolveProperty(component);
220                        if (property != null)
221                        {
222                                return property;
223                        }
224                }
225                return null;
226        }
227
228        /**
229         * By default {@link NotNull} and {@link NotEmpty} constraints make a component required.
230         * 
231         * @param constraint
232         *            constraint 
233         */
234        @Override
235        public boolean isRequiredConstraint(ConstraintDescriptor<?> constraint)
236        {
237                return REQUIRED_ANNOTATIONS.contains(constraint.getAnnotation().annotationType());
238        }
239}