View Javadoc
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   * https://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.commons.beanutils2.converters;
20  
21  import java.awt.Color;
22  import java.util.Objects;
23  import java.util.regex.Matcher;
24  import java.util.regex.Pattern;
25  
26  /**
27   * {@link org.apache.commons.beanutils2.Converter} implementation that handles conversion to and from {@link Color}.
28   *
29   * <p>
30   * Will interpret hexadecimal colors similar to CSS engines, for example #RGB is interpreted as #RRGGBB. If using the literal hexadecimal value is desired, the
31   * value should be prefixed with {@code 0x} instead of {@link #HEX_COLOR_PREFIX #}.
32   * </p>
33   *
34   * @since 2.0.0
35   */
36  public class ColorConverter extends AbstractConverter<Color> {
37  
38      /** Prefix for hexadecimal color notation. */
39      private static final String HEX_COLOR_PREFIX = "#";
40  
41      /** Regular expression matching the output of {@link Color#toString()}. */
42      private static final Pattern JAVA_COLOR_PATTERN = Pattern.compile("^(?:[A-Za-z\\d._]+)??\\[?(?:r=)?(\\d{1,3}),(?:g=)?(\\d{1,3}),(?:b=)?(\\d{1,3})\\]?$");
43  
44      /**
45       * Construct a <strong>{@link Color}</strong> <em>Converter</em> that throws a {@code ConversionException} if an error occurs.
46       */
47      public ColorConverter() {
48      }
49  
50      /**
51       * Constructs a {@link org.apache.commons.beanutils2.Converter} that will return the specified default value if a conversion error occurs.
52       *
53       * @param defaultValue The default value to be returned if the value to be converted is missing or an error occurs converting the value.
54       */
55      public ColorConverter(final Color defaultValue) {
56          super(defaultValue);
57      }
58  
59      /**
60       * Converts a {@link Color} into a {@link String}.
61       *
62       * <p>
63       * Supports hexadecimal colors like #RGB, #RRGGBB, #RGBA, and #RRGGBBAA, and interprets raw color names based on the colors defined in Java, such as:
64       * </p>
65       *
66       * <ul>
67       * <li>{@link Color#BLACK}</li>
68       * <li>{@link Color#BLUE}</li>
69       * <li>{@link Color#CYAN}</li>
70       * <li>{@link Color#DARK_GRAY}</li>
71       * <li>{@link Color#GRAY}</li>
72       * <li>{@link Color#GREEN}</li>
73       * <li>{@link Color#LIGHT_GRAY}</li>
74       * <li>{@link Color#MAGENTA}</li>
75       * <li>{@link Color#ORANGE}</li>
76       * <li>{@link Color#PINK}</li>
77       * <li>{@link Color#RED}</li>
78       * <li>{@link Color#WHITE}</li>
79       * <li>{@link Color#YELLOW}</li>
80       * </ul>
81       *
82       * @param type  Data type to which this value should be converted.
83       * @param value The String property value to convert.
84       * @return A {@link Color} which represents the compiled configuration property.
85       * @throws NullPointerException  If the value is null.
86       * @throws NumberFormatException If an invalid number is provided.
87       */
88      @Override
89      protected <T> T convertToType(final Class<T> type, final Object value) throws Throwable {
90          if (Color.class.isAssignableFrom(type)) {
91              final String stringValue = toString(value);
92  
93              switch (toLowerCase(stringValue)) {
94              case "black":
95                  return type.cast(Color.BLACK);
96              case "blue":
97                  return type.cast(Color.BLUE);
98              case "cyan":
99                  return type.cast(Color.CYAN);
100             case "darkgray":
101             case "darkgrey":
102             case "dark_gray":
103             case "dark_grey":
104                 return type.cast(Color.DARK_GRAY);
105             case "gray":
106             case "grey":
107                 return type.cast(Color.GRAY);
108             case "green":
109                 return type.cast(Color.GREEN);
110             case "lightgray":
111             case "lightgrey":
112             case "light_gray":
113             case "light_grey":
114                 return type.cast(Color.LIGHT_GRAY);
115             case "magenta":
116                 return type.cast(Color.MAGENTA);
117             case "orange":
118                 return type.cast(Color.ORANGE);
119             case "pink":
120                 return type.cast(Color.PINK);
121             case "red":
122                 return type.cast(Color.RED);
123             case "white":
124                 return type.cast(Color.WHITE);
125             case "yellow":
126                 return type.cast(Color.YELLOW);
127             default:
128                 // Do nothing.
129             }
130 
131             if (stringValue.startsWith(HEX_COLOR_PREFIX)) {
132                 return type.cast(parseHexadecimalColor(stringValue));
133             }
134 
135             if (stringValue.contains(",")) {
136                 return type.cast(parseToStringColor(stringValue));
137             }
138 
139             return type.cast(Color.decode(stringValue));
140         }
141 
142         throw conversionException(type, value);
143     }
144 
145     /**
146      * Gets the default type this {@code Converter} handles.
147      *
148      * @return The default type this {@code Converter} handles.
149      * @since 2.0.0
150      */
151     @Override
152     protected Class<Color> getDefaultType() {
153         return Color.class;
154     }
155 
156     /**
157      * Returns a {@link Color} for hexadecimal colors.
158      *
159      * @param value Hexadecimal representation of a color.
160      * @return The converted value.
161      * @throws NumberFormatException If the hexadecimal input contains non parsable characters.
162      */
163     private Color parseHexadecimalColor(final String value) {
164         Objects.requireNonNull(value);
165 
166         switch (value.length()) {
167         case 4:
168             return new Color(Integer.parseInt(value.substring(1, 2), 16) * 17, Integer.parseInt(value.substring(2, 3), 16) * 17,
169                     Integer.parseInt(value.substring(3, 4), 16) * 17);
170         case 5:
171             return new Color(Integer.parseInt(value.substring(1, 2), 16) * 17, Integer.parseInt(value.substring(2, 3), 16) * 17,
172                     Integer.parseInt(value.substring(3, 4), 16) * 17, Integer.parseInt(value.substring(4, 5), 16) * 17);
173         case 7:
174             return new Color(Integer.parseInt(value.substring(1, 3), 16), Integer.parseInt(value.substring(3, 5), 16),
175                     Integer.parseInt(value.substring(5, 7), 16));
176         case 9:
177             return new Color(Integer.parseInt(value.substring(1, 3), 16), Integer.parseInt(value.substring(3, 5), 16),
178                     Integer.parseInt(value.substring(5, 7), 16), Integer.parseInt(value.substring(7, 9), 16));
179         default:
180             throw new IllegalArgumentException("Value is an malformed hexadecimal color, if literal value decoding "
181                     + "is required, prefix with 0x instead of #, otherwise expecting 3, 4, 6, or 8 characters only.");
182         }
183     }
184 
185     /**
186      * Parses the Color based on the result of the {@link Color#toString()} method.
187      *
188      * Accepts the following values:
189      * <ul>
190      * <li>{@code java.awt.Color[r=255,g=255,b=255]}</li>
191      * <li>{@code [r=255,g=255,b=255]}</li>
192      * <li>{@code r=255,g=255,b=255}</li>
193      * <li>{@code 255,255,255}</li>
194      * </ul>
195      *
196      * @param value A color as represented by {@link Color#toString()}.
197      * @return The Java friendly {@link Color} this color represents.
198      * @throws IllegalArgumentException If the input can't be matches by the {@link #JAVA_COLOR_PATTERN} or a {@link Color} component specified is over 255.
199      */
200     private Color parseToStringColor(final String value) {
201         Objects.requireNonNull(value);
202 
203         final Matcher matcher = JAVA_COLOR_PATTERN.matcher(value);
204 
205         if (!matcher.matches()) {
206             throw new IllegalArgumentException("Invalid Color String provided. Could not parse.");
207         }
208 
209         final int red = Integer.parseInt(matcher.group(1));
210         final int green = Integer.parseInt(matcher.group(2));
211         final int blue = Integer.parseInt(matcher.group(3));
212 
213         if (red > 255 || green > 255 || blue > 255) {
214             throw new IllegalArgumentException("Color component integers must be between 0 and 255.");
215         }
216 
217         return new Color(red, green, blue);
218     }
219 }