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

org.httprpc.kilo.WebService Maven / Gradle / Ivy

There is a newer version: 4.9
Show newest version
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.httprpc.kilo;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.Part;
import org.httprpc.kilo.beans.BeanAdapter;
import org.httprpc.kilo.io.JSONDecoder;
import org.httprpc.kilo.io.JSONEncoder;
import org.httprpc.kilo.io.TemplateEncoder;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;

import static org.httprpc.kilo.util.Collections.*;
import static org.httprpc.kilo.util.Optionals.*;

/**
 * Abstract base class for web services.
 */
public abstract class WebService extends HttpServlet {
    /**
     * Describes a service instance.
     */
    public static class ServiceDescriptor {
        private String path;
        private String description;
        private boolean deprecated;

        private List endpoints = new LinkedList<>();

        private Map, EnumerationDescriptor> enumerations = new TreeMap<>(Comparator.comparing(WebService::getTypeName));
        private Map, StructureDescriptor> structures = new TreeMap<>(Comparator.comparing(WebService::getTypeName));

        private ServiceDescriptor(String path, Class type) {
            this.path = path;

            description = map(type.getAnnotation(Description.class), Description::value);

            deprecated = type.getAnnotation(Deprecated.class) != null;
        }

        /**
         * Returns the path to the service.
         *
         * @return
         * The service's path.
         */
        public String getPath() {
            return path;
        }

        /**
         * Returns a description of the service.
         *
         * @return
         * The service description, or {@code null} for no description.
         */
        public String getDescription() {
            return description;
        }

        /**
         * Indicates that the service is deprecated.
         *
         * @return
         * {@code true} if the service is deprecated; {@code false}, otherwise.
         */
        public boolean isDeprecated() {
            return deprecated;
        }

        /**
         * Returns the endpoints provided by the service.
         *
         * @return
         * The service endpoints.
         */
        public List getEndpoints() {
            return endpoints;
        }

        /**
         * Returns the enumerations defined by the service.
         *
         * @return
         * The service enumerations.
         */
        public List getEnumerations() {
            return new ArrayList<>(enumerations.values());
        }

        /**
         * Returns the structures defined by the service.
         *
         * @return
         * The service structures.
         */
        public List getStructures() {
            return new ArrayList<>(structures.values());
        }
    }

    /**
     * Describes a service endpoint.
     */
    public static class EndpointDescriptor {
        private String path;

        private List operations = new LinkedList<>();

        private EndpointDescriptor(String path) {
            this.path = path;
        }

        /**
         * Returns the path to the endpoint.
         *
         * @return
         * The endpoint's path.
         */
        public String getPath() {
            return path;
        }

        /**
         * Returns the operations provided by the endpoint.
         *
         * @return
         * The endpoint's operations.
         */
        public List getOperations() {
            return operations;
        }
    }

    /**
     * Describes a service operation.
     */
    public static class OperationDescriptor {
        private String method;
        private String description;
        private boolean deprecated;

        private TypeDescriptor produces = null;

        private List parameters = new LinkedList<>();

        private OperationDescriptor(String method, Method handler) {
            this.method = method;

            description = map(handler.getAnnotation(Description.class), Description::value);

            deprecated = handler.getAnnotation(Deprecated.class) != null;
        }

        /**
         * Returns the HTTP method used to invoke the operation.
         *
         * @return
         * The HTTP method used to invoke the operation.
         */
        public String getMethod() {
            return method;
        }

        /**
         * Returns a description of the operation.
         *
         * @return
         * The operation's description, or {@code null} for no description.
         */
        public String getDescription() {
            return description;
        }

        /**
         * Indicates that the operation is deprecated.
         *
         * @return
         * {@code true} if the operation is deprecated; {@code false},
         * otherwise.
         */
        public boolean isDeprecated() {
            return deprecated;
        }

        /**
         * Returns the type of content produced by the operation.
         *
         * @return
         * The type of content produced by the operation, or {@code null} if
         * the operation does not return response content.
         */
        public TypeDescriptor getProduces() {
            return produces;
        }

        /**
         * Returns the parameters defined by the operation.
         *
         * @return
         * The operation's parameters.
         */
        public List getParameters() {
            return parameters;
        }
    }

