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