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