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.model.rest; 018 019import java.util.HashMap; 020import java.util.Map; 021 022import javax.xml.bind.JAXBContext; 023import javax.xml.bind.annotation.XmlAccessType; 024import javax.xml.bind.annotation.XmlAccessorType; 025import javax.xml.bind.annotation.XmlAttribute; 026import javax.xml.bind.annotation.XmlRootElement; 027 028import org.apache.camel.CamelContext; 029import org.apache.camel.Processor; 030import org.apache.camel.model.NoOutputDefinition; 031import org.apache.camel.processor.binding.RestBindingProcessor; 032import org.apache.camel.spi.DataFormat; 033import org.apache.camel.spi.Metadata; 034import org.apache.camel.spi.RestConfiguration; 035import org.apache.camel.spi.RouteContext; 036import org.apache.camel.util.IntrospectionSupport; 037 038/** 039 * To configure rest binding 040 */ 041@Metadata(label = "rest") 042@XmlRootElement(name = "restBinding") 043@XmlAccessorType(XmlAccessType.FIELD) 044public class RestBindingDefinition extends NoOutputDefinition<RestBindingDefinition> { 045 046 @XmlAttribute 047 private String consumes; 048 049 @XmlAttribute 050 private String produces; 051 052 @XmlAttribute @Metadata(defaultValue = "auto") 053 private RestBindingMode bindingMode; 054 055 @XmlAttribute 056 private String type; 057 058 @XmlAttribute 059 private String outType; 060 061 @XmlAttribute 062 private Boolean skipBindingOnErrorCode; 063 064 @XmlAttribute 065 private Boolean enableCORS; 066 067 @XmlAttribute 068 private String component; 069 070 public RestBindingDefinition() { 071 } 072 073 @Override 074 public String toString() { 075 return "RestBinding"; 076 } 077 078 @Override 079 public Processor createProcessor(RouteContext routeContext) throws Exception { 080 081 CamelContext context = routeContext.getCamelContext(); 082 RestConfiguration config = context.getRestConfiguration(component, true); 083 084 // these options can be overriden per rest verb 085 String mode = config.getBindingMode().name(); 086 if (bindingMode != null) { 087 mode = bindingMode.name(); 088 } 089 boolean cors = config.isEnableCORS(); 090 if (enableCORS != null) { 091 cors = enableCORS; 092 } 093 boolean skip = config.isSkipBindingOnErrorCode(); 094 if (skipBindingOnErrorCode != null) { 095 skip = skipBindingOnErrorCode; 096 } 097 098 // cors headers 099 Map<String, String> corsHeaders = config.getCorsHeaders(); 100 101 if (mode == null || "off".equals(mode)) { 102 // binding mode is off, so create a off mode binding processor 103 return new RestBindingProcessor(context, null, null, null, null, consumes, produces, mode, skip, cors, corsHeaders); 104 } 105 106 // setup json data format 107 String name = config.getJsonDataFormat(); 108 if (name != null) { 109 // must only be a name, not refer to an existing instance 110 Object instance = context.getRegistry().lookupByName(name); 111 if (instance != null) { 112 throw new IllegalArgumentException("JsonDataFormat name: " + name + " must not be an existing bean instance from the registry"); 113 } 114 } else { 115 name = "json-jackson"; 116 } 117 // this will create a new instance as the name was not already pre-created 118 DataFormat json = context.resolveDataFormat(name); 119 DataFormat outJson = context.resolveDataFormat(name); 120 121 // is json binding required? 122 if (mode.contains("json") && json == null) { 123 throw new IllegalArgumentException("JSon DataFormat " + name + " not found."); 124 } 125 126 if (json != null) { 127 Class<?> clazz = null; 128 if (type != null) { 129 String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type; 130 clazz = context.getClassResolver().resolveMandatoryClass(typeName); 131 } 132 if (clazz != null) { 133 IntrospectionSupport.setProperty(context.getTypeConverter(), json, "unmarshalType", clazz); 134 IntrospectionSupport.setProperty(context.getTypeConverter(), json, "useList", type.endsWith("[]")); 135 } 136 setAdditionalConfiguration(config, context, json, "json.in."); 137 138 Class<?> outClazz = null; 139 if (outType != null) { 140 String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType; 141 outClazz = context.getClassResolver().resolveMandatoryClass(typeName); 142 } 143 if (outClazz != null) { 144 IntrospectionSupport.setProperty(context.getTypeConverter(), outJson, "unmarshalType", outClazz); 145 IntrospectionSupport.setProperty(context.getTypeConverter(), outJson, "useList", outType.endsWith("[]")); 146 } 147 setAdditionalConfiguration(config, context, outJson, "json.out."); 148 } 149 150 // setup xml data format 151 name = config.getXmlDataFormat(); 152 if (name != null) { 153 // must only be a name, not refer to an existing instance 154 Object instance = context.getRegistry().lookupByName(name); 155 if (instance != null) { 156 throw new IllegalArgumentException("XmlDataFormat name: " + name + " must not be an existing bean instance from the registry"); 157 } 158 } else { 159 name = "jaxb"; 160 } 161 // this will create a new instance as the name was not already pre-created 162 DataFormat jaxb = context.resolveDataFormat(name); 163 DataFormat outJaxb = context.resolveDataFormat(name); 164 165 // is xml binding required? 166 if (mode.contains("xml") && jaxb == null) { 167 throw new IllegalArgumentException("XML DataFormat " + name + " not found."); 168 } 169 170 if (jaxb != null) { 171 Class<?> clazz = null; 172 if (type != null) { 173 String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type; 174 clazz = context.getClassResolver().resolveMandatoryClass(typeName); 175 } 176 if (clazz != null) { 177 JAXBContext jc = JAXBContext.newInstance(clazz); 178 IntrospectionSupport.setProperty(context.getTypeConverter(), jaxb, "context", jc); 179 } 180 setAdditionalConfiguration(config, context, jaxb, "xml.in."); 181 182 Class<?> outClazz = null; 183 if (outType != null) { 184 String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType; 185 outClazz = context.getClassResolver().resolveMandatoryClass(typeName); 186 } 187 if (outClazz != null) { 188 JAXBContext jc = JAXBContext.newInstance(outClazz); 189 IntrospectionSupport.setProperty(context.getTypeConverter(), outJaxb, "context", jc); 190 } else if (clazz != null) { 191 // fallback and use the context from the input 192 JAXBContext jc = JAXBContext.newInstance(clazz); 193 IntrospectionSupport.setProperty(context.getTypeConverter(), outJaxb, "context", jc); 194 } 195 setAdditionalConfiguration(config, context, outJaxb, "xml.out."); 196 } 197 198 return new RestBindingProcessor(context, json, jaxb, outJson, outJaxb, consumes, produces, mode, skip, cors, corsHeaders); 199 } 200 201 private void setAdditionalConfiguration(RestConfiguration config, CamelContext context, 202 DataFormat dataFormat, String prefix) throws Exception { 203 if (config.getDataFormatProperties() != null && !config.getDataFormatProperties().isEmpty()) { 204 // must use a copy as otherwise the options gets removed during introspection setProperties 205 Map<String, Object> copy = new HashMap<String, Object>(); 206 207 // filter keys on prefix 208 // - either its a known prefix and must match the prefix parameter 209 // - or its a common configuration that we should always use 210 for (Map.Entry<String, Object> entry : config.getDataFormatProperties().entrySet()) { 211 String key = entry.getKey(); 212 String copyKey; 213 boolean known = isKeyKnownPrefix(key); 214 if (known) { 215 // remove the prefix from the key to use 216 copyKey = key.substring(prefix.length()); 217 } else { 218 // use the key as is 219 copyKey = key; 220 } 221 if (!known || key.startsWith(prefix)) { 222 copy.put(copyKey, entry.getValue()); 223 } 224 } 225 226 IntrospectionSupport.setProperties(context.getTypeConverter(), dataFormat, copy); 227 } 228 } 229 230 private boolean isKeyKnownPrefix(String key) { 231 return key.startsWith("json.in.") || key.startsWith("json.out.") || key.startsWith("xml.in.") || key.startsWith("xml.out."); 232 } 233 234 public String getConsumes() { 235 return consumes; 236 } 237 238 /** 239 * Sets the component name that this definition will apply to 240 */ 241 public void setComponent(String component) { 242 this.component = component; 243 } 244 public String getComponent() { 245 return component; 246 } 247 248 /** 249 * To define the content type what the REST service consumes (accept as input), such as application/xml or application/json 250 */ 251 public void setConsumes(String consumes) { 252 this.consumes = consumes; 253 } 254 255 public String getProduces() { 256 return produces; 257 } 258 259 /** 260 * To define the content type what the REST service produces (uses for output), such as application/xml or application/json 261 */ 262 public void setProduces(String produces) { 263 this.produces = produces; 264 } 265 266 public RestBindingMode getBindingMode() { 267 return bindingMode; 268 } 269 270 /** 271 * Sets the binding mode to use. 272 * <p/> 273 * The default value is auto 274 */ 275 public void setBindingMode(RestBindingMode bindingMode) { 276 this.bindingMode = bindingMode; 277 } 278 279 public String getType() { 280 return type; 281 } 282 283 /** 284 * Sets the class name to use for binding from input to POJO for the incoming data 285 */ 286 public void setType(String type) { 287 this.type = type; 288 } 289 290 public String getOutType() { 291 return outType; 292 } 293 294 /** 295 * Sets the class name to use for binding from POJO to output for the outgoing data 296 */ 297 public void setOutType(String outType) { 298 this.outType = outType; 299 } 300 301 public Boolean getSkipBindingOnErrorCode() { 302 return skipBindingOnErrorCode; 303 } 304 305 /** 306 * Whether to skip binding on output if there is a custom HTTP error code header. 307 * This allows to build custom error messages that do not bind to json / xml etc, as success messages otherwise will do. 308 */ 309 public void setSkipBindingOnErrorCode(Boolean skipBindingOnErrorCode) { 310 this.skipBindingOnErrorCode = skipBindingOnErrorCode; 311 } 312 313 public Boolean getEnableCORS() { 314 return enableCORS; 315 } 316 317 /** 318 * Whether to enable CORS headers in the HTTP response. 319 * <p/> 320 * The default value is false. 321 */ 322 public void setEnableCORS(Boolean enableCORS) { 323 this.enableCORS = enableCORS; 324 } 325}