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

com.yahoo.elide.jsonapi.JsonApi Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2023, the original author or authors.
 * Licensed under the Apache License, Version 2.0
 * See LICENSE file in project root for terms.
 */
package com.yahoo.elide.jsonapi;

import com.yahoo.elide.Elide;
import com.yahoo.elide.ElideResponse;
import com.yahoo.elide.ElideSettings;
import com.yahoo.elide.RefreshableElide;
import com.yahoo.elide.core.TransactionRegistry;
import com.yahoo.elide.core.audit.AuditLogger;
import com.yahoo.elide.core.datastore.DataStore;
import com.yahoo.elide.core.datastore.DataStoreTransaction;
import com.yahoo.elide.core.exceptions.BadRequestException;
import com.yahoo.elide.core.exceptions.HttpStatus;
import com.yahoo.elide.core.request.route.Route;
import com.yahoo.elide.core.security.User;
import com.yahoo.elide.jsonapi.extensions.JsonApiAtomicOperations;
import com.yahoo.elide.jsonapi.extensions.JsonApiAtomicOperationsRequestScope;
import com.yahoo.elide.jsonapi.extensions.JsonApiJsonPatch;
import com.yahoo.elide.jsonapi.extensions.JsonApiJsonPatchRequestScope;
import com.yahoo.elide.jsonapi.models.JsonApiDocument;
import com.yahoo.elide.jsonapi.parser.BaseVisitor;
import com.yahoo.elide.jsonapi.parser.DeleteVisitor;
import com.yahoo.elide.jsonapi.parser.GetVisitor;
import com.yahoo.elide.jsonapi.parser.JsonApiParser;
import com.yahoo.elide.jsonapi.parser.PatchVisitor;
import com.yahoo.elide.jsonapi.parser.PostVisitor;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * JSON:API.
 */
@Slf4j
public class JsonApi {
    @Getter
    private final Elide elide;

    private final ElideSettings elideSettings;
    private final JsonApiSettings jsonApiSettings;
    private final DataStore dataStore;
    private final JsonApiMapper mapper;
    private final TransactionRegistry transactionRegistry;
    private final AuditLogger auditLogger;
    private final JsonApiExceptionHandler jsonApiExceptionHandler;
    private final boolean strictQueryParameters;

    public JsonApi(RefreshableElide refreshableElide) {
        this(refreshableElide.getElide());
    }

    public JsonApi(Elide elide) {
        this.elide = elide;
        this.jsonApiSettings = elide.getSettings(JsonApiSettings.class);
        this.strictQueryParameters = this.jsonApiSettings.isStrictQueryParameters();
        this.mapper = this.jsonApiSettings.getJsonApiMapper();
        this.dataStore = this.elide.getDataStore();
        this.elideSettings = this.elide.getElideSettings();
        this.transactionRegistry = this.elide.getTransactionRegistry();
        this.auditLogger = this.elide.getAuditLogger();
        this.jsonApiExceptionHandler = this.jsonApiSettings.getJsonApiExceptionHandler();
    }

    /**
     * Handle GET.
     *
     * @param route the route
     * @param opaqueUser the opaque user
     * @param requestId the request ID
     * @return Elide response object
     */
    public ElideResponse get(Route route, User opaqueUser,
                             UUID requestId) {
        UUID requestUuid = requestId != null ? requestId : UUID.randomUUID();

        if (strictQueryParameters) {
            try {
                verifyQueryParams(route.getParameters());
            } catch (BadRequestException e) {
                JsonApiErrorContext errorContext = JsonApiErrorContext.builder().mapper(this.mapper).verbose(false)
                        .build();
                ElideResponse errorResponse = jsonApiExceptionHandler.handleException(e, errorContext);
                return toResponse(errorResponse.getStatus(), errorResponse.getBody());
            }
        }
        return handleRequest(true, opaqueUser, dataStore::beginReadTransaction, requestUuid, (tx, user) -> {
            JsonApiDocument jsonApiDoc = new JsonApiDocument();
            JsonApiRequestScope requestScope = JsonApiRequestScope.builder().route(route).dataStoreTransaction(tx)
                    .user(user).requestId(requestUuid).elideSettings(elideSettings).jsonApiDocument(jsonApiDoc)
                    .build();
            requestScope.setEntityProjection(new EntityProjectionMaker(elideSettings.getEntityDictionary(),
                    requestScope).parsePath(route.getPath()));
            BaseVisitor visitor = new GetVisitor(requestScope);
            return visit(route.getPath(), requestScope, visitor);
        });
    }

