Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamic creation of semantic tags #3519

Merged
merged 1 commit into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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);
}
}
Loading