001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.commons.beanutils2.converters; 020 021import java.awt.Color; 022import java.util.Objects; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026/** 027 * {@link org.apache.commons.beanutils2.Converter} implementation that handles conversion to and from {@link Color}. 028 * 029 * <p> 030 * Will interpret hexadecimal colors similar to CSS engines, for example #RGB is interpreted as #RRGGBB. If using the literal hexadecimal value is desired, the 031 * value should be prefixed with {@code 0x} instead of {@link #HEX_COLOR_PREFIX #}. 032 * </p> 033 * 034 * @since 2.0.0 035 */ 036public class ColorConverter extends AbstractConverter<Color> { 037 038 /** Prefix for hexadecimal color notation. */ 039 private static final String HEX_COLOR_PREFIX = "#"; 040 041 /** Regular expression matching the output of {@link Color#toString()}. */ 042 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})\\]?$"); 043 044 /** 045 * Construct a <strong>{@link Color}</strong> <em>Converter</em> that throws a {@code ConversionException} if an error occurs. 046 */ 047 public ColorConverter() { 048 } 049 050 /** 051 * Constructs a {@link org.apache.commons.beanutils2.Converter} that will return the specified default value if a conversion error occurs. 052 * 053 * @param defaultValue The default value to be returned if the value to be converted is missing or an error occurs converting the value. 054 */ 055 public ColorConverter(final Color defaultValue) { 056 super(defaultValue); 057 } 058 059 /** 060 * Converts a {@link Color} into a {@link String}. 061 * 062 * <p> 063 * Supports hexadecimal colors like #RGB, #RRGGBB, #RGBA, and #RRGGBBAA, and interprets raw color names based on the colors defined in Java, such as: 064 * </p> 065 * 066 * <ul> 067 * <li>{@link Color#BLACK}</li> 068 * <li>{@link Color#BLUE}</li> 069 * <li>{@link Color#CYAN}</li> 070 * <li>{@link Color#DARK_GRAY}</li> 071 * <li>{@link Color#GRAY}</li> 072 * <li>{@link Color#GREEN}</li> 073 * <li>{@link Color#LIGHT_GRAY}</li> 074 * <li>{@link Color#MAGENTA}</li> 075 * <li>{@link Color#ORANGE}</li> 076 * <li>{@link Color#PINK}</li> 077 * <li>{@link Color#RED}</li> 078 * <li>{@link Color#WHITE}</li> 079 * <li>{@link Color#YELLOW}</li> 080 * </ul> 081 * 082 * @param type Data type to which this value should be converted. 083 * @param value The String property value to convert. 084 * @return A {@link Color} which represents the compiled configuration property. 085 * @throws NullPointerException If the value is null. 086 * @throws NumberFormatException If an invalid number is provided. 087 */ 088 @Override 089 protected <T> T convertToType(final Class<T> type, final Object value) throws Throwable { 090 if (Color.class.isAssignableFrom(type)) { 091 final String stringValue = toString(value); 092 093 switch (toLowerCase(stringValue)) { 094 case "black": 095 return type.cast(Color.BLACK); 096 case "blue": 097 return type.cast(Color.BLUE); 098 case "cyan": 099 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}