View Javadoc

1   /*
2    * Copyright 2006-2016 The JGUIraffe Team.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License")
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.jguiraffe.transform;
17  
18  import java.text.MessageFormat;
19  import java.util.Locale;
20  import java.util.Map;
21  import java.util.MissingResourceException;
22  
23  import net.sf.jguiraffe.resources.Message;
24  import net.sf.jguiraffe.resources.ResourceManager;
25  
26  /**
27   * <p>
28   * A default implementation of the {@code ValidationMessageHandler} interface.
29   * </p>
30   * <p>
31   * This class performs the following steps for obtaining a
32   * {@link ValidationMessage} object for a given key:
33   * <ul>
34   * <li>It checks whether the passed in {@link TransformerContext} contains a
35   * property with the name of the given key. If this is the case, its value will
36   * be used as error message. This makes it possible to override validation
37   * messages directly in the builder script when the validator is defined.</li>
38   * <li>Otherwise the error message will be loaded from the application's
39   * resources using the key as resource ID. For the resource group the following
40   * options exist:
41   * <ul>
42   * <li>With the {@link #setAlternativeResourceGroups(String)} method a list of
43   * resource groups can be set, which will be searched for resources with message
44   * keys. So an application can define a custom resource group containing the
45   * error messages it wants to override and specify it here.</li>
46   * <li>If the resource for the message key cannot be found in one of the
47   * alternative resource groups, the resource group returned by the
48   * {@link #getDefaultResourceGroup()} method will be used. If not otherwise set,
49   * the name defined by the {@link #DEFAULT_RESOURCE_GROUP_NAME} constant is
50   * used. This is a resource group shipped with the framework, which contains
51   * default validation messages in a couple of supported languages.</li>
52   * </ul>
53   * </li>
54   * </ul>
55   * </p>
56   * <p>
57   * {@code DefaultValidationMessageHandler} also determines the
58   * {@link ValidationMessageLevel} of the messages it returns. This is done by
59   * evaluating the {@code errorLevel} property of the passed in
60   * {@link TransformerContext}. If this property is set, its value is interpreted
61   * as an enumeration literal defined by the {@link ValidationMessageLevel}
62   * class. If the property is undefined, the default message level
63   * {@link ValidationMessageLevel#ERROR} is used. The advantage behind this
64   * concept is that the message level is completely transparent for validators.
65   * So they can produce messages of any level, provided that the
66   * {@link TransformerContext} is properly configured.
67   * </p>
68   * <p>
69   * The {@link ValidationMessage} objects returned by this object are
70   * thread-safe; they can be concurrently accessed without requiring further
71   * synchronization.
72   * </p>
73   *
74   * @author Oliver Heger
75   * @version $Id: DefaultValidationMessageHandler.java 205 2012-01-29 18:29:57Z oheger $
76   */
77  public class DefaultValidationMessageHandler implements
78          ValidationMessageHandler
79  {
80      /**
81       * Constant for the default resource group with validation messages. This
82       * group name will be used if no specific default resource group has been
83       * set.
84       */
85      public static final String DEFAULT_RESOURCE_GROUP_NAME = "validators";
86  
87      /**
88       * Constant for the name of the property that defines the
89       * {@link ValidationMessageLevel} of the validation messages produced by
90       * this object. This property can be set in the properties of the
91       * {@link TransformerContext}.
92       */
93      public static final String PROP_MESSAGE_LEVEL = "errorLevel";
94  
95      /** Constant for the regex for splitting the alternative groups. */
96      private static final String SPLIT_REGEX = "\\s*,\\s*";
97  
98      /**
99       * Constant for the format key for generating the string for validation
100      * messages.
101      */
102     private static final String VM_FORMAT = "%s: \"%s\"";
103 
104     /** Stores the alternative resource groups to be searched for messages. */
105     private String alternativeResourceGroups;
106 
107     /** Stores the default resource group for validation messages. */
108     private String defaultResourceGroup;
109 
110     /** Stores an array with the alternative resource groups. */
111     private String[] alternativeGroupsList;
112 
113     /**
114      * Creates a new instance of <code>DefaultValidationMessageHandler</code>.
115      */
116     public DefaultValidationMessageHandler()
117     {
118         setAlternativeResourceGroups(null);
119     }
120 
121     /**
122      * Returns the name of the default resource group.
123      *
124      * @return the name of the default resource group
125      */
126     public String getDefaultResourceGroup()
127     {
128         return (defaultResourceGroup != null) ? defaultResourceGroup
129                 : DEFAULT_RESOURCE_GROUP_NAME;
130     }
131 
132     /**
133      * Sets the name of the default resource group. This group will be searched
134      * for resources for validation messages if the search in the alternative
135      * resource groups yielded no results. If no default resource group was set,
136      * the group defined by the
137      * <code>{@link #DEFAULT_RESOURCE_GROUP_NAME}</code> constant is used.
138      *
139      * @param defaultResourceGroup the new default resource group
140      * @see #setAlternativeResourceGroups(String)
141      */
142     public void setDefaultResourceGroup(String defaultResourceGroup)
143     {
144         this.defaultResourceGroup = defaultResourceGroup;
145     }
146 
147     /**
148      * Returns the alternative resource groups.
149      *
150      * @return the alternative resource groups
151      */
152     public String getAlternativeResourceGroups()
153     {
154         return alternativeResourceGroups;
155     }
156 
157     /**
158      * Sets the alternative resource groups. This method expects a comma
159      * separated string with names of resource groups to be searched for
160      * validation messages. If, for instance, the string
161      * <code>&quot;specialMessages, extendedMessages&quot;</code> is passed in,
162      * the text for a validation message will be searched first in the group
163      * &quot;specialMessages&quot;, then in the group
164      * &quot;extendedMessages&quot;, and finally in the default resource group.
165      * It is possible to set the alternative resource groups to <b>null</b>.
166      * Then only the default resource group will be searched.
167      *
168      * @param alternativeResourceGroups a comma separated list of alternative
169      *        resource groups
170      */
171     public void setAlternativeResourceGroups(String alternativeResourceGroups)
172     {
173         this.alternativeResourceGroups = alternativeResourceGroups;
174 
175         if (alternativeResourceGroups != null
176                 && alternativeResourceGroups.length() > 0)
177         {
178             alternativeGroupsList = alternativeResourceGroups
179                     .split(SPLIT_REGEX);
180         }
181         else
182         {
183             alternativeGroupsList = new String[0];
184         }
185     }
186 
187     /**
188      * Returns a validation message. This method implements the steps described
189      * in the class comment for obtaining a validation message for the given
190      * key.
191      *
192      * @param context the transformer context (must not be <b>null</b>)
193      * @param key the key of the message (must not be <b>null</b>)
194      * @param params additional parameters to be integrated into the message
195      * @return the validation message object for this key
196      * @throws IllegalArgumentException if the transformer context or the key
197      *         are <b>null</b>
198      */
199     public ValidationMessage getValidationMessage(TransformerContext context,
200             String key, Object... params)
201     {
202         if (context == null)
203         {
204             throw new IllegalArgumentException(
205                     "TransformerContext must not be null!");
206         }
207         if (key == null)
208         {
209             throw new IllegalArgumentException("Message key must not be null!");
210         }
211 
212         Object msg = context.properties().get(key);
213         if (msg != null)
214         {
215             return new ValidationMessagePlain(key, getMessageLevel(context),
216                     String.valueOf(msg), params);
217         }
218         else
219         {
220             return new ValidationMessageResource(key, getMessageLevel(context),
221                     context, alternativeGroupsList, getDefaultResourceGroup(),
222                     params);
223         }
224     }
225 
226     /**
227      * Determines the {@code ValidationMessageLevel} for the validation messages
228      * produced by this object. This implementation tests whether in the
229      * {@code TransformerContext} the {@link #PROP_MESSAGE_LEVEL} property is
230      * set. If so, its value is evaluated. Otherwise the default level
231      * {@link ValidationMessageLevel#ERROR} is returned.
232      *
233      * @param context the {@code TransformerContext}
234      * @return the {@code ValidationMessageLevel}
235      */
236     protected ValidationMessageLevel getMessageLevel(TransformerContext context)
237     {
238         Map<String, Object> props = context.properties();
239 
240         Object level = props.get(PROP_MESSAGE_LEVEL);
241         if (level == null)
242         {
243             return ValidationMessageLevel.ERROR;
244         }
245         else
246         {
247             if (level instanceof ValidationMessageLevel)
248             {
249                 return (ValidationMessageLevel) level;
250             }
251             else
252             {
253                 return ValidationMessageLevel.valueOf(level.toString()
254                         .toUpperCase(Locale.ENGLISH));
255             }
256         }
257     }
258 
259     /**
260      * An abstract base class for implementations of the {@code
261      * ValidationMessage} interface provided by this validation message handler.
262      * This class implements some common functionality.
263      */
264     private abstract static class ValidationMessageImpl implements
265             ValidationMessage
266     {
267         /** Stores the key of the message. */
268         private final String key;
269 
270         /** Stores the validation level. */
271         private final ValidationMessageLevel level;
272 
273         /**
274          * Creates a new instance of {@code ValidationMessageImpl} and
275          * initializes it with the key for the message and the level.
276          *
277          * @param msgKey the key
278          * @param msgLevel the {@code ValidationMessageLevel}
279          */
280         protected ValidationMessageImpl(String msgKey,
281                 ValidationMessageLevel msgLevel)
282         {
283             key = msgKey;
284             level = msgLevel;
285         }
286 
287         /**
288          * Returns the key of this validation message.
289          *
290          * @return the message key
291          */
292         public String getKey()
293         {
294             return key;
295         }
296 
297         /**
298          * Returns the validation level of this message.
299          *
300          * @return the {@code ValidationMessageLevel}
301          */
302         public ValidationMessageLevel getLevel()
303         {
304             return level;
305         }
306 
307         /**
308          * Checks equality with another object. Two {@code
309          * ValidationMessageImpl} objects are considered equal if and only if
310          * their message keys are equal and they have the same error level.
311          * (From the point of view of the validation framework only the key of a
312          * message is relevant, while the actual content is of less importance.)
313          *
314          * @param obj the object to compare to
315          * @return a flag of the objects are equal
316          */
317         @Override
318         public boolean equals(Object obj)
319         {
320             if (this == obj)
321             {
322                 return true;
323             }
324             if (!(obj instanceof ValidationMessageImpl))
325             {
326                 return false;
327             }
328 
329             assert getKey() != null : "Keys must not be null!";
330             ValidationMessageImpl c = (ValidationMessageImpl) obj;
331             return getKey().equals(c.getKey()) && getLevel() == c.getLevel();
332         }
333 
334         /**
335          * Returns a hash code for this object.
336          *
337          * @return a hash code
338          */
339         @Override
340         public int hashCode()
341         {
342             final int seed = 17;
343             final int factor = 31;
344 
345             int result = seed;
346             result = factor * result + getKey().hashCode();
347             result = factor * result + getLevel().hashCode();
348             return result;
349         }
350 
351         /**
352          * Returns a string representation for this message object. The string
353          * contains the key and the message.
354          *
355          * @return a string for this object
356          */
357         @Override
358         public String toString()
359         {
360             return String.format(VM_FORMAT, getKey(), getMessage());
361         }
362     }
363 
364     /**
365      * A specialized implementation of <code>ValidationMessage</code> that is
366      * directly initialized with the message to display. This implementation is
367      * pretty simple: it just returns the message it was passed.
368      */
369     private static class ValidationMessagePlain extends ValidationMessageImpl
370     {
371         /** Stores the message. */
372         private final String message;
373 
374         /**
375          * Creates a new instance of {@code ValidationMessagePlain} and
376          * initializes it with the message key, the level, and the content and
377          * optional parameters.
378          *
379          * @param msgKey the key
380          * @param msgLevel the message level
381          * @param msg the text of the message
382          * @param params the parameters for this message
383          */
384         public ValidationMessagePlain(String msgKey,
385                 ValidationMessageLevel msgLevel, String msg, Object... params)
386         {
387             super(msgKey, msgLevel);
388 
389             if (params.length > 0)
390             {
391                 message = MessageFormat.format(msg, params);
392             }
393             else
394             {
395                 message = msg;
396             }
397         }
398 
399         /**
400          * Returns the message text.
401          *
402          * @return the message
403          */
404         public String getMessage()
405         {
406             return message;
407         }
408     }
409 
410     /**
411      * A specialized implementation of the {@code ValidationMessage} interface
412      * that obtains the message text from the application's resources. When
413      * created, an instance is initialized with a set of resource groups, in
414      * which to look for the message text. On first access, these groups are
415      * searched in the given order. The first hit is used as message text.
416      */
417     private static class ValidationMessageResource extends
418             ValidationMessageImpl
419     {
420         /** An array with the resource groups to search for the message. */
421         private final String[] resourceGroups;
422 
423         /** Stores an array with parameters for the message. */
424         private final Object[] parameters;
425 
426         /** Stores the resource manager. */
427         private final ResourceManager resourceManager;
428 
429         /** Stores the locale. */
430         private final Locale locale;
431 
432         /** Stores the final message. */
433         private String message;
434 
435         /**
436          * Creates a new instance of {@code ValidationMessageResource} and
437          * initializes it.
438          *
439          * @param key the key of the message
440          * @param msgLevel the message level
441          * @param ctx the transformer context
442          * @param resGrps an array with the names of the alternative resource
443          *        groups
444          * @param defGrp the default resource group
445          * @param params the (optional) parameters for the message
446          */
447         public ValidationMessageResource(String key,
448                 ValidationMessageLevel msgLevel, TransformerContext ctx,
449                 String[] resGrps, String defGrp, Object... params)
450         {
451             super(key, msgLevel);
452             resourceManager = ctx.getResourceManager();
453             locale = ctx.getLocale();
454             parameters = params;
455             resourceGroups = initResourceGroups(resGrps, defGrp);
456         }
457 
458         /**
459          * Returns the message. If this is the first access, the message will be
460          * resolved from the resources (which may cause an exception).
461          *
462          * @return the message
463          * @throws MissingResourceException if the message cannot be resolved
464          */
465         public synchronized String getMessage()
466         {
467             if (message == null)
468             {
469                 message = resolveMessage();
470             }
471             return message;
472         }
473 
474         /**
475          * Initializes the array with the resource groups to search. The
476          * alternative resource groups and the default group are combined into
477          * one array.
478          *
479          * @param resGrps an array with alternative resource groups
480          * @param defGrp the default resource group
481          * @return the combined array with all resource groups
482          */
483         private String[] initResourceGroups(String[] resGrps, String defGrp)
484         {
485             if (resGrps.length == 0)
486             {
487                 return new String[] {
488                     defGrp
489                 };
490             }
491 
492             else
493             {
494                 String[] result = new String[resGrps.length + 1];
495                 System.arraycopy(resGrps, 0, result, 0, resGrps.length);
496                 result[resGrps.length] = defGrp;
497                 return result;
498             }
499         }
500 
501         /**
502          * Resolves the message from the resources.
503          *
504          * @return the resolved message
505          */
506         private String resolveMessage()
507         {
508             MissingResourceException ex = null;
509 
510             for (String grp : resourceGroups)
511             {
512                 Message msg = Message.createWithParameters(grp, getKey(),
513                         parameters);
514                 try
515                 {
516                     return msg.resolve(resourceManager, locale);
517                 }
518                 catch (MissingResourceException mrex)
519                 {
520                     // not found in this group => try others
521                     ex = mrex;
522                 }
523             }
524 
525             // The message was not found in all groups.
526             // Throw the last exception.
527             assert ex != null : "No missing resource exception thrown!";
528             throw ex;
529         }
530     }
531 }