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                Class<?> clazz = null;
094                String type = definition.getType();
095                if (type != null) {
096                    String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type;
097                    clazz = context.getClassResolver().resolveMandatoryClass(typeName);
098                }
099                if (clazz != null) {
100                    context.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(context, json, "unmarshalType", clazz);
101                    context.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(context, json, "useList", type.endsWith("[]"));
102                }
103                setAdditionalConfiguration(config, context, json, "json.in.");
104
105                Class<?> outClazz = null;
106                String outType = definition.getOutType();
107                if (outType != null) {
108                    String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType;
109                    outClazz = context.getClassResolver().resolveMandatoryClass(typeName);
110                }
111                if (outClazz != null) {
112                    context.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(context, outJson, "unmarshalType", outClazz);
113                    context.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(context, outJson, "useList", outType.endsWith("[]"));
114                }
115                setAdditionalConfiguration(config, context, outJson, "json.out.");
116            }
117        }
118
119        // setup xml data format
120        DataFormat jaxb = null;
121        DataFormat outJaxb = null;
122        if (mode.contains("xml") || "auto".equals(mode)) {
123            String name = config.getXmlDataFormat();
124            if (name != null) {
125                // must only be a name, not refer to an existing instance
126                Object instance = context.getRegistry().lookupByName(name);
127                if (instance != null) {
128                    throw new IllegalArgumentException("XmlDataFormat name: " + name + " must not be an existing bean instance from the registry");
129                }
130            } else {
131                name = "jaxb";
132            }
133            // this will create a new instance as the name was not already
134            // pre-created
135            jaxb = context.resolveDataFormat(name);
136            outJaxb = context.resolveDataFormat(name);
137
138            // is xml binding required?
139            if (mode.contains("xml") && jaxb == null) {
140                throw new IllegalArgumentException("XML DataFormat " + name + " not found.");
141            }
142
143            if (jaxb != null) {
144                Class<?> clazz = null;
145                String type = definition.getType();
146                if (type != null) {
147                    String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type;
148                    clazz = context.getClassResolver().resolveMandatoryClass(typeName);
149                }
150                if (clazz != null) {
151                    JAXBContext jc = JAXBContext.newInstance(clazz);
152                    context.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(context, jaxb, "context", jc);
153                }
154                setAdditionalConfiguration(config, context, jaxb, "xml.in.");
155
156                Class<?> outClazz = null;
157                String outType = definition.getOutType();
158                if (outType != null) {
159                    String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType;
160                    outClazz = context.getClassResolver().resolveMandatoryClass(typeName);
161                }
162                if (outClazz != null) {
163                    JAXBContext jc = JAXBContext.newInstance(outClazz);
164                    context.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(context, outJaxb, "context", jc);
165                } else if (clazz != null) {
166                    // fallback and use the context from the input
167                    JAXBContext jc = JAXBContext.newInstance(clazz);
168                    context.adapt(ExtendedCamelContext.class).getBeanIntrospection().setProperty(context, outJaxb, "context", jc);
169                }
170                setAdditionalConfiguration(config, context, outJaxb, "xml.out.");
171            }
172        }
173
174        return new RestBindingAdvice(context, json, jaxb, outJson, outJaxb, definition.getConsumes(), definition.getProduces(), mode, skip, validation, cors, corsHeaders,
175                                     definition.getDefaultValues(), definition.getRequiredBody() != null ? definition.getRequiredBody() : false,
176                                     definition.getRequiredQueryParameters(), definition.getRequiredHeaders());
177    }
178
179    private void setAdditionalConfiguration(RestConfiguration config, CamelContext context, DataFormat dataFormat, String prefix) throws Exception {
180        if (config.getDataFormatProperties() != null && !config.getDataFormatProperties().isEmpty()) {
181            // must use a copy as otherwise the options gets removed during
182            // introspection setProperties
183            Map<String, Object> copy = new HashMap<>();
184
185            // filter keys on prefix
186            // - either its a known prefix and must match the prefix parameter
187            // - or its a common configuration that we should always use
188            for (Map.Entry<String, Object> entry : config.getDataFormatProperties().entrySet()) {
189                String key = entry.getKey();
190                String copyKey;
191                boolean known = isKeyKnownPrefix(key);
192                if (known) {
193                    // remove the prefix from the key to use
194                    copyKey = key.substring(prefix.length());
195                } else {
196                    // use the key as is
197                    copyKey = key;
198                }
199                if (!known || key.startsWith(prefix)) {
200                    copy.put(copyKey, entry.getValue());
201                }
202            }
203
204            PropertyBindingSupport.build().bind(context, dataFormat, copy);
205        }
206    }
207
208    private boolean isKeyKnownPrefix(String key) {
209        return key.startsWith("json.in.") || key.startsWith("json.out.") || key.startsWith("xml.in.") || key.startsWith("xml.out.");
210    }
211
212}