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 final FcrepoEndpoint endpoint;
083
084    private FcrepoClient fcrepoClient;
085
086    private final 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                @Override
145                protected void doInTransactionWithoutResult(final TransactionStatus status) {
146                    final DefaultTransactionStatus st = (DefaultTransactionStatus)status;
147                    final FcrepoTransactionObject tx = (FcrepoTransactionObject)st.getTransaction();
148                    try {
149                        doRequest(exchange, tx.getSessionId());
150                    } catch (final FcrepoOperationFailedException ex) {
151                        throw new TransactionSystemException(
152                            "Error executing fcrepo request in transaction: ", ex);
153                    }
154                }
155            });
156        } else {
157            doRequest(exchange, null);
158        }
159    }
160
161    private void doRequest(final Exchange exchange, final String transaction) throws FcrepoOperationFailedException {
162        final Message in = exchange.getIn();
163        final HttpMethods method = getMethod(exchange);
164        final String contentType = getContentType(exchange);
165        final String accept = getAccept(exchange);
166        final String url = getUrl(exchange, transaction);
167
168        LOGGER.debug("Fcrepo Request [{}] with method [{}]", url, method);
169
170        final FcrepoResponse response;
171
172        switch (method) {
173        case PATCH:
174            response = fcrepoClient.patch(getMetadataUri(url)).body(in.getBody(InputStream.class)).perform();
175            exchange.getIn().setBody(extractResponseBodyAsStream(response.getBody(), exchange));
176            break;
177        case PUT:
178            response = fcrepoClient.put(URI.create(url)).body(in.getBody(InputStream.class), contentType).perform();
179            exchange.getIn().setBody(extractResponseBodyAsStream(response.getBody(), exchange));
180            break;
181        case POST:
182            response = fcrepoClient.post(URI.create(url)).body(in.getBody(InputStream.class), contentType).perform();
183            exchange.getIn().setBody(extractResponseBodyAsStream(response.getBody(), exchange));
184            break;
185        case DELETE:
186            response = fcrepoClient.delete(URI.create(url)).perform();
187            exchange.getIn().setBody(extractResponseBodyAsStream(response.getBody(), exchange));
188            break;
189        case HEAD:
190            response = fcrepoClient.head(URI.create(url)).perform();
191            exchange.getIn().setBody(null);
192            break;
193        case GET:
194        default:
195            final GetBuilder get = fcrepoClient.get(getUri(endpoint, url)).accept(accept);
196            final String preferHeader = in.getHeader(FCREPO_PREFER, "", String.class);
197            if (!preferHeader.isEmpty()) {
198                final FcrepoPrefer prefer = new FcrepoPrefer(preferHeader);
199                if (prefer.isMinimal()) {
200                    response = get.preferMinimal().perform();
201                } else if (prefer.isRepresentation()) {
202                    response = get.preferRepresentation(prefer.getInclude(), prefer.getOmit()).perform();
203                } else {
204                    response = get.perform();
205                }
206            } else {
207                final List<URI> include = getPreferInclude(endpoint);
208                final List<URI> omit = getPreferOmit(endpoint);
209                if (include.isEmpty() && omit.isEmpty()) {
210                    response = get.perform();
211                } else {
212                    response = get.preferRepresentation(include, omit).perform();
213                }
214            }
215            exchange.getIn().setBody(extractResponseBodyAsStream(response.getBody(), exchange));
216        }
217
218        exchange.getIn().setHeader(CONTENT_TYPE, response.getContentType());
219        exchange.getIn().setHeader(HTTP_RESPONSE_CODE, response.getStatusCode());
220    }
221
222    private URI getUri(final FcrepoEndpoint endpoint, final String url) throws FcrepoOperationFailedException {
223        if (endpoint.getFixity()) {
224            return URI.create(url + FIXITY);
225        } else if (endpoint.getMetadata()) {
226            return getMetadataUri(url);
227        }
228        return URI.create(url);
229    }
230
231    private List<URI> getPreferOmit(final FcrepoEndpoint endpoint) {
232        if (!isBlank(endpoint.getPreferOmit())) {
233            return stream(endpoint.getPreferOmit().split("\\s+")).map(addPreferNamespace).map(URI::create)
234                .collect(toList());
235        }
236        return emptyList();
237    }
238
239    private List<URI> getPreferInclude(final FcrepoEndpoint endpoint) {
240        if (!isBlank(endpoint.getPreferInclude())) {
241            return stream(endpoint.getPreferInclude().split("\\s+")).map(addPreferNamespace).map(URI::create)
242                .collect(toList());
243        }
244        return emptyList();
245    }
246
247    /**
248     * Retrieve the resource location from a HEAD request.
249     */
250    private URI getMetadataUri(final String url)
251            throws FcrepoOperationFailedException {
252        final FcrepoResponse headResponse = fcrepoClient.head(URI.create(url)).perform();
253        if (headResponse.getLocation() != null) {
254            return headResponse.getLocation();
255        } else {
256            return URI.create(url);
257        }
258    }
259
260
261    /**
262     * Given an exchange, determine which HTTP method to use. Basically, use GET unless the value of the
263     * Exchange.HTTP_METHOD header is defined. Unlike the http4: component, the request does not use POST if there is
264     * a message body defined. This is so in order to avoid inadvertant changes to the repository.
265     *
266     * @param exchange the incoming message exchange
267     */
268    private HttpMethods getMethod(final Exchange exchange) {
269        final HttpMethods method = exchange.getIn().getHeader(HTTP_METHOD, HttpMethods.class);
270        if (method == null) {
271            return GET;
272        } else {
273            return method;
274        }
275    }
276
277    /**
278     * Given an exchange, extract the contentType value for use with a Content-Type header. The order of preference is
279     * so: 1) a contentType value set on the endpoint 2) a contentType value set on the Exchange.CONTENT_TYPE header
280     *
281     * @param exchange the incoming message exchange
282     */
283    private String getContentType(final Exchange exchange) {
284        final String contentTypeString = ExchangeHelper.getContentType(exchange);
285        if (!isBlank(endpoint.getContentType())) {
286            return endpoint.getContentType();
287        } else if (!isBlank(contentTypeString)) {
288            return contentTypeString;
289        } else {
290            return null;
291        }
292    }
293
294    /**
295     * Given an exchange, extract the value for use with an Accept header. The order of preference is:
296     * 1) whether an accept value is set on the endpoint 2) a value set on
297     * the Exchange.ACCEPT_CONTENT_TYPE header 3) a value set on an "Accept" header
298     * 4) the endpoint DEFAULT_CONTENT_TYPE (i.e. application/rdf+xml)
299     *
300     * @param exchange the incoming message exchange
301     */
302    private String getAccept(final Exchange exchange) {
303        final String acceptHeader = getAcceptHeader(exchange);
304        if (!isBlank(endpoint.getAccept())) {
305            return endpoint.getAccept();
306        } else if (!isBlank(acceptHeader)) {
307            return acceptHeader;
308        } else if (!endpoint.getMetadata()) {
309            return "*/*";
310        } else {
311            return DEFAULT_CONTENT_TYPE;
312        }
313    }
314
315    /**
316     * Given an exchange, extract the value of an incoming Accept header.
317     *
318     * @param exchange the incoming message exchange
319     */
320    private String getAcceptHeader(final Exchange exchange) {
321        final Message in = exchange.getIn();
322        if (!isBlank(in.getHeader(ACCEPT_CONTENT_TYPE, String.class))) {
323            return in.getHeader(ACCEPT_CONTENT_TYPE, String.class);
324        } else if (!isBlank(in.getHeader("Accept", String.class))) {
325            return in.getHeader("Accept", String.class);
326        } else {
327            return null;
328        }
329    }
330
331    /**
332     * Given an exchange, extract the fully qualified URL for a fedora resource. By default, this will use the entire
333     * path set on the endpoint. If either of the following headers are defined, they will be appended to that path in
334     * this order of preference: 1) FCREPO_URI 2) FCREPO_BASE_URL + FCREPO_IDENTIFIER
335     *
336     * @param exchange the incoming message exchange
337     */
338    private String getUrl(final Exchange exchange, final String transaction) {
339        final String uri = exchange.getIn().getHeader(FCREPO_URI, "", String.class);
340        if (!uri.isEmpty()) {
341            return uri;
342        }
343
344        final String baseUrl = exchange.getIn().getHeader(FCREPO_BASE_URL, "", String.class);
345        final StringBuilder url = new StringBuilder(baseUrl.isEmpty() ? endpoint.getBaseUrlWithScheme() : baseUrl);
346        if (transaction != null) {
347            url.append("/");
348            url.append(transaction);
349        }
350        url.append(exchange.getIn().getHeader(FCREPO_IDENTIFIER, "", String.class));
351
352        return url.toString();
353    }
354
355    private static Object extractResponseBodyAsStream(final InputStream is, final Exchange exchange) {
356        // As httpclient is using a AutoCloseInputStream, it will be closed when the connection is closed
357        // we need to cache the stream for it.
358        if (is == null) {
359            return null;
360        }
361
362        // convert the input stream to StreamCache if the stream cache is not disabled
363        if (exchange.getProperty(DISABLE_HTTP_STREAM_CACHE, FALSE, Boolean.class)) {
364            return is;
365        } else {
366            try (final CachedOutputStream cos = new CachedOutputStream(exchange)) {
367                // This CachedOutputStream will not be closed when the exchange is onCompletion
368                IOHelper.copyAndCloseInput(is, cos);
369                // When the InputStream is closed, the CachedOutputStream will be closed
370                return cos.newStreamCache();
371            } catch (final IOException ex) {
372                LOGGER.debug("Error extracting body from http request", ex);
373                return null;
374            }
375        }
376    }
377}