001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.processor; 018 019import java.util.HashMap; 020import java.util.Locale; 021import java.util.Map; 022import java.util.Set; 023 024import org.apache.camel.AsyncProcessor; 025import org.apache.camel.CamelContext; 026import org.apache.camel.CamelContextAware; 027import org.apache.camel.Exchange; 028import org.apache.camel.Message; 029import org.apache.camel.processor.binding.BindingException; 030import org.apache.camel.spi.DataFormat; 031import org.apache.camel.spi.DataType; 032import org.apache.camel.spi.DataTypeAware; 033import org.apache.camel.spi.RestConfiguration; 034import org.apache.camel.util.ExchangeHelper; 035import org.apache.camel.util.MessageHelper; 036import org.apache.camel.util.ObjectHelper; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040/** 041 * A {@link org.apache.camel.processor.CamelInternalProcessorAdvice} that binds the REST DSL incoming 042 * and outgoing messages from sources of json or xml to Java Objects. 043 * <p/> 044 * The binding uses {@link org.apache.camel.spi.DataFormat} for the actual work to transform 045 * from xml/json to Java Objects and reverse again. 046 * <p/> 047 * The rest producer side is implemented in {@link org.apache.camel.component.rest.RestProducerBindingProcessor} 048 * 049 * @see CamelInternalProcessor 050 */ 051public class RestBindingAdvice implements CamelInternalProcessorAdvice<Map<String, Object>> { 052 053 private static final Logger LOG = LoggerFactory.getLogger(RestBindingAdvice.class); 054 private static final String STATE_KEY_DO_MARSHAL = "doMarshal"; 055 private static final String STATE_KEY_ACCEPT = "accept"; 056 private static final String STATE_JSON = "json"; 057 private static final String STATE_XML = "xml"; 058 059 private final AsyncProcessor jsonUnmarshal; 060 private final AsyncProcessor xmlUnmarshal; 061 private final AsyncProcessor jsonMarshal; 062 private final AsyncProcessor xmlMarshal; 063 private final String consumes; 064 private final String produces; 065 private final String bindingMode; 066 private final boolean skipBindingOnErrorCode; 067 private final boolean clientRequestValidation; 068 private final boolean enableCORS; 069 private final Map<String, String> corsHeaders; 070 private final Map<String, String> queryDefaultValues; 071 private final boolean requiredBody; 072 private final Set<String> requiredQueryParameters; 073 private final Set<String> requiredHeaders; 074 075 public RestBindingAdvice(CamelContext camelContext, DataFormat jsonDataFormat, DataFormat xmlDataFormat, 076 DataFormat outJsonDataFormat, DataFormat outXmlDataFormat, 077 String consumes, String produces, String bindingMode, 078 boolean skipBindingOnErrorCode, boolean clientRequestValidation, boolean enableCORS, 079 Map<String, String> corsHeaders, 080 Map<String, String> queryDefaultValues, 081 boolean requiredBody, Set<String> requiredQueryParameters, Set<String> requiredHeaders) throws Exception { 082 083 if (jsonDataFormat != null) { 084 this.jsonUnmarshal = new UnmarshalProcessor(jsonDataFormat); 085 } else { 086 this.jsonUnmarshal = null; 087 } 088 if (outJsonDataFormat != null) { 089 this.jsonMarshal = new MarshalProcessor(outJsonDataFormat); 090 } else if (jsonDataFormat != null) { 091 this.jsonMarshal = new MarshalProcessor(jsonDataFormat); 092 } else { 093 this.jsonMarshal = null; 094 } 095 096 if (xmlDataFormat != null) { 097 this.xmlUnmarshal = new UnmarshalProcessor(xmlDataFormat); 098 } else { 099 this.xmlUnmarshal = null; 100 } 101 if (outXmlDataFormat != null) { 102 this.xmlMarshal = new MarshalProcessor(outXmlDataFormat); 103 } else if (xmlDataFormat != null) { 104 this.xmlMarshal = new MarshalProcessor(xmlDataFormat); 105 } else { 106 this.xmlMarshal = null; 107 } 108 109 if (jsonMarshal != null) { 110 camelContext.addService(jsonMarshal, true); 111 } 112 if (jsonUnmarshal != null) { 113 camelContext.addService(jsonUnmarshal, true); 114 } 115 if (xmlMarshal instanceof CamelContextAware) { 116 camelContext.addService(xmlMarshal, true); 117 } 118 if (xmlUnmarshal instanceof CamelContextAware) { 119 camelContext.addService(xmlUnmarshal, true); 120 } 121 122 this.consumes = consumes; 123 this.produces = produces; 124 this.bindingMode = bindingMode; 125 this.skipBindingOnErrorCode = skipBindingOnErrorCode; 126 this.clientRequestValidation = clientRequestValidation; 127 this.enableCORS = enableCORS; 128 this.corsHeaders = corsHeaders; 129 this.queryDefaultValues = queryDefaultValues; 130 this.requiredBody = requiredBody; 131 this.requiredQueryParameters = requiredQueryParameters; 132 this.requiredHeaders = requiredHeaders; 133 } 134 135 @Override 136 public Map<String, Object> before(Exchange exchange) throws Exception { 137 Map<String, Object> state = new HashMap<>(); 138 if (isOptionsMethod(exchange, state)) { 139 return state; 140 } 141 unmarshal(exchange, state); 142 return state; 143 } 144 145 @Override 146 public void after(Exchange exchange, Map<String, Object> state) throws Exception { 147 if (enableCORS) { 148 setCORSHeaders(exchange, state); 149 } 150 if (state.get(STATE_KEY_DO_MARSHAL) != null) { 151 marshal(exchange, state); 152 } 153 } 154 155 private boolean isOptionsMethod(Exchange exchange, Map<String, Object> state) { 156 String method = exchange.getIn().getHeader(Exchange.HTTP_METHOD, String.class); 157 if ("OPTIONS".equalsIgnoreCase(method)) { 158 // for OPTIONS methods then we should not route at all as its part of CORS 159 exchange.setProperty(Exchange.ROUTE_STOP, true); 160 return true; 161 } 162 return false; 163 } 164 165 private void unmarshal(Exchange exchange, Map<String, Object> state) throws Exception { 166 boolean isXml = false; 167 boolean isJson = false; 168 169 String contentType = ExchangeHelper.getContentType(exchange); 170 if (contentType != null) { 171 isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); 172 isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); 173 } 174 // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with 175 // that information in the consumes 176 if (!isXml && !isJson) { 177 isXml = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("xml"); 178 isJson = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("json"); 179 } 180 181 // set data type if in use 182 if (exchange.getContext().isUseDataType()) { 183 if (exchange.getIn() instanceof DataTypeAware && (isJson || isXml)) { 184 ((DataTypeAware) exchange.getIn()).setDataType(new DataType(isJson ? "json" : "xml")); 185 } 186 } 187 188 // only allow xml/json if the binding mode allows that 189 isXml &= bindingMode.equals("auto") || bindingMode.contains("xml"); 190 isJson &= bindingMode.equals("auto") || bindingMode.contains("json"); 191 192 // if we do not yet know if its xml or json, then use the binding mode to know the mode 193 if (!isJson && !isXml) { 194 isXml = bindingMode.equals("auto") || bindingMode.contains("xml"); 195 isJson = bindingMode.equals("auto") || bindingMode.contains("json"); 196 } 197 198 String accept = exchange.getMessage().getHeader("Accept", String.class); 199 state.put(STATE_KEY_ACCEPT, accept); 200 201 // perform client request validation 202 if (clientRequestValidation) { 203 // check if the content-type is accepted according to consumes 204 if (!isValidOrAcceptedContentType(consumes, contentType)) { 205 LOG.trace("Consuming content type does not match contentType header {}. Stopping routing.", contentType); 206 // the content-type is not something we can process so its a HTTP_ERROR 415 207 exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 415); 208 // stop routing and return 209 exchange.setProperty(Exchange.ROUTE_STOP, true); 210 return; 211 } 212 213 // check if what is produces is accepted by the client 214 if (!isValidOrAcceptedContentType(produces, accept)) { 215 LOG.trace("Produced content type does not match accept header {}. Stopping routing.", contentType); 216 // the response type is not accepted by the client so its a HTTP_ERROR 406 217 exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 406); 218 // stop routing and return 219 exchange.setProperty(Exchange.ROUTE_STOP, true); 220 return; 221 } 222 } 223 224 String body = null; 225 if (exchange.getIn().getBody() != null) { 226 227 // okay we have a binding mode, so need to check for empty body as that can cause the marshaller to fail 228 // as they assume a non-empty body 229 if (isXml || isJson) { 230 // we have binding enabled, so we need to know if there body is empty or not 231 // so force reading the body as a String which we can work with 232 body = MessageHelper.extractBodyAsString(exchange.getIn()); 233 if (body != null) { 234 if (exchange.getIn() instanceof DataTypeAware) { 235 ((DataTypeAware)exchange.getIn()).setBody(body, new DataType(isJson ? "json" : "xml")); 236 } else { 237 exchange.getIn().setBody(body); 238 } 239 240 if (isXml && isJson) { 241 // we have still not determined between xml or json, so check the body if its xml based or not 242 isXml = body.startsWith("<"); 243 isJson = !isXml; 244 } 245 } 246 } 247 } 248 249 // add missing default values which are mapped as headers 250 if (queryDefaultValues != null) { 251 for (Map.Entry<String, String> entry : queryDefaultValues.entrySet()) { 252 if (exchange.getIn().getHeader(entry.getKey()) == null) { 253 exchange.getIn().setHeader(entry.getKey(), entry.getValue()); 254 } 255 } 256 } 257 258 // check for required 259 if (clientRequestValidation) { 260 if (requiredBody) { 261 // the body is required so we need to know if we have a body or not 262 // so force reading the body as a String which we can work with 263 if (body == null) { 264 body = MessageHelper.extractBodyAsString(exchange.getIn()); 265 if (body != null) { 266 exchange.getIn().setBody(body); 267 } 268 } 269 if (body == null) { 270 // this is a bad request, the client did not include a message body 271 exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); 272 exchange.getOut().setBody("The request body is missing."); 273 // stop routing and return 274 exchange.setProperty(Exchange.ROUTE_STOP, true); 275 return; 276 } 277 } 278 if (requiredQueryParameters != null && !exchange.getIn().getHeaders().keySet().containsAll(requiredQueryParameters)) { 279 // this is a bad request, the client did not include some of the required query parameters 280 exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); 281 exchange.getOut().setBody("Some of the required query parameters are missing."); 282 // stop routing and return 283 exchange.setProperty(Exchange.ROUTE_STOP, true); 284 return; 285 } 286 if (requiredHeaders != null && !exchange.getIn().getHeaders().keySet().containsAll(requiredHeaders)) { 287 // this is a bad request, the client did not include some of the required http headers 288 exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); 289 exchange.getOut().setBody("Some of the required HTTP headers are missing."); 290 // stop routing and return 291 exchange.setProperty(Exchange.ROUTE_STOP, true); 292 return; 293 } 294 } 295 296 // favor json over xml 297 if (isJson && jsonUnmarshal != null) { 298 // add reverse operation 299 state.put(STATE_KEY_DO_MARSHAL, STATE_JSON); 300 if (ObjectHelper.isNotEmpty(body)) { 301 jsonUnmarshal.process(exchange); 302 ExchangeHelper.prepareOutToIn(exchange); 303 } 304 return; 305 } else if (isXml && xmlUnmarshal != null) { 306 // add reverse operation 307 state.put(STATE_KEY_DO_MARSHAL, STATE_XML); 308 if (ObjectHelper.isNotEmpty(body)) { 309 xmlUnmarshal.process(exchange); 310 ExchangeHelper.prepareOutToIn(exchange); 311 } 312 return; 313 } 314 315 // we could not bind 316 if ("off".equals(bindingMode) || bindingMode.equals("auto")) { 317 // okay for auto we do not mind if we could not bind 318 state.put(STATE_KEY_DO_MARSHAL, STATE_JSON); 319 } else { 320 if (bindingMode.contains("xml")) { 321 exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange)); 322 } else { 323 exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange)); 324 } 325 } 326 327 } 328 329 private void marshal(Exchange exchange, Map<String, Object> state) { 330 // only marshal if there was no exception 331 if (exchange.getException() != null) { 332 return; 333 } 334 335 if (skipBindingOnErrorCode) { 336 Integer code = exchange.hasOut() ? exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class) : exchange.getIn().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class); 337 // if there is a custom http error code then skip binding 338 if (code != null && code >= 300) { 339 return; 340 } 341 } 342 343 boolean isXml = false; 344 boolean isJson = false; 345 346 // accept takes precedence 347 String accept = (String)state.get(STATE_KEY_ACCEPT); 348 if (accept != null) { 349 isXml = accept.toLowerCase(Locale.ENGLISH).contains("xml"); 350 isJson = accept.toLowerCase(Locale.ENGLISH).contains("json"); 351 } 352 // fallback to content type if still undecided 353 if (!isXml && !isJson) { 354 String contentType = ExchangeHelper.getContentType(exchange); 355 if (contentType != null) { 356 isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); 357 isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); 358 } 359 } 360 // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with 361 // that information in the consumes 362 if (!isXml && !isJson) { 363 isXml = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("xml"); 364 isJson = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("json"); 365 } 366 367 // only allow xml/json if the binding mode allows that (when off we still want to know if its xml or json) 368 if (bindingMode != null) { 369 isXml &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("xml"); 370 isJson &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("json"); 371 372 // if we do not yet know if its xml or json, then use the binding mode to know the mode 373 if (!isJson && !isXml) { 374 isXml = bindingMode.equals("auto") || bindingMode.contains("xml"); 375 isJson = bindingMode.equals("auto") || bindingMode.contains("json"); 376 } 377 } 378 379 // in case we have not yet been able to determine if xml or json, then use the same as in the unmarshaller 380 if (isXml && isJson) { 381 isXml = state.get(STATE_KEY_DO_MARSHAL).equals(STATE_XML); 382 isJson = !isXml; 383 } 384 385 // need to prepare exchange first 386 ExchangeHelper.prepareOutToIn(exchange); 387 388 // ensure there is a content type header (even if binding is off) 389 ensureHeaderContentType(produces, isXml, isJson, exchange); 390 391 if (bindingMode == null || "off".equals(bindingMode)) { 392 // binding is off, so no message body binding 393 return; 394 } 395 396 // is there any marshaller at all 397 if (jsonMarshal == null && xmlMarshal == null) { 398 return; 399 } 400 401 // is the body empty 402 if ((exchange.hasOut() && exchange.getOut().getBody() == null) || (!exchange.hasOut() && exchange.getIn().getBody() == null)) { 403 return; 404 } 405 406 String contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class); 407 // need to lower-case so the contains check below can match if using upper case 408 contentType = contentType.toLowerCase(Locale.US); 409 try { 410 // favor json over xml 411 if (isJson && jsonMarshal != null) { 412 // only marshal if its json content type 413 if (contentType.contains("json")) { 414 jsonMarshal.process(exchange); 415 setOutputDataType(exchange, new DataType("json")); 416 } 417 } else if (isXml && xmlMarshal != null) { 418 // only marshal if its xml content type 419 if (contentType.contains("xml")) { 420 xmlMarshal.process(exchange); 421 setOutputDataType(exchange, new DataType("xml")); 422 } 423 } else { 424 // we could not bind 425 if (bindingMode.equals("auto")) { 426 // okay for auto we do not mind if we could not bind 427 } else { 428 if (bindingMode.contains("xml")) { 429 exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange)); 430 } else { 431 exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange)); 432 } 433 } 434 } 435 } catch (Throwable e) { 436 exchange.setException(e); 437 } 438 } 439 440 private void setOutputDataType(Exchange exchange, DataType type) { 441 Message target = exchange.hasOut() ? exchange.getOut() : exchange.getIn(); 442 if (target instanceof DataTypeAware) { 443 ((DataTypeAware)target).setDataType(type); 444 } 445 } 446 447 private void ensureHeaderContentType(String contentType, boolean isXml, boolean isJson, Exchange exchange) { 448 // favor given content type 449 if (contentType != null) { 450 String type = ExchangeHelper.getContentType(exchange); 451 if (type == null) { 452 exchange.getIn().setHeader(Exchange.CONTENT_TYPE, contentType); 453 } 454 } 455 456 // favor json over xml 457 if (isJson) { 458 // make sure there is a content-type with json 459 String type = ExchangeHelper.getContentType(exchange); 460 if (type == null) { 461 exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json"); 462 } 463 } else if (isXml) { 464 // make sure there is a content-type with xml 465 String type = ExchangeHelper.getContentType(exchange); 466 if (type == null) { 467 exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/xml"); 468 } 469 } 470 } 471 472 private void setCORSHeaders(Exchange exchange, Map<String, Object> state) { 473 // add the CORS headers after routing, but before the consumer writes the response 474 Message msg = exchange.hasOut() ? exchange.getOut() : exchange.getIn(); 475 476 // use default value if none has been configured 477 String allowOrigin = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Origin") : null; 478 if (allowOrigin == null) { 479 allowOrigin = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_ORIGIN; 480 } 481 String allowMethods = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Methods") : null; 482 if (allowMethods == null) { 483 allowMethods = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_METHODS; 484 } 485 String allowHeaders = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Headers") : null; 486 if (allowHeaders == null) { 487 allowHeaders = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_HEADERS; 488 } 489 String maxAge = corsHeaders != null ? corsHeaders.get("Access-Control-Max-Age") : null; 490 if (maxAge == null) { 491 maxAge = RestConfiguration.CORS_ACCESS_CONTROL_MAX_AGE; 492 } 493 String allowCredentials = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Credentials") : null; 494 495 // Restrict the origin if credentials are allowed. 496 // https://www.w3.org/TR/cors/ - section 6.1, point 3 497 String origin = exchange.getIn().getHeader("Origin", String.class); 498 if ("true".equalsIgnoreCase(allowCredentials) && "*".equals(allowOrigin) && origin != null) { 499 allowOrigin = origin; 500 } 501 502 msg.setHeader("Access-Control-Allow-Origin", allowOrigin); 503 msg.setHeader("Access-Control-Allow-Methods", allowMethods); 504 msg.setHeader("Access-Control-Allow-Headers", allowHeaders); 505 msg.setHeader("Access-Control-Max-Age", maxAge); 506 if (allowCredentials != null) { 507 msg.setHeader("Access-Control-Allow-Credentials", allowCredentials); 508 } 509 } 510 511 private static boolean isValidOrAcceptedContentType(String valid, String target) { 512 if (valid == null || target == null) { 513 return true; 514 } 515 516 // Any MIME type 517 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept#Directives 518 if ("*/*".equals(target)) { 519 return true; 520 } 521 522 boolean isXml = valid.toLowerCase(Locale.ENGLISH).contains("xml"); 523 boolean isJson = valid.toLowerCase(Locale.ENGLISH).contains("json"); 524 525 String type = target.toLowerCase(Locale.ENGLISH); 526 527 if (isXml && !type.contains("xml")) { 528 return false; 529 } 530 if (isJson && !type.contains("json")) { 531 return false; 532 } 533 534 return true; 535 } 536 537}