001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006package org.fcrepo.camel.reindexing;
007
008import org.apache.camel.ExchangePattern;
009import org.apache.camel.LoggingLevel;
010import org.apache.camel.builder.RouteBuilder;
011import org.fcrepo.camel.common.processor.DockerRunningProcessor;
012import org.fcrepo.camel.service.FcrepoCamelConfig;
013import org.slf4j.Logger;
014import org.springframework.beans.factory.annotation.Autowired;
015
016import static java.net.InetAddress.getLocalHost;
017import static org.apache.camel.Exchange.CONTENT_TYPE;
018import static org.apache.camel.Exchange.HTTP_METHOD;
019import static org.apache.camel.Exchange.HTTP_RESPONSE_CODE;
020import static org.apache.camel.LoggingLevel.INFO;
021import static org.fcrepo.camel.FcrepoHeaders.FCREPO_BASE_URL;
022import static org.fcrepo.camel.FcrepoHeaders.FCREPO_URI;
023import static org.fcrepo.camel.reindexing.ReindexingHeaders.REINDEXING_HOST;
024import static org.fcrepo.camel.reindexing.ReindexingHeaders.REINDEXING_PORT;
025import static org.fcrepo.camel.reindexing.ReindexingHeaders.REINDEXING_PREFIX;
026import static org.fcrepo.camel.reindexing.ReindexingHeaders.REINDEXING_RECIPIENTS;
027import static org.fcrepo.client.HttpMethods.GET;
028import static org.slf4j.LoggerFactory.getLogger;
029
030/**
031 * A content router for handling JMS events.
032 *
033 * @author Aaron Coburn
034 */
035public class ReindexingRouter extends RouteBuilder {
036
037    private static final Logger LOGGER = getLogger(ReindexingRouter.class);
038    private static final int BAD_REQUEST = 400;
039    private static final String LDP_CONTAINS = "<http://www.w3.org/ns/ldp#contains>";
040
041    @Autowired
042    private FcrepoReindexingConfig config;
043
044    @Autowired
045    private FcrepoCamelConfig fcrepoCamelConfig;
046
047    /**
048     * Configure the message route workflow.
049     */
050    public void configure() throws Exception {
051        final String host = config.getRestHost();
052        final String hostname = host.startsWith("http") ? host : "http://" + host;
053        final int port = config.getRestPort();
054        /**
055         * A generic error handler (specific to this RouteBuilder)
056         */
057        onException(Exception.class)
058                .maximumRedeliveries(config.getMaxRedeliveries())
059            .log("Index Routing Error: ${routeId}");
060
061        /**
062         * Expose a RESTful endpoint for re-indexing
063         */
064        from("jetty:" + hostname + ":" + port + config.getRestPrefix() +
065                "?matchOnUriPrefix=true&httpMethodRestrict=GET,POST")
066                .routeId("FcrepoReindexingRest")
067                .routeDescription("Expose the reindexing endpoint over HTTP")
068                .setHeader(FCREPO_URI).simple(config.getFcrepoBaseUrl() + "${headers.CamelHttpPath}")
069            .choice()
070                .when(header(HTTP_METHOD).isEqualTo("GET")).to("direct:usage")
071                .otherwise().to("direct:reindex");
072
073        from("direct:usage").routeId("FcrepoReindexingUsage")
074                .setHeader(REINDEXING_PREFIX).simple(config.getRestPrefix())
075                .setHeader(REINDEXING_PORT).simple(String.valueOf(port))
076                .setHeader(FCREPO_BASE_URL).simple(fcrepoCamelConfig.getFcrepoBaseUrl())
077                .process(new DockerRunningProcessor())
078            .process(exchange -> {
079                exchange.getIn().setHeader(REINDEXING_HOST, getLocalHost().getHostName());
080            })
081            .to("mustache:org/fcrepo/camel/reindexing/usage.mustache");
082
083        /**
084         * A Re-indexing endpoint, setting where in the fcrepo hierarchy
085         * a re-indexing operation should begin.
086         */
087        from("direct:reindex").routeId("FcrepoReindexingReindex")
088                .process(new RestProcessor())
089                .removeHeaders("CamelHttp*")
090                .removeHeader("JMSCorrelationID")
091                .setBody(constant(null))
092                .choice()
093                .when(header(HTTP_RESPONSE_CODE).isGreaterThanOrEqualTo(BAD_REQUEST))
094                .endChoice()
095                .when(header(REINDEXING_RECIPIENTS).isEqualTo(""))
096                .transform().simple("No endpoints configured for indexing")
097                .endChoice()
098                .otherwise()
099                .log(INFO, LOGGER, "Initial indexing path: ${headers[CamelFcrepoUri]}")
100                .to(ExchangePattern.InOnly, config.getReindexingStream() + "?disableTimeToLive=true")
101                    .setHeader(CONTENT_TYPE).constant("text/plain")
102                    .transform().simple("Indexing started at ${headers[CamelFcrepoUri]}");
103
104        /**
105         *  A route that traverses through a fedora hierarchy
106         *  indexing nodes, as appropriate.
107         */
108        from(config.getReindexingStream() + "?asyncConsumer=true").routeId("FcrepoReindexingTraverse")
109                .to(ExchangePattern.InOnly, "direct:recipients")
110                .log(LoggingLevel.DEBUG, "Beginning traverse")
111                .removeHeaders("CamelHttp*")
112                .setHeader(HTTP_METHOD).constant(GET)
113                .to("fcrepo:" + fcrepoCamelConfig.getFcrepoBaseUrl() + "?preferInclude=PreferContainment" +
114                        "&preferOmit=ServerManaged&accept=application/n-triples")
115            // split the n-triples stream on line breaks so that each triple is split into a separate message
116            .split(body().tokenize("\\n")).streaming()
117                .removeHeader(FCREPO_URI)
118                .removeHeader("JMSCorrelationID")
119                .process(exchange -> {
120                    // This is a simple n-triples parser, spliting nodes on whitespace according to
121                    // https://www.w3.org/TR/n-triples/#n-triples-grammar
122                    // If the body is not null and the predicate is ldp:contains and the object is a URI,
123                    // then set the CamelFcrepoUri header (if that header is not set, the processing stops
124                    // at the filter() line below.
125                    final String body = exchange.getIn().getBody(String.class);
126                    if (body != null) {
127                        final String parts[] = body.split("\\s+");
128                        if (parts.length > 2 && parts[1].equals(LDP_CONTAINS) && parts[2].startsWith("<")) {
129                            exchange.getIn().setHeader(FCREPO_URI, parts[2].substring(1, parts[2].length() - 1));
130                        }
131                        exchange.getIn().setBody(null);
132                    }
133                })
134                .filter(header(FCREPO_URI).isNotNull())
135                .to(ExchangePattern.InOnly, config.getReindexingStream() + "?disableTimeToLive=true");
136
137        /**
138         *  Send the message to all of the pre-determined endpoints
139         */
140        from("direct:recipients").routeId("FcrepoReindexingRecipients")
141            .recipientList(header(REINDEXING_RECIPIENTS))
142            .ignoreInvalidEndpoints();
143    }
144}