    /**
     * Describes a variable.
     */
    public static class VariableDescriptor {
        private String name;
        private String description;
        private boolean required;

        private TypeDescriptor type = null;

        private VariableDescriptor(Parameter parameter) {
            name = coalesce(map(parameter.getAnnotation(Name.class), Name::value), parameter.getName());

            description = map(parameter.getAnnotation(Description.class), Description::value);

            required = parameter.getAnnotation(Required.class) != null;
        }

        private VariableDescriptor(String name, Method accessor) {
            this.name = name;

            description = map(accessor.getAnnotation(Description.class), Description::value);

            required = accessor.getAnnotation(Required.class) != null;
        }

        /**
         * Returns the name of the variable.
         *
         * @return
         * The variable's name.
         */
        public String getName() {
            return name;
        }

        /**
         * Returns a description of the variable.
         *
         * @return
         * The variable's description, or {@code null} for no description.
         */
        public String getDescription() {
            return description;
        }

        /**
         * Indicates that the variable is required.
         *
         * @return
         * {@code true} if the variable is required; {@code false}, otherwise.
         */
        public boolean isRequired() {
            return required;
        }

        /**
         * Returns the type of the variable.
         *
         * @return
         * The variable's type.
         */
        public TypeDescriptor getType() {
            return type;
        }
    }

    /**
     * Describes an enumeration.
     */
    public static class EnumerationDescriptor {
        private String name;
        private String description;

        private List values = new LinkedList<>();

        private EnumerationDescriptor(Class type) {
            name = getTypeName(type);

            description = map(type.getAnnotation(Description.class), Description::value);
        }

        /**
         * Returns the name of the enumeration.
         *
         * @return
         * The enumeration's name.
         */
        public String getName() {
            return name;
        }

        /**
         * Returns a description of the enumeration.
         *
         * @return
         * The enumeration's description, or {@code null} for no description.
         */
        public String getDescription() {
            return description;
        }

        /**
         * Returns the values defined by the enumeration.
         *
         * @return
         * The enumeration's values.
         */
        public List getValues() {
            return values;
        }
    }

    /**
     * Describes a constant.
     */
    public static class ConstantDescriptor {
        private String name;
        private String description;

        private ConstantDescriptor(Field field) {
            Object constant;
            try {
                constant = field.get(null);
            } catch (IllegalAccessException exception) {
                throw new RuntimeException(exception);
            }

            name = constant.toString();

            description = map(field.getAnnotation(Description.class), Description::value);
        }

        /**
         * Returns the name of the constant.
         *
         * @return
         * The constant's name.
         */
        public String getName() {
            return name;
        }

        /**
         * Returns a description of the constant.
         *
         * @return
         * The constant's description, or {@code null} for no description.
         */
        public String getDescription() {
            return description;
        }
    }

    /**
     * Describes a structure.
     */
    public static class StructureDescriptor {
        private String name;
        private String description;

        private List supertypes = new LinkedList<>();
        private List properties = new LinkedList<>();

        private StructureDescriptor(Class type) {
            name = getTypeName(type);

            description = map(type.getAnnotation(Description.class), Description::value);
        }

        /**
         * Returns the name of the structure.
         *
         * @return
         * The structure's name.
         */
        public String getName() {
            return name;
        }

        /**
         * Returns a description of the structure.
         *
         * @return
         * The structure's description, or {@code null} for no description.
         */
        public String getDescription() {
            return description;
        }

        /**
         * Returns the structure's supertypes.
         *
         * @return
         * The structure's supertypes.
         */
        public List getSupertypes() {
            return supertypes;
        }

        /**
         * Returns the properties defined by the structure.
         *
         * @return
         * The structure's properties.
         */
        public List getProperties() {
            return properties;
        }
    }

    /**
     * Describes a type.
     */
    public static class TypeDescriptor {
        private Class type;
        private boolean intrinsic;

        private TypeDescriptor(Class type, boolean intrinsic) {
            this.type = type;
            this.intrinsic = intrinsic;
        }

        /**
         * Returns the name of the type represented by the descriptor.
         *
         * @return
         * The type name.
         */
        public String getName() {
            if (type.isPrimitive()) {
                return type.getName();
            } else {
                return getTypeName(type);
            }
        }