    /**
     * Handle POST.
     *
     * @param route the route
     * @param jsonApiDocument the json api document
     * @param opaqueUser the opaque user
     * @param requestId the request ID
     * @return Elide response object
     */
    public ElideResponse post(Route route, String jsonApiDocument,
                              User opaqueUser, UUID requestId) {
        UUID requestUuid = requestId != null ? requestId : UUID.randomUUID();

        return handleRequest(false, opaqueUser, dataStore::beginTransaction, requestUuid, (tx, user) -> {
            JsonApiDocument jsonApiDoc = mapper.readJsonApiDocument(jsonApiDocument);
            JsonApiRequestScope requestScope = JsonApiRequestScope.builder().route(route).dataStoreTransaction(tx)
                    .user(user).requestId(requestUuid).elideSettings(elideSettings).jsonApiDocument(jsonApiDoc)
                    .build();
            requestScope.setEntityProjection(new EntityProjectionMaker(elideSettings.getEntityDictionary(),
                    requestScope).parsePath(route.getPath()));
            BaseVisitor visitor = new PostVisitor(requestScope);
            return visit(route.getPath(), requestScope, visitor);
        });
    }

    /**
     * Handle PATCH.
     *
     * @param route the route
     * @param jsonApiDocument the json api document
     * @param opaqueUser the opaque user
     * @param requestId the request ID
     * @return Elide response object
     */
    public ElideResponse patch(Route route, String jsonApiDocument, User opaqueUser, UUID requestId) {
        UUID requestUuid = requestId != null ? requestId : UUID.randomUUID();

        String accept = getFirstHeaderValueOrEmpty(route, "accept");
        String contentType = getFirstHeaderValueOrEmpty(route, "content-type");

        Handler handler;
        if (JsonApiJsonPatch.isPatchExtension(contentType) && JsonApiJsonPatch.isPatchExtension(accept)) {
            handler = (tx, user) -> {
                JsonApiJsonPatchRequestScope requestScope = new JsonApiJsonPatchRequestScope(route, tx, user,
                        requestUuid, elideSettings);
                try {
                    Supplier> responder = JsonApiJsonPatch.processJsonPatch(dataStore,
                            route.getPath(), jsonApiDocument, requestScope);
                    return new HandlerResult(requestScope, responder);
                } catch (RuntimeException e) {
                    return new HandlerResult(requestScope, e);
                }
            };
        } else {
            handler = (tx, user) -> {
                JsonApiDocument jsonApiDoc = mapper.readJsonApiDocument(jsonApiDocument);
                JsonApiRequestScope requestScope = JsonApiRequestScope.builder().route(route).dataStoreTransaction(tx)
                        .user(user).requestId(requestUuid).elideSettings(elideSettings).jsonApiDocument(jsonApiDoc)
                        .build();
                requestScope.setEntityProjection(new EntityProjectionMaker(elideSettings.getEntityDictionary(),
                        requestScope).parsePath(route.getPath()));
                BaseVisitor visitor = new PatchVisitor(requestScope);
                return visit(route.getPath(), requestScope, visitor);
            };
        }

        return handleRequest(false, opaqueUser, dataStore::beginTransaction, requestUuid, handler);
    }

