org.sonar.api.utils.KeyValueFormat Maven / Gradle / Ivy
The newest version!
/*
* SonarQube
* Copyright (C) 2009-2022 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.api.utils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.sonar.api.rules.RulePriority;
/**
* Formats and parses key/value pairs with the text representation : "key1=value1;key2=value2". Field typing
* is supported, to make conversion from/to primitive types easier for example.
*
* Since version 4.5.1, text keys and values are escaped if they contain the separator characters '=' or ';'.
*
* Parsing examples
*
* Map<String,String> mapOfStrings = KeyValueFormat.parse("hello=world;foo=bar");
* Map<String,Integer> mapOfStringInts = KeyValueFormat.parseStringInt("one=1;two=2");
* Map<Integer,String> mapOfIntStrings = KeyValueFormat.parseIntString("1=one;2=two");
* Map<String,Date> mapOfStringDates = KeyValueFormat.parseStringDate("d1=2014-01-14;d2=2015-07-28");
*
* // custom conversion
* Map<String,MyClass> mapOfStringMyClass = KeyValueFormat.parse("foo=xxx;bar=yyy",
* KeyValueFormat.newStringConverter(), new MyClassConverter());
*
*
* Formatting examples
*
* String output = KeyValueFormat.format(map);
*
* Map<Integer,String> mapIntString;
* KeyValueFormat.formatIntString(mapIntString);
*
* @since 1.10
*/
public final class KeyValueFormat {
public static final String PAIR_SEPARATOR = ";";
public static final String FIELD_SEPARATOR = "=";
private KeyValueFormat() {
// only static methods
}
private static class FieldParserContext {
private final StringBuilder result = new StringBuilder();
private boolean escaped = false;
private char firstChar;
private char previous = (char) -1;
}
static class FieldParser {
private static final char DOUBLE_QUOTE = '"';
private final String csv;
private int position = 0;
FieldParser(String csv) {
this.csv = csv;
}
@CheckForNull
String nextKey() {
return next('=');
}
@CheckForNull
String nextVal() {
return next(';');
}
@CheckForNull
private String next(char separator) {
if (position >= csv.length()) {
return null;
}
FieldParserContext context = new FieldParserContext();
context.firstChar = csv.charAt(position);
// check if value is escaped by analyzing first character
checkEscaped(context);
boolean isEnd = false;
while (position < csv.length() && !isEnd) {
isEnd = advance(separator, context);
}
return context.result.toString();
}
private boolean advance(char separator, FieldParserContext context) {
boolean end = false;
char c = csv.charAt(position);
if (c == separator && !context.escaped) {
end = true;
position++;
} else if (c == '\\' && context.escaped && position < csv.length() + 1 && csv.charAt(position + 1) == DOUBLE_QUOTE) {
// on a backslash that escapes double-quotes -> keep double-quotes and jump after
context.previous = DOUBLE_QUOTE;
context.result.append(context.previous);
position += 2;
} else if (c == '"' && context.escaped && context.previous != '\\') {
// on unescaped double-quotes -> end of escaping.
// assume that next character is a separator (= or ;). This could be
// improved to enforce check.
end = true;
position += 2;
} else {
context.result.append(c);
context.previous = c;
position++;
}
return end;
}
private void checkEscaped(FieldParserContext context) {
if (context.firstChar == DOUBLE_QUOTE) {
context.escaped = true;
position++;
context.previous = context.firstChar;
}
}
}
public abstract static class Converter {
abstract String format(@Nullable T type);
@CheckForNull
abstract T parse(String s);
String escape(String s) {
if (s.contains(FIELD_SEPARATOR) || s.contains(PAIR_SEPARATOR)) {
return new StringBuilder()
.append(FieldParser.DOUBLE_QUOTE)
.append(s.replace("\"", "\\\""))
.append(FieldParser.DOUBLE_QUOTE).toString();
}
return s;
}
}
public static final class StringConverter extends Converter {
private static final StringConverter INSTANCE = new StringConverter();
private StringConverter() {
}
@Override
String format(@Nullable String s) {
return s == null ? "" : escape(s);
}
@Override
String parse(String s) {
return s;
}
}
public static StringConverter newStringConverter() {
return StringConverter.INSTANCE;
}
public static final class ToStringConverter extends Converter