com.haulmont.cuba.restapi.DataServiceController.java Source code

Java tutorial

Introduction

Here is the source code for com.haulmont.cuba.restapi.DataServiceController.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.cuba.restapi;

import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaProperty;
import com.haulmont.chile.core.model.impl.AbstractInstance;
import com.haulmont.cuba.client.sys.cache.ClientCacheManager;
import com.haulmont.cuba.core.app.DataService;
import com.haulmont.cuba.core.app.DomainDescriptionService;
import com.haulmont.cuba.core.entity.BaseGenericIdEntity;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.entity.HasUuid;
import com.haulmont.cuba.core.global.*;
import com.haulmont.cuba.core.sys.AbstractViewRepository;
import com.haulmont.cuba.security.entity.EntityOp;
import freemarker.template.TemplateException;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.json.JSONException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.*;

import javax.activation.MimeType;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Controller
public class DataServiceController {

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

    @Inject
    protected ConversionFactory conversionFactory;

    @Inject
    protected DataService dataService;

    @Inject
    protected UserSessionSource userSessionSource;

    @Inject
    protected Security security;

    @Inject
    protected Metadata metadata;

    @Inject
    protected DomainDescriptionService domainDescriptionService;

    @Inject
    protected Authentication authentication;

    @Inject
    protected RestServicePermissions restServicePermissions;

    @Inject
    protected ClientCacheManager clientCacheManager;

    @Inject
    protected EntityLoadInfoBuilder entityLoadInfoBuilder;

    @RequestMapping(value = "/api/find.{type}", method = RequestMethod.GET)
    public void find(@PathVariable String type, @RequestParam(value = "e") String entityRef,
            @RequestParam(value = "s") String sessionId,
            @RequestParam(value = "dynamicAttributes", required = false) Boolean dynamicAttributes,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {

        if (!connect(sessionId, response))
            return;

        try {
            EntityLoadInfo loadInfo = EntityLoadInfo.parse(entityRef);
            if (loadInfo == null) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
                return;
            }
            MetaClass metaClass = loadInfo.getMetaClass();
            if (!readPermitted(metaClass)) {
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
                return;
            }

            Object objectId = loadInfo.getId();

            LoadContext loadCtx = new LoadContext(metaClass);
            loadCtx.setLoadDynamicAttributes(Boolean.TRUE.equals(dynamicAttributes));
            loadCtx.setId(objectId);
            if (loadInfo.getViewName() != null) {
                loadCtx.setView(loadInfo.getViewName());
            } else {
                View view = metadata.getViewRepository().getView(metaClass, View.LOCAL);
                loadCtx.setView(new View(view, "local-with-system-props", true));
            }

            Entity entity = dataService.load(loadCtx);
            if (entity == null) {
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            } else {
                Converter converter = conversionFactory.getConverter(type);
                String result = converter.process(entity, metaClass, loadCtx.getView());
                writeResponse(response, result, converter.getMimeType());
            }
        } catch (Throwable e) {
            sendError(request, response, e);
        } finally {
            authentication.end();
        }
    }

