| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.camel.processor; |
| |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.camel.AsyncProcessor; |
| import org.apache.camel.CamelContext; |
| import org.apache.camel.CamelContextAware; |
| import org.apache.camel.CamelExchangeException; |
| import org.apache.camel.Exchange; |
| import org.apache.camel.Message; |
| import org.apache.camel.spi.CamelInternalProcessorAdvice; |
| import org.apache.camel.spi.DataFormat; |
| import org.apache.camel.spi.DataType; |
| import org.apache.camel.spi.DataTypeAware; |
| import org.apache.camel.spi.RestConfiguration; |
| import org.apache.camel.support.ExchangeHelper; |
| import org.apache.camel.support.MessageHelper; |
| import org.apache.camel.support.processor.MarshalProcessor; |
| import org.apache.camel.support.processor.UnmarshalProcessor; |
| import org.apache.camel.util.ObjectHelper; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * A {@link CamelInternalProcessorAdvice} that binds the REST DSL incoming |
| * and outgoing messages from sources of json or xml to Java Objects. |
| * <p/> |
| * The binding uses {@link org.apache.camel.spi.DataFormat} for the actual work to transform |
| * from xml/json to Java Objects and reverse again. |
| * <p/> |
| * The rest producer side is implemented in {@link org.apache.camel.component.rest.RestProducerBindingProcessor} |
| * |
| * @see CamelInternalProcessor |
| */ |
| public class RestBindingAdvice implements CamelInternalProcessorAdvice<Map<String, Object>> { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(RestBindingAdvice.class); |
| private static final String STATE_KEY_DO_MARSHAL = "doMarshal"; |
| private static final String STATE_KEY_ACCEPT = "accept"; |
| private static final String STATE_JSON = "json"; |
| private static final String STATE_XML = "xml"; |
| |
| private final AsyncProcessor jsonUnmarshal; |
| private final AsyncProcessor xmlUnmarshal; |
| private final AsyncProcessor jsonMarshal; |
| private final AsyncProcessor xmlMarshal; |
| private final String consumes; |
| private final String produces; |
| private final String bindingMode; |
| private final boolean skipBindingOnErrorCode; |
| private final boolean clientRequestValidation; |
| private final boolean enableCORS; |
| private final Map<String, String> corsHeaders; |
| private final Map<String, String> queryDefaultValues; |
| private final boolean requiredBody; |
| private final Set<String> requiredQueryParameters; |
| private final Set<String> requiredHeaders; |
| |
| public RestBindingAdvice(CamelContext camelContext, DataFormat jsonDataFormat, DataFormat xmlDataFormat, |
| DataFormat outJsonDataFormat, DataFormat outXmlDataFormat, |
| String consumes, String produces, String bindingMode, |
| boolean skipBindingOnErrorCode, boolean clientRequestValidation, boolean enableCORS, |
| Map<String, String> corsHeaders, |
| Map<String, String> queryDefaultValues, |
| boolean requiredBody, Set<String> requiredQueryParameters, Set<String> requiredHeaders) throws Exception { |
| |
| if (jsonDataFormat != null) { |
| this.jsonUnmarshal = new UnmarshalProcessor(jsonDataFormat); |
| } else { |
| this.jsonUnmarshal = null; |
| } |
| if (outJsonDataFormat != null) { |
| this.jsonMarshal = new MarshalProcessor(outJsonDataFormat); |
| } else if (jsonDataFormat != null) { |
| this.jsonMarshal = new MarshalProcessor(jsonDataFormat); |
| } else { |
| this.jsonMarshal = null; |
| } |
| |
| if (xmlDataFormat != null) { |
| this.xmlUnmarshal = new UnmarshalProcessor(xmlDataFormat); |
| } else { |
| this.xmlUnmarshal = null; |
| } |
| if (outXmlDataFormat != null) { |
| this.xmlMarshal = new MarshalProcessor(outXmlDataFormat); |
| } else if (xmlDataFormat != null) { |
| this.xmlMarshal = new MarshalProcessor(xmlDataFormat); |
| } else { |
| this.xmlMarshal = null; |
| } |
| |
| if (jsonMarshal != null) { |
| camelContext.addService(jsonMarshal, true); |
| } |
| if (jsonUnmarshal != null) { |
| camelContext.addService(jsonUnmarshal, true); |
| } |
| if (xmlMarshal instanceof CamelContextAware) { |
| camelContext.addService(xmlMarshal, true); |
| } |
| if (xmlUnmarshal instanceof CamelContextAware) { |
| camelContext.addService(xmlUnmarshal, true); |
| } |
| |
| this.consumes = consumes; |
| this.produces = produces; |
| this.bindingMode = bindingMode; |
| this.skipBindingOnErrorCode = skipBindingOnErrorCode; |
| this.clientRequestValidation = clientRequestValidation; |
| this.enableCORS = enableCORS; |
| this.corsHeaders = corsHeaders; |
| this.queryDefaultValues = queryDefaultValues; |
| this.requiredBody = requiredBody; |
| this.requiredQueryParameters = requiredQueryParameters; |
| this.requiredHeaders = requiredHeaders; |
| } |
| |
| @Override |
| public Map<String, Object> before(Exchange exchange) throws Exception { |
| Map<String, Object> state = new HashMap<>(); |
| if (isOptionsMethod(exchange, state)) { |
| return state; |
| } |
| unmarshal(exchange, state); |
| return state; |
| } |
| |
| @Override |
| public void after(Exchange exchange, Map<String, Object> state) throws Exception { |
| if (enableCORS) { |
| setCORSHeaders(exchange, state); |
| } |
| if (state.get(STATE_KEY_DO_MARSHAL) != null) { |
| marshal(exchange, state); |
| } |
| } |
| |
| private boolean isOptionsMethod(Exchange exchange, Map<String, Object> state) { |
| String method = exchange.getIn().getHeader(Exchange.HTTP_METHOD, String.class); |
| if ("OPTIONS".equalsIgnoreCase(method)) { |
| // for OPTIONS methods then we should not route at all as its part of CORS |
| exchange.setProperty(Exchange.ROUTE_STOP, true); |
| return true; |
| } |
| return false; |
| } |
| |
| private void unmarshal(Exchange exchange, Map<String, Object> state) throws Exception { |
| boolean isXml = false; |
| boolean isJson = false; |
| |
| String contentType = ExchangeHelper.getContentType(exchange); |
| if (contentType != null) { |
| isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); |
| isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); |
| } |
| // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with |
| // that information in the consumes |
| if (!isXml && !isJson) { |
| isXml = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("xml"); |
| isJson = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("json"); |
| } |
| |
| // set data type if in use |
| if (exchange.getContext().isUseDataType()) { |
| if (exchange.getIn() instanceof DataTypeAware && (isJson || isXml)) { |
| ((DataTypeAware) exchange.getIn()).setDataType(new DataType(isJson ? "json" : "xml")); |
| } |
| } |
| |
| // only allow xml/json if the binding mode allows that |
| isXml &= bindingMode.equals("auto") || bindingMode.contains("xml"); |
| isJson &= bindingMode.equals("auto") || bindingMode.contains("json"); |
| |
| // if we do not yet know if its xml or json, then use the binding mode to know the mode |
| if (!isJson && !isXml) { |
| isXml = bindingMode.equals("auto") || bindingMode.contains("xml"); |
| isJson = bindingMode.equals("auto") || bindingMode.contains("json"); |
| } |
| |
| String accept = exchange.getMessage().getHeader("Accept", String.class); |
| state.put(STATE_KEY_ACCEPT, accept); |
| |
| // perform client request validation |
| if (clientRequestValidation) { |
| // check if the content-type is accepted according to consumes |
| if (!isValidOrAcceptedContentType(consumes, contentType)) { |
| LOG.trace("Consuming content type does not match contentType header {}. Stopping routing.", contentType); |
| // the content-type is not something we can process so its a HTTP_ERROR 415 |
| exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 415); |
| // set empty response body as http error code indicate the problem |
| exchange.getMessage().setBody(null); |
| // stop routing and return |
| exchange.setProperty(Exchange.ROUTE_STOP, true); |
| return; |
| } |
| |
| // check if what is produces is accepted by the client |
| if (!isValidOrAcceptedContentType(produces, accept)) { |
| LOG.trace("Produced content type does not match accept header {}. Stopping routing.", contentType); |
| // the response type is not accepted by the client so its a HTTP_ERROR 406 |
| exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 406); |
| // set empty response body as http error code indicate the problem |
| exchange.getMessage().setBody(null); |
| // stop routing and return |
| exchange.setProperty(Exchange.ROUTE_STOP, true); |
| return; |
| } |
| } |
| |
| String body = null; |
| if (exchange.getIn().getBody() != null) { |
| |
| // okay we have a binding mode, so need to check for empty body as that can cause the marshaller to fail |
| // as they assume a non-empty body |
| if (isXml || isJson) { |
| // we have binding enabled, so we need to know if there body is empty or not |
| // so force reading the body as a String which we can work with |
| body = MessageHelper.extractBodyAsString(exchange.getIn()); |
| if (body != null) { |
| if (exchange.getIn() instanceof DataTypeAware) { |
| ((DataTypeAware)exchange.getIn()).setBody(body, new DataType(isJson ? "json" : "xml")); |
| } else { |
| exchange.getIn().setBody(body); |
| } |
| |
| if (isXml && isJson) { |
| // we have still not determined between xml or json, so check the body if its xml based or not |
| isXml = body.startsWith("<"); |
| isJson = !isXml; |
| } |
| } |
| } |
| } |
| |
| // add missing default values which are mapped as headers |
| if (queryDefaultValues != null) { |
| for (Map.Entry<String, String> entry : queryDefaultValues.entrySet()) { |
| if (exchange.getIn().getHeader(entry.getKey()) == null) { |
| exchange.getIn().setHeader(entry.getKey(), entry.getValue()); |
| } |
| } |
| } |
| |
| // check for required |
| if (clientRequestValidation) { |
| if (requiredBody) { |
| // the body is required so we need to know if we have a body or not |
| // so force reading the body as a String which we can work with |
| if (body == null) { |
| body = MessageHelper.extractBodyAsString(exchange.getIn()); |
| if (body != null) { |
| exchange.getIn().setBody(body); |
| } |
| } |
| if (body == null) { |
| // this is a bad request, the client did not include a message body |
| exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); |
| exchange.getMessage().setBody("The request body is missing."); |
| // stop routing and return |
| exchange.setProperty(Exchange.ROUTE_STOP, true); |
| return; |
| } |
| } |
| if (requiredQueryParameters != null && !exchange.getIn().getHeaders().keySet().containsAll(requiredQueryParameters)) { |
| // this is a bad request, the client did not include some of the required query parameters |
| exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); |
| exchange.getMessage().setBody("Some of the required query parameters are missing."); |
| // stop routing and return |
| exchange.setProperty(Exchange.ROUTE_STOP, true); |
| return; |
| } |
| if (requiredHeaders != null && !exchange.getIn().getHeaders().keySet().containsAll(requiredHeaders)) { |
| // this is a bad request, the client did not include some of the required http headers |
| exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); |
| exchange.getMessage().setBody("Some of the required HTTP headers are missing."); |
| // stop routing and return |
| exchange.setProperty(Exchange.ROUTE_STOP, true); |
| return; |
| } |
| } |
| |
| // favor json over xml |
| if (isJson && jsonUnmarshal != null) { |
| // add reverse operation |
| state.put(STATE_KEY_DO_MARSHAL, STATE_JSON); |
| if (ObjectHelper.isNotEmpty(body)) { |
| jsonUnmarshal.process(exchange); |
| ExchangeHelper.prepareOutToIn(exchange); |
| } |
| return; |
| } else if (isXml && xmlUnmarshal != null) { |
| // add reverse operation |
| state.put(STATE_KEY_DO_MARSHAL, STATE_XML); |
| if (ObjectHelper.isNotEmpty(body)) { |
| xmlUnmarshal.process(exchange); |
| ExchangeHelper.prepareOutToIn(exchange); |
| } |
| return; |
| } |
| |
| // we could not bind |
| if ("off".equals(bindingMode) || bindingMode.equals("auto")) { |
| // okay for auto we do not mind if we could not bind |
| state.put(STATE_KEY_DO_MARSHAL, STATE_JSON); |
| } else { |
| if (bindingMode.contains("xml")) { |
| exchange.setException(new CamelExchangeException("Cannot bind to xml as message body is not xml compatible", exchange)); |
| } else { |
| exchange.setException(new CamelExchangeException("Cannot bind to json as message body is not json compatible", exchange)); |
| } |
| } |
| |
| } |
| |
| private void marshal(Exchange exchange, Map<String, Object> state) { |
| // only marshal if there was no exception |
| if (exchange.getException() != null) { |
| return; |
| } |
| |
| if (skipBindingOnErrorCode) { |
| Integer code = exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class); |
| // if there is a custom http error code then skip binding |
| if (code != null && code >= 300) { |
| return; |
| } |
| } |
| |
| boolean isXml = false; |
| boolean isJson = false; |
| |
| // accept takes precedence |
| String accept = (String)state.get(STATE_KEY_ACCEPT); |
| if (accept != null) { |
| isXml = accept.toLowerCase(Locale.ENGLISH).contains("xml"); |
| isJson = accept.toLowerCase(Locale.ENGLISH).contains("json"); |
| } |
| // fallback to content type if still undecided |
| if (!isXml && !isJson) { |
| String contentType = ExchangeHelper.getContentType(exchange); |
| if (contentType != null) { |
| isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); |
| isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); |
| } |
| } |
| // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with |
| // that information in the consumes |
| if (!isXml && !isJson) { |
| isXml = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("xml"); |
| isJson = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("json"); |
| } |
| |
| // only allow xml/json if the binding mode allows that (when off we still want to know if its xml or json) |
| if (bindingMode != null) { |
| isXml &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("xml"); |
| isJson &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("json"); |
| |
| // if we do not yet know if its xml or json, then use the binding mode to know the mode |
| if (!isJson && !isXml) { |
| isXml = bindingMode.equals("auto") || bindingMode.contains("xml"); |
| isJson = bindingMode.equals("auto") || bindingMode.contains("json"); |
| } |
| } |
| |
| // in case we have not yet been able to determine if xml or json, then use the same as in the unmarshaller |
| if (isXml && isJson) { |
| isXml = state.get(STATE_KEY_DO_MARSHAL).equals(STATE_XML); |
| isJson = !isXml; |
| } |
| |
| // need to prepare exchange first |
| ExchangeHelper.prepareOutToIn(exchange); |
| |
| // ensure there is a content type header (even if binding is off) |
| ensureHeaderContentType(produces, isXml, isJson, exchange); |
| |
| if (bindingMode == null || "off".equals(bindingMode)) { |
| // binding is off, so no message body binding |
| return; |
| } |
| |
| // is there any marshaller at all |
| if (jsonMarshal == null && xmlMarshal == null) { |
| return; |
| } |
| |
| // is the body empty |
| if (exchange.getMessage().getBody() == null) { |
| return; |
| } |
| |
| String contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class); |
| // need to lower-case so the contains check below can match if using upper case |
| contentType = contentType.toLowerCase(Locale.US); |
| try { |
| // favor json over xml |
| if (isJson && jsonMarshal != null) { |
| // only marshal if its json content type |
| if (contentType.contains("json")) { |
| jsonMarshal.process(exchange); |
| setOutputDataType(exchange, new DataType("json")); |
| } |
| } else if (isXml && xmlMarshal != null) { |
| // only marshal if its xml content type |
| if (contentType.contains("xml")) { |
| xmlMarshal.process(exchange); |
| setOutputDataType(exchange, new DataType("xml")); |
| } |
| } else { |
| // we could not bind |
| if (bindingMode.equals("auto")) { |
| // okay for auto we do not mind if we could not bind |
| } else { |
| if (bindingMode.contains("xml")) { |
| exchange.setException(new CamelExchangeException("Cannot bind to xml as message body is not xml compatible", exchange)); |
| } else { |
| exchange.setException(new CamelExchangeException("Cannot bind to json as message body is not json compatible", exchange)); |
| } |
| } |
| } |
| } catch (Throwable e) { |
| exchange.setException(e); |
| } |
| } |
| |
| private void setOutputDataType(Exchange exchange, DataType type) { |
| Message target = exchange.getMessage(); |
| if (target instanceof DataTypeAware) { |
| ((DataTypeAware)target).setDataType(type); |
| } |
| } |
| |
| private void ensureHeaderContentType(String contentType, boolean isXml, boolean isJson, Exchange exchange) { |
| // favor given content type |
| if (contentType != null) { |
| String type = ExchangeHelper.getContentType(exchange); |
| if (type == null) { |
| exchange.getIn().setHeader(Exchange.CONTENT_TYPE, contentType); |
| } |
| } |
| |
| // favor json over xml |
| if (isJson) { |
| // make sure there is a content-type with json |
| String type = ExchangeHelper.getContentType(exchange); |
| if (type == null) { |
| exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json"); |
| } |
| } else if (isXml) { |
| // make sure there is a content-type with xml |
| String type = ExchangeHelper.getContentType(exchange); |
| if (type == null) { |
| exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/xml"); |
| } |
| } |
| } |
| |
| private void setCORSHeaders(Exchange exchange, Map<String, Object> state) { |
| // add the CORS headers after routing, but before the consumer writes the response |
| Message msg = exchange.getMessage(); |
| |
| // use default value if none has been configured |
| String allowOrigin = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Origin") : null; |
| if (allowOrigin == null) { |
| allowOrigin = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_ORIGIN; |
| } |
| String allowMethods = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Methods") : null; |
| if (allowMethods == null) { |
| allowMethods = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_METHODS; |
| } |
| String allowHeaders = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Headers") : null; |
| if (allowHeaders == null) { |
| allowHeaders = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_HEADERS; |
| } |
| String maxAge = corsHeaders != null ? corsHeaders.get("Access-Control-Max-Age") : null; |
| if (maxAge == null) { |
| maxAge = RestConfiguration.CORS_ACCESS_CONTROL_MAX_AGE; |
| } |
| String allowCredentials = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Credentials") : null; |
| |
| // Restrict the origin if credentials are allowed. |
| // https://www.w3.org/TR/cors/ - section 6.1, point 3 |
| String origin = exchange.getIn().getHeader("Origin", String.class); |
| if ("true".equalsIgnoreCase(allowCredentials) && "*".equals(allowOrigin) && origin != null) { |
| allowOrigin = origin; |
| } |
| |
| msg.setHeader("Access-Control-Allow-Origin", allowOrigin); |
| msg.setHeader("Access-Control-Allow-Methods", allowMethods); |
| msg.setHeader("Access-Control-Allow-Headers", allowHeaders); |
| msg.setHeader("Access-Control-Max-Age", maxAge); |
| if (allowCredentials != null) { |
| msg.setHeader("Access-Control-Allow-Credentials", allowCredentials); |
| } |
| } |
| |
| private static boolean isValidOrAcceptedContentType(String valid, String target) { |
| if (valid == null || target == null) { |
| return true; |
| } |
| |
| // Any MIME type |
| // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept#Directives |
| if ("*/*".equals(target)) { |
| return true; |
| } |
| |
| boolean isXml = valid.toLowerCase(Locale.ENGLISH).contains("xml"); |
| boolean isJson = valid.toLowerCase(Locale.ENGLISH).contains("json"); |
| |
| String type = target.toLowerCase(Locale.ENGLISH); |
| |
| if (isXml && !type.contains("xml")) { |
| return false; |
| } |
| if (isJson && !type.contains("json")) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } |