com.haulmont.restapi.config.RestServicesConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for com.haulmont.restapi.config.RestServicesConfiguration.java

Source

/*
 * Copyright (c) 2008-2016 Haulmont.
 *
 * 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 com.haulmont.restapi.config;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.haulmont.bali.util.Dom4j;
import com.haulmont.cuba.core.global.AppBeans;
import com.haulmont.cuba.core.global.Resources;
import com.haulmont.cuba.core.sys.AppContext;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrTokenizer;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;

import javax.annotation.Nullable;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

/**
 * Class is used for loading and storing an information about service methods that are available for REST API.
 * information loaded from configuration files defined by the {@code cuba.rest.servicesConfig} application property.
 * <p>
 * Configuration file must define method name and method argument names that will be user for method invocation by the
 * REST API.
 * <p>
 * Method parameter types can be omitted if the service doesn't contain an overloaded method with the same parameters
 * number. Otherwise, types must be defined.
 */
@Component("cuba_RestServicesConfiguration")
public class RestServicesConfiguration {

    private static final Logger log = LoggerFactory.getLogger(RestServicesConfiguration.class);

    public static final String CUBA_REST_SERVICES_CONFIG_PROP_NAME = "cuba.rest.servicesConfig";

    protected Map<String, RestServiceInfo> serviceInfosMap = new ConcurrentHashMap<>();

    protected volatile boolean initialized;

    protected ReadWriteLock lock = new ReentrantReadWriteLock();

    @Inject
    protected Resources resources;

    /**
     * @deprecated the method will be removed in one of next releases. Use {@link #getRestMethodInfo(String, String,
     * List)} instead
     */
    @Nullable
    @Deprecated
    public Method getServiceMethod(String serviceName, String methodName, List<String> methodParamNames) {
        lock.readLock().lock();
        try {
            checkInitialized();
            RestServiceInfo restServiceInfo = serviceInfosMap.get(serviceName);
            if (restServiceInfo == null)
                return null;
            Optional<RestMethodInfo> methodInfoOptional = restServiceInfo.getMethods().stream()
                    .filter(restMethodInfo -> methodName.equals(restMethodInfo.getName())
                            && paramsMatches(restMethodInfo.getParams(), methodParamNames))
                    .findFirst();
            if (methodInfoOptional.isPresent()) {
                return methodInfoOptional.get().getMethod();
            }
        } finally {
            lock.readLock().unlock();
        }
        return null;
    }

    @Nullable
    public RestMethodInfo getRestMethodInfo(String serviceName, String methodName, List<String> methodParamNames) {
        lock.readLock().lock();
        try {
            checkInitialized();
            RestServiceInfo restServiceInfo = serviceInfosMap.get(serviceName);
            if (restServiceInfo == null)
                return null;
            return restServiceInfo.getMethods().stream()
                    .filter(restMethodInfo -> methodName.equals(restMethodInfo.getName())
                            && paramsMatches(restMethodInfo.getParams(), methodParamNames))
                    .findFirst().orElse(null);
        } finally {
            lock.readLock().unlock();
        }
    }

    protected boolean paramsMatches(List<RestMethodParamInfo> paramInfos, List<String> paramNames) {
        if (paramInfos.size() != paramNames.size())
            return false;
        List<String> paramInfosNames = paramInfos.stream().map(RestMethodParamInfo::getName)
                .collect(Collectors.toList());
        return CollectionUtils.isEqualCollection(paramInfosNames, paramNames);
    }

    protected void checkInitialized() {
        if (!initialized) {
            lock.readLock().unlock();
            lock.writeLock().lock();
            try {
                if (!initialized) {
                    init();
                    initialized = true;
                }
            } finally {
                lock.readLock().lock();
                lock.writeLock().unlock();
            }
        }
    }

    protected void init() {
        String configName = AppContext.getProperty(CUBA_REST_SERVICES_CONFIG_PROP_NAME);
        StrTokenizer tokenizer = new StrTokenizer(configName);
        for (String location : tokenizer.getTokenArray()) {
            Resource resource = resources.getResource(location);
            if (resource.exists()) {
                InputStream stream = null;
                try {
                    stream = resource.getInputStream();
                    loadConfig(Dom4j.readDocument(stream).getRootElement());
                } catch (IOException e) {
                    throw new RuntimeException("Error on parsing rest services config", e);
                } finally {
                    IOUtils.closeQuietly(stream);
                }
            } else {
                log.warn("Resource " + location + " not found, ignore it");
            }
        }
    }

    protected void loadConfig(Element rootElem) {
        for (Element serviceElem : Dom4j.elements(rootElem, "service")) {
            String serviceName = serviceElem.attributeValue("name");
            if (!AppBeans.containsBean(serviceName)) {
                log.error("Service not found: {}", serviceName);
                continue;
            }
            Object service = AppBeans.get(serviceName);
            List<RestMethodInfo> methodInfos = new ArrayList<>();

            for (Element methodElem : Dom4j.elements(serviceElem, "method")) {
                String methodName = methodElem.attributeValue("name");
                List<RestMethodParamInfo> params = new ArrayList<>();
                for (Element paramEl : Dom4j.elements(methodElem, "param")) {
                    params.add(new RestMethodParamInfo(paramEl.attributeValue("name"),
                            paramEl.attributeValue("type")));
                }
                Method method = _findMethod(serviceName, methodName, params);
                if (method != null) {
                    methodInfos.add(new RestMethodInfo(methodName, params, method));
                }
            }

            serviceInfosMap.put(serviceName, new RestServiceInfo(serviceName, methodInfos));
        }
    }

