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