You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MultivalueProperty.java 6.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2020 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.api.config.internal;
  21. import java.io.IOException;
  22. import java.io.StringReader;
  23. import java.util.ArrayList;
  24. import java.util.Iterator;
  25. import java.util.List;
  26. import java.util.function.Function;
  27. import org.apache.commons.csv.CSVFormat;
  28. import org.apache.commons.csv.CSVParser;
  29. import org.apache.commons.csv.CSVRecord;
  30. import org.apache.commons.lang.ArrayUtils;
  31. public class MultivalueProperty {
  32. private MultivalueProperty() {
  33. // prevents instantiation
  34. }
  35. public static String[] parseAsCsv(String key, String value) {
  36. return parseAsCsv(key, value, Function.identity());
  37. }
  38. public static String[] parseAsCsv(String key, String value, Function<String, String> valueProcessor) {
  39. String cleanValue = MultivalueProperty.trimFieldsAndRemoveEmptyFields(value);
  40. List<String> result = new ArrayList<>();
  41. try (CSVParser csvParser = CSVFormat.RFC4180
  42. .withHeader((String) null)
  43. .withIgnoreEmptyLines()
  44. .withIgnoreSurroundingSpaces()
  45. .parse(new StringReader(cleanValue))) {
  46. List<CSVRecord> records = csvParser.getRecords();
  47. if (records.isEmpty()) {
  48. return ArrayUtils.EMPTY_STRING_ARRAY;
  49. }
  50. processRecords(result, records, valueProcessor);
  51. return result.toArray(new String[result.size()]);
  52. } catch (IOException e) {
  53. throw new IllegalStateException("Property: '" + key + "' doesn't contain a valid CSV value: '" + value + "'", e);
  54. }
  55. }
  56. /**
  57. * In most cases we expect a single record. <br>Having multiple records means the input value was splitted over multiple lines (this is common in Maven).
  58. * For example:
  59. * <pre>
  60. * &lt;sonar.exclusions&gt;
  61. * src/foo,
  62. * src/bar,
  63. * src/biz
  64. * &lt;sonar.exclusions&gt;
  65. * </pre>
  66. * 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.
  67. * <p>
  68. * This is a very curious case, but we try to preserve line break in the middle of an item:
  69. * <pre>
  70. * &lt;sonar.exclusions&gt;
  71. * a
  72. * b,
  73. * c
  74. * &lt;sonar.exclusions&gt;
  75. * </pre>
  76. * will produce ['a\nb', 'c']
  77. */
  78. private static void processRecords(List<String> result, List<CSVRecord> records, Function<String, String> valueProcessor) {
  79. for (CSVRecord csvRecord : records) {
  80. Iterator<String> it = csvRecord.iterator();
  81. if (!result.isEmpty()) {
  82. String next = it.next();
  83. if (!next.isEmpty()) {
  84. int lastItemIdx = result.size() - 1;
  85. String previous = result.get(lastItemIdx);
  86. if (previous.isEmpty()) {
  87. result.set(lastItemIdx, valueProcessor.apply(next));
  88. } else {
  89. result.set(lastItemIdx, valueProcessor.apply(previous + "\n" + next));
  90. }
  91. }
  92. }
  93. it.forEachRemaining(s -> {
  94. String apply = valueProcessor.apply(s);
  95. result.add(apply);
  96. });
  97. }
  98. }
  99. /**
  100. * Removes the empty fields from the value of a multi-value property from empty fields, including trimming each field.
  101. * <p>
  102. * Quotes can be used to prevent an empty field to be removed (as it is used to preserve empty spaces).
  103. * <ul>
  104. * <li>{@code "" => ""}</li>
  105. * <li>{@code " " => ""}</li>
  106. * <li>{@code "," => ""}</li>
  107. * <li>{@code ",," => ""}</li>
  108. * <li>{@code ",,," => ""}</li>
  109. * <li>{@code ",a" => "a"}</li>
  110. * <li>{@code "a," => "a"}</li>
  111. * <li>{@code ",a," => "a"}</li>
  112. * <li>{@code "a,,b" => "a,b"}</li>
  113. * <li>{@code "a, ,b" => "a,b"}</li>
  114. * <li>{@code "a,\"\",b" => "a,b"}</li>
  115. * <li>{@code "\"a\",\"b\"" => "\"a\",\"b\""}</li>
  116. * <li>{@code "\" a \",\"b \"" => "\" a \",\"b \""}</li>
  117. * <li>{@code "\"a\",\"\",\"b\"" => "\"a\",\"\",\"b\""}</li>
  118. * <li>{@code "\"a\",\" \",\"b\"" => "\"a\",\" \",\"b\""}</li>
  119. * <li>{@code "\" a,,b,c \",\"d \"" => "\" a,,b,c \",\"d \""}</li>
  120. * <li>{@code "a,\" \",b" => "ab"]}</li>
  121. * </ul>
  122. */
  123. static String trimFieldsAndRemoveEmptyFields(String str) {
  124. char[] chars = str.toCharArray();
  125. char[] res = new char[chars.length];
  126. /*
  127. * set when reading the first non trimmable char after a separator char (or the beginning of the string)
  128. * unset when reading a separator
  129. */
  130. boolean inField = false;
  131. boolean inQuotes = false;
  132. int i = 0;
  133. int resI = 0;
  134. for (; i < chars.length; i++) {
  135. boolean isSeparator = chars[i] == ',';
  136. if (!inQuotes && isSeparator) {
  137. // exiting field (may already be unset)
  138. inField = false;
  139. if (resI > 0) {
  140. resI = retroTrim(res, resI);
  141. }
  142. } else {
  143. boolean isTrimmed = !inQuotes && istrimmable(chars[i]);
  144. if (isTrimmed && !inField) {
  145. // we haven't meet any non trimmable char since the last separator yet
  146. continue;
  147. }
  148. boolean isEscape = isEscapeChar(chars[i]);
  149. if (isEscape) {
  150. inQuotes = !inQuotes;
  151. }
  152. // add separator as we already had one field
  153. if (!inField && resI > 0) {
  154. res[resI] = ',';
  155. resI++;
  156. }
  157. // register in field (may already be set)
  158. inField = true;
  159. // copy current char
  160. res[resI] = chars[i];
  161. resI++;
  162. }
  163. }
  164. // inQuotes can only be true at this point if quotes are unbalanced
  165. if (!inQuotes) {
  166. // trim end of str
  167. resI = retroTrim(res, resI);
  168. }
  169. return new String(res, 0, resI);
  170. }
  171. private static boolean isEscapeChar(char aChar) {
  172. return aChar == '"';
  173. }
  174. private static boolean istrimmable(char aChar) {
  175. return aChar <= ' ';
  176. }
  177. /**
  178. * Reads from index {@code resI} to the beginning into {@code res} looking up the location of the trimmable char with
  179. * the lowest index before encountering a non-trimmable char.
  180. * <p>
  181. * This basically trims {@code res} from any trimmable char at its end.
  182. *
  183. * @return index of next location to put new char in res
  184. */
  185. private static int retroTrim(char[] res, int resI) {
  186. int i = resI;
  187. while (i >= 1) {
  188. if (!istrimmable(res[i - 1])) {
  189. return i;
  190. }
  191. i--;
  192. }
  193. return i;
  194. }
  195. }