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.util.HashMap;
020import java.util.HashSet;
021import java.util.Map;
022import java.util.Set;
023
024import javax.xml.bind.JAXBContext;
025import javax.xml.bind.annotation.XmlAccessType;
026import javax.xml.bind.annotation.XmlAccessorType;
027import javax.xml.bind.annotation.XmlAttribute;
028import javax.xml.bind.annotation.XmlRootElement;
029import javax.xml.bind.annotation.XmlTransient;
030
031import org.apache.camel.CamelContext;
032import org.apache.camel.model.OptionalIdentifiedDefinition;
033import org.apache.camel.processor.RestBindingAdvice;
034import org.apache.camel.spi.DataFormat;
035import org.apache.camel.spi.Metadata;
036import org.apache.camel.spi.RestConfiguration;
037import org.apache.camel.spi.RouteContext;
038import org.apache.camel.util.EndpointHelper;
039import org.apache.camel.util.IntrospectionSupport;
040
041/**
042 * To configure rest binding
043 */
044@Metadata(label = "rest")
045@XmlRootElement(name = "restBinding")
046@XmlAccessorType(XmlAccessType.FIELD)
047public class RestBindingDefinition extends OptionalIdentifiedDefinition<RestBindingDefinition> {
048
049    @XmlTransient
050    private Map<String, String> defaultValues;
051
052    @XmlTransient
053    private Boolean requiredBody;
054
055    @XmlTransient
056    private Set<String> requiredHeaders;
057
058    @XmlTransient
059    private Set<String> requiredQueryParameters;
060
061    @XmlAttribute
062    private String consumes;
063
064    @XmlAttribute
065    private String produces;
066
067    @XmlAttribute
068    @Metadata(defaultValue = "off")
069    private RestBindingMode bindingMode;
070
071    @XmlAttribute
072    private String type;
073
074    @XmlAttribute
075    private String outType;
076
077    @XmlAttribute
078    private Boolean skipBindingOnErrorCode;
079
080    @XmlAttribute
081    private Boolean clientRequestValidation;
082
083    @XmlAttribute
084    private Boolean enableCORS;
085
086    @XmlAttribute
087    private String component;
088
089    public RestBindingDefinition() {
090    }
091
092    @Override
093    public String toString() {
094        return "RestBinding";
095    }
096
097    public RestBindingAdvice createRestBindingAdvice(RouteContext routeContext) throws Exception {
098
099        CamelContext context = routeContext.getCamelContext();
100        RestConfiguration config = context.getRestConfiguration(component, true);
101
102        // these options can be overridden per rest verb
103        String mode = config.getBindingMode().name();
104        if (bindingMode != null) {
105            mode = bindingMode.name();
106        }
107        boolean cors = config.isEnableCORS();
108        if (enableCORS != null) {
109            cors = enableCORS;
110        }
111        boolean skip = config.isSkipBindingOnErrorCode();
112        if (skipBindingOnErrorCode != null) {
113            skip = skipBindingOnErrorCode;
114        }
115        boolean validation = config.isClientRequestValidation();
116        if (clientRequestValidation != null) {
117            validation = clientRequestValidation;
118        }
119
120        // cors headers
121        Map<String, String> corsHeaders = config.getCorsHeaders();
122
123        if (mode == null || "off".equals(mode)) {
124            // binding mode is off, so create a off mode binding processor
125            return new RestBindingAdvice(context, null, null, null, null, consumes, produces, mode, skip, validation,
126                cors, corsHeaders, defaultValues, requiredBody != null ? requiredBody : false, requiredQueryParameters, requiredHeaders);
127        }
128
129        // setup json data format
130        DataFormat json = null;
131        DataFormat outJson = null;
132        if (mode.contains("json") || "auto".equals(mode)) {
133            String name = config.getJsonDataFormat();
134            if (name != null) {
135                // must only be a name, not refer to an existing instance
136                Object instance = context.getRegistry().lookupByName(name);
137                if (instance != null) {
138                    throw new IllegalArgumentException("JsonDataFormat name: " + name + " must not be an existing bean instance from the registry");
139                }
140            } else {
141                name = "json-jackson";
142            }
143            // this will create a new instance as the name was not already pre-created
144            json = context.resolveDataFormat(name);
145            outJson = context.resolveDataFormat(name);
146
147            if (json != null) {
148                Class<?> clazz = null;
149                if (type != null) {
150                    String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type;
151                    clazz = context.getClassResolver().resolveMandatoryClass(typeName);
152                }
153                if (clazz != null) {
154                    IntrospectionSupport.setProperty(context.getTypeConverter(), json, "unmarshalType", clazz);
155                    IntrospectionSupport.setProperty(context.getTypeConverter(), json, "useList", type.endsWith("[]"));
156                }
157                setAdditionalConfiguration(config, context, json, "json.in.");
158
159                Class<?> outClazz = null;
160                if (outType != null) {
161                    String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType;
162                    outClazz = context.getClassResolver().resolveMandatoryClass(typeName);
163                }
164                if (outClazz != null) {
165                    IntrospectionSupport.setProperty(context.getTypeConverter(), outJson, "unmarshalType", outClazz);
166                    IntrospectionSupport.setProperty(context.getTypeConverter(), outJson, "useList", outType.endsWith("[]"));
167                }
168                setAdditionalConfiguration(config, context, outJson, "json.out.");
169            }
170        }
171
172        // setup xml data format
173        DataFormat jaxb = null;
174        DataFormat outJaxb = null;
175        if (mode.contains("xml") || "auto".equals(mode)) {
176            String name = config.getXmlDataFormat();
177            if (name != null) {
178                // must only be a name, not refer to an existing instance
179                Object instance = context.getRegistry().lookupByName(name);
180                if (instance != null) {
181                    throw new IllegalArgumentException("XmlDataFormat name: " + name + " must not be an existing bean instance from the registry");
182                }
183            } else {
184                name = "jaxb";
185            }
186            // this will create a new instance as the name was not already pre-created
187            jaxb = context.resolveDataFormat(name);
188            outJaxb = context.resolveDataFormat(name);
189
190            // is xml binding required?
191            if (mode.contains("xml") && jaxb == null) {
192                throw new IllegalArgumentException("XML DataFormat " + name + " not found.");
193            }
194
195            if (jaxb != null) {
196                Class<?> clazz = null;
197                if (type != null) {
198                    String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type;
199                    clazz = context.getClassResolver().resolveMandatoryClass(typeName);
200                }
201                if (clazz != null) {
202                    JAXBContext jc = JAXBContext.newInstance(clazz);
203                    IntrospectionSupport.setProperty(context.getTypeConverter(), jaxb, "context", jc);
204                }
205                setAdditionalConfiguration(config, context, jaxb, "xml.in.");
206
207                Class<?> outClazz = null;
208                if (outType != null) {
209                    String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType;
210                    outClazz = context.getClassResolver().resolveMandatoryClass(typeName);
211                }
212                if (outClazz != null) {
213                    JAXBContext jc = JAXBContext.newInstance(outClazz);
214                    IntrospectionSupport.setProperty(context.getTypeConverter(), outJaxb, "context", jc);
215                } else if (clazz != null) {
216                    // fallback and use the context from the input
217                    JAXBContext jc = JAXBContext.newInstance(clazz);
218                    IntrospectionSupport.setProperty(context.getTypeConverter(), outJaxb, "context", jc);
219                }
220                setAdditionalConfiguration(config, context, outJaxb, "xml.out.");
221            }
222        }
223
224        return new RestBindingAdvice(context, json, jaxb, outJson, outJaxb, consumes, produces, mode, skip, validation,
225            cors, corsHeaders, defaultValues, requiredBody != null ? requiredBody : false, requiredQueryParameters, requiredHeaders);
226    }
227
228    private void setAdditionalConfiguration(RestConfiguration config, CamelContext context,
229                                            DataFormat dataFormat, String prefix) throws Exception {
230        if (config.getDataFormatProperties() != null && !config.getDataFormatProperties().isEmpty()) {
231            // must use a copy as otherwise the options gets removed during introspection setProperties
232            Map<String, Object> copy = new HashMap<>();
233
234            // filter keys on prefix
235            // - either its a known prefix and must match the prefix parameter
236            // - or its a common configuration that we should always use
237            for (Map.Entry<String, Object> entry : config.getDataFormatProperties().entrySet()) {
238                String key = entry.getKey();
239                String copyKey;
240                boolean known = isKeyKnownPrefix(key);
241                if (known) {
242                    // remove the prefix from the key to use
243                    copyKey = key.substring(prefix.length());
244                } else {
245                    // use the key as is
246                    copyKey = key;
247                }
248                if (!known || key.startsWith(prefix)) {
249                    copy.put(copyKey, entry.getValue());
250                }
251            }
252
253            // set reference properties first as they use # syntax that fools the regular properties setter
254            EndpointHelper.setReferenceProperties(context, dataFormat, copy);
255            EndpointHelper.setProperties(context, dataFormat, copy);
256        }
257    }
258
259    private boolean isKeyKnownPrefix(String key) {
260        return key.startsWith("json.in.") || key.startsWith("json.out.") || key.startsWith("xml.in.") || key.startsWith("xml.out.");
261    }
262
263    public String getConsumes() {
264        return consumes;
265    }
266
267    /**
268     * Adds a default value for the query parameter
269     *
270     * @param paramName   query parameter name
271     * @param defaultValue the default value
272     */
273    public void addDefaultValue(String paramName, String defaultValue) {
274        if (defaultValues == null) {
275            defaultValues = new HashMap<>();
276        }
277        defaultValues.put(paramName, defaultValue);
278    }
279
280    /**
281     * Adds a required query parameter
282     *
283     * @param paramName   query parameter name
284     */
285    public void addRequiredQueryParameter(String paramName) {
286        if (requiredQueryParameters == null) {
287            requiredQueryParameters = new HashSet<>();
288        }
289        requiredQueryParameters.add(paramName);
290    }
291
292    /**
293     * Adds a required HTTP header
294     *
295     * @param headerName   HTTP header name
296     */
297    public void addRequiredHeader(String headerName) {
298        if (requiredHeaders == null) {
299            requiredHeaders = new HashSet<>();
300        }
301        requiredHeaders.add(headerName);
302    }
303
304    public Boolean getRequiredBody() {
305        return requiredBody;
306    }
307
308    public void setRequiredBody(Boolean requiredBody) {
309        this.requiredBody = requiredBody;
310    }
311
312    /**
313     * Gets the registered default values for query parameters
314     */
315    public Map<String, String> getDefaultValues() {
316        return defaultValues;
317    }
318
319    /**
320     * Sets the component name that this definition will apply to  
321     */
322    public void setComponent(String component) {
323        this.component = component;
324    }
325
326    public String getComponent() {
327        return component;
328    }
329
330    /**
331     * To define the content type what the REST service consumes (accept as input), such as application/xml or application/json
332     */
333    public void setConsumes(String consumes) {
334        this.consumes = consumes;
335    }
336
337    public String getProduces() {
338        return produces;
339    }
340
341    /**
342     * To define the content type what the REST service produces (uses for output), such as application/xml or application/json
343     */
344    public void setProduces(String produces) {
345        this.produces = produces;
346    }
347
348    public RestBindingMode getBindingMode() {
349        return bindingMode;
350    }
351
352    /**
353     * Sets the binding mode to use.
354     * <p/>
355     * The default value is off
356     */
357    public void setBindingMode(RestBindingMode bindingMode) {
358        this.bindingMode = bindingMode;
359    }
360
361    public String getType() {
362        return type;
363    }
364
365    /**
366     * Sets the class name to use for binding from input to POJO for the incoming data
367     * <p/>
368     * The canonical name of the class of the input data. Append a [] to the end of the canonical name
369     * if you want the input to be an array type.
370     */
371    public void setType(String type) {
372        this.type = type;
373    }
374
375    public String getOutType() {
376        return outType;
377    }
378
379    /**
380     * Sets the class name to use for binding from POJO to output for the outgoing data
381     * <p/>
382     * The canonical name of the class of the input data. Append a [] to the end of the canonical name
383     * if you want the input to be an array type.
384     */
385    public void setOutType(String outType) {
386        this.outType = outType;
387    }
388
389    public Boolean getSkipBindingOnErrorCode() {
390        return skipBindingOnErrorCode;
391    }
392
393    /**
394     * Whether to skip binding on output if there is a custom HTTP error code header.
395     * This allows to build custom error messages that do not bind to json / xml etc, as success messages otherwise will do.
396     */
397    public void setSkipBindingOnErrorCode(Boolean skipBindingOnErrorCode) {
398        this.skipBindingOnErrorCode = skipBindingOnErrorCode;
399    }
400
401    public Boolean getClientRequestValidation() {
402        return clientRequestValidation;
403    }
404
405    /**
406     * Whether to enable validation of the client request to check whether the Content-Type and Accept headers from
407     * the client is supported by the Rest-DSL configuration of its consumes/produces settings.
408     * <p/>
409     * This can be turned on, to enable this check. In case of validation error, then HTTP Status codes 415 or 406 is returned.
410     * <p/>
411     * The default value is false.
412     */
413    public void setClientRequestValidation(Boolean clientRequestValidation) {
414        this.clientRequestValidation = clientRequestValidation;
415    }
416
417    public Boolean getEnableCORS() {
418        return enableCORS;
419    }
420
421    /**
422     * Whether to enable CORS headers in the HTTP response.
423     * <p/>
424     * The default value is false.
425     */
426    public void setEnableCORS(Boolean enableCORS) {
427        this.enableCORS = enableCORS;
428    }
429
430    @Override
431    public String getLabel() {
432        return "";
433    }
434}