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