diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java index c166b489d3d..85ee87254ba 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/SemanticTags.java @@ -81,6 +81,44 @@ public class SemanticTags { } } + /** + * Determines the semantic root of a given tag type. + * + * @param type the tag type + * @return the semantic root of the tag type, or null if the type is not a semantic tag + */ + public static @Nullable Class getSemanticRoot(Class type) { + if (type == null) { + return null; + } + if (Point.class.isAssignableFrom(type)) { + return Point.class; + } else if (Property.class.isAssignableFrom(type)) { + return Property.class; + } else if (Location.class.isAssignableFrom(type)) { + return Location.class; + } else if (Equipment.class.isAssignableFrom(type)) { + return Equipment.class; + } else { + return null; + } + } + + /** + * Determines the name of the semantic root of a given tag type. + * + * @param type the tag type + * @return the name of the semantic root of the tag type, or an empty string if the type is not a semantic tag + */ + public static String getSemanticRootName(Class type) { + Class semanticRoot = getSemanticRoot(type); + if (semanticRoot != null) { + return semanticRoot.getSimpleName(); + } else { + return ""; + } + } + /** * Determines the {@link Property} type that a {@link Point} relates to. * diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java index 8af05e64794..d45a98f4d06 100644 --- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java +++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsServiceImpl.java @@ -17,6 +17,7 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @@ -25,8 +26,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemPredicates; import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.Metadata; @@ -35,13 +38,16 @@ import org.openhab.core.semantics.Equipment; import org.openhab.core.semantics.Location; import org.openhab.core.semantics.Point; +import org.openhab.core.semantics.Property; import org.openhab.core.semantics.SemanticTag; import org.openhab.core.semantics.SemanticTagRegistry; +import org.openhab.core.semantics.SemanticTags; import org.openhab.core.semantics.SemanticsPredicates; import org.openhab.core.semantics.SemanticsService; import org.openhab.core.semantics.Tag; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; /** @@ -49,13 +55,16 @@ * * @author Kai Kreuzer - Initial contribution * @author Laurent Garnier - Few methods moved from class SemanticTags in order to use the semantic tag registry + * @author Jimmy Tanagra - Add Item semantic tag validation */ @NonNullByDefault -@Component -public class SemanticsServiceImpl implements SemanticsService { +@Component(immediate = true) +public class SemanticsServiceImpl implements SemanticsService, RegistryChangeListener { private static final String SYNONYMS_NAMESPACE = "synonyms"; + private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SemanticsServiceImpl.class); + private final ItemRegistry itemRegistry; private final MetadataRegistry metadataRegistry; private final SemanticTagRegistry semanticTagRegistry; @@ -67,6 +76,14 @@ public SemanticsServiceImpl(final @Reference ItemRegistry itemRegistry, this.itemRegistry = itemRegistry; this.metadataRegistry = metadataRegistry; this.semanticTagRegistry = semanticTagRegistry; + + this.itemRegistry.stream().forEach(this::checkSemantics); + this.itemRegistry.addRegistryChangeListener(this); + } + + @Deactivate + public void deactivate() { + itemRegistry.removeRegistryChangeListener(this); } @Override @@ -158,4 +175,197 @@ private List getLabelAndSynonyms(SemanticTag tag, Locale locale) { Stream synonyms = localizedTag.getSynonyms().stream(); return Stream.concat(label, synonyms).map(s -> s.toLowerCase(locale)).distinct().toList(); } + + /** + * Validates the semantic tags of an item. + * + * It returns true only if one of the following is true: + * - No semantic tags at all + * - Only one Semantic tag of any kind. + * - Note: having only one Property tag is allowed. It implies that the item is a Point. + * - One Point tag and one Property tag + * + * It returns false if two Semantic tags are found, but they don't consist of one Point and one Property. + * It would also return false if more than two Semantic tags are found. + * + * @param item + * @param semanticTag the determined semantic tag of the item + * @return true if the item contains no semantic tags, or a valid combination of semantic tags, otherwise false + */ + boolean validateTags(Item item, @Nullable Class semanticTag) { + if (semanticTag == null) { + return true; + } + String semanticType = SemanticTags.getSemanticRootName(semanticTag); + // We're using Collectors here instead of Stream.toList() to resolve Java's wildcard capture conversion issue + List> tags = item.getTags().stream().map(SemanticTags::getById).filter(Objects::nonNull) + .collect(Collectors.toList()); + switch (tags.size()) { + case 0: + case 1: + return true; + case 2: + Class firstTag = tags.getFirst(); + Class lastTag = tags.getLast(); + if ((Point.class.isAssignableFrom(firstTag) && Property.class.isAssignableFrom(lastTag)) + || (Point.class.isAssignableFrom(lastTag) && Property.class.isAssignableFrom(firstTag))) { + return true; + } + String firstType = SemanticTags.getSemanticRootName(firstTag); + String lastType = SemanticTags.getSemanticRootName(lastTag); + if (firstType.equals(lastType)) { + if (Point.class.isAssignableFrom(firstTag) || Property.class.isAssignableFrom(firstTag)) { + logger.warn( + "Item '{}' ({}) has an invalid combination of semantic tags: {} ({}) and {} ({}). Only one Point and optionally one Property tag may be assigned.", + item.getName(), semanticType, firstTag.getSimpleName(), firstType, + lastTag.getSimpleName(), lastType); + } else { + logger.warn( + "Item '{}' ({}) has an invalid combination of semantic tags: {} ({}) and {} ({}). Only one {} tag may be assigned.", + item.getName(), semanticType, firstTag.getSimpleName(), firstType, + lastTag.getSimpleName(), lastType, firstType); + } + } else { + logger.warn( + "Item '{}' ({}) has an invalid combination of semantic tags: {} ({}) and {} ({}). {} and {} tags cannot be assigned at the same time.", + item.getName(), semanticType, firstTag.getSimpleName(), firstType, lastTag.getSimpleName(), + lastType, firstType, lastType); + } + return false; + default: + List allTags = tags.stream().map(tag -> { + String tagType = SemanticTags.getSemanticRootName(tag); + return String.format("%s (%s)", tag.getSimpleName(), tagType); + }).toList(); + logger.warn( + "Item '{}' ({}) has an invalid combination of semantic tags: {}. An item may only have one tag of Location, Equipment, or Point type. A Property tag may be assigned in conjunction with a Point tag.", + item.getName(), semanticType, allTags); + return false; + } + } + + /** + * Verifies the semantics of an item and logs warnings if the semantics are invalid + * + * @param item + * @return true if the semantics are valid, false otherwise + */ + boolean checkSemantics(Item item) { + String itemName = item.getName(); + Class semanticTag = SemanticTags.getSemanticType(item); + if (semanticTag == null) { + return true; + } + + if (!validateTags(item, semanticTag)) { + return false; + } + + List warnings = new ArrayList<>(); + List parentLocations = new ArrayList<>(); + List parentEquipments = new ArrayList<>(); + + for (String groupName : item.getGroupNames()) { + try { + if (itemRegistry.getItem(groupName) instanceof GroupItem groupItem) { + Class groupSemanticType = SemanticTags.getSemanticType(groupItem); + if (groupSemanticType != null) { + if (Equipment.class.isAssignableFrom(groupSemanticType)) { + parentEquipments.add(groupName); + } else if (Location.class.isAssignableFrom(groupSemanticType)) { + parentLocations.add(groupName); + } + } + } + } catch (ItemNotFoundException e) { + // we don't care about invalid parent groups here + } + } + + if (Point.class.isAssignableFrom(semanticTag)) { + if (parentLocations.size() == 1 && parentEquipments.size() == 1) { + // This case is allowed: a Point can belong to an Equipment and a Location + // + // Case 1: + // When a location contains multiple equipments -> temperature points, + // the average of the points will be used in the location's UI. + // However, when there is a point which is the direct member of the location, + // it will be used in the location's UI instead of the average. + // So setting one of the equipment's point as a direct member of the location + // allows to override the average. + // + // Case 2: + // When a central Equipment e.g. a HVAC contains Points located in multiple locations, + // e.g. room controls and sensors + String semanticType = SemanticTags.getSemanticRootName(semanticTag); + logger.info("Item '{}' ({}) belongs to location {} and equipment {}.", itemName, semanticType, + parentLocations, parentEquipments); + } else { + if (parentLocations.size() > 1) { + warnings.add(String.format( + "It belongs to multiple locations %s. It should only belong to one Equipment or one location, preferably not both at the same time.", + parentLocations.toString())); + } + if (parentEquipments.size() > 1) { + warnings.add(String.format( + "It belongs to multiple equipments %s. A Point can only belong to at most one Equipment.", + parentEquipments.toString())); + } + } + } else if (Equipment.class.isAssignableFrom(semanticTag)) { + if (parentLocations.size() > 0 && parentEquipments.size() > 0) { + warnings.add(String.format( + "It belongs to location(s) %s and equipment(s) %s. An Equipment can only belong to one Location or another Equipment, but not both.", + parentLocations.toString(), parentEquipments.toString())); + } + if (parentLocations.size() > 1) { + warnings.add(String.format( + "It belongs to multiple locations %s. An Equipment can only belong to one Location or another Equipment.", + parentLocations.toString())); + } + if (parentEquipments.size() > 1) { + warnings.add(String.format( + "It belongs to multiple equipments %s. An Equipment can only belong to at most one Equipment.", + parentEquipments.toString())); + } + } else if (Location.class.isAssignableFrom(semanticTag)) { + if (!(item instanceof GroupItem)) { + warnings.add(String.format("It is a %s item, not a group. A location should be a Group Item.", + item.getType())); + } + if (parentEquipments.size() > 0) { + warnings.add(String.format( + "It belongs to equipment(s) %s. A Location can only belong to another Location, not Equipment.", + parentEquipments.toString())); + } + if (parentLocations.size() > 1) { + warnings.add( + String.format("It belongs to multiple locations %s. It should only belong to one location.", + parentLocations.toString())); + } + } + + if (!warnings.isEmpty()) { + String semanticType = SemanticTags.getSemanticRootName(semanticTag); + logger.warn("Item '{}' ({}) has invalid semantic structure: {}", itemName, semanticType, + String.join("\n", warnings)); + return false; + } + return true; + } + + @Override + public void added(Item item) { + checkSemantics(item); + } + + @Override + public void removed(Item item) { + // nothing to do + } + + @Override + public void updated(Item oldElement, Item element) { + checkSemantics(element); + } } diff --git a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java index ecca7399174..04fc8be00d7 100644 --- a/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java +++ b/bundles/org.openhab.core.semantics/src/test/java/org/openhab/core/semantics/internal/SemanticsServiceImplTest.java @@ -25,20 +25,26 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openhab.core.i18n.UnitProvider; import org.openhab.core.items.GenericItem; import org.openhab.core.items.GroupItem; import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.MetadataRegistry; import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.library.items.NumberItem; import org.openhab.core.semantics.Location; import org.openhab.core.semantics.ManagedSemanticTagProvider; import org.openhab.core.semantics.SemanticTag; import org.openhab.core.semantics.SemanticTagImpl; import org.openhab.core.semantics.SemanticTagRegistry; +import org.openhab.core.semantics.SemanticTags; import org.openhab.core.semantics.Tag; import org.openhab.core.semantics.model.DefaultSemanticTagProvider; @@ -229,4 +235,275 @@ public void testGetByLabelOrSynonym() { tags = service.getByLabelOrSynonym("wrong label", Locale.ENGLISH); assertTrue(tags.isEmpty()); } + + private static Stream testValidateTags() { + return Stream.of( // + Arguments.of(true, List.of()), // + Arguments.of(true, List.of("Tag1")), // + Arguments.of(true, List.of("Tag1", "Tag2")), // + + Arguments.of(true, List.of("Point")), // + Arguments.of(true, List.of("Point", "Property")), // + Arguments.of(true, List.of("Property")), // + Arguments.of(true, List.of("Location")), // + Arguments.of(true, List.of("Equipment")), // + + Arguments.of(true, List.of("Control")), // Point + Arguments.of(true, List.of("Control", "Power")), // Point, Property + Arguments.of(true, List.of("Level")), // Property + Arguments.of(true, List.of("Kitchen")), // Location + Arguments.of(true, List.of("Lightbulb")), // Equipment + + Arguments.of(false, List.of("Point", "Location")), // + Arguments.of(false, List.of("Property", "Location")), // + Arguments.of(false, List.of("Point", "Property", "Location")), // + Arguments.of(false, List.of("Point", "Equipment")), // + Arguments.of(false, List.of("Property", "Equipment")), // + Arguments.of(false, List.of("Point", "Property", "Equipment")), // + Arguments.of(false, List.of("Location", "Equipment")), // + + Arguments.of(false, List.of("Control", "Switch")), // Point, Point + Arguments.of(false, List.of("Power", "Level")), // Property, Property + Arguments.of(false, List.of("Control", "Lightbulb")), // Point, Equipment + Arguments.of(false, List.of("Level", "Lightbulb")), // Property, Equipment + Arguments.of(false, List.of("Control", "Level", "Lightbulb")), // Point, Property, Equipment + Arguments.of(false, List.of("Control", "Kitchen")), // Point, Location + Arguments.of(false, List.of("Level", "Kitchen")), // Property, Location + Arguments.of(false, List.of("Control", "Level", "Kitchen")), // Point, Property, Location + Arguments.of(false, List.of("Lightbulb", "Kitchen")), // Equipment, Location + Arguments.of(false, List.of("Lightbulb", "Speaker")), // Equipment, Equipment + Arguments.of(false, List.of("Kitchen", "FirstFloor")), // Location, Location + Arguments.of(false, List.of("Switch", "Lightbulb", "Kitchen")), // Point, Equipment, Location + Arguments.of(false, List.of("Power", "Lightbulb", "Kitchen")), // Property, Equipment, Location + Arguments.of(false, List.of("Switch", "Power", "Lightbulb", "Kitchen")) // Point, Property, Equipment, + // Location + ); + } + + @ParameterizedTest + @MethodSource + public void testValidateTags(boolean expected, List tags) { + GenericItem item = new NumberItem("TestTag"); + item.addTags(tags); + Class semanticTag = SemanticTags.getSemanticType(item); + assertEquals(expected, service.validateTags(item, semanticTag)); + } + + @Test + public void testCheckSemantics() { + // Valid Locations and Equipments to be used for the tests + GroupItem Location1 = new GroupItem("Location1"); + Location1.addTag("Bathroom"); + + GroupItem Location2 = new GroupItem("Location2"); + Location2.addTag("Kitchen"); + + GroupItem Location1Sub = new GroupItem("Location1Sub"); + Location1Sub.addTag("Room"); + + GroupItem Equipment1 = new GroupItem("Equipment1"); + Equipment1.addTag("Lightbulb"); + + GroupItem Equipment2 = new GroupItem("Equipment2"); + Equipment2.addTag("Lightbulb"); + + GroupItem Equipment1Sub = new GroupItem("Equipment1Sub"); + Equipment1Sub.addTag("Lightbulb"); + + GroupItem PointGroup = new GroupItem("PointGroup"); + PointGroup.addTag("Switch"); + + Equipment1.addMember(Equipment1Sub); + Equipment1.addMember(PointGroup); + + Location1.addMember(Location1Sub); + Location1.addMember(Equipment1); + Location1.addMember(PointGroup); + + Location1Sub.addMember(Equipment2); + + Stream.of(Location1, Location2, Location1Sub, Equipment1, Equipment2, Equipment1Sub, PointGroup).forEach(i -> { + try { + when(itemRegistryMock.getItem(i.getName())).thenReturn(i); + } catch (ItemNotFoundException e) { + // should not happen for mocks + } + }); + + // Test Items + + // Valid Points + GenericItem ValidPoint1 = new NumberItem("ValidPoint1"); + ValidPoint1.addTag("Switch"); + Location1.addMember(ValidPoint1); + assertTrue(service.checkSemantics(ValidPoint1)); + + GenericItem ValidPoint2 = new NumberItem("ValidPoint2"); + ValidPoint2.addTag("Switch"); + Equipment1.addMember(ValidPoint2); + assertTrue(service.checkSemantics(ValidPoint2)); + + // A Group Item is a valid Point + GroupItem ValidPoint3 = new GroupItem("ValidPoint3"); + ValidPoint3.addTag("Switch"); + assertTrue(service.checkSemantics(ValidPoint3)); + + // Being a Member of another Point (Group) is OK, they are independent of each other + GenericItem ValidPoint4 = new NumberItem("ValidPoint4"); + ValidPoint4.addTag("Switch"); + PointGroup.addMember(ValidPoint4); + assertTrue(service.checkSemantics(ValidPoint4)); + + // Not a member of any Location or Equipment + GenericItem ValidPoint5 = new NumberItem("ValidPoint5"); + ValidPoint5.addTag("Switch"); + assertTrue(service.checkSemantics(ValidPoint5)); + + // Belonging to Location and Equipment is allowed + // for example: + // When a location contains multiple equipments / temperature points, + // a point who is the direct member of the location is preferred + GenericItem ValidPoint6 = new NumberItem("ValidPoint6"); + ValidPoint6.addTag("Switch"); + Location1.addMember(ValidPoint6); + Equipment1.addMember(ValidPoint6); + assertTrue(service.checkSemantics(ValidPoint6)); + + // Same case as above, but in a sub equipment + GenericItem ValidPoint7 = new NumberItem("ValidPoint7"); + ValidPoint7.addTag("Switch"); + Location1.addMember(ValidPoint7); + Equipment1Sub.addMember(ValidPoint7); + assertTrue(service.checkSemantics(ValidPoint7)); + + // Belongs to two independent locations + GenericItem InvalidPoint1 = new NumberItem("InvalidPoint1"); + InvalidPoint1.addTag("Switch"); + Location1.addMember(InvalidPoint1); + Location2.addMember(InvalidPoint1); + assertTrue(InvalidPoint1.getGroupNames().contains(Location1.getName())); + assertFalse(service.checkSemantics(InvalidPoint1)); + + // Belongs to Location and its sub location + GenericItem InvalidPoint2 = new NumberItem("InvalidPoint2"); + InvalidPoint2.addTag("Switch"); + Location1.addMember(InvalidPoint2); + Location1Sub.addMember(InvalidPoint2); + assertFalse(service.checkSemantics(InvalidPoint2)); + + // Belongs to two Equipments + GenericItem InvalidPoint3 = new NumberItem("InvalidPoint3"); + InvalidPoint3.addTag("Switch"); + Equipment1.addMember(InvalidPoint3); + Equipment2.addMember(InvalidPoint3); + assertFalse(service.checkSemantics(InvalidPoint3)); + + // Locations + + // It's OK not to be a member of any Location + GroupItem ValidLocation1 = new GroupItem("ValidLocation1"); + ValidLocation1.addTag("Bathroom"); + assertTrue(service.checkSemantics(ValidLocation1)); + + // Member of a Location + GroupItem ValidLocation2 = new GroupItem("ValidLocation2"); + ValidLocation2.addTag("Bathroom"); + Location1.addMember(ValidLocation2); + assertTrue(service.checkSemantics(ValidLocation2)); + + // Member of a Point is fine + GroupItem ValidLocation3 = new GroupItem("ValidLocation3"); + ValidLocation3.addTag("Bathroom"); + PointGroup.addMember(ValidLocation3); + assertTrue(service.checkSemantics(ValidLocation3)); + + // Non-GroupItem is not a valid Location + NumberItem InvalidLocation1 = new NumberItem("InvalidLocation1"); + InvalidLocation1.addTag("Bathroom"); + assertFalse(service.checkSemantics(InvalidLocation1)); + + // Belongs to two Locations + GroupItem InvalidLocation2 = new GroupItem("InvalidLocation2"); + InvalidLocation2.addTag("Bathroom"); + Location1.addMember(InvalidLocation2); + Location2.addMember(InvalidLocation2); + assertFalse(service.checkSemantics(InvalidLocation2)); + + // Belongs to Equipment + GroupItem InvalidLocation3 = new GroupItem("InvalidLocation3"); + InvalidLocation3.addTag("Bathroom"); + Equipment1.addMember(InvalidLocation3); + assertFalse(service.checkSemantics(InvalidLocation3)); + + // Belongs to Location and Equipment + GroupItem InvalidLocation4 = new GroupItem("InvalidLocation4"); + InvalidLocation4.addTag("Bathroom"); + Location1.addMember(InvalidLocation4); + Equipment1.addMember(InvalidLocation4); + assertFalse(service.checkSemantics(InvalidLocation4)); + + // Belongs to Location and Location1Sub + GroupItem InvalidLocation5 = new GroupItem("InvalidLocation5"); + InvalidLocation5.addTag("Bathroom"); + Location1Sub.addMember(InvalidLocation5); + Location1.addMember(InvalidLocation5); + assertFalse(service.checkSemantics(InvalidLocation5)); + + // Equipments + + // It's OK not to be a member of any Equipment or Location + GroupItem ValidEquipment1 = new GroupItem("ValidEquipment1"); + ValidEquipment1.addTag("Lightbulb"); + assertTrue(service.checkSemantics(ValidEquipment1)); + + // Member of an Equipment + GroupItem ValidEquipment2 = new GroupItem("ValidEquipment2"); + ValidEquipment2.addTag("Lightbulb"); + Equipment1.addMember(ValidEquipment2); + assertTrue(service.checkSemantics(ValidEquipment2)); + + // Member of a Equipment1Sub + GroupItem ValidEquipment3 = new GroupItem("ValidEquipment3"); + ValidEquipment3.addTag("Lightbulb"); + Equipment1Sub.addMember(ValidEquipment3); + assertTrue(service.checkSemantics(ValidEquipment3)); + + // Member of a Point is fine + GroupItem ValidEquipment4 = new GroupItem("ValidEquipment4"); + ValidEquipment4.addTag("Lightbulb"); + PointGroup.addMember(ValidEquipment4); + assertTrue(service.checkSemantics(ValidEquipment4)); + + // Non-GroupItem is a valid Equipment + NumberItem ValidEquipment5 = new NumberItem("ValidEquipment5"); + ValidEquipment5.addTag("Lightbulb"); + assertTrue(service.checkSemantics(ValidEquipment5)); + + // Belongs to a Location + GroupItem ValidEquipment6 = new GroupItem("ValidEquipment6"); + ValidEquipment6.addTag("Lightbulb"); + Location1.addMember(ValidEquipment6); + assertTrue(service.checkSemantics(ValidEquipment6)); + + // Belongs to two Equipments + GroupItem InvalidEquipment1 = new GroupItem("InvalidEquipment1"); + InvalidEquipment1.addTag("Lightbulb"); + Equipment1.addMember(InvalidEquipment1); + Equipment2.addMember(InvalidEquipment1); + assertFalse(service.checkSemantics(InvalidEquipment1)); + + // Belongs to Location and Equipment + GroupItem InvalidEquipment2 = new GroupItem("InvalidEquipment2"); + InvalidEquipment2.addTag("Lightbulb"); + Location1.addMember(InvalidEquipment2); + Equipment1.addMember(InvalidEquipment2); + assertFalse(service.checkSemantics(InvalidEquipment2)); + + // Belongs to two Locations + GroupItem InvalidEquipment3 = new GroupItem("InvalidEquipment3"); + InvalidEquipment3.addTag("Lightbulb"); + Location1.addMember(InvalidEquipment3); + Location2.addMember(InvalidEquipment3); + assertFalse(service.checkSemantics(InvalidEquipment3)); + } }