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 }