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

org.swisspush.gateleen.delta.DeltaHandler Maven / Gradle / Ivy

Go to download

Middleware library based on Vert.x to build advanced JSON/REST communication servers

The newest version!
package org.swisspush.gateleen.delta;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.redis.RedisClient;
import org.slf4j.Logger;
import org.swisspush.gateleen.core.http.RequestLoggerFactory;
import org.swisspush.gateleen.core.util.ExpansionDeltaUtil;
import org.swisspush.gateleen.core.util.ExpansionDeltaUtil.CollectionResourceContainer;
import org.swisspush.gateleen.core.util.ExpansionDeltaUtil.SlashHandling;
import org.swisspush.gateleen.core.util.ResourceCollectionException;
import org.swisspush.gateleen.core.util.StatusCode;
import org.swisspush.gateleen.core.util.StringUtils;
import org.swisspush.gateleen.routing.Router;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * @author https://github.com/mcweba [Marc-Andre Weber]
 */
public class DeltaHandler {

    private static final String DELTA_PARAM = "delta";
    private static final String DELTA_HEADER = "x-delta";
    private static final String IF_NONE_MATCH_HEADER = "if-none-match";
    // used as marker header to know that we should let the request continue to the router
    private static final String DELTA_BACKEND_HEADER = "x-delta-backend";
    private static final String EXPIRE_AFTER_HEADER = "X-Expire-After";
    private static final long DEFAULT_EXPIRE = 1728000; // 20 days
    private static final String SLASH = "/";
    private static final int TIMEOUT = 120000;

    private static final String SEQUENCE_KEY = "delta:sequence";
    private static final String RESOURCE_KEY_PREFIX = "delta:resources";
    private static final String ETAG_KEY_PREFIX = "delta:etags";

    private HttpClient httpClient;
    private RedisClient redisClient;

    public DeltaHandler(RedisClient redisClient, HttpClient httpClient) {
        this.redisClient = redisClient;
        this.httpClient = httpClient;
    }

    public boolean isDeltaRequest(HttpServerRequest request) {
        return isDeltaGETRequest(request) || isDeltaPUTRequest(request);
    }

    private boolean isDeltaPUTRequest(HttpServerRequest request) {
        if (HttpMethod.PUT == request.method() && request.headers().contains(DELTA_HEADER)) {
            return "auto".equalsIgnoreCase(request.headers().get(DELTA_HEADER));
        }
        return false;
    }

    private boolean isDeltaGETRequest(HttpServerRequest request) {
        if(HttpMethod.GET == request.method() &&
           request.params().contains(DELTA_PARAM) && 
           !request.headers().contains(DELTA_BACKEND_HEADER)) {
        	return true;
        }
        // remove the delta backend header, its only a marker
        if(request.headers().contains(DELTA_BACKEND_HEADER)) {
        	request.headers().remove(DELTA_BACKEND_HEADER);
        }
        return false;
    }

    public void handle(final HttpServerRequest request, Router router) {
        Logger log = RequestLoggerFactory.getLogger(DeltaHandler.class, request);
        if (isDeltaPUTRequest(request)) {
            handleResourcePUT(request, router, log);
        }
        if (isDeltaGETRequest(request)) {
            String updateId = extractStringDeltaParameter(request, log);
            if (updateId != null) {
                handleCollectionGET(request, updateId, log);
            }
        }
    }

