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<String,String> mapOfStrings = KeyValueFormat.parse("hello=world;foo=bar");
048 * Map<String,Integer> mapOfStringInts = KeyValueFormat.parseStringInt("one=1;two=2");
049 * Map<Integer,String> mapOfIntStrings = KeyValueFormat.parseIntString("1=one;2=two");
050 * Map<String,Date> mapOfStringDates = KeyValueFormat.parseStringDate("d1=2014-01-14;d2=2015-07-28");
051 *
052 * // custom conversion
053 * Map<String,MyClass> 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<Integer,String> 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 }