001/*
002 * SonarQube
003 * Copyright (C) 2009-2016 SonarSource SA
004 * mailto:contact 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(T type);
148
149    @CheckForNull
150    abstract T parse(String s);
151
152    String escape(String s) {
153      if (s.contains(FIELD_SEPARATOR) || s.contains(PAIR_SEPARATOR)) {
154        return new StringBuilder()
155          .append(FieldParser.DOUBLE_QUOTE)
156          .append(s.replace("\"", "\\\""))
157          .append(FieldParser.DOUBLE_QUOTE).toString();
158      }
159      return s;
160    }
161  }
162
163  public static final class StringConverter extends Converter<String> {
164    private static final StringConverter INSTANCE = new StringConverter();
165
166    private StringConverter() {
167    }
168
169    @Override
170    String format(String s) {
171      return escape(s);
172    }
173
174    @Override
175    String parse(String s) {
176      return s;
177    }
178  }
179
180  public static StringConverter newStringConverter() {
181    return StringConverter.INSTANCE;
182  }
183
184  public static final class ToStringConverter extends Converter<Object> {
185    private static final ToStringConverter INSTANCE = new ToStringConverter();
186
187    private ToStringConverter() {
188    }
189
190    @Override
191    String format(Object o) {
192      return escape(o.toString());
193    }
194
195    @Override
196    String parse(String s) {
197      throw new UnsupportedOperationException("Can not parse with ToStringConverter: " + s);
198    }
199  }
200
201  public static ToStringConverter newToStringConverter() {
202    return ToStringConverter.INSTANCE;
203  }
204
205  public static final class IntegerConverter extends Converter<Integer> {
206    private static final IntegerConverter INSTANCE = new IntegerConverter();
207
208    private IntegerConverter() {
209    }
210
211    @Override
212    String format(Integer s) {
213      return s == null ? "" : String.valueOf(s);
214    }
215
216    @Override
217    Integer parse(String s) {
218      return StringUtils.isBlank(s) ? null : NumberUtils.toInt(s);
219    }
220  }
221
222  public static IntegerConverter newIntegerConverter() {
223    return IntegerConverter.INSTANCE;
224  }
225
226  public static final class PriorityConverter extends Converter<RulePriority> {
227    private static final PriorityConverter INSTANCE = new PriorityConverter();
228
229    private PriorityConverter() {
230    }
231
232    @Override
233    String format(RulePriority s) {
234      return s == null ? "" : s.toString();
235    }
236
237    @Override
238    RulePriority parse(String s) {
239      return StringUtils.isBlank(s) ? null : RulePriority.valueOf(s);
240    }
241  }
242
243  public static PriorityConverter newPriorityConverter() {
244    return PriorityConverter.INSTANCE;
245  }
246
247  public static final class DoubleConverter extends Converter<Double> {
248    private static final DoubleConverter INSTANCE = new DoubleConverter();
249
250    private DoubleConverter() {
251    }
252
253    @Override
254    String format(Double d) {
255      return d == null ? "" : String.valueOf(d);
256    }
257
258    @Override
259    Double parse(String s) {
260      return StringUtils.isBlank(s) ? null : NumberUtils.toDouble(s);
261    }
262  }
263
264  public static DoubleConverter newDoubleConverter() {
265    return DoubleConverter.INSTANCE;
266  }
267
268  public static class DateConverter extends Converter<Date> {
269    private SimpleDateFormat dateFormat;
270
271    private DateConverter(String format) {
272      this.dateFormat = new SimpleDateFormat(format);
273    }
274
275    @Override
276    String format(Date d) {
277      return d == null ? "" : dateFormat.format(d);
278    }
279
280    @Override
281    Date parse(String s) {
282      try {
283        return StringUtils.isBlank(s) ? null : dateFormat.parse(s);
284      } catch (ParseException e) {
285        throw new IllegalArgumentException("Not a date with format: " + dateFormat.toPattern(), e);
286      }
287    }
288  }
289
290  public static DateConverter newDateConverter() {
291    return newDateConverter(DateUtils.DATE_FORMAT);
292  }
293
294  public static DateConverter newDateTimeConverter() {
295    return newDateConverter(DateUtils.DATETIME_FORMAT);
296  }
297
298  public static DateConverter newDateConverter(String format) {
299    return new DateConverter(format);
300  }
301
302  /**
303   * If input is null, then an empty map is returned.
304   */
305  public static <K, V> Map<K, V> parse(@Nullable String input, Converter<K> keyConverter, Converter<V> valueConverter) {
306    Map<K, V> map = new LinkedHashMap<>();
307    if (input != null) {
308      FieldParser reader = new FieldParser(input);
309      boolean end = false;
310      while (!end) {
311        String key = reader.nextKey();
312        if (key == null) {
313          end = true;
314        } else {
315          String val = StringUtils.defaultString(reader.nextVal(), "");
316          map.put(keyConverter.parse(key), valueConverter.parse(val));
317        }
318      }
319    }
320    return map;
321  }
322
323  public static Map<String, String> parse(@Nullable String data) {
324    return parse(data, newStringConverter(), newStringConverter());
325  }
326
327  /**
328   * @since 2.7
329   */
330  public static Map<String, Integer> parseStringInt(@Nullable String data) {
331    return parse(data, newStringConverter(), newIntegerConverter());
332  }
333
334  /**
335   * @since 2.7
336   */
337  public static Map<String, Double> parseStringDouble(@Nullable String data) {
338    return parse(data, newStringConverter(), newDoubleConverter());
339  }
340
341  /**
342   * @since 2.7
343   */
344  public static Map<Integer, String> parseIntString(@Nullable String data) {
345    return parse(data, newIntegerConverter(), newStringConverter());
346  }
347
348  /**
349   * @since 2.7
350   */
351  public static Map<Integer, Double> parseIntDouble(@Nullable String data) {
352    return parse(data, newIntegerConverter(), newDoubleConverter());
353  }
354
355  /**
356   * @since 2.7
357   */
358  public static Map<Integer, Date> parseIntDate(@Nullable String data) {
359    return parse(data, newIntegerConverter(), newDateConverter());
360  }
361
362  /**
363   * @since 2.7
364   */
365  public static Map<Integer, Integer> parseIntInt(@Nullable String data) {
366    return parse(data, newIntegerConverter(), newIntegerConverter());
367  }
368
369  /**
370   * @since 2.7
371   */
372  public static Map<Integer, Date> parseIntDateTime(@Nullable String data) {
373    return parse(data, newIntegerConverter(), newDateTimeConverter());
374  }
375
376  private static <K, V> String formatEntries(Collection<Map.Entry<K, V>> entries, Converter<K> keyConverter, Converter<V> valueConverter) {
377    StringBuilder sb = new StringBuilder();
378    boolean first = true;
379    for (Map.Entry<K, V> entry : entries) {
380      if (!first) {
381        sb.append(PAIR_SEPARATOR);
382      }
383      sb.append(keyConverter.format(entry.getKey()));
384      sb.append(FIELD_SEPARATOR);
385      if (entry.getValue() != null) {
386        sb.append(valueConverter.format(entry.getValue()));
387      }
388      first = false;
389    }
390    return sb.toString();
391  }
392
393  /**
394   * @since 2.7
395   */
396  public static <K, V> String format(Map<K, V> map, Converter<K> keyConverter, Converter<V> valueConverter) {
397    return formatEntries(map.entrySet(), keyConverter, valueConverter);
398  }
399
400  /**
401   * @since 2.7
402   */
403  public static String format(Map map) {
404    return format(map, newToStringConverter(), newToStringConverter());
405  }
406
407  /**
408   * @since 2.7
409   */
410  public static String formatIntString(Map<Integer, String> map) {
411    return format(map, newIntegerConverter(), newStringConverter());
412  }
413
414  /**
415   * @since 2.7
416   */
417  public static String formatIntDouble(Map<Integer, Double> map) {
418    return format(map, newIntegerConverter(), newDoubleConverter());
419  }
420
421  /**
422   * @since 2.7
423   */
424  public static String formatIntDate(Map<Integer, Date> map) {
425    return format(map, newIntegerConverter(), newDateConverter());
426  }
427
428  /**
429   * @since 2.7
430   */
431  public static String formatIntDateTime(Map<Integer, Date> map) {
432    return format(map, newIntegerConverter(), newDateTimeConverter());
433  }
434
435  /**
436   * @since 2.7
437   */
438  public static String formatStringInt(Map<String, Integer> map) {
439    return format(map, newStringConverter(), newIntegerConverter());
440  }
441
442}