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