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.component.rest; 018 019import java.util.Locale; 020import java.util.Map; 021 022import org.apache.camel.AsyncCallback; 023import org.apache.camel.AsyncProcessor; 024import org.apache.camel.CamelContext; 025import org.apache.camel.CamelContextAware; 026import org.apache.camel.Exchange; 027import org.apache.camel.Message; 028import org.apache.camel.Route; 029import org.apache.camel.processor.MarshalProcessor; 030import org.apache.camel.processor.UnmarshalProcessor; 031import org.apache.camel.processor.binding.BindingException; 032import org.apache.camel.spi.DataFormat; 033import org.apache.camel.spi.RestConfiguration; 034import org.apache.camel.support.ServiceSupport; 035import org.apache.camel.support.SynchronizationAdapter; 036import org.apache.camel.util.AsyncProcessorHelper; 037import org.apache.camel.util.ExchangeHelper; 038import org.apache.camel.util.MessageHelper; 039import org.apache.camel.util.ObjectHelper; 040import org.apache.camel.util.ServiceHelper; 041 042/** 043 * A {@link org.apache.camel.Processor} that binds the REST DSL incoming and outgoing messages 044 * from sources of json or xml to Java Objects. 045 * <p/> 046 * The binding uses {@link org.apache.camel.spi.DataFormat} for the actual work to transform 047 * from xml/json to Java Objects and reverse again. 048 */ 049public class RestConsumerBindingProcessor extends ServiceSupport implements AsyncProcessor { 050 051 private final CamelContext camelContext; 052 private final AsyncProcessor jsonUnmarshal; 053 private final AsyncProcessor xmlUnmarshal; 054 private final AsyncProcessor jsonMarshal; 055 private final AsyncProcessor xmlMarshal; 056 private final String consumes; 057 private final String produces; 058 private final String bindingMode; 059 private final boolean skipBindingOnErrorCode; 060 private final boolean enableCORS; 061 private final Map<String, String> corsHeaders; 062 private final Map<String, String> queryDefaultValues; 063 064 public RestConsumerBindingProcessor(CamelContext camelContext, DataFormat jsonDataFormat, DataFormat xmlDataFormat, 065 DataFormat outJsonDataFormat, DataFormat outXmlDataFormat, 066 String consumes, String produces, String bindingMode, 067 boolean skipBindingOnErrorCode, boolean enableCORS, 068 Map<String, String> corsHeaders, 069 Map<String, String> queryDefaultValues) { 070 071 this.camelContext = camelContext; 072 073 if (jsonDataFormat != null) { 074 this.jsonUnmarshal = new UnmarshalProcessor(jsonDataFormat); 075 } else { 076 this.jsonUnmarshal = null; 077 } 078 if (outJsonDataFormat != null) { 079 this.jsonMarshal = new MarshalProcessor(outJsonDataFormat); 080 } else if (jsonDataFormat != null) { 081 this.jsonMarshal = new MarshalProcessor(jsonDataFormat); 082 } else { 083 this.jsonMarshal = null; 084 } 085 086 if (xmlDataFormat != null) { 087 this.xmlUnmarshal = new UnmarshalProcessor(xmlDataFormat); 088 } else { 089 this.xmlUnmarshal = null; 090 } 091 if (outXmlDataFormat != null) { 092 this.xmlMarshal = new MarshalProcessor(outXmlDataFormat); 093 } else if (xmlDataFormat != null) { 094 this.xmlMarshal = new MarshalProcessor(xmlDataFormat); 095 } else { 096 this.xmlMarshal = null; 097 } 098 099 this.consumes = consumes; 100 this.produces = produces; 101 this.bindingMode = bindingMode; 102 this.skipBindingOnErrorCode = skipBindingOnErrorCode; 103 this.enableCORS = enableCORS; 104 this.corsHeaders = corsHeaders; 105 this.queryDefaultValues = queryDefaultValues; 106 } 107 108 @Override 109 public void process(Exchange exchange) throws Exception { 110 AsyncProcessorHelper.process(this, exchange); 111 } 112 113 @Override 114 public boolean process(Exchange exchange, final AsyncCallback callback) { 115 if (enableCORS) { 116 exchange.addOnCompletion(new RestConsumerBindingCORSOnCompletion(corsHeaders)); 117 } 118 119 String method = exchange.getIn().getHeader(Exchange.HTTP_METHOD, String.class); 120 if ("OPTIONS".equalsIgnoreCase(method)) { 121 // for OPTIONS methods then we should not route at all as its part of CORS 122 exchange.setProperty(Exchange.ROUTE_STOP, true); 123 callback.done(true); 124 return true; 125 } 126 127 boolean isXml = false; 128 boolean isJson = false; 129 130 String contentType = ExchangeHelper.getContentType(exchange); 131 if (contentType != null) { 132 isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); 133 isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); 134 } 135 // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with 136 // that information in the consumes 137 if (!isXml && !isJson) { 138 isXml = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("xml"); 139 isJson = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("json"); 140 } 141 142 // only allow xml/json if the binding mode allows that 143 isXml &= bindingMode.equals("auto") || bindingMode.contains("xml"); 144 isJson &= bindingMode.equals("auto") || bindingMode.contains("json"); 145 146 // if we do not yet know if its xml or json, then use the binding mode to know the mode 147 if (!isJson && !isXml) { 148 isXml = bindingMode.equals("auto") || bindingMode.contains("xml"); 149 isJson = bindingMode.equals("auto") || bindingMode.contains("json"); 150 } 151 152 String accept = exchange.getIn().getHeader("Accept", String.class); 153 154 String body = null; 155 if (exchange.getIn().getBody() != null) { 156 157 // okay we have a binding mode, so need to check for empty body as that can cause the marshaller to fail 158 // as they assume a non-empty body 159 if (isXml || isJson) { 160 // we have binding enabled, so we need to know if there body is empty or not 161 // so force reading the body as a String which we can work with 162 body = MessageHelper.extractBodyAsString(exchange.getIn()); 163 if (body != null) { 164 exchange.getIn().setBody(body); 165 166 if (isXml && isJson) { 167 // we have still not determined between xml or json, so check the body if its xml based or not 168 isXml = body.startsWith("<"); 169 isJson = !isXml; 170 } 171 } 172 } 173 } 174 175 // add missing default values which are mapped as headers 176 if (queryDefaultValues != null) { 177 for (Map.Entry<String, String> entry : queryDefaultValues.entrySet()) { 178 if (exchange.getIn().getHeader(entry.getKey()) == null) { 179 exchange.getIn().setHeader(entry.getKey(), entry.getValue()); 180 } 181 } 182 } 183 184 // favor json over xml 185 if (isJson && jsonUnmarshal != null) { 186 // add reverse operation 187 exchange.addOnCompletion(new RestConsumerBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, false, accept)); 188 if (ObjectHelper.isNotEmpty(body)) { 189 return jsonUnmarshal.process(exchange, callback); 190 } else { 191 callback.done(true); 192 return true; 193 } 194 } else if (isXml && xmlUnmarshal != null) { 195 // add reverse operation 196 exchange.addOnCompletion(new RestConsumerBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, true, accept)); 197 if (ObjectHelper.isNotEmpty(body)) { 198 return xmlUnmarshal.process(exchange, callback); 199 } else { 200 callback.done(true); 201 return true; 202 } 203 } 204 205 // we could not bind 206 if ("off".equals(bindingMode) || bindingMode.equals("auto")) { 207 // okay for auto we do not mind if we could not bind 208 exchange.addOnCompletion(new RestConsumerBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, false, accept)); 209 callback.done(true); 210 return true; 211 } else { 212 if (bindingMode.contains("xml")) { 213 exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange)); 214 } else { 215 exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange)); 216 } 217 callback.done(true); 218 return true; 219 } 220 } 221 222 @Override 223 public String toString() { 224 return "RestConsumerBindingProcessor"; 225 } 226 227 @Override 228 protected void doStart() throws Exception { 229 // inject CamelContext before starting 230 if (jsonMarshal instanceof CamelContextAware) { 231 ((CamelContextAware) jsonMarshal).setCamelContext(camelContext); 232 } 233 if (jsonUnmarshal instanceof CamelContextAware) { 234 ((CamelContextAware) jsonUnmarshal).setCamelContext(camelContext); 235 } 236 if (xmlMarshal instanceof CamelContextAware) { 237 ((CamelContextAware) xmlMarshal).setCamelContext(camelContext); 238 } 239 if (xmlUnmarshal instanceof CamelContextAware) { 240 ((CamelContextAware) xmlUnmarshal).setCamelContext(camelContext); 241 } 242 ServiceHelper.startServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal); 243 } 244 245 @Override 246 protected void doStop() throws Exception { 247 ServiceHelper.stopServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal); 248 } 249 250 /** 251 * An {@link org.apache.camel.spi.Synchronization} that does the reverse operation 252 * of marshalling from POJO to json/xml 253 */ 254 private final class RestConsumerBindingMarshalOnCompletion extends SynchronizationAdapter { 255 256 private final AsyncProcessor jsonMarshal; 257 private final AsyncProcessor xmlMarshal; 258 private final String routeId; 259 private boolean wasXml; 260 private String accept; 261 262 private RestConsumerBindingMarshalOnCompletion(String routeId, AsyncProcessor jsonMarshal, AsyncProcessor xmlMarshal, boolean wasXml, String accept) { 263 this.routeId = routeId; 264 this.jsonMarshal = jsonMarshal; 265 this.xmlMarshal = xmlMarshal; 266 this.wasXml = wasXml; 267 this.accept = accept; 268 } 269 270 @Override 271 public void onAfterRoute(Route route, Exchange exchange) { 272 // we use the onAfterRoute callback, to ensure the data has been marshalled before 273 // the consumer writes the response back 274 275 // only trigger when it was the 1st route that was done 276 if (!routeId.equals(route.getId())) { 277 return; 278 } 279 280 // only marshal if there was no exception 281 if (exchange.getException() != null) { 282 return; 283 } 284 285 if (skipBindingOnErrorCode) { 286 Integer code = exchange.hasOut() ? exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class) : exchange.getIn().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class); 287 // if there is a custom http error code then skip binding 288 if (code != null && code >= 300) { 289 return; 290 } 291 } 292 293 boolean isXml = false; 294 boolean isJson = false; 295 296 // accept takes precedence 297 if (accept != null) { 298 isXml = accept.toLowerCase(Locale.ENGLISH).contains("xml"); 299 isJson = accept.toLowerCase(Locale.ENGLISH).contains("json"); 300 } 301 // fallback to content type if still undecided 302 if (!isXml && !isJson) { 303 String contentType = ExchangeHelper.getContentType(exchange); 304 if (contentType != null) { 305 isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); 306 isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); 307 } 308 } 309 // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with 310 // that information in the consumes 311 if (!isXml && !isJson) { 312 isXml = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("xml"); 313 isJson = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("json"); 314 } 315 316 // only allow xml/json if the binding mode allows that (when off we still want to know if its xml or json) 317 if (bindingMode != null) { 318 isXml &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("xml"); 319 isJson &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("json"); 320 321 // if we do not yet know if its xml or json, then use the binding mode to know the mode 322 if (!isJson && !isXml) { 323 isXml = bindingMode.equals("auto") || bindingMode.contains("xml"); 324 isJson = bindingMode.equals("auto") || bindingMode.contains("json"); 325 } 326 } 327 328 // in case we have not yet been able to determine if xml or json, then use the same as in the unmarshaller 329 if (isXml && isJson) { 330 isXml = wasXml; 331 isJson = !wasXml; 332 } 333 334 // need to prepare exchange first 335 ExchangeHelper.prepareOutToIn(exchange); 336 337 // ensure there is a content type header (even if binding is off) 338 ensureHeaderContentType(produces, isXml, isJson, exchange); 339 340 if (bindingMode == null || "off".equals(bindingMode)) { 341 // binding is off, so no message body binding 342 return; 343 } 344 345 // is there any marshaller at all 346 if (jsonMarshal == null && xmlMarshal == null) { 347 return; 348 } 349 350 // is the body empty 351 if ((exchange.hasOut() && exchange.getOut().getBody() == null) || (!exchange.hasOut() && exchange.getIn().getBody() == null)) { 352 return; 353 } 354 355 String contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class); 356 // need to lower-case so the contains check below can match if using upper case 357 contentType = contentType.toLowerCase(Locale.US); 358 try { 359 // favor json over xml 360 if (isJson && jsonMarshal != null) { 361 // only marshal if its json content type 362 if (contentType.contains("json")) { 363 jsonMarshal.process(exchange); 364 } 365 } else if (isXml && xmlMarshal != null) { 366 // only marshal if its xml content type 367 if (contentType.contains("xml")) { 368 xmlMarshal.process(exchange); 369 } 370 } else { 371 // we could not bind 372 if (bindingMode.equals("auto")) { 373 // okay for auto we do not mind if we could not bind 374 } else { 375 if (bindingMode.contains("xml")) { 376 exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange)); 377 } else { 378 exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange)); 379 } 380 } 381 } 382 } catch (Throwable e) { 383 exchange.setException(e); 384 } 385 } 386 387 private void ensureHeaderContentType(String contentType, boolean isXml, boolean isJson, Exchange exchange) { 388 // favor given content type 389 if (contentType != null) { 390 String type = ExchangeHelper.getContentType(exchange); 391 if (type == null) { 392 exchange.getIn().setHeader(Exchange.CONTENT_TYPE, contentType); 393 } 394 } 395 396 // favor json over xml 397 if (isJson) { 398 // make sure there is a content-type with json 399 String type = ExchangeHelper.getContentType(exchange); 400 if (type == null) { 401 exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json"); 402 } 403 } else if (isXml) { 404 // make sure there is a content-type with xml 405 String type = ExchangeHelper.getContentType(exchange); 406 if (type == null) { 407 exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/xml"); 408 } 409 } 410 } 411 412 @Override 413 public String toString() { 414 return "RestConsumerBindingMarshalOnCompletion"; 415 } 416 } 417 418 private final class RestConsumerBindingCORSOnCompletion extends SynchronizationAdapter { 419 420 private final Map<String, String> corsHeaders; 421 422 private RestConsumerBindingCORSOnCompletion(Map<String, String> corsHeaders) { 423 this.corsHeaders = corsHeaders; 424 } 425 426 @Override 427 public void onAfterRoute(Route route, Exchange exchange) { 428 // add the CORS headers after routing, but before the consumer writes the response 429 Message msg = exchange.hasOut() ? exchange.getOut() : exchange.getIn(); 430 431 // use default value if none has been configured 432 String allowOrigin = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Origin") : null; 433 if (allowOrigin == null) { 434 allowOrigin = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_ORIGIN; 435 } 436 String allowMethods = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Methods") : null; 437 if (allowMethods == null) { 438 allowMethods = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_METHODS; 439 } 440 String allowHeaders = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Headers") : null; 441 if (allowHeaders == null) { 442 allowHeaders = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_HEADERS; 443 } 444 String maxAge = corsHeaders != null ? corsHeaders.get("Access-Control-Max-Age") : null; 445 if (maxAge == null) { 446 maxAge = RestConfiguration.CORS_ACCESS_CONTROL_MAX_AGE; 447 } 448 449 msg.setHeader("Access-Control-Allow-Origin", allowOrigin); 450 msg.setHeader("Access-Control-Allow-Methods", allowMethods); 451 msg.setHeader("Access-Control-Allow-Headers", allowHeaders); 452 msg.setHeader("Access-Control-Max-Age", maxAge); 453 } 454 455 @Override 456 public String toString() { 457 return "RestConsumerBindingCORSOnCompletion"; 458 } 459 } 460 461}