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