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