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