/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.camel.model.rest; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElementRef; import javax.xml.bind.annotation.XmlRootElement; import org.apache.camel.CamelContext; import org.apache.camel.model.OptionalIdentifiedDefinition; import org.apache.camel.model.ProcessorDefinition; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.ToDefinition; import org.apache.camel.model.ToDynamicDefinition; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.RestConfiguration; import org.apache.camel.util.FileUtil; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.URISupport; /** * Defines a rest service using the rest-dsl */ @Metadata(label = "rest") @XmlRootElement(name = "rest") @XmlAccessorType(XmlAccessType.FIELD) public class RestDefinition extends OptionalIdentifiedDefinition<RestDefinition> { @XmlAttribute private String path; @XmlAttribute private String tag; @XmlAttribute private String consumes; @XmlAttribute private String produces; @XmlAttribute @Metadata(defaultValue = "auto") private RestBindingMode bindingMode; @XmlAttribute private Boolean skipBindingOnErrorCode; @XmlAttribute private Boolean enableCORS; @XmlAttribute private Boolean apiDocs; @XmlElementRef private List<VerbDefinition> verbs = new ArrayList<VerbDefinition>(); @Override public String getLabel() { return "rest"; } public String getPath() { return path; } /** * Path of the rest service, such as "/foo" */ public void setPath(String path) { this.path = path; } public String getTag() { return tag; } /** * To configure a special tag for the operations within this rest definition. */ public void setTag(String tag) { this.tag = tag; } public String getConsumes() { return consumes; } /** * To define the content type what the REST service consumes (accept as input), such as application/xml or application/json. * This option will override what may be configured on a parent level */ public void setConsumes(String consumes) { this.consumes = consumes; } public String getProduces() { return produces; } /** * To define the content type what the REST service produces (uses for output), such as application/xml or application/json * This option will override what may be configured on a parent level */ public void setProduces(String produces) { this.produces = produces; } public RestBindingMode getBindingMode() { return bindingMode; } /** * Sets the binding mode to use. * This option will override what may be configured on a parent level * <p/> * The default value is auto */ public void setBindingMode(RestBindingMode bindingMode) { this.bindingMode = bindingMode; } public List<VerbDefinition> getVerbs() { return verbs; } /** * The HTTP verbs this REST service accepts and uses */ public void setVerbs(List<VerbDefinition> verbs) { this.verbs = verbs; } public Boolean getSkipBindingOnErrorCode() { return skipBindingOnErrorCode; } /** * Whether to skip binding on output if there is a custom HTTP error code header. * This allows to build custom error messages that do not bind to json / xml etc, as success messages otherwise will do. * This option will override what may be configured on a parent level */ public void setSkipBindingOnErrorCode(Boolean skipBindingOnErrorCode) { this.skipBindingOnErrorCode = skipBindingOnErrorCode; } public Boolean getEnableCORS() { return enableCORS; } /** * Whether to enable CORS headers in the HTTP response. * This option will override what may be configured on a parent level * <p/> * The default value is false. */ public void setEnableCORS(Boolean enableCORS) { this.enableCORS = enableCORS; } public Boolean getApiDocs() { return apiDocs; } /** * Whether to include or exclude the VerbDefinition in API documentation. * This option will override what may be configured on a parent level * <p/> * The default value is true. */ public void setApiDocs(Boolean apiDocs) { this.apiDocs = apiDocs; } // Fluent API //------------------------------------------------------------------------- /** * To set the base path of this REST service */ public RestDefinition path(String path) { setPath(path); return this; } /** * To set the tag to use of this REST service */ public RestDefinition tag(String tag) { setTag(tag); return this; } public RestDefinition get() { return addVerb("get", null); } public RestDefinition get(String uri) { return addVerb("get", uri); } public RestDefinition post() { return addVerb("post", null); } public RestDefinition post(String uri) { return addVerb("post", uri); } public RestDefinition put() { return addVerb("put", null); } public RestDefinition put(String uri) { return addVerb("put", uri); } public RestDefinition patch() { return addVerb("patch", null); } public RestDefinition patch(String uri) { return addVerb("patch", uri); } public RestDefinition delete() { return addVerb("delete", null); } public RestDefinition delete(String uri) { return addVerb("delete", uri); } public RestDefinition head() { return addVerb("head", null); } public RestDefinition head(String uri) { return addVerb("head", uri); } @Deprecated public RestDefinition options() { return addVerb("options", null); } @Deprecated public RestDefinition options(String uri) { return addVerb("options", uri); } public RestDefinition verb(String verb) { return addVerb(verb, null); } public RestDefinition verb(String verb, String uri) { return addVerb(verb, uri); } @Override public RestDefinition id(String id) { if (getVerbs().isEmpty()) { super.id(id); } else { // add on last verb as that is how the Java DSL works VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.id(id); } return this; } @Override public RestDefinition description(String text) { if (getVerbs().isEmpty()) { super.description(text); } else { // add on last verb as that is how the Java DSL works VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.description(text); } return this; } @Override public RestDefinition description(String id, String text, String lang) { if (getVerbs().isEmpty()) { super.description(id, text, lang); } else { // add on last verb as that is how the Java DSL works VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.description(id, text, lang); } return this; } public RestDefinition consumes(String mediaType) { if (getVerbs().isEmpty()) { this.consumes = mediaType; } else { // add on last verb as that is how the Java DSL works VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setConsumes(mediaType); } return this; } public RestOperationParamDefinition param() { if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); return param(verb); } public RestDefinition param(RestOperationParamDefinition param) { if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.getParams().add(param); return this; } public RestDefinition params(List<RestOperationParamDefinition> params) { if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.getParams().addAll(params); return this; } public RestOperationParamDefinition param(VerbDefinition verb) { return new RestOperationParamDefinition(verb); } public RestDefinition responseMessage(RestOperationResponseMsgDefinition msg) { if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.getResponseMsgs().add(msg); return this; } public RestOperationResponseMsgDefinition responseMessage() { if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); return responseMessage(verb); } public RestOperationResponseMsgDefinition responseMessage(VerbDefinition verb) { return new RestOperationResponseMsgDefinition(verb); } public RestDefinition responseMessages(List<RestOperationResponseMsgDefinition> msgs) { if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.getResponseMsgs().addAll(msgs); return this; } public RestDefinition produces(String mediaType) { if (getVerbs().isEmpty()) { this.produces = mediaType; } else { // add on last verb as that is how the Java DSL works VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setProduces(mediaType); } return this; } public RestDefinition type(Class<?> classType) { // add to last verb if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setType(classType.getCanonicalName()); return this; } /** * @param classType the canonical class name for the array passed as input * * @deprecated as of 2.19.0. Replaced wtih {@link #type(Class)} with {@code []} appended to canonical class name * , e.g. {@code type(MyClass[].class} */ @Deprecated public RestDefinition typeList(Class<?> classType) { // add to last verb if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); // list should end with [] to indicate array verb.setType(classType.getCanonicalName() + "[]"); return this; } public RestDefinition outType(Class<?> classType) { // add to last verb if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setOutType(classType.getCanonicalName()); return this; } /** * @param classType the canonical class name for the array passed as output * * @deprecated as of 2.19.0. Replaced wtih {@link #outType(Class)} with {@code []} appended to canonical class name * , e.g. {@code outType(MyClass[].class} */ @Deprecated public RestDefinition outTypeList(Class<?> classType) { // add to last verb if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); // list should end with [] to indicate array verb.setOutType(classType.getCanonicalName() + "[]"); return this; } public RestDefinition bindingMode(RestBindingMode mode) { if (getVerbs().isEmpty()) { this.bindingMode = mode; } else { // add on last verb as that is how the Java DSL works VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setBindingMode(mode); } return this; } public RestDefinition skipBindingOnErrorCode(boolean skipBindingOnErrorCode) { if (getVerbs().isEmpty()) { this.skipBindingOnErrorCode = skipBindingOnErrorCode; } else { // add on last verb as that is how the Java DSL works VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setSkipBindingOnErrorCode(skipBindingOnErrorCode); } return this; } public RestDefinition enableCORS(boolean enableCORS) { if (getVerbs().isEmpty()) { this.enableCORS = enableCORS; } else { // add on last verb as that is how the Java DSL works VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setEnableCORS(enableCORS); } return this; } /** * Include or exclude the current Rest Definition in API documentation. * <p/> * The default value is true. */ public RestDefinition apiDocs(Boolean apiDocs) { if (getVerbs().isEmpty()) { this.apiDocs = apiDocs; } else { // add on last verb as that is how the Java DSL works VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setApiDocs(apiDocs); } return this; } /** * Routes directly to the given static endpoint. * <p/> * If you need additional routing capabilities, then use {@link #route()} instead. * * @param uri the uri of the endpoint * @return this builder */ public RestDefinition to(String uri) { // add to last verb if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } ToDefinition to = new ToDefinition(uri); VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setTo(to); return this; } /** * Routes directly to the given dynamic endpoint. * <p/> * If you need additional routing capabilities, then use {@link #route()} instead. * * @param uri the uri of the endpoint * @return this builder */ public RestDefinition toD(String uri) { // add to last verb if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } ToDynamicDefinition to = new ToDynamicDefinition(uri); VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setToD(to); return this; } public RouteDefinition route() { // add to last verb if (getVerbs().isEmpty()) { throw new IllegalArgumentException("Must add verb first, such as get/post/delete"); } // link them together so we can navigate using Java DSL RouteDefinition route = new RouteDefinition(); route.setRestDefinition(this); VerbDefinition verb = getVerbs().get(getVerbs().size() - 1); verb.setRoute(route); return route; } // Implementation //------------------------------------------------------------------------- private RestDefinition addVerb(String verb, String uri) { VerbDefinition answer; if ("get".equals(verb)) { answer = new GetVerbDefinition(); } else if ("post".equals(verb)) { answer = new PostVerbDefinition(); } else if ("delete".equals(verb)) { answer = new DeleteVerbDefinition(); } else if ("head".equals(verb)) { answer = new HeadVerbDefinition(); } else if ("put".equals(verb)) { answer = new PutVerbDefinition(); } else if ("patch".equals(verb)) { answer = new PatchVerbDefinition(); } else if ("options".equals(verb)) { answer = new OptionsVerbDefinition(); } else { answer = new VerbDefinition(); answer.setMethod(verb); } getVerbs().add(answer); answer.setRest(this); answer.setUri(uri); return this; } /** * Transforms this REST definition into a list of {@link org.apache.camel.model.RouteDefinition} which * Camel routing engine can add and run. This allows us to define REST services using this * REST DSL and turn those into regular Camel routes. * * @param camelContext The Camel context */ public List<RouteDefinition> asRouteDefinition(CamelContext camelContext) { ObjectHelper.notNull(camelContext, "CamelContext"); // sanity check this rest definition do not have duplicates validateUniquePaths(); List<RouteDefinition> answer = new ArrayList<RouteDefinition>(); if (camelContext.getRestConfigurations().isEmpty()) { camelContext.getRestConfiguration(); } for (RestConfiguration config : camelContext.getRestConfigurations()) { addRouteDefinition(camelContext, answer, config.getComponent()); } return answer; } /** * Transforms this REST definition into a list of {@link org.apache.camel.model.RouteDefinition} which * Camel routing engine can add and run. This allows us to define REST services using this * REST DSL and turn those into regular Camel routes. * * @param camelContext The Camel context * @param restConfiguration The rest configuration to use */ public List<RouteDefinition> asRouteDefinition(CamelContext camelContext, RestConfiguration restConfiguration) { ObjectHelper.notNull(camelContext, "CamelContext"); ObjectHelper.notNull(restConfiguration, "RestConfiguration"); // sanity check this rest definition do not have duplicates validateUniquePaths(); List<RouteDefinition> answer = new ArrayList<RouteDefinition>(); addRouteDefinition(camelContext, answer, restConfiguration.getComponent()); return answer; } protected void validateUniquePaths() { Set<String> paths = new HashSet<String>(); for (VerbDefinition verb : verbs) { String path = verb.asVerb(); if (verb.getUri() != null) { path += ":" + verb.getUri(); } if (!paths.add(path)) { throw new IllegalArgumentException("Duplicate verb detected in rest-dsl: " + path); } } } /** * Transforms the rest api configuration into a {@link org.apache.camel.model.RouteDefinition} which * Camel routing engine uses to service the rest api docs. */ public static RouteDefinition asRouteApiDefinition(CamelContext camelContext, RestConfiguration configuration) { RouteDefinition answer = new RouteDefinition(); // create the from endpoint uri which is using the rest-api component String from = "rest-api:" + configuration.getApiContextPath(); // append options Map<String, Object> options = new HashMap<String, Object>(); String routeId = configuration.getApiContextRouteId(); if (routeId == null) { routeId = answer.idOrCreate(camelContext.getNodeIdFactory()); } options.put("routeId", routeId); if (configuration.getComponent() != null && !configuration.getComponent().isEmpty()) { options.put("componentName", configuration.getComponent()); } if (configuration.getApiContextIdPattern() != null) { options.put("contextIdPattern", configuration.getApiContextIdPattern()); } if (!options.isEmpty()) { String query; try { query = URISupport.createQueryString(options); } catch (URISyntaxException e) { throw ObjectHelper.wrapRuntimeCamelException(e); } from = from + "?" + query; } // we use the same uri as the producer (so we have a little route for the rest api) String to = from; answer.fromRest(from); answer.id(routeId); answer.to(to); return answer; } private void addRouteDefinition(CamelContext camelContext, List<RouteDefinition> answer, String component) { for (VerbDefinition verb : getVerbs()) { // either the verb has a singular to or a embedded route RouteDefinition route = verb.getRoute(); if (route == null) { // it was a singular to, so add a new route and add the singular // to as output to this route route = new RouteDefinition(); ProcessorDefinition def = verb.getTo() != null ? verb.getTo() : verb.getToD(); route.getOutputs().add(def); } // add the binding RestBindingDefinition binding = new RestBindingDefinition(); binding.setComponent(component); binding.setType(verb.getType()); binding.setOutType(verb.getOutType()); // verb takes precedence over configuration on rest if (verb.getConsumes() != null) { binding.setConsumes(verb.getConsumes()); } else { binding.setConsumes(getConsumes()); } if (verb.getProduces() != null) { binding.setProduces(verb.getProduces()); } else { binding.setProduces(getProduces()); } if (verb.getBindingMode() != null) { binding.setBindingMode(verb.getBindingMode()); } else { binding.setBindingMode(getBindingMode()); } if (verb.getSkipBindingOnErrorCode() != null) { binding.setSkipBindingOnErrorCode(verb.getSkipBindingOnErrorCode()); } else { binding.setSkipBindingOnErrorCode(getSkipBindingOnErrorCode()); } if (verb.getEnableCORS() != null) { binding.setEnableCORS(verb.getEnableCORS()); } else { binding.setEnableCORS(getEnableCORS()); } // register all the default values for the query parameters for (RestOperationParamDefinition param : verb.getParams()) { if (RestParamType.query == param.getType() && ObjectHelper.isNotEmpty(param.getDefaultValue())) { binding.addDefaultValue(param.getName(), param.getDefaultValue()); } } route.setRestBindingDefinition(binding); // create the from endpoint uri which is using the rest component String from = "rest:" + verb.asVerb() + ":" + buildUri(verb); // append options Map<String, Object> options = new HashMap<String, Object>(); // verb takes precedence over configuration on rest if (verb.getConsumes() != null) { options.put("consumes", verb.getConsumes()); } else if (getConsumes() != null) { options.put("consumes", getConsumes()); } if (verb.getProduces() != null) { options.put("produces", verb.getProduces()); } else if (getProduces() != null) { options.put("produces", getProduces()); } // append optional type binding information String inType = binding.getType(); if (inType != null) { options.put("inType", inType); } String outType = binding.getOutType(); if (outType != null) { options.put("outType", outType); } // if no route id has been set, then use the verb id as route id if (!route.hasCustomIdAssigned()) { // use id of verb as route id String id = verb.getId(); if (id != null) { route.setId(id); } } String routeId = verb.idOrCreate(camelContext.getNodeIdFactory()); if (!verb.getUsedForGeneratingNodeId()) { routeId = route.idOrCreate(camelContext.getNodeIdFactory()); } verb.setRouteId(routeId); options.put("routeId", routeId); if (component != null && !component.isEmpty()) { options.put("componentName", component); } // include optional description, which we favor from 1) to/route description 2) verb description 3) rest description // this allows end users to define general descriptions and override then per to/route or verb String description = verb.getTo() != null ? verb.getTo().getDescriptionText() : route.getDescriptionText(); if (description == null) { description = verb.getDescriptionText(); } if (description == null) { description = getDescriptionText(); } if (description != null) { options.put("description", description); } if (!options.isEmpty()) { String query; try { query = URISupport.createQueryString(options); } catch (URISyntaxException e) { throw ObjectHelper.wrapRuntimeCamelException(e); } from = from + "?" + query; } String path = getPath(); String s1 = FileUtil.stripTrailingSeparator(path); String s2 = FileUtil.stripLeadingSeparator(verb.getUri()); String allPath; if (s1 != null && s2 != null) { allPath = s1 + "/" + s2; } else if (path != null) { allPath = path; } else { allPath = verb.getUri(); } // each {} is a parameter (url templating) if (allPath != null) { String[] arr = allPath.split("\\/"); for (String a : arr) { // need to resolve property placeholders first try { a = camelContext.resolvePropertyPlaceholders(a); } catch (Exception e) { throw ObjectHelper.wrapRuntimeCamelException(e); } if (a.startsWith("{") && a.endsWith("}")) { String key = a.substring(1, a.length() - 1); // merge if exists boolean found = false; for (RestOperationParamDefinition param : verb.getParams()) { // name is mandatory String name = param.getName(); ObjectHelper.notEmpty(name, "parameter name"); // need to resolve property placeholders first try { name = camelContext.resolvePropertyPlaceholders(name); } catch (Exception e) { throw ObjectHelper.wrapRuntimeCamelException(e); } if (name.equalsIgnoreCase(key)) { param.type(RestParamType.path); found = true; break; } } if (!found) { param(verb).name(key).type(RestParamType.path).endParam(); } } } } if (verb.getType() != null) { String bodyType = verb.getType(); if (bodyType.endsWith("[]")) { bodyType = "List[" + bodyType.substring(0, bodyType.length() - 2) + "]"; } RestOperationParamDefinition param = findParam(verb, RestParamType.body.name()); if (param == null) { // must be body type and set the model class as data type param(verb).name(RestParamType.body.name()).type(RestParamType.body).dataType(bodyType).endParam(); } else { // must be body type and set the model class as data type param.type(RestParamType.body).dataType(bodyType); } } // the route should be from this rest endpoint route.fromRest(from); route.id(routeId); route.setRestDefinition(this); answer.add(route); } } private String buildUri(VerbDefinition verb) { if (path != null && verb.getUri() != null) { return path + ":" + verb.getUri(); } else if (path != null) { return path; } else if (verb.getUri() != null) { return verb.getUri(); } else { return ""; } } private RestOperationParamDefinition findParam(VerbDefinition verb, String name) { for (RestOperationParamDefinition param : verb.getParams()) { if (name.equals(param.getName())) { return param; } } return null; } }