Add dynamic creation of semantic tags (#3519)
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>pull/3553/head
parent
9610404ceb
commit
4f2af88e73
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -26,6 +26,9 @@ import java.util.stream.Stream;
|
|||
|
||||
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.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 {
|
||||
|
@ -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));
|
||||
|
@ -203,6 +212,117 @@ public class SemanticTags {
|
|||
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) {
|
||||
|
@ -211,4 +331,28 @@ public class SemanticTags {
|
|||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,10 +23,14 @@ import org.openhab.core.items.GenericItem;
|
|||
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 class SemanticTagsTest {
|
|||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue