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}