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