    private void handleResourcePUT(final HttpServerRequest request, final Router router, final Logger log) {
        request.pause(); // pause the request to avoid problems with starting another async request (storage)
        handleDeltaEtag(request, log, updateDelta -> {
            if(updateDelta){
                // increment and get update-id
                redisClient.incr(SEQUENCE_KEY, reply -> {
                    if(reply.failed()){
                        log.error("incr command for redisKey " + SEQUENCE_KEY + " failed with cause: " + logCause(reply));
                        handleError(request, "error incrementing/accessing sequence for update-id");
                        return;
                    }

                    final String resourceKey = getResourceKey(request.path(), false);
                    long expireAfter = getExpireAfterValue(request, log);
                    long updateId = reply.result();

                    // save to storage
                    redisClient.setex(resourceKey, expireAfter, String.valueOf(updateId), event -> {
                        if(event.failed()){
                            log.error("setex command for redisKey " + resourceKey + " failed with cause: " + logCause(event));
                            handleError(request, "error saving delta information");
                            request.resume();
                        } else {
                            request.resume();
                            router.route(request);
                        }
                    });
                });

            } else {
                log.debug("skip updating delta, resume request");
                request.resume();
                router.route(request);
            }
        });
    }

    private void handleDeltaEtag(final HttpServerRequest request, final Logger log, final Handler callback){
        /*
         * When no Etag is provided we just do the delta update
         */
        if(!request.headers().contains(IF_NONE_MATCH_HEADER)){
            callback.handle(Boolean.TRUE);
            return;
        }

        /*
         * Loading Delta-Etag from storage to compare with header
         */
        final String requestEtag = request.headers().get(IF_NONE_MATCH_HEADER);
        final String etagResourceKey = getResourceKey(request.path(), true);
        redisClient.get(etagResourceKey, event -> {
            if(event.failed()){
                log.error("get command for redisKey " + etagResourceKey + " failed with cause: " + logCause(event));
                callback.handle(Boolean.TRUE);
                return;
            }

            String etagFromStorage = event.result();
            if(StringUtils.isEmpty(etagFromStorage)){
                    /*
                     * No Etag entry found. Store it and then do the delta update
                     */
                saveOrUpdateDeltaEtag(etagResourceKey, request, log, aBoolean -> callback.handle(Boolean.TRUE));
            } else {
                    /*
                     * If etags match, no delta update has to be made.
                     * If not, store/update the etag and then update the delta
                     */
                if(etagFromStorage.equals(requestEtag)){
                    callback.handle(Boolean.FALSE);
                } else {
                    saveOrUpdateDeltaEtag(etagResourceKey, request, log, aBoolean -> callback.handle(Boolean.TRUE));
                }
            }
        });
    }

    private void saveOrUpdateDeltaEtag(final String etagResourceKey, final HttpServerRequest request, final Logger log, final Handler updateCallback){
        final String requestEtag = request.headers().get(IF_NONE_MATCH_HEADER);
        long expireAfter = getExpireAfterValue(request, log);
        redisClient.setex(etagResourceKey, expireAfter, requestEtag, event -> {
            if(event.failed()){
                log.error("setex command for redisKey " + etagResourceKey + " failed with cause: " + logCause(event));
            }
            updateCallback.handle(Boolean.TRUE);
        });
    }

    private String extractStringDeltaParameter(HttpServerRequest request, Logger log) {
	    String updateIdValue = request.params().get(DELTA_PARAM);
	    if(updateIdValue == null) {
            respondInvalidDeltaParameter(updateIdValue, request, log);
	        return null;
	    } else {
	    	return updateIdValue;
	    }
    }
    
    private Integer extractNumberDeltaParameter(String deltaStringId, HttpServerRequest request, Logger log) {
        String updateIdValue = null;
        try {
            return Integer.parseInt(deltaStringId);
        } catch (Exception exception) {
            respondInvalidDeltaParameter(deltaStringId, request, log);
            return null;
        }
    }

    private void respondInvalidDeltaParameter(String deltaStringId, HttpServerRequest request, Logger log){
        request.response().setStatusCode(StatusCode.BAD_REQUEST.getStatusCode());
        request.response().setStatusMessage("Invalid delta parameter");
        request.response().end(request.response().getStatusMessage());
        log.error("Bad Request: " + request.response().getStatusMessage() + " '" + deltaStringId + "'");
    }

