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