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