    /**
     * Handle DELETE.
     *
     * @param route the route
     * @param jsonApiDocument the json api document
     * @param opaqueUser the opaque user
     * @param requestId the request ID
     * @return Elide response object
     */
    public ElideResponse delete(Route route, String jsonApiDocument,
                                User opaqueUser, UUID requestId) {
        UUID requestUuid = requestId != null ? requestId : UUID.randomUUID();

        return handleRequest(false, opaqueUser, dataStore::beginTransaction, requestUuid, (tx, user) -> {
            JsonApiDocument jsonApiDoc = StringUtils.isEmpty(jsonApiDocument)
                    ? new JsonApiDocument()
                    : mapper.readJsonApiDocument(jsonApiDocument);
            JsonApiRequestScope requestScope = JsonApiRequestScope.builder().route(route).dataStoreTransaction(tx)
                    .user(user).requestId(requestUuid).elideSettings(elideSettings).jsonApiDocument(jsonApiDoc).build();
            requestScope.setEntityProjection(new EntityProjectionMaker(elideSettings.getEntityDictionary(),
                    requestScope).parsePath(route.getPath()));
            BaseVisitor visitor = new DeleteVisitor(requestScope);
            return visit(route.getPath(), requestScope, visitor);
        });
    }

    /**
     * Handle operations for the Atomic Operations extension.
     *
     * @param route the route
     * @param jsonApiDocument the json api document
     * @param opaqueUser the opaque user
     * @param requestId the request ID
     * @return Elide response object
     */
    public ElideResponse operations(Route route,
            String jsonApiDocument, User opaqueUser, UUID requestId) {

        UUID requestUuid = requestId != null ? requestId : UUID.randomUUID();

        String accept = getFirstHeaderValueOrEmpty(route, "accept");
        String contentType = getFirstHeaderValueOrEmpty(route, "content-type");

        Handler handler;
        if (JsonApiAtomicOperations.isAtomicOperationsExtension(contentType)
                && JsonApiAtomicOperations.isAtomicOperationsExtension(accept)) {
            handler = (tx, user) -> {
                JsonApiAtomicOperationsRequestScope requestScope = new JsonApiAtomicOperationsRequestScope(
                        route, tx, user, requestUuid, elideSettings);
                try {
                    Supplier> responder = JsonApiAtomicOperations
                            .processAtomicOperations(dataStore, route.getPath(), jsonApiDocument, requestScope);
                    return new HandlerResult(requestScope, responder);
                } catch (RuntimeException e) {
                    return new HandlerResult(requestScope, e);
                }
            };
        } else {
            return ElideResponse.status(HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE).body("Unsupported Media Type");
        }

        return handleRequest(false, opaqueUser, dataStore::beginTransaction, requestUuid, handler);
    }

    public HandlerResult visit(String path, JsonApiRequestScope requestScope, BaseVisitor visitor) {
        try {
            Supplier> responder = visitor.visit(JsonApiParser.parse(path));
            return new HandlerResult(requestScope, responder);
        } catch (RuntimeException e) {
            return new HandlerResult(requestScope, e);
        }
    }

    /**
     * Handle JSON API requests.
     *
     * @param isReadOnly if the transaction is read only
     * @param user the user object from the container
     * @param transaction a transaction supplier
     * @param requestId the Request ID
     * @param handler a function that creates the request scope and request handler
     * @param  The response type (JsonNode or JsonApiDocument)
     * @return the response
     */
    protected  ElideResponse handleRequest(boolean isReadOnly, User user,
                                          Supplier transaction, UUID requestId,
                                          Handler handler) {
        JsonApiErrorContext errorContext = JsonApiErrorContext.builder().mapper(this.mapper)
                .verbose(elideSettings.isVerboseErrors()).build();
        try (DataStoreTransaction tx = transaction.get()) {
            transactionRegistry.addRunningTransaction(requestId, tx);
            HandlerResult result = handler.handle(tx, user);
            JsonApiRequestScope requestScope = result.getRequestScope();
            Supplier> responder = result.getResponder();
            tx.preCommit(requestScope);
            requestScope.runQueuedPreSecurityTriggers();
            requestScope.getPermissionExecutor().executeCommitChecks();
            requestScope.runQueuedPreFlushTriggers();
            if (!isReadOnly) {
                requestScope.saveOrCreateObjects();
            }
            tx.flush(requestScope);

            requestScope.runQueuedPreCommitTriggers();

            ElideResponse response = buildResponse(responder.get());

            auditLogger.commit();
            tx.commit(requestScope);
            requestScope.runQueuedPostCommitTriggers();

            if (log.isTraceEnabled()) {
                requestScope.getPermissionExecutor().logCheckStats();
            }

            return response;
        } catch (Throwable e) {
            ElideResponse errorResponse = jsonApiExceptionHandler.handleException(e, errorContext);
            return toResponse(errorResponse.getStatus(), errorResponse.getBody());
        } finally {
            transactionRegistry.removeRunningTransaction(requestId);
            auditLogger.clear();
        }
    }

    protected ElideResponse toResponse(int status, Object body) {
        String result = null;
        if (body instanceof String data) {
            result = data;
        } else {
            try {
                result = body != null ? this.mapper.writeJsonApiDocument(body) : null;
            } catch (JsonProcessingException e) {
                return ElideResponse.status(HttpStatus.SC_INTERNAL_SERVER_ERROR).body(e.toString());
            }
        }
        return ElideResponse.status(status).body(result);
    }

    protected  ElideResponse buildResponse(Pair response) {
        T responseNode = response.getRight();
        Integer responseCode = response.getLeft();
        return toResponse(responseCode, responseNode);
    }

    private void verifyQueryParams(Map> queryParams) {
        String undefinedKeys = queryParams.keySet()
                        .stream()
                        .filter(JsonApi::notAValidKey)
                        .collect(Collectors.joining(", "));

        if (!undefinedKeys.isEmpty()) {
            throw new BadRequestException("Found undefined keys in request: " + undefinedKeys);
        }
    }

    private static boolean notAValidKey(String key) {
        boolean validKey = key.equals("sort")
                        || key.startsWith("filter")
                        || (key.startsWith("fields[") && key.endsWith("]"))
                        || key.startsWith("page[")
                        || key.equals(EntityProjectionMaker.INCLUDE);
        return !validKey;
    }

    private static String getFirstHeaderValueOrEmpty(Route route, String headerName) {
        return route.getHeaders().getOrDefault(headerName, Collections.emptyList()).stream().findFirst().orElse("");
    }

    /**
     * A function that sets up the request handling objects.
     *
     * @param  the request's transaction
     * @param  the request's user
     * @param  the request handling objects
     */
    @FunctionalInterface
    public interface Handler {
        HandlerResult handle(DataStoreTransaction a, User b) throws IOException;
    }

    /**
     * A wrapper to return multiple values, less verbose than Pair.
     * @param  Response type.
     */
    protected static class HandlerResult {
        protected JsonApiRequestScope requestScope;
        protected Supplier> result;
        protected RuntimeException cause;

        protected HandlerResult(JsonApiRequestScope requestScope, Supplier> result) {
            this.requestScope = requestScope;
            this.result = result;
        }

        public HandlerResult(JsonApiRequestScope requestScope, RuntimeException cause) {
            this.requestScope = requestScope;
            this.cause = cause;
        }

        public Supplier> getResponder() {
            if (cause != null) {
                throw cause;
            }
            return result;
        }

        public JsonApiRequestScope getRequestScope() {
            return requestScope;
        }
    }

    public static final String MEDIA_TYPE = "application/vnd.api+json";

    public static class JsonPatch {
        private JsonPatch() {
        }

        public static final String MEDIA_TYPE = "application/vnd.api+json; ext=jsonpatch";
    }

    public static class AtomicOperations {
        private AtomicOperations() {
        }

        public static final String MEDIA_TYPE = "application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic\"";
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy