001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.camel;
019
020import static org.apache.commons.lang3.StringUtils.isBlank;
021import static org.slf4j.LoggerFactory.getLogger;
022
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.URI;
026
027import org.apache.camel.Exchange;
028import org.apache.camel.Message;
029import org.apache.camel.converter.stream.CachedOutputStream;
030import org.apache.camel.impl.DefaultProducer;
031import org.apache.camel.util.ExchangeHelper;
032import org.apache.camel.util.IOHelper;
033import org.fcrepo.client.FcrepoClient;
034import org.fcrepo.client.FcrepoOperationFailedException;
035import org.fcrepo.client.FcrepoResponse;
036import org.fcrepo.client.HttpMethods;
037import org.slf4j.Logger;
038import org.springframework.transaction.TransactionStatus;
039import org.springframework.transaction.TransactionSystemException;
040import org.springframework.transaction.support.DefaultTransactionStatus;
041import org.springframework.transaction.support.TransactionCallbackWithoutResult;
042import org.springframework.transaction.support.TransactionTemplate;
043
044/**
045 * The Fedora producer.
046 *
047 * @author Aaron Coburn
048 * @since October 20, 2014
049 */
050public class FcrepoProducer extends DefaultProducer {
051
052    public static final String DEFAULT_CONTENT_TYPE = "application/rdf+xml";
053
054    private static final Logger LOGGER = getLogger(FcrepoProducer.class);
055
056    private FcrepoEndpoint endpoint;
057
058    private FcrepoClient client;
059
060    private TransactionTemplate transactionTemplate;
061
062    /**
063     * Create a FcrepoProducer object
064     *
065     * @param endpoint the FcrepoEndpoint corresponding to the exchange.
066     */
067    public FcrepoProducer(final FcrepoEndpoint endpoint) {
068        super(endpoint);
069        this.endpoint = endpoint;
070        this.transactionTemplate = endpoint.createTransactionTemplate();
071        this.client = new FcrepoClient(
072                endpoint.getAuthUsername(),
073                endpoint.getAuthPassword(),
074                endpoint.getAuthHost(),
075                endpoint.getThrowExceptionOnFailure());
076
077    }
078
079    /**
080     * Define how message exchanges are processed.
081     *
082     * @param exchange the InOut message exchange
083     * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error
084     */
085    @Override
086    public void process(final Exchange exchange) throws FcrepoOperationFailedException {
087        if (exchange.isTransacted()) {
088            transactionTemplate.execute(new TransactionCallbackWithoutResult() {
089                protected void doInTransactionWithoutResult(final TransactionStatus status) {
090                    final DefaultTransactionStatus st = (DefaultTransactionStatus)status;
091                    final FcrepoTransactionObject tx = (FcrepoTransactionObject)st.getTransaction();
092                    try {
093                        doRequest(exchange, tx.getSessionId());
094                    } catch (FcrepoOperationFailedException ex) {
095                        throw new TransactionSystemException(
096                            "Error executing fcrepo request in transaction: ", ex);
097                    }
098                }
099            });
100        } else {
101            doRequest(exchange, null);
102        }
103    }
104
105    private void doRequest(final Exchange exchange, final String transaction) throws FcrepoOperationFailedException {
106        final Message in = exchange.getIn();
107        final HttpMethods method = getMethod(exchange);
108        final String contentType = getContentType(exchange);
109        final String accept = getAccept(exchange);
110        final String url = getUrl(exchange, transaction);
111        final String prefer = getPrefer(exchange);
112
113        LOGGER.debug("Fcrepo Request [{}] with method [{}]", url, method);
114
115        FcrepoResponse response;
116
117        switch (method) {
118        case PATCH:
119            response = client.patch(getMetadataUri(url), in.getBody(InputStream.class));
120            exchange.getIn().setBody(extractResponseBodyAsStream(response.getBody(), exchange));
121            break;
122        case PUT:
123            response = client.put(URI.create(url), in.getBody(InputStream.class), contentType);
124            exchange.getIn().setBody(extractResponseBodyAsStream(response.getBody(), exchange));
125            break;
126        case POST:
127            response = client.post(URI.create(url), in.getBody(InputStream.class), contentType);
128            exchange.getIn().setBody(extractResponseBodyAsStream(response.getBody(), exchange));
129            break;
130        case DELETE:
131            response = client.delete(URI.create(url));
132            exchange.getIn().setBody(extractResponseBodyAsStream(response.getBody(), exchange));
133            break;
134        case HEAD:
135            response = client.head(URI.create(url));
136            exchange.getIn().setBody(null);
137            break;
138        case GET:
139        default:
140            if (endpoint.getFixity()) {
141                response = client.get(URI.create(url + FcrepoConstants.FIXITY), accept, prefer);
142            } else if (endpoint.getMetadata()) {
143                response = client.get(getMetadataUri(url), accept, prefer);
144            } else {
145                response = client.get(URI.create(url), accept, prefer);
146            }
147            exchange.getIn().setBody(extractResponseBodyAsStream(response.getBody(), exchange));
148        }
149
150        exchange.getIn().setHeader(Exchange.CONTENT_TYPE, response.getContentType());
151        exchange.getIn().setHeader(Exchange.HTTP_RESPONSE_CODE, response.getStatusCode());
152    }
153
154
155    /**
156     * Retrieve the resource location from a HEAD request.
157     */
158    private URI getMetadataUri(final String url)
159            throws FcrepoOperationFailedException {
160        final FcrepoResponse headResponse = client.head(URI.create(url));
161        if (headResponse.getLocation() != null) {
162            return headResponse.getLocation();
163        } else {
164            return URI.create(url);
165        }
166    }
167
168
169    /**
170     * Given an exchange, determine which HTTP method to use. Basically, use GET unless the value of the
171     * Exchange.HTTP_METHOD header is defined. Unlike the http4: component, the request does not use POST if there is
172     * a message body defined. This is so in order to avoid inadvertant changes to the repository.
173     *
174     * @param exchange the incoming message exchange
175     */
176    private HttpMethods getMethod(final Exchange exchange) {
177        final HttpMethods method = exchange.getIn().getHeader(Exchange.HTTP_METHOD, HttpMethods.class);
178        if (method == null) {
179            return HttpMethods.GET;
180        } else {
181            return method;
182        }
183    }
184
185    /**
186     * Given an exchange, extract the contentType value for use with a Content-Type header. The order of preference is
187     * so: 1) a contentType value set on the endpoint 2) a contentType value set on the Exchange.CONTENT_TYPE header
188     *
189     * @param exchange the incoming message exchange
190     */
191    private String getContentType(final Exchange exchange) {
192        final String contentTypeString = ExchangeHelper.getContentType(exchange);
193        if (!isBlank(endpoint.getContentType())) {
194            return endpoint.getContentType();
195        } else if (!isBlank(contentTypeString)) {
196            return contentTypeString;
197        } else {
198            return null;
199        }
200    }
201
202    /**
203     * Given an exchange, extract the value for use with an Accept header. The order of preference is:
204     * 1) whether a transform is being requested 2) an accept value is set on the endpoint 3) a value set on
205     * the Exchange.ACCEPT_CONTENT_TYPE header 4) a value set on an "Accept" header 5) the endpoint
206     * DEFAULT_CONTENT_TYPE (i.e. application/rdf+xml)
207     *
208     * @param exchange the incoming message exchange
209     */
210    private String getAccept(final Exchange exchange) {
211        final Message in = exchange.getIn();
212        final String fcrepoTransform = in.getHeader(FcrepoHeaders.FCREPO_TRANSFORM, String.class);
213        final String acceptHeader = getAcceptHeader(exchange);
214
215        if (!isBlank(endpoint.getTransform()) || !isBlank(fcrepoTransform)) {
216            return "application/json";
217        } else if (!isBlank(endpoint.getAccept())) {
218            return endpoint.getAccept();
219        } else if (!isBlank(acceptHeader)) {
220            return acceptHeader;
221        } else if (!endpoint.getMetadata()) {
222            return "*/*";
223        } else {
224            return DEFAULT_CONTENT_TYPE;
225        }
226    }
227
228    /**
229     * Given an exchange, extract the value of an incoming Accept header.
230     *
231     * @param exchange the incoming message exchange
232     */
233    private String getAcceptHeader(final Exchange exchange) {
234        final Message in = exchange.getIn();
235        if (!isBlank(in.getHeader(Exchange.ACCEPT_CONTENT_TYPE, String.class))) {
236            return in.getHeader(Exchange.ACCEPT_CONTENT_TYPE, String.class);
237        } else if (!isBlank(in.getHeader("Accept", String.class))) {
238            return in.getHeader("Accept", String.class);
239        } else {
240            return null;
241        }
242    }
243
244    /**
245     * The resource path can be set either by the Camel header (CamelFcrepoIdentifier)
246     * or by fedora's jms headers (org.fcrepo.jms.identifier). This method extracts
247     * a path from the appropriate header (the camel header overrides the jms header).
248     *
249     * @param exchange The camel exchange
250     * @return String
251     */
252    private String getPathFromHeaders(final Exchange exchange) {
253        final Message in = exchange.getIn();
254
255        if (!isBlank(in.getHeader(FcrepoHeaders.FCREPO_IDENTIFIER, String.class))) {
256            return in.getHeader(FcrepoHeaders.FCREPO_IDENTIFIER, String.class);
257        } else if (!isBlank(in.getHeader(JmsHeaders.IDENTIFIER, String.class))) {
258            return in.getHeader(JmsHeaders.IDENTIFIER, String.class);
259        } else {
260            return "";
261        }
262    }
263
264    /**
265     *  Extract a transformation path from the exchange if the appropriate headers
266     *  are set. This will format the URL to use the transform program defined
267     *  in the CamelFcrepoTransform header or the transform uri option (in that
268     *  order of precidence).
269     *
270     *  @param exchange the camel message exchange
271     *  @return String
272     */
273    private String getTransformPath(final Exchange exchange) {
274        final Message in = exchange.getIn();
275        final HttpMethods method = getMethod(exchange);
276        final String transformProgram = in.getHeader(FcrepoHeaders.FCREPO_TRANSFORM, String.class);
277        final String fcrTransform = "/fcr:transform";
278
279        if (!isBlank(endpoint.getTransform()) || !isBlank(transformProgram)) {
280            LOGGER.warn("The transform option for fcrepo-camel has been deprecated and will be removed in 4.5.0");
281            if (method == HttpMethods.POST) {
282                return fcrTransform;
283            } else if (method == HttpMethods.GET) {
284                if (!isBlank(transformProgram)) {
285                    return fcrTransform + "/" + transformProgram;
286                } else {
287                    return fcrTransform + "/" + endpoint.getTransform();
288                }
289            }
290        }
291        return "";
292    }
293
294    /**
295     * Given an exchange, extract the fully qualified URL for a fedora resource. By default, this will use the entire
296     * path set on the endpoint. If either of the following headers are defined, they will be appended to that path in
297     * this order of preference: 1) FCREPO_IDENTIFIER 2) org.fcrepo.jms.identifier
298     *
299     * @param exchange the incoming message exchange
300     */
301    private String getUrl(final Exchange exchange, final String transaction) {
302        final StringBuilder url = new StringBuilder();
303        final String transformPath = getTransformPath(exchange);
304        final HttpMethods method = getMethod(exchange);
305
306        url.append(endpoint.getBaseUrlWithScheme());
307        if (transaction != null) {
308            url.append("/");
309            url.append(transaction);
310        }
311        url.append(getPathFromHeaders(exchange));
312
313        if (!isBlank(transformPath)) {
314            url.append(transformPath);
315        } else if (method == HttpMethods.DELETE && endpoint.getTombstone()) {
316            LOGGER.warn("The tombstone option is deprecated and will be removed in the 4.5.0 release of fcrepo-camel");
317            url.append("/fcr:tombstone");
318        }
319
320        return url.toString();
321    }
322
323    /**
324     *  Given an exchange, extract the Prefer headers, if any.
325     *
326     *  @param exchange the incoming message exchange
327     */
328    private String getPrefer(final Exchange exchange) {
329        final Message in = exchange.getIn();
330
331        if (getMethod(exchange) == HttpMethods.GET) {
332            if (!isBlank(in.getHeader(FcrepoHeaders.FCREPO_PREFER, String.class))) {
333                return in.getHeader(FcrepoHeaders.FCREPO_PREFER, String.class);
334            } else {
335                return buildPreferHeader(endpoint.getPreferInclude(), endpoint.getPreferOmit());
336            }
337        } else {
338            return null;
339        }
340    }
341
342    /**
343     *  Build the prefer header from include and/or omit endpoint values
344     */
345    private String buildPreferHeader(final String include, final String omit) {
346        if (isBlank(include) && isBlank(omit)) {
347            return null;
348        } else {
349            final StringBuilder prefer = new StringBuilder("return=representation;");
350
351            if (!isBlank(include)) {
352                prefer.append(" include=\"" + addPreferNamespace(include) + "\";");
353            }
354            if (!isBlank(omit)) {
355                prefer.append(" omit=\"" + addPreferNamespace(omit) + "\";");
356            }
357            LOGGER.info("Prefer: {}", prefer.toString());
358            return prefer.toString();
359        }
360    }
361
362    /**
363     *  Add the appropriate namespace to the prefer header in case the
364     *  short form was supplied.
365     */
366    private String addPreferNamespace(final String property) {
367        final String prefer = RdfNamespaces.PREFER_PROPERTIES.get(property);
368        if (!isBlank(prefer)) {
369            return prefer;
370        } else {
371            return property;
372        }
373    }
374
375    private static Object extractResponseBodyAsStream(final InputStream is, final Exchange exchange) {
376        // As httpclient is using a AutoCloseInputStream, it will be closed when the connection is closed
377        // we need to cache the stream for it.
378        if (is == null) {
379            return null;
380        }
381
382        // convert the input stream to StreamCache if the stream cache is not disabled
383        if (exchange.getProperty(Exchange.DISABLE_HTTP_STREAM_CACHE, Boolean.FALSE, Boolean.class)) {
384            return is;
385        } else {
386            try (final CachedOutputStream cos = new CachedOutputStream(exchange)) {
387                // This CachedOutputStream will not be closed when the exchange is onCompletion
388                IOHelper.copyAndCloseInput(is, cos);
389                // When the InputStream is closed, the CachedOutputStream will be closed
390                return cos.newStreamCache();
391            } catch (IOException ex) {
392                LOGGER.debug("Error extracting body from http request", ex);
393                return null;
394            }
395        }
396    }
397}