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 { 220 return DEFAULT_CONTENT_TYPE; 221 } 222 } 223 224 /** 225 * Given an exchange, extract the value of an incoming Accept header. 226 * 227 * @param exchange the incoming message exchange 228 */ 229 private String getAcceptHeader(final Exchange exchange) { 230 final Message in = exchange.getIn(); 231 if (!isBlank(in.getHeader(Exchange.ACCEPT_CONTENT_TYPE, String.class))) { 232 return in.getHeader(Exchange.ACCEPT_CONTENT_TYPE, String.class); 233 } else if (!isBlank(in.getHeader("Accept", String.class))) { 234 return in.getHeader("Accept", String.class); 235 } else { 236 return null; 237 } 238 } 239 240 /** 241 * The resource path can be set either by the Camel header (CamelFcrepoIdentifier) 242 * or by fedora's jms headers (org.fcrepo.jms.identifier). This method extracts 243 * a path from the appropriate header (the camel header overrides the jms header). 244 * 245 * @param exchange The camel exchange 246 * @return String 247 */ 248 private String getPathFromHeaders(final Exchange exchange) { 249 final Message in = exchange.getIn(); 250 251 if (!isBlank(in.getHeader(FcrepoHeaders.FCREPO_IDENTIFIER, String.class))) { 252 return in.getHeader(FcrepoHeaders.FCREPO_IDENTIFIER, String.class); 253 } else if (!isBlank(in.getHeader(JmsHeaders.IDENTIFIER, String.class))) { 254 return in.getHeader(JmsHeaders.IDENTIFIER, String.class); 255 } else { 256 return ""; 257 } 258 } 259 260 /** 261 * Extract a transformation path from the exchange if the appropriate headers 262 * are set. This will format the URL to use the transform program defined 263 * in the CamelFcrepoTransform header or the transform uri option (in that 264 * order of precidence). 265 * 266 * @param exchange the camel message exchange 267 * @return String 268 */ 269 private String getTransformPath(final Exchange exchange) { 270 final Message in = exchange.getIn(); 271 final HttpMethods method = getMethod(exchange); 272 final String transformProgram = in.getHeader(FcrepoHeaders.FCREPO_TRANSFORM, String.class); 273 final String fcrTransform = "/fcr:transform"; 274 275 if (!isBlank(endpoint.getTransform()) || !isBlank(transformProgram)) { 276 if (method == HttpMethods.POST) { 277 return fcrTransform; 278 } else if (method == HttpMethods.GET) { 279 if (!isBlank(transformProgram)) { 280 return fcrTransform + "/" + transformProgram; 281 } else { 282 return fcrTransform + "/" + endpoint.getTransform(); 283 } 284 } 285 } 286 return ""; 287 } 288 289 /** 290 * Given an exchange, extract the fully qualified URL for a fedora resource. By default, this will use the entire 291 * path set on the endpoint. If either of the following headers are defined, they will be appended to that path in 292 * this order of preference: 1) FCREPO_IDENTIFIER 2) org.fcrepo.jms.identifier 293 * 294 * @param exchange the incoming message exchange 295 */ 296 private String getUrl(final Exchange exchange, final String transaction) { 297 final StringBuilder url = new StringBuilder(); 298 final String transformPath = getTransformPath(exchange); 299 final HttpMethods method = getMethod(exchange); 300 301 url.append(endpoint.getBaseUrlWithScheme()); 302 if (transaction != null) { 303 url.append("/"); 304 url.append(transaction); 305 } 306 url.append(getPathFromHeaders(exchange)); 307 308 if (!isBlank(transformPath)) { 309 url.append(transformPath); 310 } else if (method == HttpMethods.DELETE && endpoint.getTombstone()) { 311 url.append("/fcr:tombstone"); 312 } 313 314 return url.toString(); 315 } 316 317 /** 318 * Given an exchange, extract the Prefer headers, if any. 319 * 320 * @param exchange the incoming message exchange 321 */ 322 private String getPrefer(final Exchange exchange) { 323 final Message in = exchange.getIn(); 324 325 if (getMethod(exchange) == HttpMethods.GET) { 326 if (!isBlank(in.getHeader(FcrepoHeaders.FCREPO_PREFER, String.class))) { 327 return in.getHeader(FcrepoHeaders.FCREPO_PREFER, String.class); 328 } else { 329 return buildPreferHeader(endpoint.getPreferInclude(), endpoint.getPreferOmit()); 330 } 331 } else { 332 return null; 333 } 334 } 335 336 /** 337 * Build the prefer header from include and/or omit endpoint values 338 */ 339 private String buildPreferHeader(final String include, final String omit) { 340 if (isBlank(include) && isBlank(omit)) { 341 return null; 342 } else { 343 final StringBuilder prefer = new StringBuilder("return=representation;"); 344 345 if (!isBlank(include)) { 346 prefer.append(" include=\"" + addPreferNamespace(include) + "\";"); 347 } 348 if (!isBlank(omit)) { 349 prefer.append(" omit=\"" + addPreferNamespace(omit) + "\";"); 350 } 351 return prefer.toString(); 352 } 353 } 354 355 /** 356 * Add the appropriate namespace to the prefer header in case the 357 * short form was supplied. 358 */ 359 private String addPreferNamespace(final String property) { 360 final String prefer = RdfNamespaces.PREFER_PROPERTIES.get(property); 361 if (!isBlank(prefer)) { 362 return prefer; 363 } else { 364 return property; 365 } 366 } 367 368 private static Object extractResponseBodyAsStream(final InputStream is, final Exchange exchange) { 369 // As httpclient is using a AutoCloseInputStream, it will be closed when the connection is closed 370 // we need to cache the stream for it. 371 if (is == null) { 372 return null; 373 } 374 375 // convert the input stream to StreamCache if the stream cache is not disabled 376 if (exchange.getProperty(Exchange.DISABLE_HTTP_STREAM_CACHE, Boolean.FALSE, Boolean.class)) { 377 return is; 378 } else { 379 try (final CachedOutputStream cos = new CachedOutputStream(exchange)) { 380 // This CachedOutputStream will not be closed when the exchange is onCompletion 381 IOHelper.copyAndCloseInput(is, cos); 382 // When the InputStream is closed, the CachedOutputStream will be closed 383 return cos.newStreamCache(); 384 } catch (IOException ex) { 385 LOGGER.debug("Error extracting body from http request", ex); 386 return null; 387 } 388 } 389 } 390}