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}