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

org.pipservices3.rpc.services.RestService Maven / Gradle / Ivy

The newest version!
package org.pipservices3.rpc.services;

import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.glassfish.jersey.process.Inflector;
import org.pipservices3.commons.config.ConfigParams;
import org.pipservices3.commons.config.IConfigurable;
import org.pipservices3.commons.convert.JsonConverter;
import org.pipservices3.commons.errors.ApplicationException;
import org.pipservices3.commons.errors.ConfigException;
import org.pipservices3.commons.errors.InvalidStateException;
import org.pipservices3.commons.errors.InvocationException;
import org.pipservices3.commons.refer.*;
import org.pipservices3.commons.run.IOpenable;
import org.pipservices3.commons.validate.Schema;
import org.pipservices3.components.count.CompositeCounters;
import org.pipservices3.components.log.CompositeLogger;
import org.pipservices3.components.trace.CompositeTracer;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;

/**
 * Abstract service that receives remove calls via HTTP/REST protocol.
 * 

* ### Configuration parameters ### *

    *
  • base_route: base route for remote URI *
  • dependencies: *
      *
    • endpoint: override for HTTP Endpoint dependency *
    • controller: override for Controller dependency *
    *
  • connection(s): *
      *
    • discovery_key: (optional) a key to retrieve the connection from IDiscovery *
    • protocol: connection protocol: http or https *
    • host: host name or IP address *
    • port: port number *
    • uri: resource URI or connection string with all parameters in it *
    *
  • credential - the HTTPS credentials: *
      *
    • "credential.ssl_key_file" - the SSL private key in PEM *
    • "credential.ssl_crt_file" - the SSL certificate in PEM *
    • "credential.ssl_ca_file" - the certificate authorities (root cerfiticates) in PEM *
    *
*

* ### References ### *

    *
  • *:logger:*:*:1.0 (optional) ILogger components to pass log messages *
  • *:counters:*:*:1.0 (optional) ICounters components to pass collected measurements *
  • *:discovery:*:*:1.0 (optional) IDiscovery services to resolve connection *
  • *:endpoint:http:*:1.0 (optional) {@link HttpEndpoint} reference *
*

* ### Example ### *

 * {@code
 * class MyRestService extends RestService {
 *    private IMyController _controller;
 *    ...
 *    public MyRestService() {
 *       super();
 *       this._dependencyResolver.put(
 *           "controller",
 *           new Descriptor("mygroup","controller","*","*","1.0")
 *       );
 *    }
 *
 *    public void setReferences(IReferences references) {
 *       super.setReferences(references);
 *       this._controller = (IMyController)this._dependencyResolver.getRequired("controller");
 *    }
 *
 *    public void register() {
 *        ...
 *    }
 * }
 *
 * MyRestService service = new MyRestService();
 * service.configure(ConfigParams.fromTuples(
 *     "connection.protocol", "http",
 *     "connection.host", "localhost",
 *     "connection.port", 8080
 * ));
 * service.setReferences(References.fromTuples(
 *    new Descriptor("mygroup","controller","default","default","1.0"), controller
 * ));
 *
 * service.open("123");
 * System.out.println("The REST service is running on port 8080");
 * }
 * 
*/ public abstract class RestService implements IOpenable, IConfigurable, IReferenceable, IUnreferenceable, IRegisterable { private static final ConfigParams _defaultConfig = ConfigParams.fromTuples( "base_route", "", "dependencies.endpoint", "*:endpoint:http:*:1.0", "dependencies.swagger", "*:swagger-service:*:*:1.0" ); protected ConfigParams _config; private IReferences _references; private boolean _localEndpoint; private boolean _opened; /** * The base route. */ protected String _baseRoute; /** * The HTTP endpoint that exposes this service. */ protected HttpEndpoint _endpoint; /** * The dependency resolver. */ protected DependencyResolver _dependencyResolver = new DependencyResolver(_defaultConfig); /** * The logger. */ protected CompositeLogger _logger = new CompositeLogger(); /** * The performance counters. */ protected CompositeCounters _counters = new CompositeCounters(); /** * The tracer. */ protected CompositeTracer _tracer = new CompositeTracer(); protected String _url; protected ISwaggerService _swaggerService; protected boolean _swaggerEnable = false; protected String _swaggerRoute = "swagger"; protected RestService() { } /** * Configures component by passing configuration parameters. * * @param config configuration parameters to be set. * @throws ConfigException when configuration is wrong. */ @Override public void configure(ConfigParams config) throws ConfigException { _config = config.setDefaults(_defaultConfig); _dependencyResolver.configure(config); _baseRoute = config.getAsStringWithDefault("base_route", _baseRoute); this._swaggerEnable = config.getAsBooleanWithDefault("swagger.enable", this._swaggerEnable); this._swaggerRoute = config.getAsStringWithDefault("swagger.route", this._swaggerRoute); } /** * Sets references to dependent components. * * @param references references to locate the component dependencies. * @throws ReferenceException when no found references. * @throws ConfigException when configuration is wrong. */ @Override public void setReferences(IReferences references) throws ReferenceException, ConfigException { _logger.setReferences(references); _counters.setReferences(references); _tracer.setReferences(references); _dependencyResolver.setReferences(references); _references = references; // Get endpoint _endpoint = (HttpEndpoint) _dependencyResolver.getOneOptional("endpoint"); // Or create a local one if (_endpoint == null) { _endpoint = createLocalEndpoint(); _localEndpoint = true; } else { _localEndpoint = false; } // Add registration callback to the endpoint _endpoint.register(this); this._swaggerService = this._dependencyResolver.getOneOptional(ISwaggerService.class, "swagger"); } /** * Unsets (clears) previously set references to dependent components. */ public void unsetReferences() { // Remove registration callback from endpoint if (_endpoint != null) { _endpoint.unregister(this); _endpoint = null; } this._swaggerService = null; } private HttpEndpoint createLocalEndpoint() throws ConfigException, ReferenceException { HttpEndpoint endpoint = new HttpEndpoint(); if (_config != null) endpoint.configure(_config); if (_references != null) endpoint.setReferences(_references); return endpoint; } /** * Adds instrumentation to log calls and measure call time. * It returns a Timing object that is used to end the time measurement. * * @param correlationId (optional) transaction id to trace execution through call chain. * @param name a method name. * @return Timing object to end the time measurement. */ protected InstrumentTiming instrument(String correlationId, String name) { this._logger.trace(correlationId, "Executing %s method", name); this._counters.incrementOne(name + ".exec_count"); var counterTiming = this._counters.beginTiming(name + ".exec_time"); var traceTiming = this._tracer.beginTrace(correlationId, name, null); return new InstrumentTiming(correlationId, name, "exec", this._logger, this._counters, counterTiming, traceTiming); } /** * Checks if the component is opened. * * @return true if the component has been opened and false otherwise. */ @Override public boolean isOpen() { return _opened; } /** * Opens the component. * * @param correlationId (optional) transaction id to trace execution through * call chain. * @throws ApplicationException when error occured. */ @Override public void open(String correlationId) throws ApplicationException { if (isOpen()) return; if (_endpoint == null) { _endpoint = createLocalEndpoint(); _endpoint.register(this); _localEndpoint = true; } if (_localEndpoint) _endpoint.open(correlationId); _opened = true; } /** * Closes component and frees used resources. * * @param correlationId (optional) transaction id to trace execution through * call chain. * @throws ApplicationException when error occured. */ @Override public void close(String correlationId) throws ApplicationException { if (!_opened) return; if (_endpoint == null) { throw new InvalidStateException(correlationId, "NO_ENDPOINT", "HTTP endpoint is missing"); } if (_localEndpoint) { _endpoint.close(correlationId); } _opened = false; } /** * Sends error serialized as ErrorDescription object and appropriate HTTP status * code. If status code is not defined, it uses 500 status code. * * @param ex an error object to be sent. * @return HTTP response status */ protected Response sendError(Exception ex) { return HttpResponseSender.sendError(ex); } /** * Creates a callback function that sends result as JSON object. That callack * function call be called directly or passed as a parameter to business logic * components. *

* If object is not null it returns 200 status code. For null results it returns * 204 status code. If error occur it sends ErrorDescription with approproate * status code. * * @param result a body object to result. * @return execution result. */ protected Response sendResult(Object result) { return HttpResponseSender.sendResult(result); } /** * Creates a callback function that sends an empty result with 204 status code. * If error occur it sends ErrorDescription with approproate status code. * * @return HTTP response status with no content. */ protected Response sendEmptyResult() { return HttpResponseSender.sendEmptyResult(); } /** * Creates a callback function that sends newly created object as JSON. That * callack function call be called directly or passed as a parameter to business * logic components. *

* If object is not null it returns 201 status code. For null results it returns * 204 status code. If error occur it sends ErrorDescription with approproate * status code. * * @param result a body object to created result * @return execution result. */ protected Response sendCreatedResult(Object result) { return HttpResponseSender.sendCreatedResult(result); } /** * Creates a callback function that sends deleted object as JSON. That callack * function call be called directly or passed as a parameter to business logic * components. *

* If object is not null it returns 200 status code. For null results it returns * 204 status code. If error occur it sends ErrorDescription with approproate * status code. * * @param result a body object to deleted result * @return execution result. */ protected Response sendDeletedResult(Object result) { return HttpResponseSender.sendDeletedResult(result); } protected String getQueryParameter(ContainerRequestContext request, String name) { String value = null; name = URLEncoder.encode(name, StandardCharsets.UTF_8); if (request.getUriInfo().getQueryParameters().containsKey(name)) { value = request.getUriInfo().getQueryParameters().getFirst(name); value = value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : null; } return value; } /** * Gets string value of request body. * * @param request HTTP request * @return string value of data. * @throws ApplicationException when error occured. */ protected String getBodyAsString(ContainerRequestContext request) throws ApplicationException { try { InputStream streamReader = request.getEntityStream(); byte[] data = new byte[streamReader.available()]; streamReader.read(data, 0, data.length); return new String(data, StandardCharsets.UTF_8); } catch (IOException ex) { throw new InvocationException(null, "READ_ERROR", "Cannot read input stream").wrap(ex); } } /** * Gets request body from json string. * * @param type the class type of result object. * @param request HTTP request * @return converted object value * @throws ApplicationException when error occured. */ protected T getBodyAsJson(Class type, ContainerRequestContext request) throws ApplicationException { if (!request.getMediaType().toString().contains(MediaType.APPLICATION_JSON)) { throw new InvocationException(null, "EXPECTED_JSON", "Expected application/json media type"); } String json = getBodyAsString(request); try { return JsonConverter.fromJson(type, json); } catch (IOException ex) { throw new InvocationException(null, "READ_ERROR", "Failed to deserialize request from JSON").wrap(ex); } } private String appendBaseRoute(String route) { route = route == null ? "/" : route; if (this._baseRoute != null && this._baseRoute.length() > 0) { var baseRoute = this._baseRoute; if (route.length() == 0) route = "/"; if (route.charAt(0) != '/') route = "/" + route; if (baseRoute.charAt(0) != '/') baseRoute = '/' + baseRoute; route = baseRoute + route; } return route; } /** * Registers a route in HTTP endpoint. * * @param method HTTP method: "get", "head", "post", "put", "delete" * @param route a command route. Base route will be added to this route * @param action an action function that is called when operation is invoked. */ protected void registerRoute(String method, String route, Inflector action) { if (_endpoint == null) return; route = appendBaseRoute(route); _endpoint.registerRoute(method, route, action); } /** * Registers a route in HTTP endpoint. * * @param method HTTP method: "get", "head", "post", "put", "delete" * @param route a command route. Base route will be added to this route * @param schema a validation schema to validate received parameters. * @param action an action function that is called when operation is invoked. */ protected void registerRoute(String method, String route, Schema schema, Inflector action) { if (_endpoint == null) return; route = appendBaseRoute(route); _endpoint.registerRoute(method.toUpperCase(), route, schema, action); } /** * Registers a route with authorization in HTTP endpoint. * * @param method HTTP method: "get", "head", "post", "put", "delete" * @param route a command route. Base route will be added to this route * @param schema a validation schema to validate received parameters. * @param authorize an authorization interceptor * @param action an action function that is called when operation is invoked. */ protected void registerRouteWithAuth(String method, String route, Schema schema, Function authorize, Function action) { //TODO } /** * Registers a middleware for a given route in HTTP endpoint. * * @param route a command route. Base route will be added to this route * @param action an action function that is called when middleware is invoked. */ protected void registerInterceptor(String route, Function action) { if (this._endpoint == null) return; route = this.appendBaseRoute(route); this._endpoint.registerInterceptor(route, action); } protected void registerOpenApiSpecFromFile(String path) { try (var fs = new FileInputStream(path)) { var content = new String(fs.readAllBytes(), StandardCharsets.UTF_8); this.registerOpenApiSpec(content); } catch (IOException ex) { throw new RuntimeException(ex); } } protected void registerOpenApiSpec(String content) { if (!this._swaggerEnable) return; this.registerRoute(HttpMethod.GET, this._swaggerRoute, null, new Inflector() { @Override public Response apply(ContainerRequestContext req) { return Response.status(200) .entity(content) .header("Content-Length", content.length()) .header("Content-Type", "application/x-yaml") .type(MediaType.APPLICATION_XML_TYPE) .build(); } }); if (this._swaggerService != null) this._swaggerService.registerOpenApiSpec(this._baseRoute, this._swaggerRoute); } /** * Returns correlationId from request * * @param req - http request * @return Returns correlationId from request */ protected String getCorrelationId(ContainerRequestContext req) { var correlationId = getQueryParameter(req, "correlation_id"); if (correlationId == null || correlationId.equals("")) { correlationId = req.getHeaderString("correlation_id"); } return correlationId; } /** * Registers all service routes in HTTP endpoint. *

* This method is called by the service and must be overriden * in child classes. */ @Override public abstract void register(); }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy