org.sonar.api.config.internal.MultivalueProperty Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sonar-plugin-api-impl Show documentation
Show all versions of sonar-plugin-api-impl Show documentation
Open source platform for continuous inspection of code quality
/*
* 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.config.internal;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.lang.ArrayUtils;
import static java.util.function.UnaryOperator.identity;
public class MultivalueProperty {
private MultivalueProperty() {
// prevents instantiation
}
public static String[] parseAsCsv(String key, String value) {
return parseAsCsv(key, value, identity());
}
public static String[] parseAsCsv(String key, String value, UnaryOperator valueProcessor) {
String cleanValue = MultivalueProperty.trimFieldsAndRemoveEmptyFields(value);
List result = new ArrayList<>();
try (CSVParser csvParser = CSVFormat.RFC4180.builder()
.setSkipHeaderRecord(true)
.setIgnoreEmptyLines(true)
.setIgnoreSurroundingSpaces(true)
.build()
.parse(new StringReader(cleanValue))) {
List records = csvParser.getRecords();
if (records.isEmpty()) {
return ArrayUtils.EMPTY_STRING_ARRAY;
}
processRecords(result, records, valueProcessor);
return result.toArray(new String[result.size()]);
} catch (IOException e) {
throw new IllegalStateException("Property: '" + key + "' doesn't contain a valid CSV value: '" + value + "'", e);
}
}
/**
* In most cases we expect a single record.
Having multiple records means the input value was splitted over multiple lines (this is common in Maven).
* For example:
*
* <sonar.exclusions>
* src/foo,
* src/bar,
* src/biz
* <sonar.exclusions>
*
* In this case records will be merged to form a single list of items. Last item of a record is appended to first item of next record.
*
* This is a very curious case, but we try to preserve line break in the middle of an item:
*
* <sonar.exclusions>
* a
* b,
* c
* <sonar.exclusions>
*
* will produce ['a\nb', 'c']
*/
private static void processRecords(List result, List records, Function valueProcessor) {
for (CSVRecord csvRecord : records) {
Iterator it = csvRecord.iterator();
if (!result.isEmpty()) {
String next = it.next();
if (!next.isEmpty()) {
int lastItemIdx = result.size() - 1;
String previous = result.get(lastItemIdx);
if (previous.isEmpty()) {
result.set(lastItemIdx, valueProcessor.apply(next));
} else {
result.set(lastItemIdx, valueProcessor.apply(previous + "\n" + next));
}
}
}
it.forEachRemaining(s -> {
String apply = valueProcessor.apply(s);
result.add(apply);
});
}
}
/**
* Removes the empty fields from the value of a multi-value property from empty fields, including trimming each field.
*
* Quotes can be used to prevent an empty field to be removed (as it is used to preserve empty spaces).
*
* - {@code "" => ""}
* - {@code " " => ""}
* - {@code "," => ""}
* - {@code ",," => ""}
* - {@code ",,," => ""}
* - {@code ",a" => "a"}
* - {@code "a," => "a"}
* - {@code ",a," => "a"}
* - {@code "a,,b" => "a,b"}
* - {@code "a, ,b" => "a,b"}
* - {@code "a,\"\",b" => "a,b"}
* - {@code "\"a\",\"b\"" => "\"a\",\"b\""}
* - {@code "\" a \",\"b \"" => "\" a \",\"b \""}
* - {@code "\"a\",\"\",\"b\"" => "\"a\",\"\",\"b\""}
* - {@code "\"a\",\" \",\"b\"" => "\"a\",\" \",\"b\""}
* - {@code "\" a,,b,c \",\"d \"" => "\" a,,b,c \",\"d \""}
* - {@code "a,\" \",b" => "ab"]}
*
*/
static String trimFieldsAndRemoveEmptyFields(String str) {
char[] chars = str.toCharArray();
char[] res = new char[chars.length];
/*
* set when reading the first non trimmable char after a separator char (or the beginning of the string)
* unset when reading a separator
*/
boolean inField = false;
boolean inQuotes = false;
int i = 0;
int resI = 0;
for (; i < chars.length; i++) {
boolean isSeparator = chars[i] == ',';
if (!inQuotes && isSeparator) {
// exiting field (may already be unset)
inField = false;
if (resI > 0) {
resI = retroTrim(res, resI);
}
} else {
boolean isTrimmed = !inQuotes && istrimmable(chars[i]);
if (isTrimmed && !inField) {
// we haven't meet any non trimmable char since the last separator yet
continue;
}
boolean isEscape = isEscapeChar(chars[i]);
if (isEscape) {
inQuotes = !inQuotes;
}
// add separator as we already had one field
if (!inField && resI > 0) {
res[resI] = ',';
resI++;
}
// register in field (may already be set)
inField = true;
// copy current char
res[resI] = chars[i];
resI++;
}
}
// inQuotes can only be true at this point if quotes are unbalanced
if (!inQuotes) {
// trim end of str
resI = retroTrim(res, resI);
}
return new String(res, 0, resI);
}
private static boolean isEscapeChar(char aChar) {
return aChar == '"';
}
private static boolean istrimmable(char aChar) {
return aChar <= ' ';
}
/**
* Reads from index {@code resI} to the beginning into {@code res} looking up the location of the trimmable char with
* the lowest index before encountering a non-trimmable char.
*
* This basically trims {@code res} from any trimmable char at its end.
*
* @return index of next location to put new char in res
*/
private static int retroTrim(char[] res, int resI) {
int i = resI;
while (i >= 1) {
if (!istrimmable(res[i - 1])) {
return i;
}
i--;
}
return i;
}
}