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