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