    @RequestMapping(value = "/api/query.{type}", method = RequestMethod.GET)
    public void queryByGet(@PathVariable String type, @RequestParam(value = "e") String entityName,
            @RequestParam(value = "q") String queryStr,
            @RequestParam(value = "view", required = false) String viewName,
            @RequestParam(value = "first", required = false) Integer firstResult,
            @RequestParam(value = "max", required = false) Integer maxResults,
            @RequestParam(value = "s") String sessionId,
            @RequestParam(value = "dynamicAttributes", required = false) Boolean dynamicAttributes,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {

        if (!connect(sessionId, response))
            return;

        try {
            MetaClass metaClass = metadata.getClass(entityName);
            if (metaClass == null) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "Persistent entity " + entityName + " does not exist");
                return;
            }

            if (!entityOpPermitted(metaClass, EntityOp.READ)) {
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
                return;
            }

            Map<String, String[]> queryParams = new HashMap<>(request.getParameterMap());
            queryParams.remove("e");
            queryParams.remove("q");
            queryParams.remove("view");
            queryParams.remove("first");
            queryParams.remove("s");
            queryParams.remove("max");
            queryParams.remove("dynamicAttributes");

            LoadContext loadCtx = new LoadContext(metaClass);
            loadCtx.setLoadDynamicAttributes(Boolean.TRUE.equals(dynamicAttributes));
            LoadContext.Query query = new LoadContext.Query(queryStr);
            loadCtx.setQuery(query);
            if (firstResult != null)
                query.setFirstResult(firstResult);
            if (maxResults != null)
                query.setMaxResults(maxResults);

            for (Map.Entry<String, String[]> entry : queryParams.entrySet()) {
                String paramKey = entry.getKey();
                if (paramKey.endsWith("_type"))
                    continue;

                if (entry.getValue().length != 1) {
                    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
                    return;
                }
                String paramValue = entry.getValue()[0];
                Object parsedParam = parseQueryParameter(paramKey, paramValue, queryParams);
                query.setParameter(paramKey, parsedParam);
            }

            if (viewName == null) {
                View view = metadata.getViewRepository().getView(metaClass, View.LOCAL);
                loadCtx.setView(new View(view, "local-with-system-props", true));
            } else {
                loadCtx.setView(viewName);
            }
            List<Entity> entities = dataService.loadList(loadCtx);
            Converter converter = conversionFactory.getConverter(type);
            String result = converter.process(entities, metaClass, loadCtx.getView());
            writeResponse(response, result, converter.getMimeType());
        } catch (Throwable e) {
            sendError(request, response, e);
        } finally {
            authentication.end();
        }
    }

    @RequestMapping(value = "/api/query", method = RequestMethod.POST)
    public void queryByPost(@RequestParam(value = "s") String sessionId,
            @RequestHeader(value = "Content-Type") MimeType contentType, @RequestBody String requestContent,
            HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (!authentication.begin(sessionId)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        try {
            Converter converter = conversionFactory.getConverter(contentType);

            QueryRequest queryRequest = converter.parseQueryRequest(requestContent);

            MetaClass metaClass = metadata.getClass(queryRequest.getEntity());
            if (metaClass == null) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "Persistent entity " + queryRequest.getEntity() + " does not exist");
                return;
            }

            if (!entityOpPermitted(metaClass, EntityOp.READ)) {
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
                return;
            }

            LoadContext loadCtx = new LoadContext(metaClass);
            loadCtx.setLoadDynamicAttributes(Boolean.TRUE.equals(queryRequest.loadDynamicAttributes()));
            LoadContext.Query query = new LoadContext.Query(queryRequest.getQuery());

            for (String key : queryRequest.getParams().keySet()) {
                query.setParameter(key, queryRequest.getParams().get(key));
            }

            loadCtx.setQuery(query);
            if (queryRequest.getFirst() != null)
                query.setFirstResult(queryRequest.getFirst());
            if (queryRequest.getMax() != null)
                query.setMaxResults(queryRequest.getMax());

            if (queryRequest.getViewName() == null) {
                View view = metadata.getViewRepository().getView(metaClass, View.LOCAL);
                loadCtx.setView(new View(view, "local-with-system-props", true));
            } else {
                loadCtx.setView(queryRequest.getViewName());
            }
            List<Entity> entities = dataService.loadList(loadCtx);
            String result = converter.process(entities, metaClass, loadCtx.getView());
            writeResponse(response, result, converter.getMimeType());
        } catch (RowLevelSecurityException e) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN,
                    "The operation with entity " + e.getEntity() + " is denied");
        } catch (Throwable e) {
            sendError(request, response, e);
        } finally {
            authentication.end();
        }
    }

    @RequestMapping(value = "/api/commit", method = RequestMethod.POST)
    public void commit(@RequestParam(value = "s") String sessionId,
            @RequestHeader(value = "Content-Type") MimeType contentType, @RequestBody String requestContent,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {

        if (!connect(sessionId, response))
            return;

        try {
            Converter converter = conversionFactory.getConverter(contentType);

            CommitRequest commitRequest = converter.parseCommitRequest(requestContent);

            Collection commitInstances = commitRequest.getCommitInstances();
            Set<String> newInstanceIds = commitRequest.getNewInstanceIds();

            assignUuidToNewInstances(commitInstances, newInstanceIds);

            for (Object commitInstance : commitInstances) {
                if (commitInstance instanceof BaseGenericIdEntity && !isNewInstance(newInstanceIds, commitInstance)
                        && !PersistenceHelper.isDetached(commitInstance)) {
                    PersistenceHelper.makePatch((BaseGenericIdEntity) commitInstance);
                }
            }

            //send error if the user don't have permissions to commit at least one of the entities
            if (!commitPermitted(commitInstances, newInstanceIds)) {
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
                return;
            }

            Collection removeInstances = commitRequest.getRemoveInstances();
            //send error if the user don't have permissions to remove at least one of the entities
            if (!removePermitted(removeInstances)) {
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
                return;
            }

            CommitContext commitContext = new CommitContext();
            commitContext.setCommitInstances(commitInstances);
            commitContext.setRemoveInstances(removeInstances);
            commitContext.setSoftDeletion(commitRequest.isSoftDeletion());

            List<EntityLoadInfo> newInstances = new ArrayList<>(newInstanceIds.size());
            for (String str : newInstanceIds) {
                newInstances.add(entityLoadInfoBuilder.parse(str));
            }

            for (Entity entity : commitContext.getCommitInstances()) {
                MetaClass metaClass = metadata.getSession().getClassNN(entity.getClass());
                for (MetaProperty property : metaClass.getProperties()) {
                    if (property.getRange().isClass() && !metadata.getTools().isEmbedded(property)
                            && !property.getRange().getCardinality().isMany() && !property.isReadOnly()
                            && PersistenceHelper.isLoaded(entity, property.getName())) {

                        Entity refEntity = entity.getValue(property.getName());
                        if (refEntity == null || refEntity.getId() == null)
                            continue;

                        if (entityLoadInfoBuilder.contains(newInstances, refEntity)) {
                            // reference to a new entity
                            Entity e = getEntityById(commitContext.getCommitInstances(), refEntity.getId());
                            ((AbstractInstance) entity).setValue(property.getName(), e, false);
                        } else if (BaseGenericIdEntity.class
                                .isAssignableFrom(refEntity.getMetaClass().getJavaClass())) {
                            // reference to an existing entity
                            Object refEntityId = refEntity.getId();
                            refEntity = metadata.create(refEntity.getMetaClass());
                            ((BaseGenericIdEntity) refEntity).setId(refEntityId);
                            PersistenceHelper.makeDetached((BaseGenericIdEntity) refEntity);
                            ((AbstractInstance) entity).setValue(property.getName(), refEntity, false);
                        }
                    }
                }
            }

            Set<Entity> result = dataService.commit(commitContext);

            String converted = converter.process(result);
            writeResponse(response, converted, converter.getMimeType());
        } catch (RowLevelSecurityException e) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN,
                    "The operation with entity " + e.getEntity() + " is denied");
        } catch (Throwable e) {
            sendError(request, response, e);
        } finally {
            authentication.end();
        }
    }

    @Nullable
    protected Entity getEntityById(Collection<Entity> entities, Object id) {
        if (id == null)
            return null;

        for (Entity entity : entities)
            if (id.equals(entity.getId()))
                return entity;

        return null;
    }

    private boolean isNewInstance(Set<String> newInstanceIds, Object instance) {
        for (Object id : newInstanceIds) {
            Entity entity = (Entity) instance;
            if (entity instanceof HasUuid) {
                String entityFullId = EntityLoadInfo.create(entity).toString();
                if (entityFullId.equals(id)) {
                    return true;
                }
            }
        }
        return false;
    }

    private void assignUuidToNewInstances(Collection commitInstances, Collection newInstanceIds) {
        for (Object id : newInstanceIds) {
            for (Object instance : commitInstances) {
                Entity entity = (Entity) instance;
                if (entity instanceof HasUuid) {
                    String entityFullId = EntityLoadInfo.create(entity).toString();
                    if (entityFullId.equals(id) && ((HasUuid) entity).getUuid() == null) {
                        ((HasUuid) entity).setUuid(UuidProvider.createUuid());
                    }
                }
            }
        }
    }

    @RequestMapping(value = "/api/deployViews", method = RequestMethod.POST)
    public void deployViews(@RequestParam(value = "s") String sessionId, @RequestBody String requestContent,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {

        if (!connect(sessionId, response))
            return;

        try {
            ViewRepository viewRepository = metadata.getViewRepository();
            ((AbstractViewRepository) viewRepository).deployViews(new StringReader(requestContent));
        } catch (Throwable e) {
            sendError(request, response, e);
        } finally {
            authentication.end();
        }
    }

    @RequestMapping(value = "/api/printDomain", method = RequestMethod.GET)
    public void printDomain(@RequestParam(value = "s") String sessionId, HttpServletRequest request,
            HttpServletResponse response) throws IOException, InvocationTargetException, NoSuchMethodException,
            IllegalAccessException, TemplateException {

        if (!connect(sessionId, response))
            return;

        try {
            response.setContentType("text/html");
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setLocale(userSessionSource.getLocale());

            String domainDescription = domainDescriptionService.getDomainDescription();
            response.getWriter().write(domainDescription);

        } catch (Throwable e) {
            sendError(request, response, e);
        } finally {
            authentication.end();
        }
    }

    @RequestMapping(value = "/api/service.{type}", method = RequestMethod.GET)
    public void serviceByGet(@PathVariable(value = "type") String type, @RequestParam(value = "s") String sessionId,
            @RequestParam(value = "service") String serviceName, @RequestParam(value = "method") String methodName,
            HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (!connect(sessionId, response))
            return;

        if (!restServicePermissions.isPermitted(serviceName, methodName)) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        try {
            Map<String, String[]> parameterMap = request.getParameterMap();
            List<String> paramValuesString = new ArrayList<>();
            List<Class> paramTypes = new ArrayList<>();

            int idx = 0;
            while (true) {
                String[] _values = parameterMap.get("param" + idx);
                if (_values == null)
                    break;
                if (_values.length > 1) {
                    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Multiple values for param" + idx);
                    return;
                }
                paramValuesString.add(_values[0]);

                String[] _types = parameterMap.get("param" + idx + "_type");
                if (_types != null) {
                    if (_types.length > 1) {
                        response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                                "Multiple values for param" + idx + "_type");
                        return;
                    }
                    paramTypes.add(idx, ClassUtils.forName(_types[0], null));
                } else if (!paramTypes.isEmpty()) {
                    //types should be defined for all parameters or for none of them
                    response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                            "Parameter type for param" + idx + " is not defined");
                    return;
                }
                idx++;
            }

            Converter converter = conversionFactory.getConverter(type);
            ServiceRequest serviceRequest = new ServiceRequest(serviceName, methodName, converter);
            serviceRequest.setParamTypes(paramTypes);
            serviceRequest.setParamValuesString(paramValuesString);

            Object result = serviceRequest.invokeMethod();

            String converted = converter.processServiceMethodResult(result, serviceRequest.getMethodReturnType());
            writeResponse(response, converted, converter.getMimeType());
        } catch (Throwable e) {
            sendError(request, response, e);
        } finally {
            authentication.end();
        }
    }

    @RequestMapping(value = "/api/service", method = RequestMethod.POST)
    public void serviceByPost(@RequestParam(value = "s") String sessionId,
            @RequestHeader(value = "Content-Type") MimeType contentType, @RequestBody String requestContent,
            HttpServletRequest request, HttpServletResponse response) throws IOException, JSONException {

        if (!connect(sessionId, response))
            return;

        try {
            Converter converter = conversionFactory.getConverter(contentType);
            ServiceRequest serviceRequest = converter.parseServiceRequest(requestContent);

            if (!restServicePermissions.isPermitted(serviceRequest.getServiceName(),
                    serviceRequest.getMethodName())) {
                response.sendError(HttpServletResponse.SC_FORBIDDEN);
                return;
            }

            Object result = serviceRequest.invokeMethod();
            String converted = converter.processServiceMethodResult(result, serviceRequest.getMethodReturnType());
            writeResponse(response, converted, converter.getMimeType());
        } catch (RestServiceException e) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
            log.error("Error processing request: " + request.getRequestURI() + "?" + request.getQueryString(), e);
        } catch (Throwable e) {
            sendError(request, response, e);
        } finally {
            authentication.end();
        }
    }

    protected boolean connect(@RequestParam(value = "s") String sessionId, HttpServletResponse response)
            throws IOException {
        if (!authentication.begin(sessionId)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        return true;
    }

    private void writeResponse(HttpServletResponse response, String result, MimeType mimeType) throws IOException {
        response.setContentType(mimeType.toString());
        PrintWriter writer = response.getWriter();
        writer.write(result);
        writer.flush();
    }

    private void sendError(HttpServletRequest request, HttpServletResponse response, Throwable e)
            throws IOException {
        log.error("Error processing request: " + request.getRequestURI() + "?" + request.getQueryString(), e);

        Configuration configuration = AppBeans.get(Configuration.class);
        boolean isProductionMode = configuration.getConfig(RestConfig.class).getProductionMode();

        String msg;
        if (isProductionMode) {
            msg = "Internal server error";
        } else {
            Throwable t = ExceptionUtils.getRootCause(e);
            msg = t != null ? ExceptionUtils.getStackTrace(t) : ExceptionUtils.getStackTrace(e);
        }
        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg);
    }

    private Object parseQueryParameter(String paramKey, String paramValue, Map<String, String[]> queryParams) {
        String[] typeName = queryParams.get(paramKey + "_type");
        //if the type is specified
        if (typeName != null && typeName.length == 1) {
            return ParseUtils.parseByTypename(paramValue, typeName[0]);
        }
        //if the type is not specified
        else if (typeName == null) {
            return ParseUtils.tryParse(paramValue);
        }
        //if several types have been declared
        else {
            throw new IllegalStateException("Too many parameters in request");
        }
    }

    /**
     * Checks if the user have permissions to commit (create or update)
     * all of the entities.
     *
     * @param commitInstances entities to commit
     * @param newInstanceIds  ids of the new entities
     * @return true - if the user can commit all of the requested entities, false -
     * if he don't have permissions to commit at least one of the entities.
     */
    private boolean commitPermitted(Collection commitInstances, Collection newInstanceIds) {
        for (Object commitInstance : commitInstances) {
            Entity entity = (Entity) commitInstance;
            String fullId = entity.getMetaClass().getName() + "-" + entity.getId();
            if (newInstanceIds.contains(fullId)) {
                if (!createPermitted(entity.getMetaClass()))
                    return false;
            } else if (!updatePermitted(entity.getMetaClass())) {
                return false;
            }
        }
        return true;
    }

    /**
     * Checks if the user have permissions to remove all of the requested entities.
     *
     * @param removeInstances entities to remove
     * @return true - if the user can remove all of the requested entities, false -
     * if he don't have permissions to remove at least one of the entities.
     */
    private boolean removePermitted(Collection removeInstances) {
        for (Object removeInstance : removeInstances) {
            Entity next = (Entity) removeInstance;
            if (!removePermitted(next.getMetaClass()))
                return false;
        }
        return true;
    }

    private boolean readPermitted(MetaClass metaClass) {
        return entityOpPermitted(metaClass, EntityOp.READ);
    }

    private boolean createPermitted(MetaClass metaClass) {
        return entityOpPermitted(metaClass, EntityOp.CREATE);
    }

    private boolean updatePermitted(MetaClass metaClass) {
        return entityOpPermitted(metaClass, EntityOp.UPDATE);
    }

    private boolean removePermitted(MetaClass metaClass) {
        return entityOpPermitted(metaClass, EntityOp.DELETE);
    }

    private boolean entityOpPermitted(MetaClass metaClass, EntityOp entityOp) {
        return security.isEntityOpPermitted(metaClass, entityOp);
    }
}