    private DeltaResourcesContainer getDeltaResourceNames(List subResourceNames, JsonArray storageUpdateIds, int updateId) {
        List deltaResourceNames = new ArrayList<>();
        int maxUpdateId = 0;

        for (int i = 0; i < storageUpdateIds.size(); i++) {
            try {
                Integer storedUpdateId = Integer.parseInt(storageUpdateIds.getString(i));
                if (storedUpdateId > updateId) {
                    deltaResourceNames.add(subResourceNames.get(i));
                }
                if (storedUpdateId > maxUpdateId) {
                    maxUpdateId = storedUpdateId;
                }
            } catch (NumberFormatException ex) {
                // No error. Just a resource with no update-in in storage
                deltaResourceNames.add(subResourceNames.get(i));
            }
        }

        return new DeltaResourcesContainer(maxUpdateId, deltaResourceNames);
    }

    private void handleCollectionGET(final HttpServerRequest request, final String updateId, final Logger log) {
        request.pause();
        
        final String targetUri = ExpansionDeltaUtil.constructRequestUri(request.path(), request.params(), null, null, SlashHandling.KEEP);
        log.debug("constructed uri for request: " + targetUri);

        final HttpClientRequest cReq = httpClient.request(HttpMethod.GET, targetUri, cRes -> {
            request.response().setStatusCode(cRes.statusCode());
            request.response().setStatusMessage(cRes.statusMessage());
            request.response().headers().addAll(cRes.headers());
            request.response().headers().remove("Content-Length");
            request.response().setChunked(true);
            if(cRes.headers().contains(DELTA_HEADER)) {
                cRes.handler(data -> request.response().write(data));
                cRes.endHandler(v -> request.response().end());
            } else {
                cRes.bodyHandler(data -> {
                    try {
                        Set originalParams = null;
                        if(request.params() != null){
                            originalParams = request.params().names();
                        }
                        final CollectionResourceContainer dataContainer = ExpansionDeltaUtil.verifyCollectionResponse(request, data, originalParams);
                        final List subResourceNames = dataContainer.getResourceNames();
                        final List deltaResourceKeys = buildDeltaResourceKeys(request.path(), subResourceNames);

                        final int updateIdNumber = extractNumberDeltaParameter(updateId, request, log);

                        if(log.isTraceEnabled())  {
                            log.trace("DeltaHandler: deltaResourceKeys for targetUri ("+targetUri+"): " + deltaResourceKeys.toString());
                        }

                        if(deltaResourceKeys.size() > 0) {
                            if(log.isTraceEnabled())  {
                                log.trace("DeltaHandler: targetUri ("+targetUri+") using mget command.");
                            }

                            // read update-ids
                            redisClient.mgetMany(deltaResourceKeys, event -> {
                                if(event.failed()){
                                    log.error("mget command failed with cuase: " + logCause(event));
                                    handleError(request, "error reading delta information");
                                    return;
                                }
                                JsonArray mgetValues = event.result();
                                DeltaResourcesContainer deltaResourcesContainer = getDeltaResourceNames(subResourceNames, mgetValues, updateIdNumber);

                                JsonObject result = buildResultJsonObject(deltaResourcesContainer.getResourceNames(), dataContainer.getCollectionName());
                                request.response().putHeader(DELTA_HEADER, "" + deltaResourcesContainer.getMaxUpdateId());
                                request.response().end(result.toString());
                            });

                        } else {
                            if(log.isTraceEnabled())  {
                                log.trace("DeltaHandler: targetUri ("+targetUri+") NOT using database");
                            }
                            request.response().putHeader(DELTA_HEADER, "" + updateIdNumber);
                            request.response().end(data);
                        }
                    } catch (ResourceCollectionException exception) {
                        request.response().setStatusCode(exception.getStatusCode().getStatusCode());
                        request.response().setStatusMessage(exception.getStatusCode().getStatusMessage());
                        request.response().end(exception.getMessage());
                    }
                });
            }
            cRes.exceptionHandler(ExpansionDeltaUtil.createResponseExceptionHandler(request, targetUri, DeltaHandler.class));
        });
        cReq.setTimeout(TIMEOUT);
        cReq.headers().setAll(request.headers());
        // add a marker header to signalize, that in the next loop of the mainverticle we should pass the deltahandler
        cReq.headers().set(DELTA_BACKEND_HEADER, "");
        cReq.headers().set("Accept", "application/json");
        cReq.setChunked(true);
        request.handler(cReq::write);
        request.endHandler(v -> {
            cReq.end();
            log.debug("Request done. Request : " + cReq);
        });
        cReq.exceptionHandler(ExpansionDeltaUtil.createRequestExceptionHandler(request, targetUri, DeltaHandler.class));
        request.resume();
    }

