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.net.URISyntaxException;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import javax.xml.bind.annotation.XmlAccessType;
025import javax.xml.bind.annotation.XmlAccessorType;
026import javax.xml.bind.annotation.XmlAttribute;
027import javax.xml.bind.annotation.XmlElementRef;
028import javax.xml.bind.annotation.XmlRootElement;
029
030import org.apache.camel.CamelContext;
031import org.apache.camel.model.OptionalIdentifiedDefinition;
032import org.apache.camel.model.RouteDefinition;
033import org.apache.camel.model.ToDefinition;
034import org.apache.camel.spi.Metadata;
035import org.apache.camel.util.ObjectHelper;
036import org.apache.camel.util.URISupport;
037
038/**
039 * Defines a rest service using the rest-dsl
040 */
041@Metadata(label = "rest")
042@XmlRootElement(name = "rest")
043@XmlAccessorType(XmlAccessType.FIELD)
044public class RestDefinition extends OptionalIdentifiedDefinition<RestDefinition> {
045
046    @XmlAttribute
047    private String path;
048
049    @XmlAttribute
050    private String consumes;
051
052    @XmlAttribute
053    private String produces;
054
055    @XmlAttribute @Metadata(defaultValue = "auto")
056    private RestBindingMode bindingMode;
057
058    @XmlAttribute
059    private Boolean skipBindingOnErrorCode;
060
061    @XmlAttribute
062    private Boolean enableCORS;
063
064    @XmlElementRef
065    private List<VerbDefinition> verbs = new ArrayList<VerbDefinition>();
066
067    @Override
068    public String getLabel() {
069        return "rest";
070    }
071
072    public String getPath() {
073        return path;
074    }
075
076    /**
077     * Path of the rest service, such as "/foo"
078     */
079    public void setPath(String path) {
080        this.path = path;
081    }
082
083    public String getConsumes() {
084        return consumes;
085    }
086
087    /**
088     * To define the content type what the REST service consumes (accept as input), such as application/xml or application/json.
089     * This option will override what may be configured on a parent level
090     */
091    public void setConsumes(String consumes) {
092        this.consumes = consumes;
093    }
094
095    public String getProduces() {
096        return produces;
097    }
098
099    /**
100     * To define the content type what the REST service produces (uses for output), such as application/xml or application/json
101     * This option will override what may be configured on a parent level
102     */
103    public void setProduces(String produces) {
104        this.produces = produces;
105    }
106
107    public RestBindingMode getBindingMode() {
108        return bindingMode;
109    }
110
111    /**
112     * Sets the binding mode to use.
113     * This option will override what may be configured on a parent level
114     * <p/>
115     * The default value is auto
116     */
117    public void setBindingMode(RestBindingMode bindingMode) {
118        this.bindingMode = bindingMode;
119    }
120
121    public List<VerbDefinition> getVerbs() {
122        return verbs;
123    }
124
125    /**
126     * The HTTP verbs this REST service accepts and uses
127     */
128    public void setVerbs(List<VerbDefinition> verbs) {
129        this.verbs = verbs;
130    }
131   
132    public Boolean getSkipBindingOnErrorCode() {
133        return skipBindingOnErrorCode;
134    }
135
136    /**
137     * Whether to skip binding on output if there is a custom HTTP error code header.
138     * This allows to build custom error messages that do not bind to json / xml etc, as success messages otherwise will do.
139     * This option will override what may be configured on a parent level
140     */
141    public void setSkipBindingOnErrorCode(Boolean skipBindingOnErrorCode) {
142        this.skipBindingOnErrorCode = skipBindingOnErrorCode;
143    }
144
145    public Boolean getEnableCORS() {
146        return enableCORS;
147    }
148
149    /**
150     * Whether to enable CORS headers in the HTTP response.
151     * This option will override what may be configured on a parent level
152     * <p/>
153     * The default value is false.
154     */
155    public void setEnableCORS(Boolean enableCORS) {
156        this.enableCORS = enableCORS;
157    }
158
159    // Fluent API
160    //-------------------------------------------------------------------------
161
162
163    /**
164     * To set the base path of this REST service
165     */
166    public RestDefinition path(String path) {
167        setPath(path);
168        return this;
169    }
170
171    public RestDefinition get() {
172        return addVerb("get", null);
173    }
174
175    public RestDefinition get(String uri) {
176        return addVerb("get", uri);
177    }
178
179    public RestDefinition post() {
180        return addVerb("post", null);
181    }
182
183    public RestDefinition post(String uri) {
184        return addVerb("post", uri);
185    }
186
187    public RestDefinition put() {
188        return addVerb("put", null);
189    }
190
191    public RestDefinition put(String uri) {
192        return addVerb("put", uri);
193    }
194
195    public RestDefinition delete() {
196        return addVerb("delete", null);
197    }
198
199    public RestDefinition delete(String uri) {
200        return addVerb("delete", uri);
201    }
202
203    public RestDefinition head() {
204        return addVerb("head", null);
205    }
206
207    public RestDefinition head(String uri) {
208        return addVerb("head", uri);
209    }
210
211    public RestDefinition verb(String verb) {
212        return addVerb(verb, null);
213    }
214
215    public RestDefinition verb(String verb, String uri) {
216        return addVerb(verb, uri);
217    }
218
219    @Override
220    public RestDefinition id(String id) {
221        if (getVerbs().isEmpty()) {
222            super.id(id);
223        } else {
224            // add on last verb as that is how the Java DSL works
225            VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
226            verb.id(id);
227        }
228
229        return this;
230    }
231
232    @Override
233    public RestDefinition description(String text) {
234        if (getVerbs().isEmpty()) {
235            super.description(text);
236        } else {
237            // add on last verb as that is how the Java DSL works
238            VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
239            verb.description(text);
240        }
241
242        return this;
243    }
244
245    @Override
246    public RestDefinition description(String id, String text, String lang) {
247        if (getVerbs().isEmpty()) {
248            super.description(id, text, lang);
249        } else {
250            // add on last verb as that is how the Java DSL works
251            VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
252            verb.description(id, text, lang);
253        }
254
255        return this;
256    }
257
258    public RestDefinition consumes(String mediaType) {
259        if (getVerbs().isEmpty()) {
260            this.consumes = mediaType;
261        } else {
262            // add on last verb as that is how the Java DSL works
263            VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
264            verb.setConsumes(mediaType);
265        }
266
267        return this;
268    }
269
270    public RestDefinition produces(String mediaType) {
271        if (getVerbs().isEmpty()) {
272            this.produces = mediaType;
273        } else {
274            // add on last verb as that is how the Java DSL works
275            VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
276            verb.setProduces(mediaType);
277        }
278
279        return this;
280    }
281
282    public RestDefinition type(Class<?> classType) {
283        // add to last verb
284        if (getVerbs().isEmpty()) {
285            throw new IllegalArgumentException("Must add verb first, such as get/post/delete");
286        }
287
288        VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
289        verb.setType(classType.getCanonicalName());
290        return this;
291    }
292
293    public RestDefinition typeList(Class<?> classType) {
294        // add to last verb
295        if (getVerbs().isEmpty()) {
296            throw new IllegalArgumentException("Must add verb first, such as get/post/delete");
297        }
298
299        VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
300        // list should end with [] to indicate array
301        verb.setType(classType.getCanonicalName() + "[]");
302        return this;
303    }
304
305    public RestDefinition outType(Class<?> classType) {
306        // add to last verb
307        if (getVerbs().isEmpty()) {
308            throw new IllegalArgumentException("Must add verb first, such as get/post/delete");
309        }
310
311        VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
312        verb.setOutType(classType.getCanonicalName());
313        return this;
314    }
315
316    public RestDefinition outTypeList(Class<?> classType) {
317        // add to last verb
318        if (getVerbs().isEmpty()) {
319            throw new IllegalArgumentException("Must add verb first, such as get/post/delete");
320        }
321
322        VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
323        // list should end with [] to indicate array
324        verb.setOutType(classType.getCanonicalName() + "[]");
325        return this;
326    }
327
328    public RestDefinition bindingMode(RestBindingMode mode) {
329        if (getVerbs().isEmpty()) {
330            this.bindingMode = mode;
331        } else {
332            // add on last verb as that is how the Java DSL works
333            VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
334            verb.setBindingMode(mode);
335        }
336
337        return this;
338    }
339
340    public RestDefinition skipBindingOnErrorCode(boolean skipBindingOnErrorCode) {
341        if (getVerbs().isEmpty()) {
342            this.skipBindingOnErrorCode = skipBindingOnErrorCode;
343        } else {
344            // add on last verb as that is how the Java DSL works
345            VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
346            verb.setSkipBindingOnErrorCode(skipBindingOnErrorCode);
347        }
348
349        return this;
350    }
351
352    public RestDefinition enableCORS(boolean enableCORS) {
353        if (getVerbs().isEmpty()) {
354            this.enableCORS = enableCORS;
355        } else {
356            // add on last verb as that is how the Java DSL works
357            VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
358            verb.setEnableCORS(enableCORS);
359        }
360
361        return this;
362    }
363
364    /**
365     * Routes directly to the given endpoint.
366     * <p/>
367     * If you need additional routing capabilities, then use {@link #route()} instead.
368     *
369     * @param uri the uri of the endpoint
370     * @return this builder
371     */
372    public RestDefinition to(String uri) {
373        // add to last verb
374        if (getVerbs().isEmpty()) {
375            throw new IllegalArgumentException("Must add verb first, such as get/post/delete");
376        }
377
378        ToDefinition to = new ToDefinition(uri);
379
380        VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
381        verb.setTo(to);
382        return this;
383    }
384
385    public RouteDefinition route() {
386        // add to last verb
387        if (getVerbs().isEmpty()) {
388            throw new IllegalArgumentException("Must add verb first, such as get/post/delete");
389        }
390
391        // link them together so we can navigate using Java DSL
392        RouteDefinition route = new RouteDefinition();
393        route.setRestDefinition(this);
394        VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
395        verb.setRoute(route);
396        return route;
397    }
398
399    // Implementation
400    //-------------------------------------------------------------------------
401
402    private RestDefinition addVerb(String verb, String uri) {
403        VerbDefinition answer;
404
405        if ("get".equals(verb)) {
406            answer = new GetVerbDefinition();
407        } else if ("post".equals(verb)) {
408            answer = new PostVerbDefinition();
409        } else if ("delete".equals(verb)) {
410            answer = new DeleteVerbDefinition();
411        } else if ("head".equals(verb)) {
412            answer = new HeadVerbDefinition();
413        } else if ("put".equals(verb)) {
414            answer = new PutVerbDefinition();
415        } else {
416            answer = new VerbDefinition();
417            answer.setMethod(verb);
418        }
419
420        answer.setRest(this);
421        answer.setUri(uri);
422        getVerbs().add(answer);
423        return this;
424    }
425
426    /**
427     * Transforms this REST definition into a list of {@link org.apache.camel.model.RouteDefinition} which
428     * Camel routing engine can add and run. This allows us to define REST services using this
429     * REST DSL and turn those into regular Camel routes.
430     */
431    public List<RouteDefinition> asRouteDefinition(CamelContext camelContext) {
432        List<RouteDefinition> answer = new ArrayList<RouteDefinition>();
433
434        for (VerbDefinition verb : getVerbs()) {
435            // either the verb has a singular to or a embedded route
436            RouteDefinition route = verb.getRoute();
437            if (route == null) {
438                // it was a singular to, so add a new route and add the singular
439                // to as output to this route
440                route = new RouteDefinition();
441                route.getOutputs().add(verb.getTo());
442            }
443
444            // add the binding
445            RestBindingDefinition binding = new RestBindingDefinition();
446            binding.setType(verb.getType());
447            binding.setOutType(verb.getOutType());
448            // verb takes precedence over configuration on rest
449            if (verb.getConsumes() != null) {
450                binding.setConsumes(verb.getConsumes());
451            } else {
452                binding.setConsumes(getConsumes());
453            }
454            if (verb.getProduces() != null) {
455                binding.setProduces(verb.getProduces());
456            } else {
457                binding.setProduces(getProduces());
458            }
459            if (verb.getBindingMode() != null) {
460                binding.setBindingMode(verb.getBindingMode());
461            } else {
462                binding.setBindingMode(getBindingMode());
463            }
464            if (verb.getSkipBindingOnErrorCode() != null) {
465                binding.setSkipBindingOnErrorCode(verb.getSkipBindingOnErrorCode());
466            } else {
467                binding.setSkipBindingOnErrorCode(getSkipBindingOnErrorCode());
468            }
469            if (verb.getEnableCORS() != null) {
470                binding.setEnableCORS(verb.getEnableCORS());
471            } else {
472                binding.setEnableCORS(getEnableCORS());
473            }
474            route.getOutputs().add(0, binding);
475
476            // create the from endpoint uri which is using the rest component
477            String from = "rest:" + verb.asVerb() + ":" + buildUri(verb);
478
479            // append options
480            Map<String, Object> options = new HashMap<String, Object>();
481            // verb takes precedence over configuration on rest
482            if (verb.getConsumes() != null) {
483                options.put("consumes", verb.getConsumes());
484            } else if (getConsumes() != null) {
485                options.put("consumes", getConsumes());
486            }
487            if (verb.getProduces() != null) {
488                options.put("produces", verb.getProduces());
489            } else if (getProduces() != null) {
490                options.put("produces", getProduces());
491            }
492
493            // append optional type binding information
494            String inType = binding.getType();
495            if (inType != null) {
496                options.put("inType", inType);
497            }
498            String outType = binding.getOutType();
499            if (outType != null) {
500                options.put("outType", outType);
501            }
502            // if no route id has been set, then use the verb id as route id
503            if (!route.hasCustomIdAssigned()) {
504                // use id of verb as route id
505                String id = verb.getId();
506                if (id != null) {
507                    route.setId(id);
508                }
509            }
510            String routeId = route.idOrCreate(camelContext.getNodeIdFactory());
511            options.put("routeId", routeId);
512
513            // include optional description, which we favor from 1) to/route description 2) verb description 3) rest description
514            // this allows end users to define general descriptions and override then per to/route or verb
515            String description = verb.getTo() != null ? verb.getTo().getDescriptionText() : route.getDescriptionText();
516            if (description == null) {
517                description = verb.getDescriptionText();
518            }
519            if (description == null) {
520                description = getDescriptionText();
521            }
522            if (description != null) {
523                options.put("description", description);
524            }
525
526            if (!options.isEmpty()) {
527                String query;
528                try {
529                    query = URISupport.createQueryString(options);
530                } catch (URISyntaxException e) {
531                    throw ObjectHelper.wrapRuntimeCamelException(e);
532                }
533                from = from + "?" + query;
534            }
535
536            // the route should be from this rest endpoint
537            route.fromRest(from);
538            route.setRestDefinition(this);
539            answer.add(route);
540        }
541
542        return answer;
543    }
544
545    private String buildUri(VerbDefinition verb) {
546        if (path != null && verb.getUri() != null) {
547            return path + ":" + verb.getUri();
548        } else if (path != null) {
549            return path;
550        } else if (verb.getUri() != null) {
551            return verb.getUri();
552        } else {
553            return "";
554        }
555    }
556
557}