ColorFormatter.java
001 /*
002  * Copyright 2008-2017 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     private static final String ERROR_COLOR_NULL = "Cannot format given Color because it's null";
110     private static final String HASH = "#";
111     private static final String ZERO = "0";
112 
113     /**
114      <p>Returns a {@code ColorFormatter} given a color pattern.</p>
115      *
116      @param pattern the input pattern. Must be one of the 4 supported color patterns.
117      *
118      @return a {@code ColorPattern} instance
119      *
120      @throws IllegalArgumentException if the supplied {@code pattern} is not supported
121      */
122     @Nonnull
123     public static ColorFormatter getInstance(@Nonnull String pattern) {
124         return new ColorFormatter(pattern);
125     }
126 
127     private final ColorFormatterDelegate delegate;
128 
129     protected ColorFormatter(@Nullable String pattern) {
130         if (PATTERN_SHORT.equals(pattern)) {
131             delegate = new ShortColorFormatterDelegate();
132         else if (PATTERN_SHORT_WITH_ALPHA.equals(pattern)) {
133             delegate = new ShortWithAlphaColorFormatterDelegate();
134         else if (PATTERN_LONG.equals(pattern)) {
135             delegate = new LongColorFormatterDelegate();
136         else if (PATTERN_LONG_WITH_ALPHA.equals(pattern)) {
137             delegate = new LongWithAlphaColorFormatterDelegate();
138         else if (isBlank(pattern)) {
139             delegate = new LongColorFormatterDelegate();
140         else {
141             throw new IllegalArgumentException("Invalid pattern '" + pattern + "'. Valid patterns are " + Arrays.toString(PATTERNS));
142         }
143     }
144 
145     @Override
146     @Nullable
147     public String format(@Nullable Color color) {
148         return color == null null : delegate.format(color);
149     }
150 
151     @Override
152     @Nullable
153     public Color parse(@Nullable String strthrows ParseException {
154         return isBlank(strnull : delegate.parse(str);
155     }
156 
157     /**
158      * Returns the pattern used by this {@code ColorFormatter}
159      *
160      @return the pattern this {@code ColorFormatter} uses for parsing/formatting.
161      */
162     @Nonnull
163     public String getPattern() {
164         return delegate.getPattern();
165     }
166 
167     /**
168      <p>Parses a string into a {@code javafx.scene.paint.Color} instance.</p>
169      <p>The parsing pattern is chosen given the length of the input string
170      <ul>
171      <li>4 - {@code #RGB}</li>
172      <li>5 - {@code #RGBA}</li>
173      <li>7 - {@code #RRGGBB}</li>
174      <li>9 - {@code #RRGGBBAA}</li>
175      </ul>
176      </p>
177      * The input string may also be any of the Color constants identified by
178      * {@code javafx.scene.paint.Color}.
179      *
180      @param str the string representation of a {@code javafx.scene.paint.Color}
181      *
182      @return a {@code javafx.scene.paint.Color} instance matching the supplied RGBA color components
183      *
184      @throws ParseException if the string cannot be parsed by the chosen pattern
185      @see javafx.scene.paint.Color
186      */
187     @Nonnull
188     @SuppressWarnings("ConstantConditions")
189     public static Color parseColor(@Nonnull String strthrows ParseException {
190         if (str.startsWith(HASH)) {
191             switch (str.length()) {
192                 case 4:
193                     return SHORT.parse(str);
194                 case 5:
195                     return SHORT_WITH_ALPHA.parse(str);
196                 case 7:
197                     return LONG.parse(str);
198                 case 9:
199                     return LONG_WITH_ALPHA.parse(str);
200                 default:
201                     throw parseError(str, Color.class);
202             }
203         else {
204             // assume it's a Color constant
205             try {
206                 String colorFieldName = str.toUpperCase();
207                 Field field = Color.class.getField(colorFieldName);
208                 return (Colorfield.get(null);
209             catch (Exception e) {
210                 throw parseError(str, Color.class, e);
211             }
212         }
213     }
214 
215     private static interface ColorFormatterDelegate {
216         @Nonnull
217         String getPattern();
218 
219         @Nonnull
220         String format(@Nonnull Color color);
221 
222         @Nonnull
223         Color parse(@Nonnull String strthrows ParseException;
224     }
225 
226     private abstract static class AbstractColorFormatterDelegate implements ColorFormatterDelegate {
227         private final String pattern;
228 
229         private AbstractColorFormatterDelegate(@Nonnull String pattern) {
230             this.pattern = pattern;
231         }
232 
233         @Override
234         @Nonnull
235         public String getPattern() {
236             return pattern;
237         }
238     }
239 
240     private static int red(Color color) {
241         return toIntColor(color.getRed());
242     }
243 
244     private static int green(Color color) {
245         return toIntColor(color.getGreen());
246     }
247 
248     private static int blue(Color color) {
249         return toIntColor(color.getBlue());
250     }
251 
252     private static int alpha(Color color) {
253         return toIntColor(color.getOpacity());
254     }
255 
256     private static int toIntColor(double c) {
257         return (int) (c * 255);
258     }
259 
260     private static class ShortColorFormatterDelegate extends AbstractColorFormatterDelegate {
261         private ShortColorFormatterDelegate() {
262             super(PATTERN_SHORT);
263         }
264 
265         @Override
266         @Nonnull
267         public String format(@Nonnull Color color) {
268             requireNonNull(color, ERROR_COLOR_NULL);
269 
270             return new StringBuilder(HASH)
271                 .append(toHexString(red(color)).charAt(0))
272                 .append(toHexString(green(color)).charAt(0))
273                 .append(toHexString(blue(color)).charAt(0))
274                 .toString();
275         }
276 
277         @Override
278         @Nonnull
279         public Color parse(@Nonnull String strthrows ParseException {
280             if (!str.startsWith(HASH|| str.length() != 4) {
281                 throw parseError(str, Color.class);
282             }
283 
284             int r = parseHexInt(new StringBuilder()
285                 .append(str.charAt(1))
286                 .append(str.charAt(1))
287                 .toString().toUpperCase(), Color.class);
288             int g = parseHexInt(new StringBuilder()
289                 .append(str.charAt(2))
290                 .append(str.charAt(2))
291                 .toString().toUpperCase(), Color.class);
292             int b = parseHexInt(new StringBuilder()
293                 .append(str.charAt(3))
294                 .append(str.charAt(3))
295                 .toString().toUpperCase(), Color.class);
296 
297             return Color.rgb(r, g, b);
298         }
299     }
300 
301     private static class ShortWithAlphaColorFormatterDelegate extends AbstractColorFormatterDelegate {
302         private ShortWithAlphaColorFormatterDelegate() {
303             super(PATTERN_SHORT_WITH_ALPHA);
304         }
305 
306         @Override
307         @Nonnull
308         public String format(@Nonnull Color color) {
309             requireNonNull(color, ERROR_COLOR_NULL);
310 
311             return new StringBuilder(HASH)
312                 .append(toHexString(red(color)).charAt(0))
313                 .append(toHexString(green(color)).charAt(0))
314                 .append(toHexString(blue(color)).charAt(0))
315                 .append(toHexString(alpha(color)).charAt(0))
316                 .toString();
317         }
318 
319         @Override
320         @Nonnull
321         public Color parse(@Nonnull String strthrows ParseException {
322             if (!str.startsWith(HASH|| str.length() != 5) {
323                 throw parseError(str, Color.class);
324             }
325 
326             int r = parseHexInt(new StringBuilder()
327                 .append(str.charAt(1))
328                 .append(str.charAt(1))
329                 .toString().toUpperCase(), Color.class);
330             int g = parseHexInt(new StringBuilder()
331                 .append(str.charAt(2))
332                 .append(str.charAt(2))
333                 .toString().toUpperCase(), Color.class);
334             int b = parseHexInt(new StringBuilder()
335                 .append(str.charAt(3))
336                 .append(str.charAt(3))
337                 .toString().toUpperCase(), Color.class);
338             int a = parseHexInt(new StringBuilder()
339                 .append(str.charAt(4))
340                 .append(str.charAt(4))
341                 .toString().toUpperCase(), Color.class);
342 
343             return Color.rgb(r, g, b, a / 255d);
344         }
345     }
346 
347     private static class LongColorFormatterDelegate extends AbstractColorFormatterDelegate {
348         private LongColorFormatterDelegate() {
349             super(PATTERN_LONG);
350         }
351 
352         @Override
353         @Nonnull
354         public String format(@Nonnull Color color) {
355             requireNonNull(color, ERROR_COLOR_NULL);
356 
357             return new StringBuilder(HASH)
358                 .append(padLeft(toHexString(red(color)), ZERO))
359                 .append(padLeft(toHexString(green(color)), ZERO))
360                 .append(padLeft(toHexString(blue(color)), ZERO))
361                 .toString();
362         }
363 
364         @Override
365         @Nonnull
366         public Color parse(@Nonnull String strthrows ParseException {
367             if (!str.startsWith(HASH|| str.length() != 7) {
368                 throw parseError(str, Color.class);
369             }
370 
371             int r = parseHexInt(new StringBuilder()
372                 .append(str.charAt(1))
373                 .append(str.charAt(2))
374                 .toString().toUpperCase(), Color.class);
375             int g = parseHexInt(new StringBuilder()
376                 .append(str.charAt(3))
377                 .append(str.charAt(4))
378                 .toString().toUpperCase(), Color.class);
379             int b = parseHexInt(new StringBuilder()
380                 .append(str.charAt(5))
381                 .append(str.charAt(6))
382                 .toString().toUpperCase(), Color.class);
383 
384             return Color.rgb(r, g, b);
385         }
386     }
387 
388     private static class LongWithAlphaColorFormatterDelegate extends AbstractColorFormatterDelegate {
389         private LongWithAlphaColorFormatterDelegate() {
390             super(PATTERN_LONG_WITH_ALPHA);
391         }
392 
393         @Override
394         @Nonnull
395         public String format(@Nonnull Color color) {
396             requireNonNull(color, ERROR_COLOR_NULL);
397 
398             return new StringBuilder(HASH)
399                 .append(padLeft(toHexString(red(color)), ZERO))
400                 .append(padLeft(toHexString(green(color)), ZERO))
401                 .append(padLeft(toHexString(blue(color)), ZERO))
402                 .append(padLeft(toHexString(alpha(color)), ZERO))
403                 .toString();
404         }
405 
406         @Override
407         @Nonnull
408         public Color parse(@Nonnull String strthrows ParseException {
409             if (!str.startsWith(HASH|| str.length() != 9) {
410                 throw parseError(str, Color.class);
411             }
412 
413             int r = parseHexInt(new StringBuilder()
414                 .append(str.charAt(1))
415                 .append(str.charAt(2))
416                 .toString().toUpperCase(), Color.class);
417             int g = parseHexInt(new StringBuilder()
418                 .append(str.charAt(3))
419                 .append(str.charAt(4))
420                 .toString().toUpperCase(), Color.class);
421             int b = parseHexInt(new StringBuilder()
422                 .append(str.charAt(5))
423                 .append(str.charAt(6))
424                 .toString().toUpperCase(), Color.class);
425             int a = parseHexInt(new StringBuilder()
426                 .append(str.charAt(7))
427                 .append(str.charAt(8))
428                 .toString().toUpperCase(), Color.class);
429 
430             return Color.rgb(r, g, b, a / 255d);
431         }
432     }
433 
434     private static String padLeft(String self, String padding) {
435         return <= self.length() ? self : padding + self;
436     }
437 }