blob: 00900ae77259c31ddc3ae97f98313cafc752e2f6 [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2021 the Eclipse BaSyx Authors
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
******************************************************************************/
package org.eclipse.basyx.vab.protocol.http.server;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.StringJoiner;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.basyx.vab.coder.json.provider.JSONProvider;
import org.eclipse.basyx.vab.exception.provider.MalformedRequestException;
import org.eclipse.basyx.vab.exception.provider.ProviderException;
import org.eclipse.basyx.vab.modelprovider.VABPathTools;
import org.eclipse.basyx.vab.modelprovider.api.IModelProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Charsets;
import com.google.common.io.ByteSource;
/**
* VAB provider class that enables access to an IModelProvider via HTTP REST
* interface<br />
* <br />
* REST http interface is as following: <br />
* - GET /aas/submodels/{subModelId} Retrieves submodel with ID {subModelId}
* <br />
* - GET /aas/submodels/{subModelId}/properties/a Retrieve property a of
* submodel {subModelId}<br />
* - GET /aas/submodels/{subModelId}/properties/a/b Retrieve property a/b of
* submodel {subModelId} <br />
* - POST /aas/submodels/{subModelId}/operations/a Invoke operation a of
* submodel {subModelId}<br />
* - POST /aas/submodels/{subModelId}/operations/a/b Invoke operation a/b of
* submodel {subModelId}
*
* @author kuhn
*
*/
public class VABHTTPInterface<ModelProvider extends IModelProvider> extends BasysHTTPServlet {
private static Logger logger = LoggerFactory.getLogger(VABHTTPInterface.class);
/**
* Version information to identify the version of serialized instances
*/
private static final long serialVersionUID = 1L;
/**
* Reference to IModelProvider backend
*/
protected JSONProvider<ModelProvider> providerBackend = null;
/**
* Constructor
*/
public VABHTTPInterface(ModelProvider provider) {
// Store provider reference
providerBackend = new JSONProvider<ModelProvider>(provider);
}
/**
* Access model provider
*/
public ModelProvider getModelProvider() {
return providerBackend.getBackendReference();
}
/**
* Send JSON encoded response
*/
protected void sendJSONResponse(String path, PrintWriter outputStream, Object jsonValue) {
// Output result
outputStream.write(jsonValue.toString()); // FIXME throws nullpointer exception if jsonValue is null
outputStream.flush();
}
/**
* Implement "Get" operation
*
* Process HTTP get request - get sub model property value
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String path = extractPath(req);
// Setup HTML response header
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
resp.setStatus(200);
// Process get request
providerBackend.processBaSysGet(path, resp.getOutputStream());
} catch(ProviderException e) {
int httpCode = ExceptionToHTTPCodeMapper.mapFromException(e);
resp.setStatus(httpCode);
logger.debug("Exception in HTTP-GET. Response-code: " + httpCode, e);
}
}
/**
* Implement "Set" operation
*/
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String path = extractPath(req);
String serValue = extractSerializedValue(req);
logger.trace("DoPut: {}", serValue);
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
resp.setStatus(200);
providerBackend.processBaSysSet(path, serValue.toString(), resp.getOutputStream());
} catch(ProviderException e) {
int httpCode = ExceptionToHTTPCodeMapper.mapFromException(e);
resp.setStatus(httpCode);
logger.debug("Exception in HTTP-PUT. Response-code: " + httpCode, e);
}
}
/**
* <pre>
* Handle HTTP POST operation. Creates a new Property, Operation, Event,
* Submodel or AAS or invokes an operation.
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String path = extractPath(req);
String serValue = extractSerializedValue(req);
logger.trace("DoPost: {}", serValue);
// Setup HTML response header
resp.setStatus(201);
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
// Check if request is for property creation or operation invoke
if (VABPathTools.isOperationInvokationPath(path)) {
// Invoke BaSys VAB 'invoke' primitive
providerBackend.processBaSysInvoke(path, serValue, resp.getOutputStream());
} else {
// Invoke the BaSys 'create' primitive
providerBackend.processBaSysCreate(path, serValue, resp.getOutputStream());
}
} catch (ProviderException e) {
int httpCode = ExceptionToHTTPCodeMapper.mapFromException(e);
resp.setStatus(httpCode);
logger.debug("Exception in HTTP-POST. Response-code: " + httpCode, e);
}
}
/**
* Handle a HTTP PATCH operation. Updates a map or collection
*
*/
@Override
protected void doPatch(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String path = extractPath(req);
String serValue = extractSerializedValue(req);
logger.trace("DoPatch: {}", serValue);
resp.setStatus(200);
providerBackend.processBaSysDelete(path, serValue, resp.getOutputStream());
} catch(ProviderException e) {
int httpCode = ExceptionToHTTPCodeMapper.mapFromException(e);
resp.setStatus(httpCode);
logger.debug("Exception in HTTP-PATCH. Response-code: " + httpCode, e);
}
}
/**
* Implement "Delete" operation. Deletes any resource under the given path.
*/
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String path = extractPath(req);
// No parameter to read! Provide serialized null
String nullParam = "";
resp.setStatus(200);
providerBackend.processBaSysDelete(path, nullParam, resp.getOutputStream());
} catch(ProviderException e) {
int httpCode = ExceptionToHTTPCodeMapper.mapFromException(e);
resp.setStatus(httpCode);
logger.debug("Exception in HTTP-DELETE. Response-code: " + httpCode, e);
}
}
private String extractPath(HttpServletRequest req) throws UnsupportedEncodingException {
// Extract path
String uri = req.getRequestURI();
// Normalizes URI
String nUri = "/" + VABPathTools.stripSlashes(uri);
String contextPath = req.getContextPath();
if (nUri.startsWith(contextPath) && nUri.length() > getEnvironmentPathSize(req) - 1) {
String path = nUri.substring(getEnvironmentPathSize(req));
String extractedParameters = extractParameters(req);
path = VABPathTools.stripSlashes(path);
if (extractedParameters.isEmpty()) {
path += "/";
} else {
path += extractedParameters;
}
return path;
}
throw new MalformedRequestException("The passed path " + uri + " is not a possbile path for this server.");
}
/**
* Extracts request parameters from the request
*
* @param req
* @return
*/
private String extractParameters(HttpServletRequest req) {
Enumeration<String> parameterNames = req.getParameterNames();
// Collect list of parameters
List<String> parameters = new ArrayList<>();
while (parameterNames.hasMoreElements()) {
StringBuilder ret = new StringBuilder();
String paramName = parameterNames.nextElement();
ret.append(paramName);
ret.append("=");
String[] paramValues = req.getParameterValues(paramName);
for (int i = 0; i < paramValues.length; i++) {
ret.append(paramValues[i]);
}
parameters.add(ret.toString());
}
// If no parameter is existing, return an empty string. Else join the parameters
// and return them prefixed with a ?
if (parameters.isEmpty()) {
return "";
} else {
StringJoiner joiner = new StringJoiner("&");
parameters.stream().forEach(s -> joiner.add(s));
return "?" + joiner.toString();
}
}
private int getEnvironmentPathSize(HttpServletRequest req) {
return req.getContextPath().length() + req.getServletPath().length();
}
/**
* Read serialized value
* @param req
* @return
* @throws IOException
*/
private String extractSerializedValue(HttpServletRequest req) throws IOException {
// https://www.baeldung.com/convert-input-stream-to-string#guava
ByteSource byteSource = new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return req.getInputStream();
}
};
return byteSource.asCharSource(Charsets.UTF_8).read();
}
}