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(
143                candidates,
144                ApiMethodHelper.MatchType.SUPER_SET,
145                argNames);
146
147            // get the method to call
148            if (filteredMethods.isEmpty()) {
149                throw new RuntimeCamelException(String.format("Missing properties for %s, need one or more from %s",
150                    endpoint.getMethodName(),
151                    methodHelper.getMissingProperties(endpoint.getMethodName(), argNames))
152                );
153            } else if (filteredMethods.size() == 1) {
154                // found an exact match
155                method = filteredMethods.get(0);
156            } else {
157                method = ApiMethodHelper.getHighestPriorityMethod(filteredMethods);
158                log.warn("Calling highest priority operation {} from operations {}", method, filteredMethods);
159            }
160        }
161
162        return method;
163    }
164
165    // returns false on exception, which is set in exchange
166    private boolean processInBody(Exchange exchange, Map<String, Object> properties) {
167        final String inBodyProperty = endpoint.getInBody();
168        if (inBodyProperty != null) {
169
170            Object value = exchange.getIn().getBody();
171            if (value != null) {
172                try {
173                    value = endpoint.getCamelContext().getTypeConverter().mandatoryConvertTo(
174                        endpoint.getConfiguration().getClass().getDeclaredField(inBodyProperty).getType(),
175                        exchange, value);
176                } catch (Exception e) {
177                    exchange.setException(new RuntimeCamelException(String.format(
178                            "Error converting value %s to property %s: %s", value, inBodyProperty, e.getMessage()), e));
179
180                    return false;
181                }
182            } else {
183                // allow null values for inBody only if its a nullable option
184                if (!methodHelper.getNullableArguments().contains(inBodyProperty)) {
185                    exchange.setException(new NullPointerException(inBodyProperty));
186
187                    return false;
188                }
189            }
190
191            log.debug("Property [{}] has message body value {}", inBodyProperty, value);
192            properties.put(inBodyProperty, value);
193        }
194
195        return true;
196    }
197}