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

org.sonar.api.config.internal.MultivalueProperty Maven / Gradle / Ivy

There is a newer version: 24.12.0.100206
Show newest version
/*
 * SonarQube
 * Copyright (C) 2009-2024 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.io.UncheckedIOException;
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.lang3.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 | UncheckedIOException 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy