org.fcrepo.apix.routing.impl.RoutingImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.fcrepo.apix.routing.impl.RoutingImpl.java

Source

/*
 * Licensed to DuraSpace under one or more contributor license agreements.
 * See the NOTICE file distributed with this work for additional information
 * regarding copyright ownership.
 *
 * DuraSpace licenses this file to you 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 org.fcrepo.apix.routing.impl;

import static org.fcrepo.apix.model.components.Routing.HTTP_HEADER_APIX_RESOURCE_URI;
import static org.fcrepo.apix.model.components.Routing.HTTP_HEADER_APIX_ROOT_URI;
import static org.fcrepo.apix.model.components.Routing.HTTP_HEADER_EXPOSED_SERVICE_URI;
import static org.fcrepo.apix.model.components.Routing.HTTP_HEADER_REPOSITORY_RESOURCE_PATH;
import static org.fcrepo.apix.model.components.Routing.HTTP_HEADER_REPOSITORY_RESOURCE_URI;
import static org.fcrepo.apix.model.components.Routing.HTTP_HEADER_REPOSITORY_ROOT_URI;
import static org.fcrepo.apix.routing.Util.append;
import static org.fcrepo.apix.routing.Util.segment;
import static org.fcrepo.apix.routing.impl.GenericInterceptExecution.ROUTE_INTERCEPT_INCOMING;
import static org.fcrepo.apix.routing.impl.GenericInterceptExecution.ROUTE_INTERCEPT_OUTGOING;

import java.net.URI;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.fcrepo.apix.model.Extension;
import org.fcrepo.apix.model.Extension.Scope;
import org.fcrepo.apix.model.ServiceInstance;
import org.fcrepo.apix.model.WebResource;
import org.fcrepo.apix.model.components.ResourceNotFoundException;
import org.fcrepo.apix.model.components.RoutingFactory;
import org.fcrepo.apix.model.components.ServiceDiscovery;
import org.fcrepo.apix.model.components.ServiceInstanceRegistry;
import org.fcrepo.apix.model.components.ServiceRegistry;
import org.fcrepo.apix.routing.impl.ExposedServiceUriAnalyzer.ServiceExposingBinding;

import org.apache.camel.Exchange;
import org.apache.camel.Predicate;
import org.apache.camel.Processor;
import org.apache.camel.builder.RouteBuilder;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.ClientProtocolException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Stub/placeholder Routing implementation that does nothing.
 *
 * @author apb@jhu.edu
 */
public class RoutingImpl extends RouteBuilder {

    static final Logger LOG = LoggerFactory.getLogger(RoutingImpl.class);

    private final Random random = new Random();

    private URI fcrepoBaseURI;

    public static final String EXECUTION_EXPOSE_MODALITY = "direct:execute_expose";

    public static final String PROP_MESSAGE = "CamelApixMessage";

    public static final String EXPOSING_EXTENSION = "CamelApixExposingExtension";

    public static final String SERVICE_INSTANCE_URI = "CamelApixServiceInstanceUri";

    public static final String EXTENSION_NOT_FOUND = "direct:extension_not_found";

    public static final String ROUTE_INSTANCE_NOT_FOUND = "direct:instance_not_found";

    public static final String ROUTE_INTERCEPT = "direct:intercept";

    public static final String ROUTE_TO_FEDORA = "direct:fcrepo";

    public static final String ROUTE_HTTP_ERROR = "direct:http_error";

    public static final String BINDING = "CamelApixServiceExposureBinding";

    private ExposedServiceUriAnalyzer analyzer;

    private ServiceDiscovery serviceDiscovery;

    private ServiceRegistry serviceRegistry;

    private RoutingFactory routing;

    private String interceptPath;

    private String proxyPath;

    /**
     * Set Fedora's baseURI.
     *
     * @param uri the base URI.
     */
    public void setFcrepoBaseURI(final URI uri) {
        this.fcrepoBaseURI = uri;
    }

    /**
     * Set the discovery component for generating a service doc.
     *
     * @param disc service document generator.
     */
    public void setServiceDiscovery(final ServiceDiscovery disc) {
        this.serviceDiscovery = disc;
    }

    /**
     * Set the service registry.
     *
     * @param registry The registry.
     */
    public void setServiceRegistry(final ServiceRegistry registry) {
        this.serviceRegistry = registry;
    }

    /**
     * Set the URI analyzer.
     *
     * @param analyzer Analyzer impl.
     */
    public void setExposedServiceURIAnalyzer(final ExposedServiceUriAnalyzer analyzer) {
        this.analyzer = analyzer;
    }

    /**
     * Set the routing.
     *
     * @param routing routing.
     */
    public void setRouting(final RoutingFactory routing) {
        this.routing = routing;
    }

    /**
     * Set the intercept path.
     *
     * @param path The intercept path.
     */
    public void setInterceptPath(final String path) {
        this.interceptPath = path;
    }

    /**
     * Set the proxy path.
     *
     * @param path The intercept path.
     */
    public void setProxyPath(final String path) {
        this.proxyPath = path;
    }

    private String interceptBase;

    @Override
    public void configure() throws Exception {

        interceptBase = segment(interceptPath.replaceFirst("^" + proxyPath, ""));

        // It would be nice to use the rest DSL to do the service doc, if that is at all possible

        from("jetty:http://{{apix.listen.host}}:{{apix.port}}/{{apix.discoveryPath}}?matchOnUriPrefix=true")
                .routeId("service-doc-endpoint").process(WRITE_SERVICE_DOC);

        from("jetty:http://{{apix.listen.host}}:{{apix.port}}/{{apix.exposePath}}" + "?matchOnUriPrefix=true"
                + "&bridgeEndpoint=true" + "&disableStreamCache=true").routeId("endpoint-expose")
                        .routeDescription("Endpoint for exposed service mediation").process(ANALYZE_URI).choice()
                        .when(header(EXPOSING_EXTENSION).isNull()).to(EXTENSION_NOT_FOUND).otherwise()
                        .to(EXECUTION_EXPOSE_MODALITY);

        from("jetty:http://{{apix.listen.host}}:{{apix.port}}/{{apix.proxyPath}}?" + "matchOnUriPrefix=true"
                + "&bridgeEndpoint=true" + "&disableStreamCache=true").routeId("endpoint-proxy")
                        .routeDescription("Endpoint for proxy to Fedora")

                        .choice().when(IN_INTERCEPT_PATH).to(ROUTE_INTERCEPT).otherwise().doTry()
                        .to("{{fcrepo.proxyURI}}" + "?bridgeEndpoint=true" + "&disableStreamCache=true"
                                + "&throwExceptionOnFailure=false" + "&preserveHostHeader=true")
                        .doCatch(ClientProtocolException.class).to(ROUTE_HTTP_ERROR);

        from("direct:http_error").routeId("http-error")
                .process(e -> LOG.warn("HTTP Error proxying to {}", e.getIn().getHeader(Exchange.HTTP_PATH)))
                .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(500))
                .setBody(simple("HTTP error proxying to resource ${in.header.CamelHttpPath}"));

        from(ROUTE_INTERCEPT).routeId("execute-intercept").routeDescription("Endpoint for intercept to Fedora")
                .to(ROUTE_INTERCEPT_INCOMING).choice()
                .when(simple("${in.header.CamelhttpResponseCode} range '200..299'")).to(ROUTE_TO_FEDORA);

        from(ROUTE_TO_FEDORA).routeId("to-fedora").doTry()
                .to("{{fcrepo.proxyURI}}" + "?bridgeEndpoint=true" + "&throwExceptionOnFailure=false"
                        + "&disableStreamCache=true" + "&preserveHostHeader=true")
                .doCatch(ClientProtocolException.class).to(ROUTE_HTTP_ERROR).end().process(ADD_SERVICE_HEADER)
                .choice().when(simple("${in.header.CamelhttpResponseCode} range '200..299'"))
                .to(ROUTE_INTERCEPT_OUTGOING).end();

