UI component registries initial implementation (#1356)

* UI component registries initial implementation

This is an initial implementation of #1355.
It was simple enough to make to be proposed as
a PR already without waiting for remarks on the RFC.

The SitemapProvider for the `system:sitemap`
namespace as described in #1355 is not part of
this PR.

Signed-off-by: Yannick Schaus <github@schaus.net>
This commit is contained in:
Yannick Schaus 2020-02-13 21:36:54 +01:00 committed by GitHub
parent 11fa4fad4a
commit eea31e536d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1027 additions and 10 deletions

View File

@ -12,19 +12,28 @@
*/
package org.openhab.core.io.rest.ui.internal;
import java.security.InvalidParameterException;
import java.util.stream.Stream;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.io.rest.Stream2JSONInputStream;
import org.openhab.core.io.rest.ui.TileDTO;
import org.openhab.core.ui.components.RootUIComponent;
import org.openhab.core.ui.components.UIComponentRegistry;
import org.openhab.core.ui.components.UIComponentRegistryFactory;
import org.openhab.core.ui.tiles.Tile;
import org.openhab.core.ui.tiles.TileProvider;
import org.osgi.service.component.annotations.Component;
@ -58,6 +67,7 @@ public class UIResource implements RESTResource {
private UriInfo uriInfo;
private TileProvider tileProvider;
private UIComponentRegistryFactory componentRegistryFactory;
@GET
@Path("/tiles")
@ -69,9 +79,87 @@ public class UIResource implements RESTResource {
return Response.ok(new Stream2JSONInputStream(tiles)).build();
}
@GET
@Path("/components/{namespace}")
@Produces({ MediaType.APPLICATION_JSON })
@ApiOperation(value = "Get all registered UI components in the specified namespace.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = Tile.class) })
public Response getAllComponents(@PathParam("namespace") String namespace) {
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
Stream<RootUIComponent> components = registry.getAll().stream();
return Response.ok(new Stream2JSONInputStream(components)).build();
}
@GET
@Path("/components/{namespace}/{componentUID}")
@Produces({ MediaType.APPLICATION_JSON })
@ApiOperation(value = "Get a specific UI component in the specified namespace.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = Tile.class),
@ApiResponse(code = 404, message = "Component not found", response = Tile.class) })
public Response getComponentByUID(@PathParam("namespace") String namespace,
@PathParam("componentUID") String componentUID) {
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
RootUIComponent component = registry.get(componentUID);
if (component == null) {
return Response.status(Status.NOT_FOUND).build();
}
return Response.ok(component).build();
}
@POST
@Path("/components/{namespace}")
@Produces({ MediaType.APPLICATION_JSON })
@ApiOperation(value = "Add an UI component in the specified namespace.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = Tile.class) })
public Response addComponent(@PathParam("namespace") String namespace, RootUIComponent component) {
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
component.updateTimestamp();
RootUIComponent createdComponent = registry.add(component);
return Response.ok(createdComponent).build();
}
@PUT
@Path("/components/{namespace}/{componentUID}")
@Produces({ MediaType.APPLICATION_JSON })
@ApiOperation(value = "Update a specific UI component in the specified namespace.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = Tile.class),
@ApiResponse(code = 404, message = "Component not found", response = Tile.class) })
public Response updateComponent(@PathParam("namespace") String namespace,
@PathParam("componentUID") String componentUID, RootUIComponent component) {
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
RootUIComponent existingComponent = registry.get(componentUID);
if (existingComponent == null) {
return Response.status(Status.NOT_FOUND).build();
}
if (!componentUID.equals(component.getUID())) {
throw new InvalidParameterException(
"The component UID in the body of the request should match the UID in the URL");
}
component.updateTimestamp();
registry.update(component);
return Response.ok(component).build();
}
@DELETE
@Path("/components/{namespace}/{componentUID}")
@Produces({ MediaType.APPLICATION_JSON })
@ApiOperation(value = "Remove a specific UI component in the specified namespace.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = Tile.class),
@ApiResponse(code = 404, message = "Component not found", response = Tile.class) })
public Response deleteComponent(@PathParam("namespace") String namespace,
@PathParam("componentUID") String componentUID) {
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
RootUIComponent component = registry.get(componentUID);
if (component == null) {
return Response.status(Status.NOT_FOUND).build();
}
registry.remove(componentUID);
return Response.ok().build();
}
@Override
public boolean isSatisfied() {
return tileProvider != null;
return tileProvider != null && componentRegistryFactory != null;
}
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
@ -83,6 +171,15 @@ public class UIResource implements RESTResource {
this.tileProvider = null;
}
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
protected void setComponentRegistryFactory(UIComponentRegistryFactory componentRegistryFactory) {
this.componentRegistryFactory = componentRegistryFactory;
}
protected void unsetComponentRegistryFactory(UIComponentRegistryFactory componentRegistryFactory) {
this.componentRegistryFactory = null;
}
private TileDTO toTileDTO(Tile tile) {
return new TileDTO(tile.getName(), tile.getUrl(), tile.getOverlay(), tile.getImageUrl());
}

View File

@ -0,0 +1,179 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* 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.openhab.core.ui.components;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.Identifiable;
import org.openhab.core.config.core.dto.ConfigDescriptionDTO;
/**
* A root component is a special type of {@link Component} at the root of the hierarchy.
* It has a number of specific parameters, a set of tags, a timestamp, some configurable
* parameters ("props") and is identifiable by its UID (generally a GUID).
*
* @author Yannick Schaus - Initial contribution
*/
@NonNullByDefault
public class RootUIComponent extends UIComponent implements Identifiable<String> {
String uid;
Set<String> tags = new HashSet<String>();
ConfigDescriptionDTO props;
@Nullable
Date timestamp;
/**
* Constructs a root component.
*
* @param name the name of the UI component to render the card on client frontends, ie. "HbCard"
*/
public RootUIComponent(String name) {
super(name);
this.uid = UUID.randomUUID().toString();
this.props = new ConfigDescriptionDTO(null, new ArrayList<>(), new ArrayList<>());
}
/**
* Constructs a root component with a specific UID.
*
* @param uid the UID of the new card
* @param name the name of the UI component to render the card on client frontends, ie. "HbCard"
*/
public RootUIComponent(String uid, String name) {
super(name);
this.uid = uid;
this.props = new ConfigDescriptionDTO(null, new ArrayList<>(), new ArrayList<>());
}
@Override
public String getUID() {
return uid;
}
/**
* Gets the set of tags attached to the component
*
* @return the card tags
*/
public Set<String> getTags() {
return tags;
}
/**
* Gets the timestamp of the component
*
* @return the timestamp
*/
public @Nullable Date getTimestamp() {
return timestamp;
}
/**
* Sets the specified timestamp of the component
*
* @param date the timestamp
*/
public void setTimestamp(Date date) {
this.timestamp = date;
}
/**
* Updates the timestamp of the component to the current date & time.
*/
public void updateTimestamp() {
this.timestamp = new Date();
}
/**
* Returns whether the component has a certain tag
*
* @param tag the tag to check
* @return true if the component is tagged with the specified tag
*/
public boolean hasTag(String tag) {
return (tags != null && tags.contains(tag));
}
/**
* Adds a tag to the component
*
* @param tag the tag to add
*/
public void addTag(String tag) {
this.tags.add(tag);
}
/**
* Adds several tags to the component
*
* @param tags the tags to add
*/
public void addTags(Collection<String> tags) {
this.tags.addAll(tags);
}
/**
* Adds several tags to the component
*
* @param tags the tags to add
*/
public void addTags(String... tags) {
this.tags.addAll(Arrays.asList(tags));
}
/**
* Removes a tag on a component
*
* @param tag the tag to remove
*/
public void removeTag(String tag) {
this.tags.remove(tag);
}
/**
* Removes all tags on the component
*/
public void removeAllTags() {
this.tags.clear();
}
/**
* Gets the configurable parameters ("props") of the component
*
* @return the configurable parameters
*/
public ConfigDescriptionDTO getProps() {
return props;
}
/**
* Sets the configurable parameters ("props") of the component
*
* @param props the configurable parameters
*/
public void setProps(ConfigDescriptionDTO props) {
this.props = props;
}
}

View File

@ -0,0 +1,131 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* 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.openhab.core.ui.components;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* An UIComponent represents a piece of UI element for a client frontend to render; it is kept very simple and delegates
* the actual rendering and behavior to the frontend.
*
* It has a reference to a component's name as defined by the frontend, a map of configuration parameters, and several
* named "slots", or placeholders, which may contain other sub-components, thus defining a tree.
*
* No checks are performed on the actual validity of configuration parameters and their values, the validity of a
* particular slot for a certain component or the validity of certain types of sub-components within a particular slot:
* that is the frontend's responsibility.
*
* @author Yannick Schaus - Initial contribution
*/
public class UIComponent {
String component;
Map<String, Object> config;
Map<String, List<UIComponent>> slots = null;
/**
* Constructs a component by its type name - component names are not arbitrary, they are defined by the target
* frontend.
*
* @param componentType type of the component as known to the frontend
*/
public UIComponent(String componentType) {
super();
this.component = componentType;
this.config = new HashMap<String, Object>();
}
/**
* Retrieves the type of the component.
*
* @return the component type
*/
public String getType() {
return component;
}
/**
* Gets all the configuration parameters of the component
*
* @return the map of configuration parameters
*/
public Map<String, Object> getConfig() {
return config;
}
/**
* Adds a new configuration parameter to the component
*
* @param key the parameter key
* @param value the parameter value
*/
public void addConfig(String key, Object value) {
this.config.put(key, value);
}
/**
* Returns all the slots of the components including their sub-components
*
* @return the slots and their sub-components
*/
public Map<String, List<UIComponent>> getSlots() {
return slots;
}
/**
* Adds a new empty slot to the component
*
* @param slotName the name of the slot
* @return the empty list of components in the newly created slot
*/
public List<UIComponent> addSlot(String slotName) {
if (slots == null) {
slots = new HashMap<String, List<UIComponent>>();
}
List<UIComponent> newSlot = new ArrayList<UIComponent>();
this.slots.put(slotName, newSlot);
return newSlot;
}
/**
* Gets the list of sub-components in a slot
*
* @param slotName the name of the slot
* @return the list of sub-components in the slot
*/
public List<UIComponent> getSlot(String slotName) {
return this.slots.get(slotName);
}
/**
* Add a new sub-component to the specified slot. Creates the slot if necessary.
*
* @param slotName the slot to add the component to
* @param subComponent the sub-component to add
*/
public void addComponent(String slotName, UIComponent subComponent) {
List<UIComponent> slot;
if (slots == null || !slots.containsKey(slotName)) {
slot = addSlot(slotName);
} else {
slot = getSlot(slotName);
}
slot.add(subComponent);
}
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* 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.openhab.core.ui.components;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.common.registry.Registry;
/**
* A namespace-specific {@link Registry} for UI components.
* It is normally instantiated for a specific namespace by the {@link UIComponentRegistryFactory}.
*
* @author Yannick Schaus - Initial contribution
*/
@NonNullByDefault
public interface UIComponentRegistry extends Registry<RootUIComponent, String> {
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* 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.openhab.core.ui.components;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* A factory for {@link UIComponentRegistry} instances based on the namespace.
*
* @author Yannick Schaus - Initial contribution
*/
@NonNullByDefault
public interface UIComponentRegistryFactory {
/**
* Gets the {@link UIComponentRegistry} for the specified namespace.
*
* @param namespace the namespace
* @return a registry for UI elements in the namespace
*/
UIComponentRegistry getRegistry(String namespace);
}

View File

@ -0,0 +1,113 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* 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.openhab.core.ui.internal.components;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.AbstractProvider;
import org.openhab.core.common.registry.ManagedProvider;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService;
import org.openhab.core.ui.components.RootUIComponent;
/**
* A namespace-specific {@link ManagedProvider} for UI components.
*
* @author Yannick Schaus - Initial contribution
*/
@NonNullByDefault
public class UIComponentProvider extends AbstractProvider<RootUIComponent>
implements ManagedProvider<RootUIComponent, String> {
private String namespace;
private volatile Storage<RootUIComponent> storage;
/**
* Constructs a UI component provider for the specified namespace
*
* @param namespace UI components namespace of this provider
* @param storageService supporting storage service
*/
public UIComponentProvider(String namespace, StorageService storageService) {
this.namespace = namespace;
this.storage = storageService.getStorage("uicomponents_" + namespace.replace(':', '_'),
this.getClass().getClassLoader());
}
@Override
public Collection<RootUIComponent> getAll() {
List<RootUIComponent> components = new ArrayList<>();
for (RootUIComponent component : storage.getValues()) {
if (component != null) {
components.add(component);
}
}
return components;
}
@Override
public void add(@NonNull RootUIComponent element) {
if (StringUtils.isEmpty(element.getUID())) {
throw new IllegalArgumentException("Invalid UID");
}
if (storage.get(element.getUID()) != null) {
throw new IllegalArgumentException("Cannot add UI component to namespace " + namespace
+ ", because a component with same UID (" + element.getUID() + ") already exists.");
}
storage.put(element.getUID(), element);
notifyListenersAboutAddedElement(element);
}
@Override
public @Nullable RootUIComponent remove(@NonNull String key) {
RootUIComponent element = storage.remove(key);
if (element != null) {
notifyListenersAboutRemovedElement(element);
return element;
}
return null;
}
@Override
public @Nullable RootUIComponent update(@NonNull RootUIComponent element) {
if (storage.get(element.getUID()) != null) {
RootUIComponent oldElement = storage.put(element.getUID(), element);
if (oldElement != null) {
notifyListenersAboutUpdatedElement(oldElement, element);
return oldElement;
}
} else {
throw new IllegalArgumentException("Cannot update UI component " + element.getUID() + " in namespace "
+ namespace + " because it doesn't exist.");
}
return null;
}
@Override
public @Nullable RootUIComponent get(String key) {
if (StringUtils.isEmpty(key)) {
throw new IllegalArgumentException("Invalid UID");
}
return storage.get(key);
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* 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.openhab.core.ui.internal.components;
import java.util.HashMap;
import java.util.Map;
import org.openhab.core.storage.StorageService;
import org.openhab.core.ui.components.UIComponentRegistryFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Implementation for a {@link UIComponentRegistryFactory} using a {@link StorageService} and a
* {@link UIComponentProvider}.
*
* @author Yannick Schaus - Initial contribution
*/
@Component(service = UIComponentRegistryFactory.class, immediate = true)
public class UIComponentRegistryFactoryImpl implements UIComponentRegistryFactory {
Map<String, UIComponentRegistryImpl> registries = new HashMap<>();
@Reference
StorageService storageService;
@Override
public UIComponentRegistryImpl getRegistry(String namespace) {
if (registries.containsKey(namespace)) {
return registries.get(namespace);
} else {
UIComponentRegistryImpl registry = new UIComponentRegistryImpl(namespace, storageService);
registries.put(namespace, registry);
return registry;
}
}
}

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* 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.openhab.core.ui.internal.components;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.common.registry.AbstractRegistry;
import org.openhab.core.storage.StorageService;
import org.openhab.core.ui.components.RootUIComponent;
import org.openhab.core.ui.components.UIComponentRegistry;
/**
* Implementation of a {@link UIComponentRegistry} using a {@link UIComponentProvider}.
* It is instantiated by the {@link UIComponentRegistryFactoryImpl}.
*
* @author Yannick Schaus - Initial contribution
*/
@NonNullByDefault
public class UIComponentRegistryImpl extends AbstractRegistry<RootUIComponent, String, UIComponentProvider>
implements UIComponentRegistry {
String namespace;
StorageService storageService;
/**
* Constructs a UI component registry for the specified namespace.
*
* @param namespace UI components namespace of this registry
* @param storageService supporting storage service
*/
public UIComponentRegistryImpl(String namespace, StorageService storageService) {
super(null);
this.namespace = namespace;
this.storageService = storageService;
UIComponentProvider provider = new UIComponentProvider(namespace, storageService);
addProvider(provider);
setManagedProvider(provider);
}
}

View File

@ -0,0 +1,337 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* 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.openhab.core.ui.internal.components;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.eclipse.emf.common.util.EList;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.model.core.EventType;
import org.openhab.core.model.core.ModelRepositoryChangeListener;
import org.openhab.core.model.sitemap.SitemapProvider;
import org.openhab.core.model.sitemap.sitemap.LinkableWidget;
import org.openhab.core.model.sitemap.sitemap.Mapping;
import org.openhab.core.model.sitemap.sitemap.Sitemap;
import org.openhab.core.model.sitemap.sitemap.SitemapFactory;
import org.openhab.core.model.sitemap.sitemap.SitemapPackage;
import org.openhab.core.model.sitemap.sitemap.Widget;
import org.openhab.core.model.sitemap.sitemap.impl.ChartImpl;
import org.openhab.core.model.sitemap.sitemap.impl.ColorpickerImpl;
import org.openhab.core.model.sitemap.sitemap.impl.DefaultImpl;
import org.openhab.core.model.sitemap.sitemap.impl.FrameImpl;
import org.openhab.core.model.sitemap.sitemap.impl.GroupImpl;
import org.openhab.core.model.sitemap.sitemap.impl.ImageImpl;
import org.openhab.core.model.sitemap.sitemap.impl.ListImpl;
import org.openhab.core.model.sitemap.sitemap.impl.MappingImpl;
import org.openhab.core.model.sitemap.sitemap.impl.MapviewImpl;
import org.openhab.core.model.sitemap.sitemap.impl.SelectionImpl;
import org.openhab.core.model.sitemap.sitemap.impl.SetpointImpl;
import org.openhab.core.model.sitemap.sitemap.impl.SitemapImpl;
import org.openhab.core.model.sitemap.sitemap.impl.SliderImpl;
import org.openhab.core.model.sitemap.sitemap.impl.SwitchImpl;
import org.openhab.core.model.sitemap.sitemap.impl.TextImpl;
import org.openhab.core.model.sitemap.sitemap.impl.VideoImpl;
import org.openhab.core.model.sitemap.sitemap.impl.WebviewImpl;
import org.openhab.core.model.sitemap.sitemap.impl.WidgetImpl;
import org.openhab.core.ui.components.RootUIComponent;
import org.openhab.core.ui.components.UIComponent;
import org.openhab.core.ui.components.UIComponentRegistry;
import org.openhab.core.ui.components.UIComponentRegistryFactory;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This {@link SitemapProvider} provides sitemaps from all well-formed {@link RootUIComponent} found in a specific
* "system:sitemap" namespace.
*
* @author Yannick Schaus - Initial contribution
*/
@Component(service = SitemapProvider.class)
public class UIComponentSitemapProvider implements SitemapProvider, RegistryChangeListener<RootUIComponent> {
private final Logger logger = LoggerFactory.getLogger(UIComponentSitemapProvider.class);
public static final String SITEMAP_NAMESPACE = "system:sitemap";
private static final String SITEMAP_PREFIX = "uicomponents_";
private static final String SITEMAP_SUFFIX = ".sitemap";
private Map<String, Sitemap> sitemaps = new HashMap<>();
private UIComponentRegistryFactory componentRegistryFactory;
private UIComponentRegistry sitemapComponentRegistry;
private final Set<ModelRepositoryChangeListener> modelChangeListeners = new CopyOnWriteArraySet<>();
@Override
public @Nullable Sitemap getSitemap(@NonNull String sitemapName) {
buildSitemap(sitemapName.replaceFirst(SITEMAP_PREFIX, ""));
return sitemaps.get(sitemapName);
}
@Override
public @NonNull Set<@NonNull String> getSitemapNames() {
sitemaps.clear();
Collection<RootUIComponent> rootComponents = sitemapComponentRegistry.getAll();
// try building all sitemaps to leave the invalid ones out
for (RootUIComponent rootComponent : rootComponents) {
try {
Sitemap sitemap = buildSitemap(rootComponent);
sitemaps.put(sitemap.getName(), sitemap);
} catch (Exception e) {
logger.error("Cannot build sitemap {}", rootComponent.getUID(), e);
}
}
return sitemaps.keySet();
}
protected Sitemap buildSitemap(String sitemapName) {
RootUIComponent rootComponent = sitemapComponentRegistry.get(sitemapName);
if (rootComponent != null) {
try {
Sitemap sitemap = buildSitemap(rootComponent);
sitemaps.put(sitemap.getName(), sitemap);
return null;
} catch (Exception e) {
logger.error("Cannot build sitemap {}", rootComponent.getUID(), e);
}
}
return null;
}
protected Sitemap buildSitemap(RootUIComponent rootComponent) {
if (!"Sitemap".equals(rootComponent.getType())) {
throw new IllegalArgumentException("Root component type is not Sitemap");
}
SitemapImpl sitemap = (SitemapImpl) SitemapFactory.eINSTANCE.createSitemap();
sitemap.setName(SITEMAP_PREFIX + rootComponent.getUID());
sitemap.setLabel(rootComponent.getConfig().get("label").toString());
if (rootComponent.getSlots() != null && rootComponent.getSlots().containsKey("widgets")) {
for (UIComponent component : rootComponent.getSlot("widgets")) {
Widget widget = buildWidget(component);
if (widget != null) {
sitemap.getChildren().add(widget);
}
}
}
return sitemap;
}
protected Widget buildWidget(UIComponent component) {
Widget widget = null;
switch (component.getType()) {
case "Frame":
FrameImpl frameWidget = (FrameImpl) SitemapFactory.eINSTANCE.createFrame();
widget = frameWidget;
break;
case "Text":
TextImpl textWidget = (TextImpl) SitemapFactory.eINSTANCE.createText();
widget = textWidget;
break;
case "Group":
GroupImpl groupWidget = (GroupImpl) SitemapFactory.eINSTANCE.createGroup();
widget = groupWidget;
break;
case "Image":
ImageImpl imageWidget = (ImageImpl) SitemapFactory.eINSTANCE.createImage();
widget = imageWidget;
setWidgetPropertyFromComponentConfig(widget, component, "url", SitemapPackage.IMAGE__URL);
setWidgetPropertyFromComponentConfig(widget, component, "refresh", SitemapPackage.IMAGE__REFRESH);
break;
case "Video":
VideoImpl videoWidget = (VideoImpl) SitemapFactory.eINSTANCE.createVideo();
widget = videoWidget;
setWidgetPropertyFromComponentConfig(widget, component, "url", SitemapPackage.IMAGE__URL);
setWidgetPropertyFromComponentConfig(widget, component, "encoding", SitemapPackage.VIDEO__ENCODING);
break;
case "Chart":
ChartImpl chartWidget = (ChartImpl) SitemapFactory.eINSTANCE.createChart();
widget = chartWidget;
setWidgetPropertyFromComponentConfig(widget, component, "service", SitemapPackage.CHART__SERVICE);
setWidgetPropertyFromComponentConfig(widget, component, "refresh", SitemapPackage.CHART__REFRESH);
setWidgetPropertyFromComponentConfig(widget, component, "period", SitemapPackage.CHART__PERIOD);
setWidgetPropertyFromComponentConfig(widget, component, "legend", SitemapPackage.CHART__LEGEND);
break;
case "Webview":
WebviewImpl webviewWidget = (WebviewImpl) SitemapFactory.eINSTANCE.createWebview();
widget = webviewWidget;
setWidgetPropertyFromComponentConfig(widget, component, "height", SitemapPackage.WEBVIEW__HEIGHT);
setWidgetPropertyFromComponentConfig(widget, component, "url", SitemapPackage.WEBVIEW__URL);
break;
case "Switch":
SwitchImpl switchWidget = (SwitchImpl) SitemapFactory.eINSTANCE.createSwitch();
addWidgetMappings(switchWidget.getMappings(), component);
widget = switchWidget;
break;
case "Mapview":
MapviewImpl mapviewWidget = (MapviewImpl) SitemapFactory.eINSTANCE.createMapview();
widget = mapviewWidget;
setWidgetPropertyFromComponentConfig(widget, component, "height", SitemapPackage.WEBVIEW__HEIGHT);
break;
case "Slider":
SliderImpl sliderWidget = (SliderImpl) SitemapFactory.eINSTANCE.createSlider();
widget = sliderWidget;
setWidgetPropertyFromComponentConfig(widget, component, "minValue", SitemapPackage.SLIDER__MIN_VALUE);
setWidgetPropertyFromComponentConfig(widget, component, "maxValue", SitemapPackage.SLIDER__MAX_VALUE);
setWidgetPropertyFromComponentConfig(widget, component, "step", SitemapPackage.SLIDER__STEP);
setWidgetPropertyFromComponentConfig(widget, component, "switchEnabled",
SitemapPackage.SLIDER__SWITCH_ENABLED);
setWidgetPropertyFromComponentConfig(widget, component, "sendFrequency",
SitemapPackage.SLIDER__FREQUENCY);
break;
case "Selection":
SelectionImpl selectionWidget = (SelectionImpl) SitemapFactory.eINSTANCE.createSelection();
addWidgetMappings(selectionWidget.getMappings(), component);
widget = selectionWidget;
setWidgetPropertyFromComponentConfig(widget, component, "height", SitemapPackage.WEBVIEW__HEIGHT);
break;
case "List":
ListImpl listWidget = (ListImpl) SitemapFactory.eINSTANCE.createList();
widget = listWidget;
setWidgetPropertyFromComponentConfig(widget, component, "separator", SitemapPackage.LIST__SEPARATOR);
break;
case "Setpoint":
SetpointImpl setpointWidget = (SetpointImpl) SitemapFactory.eINSTANCE.createSetpoint();
widget = setpointWidget;
setWidgetPropertyFromComponentConfig(widget, component, "minValue", SitemapPackage.SETPOINT__MIN_VALUE);
setWidgetPropertyFromComponentConfig(widget, component, "maxValue", SitemapPackage.SETPOINT__MAX_VALUE);
setWidgetPropertyFromComponentConfig(widget, component, "step", SitemapPackage.SETPOINT__STEP);
break;
case "Colorpicker":
ColorpickerImpl colorpickerWidget = (ColorpickerImpl) SitemapFactory.eINSTANCE.createColorpicker();
widget = colorpickerWidget;
setWidgetPropertyFromComponentConfig(widget, component, "frequency",
SitemapPackage.COLORPICKER__FREQUENCY);
break;
case "Default":
DefaultImpl defaultWidget = (DefaultImpl) SitemapFactory.eINSTANCE.createDefault();
widget = defaultWidget;
setWidgetPropertyFromComponentConfig(widget, component, "height", SitemapPackage.DEFAULT__HEIGHT);
break;
default:
logger.warn("Unknown sitemap component type {}", component.getType());
break;
}
if (widget != null) {
setWidgetPropertyFromComponentConfig(widget, component, "label", SitemapPackage.WIDGET__LABEL);
setWidgetPropertyFromComponentConfig(widget, component, "icon", SitemapPackage.WIDGET__ICON);
setWidgetPropertyFromComponentConfig(widget, component, "item", SitemapPackage.WIDGET__ITEM);
if (widget instanceof LinkableWidget) {
LinkableWidget linkableWidget = (LinkableWidget) widget;
if (component.getSlots() != null && component.getSlots().containsKey("widgets")) {
for (UIComponent childComponent : component.getSlot("widgets")) {
Widget childWidget = buildWidget(childComponent);
if (childWidget != null) {
linkableWidget.getChildren().add(childWidget);
}
}
}
}
// TODO: process visibility & color rules
}
return widget;
}
private void setWidgetPropertyFromComponentConfig(Widget widget, UIComponent component, String configParamName,
int feature) {
if (component == null || component.getConfig() == null) {
return;
}
Object value = component.getConfig().get(configParamName);
if (value == null) {
return;
}
WidgetImpl widgetImpl = (WidgetImpl) widget;
widgetImpl.eSet(feature, ConfigUtil.normalizeType(value));
}
private void addWidgetMappings(EList<Mapping> mappings, UIComponent component) {
if (component.getConfig() != null && component.getConfig().containsKey("mappings")) {
if (component.getConfig().get("mappings") instanceof Collection<?>) {
for (Object sourceMapping : (Collection<?>) component.getConfig().get("mappings")) {
if (sourceMapping instanceof String) {
String cmd = sourceMapping.toString().split("=")[0].trim();
String label = sourceMapping.toString().split("=")[1].trim();
MappingImpl mapping = (MappingImpl) SitemapFactory.eINSTANCE.createMapping();
mapping.setCmd(cmd);
mapping.setLabel(label);
mappings.add(mapping);
}
}
}
}
}
@Override
public void addModelChangeListener(@NonNull ModelRepositoryChangeListener listener) {
modelChangeListeners.add(listener);
}
@Override
public void removeModelChangeListener(@NonNull ModelRepositoryChangeListener listener) {
modelChangeListeners.remove(listener);
}
@Override
public void added(RootUIComponent element) {
for (ModelRepositoryChangeListener listener : modelChangeListeners) {
listener.modelChanged(SITEMAP_PREFIX + element.getUID() + SITEMAP_SUFFIX, EventType.ADDED);
}
}
@Override
public void removed(RootUIComponent element) {
for (ModelRepositoryChangeListener listener : modelChangeListeners) {
listener.modelChanged(SITEMAP_PREFIX + element.getUID() + SITEMAP_SUFFIX, EventType.REMOVED);
}
}
@Override
public void updated(RootUIComponent oldElement, RootUIComponent element) {
for (ModelRepositoryChangeListener listener : modelChangeListeners) {
listener.modelChanged(SITEMAP_PREFIX + element.getUID() + SITEMAP_SUFFIX, EventType.MODIFIED);
}
}
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
protected void setComponentRegistryFactory(UIComponentRegistryFactory componentRegistryFactory) {
this.componentRegistryFactory = componentRegistryFactory;
this.sitemapComponentRegistry = this.componentRegistryFactory.getRegistry(SITEMAP_NAMESPACE);
this.sitemapComponentRegistry.addRegistryChangeListener(this);
}
protected void unsetComponentRegistryFactory(UIComponentRegistryFactory componentRegistryFactory) {
this.componentRegistryFactory = null;
this.sitemapComponentRegistry.removeRegistryChangeListener(this);
this.sitemapComponentRegistry = null;
}
}

View File

@ -63,7 +63,7 @@ public abstract class AbstractRegistry<E extends Identifiable<K>, K, P extends P
private final Logger logger = LoggerFactory.getLogger(AbstractRegistry.class);
private final Class<P> providerClazz;
private final @Nullable Class<P> providerClazz;
private @Nullable ServiceTracker<P, P> providerTracker;
private final ReentrantReadWriteLock elementLock = new ReentrantReadWriteLock();
@ -86,18 +86,19 @@ public abstract class AbstractRegistry<E extends Identifiable<K>, K, P extends P
* @param providerClazz the class of the providers (see e.g. {@link AbstractRegistry#addProvider(Provider)}), null
* if no providers should be tracked automatically after activation
*/
protected AbstractRegistry(final Class<P> providerClazz) {
protected AbstractRegistry(final @Nullable Class<P> providerClazz) {
this.providerClazz = providerClazz;
}
protected void activate(final BundleContext context) {
/*
* The handlers for 'add' and 'remove' the services implementing the provider class (cardinality is
* multiple) rely on an active component.
* To grant that the add and remove functions are called only for an active component, we use a provider
* tracker.
*/
if (providerClazz != null) {
/*
* The handlers for 'add' and 'remove' the services implementing the provider class (cardinality is
* multiple) rely on an active component.
* To grant that the add and remove functions are called only for an active component, we use a provider
* tracker.
*/
Class<P> providerClazz = this.providerClazz;
providerTracker = new ProviderTracker(context, providerClazz);
providerTracker.open();
}
@ -327,7 +328,7 @@ public abstract class AbstractRegistry<E extends Identifiable<K>, K, P extends P
}
@Override
public E update(E element) {
public @Nullable E update(E element) {
return managedProvider.orElseThrow(() -> new IllegalStateException("ManagedProvider is not available"))
.update(element);
}

View File

@ -13,6 +13,7 @@
package org.openhab.core.common.registry;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link ManagedProvider} is a specific {@link Provider} that enables to
@ -41,6 +42,7 @@ public interface ManagedProvider<E extends Identifiable<K>, K> extends Provider<
* @return element that was removed, or null if no element with the given
* key exists
*/
@Nullable
E remove(@NonNull K key);
/**
@ -50,6 +52,7 @@ public interface ManagedProvider<E extends Identifiable<K>, K> extends Provider<
* @return returns the old element or null if no element with the same key
* exists
*/
@Nullable
E update(@NonNull E element);
/**
@ -59,6 +62,7 @@ public interface ManagedProvider<E extends Identifiable<K>, K> extends Provider<
* @param key key
* @return returns element or null, if no element for the given key exists
*/
@Nullable
E get(K key);
}