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