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