All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openremote.manager.asset.AssetModelService Maven / Gradle / Ivy

/*
 * Copyright 2020, 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 .
 */
package org.openremote.manager.asset;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.TokenBuffer;
import org.apache.camel.builder.RouteBuilder;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.persistence.PersistenceService;
import org.openremote.container.timer.TimerService;
import org.openremote.manager.event.ClientEventService;
import org.openremote.manager.gateway.GatewayService;
import org.openremote.manager.security.ManagerIdentityService;
import org.openremote.manager.web.ManagerWebService;
import org.openremote.model.AssetModelProvider;
import org.openremote.model.Container;
import org.openremote.model.ContainerService;
import org.openremote.model.asset.AssetDescriptor;
import org.openremote.model.asset.AssetTypeInfo;
import org.openremote.model.attribute.MetaMap;
import org.openremote.model.syslog.SyslogCategory;
import org.openremote.model.util.TextUtil;
import org.openremote.model.util.ValueUtil;
import org.openremote.model.value.AttributeDescriptor;
import org.openremote.model.value.MetaItemDescriptor;
import org.openremote.model.value.ValueDescriptor;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.openremote.model.syslog.SyslogCategory.MODEL_AND_VALUES;

// TODO: Implement model client event support
/**
 * A service for abstracting {@link org.openremote.model.util.ValueUtil} and handling local model requests vs
 * {@link org.openremote.model.asset.impl.GatewayAsset} model requests. It also manages the {@link
 * org.openremote.model.asset.AssetModelResource} and provides support for model requests via the client event bus.
 * 

* Also implements an {@link AssetModelProvider} that loads descriptors from the file system; specifically in * {@link PersistenceService#OR_STORAGE_DIR}/{@link #DIRECTORY_NAME}; file structure should be a JSON representation * of {@link AssetTypeInfo}. */ public class AssetModelService extends RouteBuilder implements ContainerService, AssetModelProvider { protected static ObjectMapper JSON = ValueUtil.JSON.copy() .addMixIn(AssetTypeInfo.class, AssetTypeInfoMixin.class); @JsonDeserialize(using = AssetTypeInfoDeserializer.class) private static final class AssetTypeInfoMixin { @JsonDeserialize @JsonSerialize MetaItemDescriptor[] metaItemDescriptors; @JsonDeserialize @JsonSerialize ValueDescriptor[] valueDescriptors; //AttributeDescriptor[] getAttributeDescriptors() { return null; } } /** * Extracts the {@link ValueDescriptor}s then {@link MetaItemDescriptor}s then {@link AttributeDescriptor} * making each available to the next during deserialization. */ private static final class AssetTypeInfoDeserializer extends StdDeserializer { private static final JavaType VALUE_DESCRIPTOR_TYPE = TypeFactory.defaultInstance().constructType(ValueDescriptor[].class); private static final JavaType META_ITEM_DESCRIPTOR_TYPE = TypeFactory.defaultInstance().constructType(MetaItemDescriptor[].class); private static final JavaType ATTRIBUTE_DESCRIPTOR_TYPE = TypeFactory.defaultInstance().constructType(AttributeDescriptor[].class); private static final JavaType ASSET_DESCRIPTOR_TYPE = TypeFactory.defaultInstance().constructType(AssetDescriptor.class); private AssetTypeInfoDeserializer() { super(AssetTypeInfo.class); } @SuppressWarnings("unchecked") @Override public AssetTypeInfo deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JacksonException { if (!jp.isExpectedStartObjectToken()) { throw JsonMappingException.from(jp, "Must be an object"); } TokenBuffer attributeDescriptorBuffer = null; TokenBuffer metaItemDescriptorBuffer = null; AssetDescriptor assetDescriptor = null; AttributeDescriptor[] attributeDescriptors = null; final AtomicReference valueDescriptors = new AtomicReference<>(); final AtomicReference metaItemDescriptors = new AtomicReference<>(); Function> valueDescriptorProvider = (name) -> { ValueDescriptor found = null; if (valueDescriptors.get() != null) { found = Arrays.stream(valueDescriptors.get()).filter(vd -> vd.getName().equals(name)).findFirst().orElse(null); } if (found == null) { found = ValueUtil.getValueDescriptor(name).orElse(null); } return found; }; Function> metaDescriptorProvider = (name) -> { if (metaItemDescriptors.get() != null) { return Arrays.stream(metaItemDescriptors.get()).filter(mid -> mid.getName().equals(name)).findFirst().orElse(null); } return null; }; ctxt.setAttribute(ValueDescriptor.ValueDescriptorDeserializer.VALUE_DESCRIPTOR_PROVIDER, valueDescriptorProvider); ctxt.setAttribute(MetaMap.MetaObjectDeserializer.META_DESCRIPTOR_PROVIDER, metaDescriptorProvider); while (jp.nextToken() != JsonToken.END_OBJECT) { String propName = jp.currentName(); if (jp.currentToken() == JsonToken.FIELD_NAME) { jp.nextToken(); } if (jp.currentToken() == JsonToken.VALUE_NULL) { continue; } switch (propName) { case "attributeDescriptors" -> { if (metaItemDescriptors.get() == null) { attributeDescriptorBuffer = new TokenBuffer(jp, ctxt); attributeDescriptorBuffer.copyCurrentStructure(jp); } else { attributeDescriptors = (AttributeDescriptor[])ctxt.findRootValueDeserializer(ATTRIBUTE_DESCRIPTOR_TYPE).deserialize(jp, ctxt); } } case "metaItemDescriptors" -> { if (valueDescriptors.get() == null) { metaItemDescriptorBuffer = new TokenBuffer(jp, ctxt); metaItemDescriptorBuffer.copyCurrentStructure(jp); } else { metaItemDescriptors.set((MetaItemDescriptor[])ctxt.findRootValueDeserializer(META_ITEM_DESCRIPTOR_TYPE).deserialize(jp, ctxt)); } } case "valueDescriptors" -> { valueDescriptors.set((ValueDescriptor[]) ctxt.findRootValueDeserializer(VALUE_DESCRIPTOR_TYPE).deserialize(jp, ctxt)); } case "assetDescriptor" -> { assetDescriptor = (AssetDescriptor) ctxt.findRootValueDeserializer(ASSET_DESCRIPTOR_TYPE).deserialize(jp, ctxt); } } } if (metaItemDescriptorBuffer != null) { JsonParser parser = metaItemDescriptorBuffer.asParser(); parser.nextToken(); metaItemDescriptors.set((MetaItemDescriptor[])ctxt.findRootValueDeserializer(META_ITEM_DESCRIPTOR_TYPE).deserialize(parser, ctxt)); } if (attributeDescriptorBuffer != null) { JsonParser parser = attributeDescriptorBuffer.asParser(); parser.nextToken(); attributeDescriptors = (AttributeDescriptor[])ctxt.findRootValueDeserializer(ATTRIBUTE_DESCRIPTOR_TYPE).deserialize(parser, ctxt); } if (assetDescriptor == null) { throw new JsonParseException(jp, "Must contain an asset descriptor"); } return new AssetTypeInfo( assetDescriptor, attributeDescriptors, metaItemDescriptors.get() != null ? metaItemDescriptors.get() : new MetaItemDescriptor[0], valueDescriptors.get() != null ? valueDescriptors.get() : new ValueDescriptor[0] ); } } protected static Logger LOG = SyslogCategory.getLogger(MODEL_AND_VALUES, AssetModelService.class); public static final String DIRECTORY_NAME = "asset_model"; protected ManagerIdentityService identityService; protected ClientEventService clientEventService; protected GatewayService gatewayService; protected PersistenceService persistenceService; protected Map dynamicAssetTypeInfos; protected Path storageDir; @Override public int getPriority() { // Need storageDir from PersistenceService return PersistenceService.PRIORITY + 10; } @Override public void configure() throws Exception { // // React if a client wants to read assets and attributes // from(CLIENT_EVENT_TOPIC) // .routeId("FromClientReadRequests") // .filter( // or(body().isInstanceOf(ReadAssetsEvent.class), body().isInstanceOf(ReadAssetEvent.class), body().isInstanceOf(ReadAttributeEvent.class))) // .choice() // .when(body().isInstanceOf(ReadAssetEvent.class)) // .end(); } @Override public void init(Container container) throws Exception { identityService = container.getService(ManagerIdentityService.class); clientEventService = container.getService(ClientEventService.class); gatewayService = container.getService(GatewayService.class); persistenceService = container.getService(PersistenceService.class); container.getService(ManagerWebService.class).addApiSingleton( new AssetModelResourceImpl( container.getService(TimerService.class), identityService, this ) ); container.getService(MessageBrokerService.class).getContext().addRoutes(this); } protected void initDynamicModel() { try { Path rootStorageDir = persistenceService.getStorageDir(); storageDir = rootStorageDir.resolve(DIRECTORY_NAME); if (!Files.exists(storageDir)) { try { Files.createDirectories(storageDir); } catch (IOException e) { LOG.log(Level.SEVERE, "Failed to create asset model storage directory", e); throw new RuntimeException(e); } } else if (!Files.isDirectory(storageDir)) { throw new IllegalStateException("Asset model storage directory is not a directory: " + storageDir); } dynamicAssetTypeInfos = loadDescriptors(AssetTypeInfo.class, storageDir) .collect(Collectors.toMap(ati -> ati.getAssetDescriptor().getName(), ati -> ati)); LOG.fine("Loaded asset type infos from '" + storageDir + "': count = " + dynamicAssetTypeInfos.size()); } catch (Exception e) { LOG.log(Level.SEVERE, "Failed to load custom asset types from '" + storageDir + "':" + e.getMessage()); } } @Override public void start(Container container) throws Exception { } @Override public void stop(Container container) throws Exception { } @Override public boolean useAutoScan() { return false; } @Override public AssetDescriptor[] getAssetDescriptors() { if (dynamicAssetTypeInfos == null) { return null; } return dynamicAssetTypeInfos.values().stream().map(AssetTypeInfo::getAssetDescriptor).toArray(AssetDescriptor[]::new); } @Override public Map>> getAttributeDescriptors() { if (dynamicAssetTypeInfos == null) { initDynamicModel(); } return dynamicAssetTypeInfos.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, es -> es.getValue().getAttributeDescriptors().values())); } @Override public Map>> getMetaItemDescriptors() { if (dynamicAssetTypeInfos == null) { initDynamicModel(); } return dynamicAssetTypeInfos.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, es -> Arrays.asList(es.getValue().getMetaItemDescriptors()))); } @Override public Map>> getValueDescriptors() { if (dynamicAssetTypeInfos == null) { initDynamicModel(); } return dynamicAssetTypeInfos.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, es -> Arrays.asList(es.getValue().getValueDescriptors()))); } @Override public boolean isDynamic() { return true; } public AssetTypeInfo[] getAssetInfos(String parentId, String parentType) { if (!TextUtil.isNullOrEmpty(parentId) && gatewayService.getLocallyRegisteredGatewayId(parentId, null) != null) { // TODO: Asset is on a gateway so need to get model info from the gateway instance return new AssetTypeInfo[0]; } return ValueUtil.getAssetInfos(parentType); } public AssetTypeInfo getAssetInfo(String parentId, String assetType) { if (!TextUtil.isNullOrEmpty(parentId) && gatewayService.getLocallyRegisteredGatewayId(parentId, null) != null) { // TODO: Asset is on a gateway so need to get model info from the gateway instance return null; } return ValueUtil.getAssetInfo(assetType).orElse(null); } public AssetDescriptor[] getAssetDescriptors(String parentId, String parentType) { if (!TextUtil.isNullOrEmpty(parentId) && gatewayService.getLocallyRegisteredGatewayId(parentId, null) != null) { // TODO: Asset is on a gateway so need to get model info from the gateway instance return new AssetDescriptor[0]; } return ValueUtil.getAssetDescriptors(parentType); } public Map> getValueDescriptors(String parentId) { if (!TextUtil.isNullOrEmpty(parentId) && gatewayService.getLocallyRegisteredGatewayId(parentId, null) != null) { // TODO: Asset is on a gateway so need to get model info from the gateway instance return null; } return ValueUtil.getValueDescriptors(); } public Map> getMetaItemDescriptors(String parentId) { if (!TextUtil.isNullOrEmpty(parentId) && gatewayService.getLocallyRegisteredGatewayId(parentId, null) != null) { // TODO: Asset is on a gateway so need to get model info from the gateway instance return null; } return ValueUtil.getMetaItemDescriptors(); } protected T parse(String jsonString, Class type) throws JsonProcessingException { return JSON.readValue(jsonString, JSON.constructType(type)); } protected Stream loadDescriptors(Class descriptorClazz, Path descriptorPath) { try { return Files.list(descriptorPath).map(descriptorFile -> { LOG.log(Level.FINE, "Reading descriptor from: " + descriptorFile); String descriptorStr; try { descriptorStr = Files.readString(descriptorFile); if (descriptorStr != null) { return parse(descriptorStr, descriptorClazz); } } catch (JsonProcessingException e) { LOG.log(Level.SEVERE, "Failed to parse descriptor file '" + descriptorFile + "': " + e.getMessage()); } catch (IOException e) { LOG.log(Level.SEVERE, "Failed to read descriptor file '" + descriptorFile + "': " + e.getMessage()); } return null; }).filter(Objects::nonNull); } catch (IOException e) { throw new RuntimeException(e); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy