implement standard json io strategy

This commit is contained in:
Marcel Haßlinger 2021-11-04 10:22:21 +01:00
parent 3d7f28f8cc
commit bc3717d874
5 changed files with 384 additions and 0 deletions

View File

@ -0,0 +1,21 @@
package de.marhali.easyi18n.io.json;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import de.marhali.easyi18n.io.ArrayMapper;
/**
* Map json array values.
* @author marhali
*/
public class JsonArrayMapper extends ArrayMapper {
public static String read(JsonArray array) {
return read(array.iterator(), JsonElement::getAsString);
}
public static JsonArray write(String concat) {
JsonArray array = new JsonArray();
write(concat, array::add);
return array;
}
}

View File

@ -0,0 +1,92 @@
package de.marhali.easyi18n.io.json;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import de.marhali.easyi18n.io.IOStrategy;
import de.marhali.easyi18n.model.SettingsState;
import de.marhali.easyi18n.model.TranslationData;
import de.marhali.easyi18n.model.TranslationNode;
import net.minidev.json.JSONObject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.TreeMap;
import java.util.function.Consumer;
/**
* Strategy for simple json locale files. Each locale has its own file.
* For example localesPath/en.json, localesPath/de.json.
* @author marhali
*/
public class JsonIOStrategy implements IOStrategy {
private static final String FILE_EXTENSION = "json";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@Override
public boolean canUse(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state) {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(directory == null || directory.getChildren() == null) {
return false;
}
for(VirtualFile children : directory.getChildren()) {
if(!children.isDirectory() && isFileRelevant(state, children)) {
if(children.getFileType().getDefaultExtension().toLowerCase().equals(FILE_EXTENSION)) {
return true;
}
}
}
return false;
}
@Override
public void read(@NotNull Project project, @NotNull String localesPath,
@NotNull SettingsState state, @NotNull Consumer<@Nullable TranslationData> result) {
ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added)
ApplicationManager.getApplication().runReadAction(() -> {
VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(localesPath));
if(directory == null || directory.getChildren() == null) {
throw new IllegalArgumentException("Specified folder is invalid (" + localesPath + ")");
}
TranslationData data = new TranslationData(state.isSortKeys(), state.isNestedKeys());
try {
for(VirtualFile file : directory.getChildren()) {
if(!isFileRelevant(state, file)) {
continue;
}
data.addLocale(file.getNameWithoutExtension());
JSONObject tree = GSON.fromJson(new InputStreamReader(file.getInputStream(), file.getCharset()), JSONObject.class);
}
} catch(IOException e) {
e.printStackTrace();
result.accept(null);
}
});
}
@Override
public void write(@NotNull Project project, @NotNull String localesPath, @NotNull SettingsState state, @NotNull TranslationData data, @NotNull Consumer<Boolean> result) {
}
}

View File

@ -0,0 +1,73 @@
package de.marhali.easyi18n.io.json;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import de.marhali.easyi18n.model.Translation;
import de.marhali.easyi18n.model.TranslationNode;
import de.marhali.easyi18n.util.StringUtil;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.math.NumberUtils;
import java.util.Map;
/**
* Mapper for mapping json objects into translation nodes and backwards.
* @author marhali
*/
public class JsonMapper {
public static void read(String locale, JsonObject json, TranslationNode node) {
for(Map.Entry<String, JsonElement> entry : json.entrySet()) {
String key = entry.getKey();
JsonElement value = entry.getValue();
TranslationNode childNode = node.getOrCreateChildren(key);
if(value.isJsonObject()) {
// Nested element - run recursively
read(locale, value.getAsJsonObject(), childNode);
} else {
Translation translation = childNode.getValue();
String content = entry.getValue().isJsonArray()
? JsonArrayMapper.read(value.getAsJsonArray())
: StringUtil.escapeControls(value.getAsString(), true);
translation.put(locale, content);
childNode.setValue(translation);
}
}
}
public static void write(String locale, JsonObject json, TranslationNode node) {
for(Map.Entry<String, TranslationNode> entry : node.getChildren().entrySet()) {
String key = entry.getKey();
TranslationNode childNode = entry.getValue();
if(!childNode.isLeaf()) {
// Nested node - run recursively
JsonObject childJson = new JsonObject();
write(locale, childJson, childNode);
if(childJson.size() > 0) {
json.add(key, childJson);
}
} else {
Translation translation = childNode.getValue();
String content = translation.get(locale);
if(content != null) {
if(JsonArrayMapper.isArray(content)) {
json.add(key, JsonArrayMapper.write(content));
} else if(NumberUtils.isNumber(content)) {
json.add(key, new JsonPrimitive(NumberUtils.createNumber(content)));
} else {
json.add(key, new JsonPrimitive(StringEscapeUtils.unescapeJava(content)));
}
}
}
}
}
}

View File

