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