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