ResourceApiVersionRouteMatcher.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2015-2016 ForgeRock AS.
 */

package org.forgerock.http.routing;

import java.util.Objects;

import org.forgerock.http.ApiProducer;
import org.forgerock.services.context.Context;
import org.forgerock.services.routing.IncomparableRouteMatchException;
import org.forgerock.services.routing.RouteMatch;
import org.forgerock.services.routing.RouteMatcher;

/**
 * A {@link RouteMatcher} which routes requests using the resource API version
 * matching against the request's {@literal Accept-API-Version} header.
 * Examples of valid versions include:
 *
 * <pre>
 * 1
 * 1.0
 * 2.5
 * 123.123
 * </pre>
 *
 * Routes may be added and removed from a router as follows:
 *
 * <pre>
 * Handler users = ...;
 * Router router = new Router();
 * RouteMatcher routeOne = RouteMatchers.requestResourceApiVersionMatcher(Version.version(1));
 * RouteMatcher routeTwo = RouteMatchers.requestResourceApiVersionMatcher(Version.version(2, 5));
 * router.addRoute(routeOne, users);
 * router.addRoute(routeTwo, users);
 *
 * // Deregister a route.
 * router.removeRoute(routeOne, routeTwo);
 * </pre>
 *
 * <p>Default routing behaviour, for when no {@literal Accept-API-Version}
 * header is set on the request, can be selected by specifying a
 * {@link DefaultVersionBehaviour} to the {@link ResourceApiVersionBehaviourManager}
 * instance.
 *
 * <pre>
 * ResourceApiVersionBehaviourManager behaviourManager = RouteMatchers.newResourceApiVersionBehaviourManager();
 * behaviourManager.setDefaultVersionBehaviour(DefaultVersionBehaviour.OLDEST);
 * Filter apiVersionFilter = RouteMatchers.resourceApiVersionContextFilter(behaviourManager);
 * Handler handler = Handlers.chainOf(router, apiVersionFilter);
 * </pre>
 * </p>
 */
class ResourceApiVersionRouteMatcher extends RouteMatcher<Version> {

    private final Version routeVersion;

    /**
     * Creates a new API Version route matcher which will match the given
     * version.
     *
     * @param routeVersion The API version of the resource route.
     */
    ResourceApiVersionRouteMatcher(Version routeVersion) {
        this.routeVersion = routeVersion;
    }

    @Override
    public RouteMatch evaluate(Context context, Version requestedVersion) {
        //TODO should this blow up if ApiVersionRouterContext not present, as that means the filter is not in the route?
        DefaultVersionBehaviour behaviour = getRoutingBehaviour(context);
        if (requestedVersion == null && DefaultVersionBehaviour.NONE.equals(behaviour)) {
            return null;
        } else if (requestedVersion == null) {
            return new ApiVersionRouteMatch(routeVersion, behaviour);
        } else if (routeVersion.isCompatibleWith(requestedVersion)) {
            return new ApiVersionRouteMatch(routeVersion, DefaultVersionBehaviour.NONE);
        } else {
            return null;
        }
    }

    private DefaultVersionBehaviour getRoutingBehaviour(Context context) {
        DefaultVersionBehaviour behaviour = null;
        if (context.containsContext(ApiVersionRouterContext.class)) {
            behaviour = context.asContext(ApiVersionRouterContext.class).getDefaultVersionBehaviour();
        }
        if (behaviour == null) {
            behaviour = DefaultVersionBehaviour.LATEST;
        }
        return behaviour;
    }


    @Override
    public String toString() {
        return routeVersion.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ResourceApiVersionRouteMatcher that = (ResourceApiVersionRouteMatcher) o;
        return Objects.equals(routeVersion, that.routeVersion);
    }

    @Override
    public String idFragment() {
        return ":" + routeVersion.toString();
    }

    @Override
    public <T> T transformApi(T descriptor, ApiProducer<T> producer) {
        return descriptor != null ? producer.withVersion(descriptor, routeVersion) : null;
    }

    @Override
    public int hashCode() {
        return Objects.hash(routeVersion);
    }

    private static final class ApiVersionRouteMatch implements RouteMatch {

        private final Version resourceVersion;
        private final DefaultVersionBehaviour behaviour;

        private ApiVersionRouteMatch(Version resourceVersion, DefaultVersionBehaviour behaviour) {
            this.resourceVersion = resourceVersion;
            this.behaviour = behaviour;
        }

        @Override
        public boolean isBetterMatchThan(RouteMatch routeMatch) throws IncomparableRouteMatchException {
            if (routeMatch == null) {
                return true;
            } else if (!(routeMatch instanceof ApiVersionRouteMatch)) {
                throw new IncomparableRouteMatchException(this, routeMatch);
            }

            ApiVersionRouteMatch result = (ApiVersionRouteMatch) routeMatch;
            switch (behaviour) {
            case OLDEST:
                return resourceVersion.compareTo(result.resourceVersion) <= 0;
            case LATEST:
            default:
                return resourceVersion.compareTo(result.resourceVersion) >= 0;
            }
        }

        @Override
        public Context decorateContext(Context context) {
            ApiVersionRouterContext apiVersionRouterContext;
            if (!context.containsContext(ApiVersionRouterContext.class)) {
                apiVersionRouterContext = new ApiVersionRouterContext(context);
                context = apiVersionRouterContext;
            } else {
                apiVersionRouterContext = context.asContext(ApiVersionRouterContext.class);
            }
            apiVersionRouterContext.setResourceVersion(resourceVersion);
            return context;
        }

        @Override
        public String toString() {
            return "version=" + resourceVersion + ", behaviour=" + behaviour;
        }
    }
}