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.ArrayList;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.concurrent.ConcurrentHashMap;
027import java.util.concurrent.ExecutorService;
028
029import org.apache.camel.CamelContext;
030import org.apache.camel.Component;
031import org.apache.camel.impl.DefaultEndpoint;
032import org.apache.camel.spi.ExecutorServiceManager;
033import org.apache.camel.spi.ThreadPoolProfile;
034import org.apache.camel.spi.UriParam;
035import org.apache.camel.util.EndpointHelper;
036import org.apache.camel.util.ObjectHelper;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040/**
041 * Abstract base class for API Component Endpoints.
042 */
043public abstract class AbstractApiEndpoint<E extends ApiName, T>
044    extends DefaultEndpoint implements PropertyNamesInterceptor, PropertiesInterceptor {
045
046    // thread pool executor with Endpoint Class name as keys
047    private static Map<String, ExecutorService> executorServiceMap = new ConcurrentHashMap<String, ExecutorService>();
048
049    // logger
050    protected final Logger log = LoggerFactory.getLogger(getClass());
051
052    // API name
053    protected final E apiName;
054
055    // API method name
056    protected final String methodName;
057
058    // API method helper
059    protected final ApiMethodHelper<? extends ApiMethod> methodHelper;
060
061    // endpoint configuration
062    protected final T configuration;
063
064    // property name for Exchange 'In' message body
065    @UriParam
066    protected String inBody;
067
068    // candidate methods based on method name and endpoint configuration
069    private List<ApiMethod> candidates;
070
071    // cached Executor service
072    private ExecutorService executorService;
073
074    // cached property names and values
075    private Set<String> endpointPropertyNames;
076    private Map<String, Object> endpointProperties;
077
078    public AbstractApiEndpoint(String endpointUri, Component component,
079                               E apiName, String methodName, ApiMethodHelper<? extends ApiMethod> methodHelper, T endpointConfiguration) {
080        super(endpointUri, component);
081
082        this.apiName = apiName;
083        this.methodName = methodName;
084        this.methodHelper = methodHelper;
085        this.configuration = endpointConfiguration;
086    }
087
088    public boolean isSingleton() {
089        return true;
090    }
091
092    /**
093     * Returns generated helper that extends {@link ApiMethodPropertiesHelper} to work with API properties.
094     * @return properties helper.
095     */
096    protected abstract ApiMethodPropertiesHelper<T> getPropertiesHelper();
097
098    @Override
099    public void configureProperties(Map<String, Object> options) {
100        super.configureProperties(options);
101
102        // set configuration properties first
103        try {
104            T configuration = getConfiguration();
105            EndpointHelper.setReferenceProperties(getCamelContext(), configuration, options);
106            EndpointHelper.setProperties(getCamelContext(), configuration, options);
107        } catch (Exception e) {
108            throw new IllegalArgumentException(e);
109        }
110
111        // validate and initialize state
112        initState();
113
114        afterConfigureProperties();
115    }
116
117    /**
118     * Initialize proxies, create server connections, etc. after endpoint properties have been configured.
119     */
120    protected abstract void afterConfigureProperties();
121
122    /**
123     * Initialize endpoint state, including endpoint arguments, find candidate methods, etc.
124     */
125    private void initState() {
126
127        // compute endpoint property names and values
128        this.endpointPropertyNames = Collections.unmodifiableSet(
129            getPropertiesHelper().getEndpointPropertyNames(configuration));
130        final HashMap<String, Object> properties = new HashMap<String, Object>();
131        getPropertiesHelper().getEndpointProperties(configuration, properties);
132        this.endpointProperties = Collections.unmodifiableMap(properties);
133
134        // get endpoint property names
135        final Set<String> arguments = new HashSet<String>();
136        arguments.addAll(endpointPropertyNames);
137
138        // add inBody argument for producers
139        if (inBody != null) {
140            arguments.add(inBody);
141        }
142
143        interceptPropertyNames(arguments);
144
145        final String[] argNames = arguments.toArray(new String[arguments.size()]);
146
147        // create a list of candidate methods
148        candidates = new ArrayList<ApiMethod>();
149        candidates.addAll(methodHelper.getCandidateMethods(methodName, argNames));
150        candidates = Collections.unmodifiableList(candidates);
151
152        // error if there are no candidates
153        if (candidates.isEmpty()) {
154            throw new IllegalArgumentException(
155                    String.format("No matching method for %s/%s, with arguments %s",
156                            apiName.getName(), methodName, arguments));
157        }
158
159        // log missing/extra properties for debugging
160        if (log.isDebugEnabled()) {
161            final Set<String> missing = methodHelper.getMissingProperties(methodName, arguments);
162            if (!missing.isEmpty()) {
163                log.debug("Method {} could use one or more properties from {}", methodName, missing);
164            }
165        }
166    }
167
168    @Override
169    public void interceptPropertyNames(Set<String> propertyNames) {
170        // do nothing by default
171    }
172
173    @Override
174    public void interceptProperties(Map<String, Object> properties) {
175        // do nothing by default
176    }
177
178    /**
179     * Returns endpoint configuration object.
180     * One of the generated *EndpointConfiguration classes that extends component configuration class.
181     *
182     * @return endpoint configuration object
183     */
184    public final T getConfiguration() {
185        return configuration;
186    }
187
188    /**
189     * Returns API name.
190     * @return apiName property.
191     */
192    public final E getApiName() {
193        return apiName;
194    }
195
196    /**
197     * Returns method name.
198     * @return methodName property.
199     */
200    public final String getMethodName() {
201        return methodName;
202    }
203
204    /**
205     * Returns method helper.
206     * @return methodHelper property.
207     */
208    public final ApiMethodHelper<? extends ApiMethod> getMethodHelper() {
209        return methodHelper;
210    }
211
212    /**
213     * Returns candidate methods for this endpoint.
214     * @return list of candidate methods.
215     */
216    public final List<ApiMethod> getCandidates() {
217        return candidates;
218    }
219
220    /**
221     * Returns name of parameter passed in the exchange In Body.
222     * @return inBody property.
223     */
224    public final String getInBody() {
225        return inBody;
226    }
227
228    /**
229     * Sets the name of a parameter to be passed in the exchange In Body.
230     * @param inBody parameter name
231     * @throws IllegalArgumentException for invalid parameter name.
232     */
233    public final void setInBody(String inBody) throws IllegalArgumentException {
234        // validate property name
235        ObjectHelper.notNull(inBody, "inBody");
236        if (!getPropertiesHelper().getValidEndpointProperties(getConfiguration()).contains(inBody)) {
237            throw new IllegalArgumentException("Unknown property " + inBody);
238        }
239        this.inBody = inBody;
240    }
241
242    public final Set<String> getEndpointPropertyNames() {
243        return endpointPropertyNames;
244    }
245
246    public final Map<String, Object> getEndpointProperties() {
247        return endpointProperties;
248    }
249
250    /**
251     * Returns an instance of an API Proxy based on apiName, method and args.
252     * Called by {@link AbstractApiConsumer} or {@link AbstractApiProducer}.
253     *
254     * @param method method about to be invoked
255     * @param args method arguments
256     * @return a Java object that implements the method to be invoked.
257     * @see AbstractApiProducer
258     * @see AbstractApiConsumer
259     */
260    public abstract Object getApiProxy(ApiMethod method, Map<String, Object> args);
261
262    private static ExecutorService getExecutorService(
263        Class<? extends AbstractApiEndpoint> endpointClass, CamelContext context, String threadProfileName) {
264
265        // lookup executorService for extending class name
266        final String endpointClassName = endpointClass.getName();
267        ExecutorService executorService = executorServiceMap.get(endpointClassName);
268
269        // CamelContext will shutdown thread pool when it shutdown so we can
270        // lazy create it on demand
271        // but in case of hot-deploy or the likes we need to be able to
272        // re-create it (its a shared static instance)
273        if (executorService == null || executorService.isTerminated() || executorService.isShutdown()) {
274            final ExecutorServiceManager manager = context.getExecutorServiceManager();
275
276            // try to lookup a pool first based on profile
277            ThreadPoolProfile poolProfile = manager.getThreadPoolProfile(
278                threadProfileName);
279            if (poolProfile == null) {
280                poolProfile = manager.getDefaultThreadPoolProfile();
281            }
282
283            // create a new pool using the custom or default profile
284            executorService = manager.newScheduledThreadPool(endpointClass, threadProfileName, poolProfile);
285
286            executorServiceMap.put(endpointClassName, executorService);
287        }
288
289        return executorService;
290    }
291
292    public final ExecutorService getExecutorService() {
293        if (this.executorService == null) {
294            // synchronize on class to avoid creating duplicate class level executors
295            synchronized (getClass()) {
296                this.executorService = getExecutorService(getClass(), getCamelContext(), getThreadProfileName());
297            }
298        }
299        return this.executorService;
300    }
301
302    /**
303     * Returns Thread profile name. Generated as a constant THREAD_PROFILE_NAME in *Constants.
304     * @return thread profile name to use.
305     */
306    protected abstract String getThreadProfileName();
307}