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.reifier.rest;
018
019import java.util.HashMap;
020import java.util.Map;
021
022import javax.xml.bind.JAXBContext;
023
024import org.apache.camel.ExtendedCamelContext;
025import org.apache.camel.model.rest.RestBindingDefinition;
026import org.apache.camel.model.rest.RestBindingMode;
027import org.apache.camel.processor.RestBindingAdvice;
028import org.apache.camel.reifier.AbstractReifier;
029import org.apache.camel.spi.DataFormat;
030import org.apache.camel.spi.RestConfiguration;
031import org.apache.camel.spi.RouteContext;
032import org.apache.camel.support.PropertyBindingSupport;
033
034public class RestBindingReifier extends AbstractReifier {
035
036    private final RestBindingDefinition definition;
037
038    public RestBindingReifier(RouteContext routeContext, RestBindingDefinition definition) {
039        super(routeContext);
040        this.definition = definition;
041    }
042
043    public RestBindingAdvice createRestBindingAdvice() throws Exception {
044        RestConfiguration config = camelContext.getRestConfiguration(definition.getComponent(), true);
045
046        // these options can be overridden per rest verb
047        String mode = config.getBindingMode().name();
048        if (definition.getBindingMode() != null) {
049            mode = parse(RestBindingMode.class, definition.getBindingMode()).name();
050        }
051        boolean cors = config.isEnableCORS();
052        if (definition.getEnableCORS() != null) {
053            cors = parseBoolean(definition.getEnableCORS(), false);
054        }
055        boolean skip = config.isSkipBindingOnErrorCode();
056        if (definition.getSkipBindingOnErrorCode() != null) {
057            skip = parseBoolean(definition.getSkipBindingOnErrorCode(), false);
058        }
059        boolean validation = config.isClientRequestValidation();
060        if (definition.getClientRequestValidation() != null) {
061            validation = parseBoolean(definition.getClientRequestValidation(), false);
062        }
063
064        // cors headers
065        Map<String, String> corsHeaders = config.getCorsHeaders();
066
067        if (mode == null || "off".equals(mode)) {
068            // binding mode is off, so create a off mode binding processor
069            return new RestBindingAdvice(camelContext, null, null, null, null,
070                                         parseString(definition.getConsumes()), parseString(definition.getProduces()), mode, skip, validation, cors, corsHeaders,
071                                         definition.getDefaultValues(), definition.getRequiredBody() != null ? definition.getRequiredBody() : false,
072                                         definition.getRequiredQueryParameters(), definition.getRequiredHeaders());
073        }
074
075        // setup json data format
076        DataFormat json = null;
077        DataFormat outJson = null;
078        if (mode.contains("json") || "auto".equals(mode)) {
079            String name = config.getJsonDataFormat();
080            if (name != null) {
081                // must only be a name, not refer to an existing instance
082                Object instance = camelContext.getRegistry().lookupByName(name);
083                if (instance != null) {
084                    throw new IllegalArgumentException("JsonDataFormat name: " + name + " must not be an existing bean instance from the registry");
085                }
086            } else {
087                name = "json-jackson";
088            }
089            // this will create a new instance as the name was not already
090            // pre-created
091            json = camelContext.resolveDataFormat(name);
092            outJson = camelContext.resolveDataFormat(name);
093
094            if (json != null) {
095                setupJson(config, parseString(definition.getType()), parseString(definition.getOutType()), json, outJson);
096            }
097        }
098
099        // setup xml data format
100        DataFormat jaxb = null;
101        DataFormat outJaxb = null;
102        if (mode.contains("xml") || "auto".equals(mode)) {
103            String name = config.getXmlDataFormat();
104            if (name != null) {
105                // must only be a name, not refer to an existing instance
106                Object instance = camelContext.getRegistry().lookupByName(name);
107                if (instance != null) {
108                    throw new IllegalArgumentException("XmlDataFormat name: " + name + " must not be an existing bean instance from the registry");
109                }
110            } else {
111                name = "jaxb";
112            }
113            // this will create a new instance as the name was not already
114            // pre-created
115            jaxb = camelContext.resolveDataFormat(name);
116            outJaxb = camelContext.resolveDataFormat(name);
117
118            // is xml binding required?
119            if (mode.contains("xml") && jaxb == null) {
120                throw new IllegalArgumentException("XML DataFormat " + name + " not found.");
121            }
122
123            if (jaxb != null) {
124                setupJaxb(config, parseString(definition.getType()), parseString(definition.getOutType()), jaxb, outJaxb);
125            }
126        }
127
128        return new RestBindingAdvice(camelContext, json, jaxb, outJson, outJaxb,
129                                     parseString(definition.getConsumes()), parseString(definition.getProduces()),
130                                     mode, skip, validation, cors, corsHeaders,
131                                     definition.getDefaultValues(), definition.getRequiredBody() != null ? definition.getRequiredBody() : false,
132                                     definition.getRequiredQueryParameters(), definition.getRequiredHeaders());
133    }
134
135    protected void setupJson(RestConfiguration config, String type, String outType, DataFormat json, DataFormat outJson) throws Exception {
136        Class<?> clazz = null;
137        if (type != null) {
138            String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type;
139            clazz = camelContext.getClassResolver().resolveMandatoryClass(typeName);
140        }
141        if (clazz != null) {
142            camelContext.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(camelContext, json, "unmarshalType", clazz);
143            camelContext.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(camelContext, json, "useList", type.endsWith("[]"));
144        }
145        setAdditionalConfiguration(config, json, "json.in.");
146
147        Class<?> outClazz = null;
148        if (outType != null) {
149            String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType;
150            outClazz = camelContext.getClassResolver().resolveMandatoryClass(typeName);
151        }
152        if (outClazz != null) {
153            camelContext.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(camelContext, outJson, "unmarshalType", outClazz);
154            camelContext.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(camelContext, outJson, "useList", outType.endsWith("[]"));
155        }
156        setAdditionalConfiguration(config, outJson, "json.out.");
157    }
158
159    protected void setupJaxb(RestConfiguration config, String type, String outType, DataFormat jaxb, DataFormat outJaxb) throws Exception {
160        Class<?> clazz = null;
161        if (type != null) {
162            String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type;
163            clazz = camelContext.getClassResolver().resolveMandatoryClass(typeName);
164        }
165        if (clazz != null) {
166            JAXBContext jc = JAXBContext.newInstance(clazz);
167            setJaxbContext(jaxb, jc);
168        }
169        setAdditionalConfiguration(config, jaxb, "xml.in.");
170
171        Class<?> outClazz = null;
172        if (outType != null) {
173            String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType;
174            outClazz = camelContext.getClassResolver().resolveMandatoryClass(typeName);
175        }
176        if (outClazz != null) {
177            JAXBContext jc = JAXBContext.newInstance(outClazz);
178            setJaxbContext(outJaxb, jc);
179        } else if (clazz != null) {
180            // fallback and use the context from the input
181            JAXBContext jc = JAXBContext.newInstance(clazz);
182            setJaxbContext(outJaxb, jc);
183        }
184        setAdditionalConfiguration(config, outJaxb, "xml.out.");
185    }
186
187    private void setJaxbContext(DataFormat jaxb, JAXBContext jc) throws Exception {
188        camelContext.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(camelContext, jaxb, "context", jc);
189    }
190
191    private void setAdditionalConfiguration(RestConfiguration config, DataFormat dataFormat, String prefix) throws Exception {
192        if (config.getDataFormatProperties() != null && !config.getDataFormatProperties().isEmpty()) {
193            // must use a copy as otherwise the options gets removed during
194            // introspection setProperties
195            Map<String, Object> copy = new HashMap<>();
196
197            // filter keys on prefix
198            // - either its a known prefix and must match the prefix parameter
199            // - or its a common configuration that we should always use
200            for (Map.Entry<String, Object> entry : config.getDataFormatProperties().entrySet()) {
201                String key = entry.getKey();
202                String copyKey;
203                boolean known = isKeyKnownPrefix(key);
204                if (known) {
205                    // remove the prefix from the key to use
206                    copyKey = key.substring(prefix.length());
207                } else {
208                    // use the key as is
209                    copyKey = key;
210                }
211                if (!known || key.startsWith(prefix)) {
212                    copy.put(copyKey, entry.getValue());
213                }
214            }
215
216            PropertyBindingSupport.build().bind(camelContext, dataFormat, copy);
217        }
218    }
219
220    private boolean isKeyKnownPrefix(String key) {
221        return key.startsWith("json.in.") || key.startsWith("json.out.") || key.startsWith("xml.in.") || key.startsWith("xml.out.");
222    }
223
224}