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