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.util.component;
018
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Set;
023
024import org.apache.camel.AsyncCallback;
025import org.apache.camel.Exchange;
026import org.apache.camel.RuntimeCamelException;
027import org.apache.camel.impl.DefaultAsyncProducer;
028import org.apache.camel.util.ObjectHelper;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032/**
033 * Base class for API based Producers
034 */
035public abstract class AbstractApiProducer<E extends Enum<E> & ApiName, T>
036    extends DefaultAsyncProducer implements PropertiesInterceptor, ResultInterceptor {
037
038    // API Endpoint
039    protected final AbstractApiEndpoint<E, T> endpoint;
040
041    // properties helper
042    protected final ApiMethodPropertiesHelper<T> propertiesHelper;
043
044    // method helper
045    protected final ApiMethodHelper<?> methodHelper;
046
047    // logger
048    private final transient Logger log = LoggerFactory.getLogger(getClass());
049
050    public AbstractApiProducer(AbstractApiEndpoint<E, T> endpoint, ApiMethodPropertiesHelper<T> propertiesHelper) {
051        super(endpoint);
052        this.propertiesHelper = propertiesHelper;
053        this.endpoint = endpoint;
054        this.methodHelper = endpoint.getMethodHelper();
055    }
056
057    @Override
058    public boolean process(final Exchange exchange, final AsyncCallback callback) {
059        // properties for method arguments
060        final Map<String, Object> properties = new HashMap<String, Object>();
061        properties.putAll(endpoint.getEndpointProperties());
062        propertiesHelper.getExchangeProperties(exchange, properties);
063
064        // let the endpoint and the Producer intercept properties
065        endpoint.interceptProperties(properties);
066        interceptProperties(properties);
067
068        // decide which method to invoke
069        final ApiMethod method = findMethod(exchange, properties);
070        if (method == null) {
071            // synchronous failure
072            callback.done(true);
073            return true;
074        }
075
076        // create a runnable invocation task to be submitted on a background thread pool
077        // this way we avoid blocking the current thread for long running methods
078        Runnable invocation = new Runnable() {
079            @Override
080            public void run() {
081                try {
082                    if (log.isDebugEnabled()) {
083                        log.debug("Invoking operation {} with {}", method.getName(), properties.keySet());
084                    }
085
086                    Object result = doInvokeMethod(method, properties);
087
088                    // producer returns a single response, even for methods with List return types
089                    exchange.getOut().setBody(result);
090                    // copy headers
091                    exchange.getOut().setHeaders(exchange.getIn().getHeaders());
092
093                    interceptResult(result, exchange);
094
095                } catch (Throwable t) {
096                    exchange.setException(ObjectHelper.wrapRuntimeCamelException(t));
097                } finally {
098                    callback.done(false);
099                }
100            }
101        };
102
103        endpoint.getExecutorService().submit(invocation);
104        return false;
105    }
106
107    @Override
108    public void interceptProperties(Map<String, Object> properties) {
109        // do nothing by default
110    }
111
112    /**
113     * Invoke the API method. Derived classes can override, but MUST call super.doInvokeMethod().
114     * @param method API method to invoke.
115     * @param properties method arguments from endpoint properties and exchange In headers.
116     * @return API method invocation result.
117     * @throws RuntimeCamelException on error. Exceptions thrown by API method are wrapped.
118     */
119    protected Object doInvokeMethod(ApiMethod method, Map<String, Object> properties) throws RuntimeCamelException {
120        return ApiMethodHelper.invokeMethod(endpoint.getApiProxy(method, properties), method, properties);
121    }
122
123    @Override
124    public final Object splitResult(Object result) {
125        // producer never splits results
126        return result;
127    }
128
129    @Override
130    public void interceptResult(Object methodResult, Exchange resultExchange) {
131        // do nothing by default
132    }
133
134    protected ApiMethod findMethod(Exchange exchange, Map<String, Object> properties) {
135
136        ApiMethod method = null;
137        final List<ApiMethod> candidates = endpoint.getCandidates();
138        if (processInBody(exchange, properties)) {
139
140            // filter candidates based on endpoint and exchange properties
141            final Set<String> argNames = properties.keySet();
142            final List<ApiMethod> filteredMethods = methodHelper.filterMethods(candidates,
143                    ApiMethodHelper.MatchType.SUPER_SET,
144                    argNames.toArray(new String[argNames.size()]));
145
146            // get the method to call
147            if (filteredMethods.isEmpty()) {
148                final Set<String> missing = methodHelper.getMissingProperties(endpoint.getMethodName(), argNames);
149                throw new RuntimeCamelException(String.format("Missing properties for %s, need one or more from %s",
150                        endpoint.getMethodName(), missing));
151            } else if (filteredMethods.size() == 1) {
152                // found an exact match
153                method = filteredMethods.get(0);
154            } else {
155                method = ApiMethodHelper.getHighestPriorityMethod(filteredMethods);
156                log.warn("Calling highest priority operation {} from operations {}", method, filteredMethods);
157            }
158        }
159
160        return method;
161    }
162
163    // returns false on exception, which is set in exchange
164    private boolean processInBody(Exchange exchange, Map<String, Object> properties) {
165        final String inBodyProperty = endpoint.getInBody();
166        if (inBodyProperty != null) {
167
168            Object value = exchange.getIn().getBody();
169            if (value != null) {
170                try {
171                    value = endpoint.getCamelContext().getTypeConverter().mandatoryConvertTo(
172                        endpoint.getConfiguration().getClass().getDeclaredField(inBodyProperty).getType(),
173                        exchange, value);
174                } catch (Exception e) {
175                    exchange.setException(new RuntimeCamelException(String.format(
176                            "Error converting value %s to property %s: %s", value, inBodyProperty, e.getMessage()), e));
177
178                    return false;
179                }
180            } else {
181                // allow null values for inBody only if its a nullable option
182                if (!methodHelper.getNullableArguments().contains(inBodyProperty)) {
183                    exchange.setException(new NullPointerException(inBodyProperty));
184
185                    return false;
186                }
187            }
188
189            log.debug("Property [{}] has message body value {}", inBodyProperty, value);
190            properties.put(inBodyProperty, value);
191        }
192
193        return true;
194    }
195}