org.openremote.server.inventory.DiscoveryService.java Source code

Java tutorial

Introduction

Here is the source code for org.openremote.server.inventory.DiscoveryService.java

Source

/*
 * Copyright 2015, OpenRemote Inc.
 *
 * See the CONTRIBUTORS.txt file in the distribution for a
 * full listing of individual contributors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package org.openremote.server.inventory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.camel.*;
import org.apache.camel.impl.ParameterConfiguration;
import org.apache.camel.impl.UriEndpointComponent;
import org.apache.camel.model.RouteDefinition;
import org.apache.camel.spi.UriParam;
import org.apache.camel.util.CamelContextHelper;
import org.openremote.devicediscovery.domain.DiscoveredDeviceAttrDTO;
import org.openremote.devicediscovery.domain.DiscoveredDeviceDTO;
import org.openremote.server.event.EventService;
import org.openremote.server.util.IdentifierUtil;
import org.openremote.shared.component.ValidationGroupDiscovery;
import org.openremote.shared.event.InventoryDevicesUpdatedEvent;
import org.openremote.shared.inventory.Adapter;
import org.openremote.shared.inventory.Device;
import org.seamless.util.Exceptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.Properties;

// TODO Part of this class should be moved to zwave project
import static org.openremote.protocol.zwave.model.commandclasses.DeviceDiscoveryCommandClassVisitor.*;
import static org.openremote.server.util.JsonUtil.JSON;

public class DiscoveryService implements StaticService {

    private static final Logger LOG = LoggerFactory.getLogger(DiscoveryService.class);

    public static final String COMPONENT_TYPE = "org-openremote-component-type";
    public static final String COMPONENT_LABEL = "org-openremote-component-label";
    public static final String COMPONENT_DISCOVERY_ENDPOINT = "org-openremote-component-discoveryEndpoint";

    public static final String DEVICE_PROPERTY_DISCOVERY_ENDPOINT = "discoveryEndpoint";

    final protected CamelContext context;
    final protected DeviceService deviceService;
    final protected DeviceLibraryService deviceLibraryService;

    final protected List<Adapter> adapters = new ArrayList<>();

    public DiscoveryService(CamelContext context, DeviceService deviceService,
            DeviceLibraryService deviceLibraryService) {
        this.context = context;
        this.deviceService = deviceService;
        this.deviceLibraryService = deviceLibraryService;
    }

    @Override
    public void start() throws Exception {
        // TODO Persistence! We should maintain adapter properties in the database, don't forget to clean database when an adapter is gone.
        this.adapters.clear();
        this.adapters.addAll(Arrays.asList(findAdapters(context)));
    }

    @Override
    public void stop() throws Exception {
    }

    public Adapter[] getAdapters() {
        synchronized (adapters) {
            return adapters.toArray(new Adapter[adapters.size()]);
        }
    }

    public Adapter getAdapter(@Header("id") String id) {
        synchronized (adapters) {
            for (Adapter adapter : getAdapters()) {
                if (adapter.getId().equals(id))
                    return adapter;
            }
            return null;
        }
    }

    public boolean putAdapter(Adapter adapter) {
        LOG.debug("Putting adapter: " + adapter);
        synchronized (adapters) {
            boolean found = false;
            Iterator<Adapter> it = adapters.iterator();
            while (it.hasNext()) {
                Adapter existingAdapter = it.next();
                if (existingAdapter.getId().equals(adapter.getId())) {
                    it.remove();
                    found = true;
                    break;
                }
            }
            if (found) {
                adapters.add(adapter);
            }
            return found;
        }
    }

    public void triggerDiscovery(Adapter adapter) {
        LOG.debug("Trigger discovery for adapter: " + adapter);
        String discoveryEndpointUri = createDiscoveryEndpointUri(adapter);

        RouteDefinition routeDefinition = createDiscoveryRoute(adapter, discoveryEndpointUri);
        String routeId = routeDefinition.getId();

        try {

            if (context.getRoute(routeId) == null) {
                try {
                    LOG.debug("Adding and starting discovery route: " + discoveryEndpointUri);
                    context.addRouteDefinition(routeDefinition);
                    context.startRoute(routeId);
                } catch (ConstraintViolationException ex) {
                    LOG.debug("Adapter endpoint constraint violation: " + ex);
                    StringBuilder sb = new StringBuilder();
                    for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
                        sb.append(constraintViolation.getPropertyPath()).append(" => ")
                                .append(constraintViolation.getMessage());
                    }
                    throw new IllegalArgumentException("Missing properties: " + sb.toString());
                } catch (Exception ex) {
                    LOG.debug("Error starting route: " + discoveryEndpointUri, ex);
                    throw new IllegalStateException(ex);
                }
            }

            LOG.debug("Triggering discovery endpoint: " + discoveryEndpointUri);
            try {
                context.createProducerTemplate().sendBody(discoveryEndpointUri, null);
            } catch (Exception ex) {
                LOG.debug("Error triggering discovery: " + discoveryEndpointUri, ex);
                Throwable cause = Exceptions.unwrap(ex);
                throw new IllegalArgumentException(cause.getMessage());
            }

        } catch (RuntimeException ex) {
            // Must be able to retry later, so remove
            try {
                context.removeRoute(routeId);
            } catch (Exception e) {
                // Ignore
            }
            throw ex;
        }
    }

    protected RouteDefinition createDiscoveryRoute(Adapter adapter, String discoveryEndpointUri) {
        String routeId = "discovery-" + discoveryEndpointUri.hashCode();
        return new RouteDefinition(discoveryEndpointUri).id(routeId).autoStartup(false).process(exchange -> {

            Object discoveryResult = exchange.getIn().getBody();
            if (discoveryResult instanceof List) {
                List resultList = (List) discoveryResult;
                if (resultList.size() == 0)
                    return;

                LOG.debug("Processing discovered device list: " + resultList.size());
                List<Device> devices = new ArrayList<>();

                // TODO Ugly hack to support v2 and v3 device model
                Object firstResult = resultList.get(0);
                if (firstResult instanceof DiscoveredDeviceDTO) {
                    // V2
                    List<DiscoveredDeviceDTO> discoveredDeviceDTOs = exchange.getIn().getBody(List.class);

                    for (DiscoveredDeviceDTO deviceDTO : discoveredDeviceDTOs) {
                        List<DiscoveredDeviceAttrDTO> attributes = deviceDTO.getDeviceAttrs();

                        String discoveryCommand = null;
                        for (DiscoveredDeviceAttrDTO attribute : attributes) {
                            if (attribute.getName().equals(ATTR_NAME_DEVICE_DISCOVERY_COMMAND)) {
                                discoveryCommand = attribute.getValue();
                                break;
                            }
                        }

                        if (discoveryCommand == null) {
                            LOG.warn("Missing discovery command attribute, skipping discovered: " + deviceDTO);
                            continue;
                        }

                        Device converted = convertV2Device(adapter, deviceDTO, discoveryEndpointUri);

                        if (ATTR_VALUE_DEVICE_DISCOVERY_COMMAND_ADD
                                .equals(discoveryCommand.toLowerCase(Locale.ROOT))) {
                            devices.add(converted);
                        } else if (ATTR_VALUE_DEVICE_DISCOVERY_COMMAND_DELETE
                                .equals(discoveryCommand.toLowerCase(Locale.ROOT))) {
                            deviceService.setDeviceOffline(converted.getId());
                        } else {
                            LOG.warn("Unsupported discovery command attribute, skipping discovered: " + deviceDTO);
                        }
                    }

                } else if (firstResult instanceof Device) {
                    // V3
                    devices = (List<Device>) exchange.getIn().getBody(List.class);
                }

                Device[] initializedDevices = deviceLibraryService.initializeDevices(adapter, devices);
                deviceService.addDevices(initializedDevices);

                EventService eventService = context.hasService(EventService.class);
                if (eventService != null)
                    eventService.sendEvent(new InventoryDevicesUpdatedEvent());
            }
        });
    }

    protected String createDiscoveryEndpointUri(Adapter adapter) {
        ComponentConfiguration discoveryConfig = context.getComponent(adapter.getId())
                .createComponentConfiguration();
        discoveryConfig.setBaseUri(adapter.getDiscoveryEndpoint());
        try {
            ObjectNode properties = JSON.readValue(adapter.getProperties(), ObjectNode.class);
            Iterator<String> it = properties.fieldNames();
            while (it.hasNext()) {
                String propertyName = it.next();
                JsonNode property = properties.get(propertyName);
                if (property.hasNonNull("value")) {
                    discoveryConfig.setParameter(propertyName, property.get("value").asText());
                }
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        LOG.debug("Using discovery URI: " + discoveryConfig.getUriString());
        return discoveryConfig.getUriString();
    }

    protected Adapter[] findAdapters(CamelContext context) throws Exception {
        List<Adapter> result = new ArrayList<>();
        for (Map.Entry<String, Properties> entry : CamelContextHelper.findComponents(context).entrySet()) {
            Object componentType = entry.getValue().get(COMPONENT_TYPE);
            if (componentType != null) {
                Component component = context.getComponent(entry.getKey());
                if (component == null) {
                    throw new IllegalStateException(
                            "Configured component not available in context: " + entry.getKey());
                }
                if (component instanceof UriEndpointComponent) {
                    UriEndpointComponent uriEndpointComponent = (UriEndpointComponent) component;
                    result.add(createAdapter(entry.getKey(), componentType.toString(), uriEndpointComponent,
                            entry.getValue()));
                } else {
                    LOG.warn("Component should implement " + UriEndpointComponent.class.getName() + ": "
                            + entry.getKey());
                }
            }
        }
        return result.toArray(new Adapter[result.size()]);
    }

    protected Adapter createAdapter(String name, String componentType, UriEndpointComponent component,
            Properties componentProperties) {
        LOG.info("Creating adapter for component: " + name);
        Class<? extends Endpoint> endpointClass = component.getEndpointClass();

        String label = componentProperties.containsKey(COMPONENT_LABEL)
                ? componentProperties.get(COMPONENT_LABEL).toString()
                : null;

        if (label == null)
            throw new RuntimeException("Component missing label property: " + name);

        String discoveryEndpoint = componentProperties.containsKey(COMPONENT_DISCOVERY_ENDPOINT)
                ? componentProperties.get(COMPONENT_DISCOVERY_ENDPOINT).toString()
                : null;

        Adapter adapter = new Adapter(label, name, componentType, discoveryEndpoint);

        ComponentConfiguration config = component.createComponentConfiguration();
        ObjectNode properties = JSON.createObjectNode();
        for (Map.Entry<String, ParameterConfiguration> configEntry : config.getParameterConfigurationMap()
                .entrySet()) {
            try {
                Field field = endpointClass.getDeclaredField(configEntry.getKey());
                if (field.isAnnotationPresent(UriParam.class)) {
                    UriParam uriParam = field.getAnnotation(UriParam.class);

                    ObjectNode property = JSON.createObjectNode();

                    if (uriParam.label().length() > 0) {
                        property.put("label", uriParam.label());
                    }

                    if (uriParam.description().length() > 0) {
                        property.put("description", uriParam.description());

                    }

                    if (uriParam.defaultValue().length() > 0) {
                        property.put("defaultValue", uriParam.defaultValue());
                    }

                    if (uriParam.defaultValueNote().length() > 0) {
                        property.put("defaultValueNote", uriParam.defaultValueNote());
                    }

                    if (String.class.isAssignableFrom(field.getType())) {
                        property.put("type", "string");
                    } else if (Long.class.isAssignableFrom(field.getType())) {
                        property.put("type", "long");
                    } else if (Integer.class.isAssignableFrom(field.getType())) {
                        property.put("type", "integer");
                    } else if (Double.class.isAssignableFrom(field.getType())) {
                        property.put("type", "double");
                    } else if (Boolean.class.isAssignableFrom(field.getType())) {
                        property.put("type", "boolean");
                    } else {
                        throw new RuntimeException(
                                "Unsupported type of adapter endpoint property '" + name + "': " + field.getType());
                    }

                    if (field.isAnnotationPresent(NotNull.class)) {
                        for (Class<?> group : field.getAnnotation(NotNull.class).groups()) {
                            if (ValidationGroupDiscovery.class.isAssignableFrom(group)) {
                                property.put("required", true);
                                break;
                            }
                        }
                    }

                    String propertyName = uriParam.name().length() != 0 ? uriParam.name() : field.getName();
                    LOG.debug("Adding adapter property '" + propertyName + "': " + property);
                    properties.set(propertyName, property);
                }
            } catch (NoSuchFieldException ex) {
                // Ignoring config parameter if there is no annotated field on endpoint class
                // TODO: Inheritance of endpoint classes? Do we care?
            }
        }

        try {
            adapter.setProperties(JSON.writeValueAsString(properties));
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }

        return adapter;
    }

    protected Device convertV2Device(Adapter adapter, DiscoveredDeviceDTO deviceDTO, String discoveryEndpointUri) {
        List<DiscoveredDeviceAttrDTO> attributes = deviceDTO.getDeviceAttrs();

        String deviceId = null;
        // TODO: Will all protocols have an identifier attribute?
        for (DiscoveredDeviceAttrDTO attribute : attributes) {
            if (ATTR_NAME_NODE_ID.equals(attribute.getName())) {
                deviceId = attribute.getValue();
                break;
            }
        }
        if (deviceId == null) {
            LOG.warn("Missing device identifier attribute, skipping discovered: " + deviceDTO);
            return null;
        }

        String isRootDevice = null;
        // TODO: Will all protocols have this attribute?
        for (DiscoveredDeviceAttrDTO attribute : attributes) {
            if (ATTR_NAME_IS_ROOT.equals(attribute.getName())) {
                isRootDevice = attribute.getValue();
                break;
            }
        }

        String deviceLabel = deviceDTO.getName() != null && deviceDTO.getName().length() > 0 ? deviceDTO.getName()
                : deviceId;

        // TODO How do we get a stable unique ID?
        try {
            deviceId = IdentifierUtil.getEncodedHash(adapter.getId().getBytes("utf-8"), deviceId.getBytes("utf-8"),
                    deviceLabel.getBytes("utf-8"),
                    isRootDevice != null ? isRootDevice.getBytes("utf-8") : new byte[0]);
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException(ex);
        }
        ;

        Device device = new Device(deviceLabel, deviceId, "urn:openremote:adapter:" + adapter.getId());

        device.setStatus(Device.Status.UNINITIALIZED);

        ObjectNode properties = JSON.createObjectNode();

        properties.put(DEVICE_PROPERTY_DISCOVERY_ENDPOINT, discoveryEndpointUri);

        for (DiscoveredDeviceAttrDTO attribute : attributes) {
            if (ATTR_NAME_DEVICE_DISCOVERY_COMMAND.equals(attribute.getName()))
                continue;
            if (attribute.getName() != null && attribute.getValue() != null) {
                properties.put(attribute.getName(), attribute.getValue());
            }
        }

        try {
            device.setProperties(JSON.writeValueAsString(properties));
        } catch (JsonProcessingException ex) {
            throw new RuntimeException(ex);
        }

        return device;
    }

}