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 *      https://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.commons.validator;
018
019import java.io.Serializable;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.Locale;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.Objects;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029
030/**
031 * Holds a set of {@code Form}s stored associated with a {@link Locale}
032 * based on the country, language, and variant specified. Instances of this
033 * class are configured with a <formset> xml element.
034 */
035public class FormSet implements Serializable {
036
037    private static final long serialVersionUID = -8936513232763306055L;
038
039    /**
040     * This is the type of {@code FormSet}s where no locale is specified.
041     */
042    protected static final int GLOBAL_FORMSET = 1;
043
044    /**
045     * This is the type of {@code FormSet}s where only language locale is
046     * specified.
047     */
048    protected static final int LANGUAGE_FORMSET = 2;
049
050    /**
051     * This is the type of {@code FormSet}s where only language and country
052     * locale are specified.
053     */
054    protected static final int COUNTRY_FORMSET = 3;
055
056    /**
057     * This is the type of {@code FormSet}s where full locale has been set.
058     */
059    protected static final int VARIANT_FORMSET = 4;
060
061    /** Logging */
062    private transient Log log = LogFactory.getLog(FormSet.class);
063
064    /**
065     * Whether or not the this {@code FormSet} was processed for replacing
066     * variables in strings with their values.
067     */
068    private boolean processed;
069
070    /** Language component of {@link Locale} (required). */
071    private String language;
072
073    /** Country component of {@link Locale} (optional). */
074    private String country;
075
076    /** Variant component of {@link Locale} (optional). */
077    private String variant;
078
079    /**
080     * A {@link Map} of {@code Form}s using the name field of the
081     * {@code Form} as the key.
082     */
083    private final Map<String, Form> forms = new HashMap<>();
084
085    /**
086     * A {@link Map} of {@code Constant}s using the name field of the
087     * {@code Constant} as the key.
088     */
089    private final Map<String, String> constants = new HashMap<>();
090
091    /**
092     * Flag indicating if this formSet has been merged with its parent (higher
093     * rank in Locale hierarchy).
094     */
095    private volatile boolean merged;
096
097    /**
098     * Constructs a new instance.
099     */
100    public FormSet() {
101        // empty
102    }
103
104    /**
105     * Add a {@code Constant} to the locale level.
106     *
107     * @param name   The constant name
108     * @param value  The constant value
109     */
110    public void addConstant(final String name, final String value) {
111        if (constants.containsKey(name)) {
112            getLog().error("Constant '" + name + "' already exists in FormSet[" + displayKey() + "] - ignoring.");
113        } else {
114            constants.put(name, value);
115        }
116    }
117
118    /**
119     * Add a {@code Form} to the {@code FormSet}.
120     *
121     * @param f  The form
122     */
123    public void addForm(final Form f) {
124
125        final String formName = f.getName();
126        if (forms.containsKey(formName)) {
127            getLog().error("Form '" + formName + "' already exists in FormSet[" + displayKey() + "] - ignoring.");
128
129        } else {
130            forms.put(f.getName(), f);
131        }
132
133    }
134
135    /**
136     * Returns a string representation of the object's key.
137     *
138     * @return   A string representation of the key
139     */
140    public String displayKey() {
141        final StringBuilder results = new StringBuilder();
142        if (language != null && !language.isEmpty()) {
143            results.append("language=");
144            results.append(language);
145        }
146        if (country != null && !country.isEmpty()) {
147            if (results.length() > 0) {
148                results.append(", ");
149            }
150            results.append("country=");
151            results.append(country);
152        }
153        if (variant != null && !variant.isEmpty()) {
154            if (results.length() > 0) {
155                results.append(", ");
156            }
157            results.append("variant=");
158            results.append(variant);
159        }
160        if (results.length() == 0) {
161            results.append("default");
162        }
163
164        return results.toString();
165    }
166
167    /**
168     * Gets the equivalent of the country component of {@link Locale}.
169     *
170     * @return   The country value
171     */
172    public String getCountry() {
173        return country;
174    }
175
176    /**
177     * Retrieve a {@code Form} based on the form name.
178     *
179     * @param formName  The form name
180     * @return          The form
181     */
182    public Form getForm(final String formName) {
183        return forms.get(formName);
184    }
185
186    /**
187     * A {@link Map} of {@code Form}s is returned as an unmodifiable
188     * {@link Map} with the key based on the form name.
189     *
190     * @return   The forms map
191     */
192    public Map<String, Form> getForms() {
193        return Collections.unmodifiableMap(forms);
194    }
195
196    /**
197     * Gets the equivalent of the language component of {@link Locale}.
198     *
199     * @return   The language value
200     */
201    public String getLanguage() {
202        return language;
203    }
204
205    /**
206     * Accessor method for Log instance.
207     *
208     * The Log instance variable is transient and
209     * accessing it through this method ensures it
210     * is re-initialized when this instance is
211     * de-serialized.
212     *
213     * @return The Log instance.
214     */
215    private Log getLog() {
216        if (log == null) {
217            log = LogFactory.getLog(FormSet.class);
218        }
219        return log;
220    }
221
222    /**
223     * Returns the type of {@code FormSet}:{@code GLOBAL_FORMSET},
224     * {@code LANGUAGE_FORMSET},{@code COUNTRY_FORMSET} or {@code VARIANT_FORMSET}.
225     *
226     * @return                       The type value
227     * @throws NullPointerException  if there is inconsistency in the locale
228     *      definition (not sure about this)
229     * @since 1.2.0
230     */
231    protected int getType() {
232        final String myLanguage = getLanguage();
233        final String myCountry = getCountry();
234        if (getVariant() != null) {
235            Objects.requireNonNull(myLanguage, "When variant is specified, country and language must be specified.");
236            Objects.requireNonNull(myCountry, "When variant is specified, country and language must be specified.");
237            return VARIANT_FORMSET;
238        }
239        if (myCountry != null) {
240            Objects.requireNonNull(myLanguage, "When country is specified, language must be specified.");
241            return COUNTRY_FORMSET;
242        }
243        if (myLanguage != null) {
244            return LANGUAGE_FORMSET;
245        }
246        return GLOBAL_FORMSET;
247    }
248
249    /**
250     * Gets the equivalent of the variant component of {@link Locale}.
251     *
252     * @return   The variant value
253     */
254    public String getVariant() {
255        return variant;
256    }
257
258    /**
259     * Has this formSet been merged?
260     *
261     * @return   true if it has been merged
262     * @since 1.2.0
263     */
264    protected boolean isMerged() {
265        return merged;
266    }
267
268    /**
269     * Whether or not the this {@code FormSet} was processed for replacing
270     * variables in strings with their values.
271     *
272     * @return   The processed value
273     */
274    public boolean isProcessed() {
275        return processed;
276    }
277
278    /**
279     * Merges the given {@code FormSet} into this one. If any of {@code depends}
280     * s {@code Forms} are not in this {@code FormSet} then, include
281     * them, else merge both {@code Forms}. Theoretically we should only
282     * merge a "parent" formSet.
283     *
284     * @param depends  FormSet to be merged
285     * @since 1.2.0
286     */
287    protected void merge(final FormSet depends) {
288        if (depends != null) {
289            final Map<String, Form> myForms = getForms();
290            final Map<String, Form> dependsForms = depends.getForms();
291            for (final Entry<String, Form> entry : dependsForms.entrySet()) {
292                final String key = entry.getKey();
293                final Form form = myForms.get(key);
294                if (form != null) { // merge, but principal 'rules', don't overwrite
295                    // anything
296                    form.merge(entry.getValue());
297                } else { // just add
298                    addForm(entry.getValue());
299                }
300            }
301        }
302        merged = true;
303    }
304
305    /**
306     * Processes all of the {@code Form}s.
307     *
308     * @param globalConstants  Global constants
309     */
310    synchronized void process(final Map<String, String> globalConstants) {
311        for (final Form f : forms.values()) {
312            f.process(globalConstants, constants, forms);
313        }
314
315        processed = true;
316    }
317
318    /**
319     * Sets the equivalent of the country component of {@link Locale}.
320     *
321     * @param country  The new country value
322     */
323    public void setCountry(final String country) {
324        this.country = country;
325    }
326
327    /**
328     * Sets the equivalent of the language component of {@link Locale}.
329     *
330     * @param language  The new language value
331     */
332    public void setLanguage(final String language) {
333        this.language = language;
334    }
335
336    /**
337     * Sets the equivalent of the variant component of {@link Locale}.
338     *
339     * @param variant  The new variant value
340     */
341    public void setVariant(final String variant) {
342        this.variant = variant;
343    }
344
345    /**
346     * Returns a string representation of the object.
347     *
348     * @return   A string representation
349     */
350    @Override
351    public String toString() {
352        final StringBuilder results = new StringBuilder();
353
354        results.append("FormSet: language=");
355        results.append(language);
356        results.append("  country=");
357        results.append(country);
358        results.append("  variant=");
359        results.append(variant);
360        results.append("\n");
361
362        for (final Object name : getForms().values()) {
363            results.append("   ");
364            results.append(name);
365            results.append("\n");
366        }
367
368        return results.toString();
369    }
370}