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