1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.myfaces.trinidad.convert;
20
21 import java.awt.Color;
22
23 import java.text.ParseException;
24
25 import javax.el.ValueExpression;
26
27 import javax.faces.application.FacesMessage;
28 import javax.faces.component.StateHolder;
29 import javax.faces.component.UIComponent;
30 import javax.faces.context.FacesContext;
31 import javax.faces.convert.Converter;
32 import javax.faces.convert.ConverterException;
33 import javax.faces.el.ValueBinding;
34
35 import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFConverter;
36 import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFProperty;
37 import org.apache.myfaces.trinidad.bean.FacesBean;
38 import org.apache.myfaces.trinidad.bean.PropertyKey;
39 import org.apache.myfaces.trinidad.util.ComponentUtils;
40 import org.apache.myfaces.trinidad.util.MessageFactory;
41 import org.apache.myfaces.trinidad.logging.TrinidadLogger;
42
43 /**
44 * <p>Converters string to Color object and vice versa based on
45 * the patterns and the transparency set.</p>
46 *
47 * Pattern format for colors:
48 *
49 * <p>
50 * <strong>Color Format Syntax:</strong>
51 * <p>
52 * To specify the color format use a <em>color pattern</em> string.
53 * In this pattern, all ASCII letters are reserved as pattern letters,
54 * which are defined as the following:
55 * <blockquote>
56 * <pre>
57 * Symbol Meaning Presentation Example
58 * ------ ------- ------------ -------
59 * r red component (Number) 242
60 * g green component (Number) 242
61 * b blue component (Number) 242
62 * a alpha component (Number) 255
63 * R red component (Hex) F2
64 * G green component (Hex) F2
65 * B blue component (Hex) F2
66 * A alpha component (Hex) FF
67 * ' escape for text (Delimiter)
68 * '' single quote (Literal) '
69 * </pre>
70 * </blockquote>
71 * <p>
72 * <strong>Examples:</strong>
73 * <blockquote>
74 * <pre>
75 * Format Pattern Result
76 * -------------- -------
77 * "#RRGGBB" ->> #6609CC
78 * "rrr,ggg,bbb" ->> 102,009,204
79 * "t" ->> Transparent (when alpha is zero)
80 * </pre>
81 * </blockquote>
82 * <p>
83 * If patterns is not set then it defaults to patterns "#RRGGBB", "r,g,b"
84 * The first pattern is special - it is always used for formatting color values.
85 * For default case, getAsString() will use "#RRGGBB" format to represent the color.
86 * Object as String.</p>
87 *
88 * <p>The <code>getAsObject()</code> method parses a String into a {@link java.awt.Color},
89 * according to the following algorithm:</p>
90 * <ul>
91 * <li>If the specified String is null, return
92 * a <code>null</code>. Otherwise, trim leading and trailing
93 * whitespace before proceeding.</li>
94 * <li>If the specified String - after trimming - has a zero length,
95 * return <code>null</code>.</li>
96 * <li>Parses the trimmed string as mentioned in the documentation,
97 * If the string does not match the patterns specified throw
98 * {@link ConverterException} containing a {@link #CONVERT_MESSAGE_ID} message.
99 * The detail message of {@link #CONVERT_MESSAGE_ID} can be overridden
100 * by setting a custom error message, by calling
101 * method <code>setMessageDetailConvert()</code>. The custom message
102 * can contain placeholders as specified in {@link #CONVERT_MESSAGE_ID}.
103 * The placeholders will be replaced by appropriate values as mentioned
104 * in {@link #CONVERT_MESSAGE_ID}</li>
105 * </ul>
106 *
107 * <p>The <code>getAsString()</code> method expects a value of type
108 * {@link Color} (or a subclass), and creates a formatted
109 * String according to the following algorithm:</p>
110 * <ul>
111 * <li>If the specified value is null, return a zero-length String.</li>
112 * <li>If the specified value is a String, return it unmodified.</li>
113 * <li>return the string representation of color based on the first pattern.</li>
114 * </ul>
115 *
116 *
117 * @version $Name: $ ($Revision: adfrt/faces/adf-faces-api/src/main/java/oracle/adf/view/faces/convert/ColorConverter.java#0 $) $Date: 10-nov-2005.19:09:09 $
118 */
119 @JSFConverter(configExcluded=true)
120 public class ColorConverter implements Converter, StateHolder
121 {
122
123 /**
124 * <p>Standard converter id for this converter.</p>
125 */
126 public static final String CONVERTER_ID = "org.apache.myfaces.trinidad.Color";
127
128 /**
129 * <p>The message identifier of the {@link FacesMessage} to be created when
130 * input value cannot be converterd to color based on the patterns set.
131 * The message format string for this message may optionally include a
132 * <code>{0}</code>, <code>{1}</code>, <code>{4}</code> placeholdes,
133 * which will be replaced by input value, component label and the pattern
134 * set in the converter.</p>
135 */
136 public static final String CONVERT_MESSAGE_ID =
137 "org.apache.myfaces.trinidad.convert.ColorConverter.CONVERT";
138
139 /**
140 * <p>The string identifier for the "Transparent" option
141 * set in the converter.</p>
142 */
143 private static final String TRANSPARENT =
144 "org.apache.myfaces.trinidad.convert.ColorConverter.TRANSPARENT";
145
146 // Deprecated array: arrays should never be public constants,
147 // as they are mutable
148 /**
149 * @deprecated use getDefaultColorFormatPatterns()
150 */
151 @Deprecated
152 public static final String[] DEFAULT_COLOR_FORMAT_PATTERNS =
153 getDefaultColorFormatPatterns();
154
155
156 /**
157 * <p>Returns the default patterns to be used if the pattern if not supplied.
158 * The default patterns is <code>"#RRGGBB", "r,g,b"</code>
159 * The first pattern is special, it is always used for formatting color
160 * values</p>
161 */
162 public static final String[] getDefaultColorFormatPatterns()
163 {
164 return new String[]
165 {
166 "#RRGGBB",
167 "r,g,b"
168 };
169 }
170
171 /**
172 * <p>Construct a ColorConverter with preconfigured values.</p>
173 * @param patterns The set of R,G,B format patterns that
174 * are accepted by this Converter. The first
175 * pattern is special - it is always used for
176 * formatting color values.
177 * @param allowsTransparent Indicates whether or not transparent
178 * colors are considered valid.
179 */
180 public ColorConverter(
181 String[] patterns,
182 boolean allowsTransparent
183 )
184 {
185 if (patterns == null)
186 {
187 _facesBean.setProperty(_PATTERNS_KEY, getDefaultColorFormatPatterns());
188 }
189 else
190 {
191 _facesBean.setProperty(_PATTERNS_KEY, patterns);
192 }
193 setTransparentAllowed(allowsTransparent);
194 }
195
196 /**
197 * <p>Construct a ColorConverter with the default values.
198 * The defualt patterns being "#RRGGBB", "r,g,b" and allowsTransparent is set
199 * to false.</p>
200 */
201 public ColorConverter()
202 {
203 this(null, false);
204 }
205
206 /**
207 * <p>Convert the specified string value, which is associated with
208 * the specified {@link UIComponent}, into a Color object
209 * based on the patterns set.</p>
210 *
211 * @param context {@link FacesContext} for the request being processed
212 * @param component {@link UIComponent} with which this model object
213 * value is associated
214 * @param value String value to be converted (may be <code>null</code>)
215 *
216 * @return <code>null</code> if the value to convert is <code>null</code>,
217 * otherwise return a Color object.
218 *
219 * @exception ConverterException if conversion cannot be successfully
220 * performed
221 * @exception NullPointerException if <code>context</code> or
222 * <code>component</code> is <code>null</code>
223 *
224 */
225 public Object getAsObject(
226 FacesContext context,
227 UIComponent component,
228 String value)
229 {
230 if (context == null || component == null)
231 throw new NullPointerException(_LOG.getMessage(
232 "NULL_FACESCONTEXT_OR_UICOMPONENT"));
233
234 if (value == null)
235 return null;
236
237 if (isDisabled())
238 return value;
239
240 value = value.trim();
241
242 if (0 == value.length())
243 return null;
244
245 try
246 {
247 return _parseString(context, value);
248 }
249 catch (ParseException pe)
250 {
251 throw new ConverterException(
252 _getConvertMessage(context, component, value,
253 getPatterns()));
254 }
255 }
256
257 /**
258 * <p>Return a String representation for the Color object based on the first
259 * pattern in the given patterns.</p>
260 *
261 * @param context {@link FacesContext} for the request being processed
262 * @param component {@link UIComponent} with which this model object
263 * value is associated.
264 * @param value Model object value to be converted (may be <code>null</code>)
265 *
266 * @return a zero-length String if value is <code>null</code>,
267 * otherwise String representation for the Color object based on the first
268 * pattern in the specified patterns.
269 *
270 * @exception ConverterException if conversion cannot be successfully
271 * performed
272 * @exception NullPointerException if <code>context</code> or
273 * <code>component</code> is <code>null</code>
274 * @exception IllegalArgumentException if the <code>value</code> is not of
275 * type other than {@link java.awt.Color}, {@link java.lang.String} or
276 * <code>value</code> can be null.
277 *
278 */
279 public String getAsString(
280 FacesContext context,
281 UIComponent component,
282 Object value)
283 {
284 if (context == null || component == null)
285 throw new NullPointerException(_LOG.getMessage(
286 "NULL_FACESCONTEXT_OR_UICOMPONENT"));
287
288 if (value == null)
289 return "";
290
291 // if the incoming value is String then just return it.
292 if (value instanceof String)
293 return (String) value;
294
295 if (isDisabled())
296 return value.toString();
297
298 if (value instanceof Color)
299 {
300 return _formatObject(context, (Color)value);
301 }
302 else throw new IllegalArgumentException("'value' is not of type java.awt.Color'");
303 }
304
305 /**
306 * <p>Set if localized transparent text should be supported by this
307 * converter.</p>
308 * @param isTransparentAllowed
309 */
310 public void setTransparentAllowed(
311 boolean isTransparentAllowed)
312 {
313 Boolean isAllowed = (isTransparentAllowed ? Boolean.TRUE : Boolean.FALSE);
314 _facesBean.setProperty(_TRANSPARENT_ALLOWED_KEY, isAllowed);
315 }
316
317 /**
318 * <p>Set the R,G, B patterns, based on the patterns set, Color object is
319 * created during call to getAsObject(FacesContext,UIComponent, String),
320 * while based on the first pattern which is at index 0, the String
321 * representation for the object is determined with call to
322 * getAsString(FacesContext, UIComponent, Object). <code>null</code>
323 * value for patterns result in IllegalArgumentException.</p>
324 * @param patterns
325 * @throws IllegalArgumentException if a value of pattern is null or if an invalid pattern is passed.
326 */
327 public void setPatterns(String[] patterns) throws IllegalArgumentException
328 {
329 if (null == patterns || patterns.length == 0)
330 {
331 throw new IllegalArgumentException(_LOG.getMessage(
332 "PATTERN_MUST_HAVE_VALUE"));
333 }
334 String[] newPatterns = new String[patterns.length];
335 System.arraycopy(patterns, 0, newPatterns, 0, patterns.length);
336 _facesBean.setProperty(_PATTERNS_KEY, newPatterns);
337 }
338
339 /**
340 * <p>Retrun the patterns set for this converter.</p>
341 * @return patterns
342 */
343 @JSFProperty
344 public String[] getPatterns()
345 {
346 return ComponentUtils.resolveStringArray(_facesBean.getProperty(_PATTERNS_KEY));
347 }
348
349 /**
350 * <p>Return if localized transparent text should be supported by this
351 * converter.</p>
352 */
353 @JSFProperty(defaultValue="false")
354 public boolean isTransparentAllowed()
355 {
356 return ComponentUtils.resolveBoolean(_facesBean.getProperty(_TRANSPARENT_ALLOWED_KEY));
357 }
358
359 /**
360 * <p>Compares this ColorConverter with the specified Object for equality.</p>
361 * @param obj Object to which this ColorConverter is to be compared.
362 * @return true if and only if the specified Object is a ColorConverter
363 * and if the values patterns, transparentAllowed and transient are equal.
364 */
365 @Override
366 public boolean equals(Object obj)
367 {
368 if ( null == obj)
369 return false;
370
371 if (this == obj)
372 return true;
373
374 if (!(obj instanceof ColorConverter))
375 return false;
376
377 ColorConverter other = (ColorConverter)obj;
378
379 return ( ( this.isTransient() == other.isTransient() ) &&
380 ( this.isTransparentAllowed() == other.isTransparentAllowed()) &&
381 ( this.isDisabled() == other.isDisabled()) &&
382 ( _isEqualPatterns(other.getPatterns())) &&
383 ( ConverterUtils.equals(getMessageDetailConvert(),
384 other.getMessageDetailConvert()))
385 );
386 }
387
388 /**
389 * <p>Returns the hash code for this converter.</p>
390 * @return a hash code value for this object.
391 */
392 @Override
393 public int hashCode()
394 {
395 int result = 17;
396 result = 37 * result + (isTransient() ? 1 : 0);
397 result = 37 * result + (isTransparentAllowed() ? 1 : 0 );
398 result = 37 * result + (isDisabled() ? 1 : 0 );
399 String[] patterns = getPatterns();
400 for (int i = 0; i < patterns.length; i++)
401 {
402 result = 37 * result + patterns[i].hashCode();
403 }
404 String convMsgDet = getMessageDetailConvert();
405 result = result * 37 + (convMsgDet == null ? 0 : convMsgDet.hashCode());
406 return result;
407 }
408
409 public boolean isTransient()
410 {
411 return _isTransient;
412 }
413
414 public void setTransient(boolean isTransient)
415 {
416 _isTransient = isTransient;
417 }
418
419 public Object saveState(FacesContext context)
420 {
421 return _facesBean.saveState(context);
422 }
423
424 public void restoreState(FacesContext context, Object state)
425 {
426 _facesBean.restoreState(context, state);
427 }
428
429 /**
430 * <p>Set the {@link ValueExpression} used to calculate the value for the
431 * specified attribute if any.</p>
432 *
433 * @param name Name of the attribute for which to set a {@link ValueExpression}
434 * @param expression The {@link ValueExpression} to set, or <code>null</code>
435 * to remove any currently set {@link ValueExpression}
436 *
437 * @exception NullPointerException if <code>name</code>
438 * is <code>null</code>
439 * @exception IllegalArgumentException if <code>name</code> is not a valid
440 * attribute of this converter
441 */
442 public void setValueExpression(String name, ValueExpression expression)
443 {
444 ConverterUtils.setValueExpression(_facesBean, name, expression) ;
445 }
446
447
448 /**
449 * <p>Return the {@link ValueExpression} used to calculate the value for the
450 * specified attribute name, if any.</p>
451 *
452 * @param name Name of the attribute or property for which to retrieve a
453 * {@link ValueExpression}
454 *
455 * @exception NullPointerException if <code>name</code>
456 * is <code>null</code>
457 * @exception IllegalArgumentException if <code>name</code> is not a valid
458 * attribute of this converter
459 */
460 public ValueExpression getValueExpression(String name)
461 {
462 return ConverterUtils.getValueExpression(_facesBean, name);
463 }
464
465 /**
466 * <p>Set the {@link ValueBinding} used to calculate the value for the
467 * specified attribute if any.</p>
468 *
469 * @param name Name of the attribute for which to set a {@link ValueBinding}
470 * @param binding The {@link ValueBinding} to set, or <code>null</code>
471 * to remove any currently set {@link ValueBinding}
472 *
473 * @exception NullPointerException if <code>name</code>
474 * is <code>null</code>
475 * @exception IllegalArgumentException if <code>name</code> is not a valid
476 * attribute of this converter
477 * @deprecated
478 */
479 public void setValueBinding(String name, ValueBinding binding)
480 {
481 ConverterUtils.setValueBinding(_facesBean, name, binding) ;
482 }
483
484 /**
485 * <p>Return the {@link ValueBinding} used to calculate the value for the
486 * specified attribute name, if any.</p>
487 *
488 * @param name Name of the attribute or property for which to retrieve a
489 * {@link ValueBinding}
490 *
491 * @exception NullPointerException if <code>name</code>
492 * is <code>null</code>
493 * @exception IllegalArgumentException if <code>name</code> is not a valid
494 * attribute of this converter
495 * @deprecated
496 */
497 public ValueBinding getValueBinding(String name)
498 {
499 return ConverterUtils.getValueBinding(_facesBean, name);
500 }
501
502 /**
503 * Custom error message to be used, for creating detail part of
504 * the faces message, when <code>value</code> cannot be converted
505 * to {@link java.awt.Color}. Overrides the detail message identified by
506 * {@link #CONVERT_MESSAGE_ID}
507 * @param convertMessageDetail Custom error message.
508 * @see #CONVERT_MESSAGE_ID
509 */
510 public void setMessageDetailConvert(String convertMessageDetail)
511 {
512 _facesBean.setProperty(_CONVERT_MESSAGE_DETAIL_KEY, convertMessageDetail);
513 }
514
515 /**
516 * Return custom detail error message that was set for creating faces message,
517 * for values that cannot be converted to {@link java.awt.Color}
518 * @return Custom error message.
519 * @see #setMessageDetailConvert(String)
520 */
521 @JSFProperty
522 public String getMessageDetailConvert()
523 {
524 return ComponentUtils.resolveString(_facesBean.getProperty(_CONVERT_MESSAGE_DETAIL_KEY));
525 }
526
527 /**
528 * <p>Custom hint message.</p>
529 * Overrides default hint message
530 * @param hintFormat Custom hint message.
531 */
532 public void setHint(String hintFormat)
533 {
534 _facesBean.setProperty(_HINT_FORMAT_KEY, hintFormat);
535 }
536
537 /**
538 * <p>Return custom hint message.</p>
539 * @return Custom hint message.
540 * @see #setHint(String)
541 */
542 @JSFProperty(tagExcluded=true)
543 public String getHint()
544 {
545 Object obj = _facesBean.getProperty(_HINT_FORMAT_KEY);
546 return ComponentUtils.resolveString(obj);
547 }
548
549 /**
550 * <p>Set the value to property <code>disabled</code>. Default value is false.</p>
551 * @param isDisabled <code>true</code> if it's disabled, <code>false</code> otherwise.
552 */
553 public void setDisabled(boolean isDisabled)
554 {
555 _facesBean.setProperty(_DISABLED_KEY, Boolean.valueOf(isDisabled));
556 }
557
558 /**
559 * Return whether it is disabled.
560 * @return true if it's disabled and false if it's enabled.
561 */
562 public boolean isDisabled()
563 {
564 Boolean disabled = (Boolean) _facesBean.getProperty(_DISABLED_KEY);
565
566 return (disabled != null) ? disabled.booleanValue() : false;
567 }
568
569 protected String getTransparentString(FacesContext context)
570 {
571 String msg = MessageFactory.getString(context, TRANSPARENT);
572 return msg;
573 }
574
575 /**
576 * <p>Returns the value as a Color.</p>
577 */
578 private Object _parseString(
579 FacesContext context,
580 String colorString
581 ) throws ParseException
582 {
583 boolean isTrans = isTransparentAllowed();
584 if (isTrans &&
585 colorString != null &&
586 colorString.equals(getTransparentString(context)))
587 return _TRANSPARENT_COLOR;
588
589 ParseException pe = null;
590
591 String[] thePatterns = getPatterns();
592 for (int i=0; i < thePatterns.length; i++)
593 {
594 try
595 {
596 Object value = _getColorFormat(thePatterns[i]).parse(colorString);
597 return value;
598 }
599 catch (ParseException e)
600 {
601 // ignore, try lenient patterns
602 pe = e;
603 }
604 }
605 // throw the last parse exception
606 if (pe != null)
607 throw pe;
608
609 return null;
610 }
611
612 private String _formatObject(
613 FacesContext context,
614 Color color
615 )
616 {
617 if (color != null)
618 {
619 boolean isTrans = isTransparentAllowed();
620 if (isTrans && color.getAlpha() == 0)
621 return getTransparentString(context);
622
623 return _getFormattingColorFormat().format(color);
624 }
625 else
626 {
627 return null;
628 }
629 }
630
631 private ColorFormat _getFormattingColorFormat()
632 {
633 // Use the first pattern for formatting
634 return _getColorFormat(_getOutputPattern());
635 }
636
637 private ColorFormat _getColorFormat(
638 String pattern
639 )
640 {
641 return new RGBColorFormat(pattern);
642 }
643
644 private String _getOutputPattern()
645 {
646 String[] patterns = getPatterns();
647 return patterns[0];
648 }
649
650 private Object _getRawConvertMessageDetail()
651 {
652 return _facesBean.getRawProperty(_CONVERT_MESSAGE_DETAIL_KEY);
653 }
654
655 private boolean _isEqualPatterns(String[] patterns)
656 {
657 String[] thisPattern = getPatterns();
658 if (null == thisPattern && null == patterns)
659 return true;
660
661 if ((thisPattern == null && patterns != null) ||
662 (patterns == null && thisPattern != null) ||
663 (thisPattern.length != patterns.length))
664 return false;
665
666 for (int i = 0; i < thisPattern.length; i++)
667 {
668 if (!thisPattern[i].equals(patterns[i]))
669 return false;
670 }
671 return true;
672 }
673
674 private FacesMessage _getConvertMessage(
675 FacesContext context,
676 UIComponent component,
677 String value,
678 String[] patternsArray
679 )
680 {
681 Object noMatchMsgDet = _getRawConvertMessageDetail();
682
683 Object label = ConverterUtils.getComponentLabel(component);
684
685 StringBuffer patterns = new StringBuffer();
686 for (int i = 0; i < patternsArray.length ; i++)
687 {
688 patterns.append(patternsArray[i]);
689 patterns.append(' ');
690 }
691
692 Object[] params = {label, value, patterns, null, null};
693
694 FacesMessage msg = MessageFactory.getMessage(context,
695 CONVERT_MESSAGE_ID,
696 noMatchMsgDet,
697 params,
698 component);
699 return msg;
700 }
701
702
703 private static final Color _TRANSPARENT_COLOR = new Color(0,0,0,0);
704
705 /**
706 * Identifies if this class is to be persisted
707 */
708 private boolean _isTransient;
709
710 private static final FacesBean.Type _TYPE = new FacesBean.Type();
711
712 /**
713 * Should allow color to be transparent
714 */
715 private static final PropertyKey _TRANSPARENT_ALLOWED_KEY
716 = _TYPE.registerKey("transparentAllowed", Boolean.class, Boolean.FALSE);
717
718 private static final PropertyKey _PATTERNS_KEY
719 = _TYPE.registerKey("patterns", String[].class,
720 getDefaultColorFormatPatterns());
721
722 private static final PropertyKey _CONVERT_MESSAGE_DETAIL_KEY
723 = _TYPE.registerKey("messageDetailConvert", String.class);
724
725 private static final PropertyKey _HINT_FORMAT_KEY =
726 _TYPE.registerKey("hint", String.class);
727
728 // Default is false
729 private static final PropertyKey _DISABLED_KEY =
730 _TYPE.registerKey("disabled", Boolean.class, Boolean.FALSE);
731
732 private FacesBean _facesBean = ConverterUtils.getFacesBean(_TYPE);
733
734 private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(
735 ColorConverter.class);
736 }