Skip to content

Commit

Permalink
Add dynamic creation of semantic tags (openhab#3519)
Browse files Browse the repository at this point in the history
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
GitOrigin-RevId: 4f2af88
  • Loading branch information
jimtng authored and splatch committed Jul 12, 2023
1 parent e44b96f commit e1e49d6
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 0 deletions.
6 changes: 6 additions & 0 deletions bundles/org.opensmarthouse.core.semantics/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
<artifactId>org.openhab.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
<scope>provided</scope>
</dependency>
</dependencies>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,25 @@

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;
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.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 {
Expand All @@ -47,6 +53,9 @@ public class SemanticTags {

private static final Map<String, Class<? extends Tag>> 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));
Expand Down Expand Up @@ -203,6 +212,117 @@ public static String getLabel(Class<? extends Tag> 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<? extends Tag> 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<? extends Tag> add(String name, String parent, @Nullable String label,
@Nullable String synonyms, @Nullable String description) {
Class<? extends Tag> 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<? extends Tag> add(String name, Class<? extends Tag> 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<? extends Tag> add(String name, Class<? extends Tag> 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<? extends Tag> tagSet) {
String id = tagSet.getAnnotation(TagInfo.class).id();
while (id.indexOf("_") != -1) {
Expand All @@ -211,4 +331,28 @@ private static void addTagSet(Class<? extends Tag> tagSet) {
}
TAGS.put(id, tagSet);
}

private static boolean addToModel(Class<? extends Tag> tag) {
if (Location.class.isAssignableFrom(tag)) {
return Locations.add((Class<? extends Location>) tag);
} else if (Equipment.class.isAssignableFrom(tag)) {
return Equipments.add((Class<? extends Equipment>) tag);
} else if (Point.class.isAssignableFrom(tag)) {
return Points.add((Class<? extends Point>) tag);
} else if (Property.class.isAssignableFrom(tag)) {
return Properties.add((Class<? extends Property>) 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,8 @@ public class Equipments {
public static Stream<Class<? extends Equipment>> stream() {
return EQUIPMENTS.stream();
}

public static boolean add(Class<? extends Equipment> tag) {
return EQUIPMENTS.add(tag);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,8 @@ public class Locations {
public static Stream<Class<? extends Location>> stream() {
return LOCATIONS.stream();
}

public static boolean add(Class<? extends Location> tag) {
return LOCATIONS.add(tag);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ public class Points {
public static Stream<Class<? extends Point>> stream() {
return POINTS.stream();
}

public static boolean add(Class<? extends Point> tag) {
return POINTS.add(tag);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,8 @@ public class Properties {
public static Stream<Class<? extends Property>> stream() {
return PROPERTIES.stream();
}

public static boolean add(Class<? extends Property> tag) {
return PROPERTIES.add(tag);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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));
}
}
1 change: 1 addition & 0 deletions features/karaf/openhab-core/src/main/feature/feature.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.id/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.persistence/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.semantics/${project.version}</bundle>
<feature dependency="true">openhab.tp-asm</feature>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.thing/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.transform/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.audio/${project.version}</bundle>
Expand Down

0 comments on commit e1e49d6

Please sign in to comment.