diff --git a/src/main/java/edu/ie3/util/StringUtils.java b/src/main/java/edu/ie3/util/StringUtils.java index e65a8ee8..20c88037 100644 --- a/src/main/java/edu/ie3/util/StringUtils.java +++ b/src/main/java/edu/ie3/util/StringUtils.java @@ -9,6 +9,7 @@ /** Some useful functions to manipulate Strings */ public class StringUtils { + private StringUtils() { throw new IllegalStateException("Utility classes cannot be instantiated."); } @@ -80,7 +81,7 @@ public static String[] camelCaseToSnakeCase(String[] input) { * @return Quoted String */ public static String quote(String input) { - return input.replaceAll("^([^\"])", "\"$1").replaceAll("([^\"])$", "$1\""); + return input.matches("^\".*\"$") ? input : "\"" + input + "\""; } /** @@ -102,4 +103,48 @@ public static String[] quote(String[] input) { public static String cleanString(String input) { return input.replaceAll("[^\\w]", "_"); } + + /** + * Quotes a given string that contains special characters to comply with the csv specification RFC + * 4180 (https://tools.ietf.org/html/rfc4180). Double quotes are escaped according to + * specification. + * + * @param inputString string that should be converted to a valid rfc 4180 string + * @param csvSep separator of the csv file + * @return a csv string that is valid according to rfc 4180 + */ + public static String csvString(String inputString, String csvSep) { + if (needsCsvRFC4180Quote(inputString, csvSep)) { + /* Get rid of first and last quotation if there is some. */ + String inputUnquoted = unquoteStartEnd(inputString); + /* Escape every double quotation mark within the String by doubling it */ + String withEscapedQuotes = inputUnquoted.replaceAll("\"", "\"\""); + /* finally add quotes to the strings start and end again */ + return quote(withEscapedQuotes); + } else return inputString; + } + + /** + * Removes double quotes at start and end position of the provided string, if any + * + * @param input string that should be unquoted + * @return copy of the provided string without start and end double quotes + */ + public static String unquoteStartEnd(String input) { + return input.matches("^\".*\"$") ? input.substring(1, input.length() - 1) : input; + } + + /** + * Check if the provided string needs to be quoted according to the csv specification RFC 4180 + * + * @param inputString the string that should be checked + * @param csvSep separator of the csv file + * @return true of the string needs to be quoted, false otherwise + */ + private static boolean needsCsvRFC4180Quote(String inputString, String csvSep) { + return inputString.contains(csvSep) + || inputString.contains(",") + || inputString.contains("\"") + || inputString.contains("\n"); + } } diff --git a/src/test/groovy/edu/ie3/util/StringUtilsTest.groovy b/src/test/groovy/edu/ie3/util/StringUtilsTest.groovy index f6ebc246..a0c503c0 100644 --- a/src/test/groovy/edu/ie3/util/StringUtilsTest.groovy +++ b/src/test/groovy/edu/ie3/util/StringUtilsTest.groovy @@ -7,6 +7,8 @@ package edu.ie3.util import spock.lang.Specification +import java.util.stream.Collectors + class StringUtilsTest extends Specification { def "The StringUtils quote a single String correctly"() { @@ -17,11 +19,14 @@ class StringUtilsTest extends Specification { actual == expected where: - input || expected - "test" || "\"test\"" - "\"test" || "\"test\"" - "test\"" || "\"test\"" - "\"test\"" || "\"test\"" + input || expected + "test" || "\"test\"" + "\"test" || "\"\"test\"" + "test\"" || "\"test\"\"" + "\"test\"" || "\"test\"" + "\"This\" is a test" || "\"\"This\" is a test\"" + "This is \"a\" test" || "\"This is \"a\" test\"" + "This is a \"test\"" || "\"This is a \"test\"\"" } def "The StringUtils are able to quote each element of an array of Strings"() { @@ -211,4 +216,105 @@ class StringUtilsTest extends Specification { "?ab123" || "_ab123" "ßab123" || "_ab123" } + + def "The StringUtils converts a given Array of csv header elements to match the csv specification RFC 4180 "() { + given: + def input = [ + "4ca90220-74c2-4369-9afa-a18bf068840d", + "{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}", + "node_a", + "2020-03-25T15:11:31Z[UTC] \n 2020-03-24T15:11:31Z[UTC]", + "8f9682df-0744-4b58-a122-f0dc730f6510", + "true", + "1,0", + "1.0", + "Höchstspannung", + "380.0", + "olm:{(0.00,1.00)}", + "cosPhiP:{(0.0,1.0),(0.9,1.0),(1.2,-0.3)}" + ] + def expected = [ + "4ca90220-74c2-4369-9afa-a18bf068840d", + "\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528],\"\"crs\"\":{\"\"type\"\":\"\"name\"\",\"\"properties\"\":{\"\"name\"\":\"\"EPSG:4326\"\"}}}\"", + "node_a", + "\"2020-03-25T15:11:31Z[UTC] \n 2020-03-24T15:11:31Z[UTC]\"", + "8f9682df-0744-4b58-a122-f0dc730f6510", + "true", + "\"1,0\"", + "1.0", + "Höchstspannung", + "380.0", + "\"olm:{(0.00,1.00)}\"", + "\"cosPhiP:{(0.0,1.0),(0.9,1.0),(1.2,-0.3)}\""] as Set + + when: + def actual = input.stream().map({ inputElement -> StringUtils.csvString(inputElement, ",") }).collect(Collectors.toSet()) as Set + + then: + actual == expected + } + + def "The StringUtils converts a given LinkedHashMap of csv data to match the csv specification RFC 4180 "() { + given: + def input = [ + "activePowerGradient": "25.0", + "capex" : "100,0", + "cosphiRated" : "0.95", + "etaConv" : "98.0", + "id" : "test \n bmTypeInput", + "opex" : "50.0", + "sRated" : "25.0", + "uu,id" : "5ebd8f7e-dedb-4017-bb86-6373c4b68eb8", + "geoPosition" : "{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}", + "olm\"characteristic": "olm:{(0.0,1.0)}", + "cosPhiFixed" : "cosPhiFixed:{(0.0,1.0)}" + ] as LinkedHashMap + + def expected = [ + "activePowerGradient" : "25.0", + "capex" : "\"100,0\"", + "cosphiRated" : "0.95", + "etaConv" : "98.0", + "id" : "\"test \n bmTypeInput\"", + "opex" : "50.0", + "sRated" : "25.0", + "\"uu,id\"" : "5ebd8f7e-dedb-4017-bb86-6373c4b68eb8", + "geoPosition" : "\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528],\"\"crs\"\":{\"\"type\"\":\"\"name\"\",\"\"properties\"\":{\"\"name\"\":\"\"EPSG:4326\"\"}}}\"", + "\"olm\"\"characteristic\"": "\"olm:{(0.0,1.0)}\"", + "cosPhiFixed" : "\"cosPhiFixed:{(0.0,1.0)}\"" + ] as LinkedHashMap + + when: + def actualList = input.entrySet().stream().map({ mapEntry -> + return new AbstractMap.SimpleEntry(StringUtils.csvString(mapEntry.key, ","), StringUtils.csvString(mapEntry.value, ",")) + }) as Set + + def actual = actualList.collectEntries { + [it.key, it.value] + } + + then: + actual == expected + } + + def "The StringUtils converts a given String to match the csv specification RFC 4180 "() { + expect: + StringUtils.csvString(inputString, csvSep) == expect + + where: + inputString | csvSep || expect + "activePowerGradient" | "," || "activePowerGradient" + "\"100,0\"" | "," || "\"100,0\"" + "100,0" | "," || "\"100,0\"" + "100,0" | ";" || "\"100,0\"" + "100;0" | ";" || "\"100;0\"" + "\"100;0\"" | ";" || "\"100;0\"" + "100;0" | "," || "100;0" + "olm:{(0.00,1.00)}" | "," || "\"olm:{(0.00,1.00)}\"" + "olm:{(0.00,1.00)}" | ";" || "\"olm:{(0.00,1.00)}\"" + "{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528]}" | "," || "\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528]}\"" + "{\"type\":\"Point\",\"coordinates\":[7.411111,51.492528]}" | ";" || "\"{\"\"type\"\":\"\"Point\"\",\"\"coordinates\"\":[7.411111,51.492528]}\"" + "uu,id" | "," || "\"uu,id\"" + "uu,id" | ";" || "\"uu,id\"" + } }