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     */
020    package org.sonar.api.utils;
021    
022    import com.google.common.collect.Maps;
023    import com.google.common.collect.Multiset;
024    import org.apache.commons.collections.Bag;
025    import org.apache.commons.lang.StringUtils;
026    import org.apache.commons.lang.math.NumberUtils;
027    import org.sonar.api.rules.RulePriority;
028    
029    import javax.annotation.CheckForNull;
030    import javax.annotation.Nullable;
031    
032    import java.text.ParseException;
033    import java.text.SimpleDateFormat;
034    import java.util.Collection;
035    import java.util.Date;
036    import java.util.Map;
037    import 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     */
066    public 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    }