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