        from(EXTENSION_NOT_FOUND).id("not-found-extension").routeDescription("Extension not found")
                .process(e -> e.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 404));

        from(ROUTE_INSTANCE_NOT_FOUND).id("not-found-instance").routeDescription("Service instance not found")
                .removeHeaders("*").setHeader(Exchange.HTTP_RESPONSE_CODE, constant(404))
                .setBody(simple("${exchangeProperty." + PROP_MESSAGE + "}"));

        from(EXECUTION_EXPOSE_MODALITY).routeId("apix-proxy-service-endpoint")
                .routeDescription("Proxies an exposed service to a service instance")
                .process(SELECT_SERVICE_INSTANCE).setHeader(Exchange.HTTP_PATH)
                .simple("${in.header." + BINDING + ".additionalPath}").setHeader(Exchange.HTTP_URI)
                .header(SERVICE_INSTANCE_URI).choice().when(header(SERVICE_INSTANCE_URI).isNull())
                .to(ROUTE_INSTANCE_NOT_FOUND).otherwise().to("http://localhost" + "?preserveHostHeader=true"
                        + "&disableStreamCache=true" + "&throwExceptionOnFailure=false");

    }

    final Predicate IN_INTERCEPT_PATH = ex -> {
        return segment(ex.getIn().getHeader(Exchange.HTTP_PATH, String.class)).startsWith(interceptBase);
    };

    final Processor ANALYZE_URI = (ex -> {
        final ServiceExposingBinding binding = analyzer
                .match(URI.create(ex.getIn().getHeader(Exchange.HTTP_URL, String.class)));

        if (binding == null) {
            LOG.info("No binding for {}", ex.getIn().getHeader(Exchange.HTTP_URL));
            return;
        }

        ex.getIn().setHeader(BINDING, binding);
        ex.getIn().setHeader(EXPOSING_EXTENSION, binding.extension);
        ex.getIn().setHeader(HTTP_HEADER_EXPOSED_SERVICE_URI, binding.getExposedURI());

        // resource URI is only conveyed for resource-scope services
        if (Scope.RESOURCE.equals(binding.extension.exposed().scope())) {

            ex.getIn().setHeader(HTTP_HEADER_APIX_RESOURCE_URI,
                    routing.of(requestUri(ex)).interceptUriFor(binding.repositoryResourceURI));
            ex.getIn().setHeader(HTTP_HEADER_REPOSITORY_RESOURCE_URI, binding.repositoryResourceURI);
            ex.getIn().setHeader(HTTP_HEADER_REPOSITORY_RESOURCE_PATH, "/" + binding.resourcePath);
        } else {
            ex.getIn().setHeader(HTTP_HEADER_REPOSITORY_ROOT_URI, fcrepoBaseURI);
            ex.getIn().setHeader(HTTP_HEADER_APIX_ROOT_URI,
                    routing.of(requestUri(ex)).interceptUriFor(fcrepoBaseURI));
        }
    });

    final Processor WRITE_SERVICE_DOC = (ex -> {
        final String accept = ex.getIn().getHeader("Accept", "text/turtle", String.class).split("\\s*,\\s*")[0];
        final URI resource = fcrepoResourceFromPath(ex.getIn().getHeader(Exchange.HTTP_PATH, String.class));

        try (WebResource serviceDoc = serviceDiscovery.getServiceDocumentFor(resource, routing.of(requestUri(ex)),
                accept)) {
            ex.getOut().setBody(IOUtils.toByteArray(serviceDoc.representation()));
            ex.getOut().setHeader(Exchange.CONTENT_TYPE, serviceDoc.contentType());
        }
    });

    final Processor SELECT_SERVICE_INSTANCE = (ex -> {
        final Extension extension = ex.getIn().getHeader(EXPOSING_EXTENSION, Extension.class);

        final Set<URI> services = new HashSet<>(extension.exposed().consumed());
        services.add(extension.exposed().exposedService());

        final URI consumedServiceURI = exactlyOne(services,
                "Exposed services must have exactly one consumed service in extension: " + extension.uri());

        try {
            final ServiceInstanceRegistry instanceRegistry = serviceRegistry
                    .instancesOf(serviceRegistry.getService(consumedServiceURI));

            if (instanceRegistry == null) {
                throw new ResourceNotFoundException("No instance registry for service " + consumedServiceURI);
            }

            final ServiceInstance instance = oneOf(instanceRegistry.instances(),
                    "There must be at least one service instance for " + consumedServiceURI);

            ex.getIn().setHeader(SERVICE_INSTANCE_URI, oneOf(instance.endpoints(),
                    "There must be at least one endpoint for instances of " + consumedServiceURI));
        } catch (final ResourceNotFoundException e) {
            LOG.warn("No instances of service {}; {}", consumedServiceURI, e.getMessage());
            ex.setProperty(PROP_MESSAGE, e.getMessage());
        }

    });

    @SuppressWarnings("unchecked")
    final Processor ADD_SERVICE_HEADER = (ex -> {

        // Get all link headers, accounting for the fact that there may be zero, one, or multiple
        // (in the message, that means null, string, or list)
        final Set<String> rawLinkHeaders = new HashSet<>();

        final Object linkHeader = ex.getIn().getHeader("Link");

        if (linkHeader instanceof Collection) {
            rawLinkHeaders.addAll((Collection<String>) linkHeader);
        } else if (linkHeader instanceof String) {
            rawLinkHeaders.add((String) linkHeader);
        }

        rawLinkHeaders.add(String.format("<%s>; rel=\"service\"", routing.of(requestUri(ex))
                .serviceDocFor(fcrepoResourceFromPath(ex.getIn().getHeader(Exchange.HTTP_PATH, String.class)))));

        ex.getIn().setHeader("Link", rawLinkHeaders);
    });

    // Converts an http path to a repository resource path
    private URI fcrepoResourceFromPath(final String proxied) {
        final String resourcePath = segment(proxied.replaceFirst(interceptBase, ""));
        if (resourcePath.equals("")) {
            return URI.create(segment(fcrepoBaseURI.toString()) + "/");
        } else {
            return append(fcrepoBaseURI, resourcePath);
        }
    }

    private static URI requestUri(final Exchange ex) {
        return URI.create(ex.getIn().getHeader(Exchange.HTTP_URL, String.class));
    }

    private static <T> T exactlyOne(final Collection<T> of, final String errMsg) {
        if (of.size() != 1) {
            throw new ResourceNotFoundException(errMsg);
        }

        return of.iterator().next();
    }

    private <T> T oneOf(final List<T> of, final String errMsg) {
        if (of.isEmpty()) {
            throw new ResourceNotFoundException(errMsg);
        }

        return of.get(random.nextInt(of.size()));
    }
}