    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}