@ -0,0 +1,45 @@
package de.marhali.easyi18n.mapper;
import de.marhali.easyi18n.model.Translation;
import org.junit.Test;
/**
* Defines test cases for {@link de.marhali.easyi18n.model.TranslationNode} mapping.
* @author marhali
*/
public abstract class AbstractMapperTest {
protected final String specialCharacters = "Special characters: äü@Öä€/$§;.-?+~#```'' end";
protected final String arraySimple = "!arr[first;second]";
protected final String arrayEscaped = "!arr[first\\;element;second element;third\\;element]";
protected final String leadingSpace = " leading space";
@Test
public abstract void testNonSorting();
@Test
public abstract void testSorting();
@Test
public abstract void testArrays();
@Test
public abstract void testSpecialCharacters();
@Test
public abstract void testNestedKeys();
@Test
public abstract void testNonNestedKeys();
@Test
public abstract void testLeadingSpace();
@Test
public abstract void testNumbers();
protected Translation create(String content) {
return new Translation("en", content);
}
}

View File

@ -0,0 +1,153 @@
package de.marhali.easyi18n.mapper;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import de.marhali.easyi18n.io.json.JsonArrayMapper;
import de.marhali.easyi18n.io.json.JsonMapper;
import de.marhali.easyi18n.model.TranslationData;
import org.apache.commons.lang.StringEscapeUtils;
import org.junit.Assert;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Unit tests for {@link de.marhali.easyi18n.io.json.JsonMapper}
* @author marhali
*/
public class JsonMapperTest extends AbstractMapperTest {
@Override
public void testNonSorting() {
JsonObject input = new JsonObject();
input.add("zulu", new JsonPrimitive("test"));
input.add("alpha", new JsonPrimitive("test"));
input.add("bravo", new JsonPrimitive("test"));
TranslationData data = new TranslationData(false, true);
JsonMapper.read("en", input, data.getRootNode());
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Set<String> expect = new LinkedHashSet<>(Arrays.asList("zulu", "alpha", "bravo"));
Assert.assertEquals(expect, output.keySet());
}
@Override
public void testSorting() {
JsonObject input = new JsonObject();
input.add("zulu", new JsonPrimitive("test"));
input.add("alpha", new JsonPrimitive("test"));
input.add("bravo", new JsonPrimitive("test"));
TranslationData data = new TranslationData(true, true);
JsonMapper.read("en", input, data.getRootNode());
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Set<String> expect = new LinkedHashSet<>(Arrays.asList("alpha", "bravo", "zulu"));
Assert.assertEquals(expect, output.keySet());
}
@Override
public void testArrays() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("simple", create(arraySimple));
data.setTranslation("escaped", create(arrayEscaped));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertTrue(output.get("simple").isJsonArray());
Assert.assertEquals(arraySimple, JsonArrayMapper.read(output.get("simple").getAsJsonArray()));
Assert.assertTrue(output.get("escaped").isJsonArray());
Assert.assertEquals(arrayEscaped, StringEscapeUtils.unescapeJava(JsonArrayMapper.read(output.get("escaped").getAsJsonArray())));
}
@Override
public void testSpecialCharacters() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("chars", create(specialCharacters));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertEquals(specialCharacters, output.get("chars").getAsString());
TranslationData input = new TranslationData(true, true);
JsonMapper.read("en", output, input.getRootNode());
Assert.assertEquals(specialCharacters, StringEscapeUtils.unescapeJava(input.getTranslation("chars").get("en")));
}
@Override
public void testNestedKeys() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("nested.key.section", create("test"));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertEquals("test", output.getAsJsonObject("nested").getAsJsonObject("key").get("section").getAsString());
TranslationData input = new TranslationData(true, true);
JsonMapper.read("en", output, input.getRootNode());
Assert.assertEquals("test", input.getTranslation("nested.key.section").get("en"));
}
@Override
public void testNonNestedKeys() {
TranslationData data = new TranslationData(true, false);
data.setTranslation("long.key.with.many.sections", create("test"));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertTrue(output.has("long.key.with.many.sections"));
TranslationData input = new TranslationData(true, false);
JsonMapper.read("en", output, input.getRootNode());
Assert.assertEquals("test", input.getTranslation("long.key.with.many.sections").get("en"));
}
@Override
public void testLeadingSpace() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("space", create(leadingSpace));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertEquals(leadingSpace, output.get("space").getAsString());
TranslationData input = new TranslationData(true, true);
JsonMapper.read("en", output, input.getRootNode());
Assert.assertEquals(leadingSpace, input.getTranslation("space").get("en"));
}
@Override
public void testNumbers() {
TranslationData data = new TranslationData(true, true);
data.setTranslation("numbered", create("15000"));
JsonObject output = new JsonObject();
JsonMapper.write("en", output, data.getRootNode());
Assert.assertEquals(15000, output.get("numbered").getAsNumber());
JsonObject input = new JsonObject();
input.addProperty("numbered", 143.23);
JsonMapper.read("en", input, data.getRootNode());
Assert.assertEquals("143.23", data.getTranslation("numbered").get("en"));
}
}