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.component.rest; 018 019import java.io.UnsupportedEncodingException; 020import java.net.URISyntaxException; 021import java.net.URLDecoder; 022import java.util.HashMap; 023import java.util.LinkedHashMap; 024import java.util.Locale; 025import java.util.Map; 026 027import javax.xml.bind.JAXBContext; 028 029import org.apache.camel.AsyncCallback; 030import org.apache.camel.AsyncProcessor; 031import org.apache.camel.CamelContext; 032import org.apache.camel.Endpoint; 033import org.apache.camel.Exchange; 034import org.apache.camel.Message; 035import org.apache.camel.Producer; 036import org.apache.camel.impl.DefaultAsyncProducer; 037import org.apache.camel.model.rest.RestBindingMode; 038import org.apache.camel.spi.DataFormat; 039import org.apache.camel.spi.RestConfiguration; 040import org.apache.camel.util.AsyncProcessorConverterHelper; 041import org.apache.camel.util.CollectionStringBuffer; 042import org.apache.camel.util.EndpointHelper; 043import org.apache.camel.util.FileUtil; 044import org.apache.camel.util.IntrospectionSupport; 045import org.apache.camel.util.ObjectHelper; 046import org.apache.camel.util.ServiceHelper; 047import org.apache.camel.util.URISupport; 048 049import static org.apache.camel.util.ObjectHelper.isEmpty; 050import static org.apache.camel.util.ObjectHelper.isNotEmpty; 051 052/** 053 * Rest producer for calling remote REST services. 054 */ 055public class RestProducer extends DefaultAsyncProducer { 056 057 private static final String ACCEPT = "Accept"; 058 private final CamelContext camelContext; 059 private final RestConfiguration configuration; 060 private boolean prepareUriTemplate = true; 061 private RestBindingMode bindingMode; 062 private Boolean skipBindingOnErrorCode; 063 private String type; 064 private String outType; 065 066 // the producer of the Camel component that is used as the HTTP client to call the REST service 067 private AsyncProcessor producer; 068 // if binding is enabled then this processor should be used to wrap the call with binding before/after 069 private AsyncProcessor binding; 070 071 public RestProducer(Endpoint endpoint, Producer producer, RestConfiguration configuration) { 072 super(endpoint); 073 this.camelContext = endpoint.getCamelContext(); 074 this.configuration = configuration; 075 this.producer = AsyncProcessorConverterHelper.convert(producer); 076 } 077 078 @Override 079 public boolean process(Exchange exchange, AsyncCallback callback) { 080 try { 081 prepareExchange(exchange); 082 if (binding != null) { 083 return binding.process(exchange, callback); 084 } else { 085 // no binding in use call the producer directly 086 return producer.process(exchange, callback); 087 } 088 } catch (Throwable e) { 089 exchange.setException(e); 090 callback.done(true); 091 return true; 092 } 093 } 094 095 @Override 096 public RestEndpoint getEndpoint() { 097 return (RestEndpoint) super.getEndpoint(); 098 } 099 100 public boolean isPrepareUriTemplate() { 101 return prepareUriTemplate; 102 } 103 104 /** 105 * Whether to prepare the uri template and replace {key} with values from the exchange, and set 106 * as {@link Exchange#HTTP_URI} header with the resolved uri to use instead of uri from endpoint. 107 */ 108 public void setPrepareUriTemplate(boolean prepareUriTemplate) { 109 this.prepareUriTemplate = prepareUriTemplate; 110 } 111 112 public RestBindingMode getBindingMode() { 113 return bindingMode; 114 } 115 116 public void setBindingMode(final RestBindingMode bindingMode) { 117 this.bindingMode = bindingMode; 118 } 119 120 public Boolean getSkipBindingOnErrorCode() { 121 return skipBindingOnErrorCode; 122 } 123 124 public void setSkipBindingOnErrorCode(Boolean skipBindingOnErrorCode) { 125 this.skipBindingOnErrorCode = skipBindingOnErrorCode; 126 } 127 128 public String getType() { 129 return type; 130 } 131 132 public void setType(String type) { 133 this.type = type; 134 } 135 136 public String getOutType() { 137 return outType; 138 } 139 140 public void setOutType(String outType) { 141 this.outType = outType; 142 } 143 144 protected void prepareExchange(Exchange exchange) throws Exception { 145 boolean hasPath = false; 146 147 // uri template with path parameters resolved 148 // uri template may be optional and the user have entered the uri template in the path instead 149 String resolvedUriTemplate = getEndpoint().getUriTemplate() != null ? getEndpoint().getUriTemplate() : getEndpoint().getPath(); 150 151 Message inMessage = exchange.getIn(); 152 if (prepareUriTemplate) { 153 if (resolvedUriTemplate.contains("{")) { 154 // resolve template and replace {key} with the values form the exchange 155 // each {} is a parameter (url templating) 156 String[] arr = resolvedUriTemplate.split("\\/"); 157 CollectionStringBuffer csb = new CollectionStringBuffer("/"); 158 for (String a : arr) { 159 if (a.startsWith("{") && a.endsWith("}")) { 160 String key = a.substring(1, a.length() - 1); 161 String value = inMessage.getHeader(key, String.class); 162 if (value != null) { 163 hasPath = true; 164 csb.append(value); 165 } else { 166 csb.append(a); 167 } 168 } else { 169 csb.append(a); 170 } 171 } 172 resolvedUriTemplate = csb.toString(); 173 } 174 } 175 176 // resolve uri parameters 177 String query = createQueryParameters(getEndpoint().getQueryParameters(), inMessage); 178 179 if (query != null) { 180 // the query parameters for the rest call to be used 181 inMessage.setHeader(Exchange.REST_HTTP_QUERY, query); 182 } 183 184 if (hasPath) { 185 String host = getEndpoint().getHost(); 186 String basePath = getEndpoint().getUriTemplate() != null ? getEndpoint().getPath() : null; 187 basePath = FileUtil.stripLeadingSeparator(basePath); 188 resolvedUriTemplate = FileUtil.stripLeadingSeparator(resolvedUriTemplate); 189 // if so us a header for the dynamic uri template so we reuse same endpoint but the header overrides the actual url to use 190 String overrideUri = host; 191 if (!ObjectHelper.isEmpty(basePath)) { 192 overrideUri += "/" + basePath; 193 } 194 if (!ObjectHelper.isEmpty(resolvedUriTemplate)) { 195 overrideUri += "/" + resolvedUriTemplate; 196 } 197 // the http uri for the rest call to be used 198 inMessage.setHeader(Exchange.REST_HTTP_URI, overrideUri); 199 200 // when chaining RestConsumer with RestProducer, the 201 // HTTP_PATH header will be present, we remove it here 202 // as the REST_HTTP_URI contains the full URI for the 203 // request and every other HTTP producer will concatenate 204 // REST_HTTP_URI with HTTP_PATH resulting in incorrect URIs 205 inMessage.removeHeader(Exchange.HTTP_PATH); 206 } 207 208 // method 209 String method = getEndpoint().getMethod(); 210 if (method != null) { 211 // the method should be in upper case 212 String upper = method.toUpperCase(Locale.US); 213 inMessage.setHeader(Exchange.HTTP_METHOD, upper); 214 } 215 216 final String produces = getEndpoint().getProduces(); 217 if (isEmpty(inMessage.getHeader(Exchange.CONTENT_TYPE)) && isNotEmpty(produces)) { 218 inMessage.setHeader(Exchange.CONTENT_TYPE, produces); 219 } 220 221 final String consumes = getEndpoint().getConsumes(); 222 if (isEmpty(inMessage.getHeader(ACCEPT)) && isNotEmpty(consumes)) { 223 inMessage.setHeader(ACCEPT, consumes); 224 } 225 } 226 227 @Override 228 protected void doStart() throws Exception { 229 super.doStart(); 230 231 // create binding processor (returns null if binding is not in use) 232 binding = createBindingProcessor(); 233 234 ServiceHelper.startServices(binding, producer); 235 } 236 237 @Override 238 protected void doStop() throws Exception { 239 super.doStop(); 240 ServiceHelper.stopServices(producer, binding); 241 } 242 243 protected AsyncProcessor createBindingProcessor() throws Exception { 244 245 // these options can be overridden per endpoint 246 String mode = configuration.getBindingMode().name(); 247 if (bindingMode != null) { 248 mode = bindingMode.name(); 249 } 250 boolean skip = configuration.isSkipBindingOnErrorCode(); 251 if (skipBindingOnErrorCode != null) { 252 skip = skipBindingOnErrorCode; 253 } 254 255 if (mode == null || "off".equals(mode)) { 256 // binding mode is off 257 return null; 258 } 259 260 // setup json data format 261 String name = configuration.getJsonDataFormat(); 262 if (name != null) { 263 // must only be a name, not refer to an existing instance 264 Object instance = camelContext.getRegistry().lookupByName(name); 265 if (instance != null) { 266 throw new IllegalArgumentException("JsonDataFormat name: " + name + " must not be an existing bean instance from the registry"); 267 } 268 } else { 269 name = "json-jackson"; 270 } 271 // this will create a new instance as the name was not already pre-created 272 DataFormat json = camelContext.resolveDataFormat(name); 273 DataFormat outJson = camelContext.resolveDataFormat(name); 274 275 // is json binding required? 276 if (mode.contains("json") && json == null) { 277 throw new IllegalArgumentException("JSon DataFormat " + name + " not found."); 278 } 279 280 if (json != null) { 281 Class<?> clazz = null; 282 if (type != null) { 283 String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type; 284 clazz = camelContext.getClassResolver().resolveMandatoryClass(typeName); 285 } 286 if (clazz != null) { 287 IntrospectionSupport.setProperty(camelContext.getTypeConverter(), json, "unmarshalType", clazz); 288 IntrospectionSupport.setProperty(camelContext.getTypeConverter(), json, "useList", type.endsWith("[]")); 289 } 290 setAdditionalConfiguration(configuration, camelContext, json, "json.in."); 291 292 Class<?> outClazz = null; 293 if (outType != null) { 294 String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType; 295 outClazz = camelContext.getClassResolver().resolveMandatoryClass(typeName); 296 } 297 if (outClazz != null) { 298 IntrospectionSupport.setProperty(camelContext.getTypeConverter(), outJson, "unmarshalType", outClazz); 299 IntrospectionSupport.setProperty(camelContext.getTypeConverter(), outJson, "useList", outType.endsWith("[]")); 300 } 301 setAdditionalConfiguration(configuration, camelContext, outJson, "json.out."); 302 } 303 304 // setup xml data format 305 name = configuration.getXmlDataFormat(); 306 if (name != null) { 307 // must only be a name, not refer to an existing instance 308 Object instance = camelContext.getRegistry().lookupByName(name); 309 if (instance != null) { 310 throw new IllegalArgumentException("XmlDataFormat name: " + name + " must not be an existing bean instance from the registry"); 311 } 312 } else { 313 name = "jaxb"; 314 } 315 // this will create a new instance as the name was not already pre-created 316 DataFormat jaxb = camelContext.resolveDataFormat(name); 317 DataFormat outJaxb = camelContext.resolveDataFormat(name); 318 319 // is xml binding required? 320 if (mode.contains("xml") && jaxb == null) { 321 throw new IllegalArgumentException("XML DataFormat " + name + " not found."); 322 } 323 324 if (jaxb != null) { 325 Class<?> clazz = null; 326 if (type != null) { 327 String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type; 328 clazz = camelContext.getClassResolver().resolveMandatoryClass(typeName); 329 } 330 if (clazz != null) { 331 JAXBContext jc = JAXBContext.newInstance(clazz); 332 IntrospectionSupport.setProperty(camelContext.getTypeConverter(), jaxb, "context", jc); 333 } 334 setAdditionalConfiguration(configuration, camelContext, jaxb, "xml.in."); 335 336 Class<?> outClazz = null; 337 if (outType != null) { 338 String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType; 339 outClazz = camelContext.getClassResolver().resolveMandatoryClass(typeName); 340 } 341 if (outClazz != null) { 342 JAXBContext jc = JAXBContext.newInstance(outClazz); 343 IntrospectionSupport.setProperty(camelContext.getTypeConverter(), outJaxb, "context", jc); 344 } else if (clazz != null) { 345 // fallback and use the context from the input 346 JAXBContext jc = JAXBContext.newInstance(clazz); 347 IntrospectionSupport.setProperty(camelContext.getTypeConverter(), outJaxb, "context", jc); 348 } 349 setAdditionalConfiguration(configuration, camelContext, outJaxb, "xml.out."); 350 } 351 352 return new RestProducerBindingProcessor(producer, camelContext, json, jaxb, outJson, outJaxb, mode, skip, outType); 353 } 354 355 private void setAdditionalConfiguration(RestConfiguration config, CamelContext context, 356 DataFormat dataFormat, String prefix) throws Exception { 357 if (config.getDataFormatProperties() != null && !config.getDataFormatProperties().isEmpty()) { 358 // must use a copy as otherwise the options gets removed during introspection setProperties 359 Map<String, Object> copy = new HashMap<String, Object>(); 360 361 // filter keys on prefix 362 // - either its a known prefix and must match the prefix parameter 363 // - or its a common configuration that we should always use 364 for (Map.Entry<String, Object> entry : config.getDataFormatProperties().entrySet()) { 365 String key = entry.getKey(); 366 String copyKey; 367 boolean known = isKeyKnownPrefix(key); 368 if (known) { 369 // remove the prefix from the key to use 370 copyKey = key.substring(prefix.length()); 371 } else { 372 // use the key as is 373 copyKey = key; 374 } 375 if (!known || key.startsWith(prefix)) { 376 copy.put(copyKey, entry.getValue()); 377 } 378 } 379 380 // set reference properties first as they use # syntax that fools the regular properties setter 381 EndpointHelper.setReferenceProperties(context, dataFormat, copy); 382 EndpointHelper.setProperties(context, dataFormat, copy); 383 } 384 } 385 386 private boolean isKeyKnownPrefix(String key) { 387 return key.startsWith("json.in.") || key.startsWith("json.out.") || key.startsWith("xml.in.") || key.startsWith("xml.out."); 388 } 389 390 static String createQueryParameters(String query, Message inMessage) throws URISyntaxException, UnsupportedEncodingException { 391 if (query != null) { 392 final Map<String, Object> givenParams = URISupport.parseQuery(query); 393 final Map<String, Object> params = new LinkedHashMap<>(givenParams.size()); 394 for (Map.Entry<String, Object> entry : givenParams.entrySet()) { 395 Object v = entry.getValue(); 396 if (v != null) { 397 String a = v.toString(); 398 // decode the key as { may be decoded to %NN 399 a = URLDecoder.decode(a, "UTF-8"); 400 if (a.startsWith("{") && a.endsWith("}")) { 401 String key = a.substring(1, a.length() - 1); 402 boolean optional = false; 403 if (key.endsWith("?")) { 404 key = key.substring(0, key.length() - 1); 405 optional = true; 406 } 407 String value = inMessage.getHeader(key, String.class); 408 if (value != null) { 409 params.put(key, value); 410 } else if (!optional) { 411 // value is null and parameter is not optional 412 params.put(entry.getKey(), entry.getValue()); 413 } 414 } else { 415 params.put(entry.getKey(), entry.getValue()); 416 } 417 } 418 } 419 query = URISupport.createQueryString(params); 420 } 421 return query; 422 } 423}