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 }