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