All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.api.utils.KeyValueFormat Maven / Gradle / Ivy

There is a newer version: 5.1
Show newest version
/*
 * SonarQube, open source software quality management tool.
 * Copyright (C) 2008-2014 SonarSource
 * mailto:contact AT sonarsource DOT com
 *
 * SonarQube 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.
 *
 * SonarQube 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 com.google.common.collect.Maps;
import com.google.common.collect.Multiset;
import org.apache.commons.collections.Bag;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.sonar.api.rules.RulePriority;

import javax.annotation.CheckForNull;
import javax.annotation.Nullable;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Set;

/**
 * 

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(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(String s) { return escape(s); } @Override String parse(String s) { return s; } } public static StringConverter newStringConverter() { return StringConverter.INSTANCE; } public static final class ToStringConverter extends Converter { private static final ToStringConverter INSTANCE = new ToStringConverter(); private ToStringConverter() { } @Override String format(Object o) { return escape(o.toString()); } @Override String parse(String s) { throw new UnsupportedOperationException("Can not parse with ToStringConverter: " + s); } } public static ToStringConverter newToStringConverter() { return ToStringConverter.INSTANCE; } public static final class IntegerConverter extends Converter { private static final IntegerConverter INSTANCE = new IntegerConverter(); private IntegerConverter() { } @Override String format(Integer s) { return s == null ? "" : String.valueOf(s); } @Override Integer parse(String s) { return StringUtils.isBlank(s) ? null : NumberUtils.toInt(s); } } public static IntegerConverter newIntegerConverter() { return IntegerConverter.INSTANCE; } public static final class PriorityConverter extends Converter { private static final PriorityConverter INSTANCE = new PriorityConverter(); private PriorityConverter() { } @Override String format(RulePriority s) { return s == null ? "" : s.toString(); } @Override RulePriority parse(String s) { return StringUtils.isBlank(s) ? null : RulePriority.valueOf(s); } } public static PriorityConverter newPriorityConverter() { return PriorityConverter.INSTANCE; } public static final class DoubleConverter extends Converter { private static final DoubleConverter INSTANCE = new DoubleConverter(); private DoubleConverter() { } @Override String format(Double d) { return d == null ? "" : String.valueOf(d); } @Override Double parse(String s) { return StringUtils.isBlank(s) ? null : NumberUtils.toDouble(s); } } public static DoubleConverter newDoubleConverter() { return DoubleConverter.INSTANCE; } public static class DateConverter extends Converter { private SimpleDateFormat dateFormat; private DateConverter(String format) { this.dateFormat = new SimpleDateFormat(format); } @Override String format(Date d) { return d == null ? "" : dateFormat.format(d); } @Override Date parse(String s) { try { return StringUtils.isBlank(s) ? null : dateFormat.parse(s); } catch (ParseException e) { throw new IllegalArgumentException("Not a date with format: " + dateFormat.toPattern(), e); } } } public static DateConverter newDateConverter() { return newDateConverter(DateUtils.DATE_FORMAT); } public static DateConverter newDateTimeConverter() { return newDateConverter(DateUtils.DATETIME_FORMAT); } public static DateConverter newDateConverter(String format) { return new DateConverter(format); } /** * If input is null, then an empty map is returned. */ public static Map parse(@Nullable String input, Converter keyConverter, Converter valueConverter) { Map map = Maps.newLinkedHashMap(); if (input != null) { FieldParser reader = new FieldParser(input); boolean end = false; while (!end) { String key = reader.nextKey(); if (key == null) { end = true; } else { String val = StringUtils.defaultString(reader.nextVal(), ""); map.put(keyConverter.parse(key), valueConverter.parse(val)); } } } return map; } public static Map parse(@Nullable String data) { return parse(data, newStringConverter(), newStringConverter()); } /** * @since 2.7 */ public static Map parseStringInt(@Nullable String data) { return parse(data, newStringConverter(), newIntegerConverter()); } /** * @since 2.7 */ public static Map parseStringDouble(@Nullable String data) { return parse(data, newStringConverter(), newDoubleConverter()); } /** * @since 2.7 */ public static Map parseIntString(@Nullable String data) { return parse(data, newIntegerConverter(), newStringConverter()); } /** * @since 2.7 */ public static Map parseIntDouble(@Nullable String data) { return parse(data, newIntegerConverter(), newDoubleConverter()); } /** * @since 2.7 */ public static Map parseIntDate(@Nullable String data) { return parse(data, newIntegerConverter(), newDateConverter()); } /** * @since 2.7 */ public static Map parseIntInt(@Nullable String data) { return parse(data, newIntegerConverter(), newIntegerConverter()); } /** * @since 2.7 */ public static Map parseIntDateTime(@Nullable String data) { return parse(data, newIntegerConverter(), newDateTimeConverter()); } private static String formatEntries(Collection> entries, Converter keyConverter, Converter valueConverter) { StringBuilder sb = new StringBuilder(); boolean first = true; for (Map.Entry entry : entries) { if (!first) { sb.append(PAIR_SEPARATOR); } sb.append(keyConverter.format(entry.getKey())); sb.append(FIELD_SEPARATOR); if (entry.getValue() != null) { sb.append(valueConverter.format(entry.getValue())); } first = false; } return sb.toString(); } private static String formatEntries(Set> entries, Converter keyConverter) { StringBuilder sb = new StringBuilder(); boolean first = true; for (Multiset.Entry entry : entries) { if (!first) { sb.append(PAIR_SEPARATOR); } sb.append(keyConverter.format(entry.getElement())); sb.append(FIELD_SEPARATOR); sb.append(newIntegerConverter().format(entry.getCount())); first = false; } return sb.toString(); } /** * @since 2.7 */ public static String format(Map map, Converter keyConverter, Converter valueConverter) { return formatEntries(map.entrySet(), keyConverter, valueConverter); } /** * @since 2.7 */ public static String format(Map map) { return format(map, newToStringConverter(), newToStringConverter()); } /** * @since 2.7 */ public static String formatIntString(Map map) { return format(map, newIntegerConverter(), newStringConverter()); } /** * @since 2.7 */ public static String formatIntDouble(Map map) { return format(map, newIntegerConverter(), newDoubleConverter()); } /** * @since 2.7 */ public static String formatIntDate(Map map) { return format(map, newIntegerConverter(), newDateConverter()); } /** * @since 2.7 */ public static String formatIntDateTime(Map map) { return format(map, newIntegerConverter(), newDateTimeConverter()); } /** * @since 2.7 */ public static String formatStringInt(Map map) { return format(map, newStringConverter(), newIntegerConverter()); } /** * @since 2.7 */ public static String format(Multiset multiset, Converter keyConverter) { return formatEntries(multiset.entrySet(), keyConverter); } public static String format(Multiset multiset) { return format(multiset, newToStringConverter()); } /** * @since 1.11 * @deprecated use Multiset from google collections instead of commons-collections bags */ @Deprecated public static String format(Bag bag, int var) { StringBuilder sb = new StringBuilder(); if (bag != null) { boolean first = true; for (Object obj : bag.uniqueSet()) { if (!first) { sb.append(PAIR_SEPARATOR); } sb.append(obj.toString()); sb.append(FIELD_SEPARATOR); sb.append(bag.getCount(obj) + var); first = false; } } return sb.toString(); } }