Skip to content

Commit

Permalink
Add dynamic creation of semantic tags
Browse files Browse the repository at this point in the history
Signed-off-by: Jimmy Tanagra <[email protected]>
  • Loading branch information
jimtng committed Apr 3, 2023
1 parent 41ff466 commit aa65b64
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 0 deletions.
16 changes: 16 additions & 0 deletions bundles/org.openhab.core.semantics/model/generateTagClasses.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ 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);
}
}
""")
file.close()
Expand Down Expand Up @@ -172,6 +176,10 @@ 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);
}
}
""")
file.close()
Expand Down Expand Up @@ -210,6 +218,10 @@ 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);
}
}
""")
file.close()
Expand Down Expand Up @@ -248,6 +260,10 @@ 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);
}
}
""")
file.close()
Expand Down
6 changes: 6 additions & 0 deletions bundles/org.openhab.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,24 @@ 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 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);
}
}
Loading

0 comments on commit aa65b64

Please sign in to comment.