blob: 863e498d5528e4d86c8a8e2b82b1e4962644f161 [file] [log] [blame]
/*
* Copyright (c) 2020 Kentyou.
* 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
*
* Contributors:
* Kentyou - initial API and implementation
*/
package org.eclipse.sensinact.gateway.core;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import org.eclipse.sensinact.gateway.common.execution.Executable;
import org.eclipse.sensinact.gateway.common.primitive.Description;
import org.eclipse.sensinact.gateway.common.primitive.ElementsProxy;
import org.eclipse.sensinact.gateway.common.primitive.InvalidValueException;
import org.eclipse.sensinact.gateway.common.primitive.Modifiable;
import org.eclipse.sensinact.gateway.core.message.SnaLifecycleMessage.Lifecycle;
import org.eclipse.sensinact.gateway.core.method.AccessMethod;
import org.eclipse.sensinact.gateway.core.method.AccessMethodExecutor;
import org.eclipse.sensinact.gateway.core.method.AccessMethodResponseBuilder;
import org.eclipse.sensinact.gateway.core.method.InvalidTriggerException;
import org.eclipse.sensinact.gateway.core.method.Signature;
import org.eclipse.sensinact.gateway.core.method.trigger.AccessMethodTrigger;
import org.eclipse.sensinact.gateway.core.method.trigger.TriggerArgumentBuilder;
import org.eclipse.sensinact.gateway.core.security.AccessTree;
import org.eclipse.sensinact.gateway.core.security.ImmutableAccessTree;
import org.eclipse.sensinact.gateway.core.security.MethodAccessibility;
import org.eclipse.sensinact.gateway.util.JSONUtils;
import org.eclipse.sensinact.gateway.util.UriUtils;
import org.json.JSONObject;
/**
* Service implementation
*
* @author <a href="mailto:christophe.munilla@cea.fr">Christophe Munilla</a>
*/
public class ServiceImpl extends ModelElement<ModelInstance<?>, ServiceProxy, ServiceProcessableData<?>, ResourceImpl, Resource> {
class ServiceProxyWrapper extends ModelElementProxyWrapper implements ResourceCollection {
protected ServiceProxyWrapper(ServiceProxy proxy, ImmutableAccessTree tree) {
super(proxy, tree);
}
@Override
public List<Resource> getResources() {
return super.list();
}
@Override
public <R extends Resource> R getResource(String resource) {
return (R) super.element(resource);
}
@Override
public Object invoke(Object proxy, Method method, Object[] parameters) throws Throwable {
Object result = null;
if (this.proxy.getProxied().isAssignableFrom(method.getDeclaringClass())) {
Object[] calledParameters = null;
if (method.isVarArgs() && parameters != null && parameters.length == 1
&& parameters[0].getClass().isArray()) {
calledParameters = (Object[]) parameters[0];
} else
calledParameters = parameters;
if (calledParameters.length > 0) {
String resourceName = (String) calledParameters[0];
calledParameters[0] = getResource(resourceName);
}
result = this.proxy.invoke(method.getName().toUpperCase(), calledParameters);
} else
result = method.invoke(this, parameters);
if (result == this.proxy || result == this)
return proxy;
return result;
}
@Override
public boolean isAccessible() {
return true;
}
@Override
public Description getDescription() {
return new Description() {
@Override
public String getName() {
return ServiceImpl.this.getName();
}
@Override
public String getJSON() {
StringBuilder buffer = new StringBuilder();
buffer.append(JSONUtils.OPEN_BRACE);
buffer.append(JSONUtils.QUOTE);
buffer.append("name");
buffer.append(JSONUtils.QUOTE);
buffer.append(JSONUtils.COLON);
buffer.append(JSONUtils.QUOTE);
buffer.append(this.getName());
buffer.append(JSONUtils.QUOTE);
buffer.append(JSONUtils.COMMA);
buffer.append(JSONUtils.QUOTE);
buffer.append("resources");
buffer.append(JSONUtils.QUOTE);
buffer.append(JSONUtils.COLON);
buffer.append(JSONUtils.OPEN_BRACKET);
int index = 0;
Enumeration<Resource> enumeration = ServiceProxyWrapper.this.elements();
while (enumeration.hasMoreElements()) {
buffer.append(index > 0 ? JSONUtils.COMMA : JSONUtils.EMPTY);
buffer.append(enumeration.nextElement().getDescription().getJSON());
index++;
}
buffer.append(JSONUtils.CLOSE_BRACKET);
buffer.append(JSONUtils.CLOSE_BRACE);
return buffer.toString();
}
@Override
public String getJSONDescription() {
StringBuilder buffer = new StringBuilder();
buffer.append(JSONUtils.OPEN_BRACE);
buffer.append(JSONUtils.QUOTE);
buffer.append("name");
buffer.append(JSONUtils.QUOTE);
buffer.append(JSONUtils.COLON);
buffer.append(JSONUtils.QUOTE);
buffer.append(this.getName());
buffer.append(JSONUtils.QUOTE);
buffer.append(JSONUtils.COMMA);
buffer.append(JSONUtils.QUOTE);
buffer.append("resources");
buffer.append(JSONUtils.QUOTE);
buffer.append(JSONUtils.COLON);
buffer.append(JSONUtils.OPEN_BRACKET);
int index = 0;
Enumeration<Resource> enumeration = ServiceProxyWrapper.this.elements();
while (enumeration.hasMoreElements()) {
buffer.append(index > 0 ? JSONUtils.COMMA : JSONUtils.EMPTY);
buffer.append(enumeration.nextElement().getDescription().getJSONDescription());
index++;
}
buffer.append(JSONUtils.CLOSE_BRACKET);
buffer.append(JSONUtils.CLOSE_BRACE);
return buffer.toString();
}
};
}
}
/**
* the number of subscriptions made on {@link Resource}s which belong to this
* service
*/
protected int subscriptionsCount;
/**
* Constructor
*
* @param modelInstance the {@link ModelInstance} of the service to be instantiated
* @param name the name of the service to be instantiated
* @param serviceProvider the {@link ServiceProviderImpl} parent of the service to be
* instantiated
*
* @throws InvalidServiceException
* if an error occurred while instantiating the service
*/
protected ServiceImpl(ModelInstance<?> modelInstance, String name, ServiceProviderImpl serviceProvider)
throws InvalidServiceException {
super(modelInstance, serviceProvider, UriUtils.getUri(new String[] { serviceProvider.getPath(), name }));
this.subscriptionsCount = 0;
}
/**
* @inheritDoc
*
* @see org.eclipse.sensinact.gateway.core.ModelElement#
* process(org.eclipse.sensinact.gateway.common.primitive.ProcessableData)
*/
public void process(ServiceProcessableData<?> data) {
if (data == null) {
return;
}
String resourceId = data.getResourceId();
if (resourceId == null) {
super.modelInstance.mediator().warn("Resource identifier not found");
return;
}
ResourceImpl resource = this.getResource(resourceId);
if (resource == null) {
Class<?> dataType = null;
for(Iterator<?> it = data.iterator();it.hasNext();) {
ResourceProcessableData rpd = (ResourceProcessableData) it.next();
if(rpd.getAttributeId()!=null && !DataResource.VALUE.equals(rpd.getAttributeId()))
continue;
Object obj = rpd.getData();
Class<?> clazz = obj==null?null:obj.getClass();
if(clazz == null)
break;
if(clazz.isPrimitive()) {
switch(clazz.getName()) {
case "byte":
case "short":
case "int":
case "long":
case "float":
case "double":
dataType = double.class;
break;
case "boolean":
dataType = boolean.class;
break;
case "char":
dataType = String.class;
break;
}
} else if (obj instanceof Number) {
switch(clazz.getName()) {
case "java.lang.Byte":
case "java.lang.Short":
case "java.lang.Integer":
case "java.lang.Long":
case "java.lang.Float":
case "java.lang.Double":
dataType = double.class;
break;
default:
dataType = String.class;
break;
}
} else
dataType = String.class;
break;
}
ResourceDescriptor descriptor = super.getModelInstance().configuration(
).getResourceDescriptor(
).withServiceName(super.getName()
).withResourceName(resourceId
).withDataType(dataType);
ResourceBuilder builder = super.getModelInstance().getResourceBuilder(descriptor,
this.modelInstance.configuration().getResourceBuildPolicy());
if (builder != null) {
try {
resource = this.addResource(builder);
} catch (Exception e) {
super.modelInstance.mediator().error(e);
}
}
}
if (resource == null) {
super.modelInstance.mediator().warn("Resource '%s' not found for '%s' service", resourceId,
super.getName());
return;
}
resource.process(data);
}
/**
* Creates and returns a new {@link DataResource} associated to this ServiceImpl
*
* @param resourceClass
* extended {@link DataResource} type to instantiate
* @param name
* the name of the {@link DataResource} to instantiate
* @param type
* the type of the 'value' {@link Attribute} of the
* {@link DataResource} to instantiate
* @param value
* the value of the 'value' {@link Attribute} of the
* {@link DataResource} to instantiate
* @param attributes
* extended set of attributes to add to the resource to create
* @return a new created {@link DataResource} instance
*
* @throws InvalidResourceException
* if an error occurred while instantiating the new
* {@link DataResource}
*/
public <D extends DataResource> ResourceImpl addDataResource(Class<D> resourceClass, String name, Class<?> type, Object value)
throws InvalidResourceException {
ResourceDescriptor descriptor = super.getModelInstance().configuration().getResourceDescriptor()
.withServiceName(this.getName()).withResourceName(name).withResourceType(resourceClass)
.withDataType(type).withDataValue(value);
ResourceBuilder builder = super.getModelInstance().getResourceBuilder(descriptor,
this.modelInstance.configuration().getResourceBuildPolicy());
return this.addResource(builder);
}
/**
* Creates and adds a new {@link ActionResource} to this service
*
* @param name
* the name of the {@link ActionResource} to instantiate
* @param resourceClass
* extended {@link ActionResource} type to instantiate
* @param attributes
* extended set of attributes to add to the resource to create
* @return a new created {@link ActionResource} instance
*
* @throws InvalidResourceException
*/
public <D extends ActionResource> ResourceImpl addActionResource(String name, Class<D> resourceClass)
throws InvalidResourceException {
ResourceDescriptor descriptor = super.getModelInstance().configuration().getResourceDescriptor()
.withServiceName(this.getName()).withResourceName(name).withResourceType(resourceClass);
ResourceBuilder builder = super.getModelInstance().getResourceBuilder(descriptor,
this.modelInstance.configuration().getResourceBuildPolicy());
return this.addResource(builder);
}
/**
* Creates and adds a {@link LinkedResourceImpl} linked to the
* {@link ResourceImpl} passed as parameter to this service
*
* @param resource
* the {@link ResourceImpl} to which to link the
* {@link LinkedResourceImpl} to instantiate
* @param name
* the name of the {@link LinkedResourceImpl} to instantiate
* @return a new created {@link LinkedResourceImpl} instance
*
* @throws InvalidResourceException
*/
public ResourceImpl addLinkedResource(String link, ResourceImpl targetedResource) throws InvalidResourceException {
if (targetedResource == null || this.getResource(link) != null
|| LinkedResourceImpl.class.isAssignableFrom(targetedResource.getClass())) {
super.modelInstance.mediator().debug("Unable to create the linked resource : %s", link);
return null;
}
if (targetedResource.getType() == Resource.Type.ACTION) {
return addLinkedActionResource(link, targetedResource, true);
}
ResourceBuilder builder = super.getModelInstance()
.createResourceBuilder(super.getModelInstance().configuration().getResourceDescriptor()
.withResourceName(link).withResourceType(targetedResource.getResourceType()));
ResourceImpl linkedResource = builder.buildLinkedResource(this, targetedResource);
if (this.addResource(linkedResource)) {
targetedResource.registerLink(linkedResource.getPath());
}
return linkedResource;
}
/**
* Creates and adds a {@link LinkedResourceImpl} linked to the
* {@link Resource.Type.ACTION} typed {@link ResourceImpl} passed as parameter
* to this service
*
* @param resource
* the {@link Resource.Type.ACTION} typed {@link ResourceImpl} to
* which to link the {@link LinkedResourceImpl} to instantiate
* @param name
* the name of the {@link LinkedResourceImpl} to instantiate
* @param copyActMethod
* true if the {@link LinkedActMethod} of the created
* {@link LinkedResourceImpl} is a simple copy of the
* {@link ActMethod} of the targeted {@link ActionResource} ; if
* false an empty {@link LinkedActMethod} (containing no signature)
* is registered into the created {@link LinkedResourceImpl}
* @return a new created {@link LinkedResourceImpl} instance
*
* @throws InvalidResourceException
*/
public ResourceImpl addLinkedActionResource(String link, ResourceImpl targetedResource, boolean copyActMethod)
throws InvalidResourceException {
if (targetedResource == null || targetedResource.getType() != Resource.Type.ACTION
|| this.getResource(link) != null) {
super.modelInstance.mediator().debug(new StringBuilder()
.append("Unable to create the resource - Invalid Name :").append(link).toString());
return null;
}
ResourceBuilder builder = super.getModelInstance()
.createResourceBuilder(super.getModelInstance().configuration().getResourceDescriptor()
.withResourceName(link).withResourceType(targetedResource.getResourceType()));
ResourceImpl linkedResource = builder.buildLinkedActionResource(this, targetedResource, copyActMethod);
if (this.addResource(linkedResource)) {
targetedResource.registerLink(linkedResource.getPath());
return linkedResource;
}
return null;
}
/**
* Creates and returns a new {@link ResourceImpl} and updates existing
* {@link ServiceProxy}s its {@link ResourceProxy}
*
* @param builder
* the {@link ResourceBuilder} containing the definition of the
* resource to create
* @return a new created {@link Resource} instance
*
* @throws InvalidResourceException
* if an error occurred while instantiating the new {@link Resource}
*/
public ResourceImpl addResource(ResourceBuilder builder) throws InvalidResourceException {
String resourceName = builder.getConfiguredName();
ResourceImpl resource = null;
if (resourceName == null || (resource = this.getResource(resourceName)) != null) {
super.modelInstance.mediator().debug("The resource '%s' already exists", resourceName);
return null;
}
if ((resource = builder.build(super.modelInstance, this)) != null && this.addResource(resource)) {
return resource;
}
return null;
}
/**
* Add the {@link ResourceImpl} passed as parameter to this ServiceImpl.
* If this ServiceImpl is already registered, the added {@link ResourceImpl}
* is started
*
* @param resource the {@link ResourceImpl} to be added and potentially
* started
*
* @return true if the {@link ResourceImpl} has been added - false otherwise
*/
private boolean addResource(ResourceImpl resource) {
if (resource == null || !super.addElement(resource)) {
return false;
}
if (super.modelInstance.isRegistered()) {
resource.start();
}
return true;
}
/**
* Stop and remove from this ServiceImpl the {@link ResourceImpl} whose
* name is passed as parameter
*
* @param resource the name of the {@link ResourceImpl} to be removed
*
* @return the removed {@link ResourceImpl}
*/
public ResourceImpl removeResource(String resource) {
return super.removeElement(resource);
}
/**
* Registers an {@link AccessMethodTrigger} and links its execution to a
* resource to define the value of a state variable one
*
* @param method the name of the AccessMethod for which a signature is provided
* @param resourceType the extended {@link Resource} type of the listened resource
* @param listened the name of the listened resource
* @param target the name of the state variable resource to link to the listened resource
* @param signature the signature to which attach the specified {@link AccessMethodTrigger}
* @param trigger the {@link AccessMethodTrigger} to call when the specified signature is called
* @param policy the {@link AccessMethodExecutor.ExecutionPolicy} defining the trigger
* execution order
* @throws InvalidValueException
*/
public void addTrigger(String listen, final String target, Signature signature,
final AccessMethodTrigger trigger, AccessMethodExecutor.ExecutionPolicy policy)
throws InvalidValueException {
if (!(signature.getName().equals(AccessMethod.ACT) || signature.getName().equals(AccessMethod.SET))){
throw new InvalidTriggerException(String.format("ACT or SET method expected"));
}
ResourceImpl listened = this.getResource(listen);
final ResourceImpl variable = this.getResource(target);
if(listened == null||variable == null) {
throw new InvalidTriggerException("Both observed and updated Resources are required");
}
Class<? extends Resource> resourceType = listened.getResourceType();
// ensures that the listened resource exists and is an action one
// and that the targeted one exists and is a variable
if ((signature.getName().equals(AccessMethod.ACT) && !ActionResource.class.isAssignableFrom(resourceType))
||(signature.getName().equals(AccessMethod.SET) && !DataResource.class.isAssignableFrom(resourceType))
|| !StateVariableResource.class.isAssignableFrom(variable.getResourceType())) {
throw new InvalidTriggerException(String.format("An %s and a StateVariableResource were expected", resourceType.getSimpleName()));
}
final Attribute attribute = variable.getAttribute(variable.getDefault());
if (Modifiable.FIXED.equals(attribute.getModifiable())) {
throw new InvalidValueException("Trigger cannot modify the resource value");
}
attribute.lock();
// registers the executor which calls the trigger
listened.registerExecutor(signature, new AccessMethodExecutor() {
/**
* @inheritDoc
*
* @see Executable#execute(java.lang.Object)
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public Void execute(AccessMethodResponseBuilder builder) throws Exception {
Object result = null;
switch (trigger.getArgumentBuilder()) {
case TriggerArgumentBuilder.EMPTY:
result = trigger.execute(new TriggerArgumentBuilder.Empty().build(null));
break;
case TriggerArgumentBuilder.PARAMETER:
result = trigger.execute(new TriggerArgumentBuilder.Parameter(
trigger.<Integer>getArgument()).build(builder));
break;
case TriggerArgumentBuilder.RESPONSE:
result = trigger.execute(new TriggerArgumentBuilder.Response().build(builder));
break;
case TriggerArgumentBuilder.INTERMEDIATE:
result = trigger.execute(new TriggerArgumentBuilder.Intermediate().build(builder));
break;
case TriggerArgumentBuilder.MODEL:
result = trigger.execute(new TriggerArgumentBuilder.Model(
trigger.<String>getArgument()).build(ServiceImpl.this).getValue());
break;
default:
break;
}
if (trigger.passOn())
ServiceImpl.this.passOn(AccessMethod.SET, variable.getPath(), new Object[] { DataResource.VALUE, result });
attribute.setValue(result);
builder.push(new JSONObject(attribute.getDescription().getJSON()));
return null;
}
}, policy);
}
/**
* Returns an {@link Executable} whose execution returns the value of the
* {@link ResourceImpl} of this ServiceImpl, and whose name is passed as
* parameter. The value of the targeted {@link ResourceImpl} is the one of its
* default {@link Attribute} if it has been defined
*
* @param resourceName
* the name of the {@link ResourceImpl} for which to create a value
* extractor
* @return an {@link Executable} value extractor for the specified
* {@link ResourceImpl}
*/
public Executable<Void, Object> getResourceValueExtractor(String resourceName) {
final Attribute attribute;
String defaultAttributeName = null;
ResourceImpl resource = this.getResource(resourceName);
if (resource != null && (defaultAttributeName = resource.getDefault()) != null) {
attribute = resource.getAttribute(defaultAttributeName);
} else {
attribute = null;
}
return new Executable<Void, Object>() {
@Override
public Object execute(Void parameter) throws Exception {
if (attribute != null) {
return attribute.getValue();
}
return null;
}
};
}
/**
* Passes on the invocation of a method of a resource of this service to its
* parent service provider
*
* @param type
* the type of the invoked method
* @param resource
* the name of the targeted resource
* @param parameters
* the objects array parameterizing the call
* @return the JSON formated result object
* @throws Exception
*/
@Override
protected <TASK> TASK passOn(String type, String path, Object[] parameters) throws Exception {
this.subscriptionsCount += (type == AccessMethod.UNSUBSCRIBE) ? -1 : 0;
TASK task = super.passOn(type, path, parameters);
this.subscriptionsCount += (type == AccessMethod.SUBSCRIBE) ? 1 : 0;
return task;
}
/**
* Returns the {@link ResourceImpl} provided by this ServiceImpl, whose name is
* passed as parameter
*
* @param resource
* the name of the {@link ResourceImpl}
* @return the {@link ResourceImpl} with the specified name
*/
public ResourceImpl getResource(String resource) {
return super.element(resource);
}
/**
* Returns the set of {@link ResourceImpl}s held by this ServiceImpl
*
* @return the set of this ServiceImpl's {@link ResourceImpl}s
*/
public List<ResourceImpl> getResources() {
synchronized (super.elements) {
return Collections.<ResourceImpl>unmodifiableList(super.elements);
}
}
/**
* Returns the number of subscriptions registered on this service's resources
*
* @return the number of registered subscriptions for this service
*/
public int getSubscriptionsCount() {
return this.subscriptionsCount;
}
@Override
protected Lifecycle getRegisteredEvent() {
return Lifecycle.SERVICE_APPEARING;
}
@Override
protected Lifecycle getUnregisteredEvent() {
return Lifecycle.SERVICE_DISAPPEARING;
}
@Override
protected Class<? extends ElementsProxy<Resource>> getProxyType() {
return Service.class;
}
@Override
public ServiceProxy getProxy(List<MethodAccessibility> methodAccessibilities) {
try {
ServiceProxy proxy = new ServiceProxy(super.modelInstance.mediator(), super.getPath());
proxy.buildMethods(null, methodAccessibilities);
return proxy;
} catch (Exception e) {
super.modelInstance.mediator().error(e);
}
return null;
}
@Override
protected Resource getElementProxy(AccessTree<?> tree, ResourceImpl element)
throws ModelElementProxyBuildException {
if (element.isHidden()) {
return null;
}
return element.getProxy(tree);
}
@Override
protected ModelElementProxyWrapper getWrapper(ServiceProxy proxy, ImmutableAccessTree tree) {
ServiceProxyWrapper wrapper = new ServiceProxyWrapper(proxy, tree);
return wrapper;
}
}