    @Nullable
    protected Method _findMethod(String serviceName, String methodName, List<RestMethodParamInfo> paramInfos) {
        List<Class> paramTypes = new ArrayList<>();
        for (RestMethodParamInfo paramInfo : paramInfos) {
            if (StringUtils.isNotEmpty(paramInfo.getType())) {
                try {
                    paramTypes.add(ClassUtils.forName(paramInfo.getType(), null));
                } catch (ClassNotFoundException e) {
                    log.error("Class {} for method parameter not found. Service: {}, method: {}, param: {}",
                            paramInfo.getType(), serviceName, methodName, paramInfo.getName());
                    return null;
                }
            }
        }

        if (!paramTypes.isEmpty() && paramInfos.size() != paramTypes.size()) {
            log.error(
                    "Service method parameters types must be defined for all parameters or for none of them. Service: {}, method: {}",
                    serviceName, methodName);
            return null;
        }

        Object service = AppBeans.get(serviceName);
        Method serviceMethod = null;
        //the service object we get here is proxy. To get methods with type information
        //we need tp know actual interfaces implemented by the service (this is required when parameterized
        //collection of entities is passed as an argument)
        Class<?>[] serviceInterfaces = service.getClass().getInterfaces();
        if (serviceInterfaces.length == 0) {
            log.error("Service {} doesn't implement any interface. It cannot be user for the REST API",
                    serviceName);
            return null;
        }

        if (paramTypes.isEmpty()) {
            List<Method> appropriateMethods = new ArrayList<>();
            for (Class<?> serviceInterface : serviceInterfaces) {
                //trying to guess which method to invoke
                Method[] methods = serviceInterface.getMethods();
                for (Method method : methods) {
                    if (methodName.equals(method.getName())
                            && method.getParameterTypes().length == paramInfos.size()) {
                        appropriateMethods.add(method);
                    }
                }
            }
            if (appropriateMethods.size() == 1) {
                serviceMethod = appropriateMethods.get(0);
            } else if (appropriateMethods.size() > 1) {
                log.error(
                        "There are multiple methods with given argument numbers. Parameters type must be defined. Service: {}, method: {}",
                        serviceName, methodName);
                return null;
            } else {
                log.error("Method not found. Service: {}, method: {}, number of arguments: {}", serviceName,
                        methodName, paramInfos.size());
                return null;
            }
        } else {
            for (Class<?> serviceInterface : serviceInterfaces) {
                try {
                    serviceMethod = serviceInterface.getMethod(methodName,
                            paramTypes.toArray(new Class[paramTypes.size()]));
                } catch (NoSuchMethodException ignored) {
                }
            }
            if (serviceMethod == null) {
                log.error("Method not found. Service: {}, method: {}, argument types: {}", serviceName, methodName,
                        paramTypes);
                return null;
            }
        }
        return serviceMethod;
    }

    public Collection<RestServiceInfo> getServiceInfos() {
        lock.readLock().lock();
        try {
            checkInitialized();
            return serviceInfosMap.values();
        } finally {
            lock.readLock().unlock();
        }
    }

    @Nullable
    public RestServiceInfo getServiceInfo(String serviceName) {
        lock.readLock().lock();
        try {
            checkInitialized();
            return serviceInfosMap.get(serviceName);
        } finally {
            lock.readLock().unlock();
        }
    }

    public static class RestServiceInfo {
        protected String name;
        protected List<RestMethodInfo> methods = new ArrayList<>();

        public RestServiceInfo(String name, List<RestMethodInfo> methods) {
            this.name = name;
            this.methods = methods;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public List<RestMethodInfo> getMethods() {
            return methods;
        }

        public void setMethods(List<RestMethodInfo> methods) {
            this.methods = methods;
        }
    }

    public static class RestMethodInfo {
        protected String name;
        protected List<RestMethodParamInfo> params = new ArrayList<>();
        @JsonIgnore
        protected Method method;

        public RestMethodInfo(String name, List<RestMethodParamInfo> params, Method method) {
            this.name = name;
            this.params = params;
            this.method = method;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public List<RestMethodParamInfo> getParams() {
            return params;
        }

        public void setParams(List<RestMethodParamInfo> params) {
            this.params = params;
        }

        public Method getMethod() {
            return method;
        }

        public void setMethod(Method method) {
            this.method = method;
        }
    }

    public static class RestMethodParamInfo {
        protected String name;
        protected String type;

        public RestMethodParamInfo(String name, String type) {
            this.name = name;
            this.type = type;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }
    }
}