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