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(description = "Sets the name of a parameter to be passed in the exchange In Body")
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<>(endpointPropertyNames);
136        // add inBody argument for producers
137        if (inBody != null) {
138            arguments.add(inBody);
139        }
140
141        interceptPropertyNames(arguments);
142
143        // create a list of candidate methods
144        candidates = new ArrayList<>();
145        candidates.addAll(methodHelper.getCandidateMethods(methodName, arguments));
146        candidates = Collections.unmodifiableList(candidates);
147
148        // error if there are no candidates
149        if (candidates.isEmpty()) {
150            throw new IllegalArgumentException(
151                    String.format("No matching method for %s/%s, with arguments %s",
152                            apiName.getName(), methodName, arguments));
153        }
154
155        // log missing/extra properties for debugging
156        if (log.isDebugEnabled()) {
157            final Set<String> missing = methodHelper.getMissingProperties(methodName, arguments);
158            if (!missing.isEmpty()) {
159                log.debug("Method {} could use one or more properties from {}", methodName, missing);
160            }
161        }
162    }
163
164    @Override
165    public void interceptPropertyNames(Set<String> propertyNames) {
166        // do nothing by default
167    }
168
169    @Override
170    public void interceptProperties(Map<String, Object> properties) {
171        // do nothing by default
172    }
173
174    /**
175     * Returns endpoint configuration object.
176     * One of the generated *EndpointConfiguration classes that extends component configuration class.
177     *
178     * @return endpoint configuration object
179     */
180    public final T getConfiguration() {
181        return configuration;
182    }
183
184    /**
185     * Returns API name.
186     * @return apiName property.
187     */
188    public final E getApiName() {
189        return apiName;
190    }
191
192    /**
193     * Returns method name.
194     * @return methodName property.
195     */
196    public final String getMethodName() {
197        return methodName;
198    }
199
200    /**
201     * Returns method helper.
202     * @return methodHelper property.
203     */
204    public final ApiMethodHelper<? extends ApiMethod> getMethodHelper() {
205        return methodHelper;
206    }
207
208    /**
209     * Returns candidate methods for this endpoint.
210     * @return list of candidate methods.
211     */
212    public final List<ApiMethod> getCandidates() {
213        return candidates;
214    }
215
216    /**
217     * Returns name of parameter passed in the exchange In Body.
218     * @return inBody property.
219     */
220    public final String getInBody() {
221        return inBody;
222    }
223
224    /**
225     * Sets the name of a parameter to be passed in the exchange In Body.
226     * @param inBody parameter name
227     * @throws IllegalArgumentException for invalid parameter name.
228     */
229    public final void setInBody(String inBody) throws IllegalArgumentException {
230        // validate property name
231        ObjectHelper.notNull(inBody, "inBody");
232        if (!getPropertiesHelper().getValidEndpointProperties(getConfiguration()).contains(inBody)) {
233            throw new IllegalArgumentException("Unknown property " + inBody);
234        }
235        this.inBody = inBody;
236    }
237
238    public final Set<String> getEndpointPropertyNames() {
239        return endpointPropertyNames;
240    }
241
242    public final Map<String, Object> getEndpointProperties() {
243        return endpointProperties;
244    }
245
246    /**
247     * Returns an instance of an API Proxy based on apiName, method and args.
248     * Called by {@link AbstractApiConsumer} or {@link AbstractApiProducer}.
249     *
250     * @param method method about to be invoked
251     * @param args method arguments
252     * @return a Java object that implements the method to be invoked.
253     * @see AbstractApiProducer
254     * @see AbstractApiConsumer
255     */
256    public abstract Object getApiProxy(ApiMethod method, Map<String, Object> args);
257
258    private static ExecutorService getExecutorService(
259        Class<? extends AbstractApiEndpoint> endpointClass, CamelContext context, String threadProfileName) {
260
261        // lookup executorService for extending class name
262        final String endpointClassName = endpointClass.getName();
263        ExecutorService executorService = executorServiceMap.get(endpointClassName);
264
265        // CamelContext will shutdown thread pool when it shutdown so we can
266        // lazy create it on demand
267        // but in case of hot-deploy or the likes we need to be able to
268        // re-create it (its a shared static instance)
269        if (executorService == null || executorService.isTerminated() || executorService.isShutdown()) {
270            final ExecutorServiceManager manager = context.getExecutorServiceManager();
271
272            // try to lookup a pool first based on profile
273            ThreadPoolProfile poolProfile = manager.getThreadPoolProfile(
274                threadProfileName);
275            if (poolProfile == null) {
276                poolProfile = manager.getDefaultThreadPoolProfile();
277            }
278
279            // create a new pool using the custom or default profile
280            executorService = manager.newScheduledThreadPool(endpointClass, threadProfileName, poolProfile);
281
282            executorServiceMap.put(endpointClassName, executorService);
283        }
284
285        return executorService;
286    }
287
288    public final ExecutorService getExecutorService() {
289        if (this.executorService == null) {
290            // synchronize on class to avoid creating duplicate class level executors
291            synchronized (getClass()) {
292                this.executorService = getExecutorService(getClass(), getCamelContext(), getThreadProfileName());
293            }
294        }
295        return this.executorService;
296    }
297
298    /**
299     * Returns Thread profile name. Generated as a constant THREAD_PROFILE_NAME in *Constants.
300     * @return thread profile name to use.
301     */
302    protected abstract String getThreadProfileName();
303}