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