        /**
         * Indicates that the type is intrinsic.
         *
         * @return
         * {@code true} if the type is intrinsic; {@code false}, otherwise.
         */
        public boolean isIntrinsic() {
            return intrinsic;
        }

        /**
         * Indicates that the type is iterable.
         *
         * @return
         * {@code true} if the type is iterable; {@code false}, otherwise.
         */
        public boolean isIterable() {
            return false;
        }

        /**
         * Returns the element type.
         *
         * @return
         * The element type, or {@code null} if the type is not a list.
         */
        public TypeDescriptor getElementType() {
            return null;
        }

        /**
         * Indicates that the type is a map.
         *
         * @return
         * {@code true} if the type is a map; {@code false}, otherwise.
         */
        public boolean isMap() {
            return false;
        }

        /**
         * Returns the key type.
         *
         * @return
         * The key type, or {@code null} if the type is not a map.
         */
        public TypeDescriptor getKeyType() {
            return null;
        }

        /**
         * Returns the value type.
         *
         * @return
         * The value type, or {@code null} if the type is not a map.
         */
        public TypeDescriptor getValueType() {
            return null;
        }
    }

    /**
     * Describes an iterable type.
     */
    public static class IterableTypeDescriptor extends TypeDescriptor {
        private TypeDescriptor elementType;

        private IterableTypeDescriptor(TypeDescriptor elementType) {
            super(Iterable.class, true);

            this.elementType = elementType;
        }

        @Override
        public boolean isIterable() {
            return true;
        }

        @Override
        public TypeDescriptor getElementType() {
            return elementType;
        }
    }

    /**
     * Describes a map type.
     */
    public static class MapTypeDescriptor extends TypeDescriptor {
        private TypeDescriptor keyType;
        private TypeDescriptor valueType;

        private MapTypeDescriptor(TypeDescriptor keyType, TypeDescriptor valueType) {
            super(Map.class, true);

            this.keyType = keyType;
            this.valueType = valueType;
        }

        @Override
        public boolean isMap() {
            return true;
        }

        @Override
        public TypeDescriptor getKeyType() {
            return keyType;
        }

        @Override
        public TypeDescriptor getValueType() {
            return valueType;
        }
    }

    private static class Resource {
        static List order = listOf("GET", "POST", "PUT", "DELETE");

        final Map> handlerMap = new TreeMap<>((method1, method2) -> {
            var i1 = order.indexOf(method1);
            var i2 = order.indexOf(method2);

            return Integer.compare((i1 == -1) ? order.size() : i1, (i2 == -1) ? order.size() : i2);
        });

        final Map resources = new TreeMap<>();
    }

    private static class PartURLConnection extends URLConnection {
        Part part;

        PartURLConnection(URL url, Part part) {
            super(url);

            this.part = part;
        }

        @Override
        public void connect() {
            // No-op
        }

        @Override
        public InputStream getInputStream() throws IOException {
            return part.getInputStream();
        }
    }

    private static class PartURLStreamHandler extends URLStreamHandler {
        Part part;

        PartURLStreamHandler(Part part) {
            this.part = part;
        }

        @Override
        protected URLConnection openConnection(URL url) {
            return new PartURLConnection(url, part);
        }
    }

    private Resource root = null;

    private ThreadLocal request = new ThreadLocal<>();
    private ThreadLocal response = new ThreadLocal<>();

    private ServiceDescriptor serviceDescriptor = null;

    private static final Map, WebService> instances = new HashMap<>();

    private static final Comparator methodNameComparator = Comparator.comparing(Method::getName);
    private static final Comparator methodParameterCountComparator = Comparator.comparing(Method::getParameterCount);

    /**
     * JSON MIME type.
     */
    protected static final String APPLICATION_JSON = "application/json";

    /**
     * CSV MIME type.
     */
    protected static final String TEXT_CSV = "text/csv";

    /**
     * HTML MIME type.
     */
    protected static final String TEXT_HTML = "text/html";

    /**
     * XML MIME type.
     */
    protected static final String TEXT_XML = "text/xml";

    private static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
    private static final String MULTIPART_FORM_DATA = "multipart/form-data";

    private static final String UTF_8 = "UTF-8";

