From 31bd21bb061bb1fba99a566ea617dc268ad90a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ha=C3=9Flinger?= Date: Sat, 15 May 2021 22:41:17 +0200 Subject: [PATCH] Properly escape/unescape strings and sort properties files For more information see issue #10 --- .../io/implementation/JsonTranslatorIO.java | 10 +- .../PropertiesTranslatorIO.java | 13 ++- .../de/marhali/easyi18n/util/JsonUtil.java | 8 +- .../easyi18n/util/SortedProperties.java | 31 +++++++ .../de/marhali/easyi18n/util/StringUtil.java | 91 +++++++++++++++++++ 5 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 src/main/java/de/marhali/easyi18n/util/SortedProperties.java create mode 100644 src/main/java/de/marhali/easyi18n/util/StringUtil.java diff --git a/src/main/java/de/marhali/easyi18n/io/implementation/JsonTranslatorIO.java b/src/main/java/de/marhali/easyi18n/io/implementation/JsonTranslatorIO.java index 40e0eb1..1efbd0e 100644 --- a/src/main/java/de/marhali/easyi18n/io/implementation/JsonTranslatorIO.java +++ b/src/main/java/de/marhali/easyi18n/io/implementation/JsonTranslatorIO.java @@ -27,6 +27,7 @@ import java.util.function.Consumer; public class JsonTranslatorIO implements TranslatorIO { private static final String FILE_EXTENSION = "json"; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); @Override public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer callback) { @@ -53,8 +54,8 @@ public class JsonTranslatorIO implements TranslatorIO { locales.add(file.getNameWithoutExtension()); - JsonObject tree = JsonParser.parseReader(new InputStreamReader(file.getInputStream(), - file.getCharset())).getAsJsonObject(); + JsonObject tree = GSON.fromJson(new InputStreamReader(file.getInputStream(), + file.getCharset()), JsonObject.class); JsonUtil.readTree(file.getNameWithoutExtension(), tree, nodes); } @@ -71,9 +72,6 @@ public class JsonTranslatorIO implements TranslatorIO { @Override public void save(@NotNull Project project, @NotNull Translations translations, @NotNull String directoryPath, @NotNull Consumer callback) { - - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - ApplicationManager.getApplication().runWriteAction(() -> { try { for(String locale : translations.getLocales()) { @@ -87,7 +85,7 @@ public class JsonTranslatorIO implements TranslatorIO { VirtualFile vf = created ? LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file) : LocalFileSystem.getInstance().findFileByIoFile(file); - vf.setBinaryContent(gson.toJson(content).getBytes(vf.getCharset())); + vf.setBinaryContent(GSON.toJson(content).getBytes(vf.getCharset())); } // Successfully saved diff --git a/src/main/java/de/marhali/easyi18n/io/implementation/PropertiesTranslatorIO.java b/src/main/java/de/marhali/easyi18n/io/implementation/PropertiesTranslatorIO.java index 5dcfd75..458c49a 100644 --- a/src/main/java/de/marhali/easyi18n/io/implementation/PropertiesTranslatorIO.java +++ b/src/main/java/de/marhali/easyi18n/io/implementation/PropertiesTranslatorIO.java @@ -9,8 +9,11 @@ import de.marhali.easyi18n.io.TranslatorIO; import de.marhali.easyi18n.model.LocalizedNode; import de.marhali.easyi18n.model.Translations; import de.marhali.easyi18n.util.IOUtil; +import de.marhali.easyi18n.util.SortedProperties; +import de.marhali.easyi18n.util.StringUtil; import de.marhali.easyi18n.util.TranslationsUtil; +import org.apache.commons.lang.StringEscapeUtils; import org.jetbrains.annotations.NotNull; import java.io.*; @@ -49,7 +52,7 @@ public class PropertiesTranslatorIO implements TranslatorIO { } locales.add(file.getNameWithoutExtension()); - Properties properties = new Properties(); + SortedProperties properties = new SortedProperties(); properties.load(new InputStreamReader(file.getInputStream(), file.getCharset())); readProperties(file.getNameWithoutExtension(), properties, nodes); } @@ -70,7 +73,7 @@ public class PropertiesTranslatorIO implements TranslatorIO { ApplicationManager.getApplication().runWriteAction(() -> { try { for(String locale : translations.getLocales()) { - Properties properties = new Properties(); + SortedProperties properties = new SortedProperties(); writeProperties(locale, properties, translations.getNodes(), ""); String fullPath = directoryPath + "/" + locale + "." + FILE_EXTENSION; @@ -95,7 +98,8 @@ public class PropertiesTranslatorIO implements TranslatorIO { private void writeProperties(String locale, Properties props, LocalizedNode node, String parentPath) { if(node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) { if(node.getValue().get(locale) != null) { // Translation is defined - track it - props.setProperty(parentPath, node.getValue().get(locale)); + String value = StringEscapeUtils.unescapeJava(node.getValue().get(locale)); + props.setProperty(parentPath, value); } } else { @@ -124,7 +128,8 @@ public class PropertiesTranslatorIO implements TranslatorIO { } Map messages = node.getValue(); - messages.put(locale, String.valueOf(value)); + String escapedValue = StringUtil.escapeControls(String.valueOf(value), true); + messages.put(locale, escapedValue); node.setValue(messages); }); } diff --git a/src/main/java/de/marhali/easyi18n/util/JsonUtil.java b/src/main/java/de/marhali/easyi18n/util/JsonUtil.java index 145f4b3..10646dc 100644 --- a/src/main/java/de/marhali/easyi18n/util/JsonUtil.java +++ b/src/main/java/de/marhali/easyi18n/util/JsonUtil.java @@ -6,6 +6,8 @@ import com.google.gson.JsonPrimitive; import de.marhali.easyi18n.model.LocalizedNode; +import org.apache.commons.lang.StringEscapeUtils; + import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -25,7 +27,8 @@ public class JsonUtil { public static void writeTree(String locale, JsonObject parent, LocalizedNode node) { if(node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) { if(node.getValue().get(locale) != null) { - parent.add(node.getKey(), new JsonPrimitive(node.getValue().get(locale))); + String value = StringEscapeUtils.unescapeJava(node.getValue().get(locale)); + parent.add(node.getKey(), new JsonPrimitive(value)); } } else { @@ -75,7 +78,8 @@ public class JsonUtil { } Map messages = leafNode.getValue(); - messages.put(locale, entry.getValue().getAsString()); + String value = StringUtil.escapeControls(entry.getValue().getAsString(), true); + messages.put(locale, value); leafNode.setValue(messages); } } diff --git a/src/main/java/de/marhali/easyi18n/util/SortedProperties.java b/src/main/java/de/marhali/easyi18n/util/SortedProperties.java new file mode 100644 index 0000000..5b8f9c7 --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/util/SortedProperties.java @@ -0,0 +1,31 @@ +package de.marhali.easyi18n.util; + +import java.util.*; + +/** + * Applies sorting to {@link Properties} files. + * @author marhali + */ +public class SortedProperties extends Properties { + + @Override + public Set keySet() { + return Collections.unmodifiableSet(new TreeSet<>(super.keySet())); + } + + @Override + public Set> entrySet() { + TreeMap sorted = new TreeMap<>(); + + for(Object key : super.keySet()) { + sorted.put(key, get(key)); + } + + return sorted.entrySet(); + } + + @Override + public synchronized Enumeration keys() { + return Collections.enumeration(new TreeSet<>(super.keySet())); + } +} \ No newline at end of file diff --git a/src/main/java/de/marhali/easyi18n/util/StringUtil.java b/src/main/java/de/marhali/easyi18n/util/StringUtil.java new file mode 100644 index 0000000..88c7ab8 --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/util/StringUtil.java @@ -0,0 +1,91 @@ +package de.marhali.easyi18n.util; + +import org.jetbrains.annotations.NotNull; + +import java.io.StringWriter; + +/** + * String utilities + * @author marhali, Apache Commons + */ +public class StringUtil { + + /** + * Escapes control characters for the given input string. + * Inspired by Apache Commons (see {@link org.apache.commons.lang.StringEscapeUtils} + * @param input The input string + * @param skipStrings Should every string literal indication ("", '') be skipped? (Needed e.g. for json) + * @return Escaped string + */ + public static @NotNull String escapeControls(@NotNull String input, boolean skipStrings) { + int length = input.length(); + StringWriter out = new StringWriter(length * 2); + + for(int i = 0; i < length; i++) { + char ch = input.charAt(i); + + if(ch < ' ') { + switch(ch) { + case '\b': + out.write(92); + out.write(98); + break; + case '\t': + out.write(92); + out.write(116); + break; + case '\n': + out.write(92); + out.write(110); + break; + case '\u000b': + default: + if (ch > 15) { + out.write("\\u00" + hex(ch)); + } else { + out.write("\\u000" + hex(ch)); + } + break; + case '\f': + out.write(92); + out.write(102); + break; + case '\r': + out.write(92); + out.write(114); + } + } else { + switch(ch) { + case '"': + if(!skipStrings) { + out.write(92); + } + out.write(34); + break; + case '\'': + if(!skipStrings) { + out.write(92); + } + out.write(39); + break; + case '/': + out.write(92); + out.write(47); + break; + case '\\': + out.write(92); + out.write(92); + break; + default: + out.write(ch); + } + } + } + + return out.toString(); + } + + private static @NotNull String hex(char ch) { + return Integer.toHexString(ch).toUpperCase(); + } +} \ No newline at end of file