/**
* 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.model.rest;
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import org.apache.camel.CamelContext;
import org.apache.camel.model.OptionalIdentifiedDefinition;
import org.apache.camel.processor.RestBindingAdvice;
import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.spi.RouteContext;
import org.apache.camel.util.EndpointHelper;
import org.apache.camel.util.IntrospectionSupport;
/**
* To configure rest binding
*/
@Metadata(label = "rest")
@XmlRootElement(name = "restBinding")
@XmlAccessorType(XmlAccessType.FIELD)
public class RestBindingDefinition extends OptionalIdentifiedDefinition<RestBindingDefinition> {
@XmlTransient
private Map<String, String> defaultValues;
@XmlAttribute
private String consumes;
@XmlAttribute
private String produces;
@XmlAttribute
@Metadata(defaultValue = "off")
private RestBindingMode bindingMode;
@XmlAttribute
private String type;
@XmlAttribute
private String outType;
@XmlAttribute
private Boolean skipBindingOnErrorCode;
@XmlAttribute
private Boolean enableCORS;
@XmlAttribute
private String component;
public RestBindingDefinition() {
}
@Override
public String toString() {
return "RestBinding";
}
public RestBindingAdvice createRestBindingAdvice(RouteContext routeContext) throws Exception {
CamelContext context = routeContext.getCamelContext();
RestConfiguration config = context.getRestConfiguration(component, true);
// these options can be overridden per rest verb
String mode = config.getBindingMode().name();
if (bindingMode != null) {
mode = bindingMode.name();
}
boolean cors = config.isEnableCORS();
if (enableCORS != null) {
cors = enableCORS;
}
boolean skip = config.isSkipBindingOnErrorCode();
if (skipBindingOnErrorCode != null) {
skip = skipBindingOnErrorCode;
}
// cors headers
Map<String, String> corsHeaders = config.getCorsHeaders();
if (mode == null || "off".equals(mode)) {
// binding mode is off, so create a off mode binding processor
return new RestBindingAdvice(context, null, null, null, null, consumes, produces, mode, skip, cors, corsHeaders, defaultValues);
}
// setup json data format
String name = config.getJsonDataFormat();
if (name != null) {
// must only be a name, not refer to an existing instance
Object instance = context.getRegistry().lookupByName(name);
if (instance != null) {
throw new IllegalArgumentException("JsonDataFormat name: " + name + " must not be an existing bean instance from the registry");
}
} else {
name = "json-jackson";
}
// this will create a new instance as the name was not already pre-created
DataFormat json = context.resolveDataFormat(name);
DataFormat outJson = context.resolveDataFormat(name);
// is json binding required?
if (mode.contains("json") && json == null) {
throw new IllegalArgumentException("JSon DataFormat " + name + " not found.");
}
if (json != null) {
Class<?> clazz = null;
if (type != null) {
String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type;
clazz = context.getClassResolver().resolveMandatoryClass(typeName);
}
if (clazz != null) {
IntrospectionSupport.setProperty(context.getTypeConverter(), json, "unmarshalType", clazz);
IntrospectionSupport.setProperty(context.getTypeConverter(), json, "useList", type.endsWith("[]"));
}
setAdditionalConfiguration(config, context, json, "json.in.");
Class<?> outClazz = null;
if (outType != null) {
String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType;
outClazz = context.getClassResolver().resolveMandatoryClass(typeName);
}
if (outClazz != null) {
IntrospectionSupport.setProperty(context.getTypeConverter(), outJson, "unmarshalType", outClazz);
IntrospectionSupport.setProperty(context.getTypeConverter(), outJson, "useList", outType.endsWith("[]"));
}
setAdditionalConfiguration(config, context, outJson, "json.out.");
}
// setup xml data format
name = config.getXmlDataFormat();
if (name != null) {
// must only be a name, not refer to an existing instance
Object instance = context.getRegistry().lookupByName(name);
if (instance != null) {
throw new IllegalArgumentException("XmlDataFormat name: " + name + " must not be an existing bean instance from the registry");
}
} else {
name = "jaxb";
}
// this will create a new instance as the name was not already pre-created
DataFormat jaxb = context.resolveDataFormat(name);
DataFormat outJaxb = context.resolveDataFormat(name);
// is xml binding required?
if (mode.contains("xml") && jaxb == null) {
throw new IllegalArgumentException("XML DataFormat " + name + " not found.");
}
if (jaxb != null) {
Class<?> clazz = null;
if (type != null) {
String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type;
clazz = context.getClassResolver().resolveMandatoryClass(typeName);
}
if (clazz != null) {
JAXBContext jc = JAXBContext.newInstance(clazz);
IntrospectionSupport.setProperty(context.getTypeConverter(), jaxb, "context", jc);
}
setAdditionalConfiguration(config, context, jaxb, "xml.in.");
Class<?> outClazz = null;
if (outType != null) {
String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType;
outClazz = context.getClassResolver().resolveMandatoryClass(typeName);
}
if (outClazz != null) {
JAXBContext jc = JAXBContext.newInstance(outClazz);
IntrospectionSupport.setProperty(context.getTypeConverter(), outJaxb, "context", jc);
} else if (clazz != null) {
// fallback and use the context from the input
JAXBContext jc = JAXBContext.newInstance(clazz);
IntrospectionSupport.setProperty(context.getTypeConverter(), outJaxb, "context", jc);
}
setAdditionalConfiguration(config, context, outJaxb, "xml.out.");
}
return new RestBindingAdvice(context, json, jaxb, outJson, outJaxb, consumes, produces, mode, skip, cors, corsHeaders, defaultValues);
}
private void setAdditionalConfiguration(RestConfiguration config, CamelContext context,
DataFormat dataFormat, String prefix) throws Exception {
if (config.getDataFormatProperties() != null && !config.getDataFormatProperties().isEmpty()) {
// must use a copy as otherwise the options gets removed during introspection setProperties
Map<String, Object> copy = new HashMap<String, Object>();
// filter keys on prefix
// - either its a known prefix and must match the prefix parameter
// - or its a common configuration that we should always use
for (Map.Entry<String, Object> entry : config.getDataFormatProperties().entrySet()) {
String key = entry.getKey();
String copyKey;
boolean known = isKeyKnownPrefix(key);
if (known) {
// remove the prefix from the key to use
copyKey = key.substring(prefix.length());
} else {
// use the key as is
copyKey = key;
}
if (!known || key.startsWith(prefix)) {
copy.put(copyKey, entry.getValue());
}
}
// set reference properties first as they use # syntax that fools the regular properties setter
EndpointHelper.setReferenceProperties(context, dataFormat, copy);
EndpointHelper.setProperties(context, dataFormat, copy);
}
}
private boolean isKeyKnownPrefix(String key) {
return key.startsWith("json.in.") || key.startsWith("json.out.") || key.startsWith("xml.in.") || key.startsWith("xml.out.");
}
public String getConsumes() {
return consumes;
}
/**
* Adds a default value for the query parameter
*
* @param paramName query parameter name
* @param defaultValue the default value
*/
public void addDefaultValue(String paramName, String defaultValue) {
if (defaultValues == null) {
defaultValues = new HashMap<String, String>();
}
defaultValues.put(paramName, defaultValue);
}
/**
* Gets the registered default values for query parameters
*/
public Map<String, String> getDefaultValues() {
return defaultValues;
}
/**
* Sets the component name that this definition will apply to
*/
public void setComponent(String component) {
this.component = component;
}
public String getComponent() {
return component;
}
/**
* To define the content type what the REST service consumes (accept as input), such as application/xml or application/json
*/
public void setConsumes(String consumes) {
this.consumes = consumes;
}
public String getProduces() {
return produces;
}
/**
* To define the content type what the REST service produces (uses for output), such as application/xml or application/json
*/
public void setProduces(String produces) {
this.produces = produces;
}
public RestBindingMode getBindingMode() {
return bindingMode;
}
/**
* Sets the binding mode to use.
* <p/>
* The default value is off
*/
public void setBindingMode(RestBindingMode bindingMode) {
this.bindingMode = bindingMode;
}
public String getType() {
return type;
}
/**
* Sets the class name to use for binding from input to POJO for the incoming data
* <p/>
* The canonical name of the class of the input data. Append a [] to the end of the canonical name
* if you want the input to be an array type.
*/
public void setType(String type) {
this.type = type;
}
public String getOutType() {
return outType;
}
/**
* Sets the class name to use for binding from POJO to output for the outgoing data
* <p/>
* The canonical name of the class of the input data. Append a [] to the end of the canonical name
* if you want the input to be an array type.
*/
public void setOutType(String outType) {
this.outType = outType;
}
public Boolean getSkipBindingOnErrorCode() {
return skipBindingOnErrorCode;
}
/**
* Whether to skip binding on output if there is a custom HTTP error code header.
* This allows to build custom error messages that do not bind to json / xml etc, as success messages otherwise will do.
*/
public void setSkipBindingOnErrorCode(Boolean skipBindingOnErrorCode) {
this.skipBindingOnErrorCode = skipBindingOnErrorCode;
}
public Boolean getEnableCORS() {
return enableCORS;
}
/**
* Whether to enable CORS headers in the HTTP response.
* <p/>
* The default value is false.
*/
public void setEnableCORS(Boolean enableCORS) {
this.enableCORS = enableCORS;
}
@Override
public String getLabel() {
return "";
}
}