    /**
     * Returns a service instance.
     *
     * @param 
     * The service type.
     *
     * @param type
     * The service type.
     *
     * @return
     * The service instance, or {@code null} if no service of the given type
     * exists.
     */
    @SuppressWarnings("unchecked")
    public static synchronized  T getInstance(Class type) {
        return (T)instances.get(type);
    }

    /**
     * Returns a list of descriptors for all active services.
     *
     * @return
     * A list of active service descriptors.
     */
    public static synchronized List getServiceDescriptors() {
        return instances.values().stream()
            .map(WebService::getServiceDescriptor)
            .sorted(Comparator.comparing(WebService.ServiceDescriptor::getPath))
            .toList();
    }

    /**
     * Initializes the service instance.
     * {@inheritDoc}
     */
    @Override
    public void init() throws ServletException {
        var type = getClass();

        var webServlet = type.getAnnotation(WebServlet.class);

        if (webServlet == null) {
            throw new ServletException("Missing web servlet annotation.");
        }

        var urlPatterns = webServlet.urlPatterns();

        if (urlPatterns.length == 0) {
            throw new ServletException("At least one URL pattern is required.");
        }

        var path = urlPatterns[0];

        if (!(path.startsWith("/") && path.endsWith("/*"))) {
            throw new ServletException("Invalid URL pattern.");
        }

        path = path.substring(0, path.length() - 2);

        root = index(type.getMethods());

        serviceDescriptor = new ServiceDescriptor(path, type);

        describeResource(path, root);

        synchronized (WebService.class) {
            instances.put(type, this);
        }
    }

    private static Resource index(Method[] methods) throws ServletException {
        var root = new Resource();

        for (var i = 0; i < methods.length; i++) {
            var handler = methods[i];

            var requestMethod = handler.getAnnotation(RequestMethod.class);

            if (requestMethod != null) {
                var method = requestMethod.value().toUpperCase();

                var resource = root;

                var resourcePath = handler.getAnnotation(ResourcePath.class);

                if (resourcePath != null) {
                    var components = resourcePath.value().split("/");

                    for (var j = 0; j < components.length; j++) {
                        var component = components[j];

                        if (component.isEmpty()) {
                            throw new ServletException("Invalid resource path.");
                        }

                        resource = resource.resources.computeIfAbsent(component, key -> new Resource());
                    }
                }

                resource.handlerMap.computeIfAbsent(method, key -> new LinkedList<>()).add(handler);
            }
        }

        sort(root);

        return root;
    }

    private static void sort(Resource root) {
        for (var handlers : root.handlerMap.values()) {
            handlers.sort(methodNameComparator.thenComparing(methodParameterCountComparator.reversed()));
        }

        for (var resource : root.resources.values()) {
            sort(resource);
        }
    }

    /**
     * Processes a service request.
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        var method = request.getMethod().toUpperCase();
        var pathInfo = request.getPathInfo();

        if (method.equals("GET") && pathInfo == null) {
            var api = request.getParameter("api");

            if (api != null) {
                var accept = request.getHeader("Accept");

                if (accept != null && accept.equalsIgnoreCase(APPLICATION_JSON)) {
                    response.setContentType(String.format("%s;charset=%s", APPLICATION_JSON, UTF_8));

                    var jsonEncoder = new JSONEncoder();

                    jsonEncoder.write(new BeanAdapter(serviceDescriptor), response.getOutputStream());
                } else {
                    response.setContentType(String.format("%s;charset=%s", TEXT_HTML, UTF_8));

                    var templateEncoder = new TemplateEncoder(WebService.class.getResource("api.html"));

                    templateEncoder.setResourceBundle(ResourceBundle.getBundle(WebService.class.getName(), request.getLocale()));

                    templateEncoder.write(mapOf(
                        entry("contextPath", request.getContextPath()),
                        entry("service", new BeanAdapter(serviceDescriptor))
                    ), response.getOutputStream());
                }

                return;
            }
        }

        var resource = root;

        List keys = new ArrayList<>();

        if (pathInfo != null) {
            var components = pathInfo.split("/");

            for (var i = 1; i < components.length; i++) {
                var component = components[i];

                var child = resource.resources.get(component);

                if (child == null) {
                    child = resource.resources.get("?");

                    if (child == null) {
                        super.service(request, response);
                        return;
                    }

                    keys.add(component);
                }

                resource = child;
            }
        }

        var handlerList = resource.handlerMap.get(method);

        if (handlerList == null) {
            super.service(request, response);
            return;
        }

        if (request.getCharacterEncoding() == null) {
            request.setCharacterEncoding(UTF_8);
        }

        Map> argumentMap = new HashMap<>();

        var parameterNames = request.getParameterNames();

        while (parameterNames.hasMoreElements()) {
            var name = parameterNames.nextElement();

            argumentMap.put(name, Arrays.asList(request.getParameterValues(name)));
        }

        var contentType = map(request.getContentType(), String::toLowerCase);

        if (contentType != null && contentType.startsWith(MULTIPART_FORM_DATA)) {
            for (var part : request.getParts()) {
                var submittedFileName = part.getSubmittedFileName();

                if (submittedFileName == null || submittedFileName.isEmpty()) {
                    continue;
                }

                var name = part.getName();

                var values = (List)argumentMap.get(name);

                if (values == null) {
                    values = new ArrayList<>();

                    argumentMap.put(name, values);
                }

                values.add(new URL("part", null, -1, submittedFileName, new PartURLStreamHandler(part)));
            }
        }

        var empty = contentType == null
            || contentType.startsWith(APPLICATION_X_WWW_FORM_URLENCODED)
            || contentType.startsWith(MULTIPART_FORM_DATA);

        var handler = getHandler(handlerList, keys.size(), argumentMap.keySet(), empty);

        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
            return;
        }

        Object[] arguments;
        try {
            arguments = getArguments(handler.getParameters(), keys, argumentMap, empty, request);
        } catch (Exception exception) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);

            reportError(response, exception);

            return;
        }

        this.request.set(request);
        this.response.set(response);

        Object result;
        try {
            result = handler.invoke(this, arguments);
        } catch (IllegalAccessException | InvocationTargetException exception) {
            if (response.isCommitted()) {
                return;
            }

            var cause = exception.getCause();

            int status;
            if (cause instanceof IllegalArgumentException || cause instanceof UnsupportedOperationException) {
                status = HttpServletResponse.SC_FORBIDDEN;
            } else if (cause instanceof NoSuchElementException) {
                status = HttpServletResponse.SC_NOT_FOUND;
            } else if (cause instanceof IllegalStateException) {
                status = HttpServletResponse.SC_CONFLICT;
            } else {
                status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
            }

            response.setStatus(status);

            reportError(response, cause);

            return;
        } finally {
            this.request.remove();
            this.response.remove();
        }

        if (response.isCommitted()) {
            return;
        }

        if (result != null) {
            if (handler.getAnnotation(Creates.class) != null) {
                response.setStatus(HttpServletResponse.SC_CREATED);
            }

            encodeResult(request, response, result);
        } else {
            var returnType = handler.getReturnType();

            if (returnType == Void.TYPE || returnType == Void.class) {
                response.setStatus(HttpServletResponse.SC_NO_CONTENT);
            } else {
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            }
        }
    }

    private static Method getHandler(List handlerList, int keyCount, Set argumentNames, boolean empty) {
        for (var handler : handlerList) {
            var parameters = handler.getParameters();

            var n = parameters.length;

            if (!empty) {
                n--;
            }

            if (keyCount > n) {
                continue;
            }

            if (argumentNames.isEmpty()) {
                return handler;
            }

            var c = 0;

            var argumentCount = argumentNames.size();

            for (var i = keyCount; i < n; i++) {
                var parameter = parameters[i];

                var name = coalesce(map(parameter.getAnnotation(Name.class), Name::value), parameter.getName());

                if (argumentNames.contains(name) && ++c == argumentCount) {
                    return handler;
                }
            }
        }

        return null;
    }

    private Object[] getArguments(Parameter[] parameters, List keys, Map> argumentMap, boolean empty, HttpServletRequest request) {
        var n = parameters.length;

        var arguments = new Object[n];

        if (!empty) {
            n--;
        }

        var keyCount = keys.size();

        for (var i = 0; i < n; i++) {
            var parameter = parameters[i];

            if (i < keyCount) {
                arguments[i] = BeanAdapter.coerce(keys.get(i), parameter.getType());
            } else {
                var name = coalesce(map(parameter.getAnnotation(Name.class), Name::value), parameter.getName());
                var type = parameter.getType();

                var values = argumentMap.get(name);

                Object argument;
                if (type.isArray()) {
                    var componentType = type.getComponentType();

                    if (values != null) {
                        argument = Array.newInstance(componentType, values.size());

                        var j = 0;

                        for (var value : values) {
                            Array.set(argument, j++, BeanAdapter.coerce(value, componentType));
                        }
                    } else {
                        argument = Array.newInstance(componentType, 0);
                    }
                } else if (Collection.class.isAssignableFrom(type)) {
                    var elementType = ((ParameterizedType)parameter.getParameterizedType()).getActualTypeArguments()[0];

                    if (elementType instanceof Class) {
                        if (type == List.class) {
                            if (values == null) {
                                argument = listOf();
                            } else {
                                argument = BeanAdapter.coerceList(values, (Class)elementType);
                            }
                        } else if (type == Set.class) {
                            if (values == null) {
                                argument = setOf();
                            } else {
                                argument = BeanAdapter.coerceSet(values, (Class)elementType);
                            }
                        } else {
                            throw new UnsupportedOperationException("Unsupported collection type.");
                        }
                    } else {
                        throw new UnsupportedOperationException("Invalid element type.");
                    }
                } else {
                    Object value;
                    if (values != null) {
                        value = values.get(values.size() - 1);
                    } else {
                        value = null;
                    }

                    if (parameter.getAnnotation(Required.class) != null && value == null) {
                        throw new IllegalArgumentException("Required argument is not defined.");
                    }

                    argument = BeanAdapter.coerce(value, type);
                }

                arguments[i] = argument;
            }
        }

        if (n < parameters.length) {
            try {
                arguments[n] = decodeBody(request, parameters[n].getType());
            } catch (IOException exception) {
                throw new UnsupportedOperationException(exception);
            }
        }

        return arguments;
    }

    /**
     * Returns the servlet request.
     *
     * @return
     * The servlet request.
     */
    protected HttpServletRequest getRequest() {
        return request.get();
    }

