| 
001 /*002  * Copyright 2008-2015 the original author or authors.
 003  *
 004  * Licensed under the Apache License, Version 2.0 (the "License");
 005  * you may not use this file except in compliance with the License.
 006  * You may obtain a copy of the License at
 007  *
 008  *     http://www.apache.org/licenses/LICENSE-2.0
 009  *
 010  * Unless required by applicable law or agreed to in writing, software
 011  * distributed under the License is distributed on an "AS IS" BASIS,
 012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 013  * See the License for the specific language governing permissions and
 014  * limitations under the License.
 015  */
 016 package griffon.javafx.formatters;
 017
 018 import griffon.core.formatters.AbstractFormatter;
 019 import griffon.core.formatters.ParseException;
 020 import javafx.scene.paint.Color;
 021
 022 import javax.annotation.Nonnull;
 023 import javax.annotation.Nullable;
 024 import java.lang.reflect.Field;
 025 import java.util.Arrays;
 026
 027 import static griffon.util.GriffonNameUtils.isBlank;
 028 import static java.lang.Integer.toHexString;
 029 import static java.util.Objects.requireNonNull;
 030
 031 /**
 032  * <p>A {@code Formatter} that can parse Strings into {@code javafx.scene.paint.Color} and back
 033  * using several patterns</p>
 034  * <p/>
 035  * <p>
 036  * Supported patterns are:
 037  * <ul>
 038  * <li>{@code #RGB}</li>
 039  * <li>{@code #RGBA}</li>
 040  * <li>{@code #RRGGBB}</li>
 041  * <li>{@code #RRGGBBAA}</li>
 042  * </ul>
 043  * Where each letter stands for a particular color components in hexadecimal
 044  * <ul>
 045  * <li>{@code R} - red</li>
 046  * <li>{@code G} - green</li>
 047  * <li>{@code B} - blue</li>
 048  * <li>{@code A} - alpha</li>
 049  * </ul>
 050  * </p>
 051  *
 052  * @author Andres Almiray
 053  * @see griffon.core.formatters.Formatter
 054  * @since 2.0.0
 055  */
 056 public class ColorFormatter extends AbstractFormatter<Color> {
 057     /**
 058      * "#RGB"
 059      */
 060     public static final String PATTERN_SHORT = "#RGB";
 061
 062     /**
 063      * "#RGBA"
 064      */
 065     public static final String PATTERN_SHORT_WITH_ALPHA = "#RGBA";
 066
 067     /**
 068      * "#RRGGBB"
 069      */
 070     public static final String PATTERN_LONG = "#RRGGBB";
 071
 072     /**
 073      * "#RRGGBBAA"
 074      */
 075     public static final String PATTERN_LONG_WITH_ALPHA = "#RRGGBBAA";
 076
 077     /**
 078      * "#RRGGBB"
 079      */
 080     public static final String DEFAULT_PATTERN = PATTERN_LONG;
 081
 082     private static final String[] PATTERNS = new String[]{
 083         PATTERN_LONG,
 084         PATTERN_LONG_WITH_ALPHA,
 085         PATTERN_SHORT,
 086         PATTERN_SHORT_WITH_ALPHA
 087     };
 088
 089     /**
 090      * {@code ColorFormatter} that uses the <b>{@code PATTERN_SHORT}</b> pattern
 091      */
 092     public static final ColorFormatter SHORT = new ColorFormatter(PATTERN_SHORT);
 093
 094     /**
 095      * {@code ColorFormatter} that uses the <b>{@code PATTERN_SHORT_WITH_ALPHA}</b> pattern
 096      */
 097     public static final ColorFormatter SHORT_WITH_ALPHA = new ColorFormatter(PATTERN_SHORT_WITH_ALPHA);
 098
 099     /**
 100      * {@code ColorFormatter} that uses the <b>{@code PATTERN_LONG}</b> pattern
 101      */
 102     public static final ColorFormatter LONG = new ColorFormatter(PATTERN_LONG);
 103
 104     /**
 105      * {@code ColorFormatter} that uses the <b>{@code PATTERN_LONG_WITH_ALPHA}</b> pattern
 106      */
 107     public static final ColorFormatter LONG_WITH_ALPHA = new ColorFormatter(PATTERN_LONG_WITH_ALPHA);
 108
 109     /**
 110      * <p>Returns a {@code ColorFormatter} given a color pattern.</p>
 111      *
 112      * @param pattern the input pattern. Must be one of the 4 supported color patterns.
 113      * @return a {@code ColorPattern} instance
 114      * @throws IllegalArgumentException if the supplied {@code pattern} is not supported
 115      */
 116     @Nonnull
 117     public static ColorFormatter getInstance(@Nonnull String pattern) {
 118         return new ColorFormatter(pattern);
 119     }
 120
 121     private final ColorFormatterDelegate delegate;
 122
 123     protected ColorFormatter(@Nullable String pattern) {
 124         if (PATTERN_SHORT.equals(pattern)) {
 125             delegate = new ShortColorFormatterDelegate();
 126         } else if (PATTERN_SHORT_WITH_ALPHA.equals(pattern)) {
 127             delegate = new ShortWithAlphaColorFormatterDelegate();
 128         } else if (PATTERN_LONG.equals(pattern)) {
 129             delegate = new LongColorFormatterDelegate();
 130         } else if (PATTERN_LONG_WITH_ALPHA.equals(pattern)) {
 131             delegate = new LongWithAlphaColorFormatterDelegate();
 132         } else if (isBlank(pattern)) {
 133             delegate = new LongColorFormatterDelegate();
 134         } else {
 135             throw new IllegalArgumentException("Invalid pattern '" + pattern + "'. Valid patterns are " + Arrays.toString(PATTERNS));
 136         }
 137     }
 138
 139     @Nullable
 140     public String format(@Nullable Color color) {
 141         return color == null ? null : delegate.format(color);
 142     }
 143
 144     @Nullable
 145     public Color parse(@Nullable String str) throws ParseException {
 146         return isBlank(str) ? null : delegate.parse(str);
 147     }
 148
 149     /**
 150      * Returns the pattern used by this {@code ColorFormatter}
 151      *
 152      * @return the pattern this {@code ColorFormatter} uses for parsing/formatting.
 153      */
 154     @Nonnull
 155     public String getPattern() {
 156         return delegate.getPattern();
 157     }
 158
 159     /**
 160      * <p>Parses a string into a {@code javafx.scene.paint.Color} instance.</p>
 161      * <p>The parsing pattern is chosen given the length of the input string
 162      * <ul>
 163      * <li>4 - {@code #RGB}</li>
 164      * <li>5 - {@code #RGBA}</li>
 165      * <li>7 - {@code #RRGGBB}</li>
 166      * <li>9 - {@code #RRGGBBAA}</li>
 167      * </ul>
 168      * </p>
 169      * The input string may also be any of the Color constants identified by
 170      * {@code javafx.scene.paint.Color}.
 171      *
 172      * @param str the string representation of a {@code javafx.scene.paint.Color}
 173      * @return a {@code javafx.scene.paint.Color} instance matching the supplied RGBA color components
 174      * @throws ParseException if the string cannot be parsed by the chosen pattern
 175      * @see javafx.scene.paint.Color
 176      */
 177     @Nonnull
 178     @SuppressWarnings("ConstantConditions")
 179     public static Color parseColor(@Nonnull String str) throws ParseException {
 180         if (str.startsWith("#")) {
 181             switch (str.length()) {
 182                 case 4:
 183                     return SHORT.parse(str);
 184                 case 5:
 185                     return SHORT_WITH_ALPHA.parse(str);
 186                 case 7:
 187                     return LONG.parse(str);
 188                 case 9:
 189                     return LONG_WITH_ALPHA.parse(str);
 190                 default:
 191                     throw parseError(str, Color.class);
 192             }
 193         } else {
 194             // assume it's a Color constant
 195             try {
 196                 String colorFieldName = str.toUpperCase();
 197                 Field field = Color.class.getField(colorFieldName);
 198                 return (Color) field.get(null);
 199             } catch (Exception e) {
 200                 throw parseError(str, Color.class, e);
 201             }
 202         }
 203     }
 204
 205     private static interface ColorFormatterDelegate {
 206         @Nonnull
 207         String getPattern();
 208
 209         @Nonnull
 210         String format(@Nonnull Color color);
 211
 212         @Nonnull
 213         Color parse(@Nonnull String str) throws ParseException;
 214     }
 215
 216     private static abstract class AbstractColorFormatterDelegate implements ColorFormatterDelegate {
 217         private final String pattern;
 218
 219         private AbstractColorFormatterDelegate(@Nonnull String pattern) {
 220             this.pattern = pattern;
 221         }
 222
 223         @Nonnull
 224         public String getPattern() {
 225             return pattern;
 226         }
 227     }
 228
 229     private static int red(Color color) {
 230         return toIntColor(color.getRed());
 231     }
 232
 233     private static int green(Color color) {
 234         return toIntColor(color.getGreen());
 235     }
 236
 237     private static int blue(Color color) {
 238         return toIntColor(color.getBlue());
 239     }
 240
 241     private static int alpha(Color color) {
 242         return toIntColor(color.getOpacity());
 243     }
 244
 245     private static int toIntColor(double c) {
 246         return new Double(c * 255).intValue();
 247     }
 248
 249     private static class ShortColorFormatterDelegate extends AbstractColorFormatterDelegate {
 250         private ShortColorFormatterDelegate() {
 251             super(PATTERN_SHORT);
 252         }
 253
 254         @Nonnull
 255         public String format(@Nonnull Color color) {
 256             requireNonNull(color, "Cannot format given Color because it's null");
 257
 258             return new StringBuilder("#")
 259                 .append(toHexString(red(color)).charAt(0))
 260                 .append(toHexString(green(color)).charAt(0))
 261                 .append(toHexString(blue(color)).charAt(0))
 262                 .toString();
 263         }
 264
 265         @Nonnull
 266         public Color parse(@Nonnull String str) throws ParseException {
 267             if (!str.startsWith("#") || str.length() != 4) {
 268                 throw parseError(str, Color.class);
 269             }
 270
 271             int r = parseHexInt(new StringBuilder()
 272                 .append(str.charAt(1))
 273                 .append(str.charAt(1))
 274                 .toString().toUpperCase(), Color.class);
 275             int g = parseHexInt(new StringBuilder()
 276                 .append(str.charAt(2))
 277                 .append(str.charAt(2))
 278                 .toString().toUpperCase(), Color.class);
 279             int b = parseHexInt(new StringBuilder()
 280                 .append(str.charAt(3))
 281                 .append(str.charAt(3))
 282                 .toString().toUpperCase(), Color.class);
 283
 284             return Color.rgb(r, g, b);
 285         }
 286     }
 287
 288     private static class ShortWithAlphaColorFormatterDelegate extends AbstractColorFormatterDelegate {
 289         private ShortWithAlphaColorFormatterDelegate() {
 290             super(PATTERN_SHORT_WITH_ALPHA);
 291         }
 292
 293         @Nonnull
 294         public String format(@Nonnull Color color) {
 295             requireNonNull(color, "Cannot format given Color because it's null");
 296
 297             return new StringBuilder("#")
 298                 .append(toHexString(red(color)).charAt(0))
 299                 .append(toHexString(green(color)).charAt(0))
 300                 .append(toHexString(blue(color)).charAt(0))
 301                 .append(toHexString(alpha(color)).charAt(0))
 302                 .toString();
 303         }
 304
 305         @Nonnull
 306         public Color parse(@Nonnull String str) throws ParseException {
 307             if (!str.startsWith("#") || str.length() != 5) {
 308                 throw parseError(str, Color.class);
 309             }
 310
 311             int r = parseHexInt(new StringBuilder()
 312                 .append(str.charAt(1))
 313                 .append(str.charAt(1))
 314                 .toString().toUpperCase(), Color.class);
 315             int g = parseHexInt(new StringBuilder()
 316                 .append(str.charAt(2))
 317                 .append(str.charAt(2))
 318                 .toString().toUpperCase(), Color.class);
 319             int b = parseHexInt(new StringBuilder()
 320                 .append(str.charAt(3))
 321                 .append(str.charAt(3))
 322                 .toString().toUpperCase(), Color.class);
 323             int a = parseHexInt(new StringBuilder()
 324                 .append(str.charAt(4))
 325                 .append(str.charAt(4))
 326                 .toString().toUpperCase(), Color.class);
 327
 328             return Color.rgb(r, g, b, a / 255d);
 329         }
 330     }
 331
 332     private static class LongColorFormatterDelegate extends AbstractColorFormatterDelegate {
 333         private LongColorFormatterDelegate() {
 334             super(PATTERN_LONG);
 335         }
 336
 337         @Nonnull
 338         public String format(@Nonnull Color color) {
 339             requireNonNull(color, "Cannot format given Color because it's null");
 340
 341             return new StringBuilder("#")
 342                 .append(padLeft(toHexString(red(color)), "0"))
 343                 .append(padLeft(toHexString(green(color)), "0"))
 344                 .append(padLeft(toHexString(blue(color)), "0"))
 345                 .toString();
 346         }
 347
 348         @Nonnull
 349         public Color parse(@Nonnull String str) throws ParseException {
 350             if (!str.startsWith("#") || str.length() != 7) {
 351                 throw parseError(str, Color.class);
 352             }
 353
 354             int r = parseHexInt(new StringBuilder()
 355                 .append(str.charAt(1))
 356                 .append(str.charAt(2))
 357                 .toString().toUpperCase(), Color.class);
 358             int g = parseHexInt(new StringBuilder()
 359                 .append(str.charAt(3))
 360                 .append(str.charAt(4))
 361                 .toString().toUpperCase(), Color.class);
 362             int b = parseHexInt(new StringBuilder()
 363                 .append(str.charAt(5))
 364                 .append(str.charAt(6))
 365                 .toString().toUpperCase(), Color.class);
 366
 367             return Color.rgb(r, g, b);
 368         }
 369     }
 370
 371     private static class LongWithAlphaColorFormatterDelegate extends AbstractColorFormatterDelegate {
 372         private LongWithAlphaColorFormatterDelegate() {
 373             super(PATTERN_LONG_WITH_ALPHA);
 374         }
 375
 376         @Nonnull
 377         public String format(@Nonnull Color color) {
 378             requireNonNull(color, "Cannot format given Color because it's null");
 379
 380             return new StringBuilder("#")
 381                 .append(padLeft(toHexString(red(color)), "0"))
 382                 .append(padLeft(toHexString(green(color)), "0"))
 383                 .append(padLeft(toHexString(blue(color)), "0"))
 384                 .append(padLeft(toHexString(alpha(color)), "0"))
 385                 .toString();
 386         }
 387
 388         @Nonnull
 389         public Color parse(@Nonnull String str) throws ParseException {
 390             if (!str.startsWith("#") || str.length() != 9) {
 391                 throw parseError(str, Color.class);
 392             }
 393
 394             int r = parseHexInt(new StringBuilder()
 395                 .append(str.charAt(1))
 396                 .append(str.charAt(2))
 397                 .toString().toUpperCase(), Color.class);
 398             int g = parseHexInt(new StringBuilder()
 399                 .append(str.charAt(3))
 400                 .append(str.charAt(4))
 401                 .toString().toUpperCase(), Color.class);
 402             int b = parseHexInt(new StringBuilder()
 403                 .append(str.charAt(5))
 404                 .append(str.charAt(6))
 405                 .toString().toUpperCase(), Color.class);
 406             int a = parseHexInt(new StringBuilder()
 407                 .append(str.charAt(7))
 408                 .append(str.charAt(8))
 409                 .toString().toUpperCase(), Color.class);
 410
 411             return Color.rgb(r, g, b, a / 255d);
 412         }
 413     }
 414
 415     private static String padLeft(String self, String padding) {
 416         return 2 <= self.length() ? self : padding + self;
 417     }
 418 }
 |