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}