    /**
     * Returns the servlet response.
     *
     * @return
     * The servlet response.
     */
    protected HttpServletResponse getResponse() {
        return response.get();
    }

    /**
     * Decodes the body of a service request.
     *
     * @param request
     * The servlet request.
     *
     * @param type
     * The body type.
     *
     * @return
     * The decoded body.
     *
     * @throws IOException
     * If an exception occurs while decoding the content.
     */
    protected Object decodeBody(HttpServletRequest request, Type type) throws IOException {
        var jsonDecoder = new JSONDecoder();

        return BeanAdapter.toGenericType(jsonDecoder.read(request.getInputStream()), type);
    }

    /**
     * Encodes the result of a service operation.
     *
     * @param request
     * The servlet request.
     *
     * @param response
     * The servlet response.
     *
     * @param result
     * The operation result.
     *
     * @throws IOException
     * If an exception occurs while encoding the result.
     */
    protected void encodeResult(HttpServletRequest request, HttpServletResponse response, Object result) throws IOException {
        response.setContentType(String.format("%s;charset=%s", APPLICATION_JSON, UTF_8));

        var jsonEncoder = new JSONEncoder();

        jsonEncoder.write(BeanAdapter.adapt(result), response.getOutputStream());
    }

    /**
     * Reports an error.
     *
     * @param response
     * The servlet response.
     *
     * @param cause
     * The cause of the error.
     */
    protected void reportError(HttpServletResponse response, Throwable cause) throws IOException {
        response.setContentType(String.format("text/plain;charset=%s", UTF_8));

        if (cause != null) {
            var message = cause.getMessage();

            if (message != null) {
                response.getWriter().append(message);
            }
        }
    }

    /**
     * Returns the service descriptor.
     *
     * @return
     * The service descriptor.
     */
    public ServiceDescriptor getServiceDescriptor() {
        return serviceDescriptor;
    }

