diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9dffb..becc0ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ # easy-i18n Changelog ## [Unreleased] +### Added +- Support for YAML locale files. Thanks to @sunarya-thito +- Optional path-prefix for translations + +### Changed +- Optimized i18n key completion + ## [1.4.1] ### Added - Support for IntelliJ 2021.2 diff --git a/example/yaml/locale-de.yml b/example/yaml/locale-de.yml new file mode 100644 index 0000000..7f30ab6 --- /dev/null +++ b/example/yaml/locale-de.yml @@ -0,0 +1,17 @@ +alpha: + spacing: ' führendes Leerzeichen' + first: Beispiel Übersetzung +beta: + title: Titel + nested: + title: Ein verschachtelter Titel +gamma: + array: + escaped: + - Erstes;Element + - Zweites Element + - Drittes;Element + simple: + - Erstes Element + - Zweites Element + title: Gamma Titel diff --git a/example/yaml/locale-en.yml b/example/yaml/locale-en.yml new file mode 100644 index 0000000..f8f0737 --- /dev/null +++ b/example/yaml/locale-en.yml @@ -0,0 +1,17 @@ +alpha: + spacing: ' leading space' + first: Example Translation +beta: + title: Title + nested: + title: some nested title +gamma: + array: + escaped: + - first;element + - second element + - third;element + simple: + - first element + - second element + title: gamma title diff --git a/src/main/java/de/marhali/easyi18n/dialog/SettingsDialog.java b/src/main/java/de/marhali/easyi18n/dialog/SettingsDialog.java index fc1eb92..b774b3d 100644 --- a/src/main/java/de/marhali/easyi18n/dialog/SettingsDialog.java +++ b/src/main/java/de/marhali/easyi18n/dialog/SettingsDialog.java @@ -26,7 +26,8 @@ public class SettingsDialog { private TextFieldWithBrowseButton pathText; private JBTextField filePatternText; - private JBTextField previewText; + private JBTextField previewLocaleText; + private JBTextField pathPrefixText; private JBCheckBox codeAssistanceCheckbox; public SettingsDialog(Project project) { @@ -37,22 +38,25 @@ public class SettingsDialog { String localesPath = SettingsService.getInstance(project).getState().getLocalesPath(); String filePattern = SettingsService.getInstance(project).getState().getFilePattern(); String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale(); + String pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix(); boolean codeAssistance = SettingsService.getInstance(project).getState().isCodeAssistance(); - if(prepare(localesPath, filePattern, previewLocale, codeAssistance).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes + if(prepare(localesPath, filePattern, previewLocale, pathPrefix, codeAssistance).show() == DialogWrapper.OK_EXIT_CODE) { // Save changes SettingsService.getInstance(project).getState().setLocalesPath(pathText.getText()); SettingsService.getInstance(project).getState().setFilePattern(filePatternText.getText()); - SettingsService.getInstance(project).getState().setPreviewLocale(previewText.getText()); + SettingsService.getInstance(project).getState().setPreviewLocale(previewLocaleText.getText()); SettingsService.getInstance(project).getState().setCodeAssistance(codeAssistanceCheckbox.isSelected()); + SettingsService.getInstance(project).getState().setPathPrefix(pathPrefixText.getText()); // Reload instance DataStore.getInstance(project).reloadFromDisk(); } } - private DialogBuilder prepare(String localesPath, String filePattern, String previewLocale, boolean codeAssistance) { + private DialogBuilder prepare(String localesPath, String filePattern, String previewLocale, String pathPrefix, boolean codeAssistance) { JPanel rootPanel = new JPanel(new GridLayout(0, 1, 2, 2)); + /* path */ JBLabel pathLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.text")); pathText = new TextFieldWithBrowseButton(new JTextField(localesPath)); @@ -63,20 +67,29 @@ public class SettingsDialog { rootPanel.add(pathLabel); rootPanel.add(pathText); + /* file pattern */ JBLabel filePatternLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.file-pattern")); filePatternText = new JBTextField(filePattern); rootPanel.add(filePatternLabel); rootPanel.add(filePatternText); + /* preview locale */ + JBLabel previewLocaleLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.preview")); + previewLocaleText = new JBTextField(previewLocale); + previewLocaleLabel.setLabelFor(previewLocaleText); - JBLabel previewLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.preview")); - previewText = new JBTextField(previewLocale); - previewLabel.setLabelFor(previewText); + rootPanel.add(previewLocaleLabel); + rootPanel.add(previewLocaleText); - rootPanel.add(previewLabel); - rootPanel.add(previewText); + /* path prefix */ + JBLabel pathPrefixLabel = new JBLabel(ResourceBundle.getBundle("messages").getString("settings.path.prefix")); + pathPrefixText = new JBTextField(pathPrefix); + rootPanel.add(pathPrefixLabel); + rootPanel.add(pathPrefixText); + + /* code assistance */ codeAssistanceCheckbox = new JBCheckBox(ResourceBundle.getBundle("messages").getString("settings.editor.assistance")); codeAssistanceCheckbox.setSelected(codeAssistance); diff --git a/src/main/java/de/marhali/easyi18n/editor/KeyAnnotator.java b/src/main/java/de/marhali/easyi18n/editor/KeyAnnotator.java index 3395133..baad8bb 100644 --- a/src/main/java/de/marhali/easyi18n/editor/KeyAnnotator.java +++ b/src/main/java/de/marhali/easyi18n/editor/KeyAnnotator.java @@ -29,7 +29,17 @@ public class KeyAnnotator { } String previewLocale = SettingsService.getInstance(project).getState().getPreviewLocale(); - LocalizedNode node = DataStore.getInstance(project).getTranslations().getNode(key); + String pathPrefix = SettingsService.getInstance(project).getState().getPathPrefix(); + + String searchKey = key.length() >= pathPrefix.length() + ? key.substring(pathPrefix.length()) + : key; + + if(searchKey.startsWith(".")) { + searchKey = searchKey.substring(1); + } + + LocalizedNode node = DataStore.getInstance(project).getTranslations().getNode(searchKey); if(node == null) { // Unknown translation. Just ignore it return; diff --git a/src/main/java/de/marhali/easyi18n/editor/KeyCompletionProvider.java b/src/main/java/de/marhali/easyi18n/editor/KeyCompletionProvider.java index 27633aa..a3a10b3 100644 --- a/src/main/java/de/marhali/easyi18n/editor/KeyCompletionProvider.java +++ b/src/main/java/de/marhali/easyi18n/editor/KeyCompletionProvider.java @@ -1,20 +1,16 @@ package de.marhali.easyi18n.editor; -import com.intellij.codeInsight.completion.CompletionParameters; -import com.intellij.codeInsight.completion.CompletionProvider; -import com.intellij.codeInsight.completion.CompletionResultSet; -import com.intellij.codeInsight.lookup.LookupElementBuilder; -import com.intellij.openapi.project.Project; -import com.intellij.util.ProcessingContext; - -import de.marhali.easyi18n.model.LocalizedNode; -import de.marhali.easyi18n.service.DataStore; -import de.marhali.easyi18n.service.SettingsService; +import com.intellij.codeInsight.completion.*; +import com.intellij.codeInsight.lookup.*; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.project.*; +import com.intellij.util.*; +import de.marhali.easyi18n.model.*; +import de.marhali.easyi18n.service.*; import de.marhali.easyi18n.util.TranslationsUtil; +import org.jetbrains.annotations.*; -import org.jetbrains.annotations.NotNull; - -import java.util.List; +import java.util.*; /** * I18n translation key completion provider. @@ -33,38 +29,58 @@ public class KeyCompletionProvider extends CompletionProvider sections = TranslationsUtil.getSections(query); - String lastSection = null; + String path = result.getPrefixMatcher().getPrefix(); - if(!sections.isEmpty() && !query.endsWith(".")) { - lastSection = sections.remove(sections.size() - 1); + if(pathPrefix == null) { + pathPrefix = ""; } - String path = TranslationsUtil.sectionsToFullPath(sections); + if(path.startsWith(pathPrefix)) { + path = path.substring(pathPrefix.length()); - LocalizedNode node = sections.isEmpty() ? DataStore.getInstance(project).getTranslations().getNodes() - : DataStore.getInstance(project).getTranslations().getNode(path); + if(path.startsWith(".")) { // Remove leading dot + path = path.substring(1); + } - if(node == null) { // Unknown translation - return; + } else { + path = ""; // Show suggestions for root view } - for(LocalizedNode children : node.getChildren()) { - if(lastSection == null || children.getKey().startsWith(lastSection)) { - // Construct full key path / Fore nested objects add '.' to indicate deeper level - String fullKey = (path.isEmpty() ? children.getKey() : path + "." + children.getKey()) + (children.isLeaf() ? "" : "."); + if(pathPrefix.length() > 0 && !pathPrefix.endsWith(".")) { + pathPrefix += "."; + } - result.addElement(LookupElementBuilder.create(fullKey) - .appendTailText(getTailText(children, previewLocale), true)); + List fullKeys = store.getTranslations().getFullKeys(); + + int sections = path.split("\\.").length; + int maxSectionForwardLookup = 5; + + for(String key : fullKeys) { + // Path matches + if(key.startsWith(path)) { + String[] keySections = key.split("\\."); + + if(keySections.length > sections + maxSectionForwardLookup) { // Key is too deep nested + String shrinkKey = TranslationsUtil.sectionsToFullPath(Arrays.asList( + Arrays.copyOf(keySections, sections + maxSectionForwardLookup))); + + result.addElement(LookupElementBuilder.create(pathPrefix + shrinkKey) + .appendTailText(" I18n([])", true)); + + } else { + LocalizedNode node = store.getTranslations().getNode(key); + String translation = node != null ? node.getValue().get(previewLocale) : null; + + result.addElement(LookupElementBuilder.create(pathPrefix + key) + .withIcon(AllIcons.Actions.PreserveCaseHover) + .appendTailText(" I18n(" + previewLocale + ": " + translation + ")", true) + ); + } } } } - - private String getTailText(LocalizedNode node, String previewLocale) { - return !node.isLeaf() ? " I18n([])" - : " I18n(" + previewLocale + ": " + node.getValue().get(previewLocale) + ")"; - } } diff --git a/src/main/java/de/marhali/easyi18n/editor/generic/GenericKeyCompletionContributor.java b/src/main/java/de/marhali/easyi18n/editor/generic/GenericKeyCompletionContributor.java index e84b7ec..a8ed832 100644 --- a/src/main/java/de/marhali/easyi18n/editor/generic/GenericKeyCompletionContributor.java +++ b/src/main/java/de/marhali/easyi18n/editor/generic/GenericKeyCompletionContributor.java @@ -3,7 +3,8 @@ package de.marhali.easyi18n.editor.generic; import com.intellij.codeInsight.completion.CompletionContributor; import com.intellij.codeInsight.completion.CompletionType; import com.intellij.patterns.*; -import com.intellij.psi.PsiLiteralValue; +import com.intellij.psi.*; +import com.intellij.psi.xml.*; import de.marhali.easyi18n.editor.KeyCompletionProvider; /** @@ -13,6 +14,10 @@ import de.marhali.easyi18n.editor.KeyCompletionProvider; public class GenericKeyCompletionContributor extends CompletionContributor { public GenericKeyCompletionContributor() { + extend(CompletionType.BASIC, PlatformPatterns.psiElement(PlainTextTokenTypes.PLAIN_TEXT), + new KeyCompletionProvider()); + extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(XmlElement.class), + new KeyCompletionProvider()); extend(CompletionType.BASIC, PlatformPatterns.psiElement().inside(PsiLiteralValue.class), new KeyCompletionProvider()); } diff --git a/src/main/java/de/marhali/easyi18n/io/implementation/YamlTranslatorIO.java b/src/main/java/de/marhali/easyi18n/io/implementation/YamlTranslatorIO.java new file mode 100644 index 0000000..4ed8e97 --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/io/implementation/YamlTranslatorIO.java @@ -0,0 +1,125 @@ +package de.marhali.easyi18n.io.implementation; + +import com.intellij.openapi.application.*; +import com.intellij.openapi.project.*; +import com.intellij.openapi.vfs.*; + +import de.marhali.easyi18n.io.*; +import de.marhali.easyi18n.model.*; +import de.marhali.easyi18n.util.*; +import de.marhali.easyi18n.util.array.YamlArrayUtil; + +import org.jetbrains.annotations.*; + +import thito.nodeflow.config.*; + +import java.io.*; +import java.nio.charset.*; +import java.util.*; +import java.util.function.*; + +public class YamlTranslatorIO implements TranslatorIO { + @Override + public void read(@NotNull Project project, @NotNull String directoryPath, @NotNull Consumer callback) { + ApplicationManager.getApplication().saveAll(); // Save opened files (required if new locales were added) + + ApplicationManager.getApplication().runReadAction(() -> { + VirtualFile directory = LocalFileSystem.getInstance().findFileByIoFile(new File(directoryPath)); + + if(directory == null || directory.getChildren() == null) { + throw new IllegalArgumentException("Specified folder is invalid (" + directoryPath + ")"); + } + + VirtualFile[] files = directory.getChildren(); + + List locales = new ArrayList<>(); + LocalizedNode nodes = new LocalizedNode(LocalizedNode.ROOT_KEY, new ArrayList<>()); + + try { + for(VirtualFile file : files) { + + if(!IOUtil.isFileRelevant(project, file)) { // File does not matches pattern + continue; + } + + locales.add(file.getNameWithoutExtension()); + + try (Reader reader = new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8)) { + Section section = Section.parseToMap(reader); + load(file.getNameWithoutExtension(), nodes, section); + } + } + + callback.accept(new Translations(locales, nodes)); + + } catch(IOException e) { + e.printStackTrace(); + callback.accept(null); + } + }); + } + + private void load(String locale, LocalizedNode node, Section section) { + if (section instanceof MapSection) { + for (String key : section.getKeys()) { + LocalizedNode child = node.getChildren(key); + if (child == null) { + node.addChildren(child = new LocalizedNode(key, new ArrayList<>())); + } + LocalizedNode finalChild = child; + MapSection map = section.getMap(key).orElse(null); + if (map != null) { + load(locale, finalChild, map); + } else { + + if(section.isList(key) && section.getList(key).isPresent()) { + child.getValue().put(locale, YamlArrayUtil.read(section.getList(key).get())); + } else { + String value = section.getString(key).orElse(null); + if (value != null) { + child.getValue().put(locale, value); + } + } + } + } + } + } + + private void save(LocalizedNode node, String locale, Section section, String path) { + if (node.isLeaf() && !node.getKey().equals(LocalizedNode.ROOT_KEY)) { + String value = node.getValue().get(locale); + if (value != null) { + section.set(path, YamlArrayUtil.isArray(value) ? YamlArrayUtil.write(value) : value); + } + } else { + for (LocalizedNode child : node.getChildren()) { + save(child, locale, section, path == null ? child.getKey() : path + "." + child.getKey()); + } + } + } + + @Override + public void save(@NotNull Project project, @NotNull Translations translations, @NotNull String directoryPath, @NotNull Consumer callback) { + ApplicationManager.getApplication().runWriteAction(() -> { + try { + for(String locale : translations.getLocales()) { + Section section = new MapSection(); + + save(translations.getNodes(), locale, section, null); + + String fullPath = directoryPath + "/" + locale + ".yml"; + VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(new File(fullPath)); + + file.setBinaryContent(Section.toString(section).getBytes(file.getCharset())); + } + + // Successfully saved + callback.accept(true); + + } catch(IOException e) { + e.printStackTrace(); + callback.accept(false); + } + }); + } +} diff --git a/src/main/java/de/marhali/easyi18n/model/SettingsState.java b/src/main/java/de/marhali/easyi18n/model/SettingsState.java index ba2dfb8..5119119 100644 --- a/src/main/java/de/marhali/easyi18n/model/SettingsState.java +++ b/src/main/java/de/marhali/easyi18n/model/SettingsState.java @@ -16,6 +16,7 @@ public class SettingsState { private String localesPath; private String filePattern; private String previewLocale; + private String pathPrefix; private Boolean codeAssistance; public SettingsState() {} @@ -51,4 +52,12 @@ public class SettingsState { public void setCodeAssistance(boolean codeAssistance) { this.codeAssistance = codeAssistance; } + + public void setPathPrefix(String pathPrefix) { + this.pathPrefix = pathPrefix; + } + + public String getPathPrefix() { + return pathPrefix; + } } \ No newline at end of file diff --git a/src/main/java/de/marhali/easyi18n/service/SettingsService.java b/src/main/java/de/marhali/easyi18n/service/SettingsService.java index 92a1cd1..59f81ad 100644 --- a/src/main/java/de/marhali/easyi18n/service/SettingsService.java +++ b/src/main/java/de/marhali/easyi18n/service/SettingsService.java @@ -4,7 +4,6 @@ import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.project.Project; -import com.intellij.util.xmlb.XmlSerializerUtil; import de.marhali.easyi18n.model.SettingsState; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/de/marhali/easyi18n/util/IOUtil.java b/src/main/java/de/marhali/easyi18n/util/IOUtil.java index 76bfa00..b5e81f1 100644 --- a/src/main/java/de/marhali/easyi18n/util/IOUtil.java +++ b/src/main/java/de/marhali/easyi18n/util/IOUtil.java @@ -3,9 +3,7 @@ package de.marhali.easyi18n.util; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; -import de.marhali.easyi18n.io.implementation.JsonTranslatorIO; -import de.marhali.easyi18n.io.implementation.ModularizedJsonTranslatorIO; -import de.marhali.easyi18n.io.implementation.PropertiesTranslatorIO; +import de.marhali.easyi18n.io.implementation.*; import de.marhali.easyi18n.io.TranslatorIO; import de.marhali.easyi18n.service.SettingsService; @@ -50,7 +48,8 @@ public class IOUtil { case "properties": return new PropertiesTranslatorIO(); - + case "yml": + return new YamlTranslatorIO(); default: throw new UnsupportedOperationException("Unsupported i18n locale file format: " + any.get().getFileType().getDefaultExtension()); diff --git a/src/main/java/de/marhali/easyi18n/util/JsonArrayUtil.java b/src/main/java/de/marhali/easyi18n/util/JsonArrayUtil.java deleted file mode 100644 index c1a73e0..0000000 --- a/src/main/java/de/marhali/easyi18n/util/JsonArrayUtil.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.marhali.easyi18n.util; - -import com.google.gson.JsonArray; -import org.apache.commons.lang.StringEscapeUtils; - -import java.util.regex.Pattern; - -/** - * Utility methods to read and write json arrays. - * @author marhali - */ -public class JsonArrayUtil { - - public static String ARRAY_PREFIX = "!arr["; - public static String ARRAY_SUFFIX = "]"; - public static char ARRAY_DELIMITER = ';'; - - public static String read(JsonArray array) { - StringBuilder builder = new StringBuilder(ARRAY_PREFIX); - - for(int i = 0; i < array.size(); i++) { - if(i > 0) { - builder.append(ARRAY_DELIMITER); - } - - String value = array.get(i).getAsString().replace(";", "\\;"); - builder.append(StringUtil.escapeControls(value, true)); - } - - builder.append(ARRAY_SUFFIX); - return builder.toString(); - } - - public static JsonArray write(String concat) { - concat = concat.substring(ARRAY_PREFIX.length(), concat.length() - ARRAY_SUFFIX.length()); - String regex = "(? String read(Iterator elements, Function stringFactory) { + StringBuilder builder = new StringBuilder(PREFIX); + + int i = 0; + while(elements.hasNext()) { + if(i > 0) { + builder.append(DELIMITER); + } + + String value = stringFactory.apply(elements.next()); + + builder.append(StringUtil.escapeControls( + value.replace(String.valueOf(DELIMITER), "\\" + DELIMITER), true)); + + i++; + } + + builder.append(SUFFIX); + return builder.toString(); + } + + static void write(String concat, Consumer writeElement) { + concat = concat.substring(PREFIX.length(), concat.length() - SUFFIX.length()); + + for(String element : concat.split(SPLITERATOR_REGEX)) { + element = element.replace("\\" + DELIMITER, String.valueOf(DELIMITER)); + writeElement.accept(StringEscapeUtils.unescapeJava(element)); + } + } + + public static boolean isArray(String concat) { + return concat != null && concat.startsWith(PREFIX) && concat.endsWith(SUFFIX); + } +} diff --git a/src/main/java/de/marhali/easyi18n/util/array/JsonArrayUtil.java b/src/main/java/de/marhali/easyi18n/util/array/JsonArrayUtil.java new file mode 100644 index 0000000..d61969c --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/util/array/JsonArrayUtil.java @@ -0,0 +1,20 @@ +package de.marhali.easyi18n.util.array; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +/** + * Utility methods to read and write json arrays. + * @author marhali + */ +public class JsonArrayUtil extends ArrayUtil { + 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; + } +} diff --git a/src/main/java/de/marhali/easyi18n/util/array/YamlArrayUtil.java b/src/main/java/de/marhali/easyi18n/util/array/YamlArrayUtil.java new file mode 100644 index 0000000..86f4db6 --- /dev/null +++ b/src/main/java/de/marhali/easyi18n/util/array/YamlArrayUtil.java @@ -0,0 +1,20 @@ +package de.marhali.easyi18n.util.array; + +import thito.nodeflow.config.ListSection; + +/** + * Utility methods to read and write yaml lists. + * @author marhali + */ +public class YamlArrayUtil extends ArrayUtil { + + public static String read(ListSection list) { + return read(list.iterator(), Object::toString); + } + + public static ListSection write(String concat) { + ListSection list = new ListSection(); + write(concat, list::add); + return list; + } +} \ No newline at end of file diff --git a/src/main/java/thito/nodeflow/config/ListSection.java b/src/main/java/thito/nodeflow/config/ListSection.java new file mode 100644 index 0000000..c5ae57e --- /dev/null +++ b/src/main/java/thito/nodeflow/config/ListSection.java @@ -0,0 +1,227 @@ +package thito.nodeflow.config; + +import java.util.*; +import java.util.stream.*; + +public class ListSection extends ArrayList implements Section { + private Section parent; + private String name; + + public ListSection() { + super(); + } + + public ListSection(Collection c) { + super(); + addAll(c); + } + + protected void setParent(Section parent, String name) { + this.parent = parent; + this.name = name; + } + + @Override + public String getName() { + if (name == null && parent != null) { + if (parent instanceof ListSection) { + return String.valueOf(((ListSection) parent).indexOf(this)); + } + if (parent instanceof MapSection) { + return ((MapSection) parent).entrySet().stream().filter(e -> Objects.equals(e.getValue(), this)) + .findAny().map(Map.Entry::getKey).orElse(null); + } + } + return name; + } + + public Optional getObject(int index) { + return index >= 0 && index < size() ? Optional.ofNullable(get(index)) : Optional.empty(); + } + + @Override + public Section getParent() { + return parent; + } + + @Override + public Set getKeys() { + return IntStream.range(0, size()).mapToObj(String::valueOf).collect(Collectors.toSet()); + } + + @Override + public Optional getInScope(String key) { + try { + return Optional.ofNullable(get(Integer.parseInt(key))); + } catch (Throwable t) { + } + return Optional.empty(); + } + + @Override + public void setInScope(String key, Object value) { + try { + set(Integer.parseInt(key), value); + } catch (Throwable t) { + } + } + + @Override + public Object set(int index, Object element) { + element = Section.wrap(element); + if (element instanceof Section) element = Section.wrapParent(this, null, (Section) element); + return super.set(index, element); + } + + @Override + public boolean add(Object o) { + o = Section.wrap(o); + if (o instanceof Section) o = Section.wrapParent(this, null, (Section) o); + return super.add(o); + } + + @Override + public void add(int index, Object element) { + element = Section.wrap(element); + if (element instanceof Section) element = Section.wrapParent(this, null, (Section) element); + super.add(index, element); + } + + @Override + public boolean addAll(Collection c) { + c.forEach(o -> add(o)); + return !c.isEmpty(); + } + + @Override + public boolean addAll(int index, Collection c) { + List wrapped = new ArrayList<>(); + c.forEach(obj -> { + Object o = Section.wrap(obj); + if (o instanceof Section) o = Section.wrapParent(this, null, (Section) o); + wrapped.add(o); + }); + return super.addAll(index, wrapped); + } + + @Override + public String toString() { + return Section.toString(this); + } + + public > Optional getEnum(int index, Class clz) { + return getObject(index).map(o -> { + try { + return Enum.valueOf(clz, String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + + public Optional getString(int index) { + return getObject(index).map(String::valueOf); + } + + public Optional getInteger(int index) { + return getObject(index).map(o -> { + try { + return Integer.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + + public Optional getDouble(int index) { + return getObject(index).map(o -> { + try { + return Double.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + + public Optional getLong(int index) { + return getObject(index).map(o -> { + try { + return Long.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + + public Optional getFloat(int index) { + return getObject(index).map(o -> { + try { + return Float.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + + public Optional getShort(int index) { + return getObject(index).map(o -> { + try { + return Short.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + + public Optional getByte(int index) { + return getObject(index).map(o -> { + try { + return Byte.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + + public Optional getCharacter(int index) { + return getObject(index).map(o -> { + String text = String.valueOf(o); + return text.isEmpty() ? null : text.charAt(0); + }); + } + + public Optional getBoolean(int index) { + return getObject(index).map(o -> { + String text = String.valueOf(o); + return text.equals("true") ? Boolean.TRUE : text.equals("false") ? Boolean.FALSE : null; + }); + } + + public Optional getMap(int index) { + return getObject(index).map(o -> { + if (o instanceof Map) { + if (o instanceof MapSection) return (MapSection) o; + MapSection mapSection = new MapSection((Map) o); + mapSection.setParent(this, null); + return mapSection; + } + return null; + }); + } + + public Optional getList(int index) { + return getObject(index).map(o -> { + if (o instanceof List) { + if (o instanceof ListSection) { + return (ListSection) o; + } + ListSection list = new ListSection((List) o); + list.setParent(this, null); + return list; + } + ListSection list = new ListSection(Collections.singleton(o)); + list.setParent(this, null); + return list; + }); + } +} diff --git a/src/main/java/thito/nodeflow/config/MapSection.java b/src/main/java/thito/nodeflow/config/MapSection.java new file mode 100644 index 0000000..4581c3f --- /dev/null +++ b/src/main/java/thito/nodeflow/config/MapSection.java @@ -0,0 +1,73 @@ +package thito.nodeflow.config; + +import java.util.*; + +public class MapSection extends HashMap implements Section { + private Section parent; + private String name; + + public MapSection() { + super(); + } + + public MapSection(Map m) { + super(); + m.forEach((key, value) -> put(String.valueOf(key), value)); + } + + protected void setParent(Section parent, String name) { + this.parent = parent; + this.name = name; + } + + @Override + public String getName() { + if (name == null && parent != null) { + if (parent instanceof ListSection) { + return String.valueOf(((ListSection) parent).indexOf(this)); + } + if (parent instanceof MapSection) { + return ((MapSection) parent).entrySet().stream().filter(e -> Objects.equals(e.getValue(), this)) + .findAny().map(Entry::getKey).orElse(null); + } + } + return name; + } + + @Override + public Section getParent() { + return parent; + } + + @Override + public void setInScope(String key, Object value) { + put(key, value); + } + + @Override + public Object put(String key, Object value) { + value = Section.wrap(value); + if (value instanceof Section) value = Section.wrapParent(this, key, (Section) value); + return super.put(key, value); + } + + @Override + public void putAll(Map m) { + m.forEach(this::put); + } + + @Override + public Set getKeys() { + return keySet(); + } + + @Override + public Optional getInScope(String key) { + return Optional.ofNullable(get(key)); + } + + @Override + public String toString() { + return Section.toString(this); + } +} diff --git a/src/main/java/thito/nodeflow/config/Section.java b/src/main/java/thito/nodeflow/config/Section.java new file mode 100644 index 0000000..2d9283e --- /dev/null +++ b/src/main/java/thito/nodeflow/config/Section.java @@ -0,0 +1,221 @@ +package thito.nodeflow.config; + +import org.yaml.snakeyaml.*; + +import java.io.*; +import java.util.*; +import java.util.regex.*; + +public interface Section { + String SEPARATOR = "."; + static Object wrap(Object o) { + if (o instanceof Section) return o; + if (o instanceof List) { + return new ListSection((List) o); + } + if (o instanceof Map) { + return new MapSection((Map) o); + } + return o; + } + static String getName(String path) { + String[] split = path.split(Pattern.quote(SEPARATOR)); + return split[split.length - 1]; + } + static Section wrapParent(Section parent, String name, Section current) { + if (current.getParent() != null && current.getParent() != parent) { + if (current instanceof MapSection) { + MapSection mapSection = new MapSection((MapSection) current); + mapSection.setParent(parent, name); + return mapSection; + } + if (current instanceof ListSection) { + ListSection objects = new ListSection((ListSection) current); + objects.setParent(parent, name); + return objects; + } + } else { + if (current instanceof MapSection) { + ((MapSection) current).setParent(parent, name); + } + if (current instanceof ListSection) { + ((ListSection) current).setParent(parent, name); + } + } + return current; + } + static String toString(Section section) { + DumperOptions options = new DumperOptions(); + options.setIndent(4); + options.setAllowUnicode(true); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + Yaml yaml = new Yaml(options); + return yaml.dumpAsMap(section); + } + static MapSection parseToMap(Reader reader) { + Yaml yaml = new Yaml(); + return new MapSection(yaml.loadAs(reader, Map.class)); + } + Set getKeys(); + default Set getPaths() { + Set paths = new HashSet<>(); + for (String k : getKeys()) { + Object lookup = getInScope(k).orElse(null); + if (lookup instanceof Section) { + for (String p : ((Section) lookup).getPaths()) { + paths.add(k + "." + p); + } + } + } + return paths; + } + String getName(); + default String getPath() { + StringBuilder path = new StringBuilder(getName()); + Section parent; + while ((parent = getParent()) != null) { + path.insert(0, parent.getName() + SEPARATOR); + } + return path.toString(); + } + Section getParent(); + Optional getInScope(String key); + void setInScope(String key, Object value); + default void set(String path, Object value) { + String[] paths = path.split(Pattern.quote(SEPARATOR)); + Object lookup = this; + for (int i = 0; i < paths.length - 1; i++) { + Section oldLookup = (Section) lookup; + lookup = oldLookup.getInScope(paths[i]).orElse(null); + if (!(lookup instanceof Section)) { + oldLookup.setInScope(paths[i], lookup = new MapSection()); + } + } + if (paths.length > 0) { + ((Section) lookup).setInScope(paths[paths.length - 1], value); + } + } + default Optional getObject(String path) { + String[] paths = path.split(Pattern.quote(SEPARATOR)); + Object lookup = this; + for (String s : paths) { + if (lookup instanceof Section) { + lookup = ((Section) lookup).getInScope(s).orElse(null); + } else { + return Optional.empty(); + } + } + return Optional.ofNullable(lookup); + } + default > Optional getEnum(String path, Class clz) { + return getObject(path).map(o -> { + try { + return Enum.valueOf(clz, String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + default Optional getString(String path) { + return getObject(path).map(String::valueOf); + } + default Optional getInteger(String path) { + return getObject(path).map(o -> { + try { + return Integer.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + default Optional getDouble(String path) { + return getObject(path).map(o -> { + try { + return Double.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + default Optional getLong(String path) { + return getObject(path).map(o -> { + try { + return Long.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + default Optional getFloat(String path) { + return getObject(path).map(o -> { + try { + return Float.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + default Optional getShort(String path) { + return getObject(path).map(o -> { + try { + return Short.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + default Optional getByte(String path) { + return getObject(path).map(o -> { + try { + return Byte.valueOf(String.valueOf(o)); + } catch (Throwable t) { + return null; + } + }); + } + default Optional getCharacter(String path) { + return getObject(path).map(o -> { + String text = String.valueOf(o); + return text.isEmpty() ? null : text.charAt(0); + }); + } + default Optional getBoolean(String path) { + return getObject(path).map(o -> { + String text = String.valueOf(o); + return text.equals("true") ? Boolean.TRUE : text.equals("false") ? Boolean.FALSE : null; + }); + } + default Optional getMap(String path) { + return getObject(path).map(o -> { + if (o instanceof Map) { + if (o instanceof MapSection) return (MapSection) o; + MapSection mapSection = new MapSection((Map) o); + mapSection.setParent(this, Section.getName(path)); + return mapSection; + } + return null; + }); + } + + default boolean isList(String path) { + Optional o = getObject(path); + return o.isPresent() && o.get() instanceof List; + } + + default Optional getList(String path) { + return getObject(path).map(o -> { + if (o instanceof List) { + if (o instanceof ListSection) { + return (ListSection) o; + } + ListSection list = new ListSection((List) o); + list.setParent(this, Section.getName(path)); + return list; + } + ListSection list = new ListSection(Collections.singleton(o)); + list.setParent(this, Section.getName(path)); + return list; + }); + } +} diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 727cd4f..2fee2ba 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -14,5 +14,6 @@ translation.locales=Locales settings.path.title=Locales Directory settings.path.text=Locales directory settings.path.file-pattern=Translation file pattern +settings.path.prefix=Path prefix settings.preview=Preview locale settings.editor.assistance=I18n key completion and annotation inside editor \ No newline at end of file