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