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