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}