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