ColorFormatter.java
001 /*
002  * Copyright 2008-2014 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 strthrows ParseException {
146         return isBlank(strnull : 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 strthrows 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 (Colorfield.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 strthrows 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 strthrows 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 strthrows 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 strthrows 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 strthrows 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 <= self.length() ? self : padding + self;
417     }
418 }