001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.utils;
021
022import java.text.ParseException;
023import java.text.SimpleDateFormat;
024import java.util.Collection;
025import java.util.Date;
026import java.util.LinkedHashMap;
027import java.util.Map;
028import javax.annotation.CheckForNull;
029import javax.annotation.Nullable;
030import org.apache.commons.lang.StringUtils;
031import org.apache.commons.lang.math.NumberUtils;
032import org.sonar.api.rules.RulePriority;
033
034/**
035 * <p>Formats and parses key/value pairs with the text representation : "key1=value1;key2=value2". Field typing
036 * is supported, to make conversion from/to primitive types easier for example.
037 * <br>
038 * Since version 4.5.1, text keys and values are escaped if they contain the separator characters '=' or ';'.
039 * <br>
040 * <b>Parsing examples</b>
041 * <pre>
042 *   Map&lt;String,String&gt; mapOfStrings = KeyValueFormat.parse("hello=world;foo=bar");
043 *   Map&lt;String,Integer&gt; mapOfStringInts = KeyValueFormat.parseStringInt("one=1;two=2");
044 *   Map&lt;Integer,String&gt; mapOfIntStrings = KeyValueFormat.parseIntString("1=one;2=two");
045 *   Map&lt;String,Date&gt; mapOfStringDates = KeyValueFormat.parseStringDate("d1=2014-01-14;d2=2015-07-28");
046 *
047 *   // custom conversion
048 *   Map&lt;String,MyClass&gt; mapOfStringMyClass = KeyValueFormat.parse("foo=xxx;bar=yyy",
049 *     KeyValueFormat.newStringConverter(), new MyClassConverter());
050 * </pre>
051 * <br>
052 * <b>Formatting examples</b>
053 * <pre>
054 *   String output = KeyValueFormat.format(map);
055 *
056 *   Map&lt;Integer,String&gt; mapIntString;
057 *   KeyValueFormat.formatIntString(mapIntString);
058 * </pre>
059 * @since 1.10
060 */
061public final class KeyValueFormat {
062  public static final String PAIR_SEPARATOR = ";";
063  public static final String FIELD_SEPARATOR = "=";
064
065  private KeyValueFormat() {
066    // only static methods
067  }
068
069  private static class FieldParserContext {
070    private final StringBuilder result = new StringBuilder();
071    private boolean escaped = false;
072    private char firstChar;
073    private char previous = (char) -1;
074  }
075
076  static class FieldParser {
077    private static final char DOUBLE_QUOTE = '"';
078    private final String csv;
079    private int position = 0;
080
081    FieldParser(String csv) {
082      this.csv = csv;
083    }
084
085    @CheckForNull
086    String nextKey() {
087      return next('=');
088    }
089
090    @CheckForNull
091    String nextVal() {
092      return next(';');
093    }
094
095    @CheckForNull
096    private String next(char separator) {
097      if (position >= csv.length()) {
098        return null;
099      }
100      FieldParserContext context = new FieldParserContext();
101      context.firstChar = csv.charAt(position);
102      // check if value is escaped by analyzing first character
103      checkEscaped(context);
104
105      boolean isEnd = false;
106      while (position < csv.length() && !isEnd) {
107        isEnd = advance(separator, context);
108      }
109      return context.result.toString();
110    }
111
112    private boolean advance(char separator, FieldParserContext context) {
113      boolean end = false;
114      char c = csv.charAt(position);
115      if (c == separator && !context.escaped) {
116        end = true;
117        position++;
118      } else if (c == '\\' && context.escaped && position < csv.length() + 1 && csv.charAt(position + 1) == DOUBLE_QUOTE) {
119        // on a backslash that escapes double-quotes -> keep double-quotes and jump after
120        context.previous = DOUBLE_QUOTE;
121        context.result.append(context.previous);
122        position += 2;
123      } else if (c == '"' && context.escaped && context.previous != '\\') {
124        // on unescaped double-quotes -> end of escaping.
125        // assume that next character is a separator (= or ;). This could be
126        // improved to enforce check.
127        end = true;
128        position += 2;
129      } else {
130        context.result.append(c);
131        context.previous = c;
132        position++;
133      }
134      return end;
135    }
136
137    private void checkEscaped(FieldParserContext context) {
138      if (context.firstChar == DOUBLE_QUOTE) {
139        context.escaped = true;
140        position++;
141        context.previous = context.firstChar;
142      }
143    }
144  }
145
146  public abstract static class Converter<T> {
147    abstract String format(@Nullable T type);
148
149    @CheckForNull
150    abstract T parse(String s);
151
152
153    String escape(String s) {
154      if (s.contains(FIELD_SEPARATOR) || s.contains(PAIR_SEPARATOR)) {
155        return new StringBuilder()
156          .append(FieldParser.DOUBLE_QUOTE)
157          .append(s.replace("\"", "\\\""))
158          .append(FieldParser.DOUBLE_QUOTE).toString();
159      }
160      return s;
161    }
162  }
163
164  public static final class StringConverter extends Converter<String> {
165    private static final StringConverter INSTANCE = new StringConverter();
166
167    private StringConverter() {
168    }
169
170    @Override
171    String format(@Nullable String s) {
172      return s == null ? "" : escape(s);
173    }
174
175    @Override
176    String parse(String s) {
177      return s;
178    }
179  }
180
181  public static StringConverter newStringConverter() {
182    return StringConverter.INSTANCE;
183  }
184
185  public static final class ToStringConverter extends Converter<Object> {
186    private static final ToStringConverter INSTANCE = new ToStringConverter();
187
188    private ToStringConverter() {
189    }
190
191    @Override
192    String format(@Nullable Object o) {
193      return o == null ? "" : escape(o.toString());
194    }
195
196    @Override
197    String parse(String s) {
198      throw new UnsupportedOperationException("Can not parse with ToStringConverter: " + s);
199    }
200  }
201
202  public static ToStringConverter newToStringConverter() {
203    return ToStringConverter.INSTANCE;
204  }
205
206  public static final class IntegerConverter extends Converter<Integer> {
207    private static final IntegerConverter INSTANCE = new IntegerConverter();
208
209    private IntegerConverter() {
210    }
211
212    @Override
213    String format(@Nullable Integer s) {
214      return s == null ? "" : String.valueOf(s);
215    }
216
217    @Override
218    Integer parse(String s) {
219      return StringUtils.isBlank(s) ? null : NumberUtils.toInt(s);
220    }
221  }
222
223  public static IntegerConverter newIntegerConverter() {
224    return IntegerConverter.INSTANCE;
225  }
226
227  public static final class PriorityConverter extends Converter<RulePriority> {
228    private static final PriorityConverter INSTANCE = new PriorityConverter();
229
230    private PriorityConverter() {
231    }
232
233    @Override
234    String format(@Nullable RulePriority s) {
235      return s == null ? "" : s.toString();
236    }
237
238    @Override
239    RulePriority parse(String s) {
240      return StringUtils.isBlank(s) ? null : RulePriority.valueOf(s);
241    }
242  }
243
244  public static PriorityConverter newPriorityConverter() {
245    return PriorityConverter.INSTANCE;
246  }
247
248  public static final class DoubleConverter extends Converter<Double> {
249    private static final DoubleConverter INSTANCE = new DoubleConverter();
250
251    private DoubleConverter() {
252    }
253
254    @Override
255    String format(@Nullable Double d) {
256      return d == null ? "" : String.valueOf(d);
257    }
258
259    @Override
260    Double parse(String s) {
261      return StringUtils.isBlank(s) ? null : NumberUtils.toDouble(s);
262    }
263  }
264
265  public static DoubleConverter newDoubleConverter() {
266    return DoubleConverter.INSTANCE;
267  }
268
269  public static class DateConverter extends Converter<Date> {
270    private SimpleDateFormat dateFormat;
271
272    private DateConverter(String format) {
273      this.dateFormat = new SimpleDateFormat(format);
274    }
275
276    @Override
277    String format(@Nullable Date d) {
278      return d == null ? "" : dateFormat.format(d);
279    }
280
281    @Override
282    Date parse(String s) {
283      try {
284        return StringUtils.isBlank(s) ? null : dateFormat.parse(s);
285      } catch (ParseException e) {
286        throw new IllegalArgumentException("Not a date with format: " + dateFormat.toPattern(), e);
287      }
288    }
289  }
290
291  public static DateConverter newDateConverter() {
292    return newDateConverter(DateUtils.DATE_FORMAT);
293  }
294
295  public static DateConverter newDateTimeConverter() {
296    return newDateConverter(DateUtils.DATETIME_FORMAT);
297  }
298
299  public static DateConverter newDateConverter(String format) {
300    return new DateConverter(format);
301  }
302
303  /**
304   * If input is null, then an empty map is returned.
305   */
306  public static <K, V> Map<K, V> parse(@Nullable String input, Converter<K> keyConverter, Converter<V> valueConverter) {
307    Map<K, V> map = new LinkedHashMap<>();
308    if (input != null) {
309      FieldParser reader = new FieldParser(input);
310      boolean end = false;
311      while (!end) {
312        String key = reader.nextKey();
313        if (key == null) {
314          end = true;
315        } else {
316          String val = StringUtils.defaultString(reader.nextVal(), "");
317          map.put(keyConverter.parse(key), valueConverter.parse(val));
318        }
319      }
320    }
321    return map;
322  }
323
324  public static Map<String, String> parse(@Nullable String data) {
325    return parse(data, newStringConverter(), newStringConverter());
326  }
327
328  /**
329   * @since 2.7
330   */
331  public static Map<String, Integer> parseStringInt(@Nullable String data) {
332    return parse(data, newStringConverter(), newIntegerConverter());
333  }
334
335  /**
336   * @since 2.7
337   */
338  public static Map<String, Double> parseStringDouble(@Nullable String data) {
339    return parse(data, newStringConverter(), newDoubleConverter());
340  }
341
342  /**
343   * @since 2.7
344   */
345  public static Map<Integer, String> parseIntString(@Nullable String data) {
346    return parse(data, newIntegerConverter(), newStringConverter());
347  }
348
349  /**
350   * @since 2.7
351   */
352  public static Map<Integer, Double> parseIntDouble(@Nullable String data) {
353    return parse(data, newIntegerConverter(), newDoubleConverter());
354  }
355
356  /**
357   * @since 2.7
358   */
359  public static Map<Integer, Date> parseIntDate(@Nullable String data) {
360    return parse(data, newIntegerConverter(), newDateConverter());
361  }
362
363  /**
364   * @since 2.7
365   */
366  public static Map<Integer, Integer> parseIntInt(@Nullable String data) {
367    return parse(data, newIntegerConverter(), newIntegerConverter());
368  }
369
370  /**
371   * @since 2.7
372   */
373  public static Map<Integer, Date> parseIntDateTime(@Nullable String data) {
374    return parse(data, newIntegerConverter(), newDateTimeConverter());
375  }
376
377  private static <K, V> String formatEntries(Collection<Map.Entry<K, V>> entries, Converter<K> keyConverter, Converter<V> valueConverter) {
378    StringBuilder sb = new StringBuilder();
379    boolean first = true;
380    for (Map.Entry<K, V> entry : entries) {
381      if (!first) {
382        sb.append(PAIR_SEPARATOR);
383      }
384      sb.append(keyConverter.format(entry.getKey()));
385      sb.append(FIELD_SEPARATOR);
386      if (entry.getValue() != null) {
387        sb.append(valueConverter.format(entry.getValue()));
388      }
389      first = false;
390    }
391    return sb.toString();
392  }
393
394  /**
395   * @since 2.7
396   */
397  public static <K, V> String format(Map<K, V> map, Converter<K> keyConverter, Converter<V> valueConverter) {
398    return formatEntries(map.entrySet(), keyConverter, valueConverter);
399  }
400
401  /**
402   * @since 2.7
403   */
404  public static String format(Map map) {
405    return format(map, newToStringConverter(), newToStringConverter());
406  }
407
408  /**
409   * @since 2.7
410   */
411  public static String formatIntString(Map<Integer, String> map) {
412    return format(map, newIntegerConverter(), newStringConverter());
413  }
414
415  /**
416   * @since 2.7
417   */
418  public static String formatIntDouble(Map<Integer, Double> map) {
419    return format(map, newIntegerConverter(), newDoubleConverter());
420  }
421
422  /**
423   * @since 2.7
424   */
425  public static String formatIntDate(Map<Integer, Date> map) {
426    return format(map, newIntegerConverter(), newDateConverter());
427  }
428
429  /**
430   * @since 2.7
431   */
432  public static String formatIntDateTime(Map<Integer, Date> map) {
433    return format(map, newIntegerConverter(), newDateTimeConverter());
434  }
435
436  /**
437   * @since 2.7
438   */
439  public static String formatStringInt(Map<String, Integer> map) {
440    return format(map, newStringConverter(), newIntegerConverter());
441  }
442
443}