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