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