Skip to content

Commit

Permalink
[i18n] Allow generation of default translations for automation modules (
Browse files Browse the repository at this point in the history
#2966)

* Allow generation of default translations for automation modules

Signed-off-by: Jan N. Klug <github@klug.nrw>
  • Loading branch information
J-N-K authored May 29, 2022
1 parent 344a9ae commit d6c269d
Show file tree
Hide file tree
Showing 13 changed files with 372 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ private String inferModuleTypeKey(String moduleTypeUID, String lastSegment) {
}

private String inferInputKey(String moduleTypeUID, String inputName, String lastSegment) {
return MODULE_TYPE + ".input." + moduleTypeUID + ".name." + inputName + "." + lastSegment;
return MODULE_TYPE + "." + moduleTypeUID + ".input." + inputName + "." + lastSegment;
}

private String inferOutputKey(String moduleTypeUID, String outputName, String lastSegment) {
return MODULE_TYPE + ".output." + moduleTypeUID + ".name." + outputName + "." + lastSegment;
return MODULE_TYPE + "." + moduleTypeUID + ".output." + outputName + "." + lastSegment;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
{
"name": "operator",
"type": "TEXT",
"label": "Operator",
"description": "the compare operator, allowed are <,>,=,!=,>=,<= matches",
"required": true,
"default": "="
Expand Down
5 changes: 5 additions & 0 deletions tools/i18n-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
<artifactId>org.openhab.core.thing.xml</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.openhab.core.thing.xml.internal.ChannelTypeXmlResult;
import org.openhab.core.thing.xml.internal.ThingTypeXmlResult;

import com.google.gson.JsonObject;

/**
* The bundle information provided by the openHAB XML files in the <code>OH-INF</code> directory.
*
Expand All @@ -39,6 +41,8 @@ public class BundleInfo {
private List<ChannelGroupTypeXmlResult> channelGroupTypesXml = new ArrayList<>(5);
private List<ChannelTypeXmlResult> channelTypesXml = new ArrayList<>(5);
private List<ThingTypeXmlResult> thingTypesXml = new ArrayList<>(5);
private List<JsonObject> moduleTypesJson = new ArrayList<>(5);
private List<JsonObject> ruleTemplateJson = new ArrayList<>(5);

public String getBindingId() {
return bindingId;
Expand Down Expand Up @@ -80,6 +84,22 @@ public void setChannelTypesXml(List<ChannelTypeXmlResult> channelTypesXml) {
this.channelTypesXml = channelTypesXml;
}

public List<JsonObject> getModuleTypesJson() {
return moduleTypesJson;
}

public void setModuleTypesJson(List<JsonObject> moduleTypesJson) {
this.moduleTypesJson = moduleTypesJson;
}

public List<JsonObject> getRuleTemplateJson() {
return ruleTemplateJson;
}

public void setRuleTemplateJson(List<JsonObject> ruleTemplateJson) {
this.ruleTemplateJson = ruleTemplateJson;
}

public List<ThingTypeXmlResult> getThingTypesXml() {
return thingTypesXml;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.apache.maven.plugin.logging.Log;
import org.eclipse.jdt.annotation.NonNullByDefault;
Expand All @@ -30,6 +34,10 @@
import org.openhab.core.thing.xml.internal.ThingDescriptionReader;
import org.openhab.core.thing.xml.internal.ThingTypeXmlResult;

import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.thoughtworks.xstream.converters.ConversionException;

/**
Expand All @@ -42,6 +50,8 @@ public class BundleInfoReader {

private final Log log;

private final Predicate<Path> isJsonFile = path -> Files.isRegularFile(path) && path.toString().endsWith(".json");

public BundleInfoReader(Log log) {
this.log = log;
}
Expand All @@ -51,6 +61,8 @@ public BundleInfo readBundleInfo(Path ohinfPath) throws IOException {
readBindingInfo(ohinfPath, bundleInfo);
readConfigInfo(ohinfPath, bundleInfo);
readThingInfo(ohinfPath, bundleInfo);
readModuleTypeInfo(ohinfPath, bundleInfo);
readRuleTemplateInfo(ohinfPath, bundleInfo);
return bundleInfo;
}

Expand Down Expand Up @@ -129,4 +141,43 @@ private void readThingInfo(Path ohinfPath, BundleInfo bundleInfo) throws IOExcep
}
});
}

private void readModuleTypeInfo(Path ohinfPath, BundleInfo bundleInfo) throws IOException {
Path modulePath = ohinfPath.resolve("automation").resolve("moduletypes");
if (Files.exists(modulePath)) {
try (Stream<Path> files = Files.walk(modulePath)) {
List<JsonObject> moduleTypes = files.filter(isJsonFile).flatMap(this::readJsonElementsFromFile)
.map(JsonElement::getAsJsonObject).collect(Collectors.toList());
if (!moduleTypes.isEmpty()) {
bundleInfo.setModuleTypesJson(moduleTypes);
}
}
}
}

private void readRuleTemplateInfo(Path ohinfPath, BundleInfo bundleInfo) throws IOException {
Path template = ohinfPath.resolve("automation").resolve("templates");
if (Files.exists(template)) {
try (Stream<Path> files = Files.walk(template)) {
List<JsonObject> ruleTemplates = files.filter(isJsonFile).flatMap(this::readJsonElementsFromFile)
.map(JsonElement::getAsJsonObject).collect(Collectors.toList());
if (!ruleTemplates.isEmpty()) {
bundleInfo.setRuleTemplateJson(ruleTemplates);
}
}
}
}

private Stream<JsonElement> readJsonElementsFromFile(Path path) {
try {
JsonElement element = JsonParser.parseString(Files.readString(path));
if (element.isJsonObject()) {
return element.getAsJsonObject().entrySet().stream().map(Map.Entry::getValue)
.filter(JsonElement::isJsonArray)
.flatMap(a -> StreamSupport.stream(a.getAsJsonArray().spliterator(), false));
}
} catch (IOException ignored) {
}
return Stream.of(JsonNull.INSTANCE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ protected String generateDefaultTranslations(Path defaultTranslationsPath) {
XmlToTranslationsConverter xmlConverter = new XmlToTranslationsConverter();
Translations generatedTranslations = xmlConverter.convert(bundleInfo);

JsonToTranslationsConverter jsonConverter = new JsonToTranslationsConverter();
jsonConverter.convert(bundleInfo).sections.forEach(generatedTranslations::addSection);

PropertiesToTranslationsConverter propertiesConverter = new PropertiesToTranslationsConverter(getLog());
Translations existingTranslations = propertiesConverter.convert(defaultTranslationsPath);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.tools.i18n.plugin;

import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsEntry.entry;
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsGroup.group;

import java.util.Comparator;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.Stream.Builder;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsEntry;
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsGroup;
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsSection;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

/**
* Converts JSON based {@link BundleInfo} to {@link Translations}.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class JsonToTranslationsConverter {

private static final Pattern OPTION_ESCAPE_PATTERN = Pattern.compile("([ :=])");

public Translations convert(BundleInfo bundleInfo) {
return new Translations(Stream.of( //
moduleTypeSection(bundleInfo), ruleTemplateSection(bundleInfo)//
).flatMap(Function.identity()).collect(Collectors.toList()));
}

private Stream<TranslationsSection> moduleTypeSection(BundleInfo bundleInfo) {
Builder<TranslationsSection> sectionBuilder = Stream.builder();

bundleInfo.getModuleTypesJson().stream().flatMap(this::getModuleType).forEach(sectionBuilder::add);
return sectionBuilder.build().sorted(Comparator.comparing(s -> s.header));
}

private Stream<TranslationsSection> ruleTemplateSection(BundleInfo bundleInfo) {
Builder<TranslationsSection> sectionBuilder = Stream.builder();

bundleInfo.getRuleTemplateJson().stream().flatMap(this::getRuleTemplate).forEach(sectionBuilder::add);
return sectionBuilder.build().sorted(Comparator.comparing(s -> s.header));
}

private Stream<TranslationsSection> getModuleType(JsonObject moduleType) {
String uid = getAsString(moduleType, "uid").orElse("");
if (uid.isBlank()) {
return Stream.of();
}

String globalPrefix = "module-type." + uid + ".";
Builder<TranslationsGroup> groupBuilder = Stream.builder();

// global entries
Builder<TranslationsEntry> entriesBuilder = Stream.builder();
getAsString(moduleType, "label").ifPresent(label -> entriesBuilder.add(entry(globalPrefix + "label", label)));
getAsString(moduleType, "description")
.ifPresent(description -> entriesBuilder.add(entry(globalPrefix + "description", description)));
groupBuilder.add(group(entriesBuilder.build()));

// configDescriptions
JsonElement configDescriptionsElement = moduleType.get("configDescriptions");
if (configDescriptionsElement != null) {
String prefix = globalPrefix + "config.";
groupBuilder.add(getGroupFromArray(configDescriptionsElement, prefix));
}

// inputs
JsonElement inputsElement = moduleType.get("inputs");
if (inputsElement != null) {
String prefix = globalPrefix + "input.";
groupBuilder.add(getGroupFromArray(inputsElement, prefix));
}

// outputs
JsonElement outputsElement = moduleType.get("outputs");
if (outputsElement != null) {
String prefix = globalPrefix + "output.";
groupBuilder.add(getGroupFromArray(outputsElement, prefix));
}

return Stream.of(new TranslationsSection(uid, groupBuilder.build().collect(Collectors.toList())));
}

private Stream<TranslationsSection> getRuleTemplate(JsonObject ruleTemplate) {
String uid = getAsString(ruleTemplate, "uid").orElse("");
if (uid.isBlank()) {
return Stream.of();
}

String globalPrefix = "rule-template." + uid + ".";
Builder<TranslationsGroup> groupBuilder = Stream.builder();

// global entries
Builder<TranslationsEntry> entriesBuilder = Stream.builder();
getAsString(ruleTemplate, "label").ifPresent(label -> entriesBuilder.add(entry(globalPrefix + "label", label)));
getAsString(ruleTemplate, "description")
.ifPresent(description -> entriesBuilder.add(entry(globalPrefix + "description", description)));
groupBuilder.add(group(entriesBuilder.build()));

return Stream.of(new TranslationsSection(uid, groupBuilder.build().collect(Collectors.toList())));
}

private TranslationsGroup getGroupFromArray(JsonElement parentElement, String prefix) {
Builder<TranslationsEntry> entriesBuilder = Stream.builder();
for (JsonElement jsonElement : parentElement.getAsJsonArray()) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
getAsString(jsonObject, "name").ifPresent(name -> {
String namePrefix = prefix + name + ".";
getAsString(jsonObject, "label")
.ifPresent(label -> entriesBuilder.add(entry(namePrefix + "label", label)));
getAsString(jsonObject, "description")
.ifPresent(description -> entriesBuilder.add(entry(namePrefix + "description", description)));
JsonElement optionsElement = jsonObject.get("options");
if (optionsElement != null) {
for (JsonElement optionElement : optionsElement.getAsJsonArray()) {
JsonObject optionObject = optionElement.getAsJsonObject();
getAsString(optionObject, "value")
.ifPresent(value -> getAsString(optionObject, "label").ifPresent(label -> {
String optionKey = namePrefix + "option."
+ OPTION_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$1");
entriesBuilder.add(entry(optionKey, label));
}));
}
}
});
}
return group(entriesBuilder.build());
}

private Optional<String> getAsString(JsonObject object, String key) {
JsonElement element = object.get(key);
return element == null ? Optional.empty() : Optional.of(element.getAsString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public void readBindingInfo() throws IOException {
assertThat(bundleInfo.getChannelTypesXml().size(), is(2));
assertThat(bundleInfo.getConfigDescriptions().size(), is(1));
assertThat(bundleInfo.getThingTypesXml().size(), is(2));
assertThat(bundleInfo.getModuleTypesJson().size(), is(2));
assertThat(bundleInfo.getRuleTemplateJson().size(), is(0));
}

@Test
Expand All @@ -62,6 +64,8 @@ public void readGenericBundleInfo() throws IOException {
assertThat(bundleInfo.getChannelTypesXml().size(), is(0));
assertThat(bundleInfo.getConfigDescriptions().size(), is(1));
assertThat(bundleInfo.getThingTypesXml().size(), is(0));
assertThat(bundleInfo.getModuleTypesJson().size(), is(0));
assertThat(bundleInfo.getRuleTemplateJson().size(), is(0));
}

@Test
Expand All @@ -75,5 +79,7 @@ public void readPathWithoutAnyInfo() throws IOException {
assertThat(bundleInfo.getChannelTypesXml().size(), is(0));
assertThat(bundleInfo.getConfigDescriptions().size(), is(0));
assertThat(bundleInfo.getThingTypesXml().size(), is(0));
assertThat(bundleInfo.getModuleTypesJson().size(), is(0));
assertThat(bundleInfo.getRuleTemplateJson().size(), is(0));
}
}
Loading

0 comments on commit d6c269d

Please sign in to comment.