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>"specialMessages, extendedMessages"</code> is passed in,
162 * the text for a validation message will be searched first in the group
163 * "specialMessages", then in the group
164 * "extendedMessages", 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 }