diff --git a/bundles/config/org.eclipse.smarthome.config.core.test/src/test/java/org/eclipse/smarthome/config/core/internal/items/MetadataConfigDescriptionProviderImplTest.java b/bundles/config/org.eclipse.smarthome.config.core.test/src/test/java/org/eclipse/smarthome/config/core/internal/items/MetadataConfigDescriptionProviderImplTest.java new file mode 100644 index 00000000000..4cebefc603b --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.core.test/src/test/java/org/eclipse/smarthome/config/core/internal/items/MetadataConfigDescriptionProviderImplTest.java @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.config.core.internal.items; + +import static org.eclipse.smarthome.config.core.internal.metadata.MetadataConfigDescriptionProviderImpl.*; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; + +import org.eclipse.smarthome.config.core.ConfigDescription; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter.Type; +import org.eclipse.smarthome.config.core.metadata.MetadataConfigDescriptionProvider; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameterBuilder; +import org.eclipse.smarthome.config.core.ParameterOption; +import org.eclipse.smarthome.test.java.JavaTest; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +/** + * + * @author Simon Kaufmann - initial contribution and API + * + */ +public class MetadataConfigDescriptionProviderImplTest extends JavaTest { + + private static final String LIBERAL = "liberal"; + private static final String RESTRICTED = "restricted"; + + private static final URI URI_RESTRICTED = URI.create(SCHEME + SEPARATOR + RESTRICTED); + private static final URI URI_LIBERAL = URI.create(SCHEME + SEPARATOR + LIBERAL); + + private static final URI URI_RESTRICTED_DIMMER = URI.create(SCHEME + SEPARATOR + RESTRICTED + SEPARATOR + "dimmer"); + + private @Mock MetadataConfigDescriptionProvider mockProviderRestricted; + private @Mock MetadataConfigDescriptionProvider mockProviderLiberal; + + private MetadataConfigDescriptionProviderImpl service; + + @Before + public void setup() { + initMocks(this); + service = new MetadataConfigDescriptionProviderImpl(); + + when(mockProviderRestricted.getNamespace()).thenReturn(RESTRICTED); + when(mockProviderRestricted.getDescription(any())).thenReturn("Restricted"); + when(mockProviderRestricted.getParameterOptions(any())).thenReturn(Arrays.asList( // + new ParameterOption("dimmer", "Dimmer"), // + new ParameterOption("switch", "Switch") // + )); + when(mockProviderRestricted.getParameters(eq("dimmer"), any())).thenReturn(Arrays.asList( // + ConfigDescriptionParameterBuilder.create("width", Type.INTEGER).build(), // + ConfigDescriptionParameterBuilder.create("height", Type.INTEGER).build() // + )); + + when(mockProviderLiberal.getNamespace()).thenReturn(LIBERAL); + when(mockProviderLiberal.getDescription(any())).thenReturn("Liberal"); + when(mockProviderLiberal.getParameterOptions(any())).thenReturn(null); + } + + @Test + public void testGetConfigDescriptions_noOptions() { + service.addMetadataConfigDescriptionProvider(mockProviderLiberal); + + Collection res = service.getConfigDescriptions(Locale.ENGLISH); + assertNotNull(res); + assertEquals(1, res.size()); + + ConfigDescription desc = res.iterator().next(); + assertEquals(URI_LIBERAL, desc.getUID()); + assertEquals(1, desc.getParameters().size()); + + ConfigDescriptionParameter param = desc.getParameters().get(0); + assertEquals("value", param.getName()); + assertEquals("Liberal", param.getDescription()); + assertFalse(param.getLimitToOptions()); + } + + @Test + public void testGetConfigDescriptions_withOptions() { + service.addMetadataConfigDescriptionProvider(mockProviderRestricted); + + Collection res = service.getConfigDescriptions(Locale.ENGLISH); + assertNotNull(res); + assertEquals(1, res.size()); + + ConfigDescription desc = res.iterator().next(); + assertEquals(URI_RESTRICTED, desc.getUID()); + assertEquals(1, desc.getParameters().size()); + + ConfigDescriptionParameter param = desc.getParameters().get(0); + assertEquals("value", param.getName()); + assertEquals("Restricted", param.getDescription()); + assertTrue(param.getLimitToOptions()); + assertEquals("dimmer", param.getOptions().get(0).getValue()); + assertEquals("switch", param.getOptions().get(1).getValue()); + } + + @Test + public void testGetConfigDescription_wrongScheme() { + service.addMetadataConfigDescriptionProvider(mockProviderRestricted); + service.addMetadataConfigDescriptionProvider(mockProviderLiberal); + + assertNull(service.getConfigDescription(URI.create("some:nonsense"), null)); + } + + @Test + public void testGetConfigDescription_valueDescription() { + service.addMetadataConfigDescriptionProvider(mockProviderRestricted); + service.addMetadataConfigDescriptionProvider(mockProviderLiberal); + + ConfigDescription desc = service.getConfigDescription(URI_LIBERAL, null); + assertNotNull(desc); + assertEquals(URI_LIBERAL, desc.getUID()); + assertEquals(1, desc.getParameters().size()); + + ConfigDescriptionParameter param = desc.getParameters().get(0); + assertEquals("value", param.getName()); + assertEquals("Liberal", param.getDescription()); + assertFalse(param.getLimitToOptions()); + } + + @Test + public void testGetConfigDescription_valueDescriptionNonExistingNamespace() { + service.addMetadataConfigDescriptionProvider(mockProviderRestricted); + service.addMetadataConfigDescriptionProvider(mockProviderLiberal); + + ConfigDescription desc = service.getConfigDescription(URI.create("metadata:nonsense"), null); + assertNull(desc); + } + + @Test + public void testGetConfigDescription_propertiesDescription() { + service.addMetadataConfigDescriptionProvider(mockProviderRestricted); + service.addMetadataConfigDescriptionProvider(mockProviderLiberal); + + ConfigDescription desc = service.getConfigDescription(URI_RESTRICTED_DIMMER, null); + assertNotNull(desc); + assertEquals(URI_RESTRICTED_DIMMER, desc.getUID()); + assertEquals(2, desc.getParameters().size()); + + ConfigDescriptionParameter paramWidth = desc.getParameters().get(0); + assertEquals("width", paramWidth.getName()); + + ConfigDescriptionParameter paramHeight = desc.getParameters().get(1); + assertEquals("height", paramHeight.getName()); + } + + @Test + public void testGetConfigDescription_propertiesDescriptionNonExistingNamespace() { + service.addMetadataConfigDescriptionProvider(mockProviderRestricted); + service.addMetadataConfigDescriptionProvider(mockProviderLiberal); + + ConfigDescription desc = service.getConfigDescription(URI.create("metadata:nonsense:nonsense"), null); + assertNull(desc); + } + + @Test + public void testGetConfigDescription_propertiesDescriptionNonExistingValue() { + service.addMetadataConfigDescriptionProvider(mockProviderRestricted); + service.addMetadataConfigDescriptionProvider(mockProviderLiberal); + + ConfigDescription desc = service.getConfigDescription(URI.create("metadata:foo:nonsense"), null); + assertNull(desc); + } + +} diff --git a/bundles/config/org.eclipse.smarthome.config.core/META-INF/MANIFEST.MF b/bundles/config/org.eclipse.smarthome.config.core/META-INF/MANIFEST.MF index a684cc9e863..96bcdd90e68 100644 --- a/bundles/config/org.eclipse.smarthome.config.core/META-INF/MANIFEST.MF +++ b/bundles/config/org.eclipse.smarthome.config.core/META-INF/MANIFEST.MF @@ -9,6 +9,7 @@ Export-Package: org.eclipse.smarthome.config.core, org.eclipse.smarthome.config.core.dto, org.eclipse.smarthome.config.core.i18n, + org.eclipse.smarthome.config.core.metadata, org.eclipse.smarthome.config.core.status, org.eclipse.smarthome.config.core.status.events, org.eclipse.smarthome.config.core.validation @@ -21,6 +22,7 @@ Import-Package: org.eclipse.smarthome.config.core, org.eclipse.smarthome.config.core.dto, org.eclipse.smarthome.config.core.i18n, + org.eclipse.smarthome.config.core.metadata, org.eclipse.smarthome.config.core.status, org.eclipse.smarthome.config.core.status.events, org.eclipse.smarthome.config.core.validation, diff --git a/bundles/config/org.eclipse.smarthome.config.core/src/main/java/org/eclipse/smarthome/config/core/internal/metadata/MetadataConfigDescriptionProviderImpl.java b/bundles/config/org.eclipse.smarthome.config.core/src/main/java/org/eclipse/smarthome/config/core/internal/metadata/MetadataConfigDescriptionProviderImpl.java new file mode 100644 index 00000000000..ea747c57b3f --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.core/src/main/java/org/eclipse/smarthome/config/core/internal/metadata/MetadataConfigDescriptionProviderImpl.java @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.config.core.internal.metadata; + +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.core.ConfigDescription; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter.Type; +import org.eclipse.smarthome.config.core.metadata.MetadataConfigDescriptionProvider; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameterBuilder; +import org.eclipse.smarthome.config.core.ConfigDescriptionProvider; +import org.eclipse.smarthome.config.core.ParameterOption; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; + +/** + * A {@link ConfigDescriptionProvider} which translated the information of {@link MetadataConfigDescriptionProvider} + * implementations to normal {@link ConfigDescription}s. + *

+ * It exposes the config description for the "main" value under + * + *

+ * {@code
+ *     metadata:
+ * }
+ * 
+ * + * and the config descriptions for the parameters under + * + *
+ * {@code
+ *     metadata::
+ * }
+ * 
+ * + * so that it becomes dependent of the main value and extensions can request different parameters from the user + * depending on which main value was chosen. Implementations of course are free to ignore the {@code value} parameter + * and always return the same set of config descriptions. + * + * @author Simon Kaufmann - initial contribution and API + * + */ +@Component +@NonNullByDefault +public class MetadataConfigDescriptionProviderImpl implements ConfigDescriptionProvider { + + static final String SCHEME = "metadata"; + static final String SEPARATOR = ":"; + + private final List providers = new CopyOnWriteArrayList<>(); + + @Override + public Collection getConfigDescriptions(@Nullable Locale locale) { + List ret = new LinkedList<>(); + ret.addAll(getValueConfigDescriptions(locale)); + return ret; + } + + @Override + public @Nullable ConfigDescription getConfigDescription(URI uri, @Nullable Locale locale) { + if (!SCHEME.equals(uri.getScheme())) { + return null; + } + String part = uri.getSchemeSpecificPart(); + String namespace = part.contains(SEPARATOR) ? part.substring(0, part.indexOf(SEPARATOR)) : part; + String value = part.contains(SEPARATOR) ? part.substring(part.indexOf(SEPARATOR) + 1) : null; + for (MetadataConfigDescriptionProvider provider : providers) { + if (namespace.equals(provider.getNamespace())) { + if (value == null) { + return createValueConfigDescription(provider, locale); + } else { + return createParamConfigDescription(provider, value, locale); + } + } + } + return null; + } + + private List getValueConfigDescriptions(@Nullable Locale locale) { + List ret = new LinkedList<>(); + for (MetadataConfigDescriptionProvider provider : providers) { + ret.add(createValueConfigDescription(provider, locale)); + } + return ret; + } + + private ConfigDescription createValueConfigDescription(MetadataConfigDescriptionProvider provider, + @Nullable Locale locale) { + String namespace = provider.getNamespace(); + String description = provider.getDescription(locale); + List options = provider.getParameterOptions(locale); + URI uri = URI.create(SCHEME + SEPARATOR + namespace); + + ConfigDescriptionParameterBuilder builder = ConfigDescriptionParameterBuilder.create("value", Type.TEXT); + if (options != null && !options.isEmpty()) { + builder.withOptions(options); + builder.withLimitToOptions(true); + } else { + builder.withLimitToOptions(false); + } + builder.withDescription(description != null ? description : namespace); + ConfigDescriptionParameter parameter = builder.build(); + + return new ConfigDescription(uri, Collections.singletonList(parameter)); + } + + private @Nullable ConfigDescription createParamConfigDescription(MetadataConfigDescriptionProvider provider, + String value, @Nullable Locale locale) { + String namespace = provider.getNamespace(); + URI uri = URI.create(SCHEME + SEPARATOR + namespace + SEPARATOR + value); + List parameters = provider.getParameters(value, locale); + if (parameters == null || parameters.isEmpty()) { + return null; + } + return new ConfigDescription(uri, parameters); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + protected void addMetadataConfigDescriptionProvider( + MetadataConfigDescriptionProvider metadataConfigDescriptionProvider) { + providers.add(metadataConfigDescriptionProvider); + } + + protected void removeMetadataConfigDescriptionProvider( + MetadataConfigDescriptionProvider metadataConfigDescriptionProvider) { + providers.remove(metadataConfigDescriptionProvider); + } + +} diff --git a/bundles/config/org.eclipse.smarthome.config.core/src/main/java/org/eclipse/smarthome/config/core/metadata/MetadataConfigDescriptionProvider.java b/bundles/config/org.eclipse.smarthome.config.core/src/main/java/org/eclipse/smarthome/config/core/metadata/MetadataConfigDescriptionProvider.java new file mode 100644 index 00000000000..7be343dc6c4 --- /dev/null +++ b/bundles/config/org.eclipse.smarthome.config.core/src/main/java/org/eclipse/smarthome/config/core/metadata/MetadataConfigDescriptionProvider.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.config.core.metadata; + +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter; +import org.eclipse.smarthome.config.core.ParameterOption; + +/** + * A {@link MetadataConfigDescriptionProvider} implementation can be registered as an OSGi service in order to give + * guidance to UIs what metadata namespaces should be available and what metadata properties are expected. + *

+ * It will be tracked by the framework and the given information will be translated into config descriptions. + *

+ * Every extension which deals with specific metadata (in its own namespace) is expected to provide an implementation of + * this interface. + * + * @author Simon Kaufmann - initial contribution and API + */ +@NonNullByDefault +public interface MetadataConfigDescriptionProvider { + + /** + * Get the identifier of the metadata namespace + * + * @return the metadata namespace + */ + String getNamespace(); + + /** + * Get the human-readable description of the metadata namespace + *

+ * Overriding this method is optional - it will default to the namespace identifier. + * + * @param locale a locale, if available + * @return the metadata namespace description + */ + @Nullable + String getDescription(@Nullable Locale locale); + + /** + * Get all valid options if the main metadata value should be restricted to certain values. + * + * @param locale a locale, if available + * @return a list of parameter options or {@code null} + */ + @Nullable + List getParameterOptions(@Nullable Locale locale); + + /** + * Get the config descriptions for all expected parameters. + *

+ * This list may depend on the current "main" value + * + * @param value the current "main" value + * @param locale a locale, if available + * @return a list of config description parameters or {@code null} + */ + @Nullable + List getParameters(String value, @Nullable Locale locale); + +} diff --git a/bundles/core/org.eclipse.smarthome.core.test/src/test/java/org/eclipse/smarthome/core/internal/items/MetadataRegistryImplTest.java b/bundles/core/org.eclipse.smarthome.core.test/src/test/java/org/eclipse/smarthome/core/internal/items/MetadataRegistryImplTest.java new file mode 100644 index 00000000000..9aba28de985 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.test/src/test/java/org/eclipse/smarthome/core/internal/items/MetadataRegistryImplTest.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.internal.items; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.util.Collections; + +import org.eclipse.smarthome.core.common.registry.ProviderChangeListener; +import org.eclipse.smarthome.core.items.Item; +import org.eclipse.smarthome.core.items.ManagedItemProvider; +import org.eclipse.smarthome.core.items.ManagedMetadataProvider; +import org.eclipse.smarthome.core.items.Metadata; +import org.eclipse.smarthome.core.items.MetadataKey; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * @author Simon Kaufmann - initial contribution and API + */ +public class MetadataRegistryImplTest { + + private static final String ITEM_NAME = "itemName"; + + @SuppressWarnings("rawtypes") + private @Mock ServiceReference managedProviderRef; + private @Mock BundleContext bundleContext; + private @Mock ManagedItemProvider itemProvider; + private @Mock ManagedMetadataProvider managedProvider; + private @Mock Item item; + + private ServiceListener providerTracker; + + private MetadataRegistryImpl registry; + + private ProviderChangeListener providerChangeListener; + + @Before + @SuppressWarnings("unchecked") + public void setup() throws Exception { + initMocks(this); + + when(bundleContext.getService(same(managedProviderRef))).thenReturn(managedProvider); + + when(item.getName()).thenReturn(ITEM_NAME); + + registry = new MetadataRegistryImpl(); + + registry.setManagedItemProvider(itemProvider); + registry.setManagedProvider(managedProvider); + registry.activate(bundleContext); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServiceListener.class); + verify(bundleContext).addServiceListener(captor.capture(), any()); + providerTracker = captor.getValue(); + providerTracker.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, managedProviderRef)); + + ArgumentCaptor> captorChangeListener = ArgumentCaptor + .forClass(ProviderChangeListener.class); + verify(itemProvider).addProviderChangeListener(captorChangeListener.capture()); + providerChangeListener = captorChangeListener.getValue(); + } + + @Test + public void testManagedItemProviderChangeListenerRegistration() { + verify(itemProvider).addProviderChangeListener(any()); + verifyNoMoreInteractions(itemProvider); + + registry.unsetManagedItemProvider(itemProvider); + verify(itemProvider).removeProviderChangeListener(any()); + verifyNoMoreInteractions(itemProvider); + } + + @Test + public void testRemoved() { + providerChangeListener.removed(itemProvider, item); + verify(managedProvider).removeItemMetadata(eq(ITEM_NAME)); + } + + @Test + public void testGet_empty() throws Exception { + MetadataKey key = new MetadataKey("namespace", "itemName"); + + Metadata res = registry.get(key); + assertNull(res); + } + + @Test + public void testGet() throws Exception { + MetadataKey key = new MetadataKey("namespace", "itemName"); + registry.added(managedProvider, new Metadata(key, "value", Collections.emptyMap())); + registry.added(managedProvider, + new Metadata(new MetadataKey("other", "itemName"), "other", Collections.emptyMap())); + registry.added(managedProvider, + new Metadata(new MetadataKey("namespace", "other"), "other", Collections.emptyMap())); + + Metadata res = registry.get(key); + assertNotNull(res); + assertEquals("value", res.getValue()); + assertEquals("namespace", res.getUID().getNamespace()); + assertEquals("itemName", res.getUID().getItemName()); + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core.test/src/test/java/org/eclipse/smarthome/core/items/MetadataKeyTest.java b/bundles/core/org.eclipse.smarthome.core.test/src/test/java/org/eclipse/smarthome/core/items/MetadataKeyTest.java new file mode 100644 index 00000000000..f36d7ebb0b5 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.test/src/test/java/org/eclipse/smarthome/core/items/MetadataKeyTest.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.items; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * @author Simon Kaufmann - initial contribution and API + */ +public class MetadataKeyTest { + + @Test + public void testGetNamespace() { + assertEquals("namespace", new MetadataKey("namespace", "itemName").getNamespace()); + } + + @Test + public void testGetItemName() { + assertEquals("itemName", new MetadataKey("namespace", "itemName").getItemName()); + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core.thing.test/src/test/java/org/eclipse/smarthome/core/thing/ThingUIDTest.java b/bundles/core/org.eclipse.smarthome.core.thing.test/src/test/java/org/eclipse/smarthome/core/thing/ThingUIDTest.java index 88240daff83..b98b24338d3 100644 --- a/bundles/core/org.eclipse.smarthome.core.thing.test/src/test/java/org/eclipse/smarthome/core/thing/ThingUIDTest.java +++ b/bundles/core/org.eclipse.smarthome.core.thing.test/src/test/java/org/eclipse/smarthome/core/thing/ThingUIDTest.java @@ -36,4 +36,14 @@ public void testTwoSegments() { assertEquals("gaga", t.getId()); assertEquals("fake::gaga", t.getAsString()); } + + @Test + public void testGetBridgeIds() { + ThingTypeUID thingType = new ThingTypeUID("fake", "type"); + ThingUID t = new ThingUID(thingType, new ThingUID("fake", "something", "bridge"), "thing"); + + assertEquals("fake:type:bridge:thing", t.getAsString()); + assertEquals(1, t.getBridgeIds().size()); + assertEquals("bridge", t.getBridgeIds().get(0)); + } } diff --git a/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/ChannelUID.java b/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/ChannelUID.java index cccedd59c34..024118ae49d 100644 --- a/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/ChannelUID.java +++ b/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/ChannelUID.java @@ -12,7 +12,12 @@ */ package org.eclipse.smarthome.core.thing; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; /** * {@link ChannelUID} represents a unique identifier for channels. @@ -22,6 +27,7 @@ * @author Dennis Nobel - Added channel group id * @author Kai Kreuzer - Changed creation of channels to not require a thing type */ +@NonNullByDefault public class ChannelUID extends UID { private static final String CHANNEL_GROUP_SEPERATOR = "#"; @@ -43,12 +49,12 @@ public ChannelUID(String channelUid) { * @param id the channel's id */ public ChannelUID(ThingUID thingUID, String id) { - super(getArray(thingUID, null, id)); + super(toSegments(thingUID, null, id)); } @Deprecated public ChannelUID(ThingTypeUID thingTypeUID, ThingUID thingUID, String id) { - super(getArray(thingUID, null, id)); + super(toSegments(thingUID, null, id)); } /** @@ -57,12 +63,12 @@ public ChannelUID(ThingTypeUID thingTypeUID, ThingUID thingUID, String id) { * @param id the channel's id */ public ChannelUID(ThingUID thingUID, String groupId, String id) { - super(getArray(thingUID, groupId, id)); + super(toSegments(thingUID, groupId, id)); } @Deprecated public ChannelUID(ThingTypeUID thingTypeUID, ThingUID thingUID, String groupId, String id) { - super(getArray(thingUID, groupId, id)); + super(toSegments(thingUID, groupId, id)); } /** @@ -98,17 +104,13 @@ public ChannelUID(String bindingId, String thingTypeId, String thingId, String g super(bindingId, thingTypeId, thingId, getChannelId(groupId, id)); } - private static String[] getArray(ThingUID thingUID, String groupId, String id) { - String[] result = new String[thingUID.getSegments().length + 1]; - for (int i = 0; i < thingUID.getSegments().length; i++) { - result[i] = thingUID.getSegments()[i]; - } - result[result.length - 1] = getChannelId(groupId, id); - - return result; + private static List toSegments(ThingUID thingUID, @Nullable String groupId, String id) { + List ret = new ArrayList<>(thingUID.getAllSegments()); + ret.add(getChannelId(groupId, id)); + return ret; } - private static String getChannelId(String groupId, String id) { + private static String getChannelId(@Nullable String groupId, String id) { return groupId != null ? groupId + CHANNEL_GROUP_SEPERATOR + id : id; } @@ -118,8 +120,8 @@ private static String getChannelId(String groupId, String id) { * @return id */ public String getId() { - String[] segments = getSegments(); - return segments[segments.length - 1]; + List segments = getAllSegments(); + return segments.get(segments.size() - 1); } /** @@ -128,17 +130,15 @@ public String getId() { * @return id id without group id */ public String getIdWithoutGroup() { - String[] segments = getSegments(); if (!isInGroup()) { - return segments[segments.length - 1]; + return getId(); } else { - return segments[segments.length - 1].split(CHANNEL_GROUP_SEPERATOR)[1]; + return getId().split(CHANNEL_GROUP_SEPERATOR)[1]; } } public boolean isInGroup() { - String[] segments = getSegments(); - return segments[segments.length - 1].contains(CHANNEL_GROUP_SEPERATOR); + return getId().contains(CHANNEL_GROUP_SEPERATOR); } /** @@ -146,9 +146,8 @@ public boolean isInGroup() { * * @return group id or null if channel is not in a group */ - public String getGroupId() { - String[] segments = getSegments(); - return isInGroup() ? segments[segments.length - 1].split(CHANNEL_GROUP_SEPERATOR)[0] : null; + public @Nullable String getGroupId() { + return isInGroup() ? getId().split(CHANNEL_GROUP_SEPERATOR)[0] : null; } @Override @@ -174,7 +173,8 @@ protected void validateSegment(String segment, int index, int length) { * @return the thing UID */ public ThingUID getThingUID() { - return new ThingUID(Arrays.copyOfRange(getSegments(), 0, getSegments().length - 1)); + List<@NonNull String> allSegments = getAllSegments(); + return new ThingUID(allSegments.subList(0, allSegments.size() - 1).toArray(new String[allSegments.size() - 1])); } } diff --git a/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/ThingUID.java b/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/ThingUID.java index 2a4d5abdab7..6ecb99be6bd 100644 --- a/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/ThingUID.java +++ b/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/ThingUID.java @@ -173,12 +173,8 @@ public ThingTypeUID getThingTypeUID() { * @return list of bridge ids */ public List getBridgeIds() { - List bridgeIds = new ArrayList<>(); - String[] segments = getSegments(); - for (int i = 2; i < segments.length - 1; i++) { - bridgeIds.add(segments[i]); - } - return bridgeIds; + List allSegments = getAllSegments(); + return allSegments.subList(2, allSegments.size() - 1); } /** @@ -187,8 +183,8 @@ public List getBridgeIds() { * @return id the id */ public String getId() { - String[] segments = getSegments(); - return segments[segments.length - 1]; + List segments = getAllSegments(); + return segments.get(segments.size() - 1); } @Override diff --git a/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/UID.java b/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/UID.java index 4420531b741..0e6632b1f08 100644 --- a/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/UID.java +++ b/bundles/core/org.eclipse.smarthome.core.thing/src/main/java/org/eclipse/smarthome/core/thing/UID.java @@ -12,28 +12,30 @@ */ package org.eclipse.smarthome.core.thing; -import java.util.Arrays; -import java.util.stream.Collectors; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.common.AbstractUID; /** - * {@link UID} is the base class for unique identifiers within the SmartHome - * framework. A UID must always start with a binding ID. + * Base class for binding related unique identifiers within the SmartHome framework. + *

+ * A UID must always start with a binding ID. * * @author Dennis Nobel - Initial contribution * @author Oliver Libutzki - Added possibility to define UIDs with variable amount of segments * @author Jochen Hiller - Bugfix 455434: added default constructor, object is now mutable */ -public abstract class UID { - - public static final String SEGMENT_PATTERN = "[A-Za-z0-9_-]*"; - public static final String SEPARATOR = ":"; - private final String[] segments; +@NonNullByDefault +public abstract class UID extends AbstractUID { /** - * Constructor must be public, otherwise it can not be called by subclasses from another package. + * For reflection only. + * Constructor must be public, otherwise it cannot be called by subclasses from another package. */ public UID() { - this.segments = null; + super(); } /** @@ -43,14 +45,7 @@ public UID() { * @param uid uid in form a string (must not be null) */ public UID(String uid) { - this(splitToSegments(uid)); - } - - private static String[] splitToSegments(String uid) { - if (uid == null) { - throw new IllegalArgumentException("Given uid must not be null."); - } - return uid.split(SEPARATOR); + super(uid); } /** @@ -59,40 +54,16 @@ private static String[] splitToSegments(String uid) { * @param segments segments (must not be null) */ public UID(String... segments) { - if (segments == null) { - throw new IllegalArgumentException("Given segments argument must not be null."); - } - int numberOfSegments = getMinimalNumberOfSegments(); - if (segments.length < numberOfSegments) { - throw new IllegalArgumentException("UID must have at least " + numberOfSegments + " segments."); - } - for (int i = 0; i < segments.length; i++) { - String segment = segments[i]; - validateSegment(segment, i, segments.length); - } - this.segments = segments; + super(segments); } /** - * Specifies how many segments the UID has to have at least. + * Creates a UID for list of segments. * - * @return + * @param segments segments (must not be null) */ - protected abstract int getMinimalNumberOfSegments(); - - protected String[] getSegments() { - return this.segments; - } - - protected String getSegment(int segment) { - return this.segments[segment]; - } - - protected void validateSegment(String segment, int index, int length) { - if (!segment.matches(SEGMENT_PATTERN)) { - throw new IllegalArgumentException("UID segment '" + segment - + "' contains invalid characters. Each segment of the UID must match the pattern [A-Za-z0-9_-]*."); - } + protected UID(List segments) { + super(segments); } /** @@ -101,38 +72,46 @@ protected void validateSegment(String segment, int index, int length) { * @return binding id */ public String getBindingId() { - return segments[0]; + return getSegment(0); + } + + /** + * @deprecated use {@link #getAllSegments()} instead + */ + @Deprecated + protected String[] getSegments() { + final List segments = super.getAllSegments(); + return segments.toArray(new String[segments.size()]); } @Override + // Avoid subclasses to require importing the o.e.sh.core.common package + protected List getAllSegments() { + return super.getAllSegments(); + } + + @Override + // Avoid bindings to require importing the o.e.sh.core.common package public String toString() { - return getAsString(); + return super.toString(); } + @Override + // Avoid bindings to require importing the o.e.sh.core.common package public String getAsString() { - return Arrays.stream(segments).collect(Collectors.joining(SEPARATOR)); + return super.getAsString(); } @Override + // Avoid bindings to require importing the o.e.sh.core.common package public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(segments); - return result; + return super.hashCode(); } @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - UID other = (UID) obj; - if (!Arrays.equals(segments, other.segments)) - return false; - return true; + // Avoid bindings to require importing the o.e.sh.core.common package + public boolean equals(@Nullable Object obj) { + return super.equals(obj); } } diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/common/AbstractUID.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/common/AbstractUID.java new file mode 100644 index 00000000000..5ef4ea2661c --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/common/AbstractUID.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.common; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * A non specific base class for unique identifiers within the SmartHome framework. + * + * @author Markus Rathgeb - Splitted from the Thing's UID class + */ +@NonNullByDefault +public abstract class AbstractUID { + + public static final String SEGMENT_PATTERN = "[A-Za-z0-9_-]*"; + public static final String SEPARATOR = ":"; + private final List segments; + + /** + * Constructor must be public, otherwise it can not be called by subclasses from another package. + */ + public AbstractUID() { + this.segments = Collections.emptyList(); + } + + /** + * Parses a UID for a given string. The UID must be in the format + * 'bindingId:segment:segment:...'. + * + * @param uid uid in form a string (must not be null) + */ + public AbstractUID(String uid) { + this(splitToSegments(uid)); + } + + /** + * Creates a AbstractUID for a list of segments. + * + * @param segments the id segments + */ + public AbstractUID(final String... segments) { + this(Arrays.asList(segments)); + } + + /** + * Creates a UID for list of segments. + * + * @param segments segments (must not be null) + */ + public AbstractUID(List segments) { + int minNumberOfSegments = getMinimalNumberOfSegments(); + int numberOfSegments = segments.size(); + if (numberOfSegments < minNumberOfSegments) { + throw new IllegalArgumentException( + String.format("UID must have at least %d segments.", minNumberOfSegments)); + } + for (int i = 0; i < numberOfSegments; i++) { + String segment = segments.get(i); + validateSegment(segment, i, numberOfSegments); + } + this.segments = Collections.unmodifiableList(new ArrayList<>(segments)); + } + + /** + * Specifies how many segments the UID has to have at least. + * + * @return + */ + protected abstract int getMinimalNumberOfSegments(); + + protected List getAllSegments() { + return segments; + } + + protected String getSegment(int segment) { + return segments.get(segment); + } + + protected void validateSegment(String segment, int index, int length) { + if (!segment.matches(SEGMENT_PATTERN)) { + throw new IllegalArgumentException(String.format( + "ID segment '%s' contains invalid characters. Each segment of the ID must match the pattern %s.", + segment, SEGMENT_PATTERN)); + } + } + + @Override + public String toString() { + return getAsString(); + } + + public String getAsString() { + return String.join(SEPARATOR, segments); + } + + private static List splitToSegments(final String id) { + return Arrays.asList(id.split(SEPARATOR)); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + segments.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AbstractUID other = (AbstractUID) obj; + if (!segments.equals(other.segments)) { + return false; + } + return true; + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/items/ManagedMetadataProviderImpl.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/items/ManagedMetadataProviderImpl.java new file mode 100644 index 00000000000..5bb673af4f5 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/items/ManagedMetadataProviderImpl.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.internal.items; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.smarthome.core.common.registry.AbstractManagedProvider; +import org.eclipse.smarthome.core.items.ManagedMetadataProvider; +import org.eclipse.smarthome.core.items.Metadata; +import org.eclipse.smarthome.core.items.MetadataKey; +import org.eclipse.smarthome.core.items.MetadataPredicates; +import org.eclipse.smarthome.core.items.MetadataProvider; +import org.eclipse.smarthome.core.storage.StorageService; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link ManagedMetadataProviderImpl} is an OSGi service, that allows to add or remove + * metadata for items at runtime. Persistence of added metadata is handled by + * a {@link StorageService}. + * + * @author Kai Kreuzer - Initial contribution + */ +@Component(immediate = true, service = { MetadataProvider.class, ManagedMetadataProvider.class }) +public class ManagedMetadataProviderImpl extends AbstractManagedProvider + implements ManagedMetadataProvider { + + private final Logger logger = LoggerFactory.getLogger(ManagedMetadataProviderImpl.class); + + @Override + protected String getStorageName() { + return Metadata.class.getName(); + } + + @Override + protected @NonNull String keyToString(@NonNull MetadataKey key) { + return key.toString(); + } + + @Override + protected Metadata toElement(@NonNull String key, @NonNull Metadata persistableElement) { + return persistableElement; + } + + @Override + protected Metadata toPersistableElement(Metadata element) { + return element; + } + + @Override + @Reference + protected void setStorageService(StorageService storageService) { + super.setStorageService(storageService); + } + + @Override + protected void unsetStorageService(StorageService storageService) { + super.unsetStorageService(storageService); + } + + /** + * Removes all metadata of a given item + * + * @param itemname the name of the item for which the metadata is to be removed. + */ + @Override + public void removeItemMetadata(@NonNull String name) { + logger.debug("Removing all metadata for item {}", name); + getAll().stream().filter(MetadataPredicates.ofItem(name)).map(Metadata::getUID).forEach(this::remove); + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/items/MetadataRegistryImpl.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/items/MetadataRegistryImpl.java new file mode 100644 index 00000000000..3c0766f2a3d --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/internal/items/MetadataRegistryImpl.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.internal.items; + +import org.eclipse.smarthome.core.common.registry.AbstractRegistry; +import org.eclipse.smarthome.core.common.registry.Provider; +import org.eclipse.smarthome.core.common.registry.ProviderChangeListener; +import org.eclipse.smarthome.core.events.EventPublisher; +import org.eclipse.smarthome.core.items.Item; +import org.eclipse.smarthome.core.items.ManagedItemProvider; +import org.eclipse.smarthome.core.items.ManagedMetadataProvider; +import org.eclipse.smarthome.core.items.Metadata; +import org.eclipse.smarthome.core.items.MetadataKey; +import org.eclipse.smarthome.core.items.MetadataProvider; +import org.eclipse.smarthome.core.items.MetadataRegistry; +import org.osgi.framework.BundleContext; +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; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the main implementing class of the {@link MetadataRegistry} interface. It + * keeps track of all declared metadata of all metadata providers. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +@Component(immediate = true, service = { MetadataRegistry.class }) +public class MetadataRegistryImpl extends AbstractRegistry + implements MetadataRegistry { + + private final Logger logger = LoggerFactory.getLogger(MetadataRegistryImpl.class); + + private final ProviderChangeListener itemProviderChangeListener = new ProviderChangeListener() { + @Override + public void added(Provider provider, Item element) { + } + + @Override + public void removed(Provider provider, Item element) { + if (managedProvider != null) { + // remove our metadata for that item + logger.debug("Item {} was removed, trying to clean up corresponding metadata", element.getUID()); + ((ManagedMetadataProvider) managedProvider).removeItemMetadata(element.getName()); + } + } + + @Override + public void updated(Provider provider, Item oldelement, Item element) { + } + }; + + public MetadataRegistryImpl() { + super(MetadataProvider.class); + } + + @Override + @Activate + protected void activate(BundleContext context) { + super.activate(context); + } + + @Override + @Deactivate + protected void deactivate() { + super.deactivate(); + } + + @Reference + protected void setManagedItemProvider(ManagedItemProvider managedItemProvider) { + managedItemProvider.addProviderChangeListener(itemProviderChangeListener); + } + + protected void unsetManagedItemProvider(ManagedItemProvider managedItemProvider) { + managedItemProvider.removeProviderChangeListener(itemProviderChangeListener); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + @Override + protected void setEventPublisher(EventPublisher eventPublisher) { + super.setEventPublisher(eventPublisher); + } + + @Override + protected void unsetEventPublisher(EventPublisher eventPublisher) { + super.unsetEventPublisher(eventPublisher); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setManagedProvider(ManagedMetadataProvider provider) { + super.setManagedProvider(provider); + } + + protected void unsetManagedProvider(ManagedMetadataProvider managedProvider) { + super.removeManagedProvider(managedProvider); + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/ManagedMetadataProvider.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/ManagedMetadataProvider.java new file mode 100644 index 00000000000..4b7c4ae07a9 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/ManagedMetadataProvider.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.items; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.smarthome.core.common.registry.ManagedProvider; +import org.eclipse.smarthome.core.storage.StorageService; + +/** + * {@link ManagedMetadataProvider} is an OSGi service interface that allows to add or remove + * metadata for items at runtime. Persistence of added metadata is handled by + * a {@link StorageService}. + * + * @author Kai Kreuzer - Initial contribution + */ +public interface ManagedMetadataProvider extends ManagedProvider, MetadataProvider { + + /** + * Removes all metadata of a given item + * + * @param itemname the name of the item for which the metadata is to be removed. + */ + void removeItemMetadata(@NonNull String name); + +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/Metadata.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/Metadata.java new file mode 100644 index 00000000000..ad751ec9d5c --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/Metadata.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.items; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.common.registry.Identifiable; + +/** + * This is a data class for storing meta-data for a given item and namespace. + * It is the entity used for within the {@link MetadataRegistry}. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +@NonNullByDefault +public final class Metadata implements Identifiable { + + private final MetadataKey key; + private final String value; + private final Map configuration; + + public Metadata(MetadataKey key, String value, @Nullable Map configuration) { + this.key = key; + this.value = value; + this.configuration = configuration != null ? Collections.unmodifiableMap(new HashMap<>(configuration)) + : Collections.emptyMap(); + } + + @Override + public MetadataKey getUID() { + return key; + } + + /** + * Provides the configuration meta-data. + * + * @return configuration as a map of key-value pairs + */ + public Map getConfiguration() { + return configuration; + } + + /** + * Provides the main value of the meta-data. + * + * @return the main meta-data as a string + */ + public String getValue() { + return value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + key.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Metadata other = (Metadata) obj; + if (!key.equals(other.key)) { + return false; + } + return true; + } + +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataKey.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataKey.java new file mode 100644 index 00000000000..2132a96340d --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataKey.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.items; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.common.AbstractUID; + +/** + * This class represents the key of a {@link Metadata} entity. + * It is a simple combination of a namespace and an item name. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +@NonNullByDefault +public final class MetadataKey extends AbstractUID { + + /** + * Creates a new instance. + * + * @param namespace + * @param itemName + */ + public MetadataKey(String namespace, String itemName) { + super(namespace, itemName); + } + + /** + * Provides the item name of this key + * + * @return the item name + */ + public String getItemName() { + return getSegment(1); + } + + /** + * Provides the namespace of this key + * + * @return the namespace + */ + public String getNamespace() { + return getSegment(0); + } + + @Override + protected int getMinimalNumberOfSegments() { + return 2; + } +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataPredicates.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataPredicates.java new file mode 100644 index 00000000000..a49509968c5 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataPredicates.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.items; + +import java.util.function.Predicate; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Provides some default predicates that are helpful when working with metadata. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +@NonNullByDefault +public final class MetadataPredicates { + + /** + * Creates a {@link Predicate} which can be used to filter {@link Metadata} for a given namespace. + * + * @param namespace to filter + * @return created {@link Predicate} + */ + public static Predicate hasNamespace(String namespace) { + return md -> md.getUID().getNamespace().equals(namespace); + } + + /** + * Creates a {@link Predicate} which can be used to filter {@link Metadata} of a given item. + * + * @param itemname to filter + * @return created {@link Predicate} + */ + public static Predicate ofItem(String itemname) { + return md -> md.getUID().getItemName().equals(itemname); + } +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataProvider.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataProvider.java new file mode 100644 index 00000000000..4f9b97104ca --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataProvider.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.items; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.common.registry.Provider; + +/** + * This is a marker interface for metadata provider implementations that should be used to register those as an OSGi + * service. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +@NonNullByDefault +public interface MetadataProvider extends Provider { + +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataRegistry.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataRegistry.java new file mode 100644 index 00000000000..6854e1560c9 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/MetadataRegistry.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.items; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.common.registry.Registry; + +/** + * The MetadataRegistry is the central place, where additional information about items is kept. + * + * Metadata can be supplied by {@link MetadataProvider}s, which can provision them from any source + * they like and also dynamically remove or add data. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +@NonNullByDefault +public interface MetadataRegistry extends Registry { + +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/dto/MetadataDTO.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/dto/MetadataDTO.java new file mode 100644 index 00000000000..e0875176d12 --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/items/dto/MetadataDTO.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.core.items.dto; + +import java.util.Map; + +/** + * This is a data transfer object that is used to serialize metadata for a certain namespace and item. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +public class MetadataDTO { + + public String value; + public Map config; + + public MetadataDTO() { + } + +} diff --git a/bundles/io/org.eclipse.smarthome.io.rest.core.test/src/test/java/org/eclipse/smarthome/io/rest/core/internal/item/ItemResourceOSGiTest.java b/bundles/io/org.eclipse.smarthome.io.rest.core.test/src/test/java/org/eclipse/smarthome/io/rest/core/internal/item/ItemResourceOSGiTest.java index 851d7dc7a9f..17df095dee2 100644 --- a/bundles/io/org.eclipse.smarthome.io.rest.core.test/src/test/java/org/eclipse/smarthome/io/rest/core/internal/item/ItemResourceOSGiTest.java +++ b/bundles/io/org.eclipse.smarthome.io.rest.core.test/src/test/java/org/eclipse/smarthome/io/rest/core/internal/item/ItemResourceOSGiTest.java @@ -34,7 +34,11 @@ import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.items.ItemProvider; import org.eclipse.smarthome.core.items.ManagedItemProvider; +import org.eclipse.smarthome.core.items.Metadata; +import org.eclipse.smarthome.core.items.MetadataKey; +import org.eclipse.smarthome.core.items.MetadataProvider; import org.eclipse.smarthome.core.items.dto.GroupItemDTO; +import org.eclipse.smarthome.core.items.dto.MetadataDTO; import org.eclipse.smarthome.core.library.items.DimmerItem; import org.eclipse.smarthome.core.library.items.SwitchItem; import org.eclipse.smarthome.test.java.JavaOSGiTest; @@ -87,25 +91,25 @@ public void shouldFilterItemsByTag() throws Exception { item2.addTag("Tag2"); item3.addTag("Tag2"); - Response response = itemResource.getItems(null, null, "Tag1", false, null); + Response response = itemResource.getItems(null, null, "Tag1", null, false, null); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME1, ITEM_NAME2)); - response = itemResource.getItems(null, null, "Tag2", false, null); + response = itemResource.getItems(null, null, "Tag2", null, false, null); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME2, ITEM_NAME3)); - response = itemResource.getItems(null, null, "NotExistingTag", false, null); + response = itemResource.getItems(null, null, "NotExistingTag", null, false, null); assertThat(readItemNamesFromResponse(response), hasSize(0)); } @Test public void shouldFilterItemsByType() throws Exception { - Response response = itemResource.getItems(null, "Switch", null, false, null); + Response response = itemResource.getItems(null, "Switch", null, null, false, null); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME1, ITEM_NAME2)); - response = itemResource.getItems(null, "Dimmer", null, false, null); + response = itemResource.getItems(null, "Dimmer", null, null, false, null); assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME3)); - response = itemResource.getItems(null, "Color", null, false, null); + response = itemResource.getItems(null, "Color", null, null, false, null); assertThat(readItemNamesFromResponse(response), hasSize(0)); } @@ -113,15 +117,15 @@ public void shouldFilterItemsByType() throws Exception { public void shouldAddAndRemoveTags() throws Exception { managedItemProvider.add(new SwitchItem("Switch")); - Response response = itemResource.getItems(null, null, "MyTag", false, null); + Response response = itemResource.getItems(null, null, "MyTag", null, false, null); assertThat(readItemNamesFromResponse(response), hasSize(0)); itemResource.addTag("Switch", "MyTag"); - response = itemResource.getItems(null, null, "MyTag", false, null); + response = itemResource.getItems(null, null, "MyTag", null, false, null); assertThat(readItemNamesFromResponse(response), hasSize(1)); itemResource.removeTag("Switch", "MyTag"); - response = itemResource.getItems(null, null, "MyTag", false, null); + response = itemResource.getItems(null, null, "MyTag", null, false, null); assertThat(readItemNamesFromResponse(response), hasSize(0)); } @@ -130,7 +134,7 @@ public void shouldIncludeRequestedFieldsOnly() throws Exception { JsonParser parser = new JsonParser(); managedItemProvider.add(new SwitchItem("Switch")); itemResource.addTag("Switch", "MyTag"); - Response response = itemResource.getItems(null, null, "MyTag", false, "type,name"); + Response response = itemResource.getItems(null, null, "MyTag", null, false, "type,name"); JsonElement result = parser.parse(IOUtils.toString((InputStream) response.getEntity())); JsonElement expected = parser.parse("[{type: \"Switch\", name: \"Switch\"}]"); @@ -206,4 +210,54 @@ public void addMultipleItems() throws IOException { assertThat(statusCodes.get(1), is("updated")); } + @Test + public void testMetadata() { + MetadataDTO dto = new MetadataDTO(); + dto.value = "some value"; + assertEquals(201, itemResource.addMetadata(ITEM_NAME1, "namespace", dto).getStatus()); + assertEquals(200, itemResource.removeMetadata(ITEM_NAME1, "namespace").getStatus()); + assertEquals(404, itemResource.removeMetadata(ITEM_NAME1, "namespace").getStatus()); + } + + @Test + public void testAddMetadata_nonExistingItem() { + MetadataDTO dto = new MetadataDTO(); + dto.value = "some value"; + Response response = itemResource.addMetadata("nonExisting", "foo", dto); + assertEquals(404, response.getStatus()); + } + + @Test + public void testAddMetadata_update() { + MetadataDTO dto = new MetadataDTO(); + dto.value = "some value"; + assertEquals(201, itemResource.addMetadata(ITEM_NAME1, "namespace", dto).getStatus()); + MetadataDTO dto2 = new MetadataDTO(); + dto2.value = "new value"; + assertEquals(200, itemResource.addMetadata(ITEM_NAME1, "namespace", dto2).getStatus()); + } + + @Test + public void testRemoveMetadata_nonExistingItem() { + Response response = itemResource.removeMetadata("nonExisting", "anything"); + assertEquals(404, response.getStatus()); + } + + @Test + public void testRemoveMetadata_nonExistingNamespace() { + Response response = itemResource.removeMetadata(ITEM_NAME1, "anything"); + assertEquals(404, response.getStatus()); + } + + @Test + public void testRemoveMetadata_unmanagedMetadata() { + MetadataProvider provider = mock(MetadataProvider.class); + when(provider.getAll()).thenReturn( + Collections.singleton(new Metadata(new MetadataKey("namespace", ITEM_NAME1), "some value", null))); + registerService(provider); + + Response response = itemResource.removeMetadata(ITEM_NAME1, "namespace"); + assertEquals(409, response.getStatus()); + } + } diff --git a/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/internal/config/ConfigDescriptionResource.java b/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/internal/config/ConfigDescriptionResource.java index 4624a90bef5..e4964e93f7a 100644 --- a/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/internal/config/ConfigDescriptionResource.java +++ b/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/internal/config/ConfigDescriptionResource.java @@ -22,11 +22,13 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriBuilder; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.config.core.ConfigDescription; import org.eclipse.smarthome.config.core.ConfigDescriptionRegistry; import org.eclipse.smarthome.config.core.dto.ConfigDescriptionDTO; @@ -69,12 +71,14 @@ public class ConfigDescriptionResource implements RESTResource { @Produces(MediaType.APPLICATION_JSON) @ApiOperation(value = "Gets all available config descriptions.", response = ConfigDescriptionDTO.class, responseContainer = "List") @ApiResponses(value = @ApiResponse(code = 200, message = "OK", response = ConfigDescriptionDTO.class, responseContainer = "List")) - public Response getAll(@HeaderParam("Accept-Language") @ApiParam(value = "Accept-Language") String language) { + public Response getAll(@HeaderParam("Accept-Language") @ApiParam(value = "Accept-Language") String language, // + @QueryParam("scheme") @ApiParam(value = "scheme filter", required = false) @Nullable String scheme) { Locale locale = LocaleUtil.getLocale(language); Collection configDescriptions = configDescriptionRegistry.getConfigDescriptions(locale); - return Response.ok(new Stream2JSONInputStream(configDescriptions.stream().map(ConfigDescriptionDTOMapper::map))) - .build(); + return Response.ok(new Stream2JSONInputStream(configDescriptions.stream().filter(configDescription -> { + return scheme == null || scheme.equals(configDescription.getUID().getScheme()); + }).map(ConfigDescriptionDTOMapper::map))).build(); } @GET @@ -105,4 +109,5 @@ protected void unsetConfigDescriptionRegistry(ConfigDescriptionRegistry configDe public boolean isSatisfied() { return configDescriptionRegistry != null; } + } diff --git a/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/internal/item/ItemResource.java b/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/internal/item/ItemResource.java index 4ac6b872a7e..6d408fabee4 100644 --- a/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/internal/item/ItemResource.java +++ b/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/internal/item/ItemResource.java @@ -13,11 +13,17 @@ package org.eclipse.smarthome.io.rest.core.internal.item; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.security.RolesAllowed; @@ -53,8 +59,12 @@ import org.eclipse.smarthome.core.items.ItemNotFoundException; import org.eclipse.smarthome.core.items.ItemRegistry; import org.eclipse.smarthome.core.items.ManagedItemProvider; +import org.eclipse.smarthome.core.items.Metadata; +import org.eclipse.smarthome.core.items.MetadataKey; +import org.eclipse.smarthome.core.items.MetadataRegistry; import org.eclipse.smarthome.core.items.dto.GroupItemDTO; import org.eclipse.smarthome.core.items.dto.ItemDTOMapper; +import org.eclipse.smarthome.core.items.dto.MetadataDTO; import org.eclipse.smarthome.core.items.events.ItemEventFactory; import org.eclipse.smarthome.core.library.items.RollershutterItem; import org.eclipse.smarthome.core.library.items.SwitchItem; @@ -68,6 +78,7 @@ import org.eclipse.smarthome.io.rest.LocaleUtil; import org.eclipse.smarthome.io.rest.RESTResource; import org.eclipse.smarthome.io.rest.Stream2JSONInputStream; +import org.eclipse.smarthome.io.rest.core.item.EnrichedGroupItemDTO; import org.eclipse.smarthome.io.rest.core.item.EnrichedItemDTO; import org.eclipse.smarthome.io.rest.core.item.EnrichedItemDTOMapper; import org.osgi.service.component.annotations.Component; @@ -124,6 +135,8 @@ public class ItemResource implements RESTResource { @NonNullByDefault({}) private ItemRegistry itemRegistry; @NonNullByDefault({}) + private MetadataRegistry metadataRegistry; + @NonNullByDefault({}) private EventPublisher eventPublisher; @NonNullByDefault({}) private ManagedItemProvider managedItemProvider; @@ -140,6 +153,15 @@ protected void unsetItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = null; } + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setMetadataRegistry(MetadataRegistry metadataRegistry) { + this.metadataRegistry = metadataRegistry; + } + + protected void unsetMetadataRegistry(MetadataRegistry metadataRegistry) { + this.metadataRegistry = null; + } + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) protected void setEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = eventPublisher; @@ -186,13 +208,17 @@ public Response getItems( @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = "language") @Nullable String language, @QueryParam("type") @ApiParam(value = "item type filter", required = false) @Nullable String type, @QueryParam("tags") @ApiParam(value = "item tag filter", required = false) @Nullable String tags, - @DefaultValue("false") @QueryParam("recursive") @ApiParam(value = "get member items recursivly", required = false) boolean recursive, + @QueryParam("metadata") @ApiParam(value = "metadata selector", required = false) @Nullable String namespaceSelector, + @DefaultValue("false") @QueryParam("recursive") @ApiParam(value = "get member items recursively", required = false) boolean recursive, @QueryParam("fields") @ApiParam(value = "limit output to the given fields (comma separated)", required = false) @Nullable String fields) { final Locale locale = LocaleUtil.getLocale(language); + final Set namespaces = namespaceSelector == null ? Collections.emptySet() + : Arrays.stream(namespaceSelector.split(",")).collect(Collectors.toSet()); logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath()); Stream itemStream = getItems(type, tags).stream() - .map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriInfo.getBaseUri(), locale)); + .map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriInfo.getBaseUri(), locale)) + .peek(dto -> addMetadata(dto, namespaces, null)); itemStream = dtoMapper.limitToFields(itemStream, fields); return Response.ok(new Stream2JSONInputStream(itemStream)).build(); } @@ -205,8 +231,12 @@ public Response getItems( @ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = EnrichedItemDTO.class), @ApiResponse(code = 404, message = "Item not found") }) public Response getItemData(@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = "language") String language, + @QueryParam("metadata") @ApiParam(value = "metadata selector", required = false) @Nullable String namespaceSelector, @PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname) { + final Locale locale = LocaleUtil.getLocale(language); + final Set namespaces = namespaceSelector == null ? Collections.emptySet() + : Arrays.stream(namespaceSelector.split(",")).collect(Collectors.toSet()); logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath()); // get item @@ -215,7 +245,9 @@ public Response getItemData(@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam( // if it exists if (item != null) { logger.debug("Received HTTP GET request at '{}'.", uriInfo.getPath()); - return getItemResponse(Status.OK, item, locale, null); + EnrichedItemDTO dto = EnrichedItemDTOMapper.map(item, true, null, uriInfo.getBaseUri(), locale); + addMetadata(dto, namespaces, null); + return JSONResponse.createResponse(Status.OK, dto, null); } else { logger.info("Received HTTP GET request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname); return getItemNotFoundResponse(itemname); @@ -482,6 +514,73 @@ public Response removeTag(@PathParam("itemname") @ApiParam(value = "item name", return Response.ok(null, MediaType.TEXT_PLAIN).build(); } + @PUT + @RolesAllowed({ Role.ADMIN }) + @Path("/{itemname: [a-zA-Z_0-9]*}/metadata/{namespace}") + @Consumes(MediaType.APPLICATION_JSON) + @ApiOperation(value = "Adds metadata to an item.") + @ApiResponses(value = { // + @ApiResponse(code = 200, message = "OK"), // + @ApiResponse(code = 201, message = "Created"), // + @ApiResponse(code = 404, message = "Item not found."), // + @ApiResponse(code = 405, message = "Metadata not editable.") }) + public Response addMetadata(@PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname, + @PathParam("namespace") @ApiParam(value = "namespace", required = true) String namespace, + @ApiParam(value = "metadata", required = true) MetadataDTO metadata) { + + Item item = getItem(itemname); + + if (item == null) { + logger.info("Received HTTP PUT request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname); + return Response.status(Status.NOT_FOUND).build(); + } + + MetadataKey key = new MetadataKey(namespace, itemname); + Metadata md = new Metadata(key, metadata.value, metadata.config); + if (metadataRegistry.get(key) == null) { + metadataRegistry.add(md); + return Response.status(Status.CREATED).type(MediaType.TEXT_PLAIN).build(); + } else { + metadataRegistry.update(md); + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } + + } + + @DELETE + @RolesAllowed({ Role.ADMIN }) + @Path("/{itemname: [a-zA-Z_0-9]*}/metadata/{namespace}") + @ApiOperation(value = "Removes metadata from an item.") + @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), + @ApiResponse(code = 404, message = "Item not found."), + @ApiResponse(code = 405, message = "Meta data not editable.") }) + public Response removeMetadata( + @PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname, + @PathParam("namespace") @ApiParam(value = "namespace", required = true) String namespace) { + + Item item = getItem(itemname); + + if (item == null) { + logger.info("Received HTTP DELETE request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname); + return Response.status(Status.NOT_FOUND).build(); + } + + MetadataKey key = new MetadataKey(namespace, itemname); + if (metadataRegistry.get(key) != null) { + if (metadataRegistry.remove(key) == null) { + logger.info("Received HTTP DELETE request at '{}' for unmanaged item meta-data '{}'.", + uriInfo.getPath(), key); + return Response.status(Status.CONFLICT).build(); + } + } else { + logger.info("Received HTTP DELETE request at '{}' for unknown item meta-data '{}'.", uriInfo.getPath(), + key); + return Response.status(Status.NOT_FOUND).build(); + } + + return Response.ok(null, MediaType.TEXT_PLAIN).build(); + } + /** * Create or Update an item by supplying an item bean. * @@ -685,6 +784,29 @@ private Collection getItems(@Nullable String type, @Nullable String tags) return items; } + private void addMetadata(EnrichedItemDTO dto, Set namespaces, @Nullable Predicate filter) { + Map metadata = new HashMap<>(); + for (String namespace : namespaces) { + MetadataKey key = new MetadataKey(namespace, dto.name); + Metadata md = metadataRegistry.get(key); + if (md != null && (filter == null || filter.test(md))) { + MetadataDTO mdDto = new MetadataDTO(); + mdDto.value = md.getValue(); + mdDto.config = md.getConfiguration().isEmpty() ? null : md.getConfiguration(); + metadata.put(namespace, mdDto); + } + } + if (dto instanceof EnrichedGroupItemDTO) { + for (EnrichedItemDTO member : ((EnrichedGroupItemDTO) dto).members) { + addMetadata(member, namespaces, filter); + } + } + if (!metadata.isEmpty()) { + // we only set it in the dto if there is really data available + dto.metadata = metadata; + } + } + @Override public boolean isSatisfied() { return itemRegistry != null && managedItemProvider != null && eventPublisher != null && !itemFactories.isEmpty() diff --git a/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/item/EnrichedItemDTO.java b/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/item/EnrichedItemDTO.java index 4e6570cb988..637c1dfd79f 100644 --- a/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/item/EnrichedItemDTO.java +++ b/bundles/io/org.eclipse.smarthome.io.rest.core/src/main/java/org/eclipse/smarthome/io/rest/core/item/EnrichedItemDTO.java @@ -12,6 +12,8 @@ */ package org.eclipse.smarthome.io.rest.core.item; +import java.util.Map; + import org.eclipse.smarthome.core.items.dto.ItemDTO; import org.eclipse.smarthome.core.types.StateDescription; @@ -20,6 +22,7 @@ * description and the link. * * @author Dennis Nobel - Initial contribution + * @author Kai Kreuzer - Added metadata * */ public class EnrichedItemDTO extends ItemDTO { @@ -28,6 +31,7 @@ public class EnrichedItemDTO extends ItemDTO { public String state; public String transformedState; public StateDescription stateDescription; + public Map metadata; public EnrichedItemDTO(ItemDTO itemDTO, String link, String state, String transformedState, StateDescription stateDescription) { diff --git a/bundles/model/org.eclipse.smarthome.model.item.tests/org.eclipse.smarthome.model.item.tests.launch b/bundles/model/org.eclipse.smarthome.model.item.tests/org.eclipse.smarthome.model.item.tests.launch index 40f79519ed1..9b642ab6935 100644 --- a/bundles/model/org.eclipse.smarthome.model.item.tests/org.eclipse.smarthome.model.item.tests.launch +++ b/bundles/model/org.eclipse.smarthome.model.item.tests/org.eclipse.smarthome.model.item.tests.launch @@ -34,7 +34,7 @@ - + diff --git a/bundles/model/org.eclipse.smarthome.model.item.tests/src/org/eclipse/smarthome/model/item/internal/GenericItemProvider2Test.java b/bundles/model/org.eclipse.smarthome.model.item.tests/src/org/eclipse/smarthome/model/item/internal/GenericItemProvider2Test.java index f13e1d91706..835a44350a0 100644 --- a/bundles/model/org.eclipse.smarthome.model.item.tests/src/org/eclipse/smarthome/model/item/internal/GenericItemProvider2Test.java +++ b/bundles/model/org.eclipse.smarthome.model.item.tests/src/org/eclipse/smarthome/model/item/internal/GenericItemProvider2Test.java @@ -12,16 +12,19 @@ */ package org.eclipse.smarthome.model.item.internal; -import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import java.io.ByteArrayInputStream; +import java.math.BigDecimal; import java.util.Iterator; import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.items.GroupItem; import org.eclipse.smarthome.core.items.Item; import org.eclipse.smarthome.core.items.ItemRegistry; +import org.eclipse.smarthome.core.items.Metadata; +import org.eclipse.smarthome.core.items.MetadataKey; +import org.eclipse.smarthome.core.items.MetadataRegistry; import org.eclipse.smarthome.core.library.items.NumberItem; import org.eclipse.smarthome.core.library.items.SwitchItem; import org.eclipse.smarthome.core.library.types.ArithmeticGroupFunction; @@ -45,15 +48,25 @@ public class GenericItemProvider2Test extends JavaOSGiTest { private ModelRepository modelRepository; private ItemRegistry itemRegistry; + private MetadataRegistry metadataRegistry; @Before public void setUp() { + registerVolatileStorageService(); + itemRegistry = getService(ItemRegistry.class); - assertThat(itemRegistry, is(notNullValue())); + assertNotNull(itemRegistry); + modelRepository = getService(ModelRepository.class); - assertThat(modelRepository, is(notNullValue())); + assertNotNull(modelRepository); + + metadataRegistry = getService(MetadataRegistry.class); + assertNotNull(metadataRegistry); + modelRepository.removeModel(TESTMODEL_NAME); modelRepository.removeModel(TESTMODEL_NAME2); + + assertEquals(0, itemRegistry.getAll().size()); } @After @@ -64,8 +77,6 @@ public void tearDown() { @Test public void testStableOrder() { - assertThat(itemRegistry.getAll().size(), is(0)); - String model = "Group testGroup " + // "Number number1 (testGroup) " + // "Number number2 (testGroup) " + // @@ -91,8 +102,6 @@ public void testStableOrder() { @Test public void testStableReloadOrder() { - assertThat(itemRegistry.getAll().size(), is(0)); - String model = "Group testGroup " + // "Number number1 (testGroup) " + // "Number number2 (testGroup) " + // @@ -105,7 +114,7 @@ public void testStableReloadOrder() { "Number number9 (testGroup) "; modelRepository.addOrRefreshModel(TESTMODEL_NAME, new ByteArrayInputStream(model.getBytes())); - assertThat(itemRegistry.getAll().size(), is(10)); + assertEquals(10, itemRegistry.getAll().size()); model = "Group testGroup " + // "Number number1 (testGroup) " + // @@ -135,8 +144,6 @@ public void testStableReloadOrder() { @Test public void testGroupAssignmentsAreConsidered() { - assertThat(itemRegistry.getAll().size(), is(0)); - String model = "Group testGroup " + // "Number number1 (testGroup) " + // "Number number2 "; @@ -201,4 +208,39 @@ public void testGroupItemChangesBaseItemAndFunction() { assertTrue(gip.hasItemChanged(g1, g2)); } + + @Test + public void testMetadata_simple() { + String model = "Switch simple { namespace=\"value\" } "; + + modelRepository.addOrRefreshModel(TESTMODEL_NAME, new ByteArrayInputStream(model.getBytes())); + Item item = itemRegistry.get("simple"); + assertNotNull(item); + + Metadata res = metadataRegistry.get(new MetadataKey("namespace", "simple")); + assertNotNull(res); + assertEquals("value", res.getValue()); + assertNotNull(res.getConfiguration()); + } + + @Test + public void testMetadata_configured() { + String model = "Switch simple { namespace=\"value\" } " + // + "Switch configured { foo=\"bar\" [ answer=42 ] } "; + + modelRepository.addOrRefreshModel(TESTMODEL_NAME, new ByteArrayInputStream(model.getBytes())); + Item item = itemRegistry.get("configured"); + assertNotNull(item); + + Metadata res = metadataRegistry.get(new MetadataKey("foo", "configured")); + assertNotNull(res); + assertEquals("bar", res.getValue()); + assertEquals(new BigDecimal(42), res.getConfiguration().get("answer")); + + modelRepository.removeModel(TESTMODEL_NAME); + + res = metadataRegistry.get(new MetadataKey("foo", "configured")); + assertNull(res); + } + } diff --git a/bundles/model/org.eclipse.smarthome.model.item.tests/src/org/eclipse/smarthome/model/item/internal/GenericMetadataProviderTest.java b/bundles/model/org.eclipse.smarthome.model.item.tests/src/org/eclipse/smarthome/model/item/internal/GenericMetadataProviderTest.java new file mode 100644 index 00000000000..7e7e2724dcf --- /dev/null +++ b/bundles/model/org.eclipse.smarthome.model.item.tests/src/org/eclipse/smarthome/model/item/internal/GenericMetadataProviderTest.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.model.item.internal; + +import static org.junit.Assert.*; + +import java.util.Collection; + +import org.eclipse.smarthome.core.items.Metadata; +import org.junit.Test; + +/** + * @author Simon Kaufmann - initial contribution and API + */ +public class GenericMetadataProviderTest { + + @Test + public void testGetAll_empty() { + GenericMetadataProvider provider = new GenericMetadataProvider(); + Collection res = provider.getAll(); + assertNotNull(res); + assertEquals(0, res.size()); + } + + @Test + public void testAddMetadata() { + GenericMetadataProvider provider = new GenericMetadataProvider(); + provider.addMetadata("binding", "item", "value", null); + Collection res = provider.getAll(); + assertEquals(1, res.size()); + assertEquals("value", res.iterator().next().getValue()); + } + + @Test + public void testRemoveMetadata_nonExistentItem() { + GenericMetadataProvider provider = new GenericMetadataProvider(); + provider.removeMetadata("nonExistentItem"); + } + + @Test + public void testRemoveMetadata() { + GenericMetadataProvider provider = new GenericMetadataProvider(); + provider.addMetadata("other", "item", "value", null); + provider.addMetadata("binding", "item", "value", null); + provider.addMetadata("binding", "other", "value", null); + assertEquals(3, provider.getAll().size()); + + provider.removeMetadata("item"); + assertEquals(1, provider.getAll().size()); + } + +} diff --git a/bundles/model/org.eclipse.smarthome.model.item/src/org/eclipse/smarthome/model/item/internal/GenericItemProvider.java b/bundles/model/org.eclipse.smarthome.model.item/src/org/eclipse/smarthome/model/item/internal/GenericItemProvider.java index ca358500186..966a945eccb 100644 --- a/bundles/model/org.eclipse.smarthome.model.item/src/org/eclipse/smarthome/model/item/internal/GenericItemProvider.java +++ b/bundles/model/org.eclipse.smarthome.model.item/src/org/eclipse/smarthome/model/item/internal/GenericItemProvider.java @@ -74,6 +74,8 @@ public class GenericItemProvider extends AbstractProvider private ModelRepository modelRepository = null; + private GenericMetadataProvider genericMetaDataProvider = null; + private final Map> itemsMap = new ConcurrentHashMap<>(); private final Collection itemFactorys = new ArrayList(); @@ -81,6 +83,7 @@ public class GenericItemProvider extends AbstractProvider private final Map stateDescriptions = new ConcurrentHashMap<>(); private Integer rank; + private boolean active = false; protected void activate(Map properties) { Object serviceRanking = properties.get(Constants.SERVICE_RANKING); @@ -89,6 +92,21 @@ protected void activate(Map properties) { } else { rank = 0; } + + itemFactorys.forEach(itemFactory -> dispatchBindingsPerItemType(null, itemFactory.getSupportedItemTypes())); + + // process models which are already parsed by modelRepository: + for (String modelName : modelRepository.getAllModelNamesOfType("items")) { + modelChanged(modelName, EventType.ADDED); + } + modelRepository.addModelRepositoryChangeListener(this); + + active = true; + } + + protected void deactivate() { + active = false; + modelRepository.removeModelRepositoryChangeListener(this); } @Override @@ -96,23 +114,24 @@ public Integer getRank() { return rank; } - @Reference() + @Reference public void setModelRepository(ModelRepository modelRepository) { this.modelRepository = modelRepository; - - // process models which are already parsed by modelRepository: - for (String modelName : modelRepository.getAllModelNamesOfType("items")) { - modelChanged(modelName, EventType.ADDED); - } - - modelRepository.addModelRepositoryChangeListener(this); } public void unsetModelRepository(ModelRepository modelRepository) { - modelRepository.removeModelRepositoryChangeListener(this); this.modelRepository = null; } + @Reference + protected void setGenericMetadataProvider(GenericMetadataProvider genericMetadataProvider) { + this.genericMetaDataProvider = genericMetadataProvider; + } + + protected void unsetGenericMetadataProvider(GenericMetadataProvider genericMetadataProvider) { + this.genericMetaDataProvider = null; + } + /** * Add another instance of an {@link ItemFactory}. Used by Declarative Services. * @@ -121,7 +140,9 @@ public void unsetModelRepository(ModelRepository modelRepository) { @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) public void addItemFactory(ItemFactory factory) { itemFactorys.add(factory); - dispatchBindingsPerItemType(null, factory.getSupportedItemTypes()); + if (active) { + dispatchBindingsPerItemType(null, factory.getSupportedItemTypes()); + } } /** @@ -369,8 +390,7 @@ private void internalDispatchBindings(BindingConfigReader reader, String modelNa bindingType, item.getName(), e); } } else { - logger.trace("Couldn't find config reader for binding type '{}' > " - + "parsing binding configuration of Item '{}' aborted!", bindingType, item); + genericMetaDataProvider.addMetadata(bindingType, item.getName(), config, configuration.getProperties()); } } } @@ -397,8 +417,7 @@ public void modelChanged(String modelName, EventType type) { processBindingConfigsFromModel(modelName, type); for (Item oldItem : oldItems.values()) { if (!newItems.containsKey(oldItem.getName())) { - notifyListenersAboutRemovedElement(oldItem); - this.stateDescriptions.remove(oldItem.getName()); + notifyAndCleanup(oldItem); } } break; @@ -407,13 +426,19 @@ public void modelChanged(String modelName, EventType type) { Collection itemsFromModel = getItemsFromModel(modelName); itemsMap.remove(modelName); for (Item item : itemsFromModel) { - notifyListenersAboutRemovedElement(item); + notifyAndCleanup(item); } break; } } } + private void notifyAndCleanup(Item oldItem) { + notifyListenersAboutRemovedElement(oldItem); + this.stateDescriptions.remove(oldItem.getName()); + genericMetaDataProvider.removeMetadata(oldItem.getName()); + } + protected boolean hasItemChanged(Item item1, Item item2) { return !Objects.equals(item1.getClass(), item2.getClass()) || // !Objects.equals(item1.getName(), item2.getName()) || // diff --git a/bundles/model/org.eclipse.smarthome.model.item/src/org/eclipse/smarthome/model/item/internal/GenericMetadataProvider.java b/bundles/model/org.eclipse.smarthome.model.item/src/org/eclipse/smarthome/model/item/internal/GenericMetadataProvider.java new file mode 100644 index 00000000000..f87c9beefa7 --- /dev/null +++ b/bundles/model/org.eclipse.smarthome.model.item/src/org/eclipse/smarthome/model/item/internal/GenericMetadataProvider.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2014-2017 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.smarthome.model.item.internal; + +import static java.util.stream.Collectors.toSet; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.eclipse.emf.codegen.ecore.templates.edit.ItemProvider; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.common.registry.AbstractProvider; +import org.eclipse.smarthome.core.items.Metadata; +import org.eclipse.smarthome.core.items.MetadataKey; +import org.eclipse.smarthome.core.items.MetadataPredicates; +import org.eclipse.smarthome.core.items.MetadataProvider; +import org.osgi.service.component.annotations.Component; + +/** + * This class serves as a provider for all metadata that is found within item files. + * It is filled with content by the {@link GenericItemProvider}, which cannot itself implement the + * {@link MetadataProvider} interface as it already implements {@link ItemProvider}, which would lead to duplicate + * methods. + * + * @author Kai Kreuzer - Initial contribution and API + * + */ +@NonNullByDefault +@Component(service = { MetadataProvider.class, GenericMetadataProvider.class }) +public class GenericMetadataProvider extends AbstractProvider implements MetadataProvider { + + private final Set metadata = new HashSet<>(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(true); + + /** + * Adds metadata to this provider + * + * @param bindingType + * @param itemName + * @param configuration + */ + public void addMetadata(String bindingType, String itemName, String value, + @Nullable Map configuration) { + MetadataKey key = new MetadataKey(bindingType, itemName); + Metadata md = new Metadata(key, value, configuration); + try { + lock.writeLock().lock(); + metadata.add(md); + } finally { + lock.writeLock().unlock(); + } + notifyListenersAboutAddedElement(md); + } + + /** + * Removes all meta-data for a given item name + * + * @param itemName + */ + public void removeMetadata(String itemName) { + Set toBeRemoved; + try { + lock.writeLock().lock(); + toBeRemoved = metadata.stream().filter(MetadataPredicates.ofItem(itemName)).collect(toSet()); + metadata.removeAll(toBeRemoved); + } finally { + lock.writeLock().unlock(); + } + for (Metadata m : toBeRemoved) { + notifyListenersAboutRemovedElement(m); + } + } + + @Override + public Collection getAll() { + try { + lock.readLock().lock(); + return Collections.unmodifiableSet(metadata); + } finally { + lock.readLock().unlock(); + } + } + +} diff --git a/bundles/model/org.eclipse.smarthome.model.thing/src/org/eclipse/smarthome/model/thing/internal/GenericThingProvider.xtend b/bundles/model/org.eclipse.smarthome.model.thing/src/org/eclipse/smarthome/model/thing/internal/GenericThingProvider.xtend index d4ccc5c13ba..7ead6ed0743 100644 --- a/bundles/model/org.eclipse.smarthome.model.thing/src/org/eclipse/smarthome/model/thing/internal/GenericThingProvider.xtend +++ b/bundles/model/org.eclipse.smarthome.model.thing/src/org/eclipse/smarthome/model/thing/internal/GenericThingProvider.xtend @@ -319,7 +319,8 @@ class GenericThingProvider extends AbstractProvider implements ThingProvi } def private getParentPath(ThingUID bridgeUID) { - var bridgeIds = bridgeUID.bridgeIds + val bridgeIds = newArrayList + bridgeIds.addAll(bridgeUID.bridgeIds) bridgeIds.add(bridgeUID.id) return bridgeIds } diff --git a/bundles/test/org.eclipse.smarthome.magic/META-INF/MANIFEST.MF b/bundles/test/org.eclipse.smarthome.magic/META-INF/MANIFEST.MF index 592e4fcbabf..38f1e2295d8 100644 --- a/bundles/test/org.eclipse.smarthome.magic/META-INF/MANIFEST.MF +++ b/bundles/test/org.eclipse.smarthome.magic/META-INF/MANIFEST.MF @@ -17,7 +17,11 @@ Import-Package: javax.servlet.http, org.eclipse.jdt.annotation;resolution:=optional, org.eclipse.smarthome.config.core, + org.eclipse.smarthome.config.core.metadata, org.eclipse.smarthome.config.discovery, + org.eclipse.smarthome.core.common, + org.eclipse.smarthome.core.common.registry, + org.eclipse.smarthome.core.items, org.eclipse.smarthome.core.library.types, org.eclipse.smarthome.core.thing, org.eclipse.smarthome.core.thing.binding, diff --git a/bundles/test/org.eclipse.smarthome.magic/src/main/java/org/eclipse/smarthome/magic/internal/metadata/MagicMetadataProvider.java b/bundles/test/org.eclipse.smarthome.magic/src/main/java/org/eclipse/smarthome/magic/internal/metadata/MagicMetadataProvider.java new file mode 100644 index 00000000000..b2351ff0a30 --- /dev/null +++ b/bundles/test/org.eclipse.smarthome.magic/src/main/java/org/eclipse/smarthome/magic/internal/metadata/MagicMetadataProvider.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.magic.internal.metadata; + +import static java.util.stream.Collectors.toList; +import static org.eclipse.smarthome.config.core.ConfigDescriptionParameterBuilder.create; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter; +import org.eclipse.smarthome.config.core.ConfigDescriptionParameter.Type; +import org.eclipse.smarthome.config.core.ParameterOption; +import org.eclipse.smarthome.config.core.metadata.MetadataConfigDescriptionProvider; +import org.osgi.service.component.annotations.Component; + +/** + * Describes the metadata for the "magic" namespace. + * + * @author Simon Kaufmann - initial contribution and API + * + */ +@Component +@NonNullByDefault +public class MagicMetadataProvider implements MetadataConfigDescriptionProvider { + + @Override + public String getNamespace() { + return "magic"; + } + + @Override + public @Nullable String getDescription(@Nullable Locale locale) { + return "Make items magic"; + } + + @Override + public @Nullable List getParameterOptions(@Nullable Locale locale) { + return Stream.of( // + new ParameterOption("just", "Just Magic"), // + new ParameterOption("pure", "Pure Magic") // + ).collect(toList()); + } + + @Override + public @Nullable List getParameters(String value, @Nullable Locale locale) { + switch (value) { + case "just": + return Stream.of( // + create("electric", Type.BOOLEAN).withLabel("Use Electricity").build() // + ).collect(toList()); + case "pure": + return Stream.of( // + create("spell", Type.TEXT).withLabel("Spell").withDescription("The exact spell to use").build(), // + create("price", Type.DECIMAL).withLabel("Price") + .withDescription("...because magic always comes with a price").build(), // + create("power", Type.INTEGER).withLabel("Power").withLimitToOptions(true).withOptions( // + Stream.of( // + new ParameterOption("0", "Very High"), // + new ParameterOption("1", "Incredible"), // + new ParameterOption("2", "Insane"), // + new ParameterOption("3", "Ludicrous") // + ).collect(toList())).build() // + ).collect(toList()); + } + return null; + } + +} diff --git a/bundles/test/org.eclipse.smarthome.magic/src/main/java/org/eclipse/smarthome/magic/internal/metadata/MagicMetadataUsingService.java b/bundles/test/org.eclipse.smarthome.magic/src/main/java/org/eclipse/smarthome/magic/internal/metadata/MagicMetadataUsingService.java new file mode 100644 index 00000000000..b9affe97610 --- /dev/null +++ b/bundles/test/org.eclipse.smarthome.magic/src/main/java/org/eclipse/smarthome/magic/internal/metadata/MagicMetadataUsingService.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2014,2018 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.smarthome.magic.internal.metadata; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.common.ThreadPoolManager; +import org.eclipse.smarthome.core.items.MetadataPredicates; +import org.eclipse.smarthome.core.items.MetadataRegistry; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Example service which makes use of the metadata of the "magic" namespace. + * + * @author Simon Kaufmann - initial contribution and API + * + */ +@NonNullByDefault +@Component(immediate = true) +public class MagicMetadataUsingService { + + private final Logger logger = LoggerFactory.getLogger(MagicMetadataUsingService.class); + private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("magic"); + + private @NonNullByDefault({}) MetadataRegistry metadataRegistry; + + private @Nullable ScheduledFuture job; + + @Activate + public void activate() { + job = scheduler.scheduleWithFixedDelay(() -> run(), 30, 30, TimeUnit.SECONDS); + } + + @Deactivate + public void deactivate() { + if (job != null) { + job.cancel(false); + job = null; + } + } + + private void run() { + metadataRegistry.stream().filter(MetadataPredicates.hasNamespace("magic")).forEach(metadata -> { + logger.info("Item {} is {} with {}", metadata.getUID().getItemName(), metadata.getValue(), + metadata.getConfiguration()); + }); + } + + @Reference + protected void setMetadataRegistry(MetadataRegistry metadataRegistry) { + this.metadataRegistry = metadataRegistry; + } + + protected void unsetMetadataRegistry(MetadataRegistry metadataRegistry) { + this.metadataRegistry = null; + } + +} diff --git a/docs/documentation/concepts/items.md b/docs/documentation/concepts/items.md index 2cc20854c47..9c29c3543f7 100644 --- a/docs/documentation/concepts/items.md +++ b/docs/documentation/concepts/items.md @@ -137,3 +137,23 @@ Here is a short table demonstrating conversions for the examples above: | Color | `HSBType` | • `OnOffType` - `OFF` if the brightness level in the `HSBType` equals 0, `ON` otherwise
• `PercentType` - the value for the brightness level in the `HSBType` | | Dimmer | `PercentType` | `OnOffType` - `OFF` if the brightness level indicated by the percent type equals 0, `ON` otherwise | | Rollershutter | `PercentType` | `UpDownType` - `UP` if the shutter level indicated by the percent type equals 0, `DOWN` if it equals 100, and `UnDefType.UNDEF` for any other value| + +## Item Metadata + +Sometimes additional information is required to be attached to items for certain use-cases. +This could be e.g. an application which needs some hints in order to render the items in a generic way or an integration with voice controlled assistants or any other services which access the items and need to understand their "meaning". + +For this purpose, such meta-information can be attached to items using disjunct namespaces so they won't conflict with each other. +Each metadata entry has a main value and optionally additional key/value pairs. +There can be metadata attached to an item for as many namespaces as desired, like in the following example: + + Switch "My Fan" { homekit="Fan.v2", alexa="Fan" [ type="oscillating", speedSteps=3 ] } + +The metadata can alternatively maintained via a dedicated REST endpoint and is included in the `EnrichedItemDTO` responses. + +Extensions which can infer some metadata automatically need to implement an register a `MetadataProvider` service in order to make them available to the system. +They may provision them from any source they like and also dynamically remove or add data. +They are also not restricted to a single namespace. + +The `MetadataRegistry` provides access for all extensions which need to read the item metadata programmatically. +It is the central place where additional information about items is kept.