    private void describeResource(String path, Resource resource) {
        if (!resource.handlerMap.isEmpty()) {
            var endpoint = new EndpointDescriptor(path);

            for (var entry : resource.handlerMap.entrySet()) {
                for (var handler : entry.getValue()) {
                    var operation = new OperationDescriptor(entry.getKey().toUpperCase(), handler);

                    operation.deprecated |= serviceDescriptor.deprecated;

                    operation.produces = describeGenericType(handler.getGenericReturnType());

                    var parameters = handler.getParameters();

                    for (var i = 0; i < parameters.length; i++) {
                        var parameter = parameters[i];

                        var parameterDescriptor = new VariableDescriptor(parameter);

                        parameterDescriptor.type = describeGenericType(parameter.getParameterizedType());

                        operation.parameters.add(parameterDescriptor);
                    }

                    endpoint.operations.add(operation);
                }
            }

            serviceDescriptor.endpoints.add(endpoint);
        }

        for (var entry : resource.resources.entrySet()) {
            describeResource(String.format("%s/%s", path, entry.getKey()), entry.getValue());
        }
    }

    private TypeDescriptor describeGenericType(Type type) {
        if (type instanceof Class) {
            return describeRawType((Class)type);
        } else if (type instanceof ParameterizedType parameterizedType) {
            var rawType = (Class)parameterizedType.getRawType();
            var actualTypeArguments = parameterizedType.getActualTypeArguments();

            if (Iterable.class.isAssignableFrom(rawType)) {
                return new IterableTypeDescriptor(describeGenericType(actualTypeArguments[0]));
            } else if (Map.class.isAssignableFrom(rawType)) {
                return new MapTypeDescriptor(describeGenericType(actualTypeArguments[0]), describeGenericType(actualTypeArguments[1]));
            } else {
                throw new IllegalArgumentException();
            }
        } else {
            throw new IllegalArgumentException();
        }
    }

    private TypeDescriptor describeRawType(Class type) {
        if (type.isPrimitive()
            || type == Object.class
            || type == Boolean.class
            || Number.class.isAssignableFrom(type)
            || String.class.isAssignableFrom(type)
            || type == Void.class
            || Date.class.isAssignableFrom(type)
            || type == Instant.class
            || type == LocalDate.class
            || type == LocalTime.class
            || type == LocalDateTime.class
            || type == Duration.class
            || type == Period.class
            || type == UUID.class
            || type == URL.class) {
            return new TypeDescriptor(type, true);
        } else if (type.isArray()) {
            return new IterableTypeDescriptor(describeRawType(type.getComponentType()));
        } else if (Iterable.class.isAssignableFrom(type)) {
            return new IterableTypeDescriptor(describeRawType(Object.class));
        } else if (Map.class.isAssignableFrom(type)) {
            return new MapTypeDescriptor(describeRawType(Object.class), describeRawType(Object.class));
        } else {
            if (type.isEnum()) {
                var enumeration = serviceDescriptor.enumerations.get(type);

                if (enumeration == null) {
                    enumeration = new EnumerationDescriptor(type);

                    serviceDescriptor.enumerations.put(type, enumeration);

                    var fields = type.getDeclaredFields();

                    for (var i = 0; i < fields.length; i++) {
                        var field = fields[i];

                        if (!field.isEnumConstant()) {
                            continue;
                        }

                        enumeration.values.add(new ConstantDescriptor(field));
                    }
                }
            } else {
                var structure = serviceDescriptor.structures.get(type);

                if (structure == null) {
                    structure = new StructureDescriptor(type);

                    serviceDescriptor.structures.put(type, structure);

                    if (type.isInterface()) {
                        var interfaces = type.getInterfaces();

                        for (var i = 0; i < interfaces.length; i++) {
                            structure.supertypes.add(describeRawType(interfaces[i]));
                        }
                    } else {
                        var baseType = type.getSuperclass();

                        if (baseType != Object.class && baseType != Record.class) {
                            structure.supertypes.add(describeRawType(baseType));
                        }
                    }

                    for (var entry : BeanAdapter.getProperties(type).entrySet()) {
                        var accessor = entry.getValue().getAccessor();

                        if (accessor.getDeclaringClass() != type) {
                            continue;
                        }

                        var propertyDescriptor = new VariableDescriptor(entry.getKey(), accessor);

                        propertyDescriptor.type = describeGenericType(accessor.getGenericReturnType());

                        structure.properties.add(propertyDescriptor);
                    }
                }
            }

            return new TypeDescriptor(type, false);
        }
    }

    private static String getTypeName(Class type) {
        return type.getCanonicalName().substring(type.getPackageName().length() + 1);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy