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 if (!endpoint.getMetadata()) {
220            return "*/*";
221        } else {
222            return DEFAULT_CONTENT_TYPE;
223        }
224    }
225
226    /**
227     * Given an exchange, extract the value of an incoming Accept header.
228     *
229     * @param exchange the incoming message exchange
230     */
231    private String getAcceptHeader(final Exchange exchange) {
232        final Message in = exchange.getIn();
233        if (!isBlank(in.getHeader(Exchange.ACCEPT_CONTENT_TYPE, String.class))) {
234            return in.getHeader(Exchange.ACCEPT_CONTENT_TYPE, String.class);
235        } else if (!isBlank(in.getHeader("Accept", String.class))) {
236            return in.getHeader("Accept", String.class);
237        } else {
238            return null;
239        }
240    }
241
242    /**
243     * The resource path can be set either by the Camel header (CamelFcrepoIdentifier)
244     * or by fedora's jms headers (org.fcrepo.jms.identifier). This method extracts
245     * a path from the appropriate header (the camel header overrides the jms header).
246     *
247     * @param exchange The camel exchange
248     * @return String
249     */
250    private String getPathFromHeaders(final Exchange exchange) {
251        final Message in = exchange.getIn();
252
253        if (!isBlank(in.getHeader(FcrepoHeaders.FCREPO_IDENTIFIER, String.class))) {
254            return in.getHeader(FcrepoHeaders.FCREPO_IDENTIFIER, String.class);
255        } else if (!isBlank(in.getHeader(JmsHeaders.IDENTIFIER, String.class))) {
256            return in.getHeader(JmsHeaders.IDENTIFIER, String.class);
257        } else {
258            return "";
259        }
260    }
261
262    /**
263     *  Extract a transformation path from the exchange if the appropriate headers
264     *  are set. This will format the URL to use the transform program defined
265     *  in the CamelFcrepoTransform header or the transform uri option (in that
266     *  order of precidence).
267     *
268     *  @param exchange the camel message exchange
269     *  @return String
270     */
271    private String getTransformPath(final Exchange exchange) {
272        final Message in = exchange.getIn();
273        final HttpMethods method = getMethod(exchange);
274        final String transformProgram = in.getHeader(FcrepoHeaders.FCREPO_TRANSFORM, String.class);
275        final String fcrTransform = "/fcr:transform";
276
277        if (!isBlank(endpoint.getTransform()) || !isBlank(transformProgram)) {
278            if (method == HttpMethods.POST) {
279                return fcrTransform;
280            } else if (method == HttpMethods.GET) {
281                if (!isBlank(transformProgram)) {
282                    return fcrTransform + "/" + transformProgram;
283                } else {
284                    return fcrTransform + "/" + endpoint.getTransform();
285                }
286            }
287        }
288        return "";
289    }
290
291    /**
292     * Given an exchange, extract the fully qualified URL for a fedora resource. By default, this will use the entire
293     * path set on the endpoint. If either of the following headers are defined, they will be appended to that path in
294     * this order of preference: 1) FCREPO_IDENTIFIER 2) org.fcrepo.jms.identifier
295     *
296     * @param exchange the incoming message exchange
297     */
298    private String getUrl(final Exchange exchange, final String transaction) {
299        final StringBuilder url = new StringBuilder();
300        final String transformPath = getTransformPath(exchange);
301        final HttpMethods method = getMethod(exchange);
302
303        url.append(endpoint.getBaseUrlWithScheme());
304        if (transaction != null) {
305            url.append("/");
306            url.append(transaction);
307        }
308        url.append(getPathFromHeaders(exchange));
309
310        if (!isBlank(transformPath)) {
311            url.append(transformPath);
312        } else if (method == HttpMethods.DELETE && endpoint.getTombstone()) {
313            url.append("/fcr:tombstone");
314        }
315
316        return url.toString();
317    }
318
319    /**
320     *  Given an exchange, extract the Prefer headers, if any.
321     *
322     *  @param exchange the incoming message exchange
323     */
324    private String getPrefer(final Exchange exchange) {
325        final Message in = exchange.getIn();
326
327        if (getMethod(exchange) == HttpMethods.GET) {
328            if (!isBlank(in.getHeader(FcrepoHeaders.FCREPO_PREFER, String.class))) {
329                return in.getHeader(FcrepoHeaders.FCREPO_PREFER, String.class);
330            } else {
331                return buildPreferHeader(endpoint.getPreferInclude(), endpoint.getPreferOmit());
332            }
333        } else {
334            return null;
335        }
336    }
337
338    /**
339     *  Build the prefer header from include and/or omit endpoint values
340     */
341    private String buildPreferHeader(final String include, final String omit) {
342        if (isBlank(include) && isBlank(omit)) {
343            return null;
344        } else {
345            final StringBuilder prefer = new StringBuilder("return=representation;");
346
347            if (!isBlank(include)) {
348                prefer.append(" include=\"" + addPreferNamespace(include) + "\";");
349            }
350            if (!isBlank(omit)) {
351                prefer.append(" omit=\"" + addPreferNamespace(omit) + "\";");
352            }
353            return prefer.toString();
354        }
355    }
356
357    /**
358     *  Add the appropriate namespace to the prefer header in case the
359     *  short form was supplied.
360     */
361    private String addPreferNamespace(final String property) {
362        final String prefer = RdfNamespaces.PREFER_PROPERTIES.get(property);
363        if (!isBlank(prefer)) {
364            return prefer;
365        } else {
366            return property;
367        }
368    }
369
370    private static Object extractResponseBodyAsStream(final InputStream is, final Exchange exchange) {
371        // As httpclient is using a AutoCloseInputStream, it will be closed when the connection is closed
372        // we need to cache the stream for it.
373        if (is == null) {
374            return null;
375        }
376
377        // convert the input stream to StreamCache if the stream cache is not disabled
378        if (exchange.getProperty(Exchange.DISABLE_HTTP_STREAM_CACHE, Boolean.FALSE, Boolean.class)) {
379            return is;
380        } else {
381            try (final CachedOutputStream cos = new CachedOutputStream(exchange)) {
382                // This CachedOutputStream will not be closed when the exchange is onCompletion
383                IOHelper.copyAndCloseInput(is, cos);
384                // When the InputStream is closed, the CachedOutputStream will be closed
385                return cos.newStreamCache();
386            } catch (IOException ex) {
387                LOGGER.debug("Error extracting body from http request", ex);
388                return null;
389            }
390        }
391    }
392}