From e1e49d6f3632824dc5f2775b8d311787ab35858e Mon Sep 17 00:00:00 2001 From: jimtng <2554958+jimtng@users.noreply.github.com> Date: Fri, 14 Apr 2023 22:35:43 +1000 Subject: [PATCH] Add dynamic creation of semantic tags (#3519) Signed-off-by: Jimmy Tanagra GitOrigin-RevId: 4f2af88e73194d7b04747528fc4c13dd50f1b385 --- .../org.opensmarthouse.core.semantics/pom.xml | 6 + .../openhab/core/semantics/SemanticTags.java | 144 ++++++++++++++++++ .../semantics/model/equipment/Equipments.java | 4 + .../semantics/model/location/Locations.java | 4 + .../core/semantics/model/point/Points.java | 4 + .../semantics/model/property/Properties.java | 4 + .../core/semantics/SemanticTagsTest.java | 119 +++++++++++++++ .../openhab-core/src/main/feature/feature.xml | 1 + 8 files changed, 286 insertions(+) diff --git a/bundles/org.opensmarthouse.core.semantics/pom.xml b/bundles/org.opensmarthouse.core.semantics/pom.xml index 92a08fd1236..54962c3b240 100644 --- a/bundles/org.opensmarthouse.core.semantics/pom.xml +++ b/bundles/org.opensmarthouse.core.semantics/pom.xml @@ -20,6 +20,12 @@ org.openhab.core ${project.version} + + org.ow2.asm + asm + 9.2 + provided + diff --git a/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java b/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java index e0a8d5db7b2..a6087ad1e0b 100644 --- a/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java +++ b/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java @@ -26,6 +26,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; import org.openhab.core.items.Item; import org.openhab.core.semantics.model.equipment.Equipments; import org.openhab.core.semantics.model.location.Locations; @@ -33,12 +36,15 @@ import org.openhab.core.semantics.model.point.Points; import org.openhab.core.semantics.model.property.Properties; import org.openhab.core.types.StateDescription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * This is a class that gives static access to the semantic tag library. * For everything that is not static, the {@link SemanticsService} should be used instead. * * @author Kai Kreuzer - Initial contribution + * @author Jimmy Tanagra - Add the ability to add new tags at runtime */ @NonNullByDefault public class SemanticTags { @@ -47,6 +53,9 @@ public class SemanticTags { private static final Map> TAGS = new TreeMap<>(); + private static final Logger LOGGER = LoggerFactory.getLogger(SemanticTags.class); + private static final SemanticClassLoader CLASS_LOADER = new SemanticClassLoader(); + static { Locations.stream().forEach(location -> addTagSet(location)); Equipments.stream().forEach(equipment -> addTagSet(equipment)); @@ -203,6 +212,117 @@ public static String getLabel(Class tag, Locale locale) { return null; } + /** + * Adds a new semantic tag with inferred label, empty synonyms and description. + * + * The label will be inferred from the tag name by splitting the CamelCase with a space. + * + * @param name the tag name to add + * @param parent the parent tag that the new tag should belong to + * @return the created semantic tag class, or null if it was already added. + */ + public static @Nullable Class add(String name, String parent) { + return add(name, parent, null, null, null); + } + + /** + * Adds a new semantic tag. + * + * @param name the tag name to add + * @param parent the parent tag that the new tag should belong to + * @param label an optional label. When null, the label will be inferred from the tag name, + * splitting the CamelCase with a space. + * @param synonyms a comma separated list of synonyms + * @param description the tag description + * @return the created semantic tag class, or null if it was already added. + */ + public static @Nullable Class add(String name, String parent, @Nullable String label, + @Nullable String synonyms, @Nullable String description) { + Class parentClass = getById(parent); + if (parentClass == null) { + LOGGER.warn("Adding semantic tag '{}' failed because parent tag '{}' is not found.", name, parent); + return null; + } + return add(name, parentClass, label, synonyms, description); + } + + /** + * Adds a new semantic tag with inferred label, empty synonyms and description. + * + * The label will be inferred from the tag name by splitting the CamelCase with a space. + * + * @param name the tag name to add + * @param parent the parent tag that the new tag should belong to + * @return the created semantic tag class, or null if it was already added. + */ + public static @Nullable Class add(String name, Class parent) { + return add(name, parent, null, null, null); + } + + /** + * Adds a new semantic tag. + * + * @param name the tag name to add + * @param parent the parent tag that the new tag should belong to + * @param label an optional label. When null, the label will be inferred from the tag name, + * splitting the CamelCase with a space. + * @param synonyms a comma separated list of synonyms + * @param description the tag description + * @return the created semantic tag class, or null if it was already added. + */ + public static @Nullable Class add(String name, Class parent, @Nullable String label, + @Nullable String synonyms, @Nullable String description) { + if (getById(name) != null) { + return null; + } + + if (!name.matches("[A-Z][a-zA-Z0-9]+")) { + throw new IllegalArgumentException( + "The tag name '" + name + "' must start with a capital letter and contain only alphanumerics."); + } + + String parentId = parent.getAnnotation(TagInfo.class).id(); + String type = parentId.split("_")[0]; + String className = "org.openhab.core.semantics.model." + type.toLowerCase() + "." + name; + + // Infer label from name, splitting up CamelCaseALL99 -> Camel Case ALL 99 + label = Optional.ofNullable(label).orElseGet(() -> name.replaceAll("([A-Z][a-z]+|[A-Z][A-Z]+|[0-9]+)", " $1")) + .trim(); + synonyms = Optional.ofNullable(synonyms).orElse("").replaceAll("\\s*,\\s*", ",").trim(); + + // Create the tag interface + ClassWriter classWriter = new ClassWriter(0); + classWriter.visit(Opcodes.V11, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE, + className.replace('.', '/'), null, "java/lang/Object", + new String[] { parent.getName().replace('.', '/') }); + + // Add TagInfo Annotation + classWriter.visitSource("Status.java", null); + + AnnotationVisitor annotation = classWriter.visitAnnotation("Lorg/openhab/core/semantics/TagInfo;", true); + annotation.visit("id", parentId + "_" + name); + annotation.visit("label", label); + annotation.visit("synonyms", synonyms); + annotation.visit("description", Optional.ofNullable(description).orElse("").trim()); + annotation.visitEnd(); + + classWriter.visitEnd(); + byte[] byteCode = classWriter.toByteArray(); + Class newTag = null; + try { + newTag = CLASS_LOADER.defineClass(className, byteCode); + } catch (Exception e) { + LOGGER.warn("Failed creating a new semantic tag '{}': {}", className, e.getMessage()); + return null; + } + addToModel(newTag); + addTagSet(newTag); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("'{}' semantic {} tag added.", className, type); + } + return newTag; + } + private static void addTagSet(Class tagSet) { String id = tagSet.getAnnotation(TagInfo.class).id(); while (id.indexOf("_") != -1) { @@ -211,4 +331,28 @@ private static void addTagSet(Class tagSet) { } TAGS.put(id, tagSet); } + + private static boolean addToModel(Class tag) { + if (Location.class.isAssignableFrom(tag)) { + return Locations.add((Class) tag); + } else if (Equipment.class.isAssignableFrom(tag)) { + return Equipments.add((Class) tag); + } else if (Point.class.isAssignableFrom(tag)) { + return Points.add((Class) tag); + } else if (Property.class.isAssignableFrom(tag)) { + return Properties.add((Class) tag); + } + throw new IllegalArgumentException("Unknown type of tag " + tag); + } + + private static class SemanticClassLoader extends ClassLoader { + public SemanticClassLoader() { + super(SemanticTags.class.getClassLoader()); + } + + public Class defineClass(String className, byte[] byteCode) { + // defineClass is protected in the normal ClassLoader + return defineClass(className, byteCode, 0, byteCode.length); + } + } } diff --git a/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java b/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java index 30b6bbe0836..d99172b88c6 100644 --- a/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java +++ b/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/equipment/Equipments.java @@ -89,4 +89,8 @@ public class Equipments { public static Stream> stream() { return EQUIPMENTS.stream(); } + + public static boolean add(Class tag) { + return EQUIPMENTS.add(tag); + } } diff --git a/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java b/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java index df755f38251..6c5063dfb1d 100644 --- a/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java +++ b/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/location/Locations.java @@ -72,4 +72,8 @@ public class Locations { public static Stream> stream() { return LOCATIONS.stream(); } + + public static boolean add(Class tag) { + return LOCATIONS.add(tag); + } } diff --git a/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java b/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java index 44f78811272..d7950695f1e 100644 --- a/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java +++ b/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/point/Points.java @@ -47,4 +47,8 @@ public class Points { public static Stream> stream() { return POINTS.stream(); } + + public static boolean add(Class tag) { + return POINTS.add(tag); + } } diff --git a/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java b/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java index 7a747b23830..06de5ebba85 100644 --- a/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java +++ b/bundles/org.opensmarthouse.core.semantics/src/main/java/org/openhab/core/semantics/model/property/Properties.java @@ -63,4 +63,8 @@ public class Properties { public static Stream> stream() { return PROPERTIES.stream(); } + + public static boolean add(Class tag) { + return PROPERTIES.add(tag); + } } diff --git a/bundles/org.opensmarthouse.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java b/bundles/org.opensmarthouse.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java index 51bc065197e..c6c37d81834 100644 --- a/bundles/org.opensmarthouse.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java +++ b/bundles/org.opensmarthouse.core.semantics/src/test/java/org/openhab/core/semantics/SemanticTagsTest.java @@ -23,10 +23,14 @@ import org.openhab.core.items.GroupItem; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.semantics.model.equipment.CleaningRobot; +import org.openhab.core.semantics.model.equipment.Equipments; import org.openhab.core.semantics.model.location.Bathroom; import org.openhab.core.semantics.model.location.Kitchen; +import org.openhab.core.semantics.model.location.Locations; import org.openhab.core.semantics.model.location.Room; import org.openhab.core.semantics.model.point.Measurement; +import org.openhab.core.semantics.model.point.Points; +import org.openhab.core.semantics.model.property.Properties; import org.openhab.core.semantics.model.property.Temperature; /** @@ -105,4 +109,119 @@ public void testGetPoint() { public void testGetProperty() { assertEquals(Temperature.class, SemanticTags.getProperty(pointItem)); } + + @Test + public void testAddLocation() { + String tagName = "CustomLocation"; + Class customTag = SemanticTags.add(tagName, Location.class); + assertNotNull(customTag); + assertEquals(customTag, SemanticTags.getById(tagName)); + assertEquals(customTag, SemanticTags.getByLabel("Custom Location", Locale.getDefault())); + assertTrue(Locations.stream().toList().contains(customTag)); + + GroupItem myItem = new GroupItem("MyLocation"); + myItem.addTag(tagName); + + assertEquals(customTag, SemanticTags.getLocation(myItem)); + } + + @Test + public void testAddLocationWithParentString() { + String tagName = "CustomLocationParentString"; + Class customTag = SemanticTags.add(tagName, "Location"); + assertNotNull(customTag); + assertTrue(Locations.stream().toList().contains(customTag)); + } + + @Test + public void testAddEquipment() { + String tagName = "CustomEquipment"; + Class customTag = SemanticTags.add(tagName, Equipment.class); + assertNotNull(customTag); + assertEquals(customTag, SemanticTags.getById(tagName)); + assertEquals(customTag, SemanticTags.getByLabel("Custom Equipment", Locale.getDefault())); + assertTrue(Equipments.stream().toList().contains(customTag)); + + GroupItem myItem = new GroupItem("MyEquipment"); + myItem.addTag(tagName); + + assertEquals(customTag, SemanticTags.getEquipment(myItem)); + } + + @Test + public void testAddEquipmentWithParentString() { + String tagName = "CustomEquipmentParentString"; + Class customTag = SemanticTags.add(tagName, "Television"); + assertNotNull(customTag); + assertTrue(Equipments.stream().toList().contains(customTag)); + } + + @Test + public void testAddPoint() { + String tagName = "CustomPoint"; + Class customTag = SemanticTags.add(tagName, Point.class); + assertNotNull(customTag); + assertEquals(customTag, SemanticTags.getById(tagName)); + assertEquals(customTag, SemanticTags.getByLabel("Custom Point", Locale.getDefault())); + assertTrue(Points.stream().toList().contains(customTag)); + + GroupItem myItem = new GroupItem("MyItem"); + myItem.addTag(tagName); + + assertEquals(customTag, SemanticTags.getPoint(myItem)); + } + + @Test + public void testAddPointParentString() { + String tagName = "CustomPointParentString"; + Class customTag = SemanticTags.add(tagName, "Control"); + assertNotNull(customTag); + assertTrue(Points.stream().toList().contains(customTag)); + } + + @Test + public void testAddProperty() { + String tagName = "CustomProperty"; + Class customTag = SemanticTags.add(tagName, Property.class); + assertNotNull(customTag); + assertEquals(customTag, SemanticTags.getById(tagName)); + assertEquals(customTag, SemanticTags.getByLabel("Custom Property", Locale.getDefault())); + assertTrue(Properties.stream().toList().contains(customTag)); + + GroupItem myItem = new GroupItem("MyItem"); + myItem.addTag(tagName); + + assertEquals(customTag, SemanticTags.getProperty(myItem)); + } + + @Test + public void testAddPropertyParentString() { + String tagName = "CustomPropertyParentString"; + Class customTag = SemanticTags.add(tagName, "Property"); + assertNotNull(customTag); + assertTrue(Properties.stream().toList().contains(customTag)); + } + + @Test + public void testAddingExistingTagShouldFail() { + assertNull(SemanticTags.add("Room", Location.class)); + + assertNotNull(SemanticTags.add("CustomLocation1", Location.class)); + assertNull(SemanticTags.add("CustomLocation1", Location.class)); + } + + @Test + public void testAddWithCustomLabel() { + Class tag = SemanticTags.add("CustomProperty2", Property.class, " Custom Label ", null, null); + assertEquals(tag, SemanticTags.getByLabel("Custom Label", Locale.getDefault())); + } + + @Test + public void testAddWithSynonyms() { + String synonyms = " Synonym1, Synonym2 , Synonym With Space "; + Class tag = SemanticTags.add("CustomProperty3", Property.class, null, synonyms, null); + assertEquals(tag, SemanticTags.getByLabelOrSynonym("Synonym1", Locale.getDefault()).get(0)); + assertEquals(tag, SemanticTags.getByLabelOrSynonym("Synonym2", Locale.getDefault()).get(0)); + assertEquals(tag, SemanticTags.getByLabelOrSynonym("Synonym With Space", Locale.getDefault()).get(0)); + } } diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 1b3d69887ef..5fe065535e4 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -54,6 +54,7 @@ mvn:org.openhab.core.bundles/org.openhab.core.id/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.persistence/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.semantics/${project.version} + openhab.tp-asm mvn:org.openhab.core.bundles/org.openhab.core.thing/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.transform/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.audio/${project.version}