    private List buildDeltaResourceKeys(String requestPath, List subResourceNames) {
        List storageResourceKeys = new ArrayList<>();
        String resourceKeyPrefix = getResourceKey(requestPath, false);
        for (String entry : subResourceNames) {
            storageResourceKeys.add(resourceKeyPrefix + ":" + entry);
        }
        return storageResourceKeys;
    }

    private JsonObject buildResultJsonObject(List subResourceNames, String collectionName) {
        JsonArray arr = new JsonArray();
        subResourceNames.forEach(arr::add);
        JsonObject result = new JsonObject();
        result.put(collectionName, arr);
        return result;
    }

    private void handleError(HttpServerRequest request, String errorMessage) {
        request.response().setStatusCode(StatusCode.INTERNAL_SERVER_ERROR.getStatusCode());
        request.response().setStatusMessage(StatusCode.INTERNAL_SERVER_ERROR.getStatusMessage());
        request.response().end(errorMessage);
    }

    private String getResourceKey(String path, boolean useEtagPrefix) {
        List pathSegments = Lists.newArrayList(Splitter.on(SLASH).omitEmptyStrings().split(path));
        if(useEtagPrefix){
            pathSegments.add(0, ETAG_KEY_PREFIX);
        } else {
            pathSegments.add(0, RESOURCE_KEY_PREFIX);
        }
        return Joiner.on(":").skipNulls().join(pathSegments);
    }

    private long getExpireAfterValue(HttpServerRequest request, Logger log) {
        long value = DEFAULT_EXPIRE;
        MultiMap requestHeaders = request.headers();
        String expireAfterHeaderValue = requestHeaders.get(EXPIRE_AFTER_HEADER);
        if (expireAfterHeaderValue == null) {
            log.debug("Setting Expire-After value to a default of " + DEFAULT_EXPIRE + " seconds since header " + EXPIRE_AFTER_HEADER + " not defined");
            return value;
        }

        try {
            value = Long.parseLong(expireAfterHeaderValue);

            // redis returns an error if setex is called with negativ values
            if(value < 0) {
                log.warn("Setting Expire-After value to a default of " + DEFAULT_EXPIRE + ", since defined value for header " + EXPIRE_AFTER_HEADER + " is a negative number: " + expireAfterHeaderValue);
                value = DEFAULT_EXPIRE;
            } else {
                log.debug("Setting Expire-After value to " + value + " seconds as defined in header " + EXPIRE_AFTER_HEADER);
            }

        } catch (Exception e) {
            log.warn("Setting Expire-After value to a default of " + DEFAULT_EXPIRE + ", since defined value for header " + EXPIRE_AFTER_HEADER + " is not a number: " + expireAfterHeaderValue);
        }

        return value;
    }

    private class DeltaResourcesContainer {
        private final int maxUpdateId;
        private final List resourceNames;

        public DeltaResourcesContainer(int maxUpdateId, List resourceNames) {
            this.maxUpdateId = maxUpdateId;
            this.resourceNames = resourceNames;
        }

        public int getMaxUpdateId() {
            return maxUpdateId;
        }

        public List getResourceNames() {
            return resourceNames;
        }
    }

    private String logCause(AsyncResult result){
        if(result.cause() != null){
            return result.cause().getMessage();
        }
        return null;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy