001/*
002 * Copyright (C) 2019,2022 Michael N. Lipp (http://www.mnl.de)
003 * 
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *        http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package de.mnl.osgi.coreutils;
018
019import java.util.HashMap;
020import java.util.Map;
021import java.util.Optional;
022import java.util.function.Consumer;
023import java.util.function.Function;
024import org.osgi.framework.BundleActivator;
025import org.osgi.framework.BundleContext;
026import org.osgi.framework.Filter;
027import org.osgi.framework.ServiceReference;
028
029/**
030 * Maintains and attempts to resolve dependencies on services.
031 * <P>
032 * The class supports two usage pattern. The first is to use the
033 * {@code ServiceResolver} as base class for the bundle's activator.
034 * The derived class must override {@link #configure()} to add
035 * at least one dependency.
036 * <P>
037 * The methods {@link #onResolved()}, {@link #onDissolving()} and
038 * {@link #onRebound(String)} can be overridden as required.
039 * 
040 * <pre>
041 * public class Activator extends ServiceResolver {
042 *     &#x40;Override
043 *     protected void configure() {
044 *         addDependency(...);
045 *     }
046 *     
047 *     &#x40;Override
048 *     protected void onResolved() {
049 *         // Enable usage or register as service or start worker thread ...
050 *     }
051 *     
052 *     &#x40;Override
053 *     protected void onDissolving() {
054 *         // Disable usage or unregister or stop and join worker thread ...
055 *     }
056 *     
057 *     &#x40;Override
058 *     protected void onRebound(String dependency) {
059 *         // Only required if there is a "long term" usage of a dependency.
060 *     }
061 * }
062 * </pre>
063 * 
064 * The second usage pattern is to create a {@code ServiceResolver} with
065 * a {@link BundleContext}, add dependencies and callbacks as required
066 * and call {@link #open()}. When resolving is no longer required,
067 * {@link #close()} must be called.
068 * 
069 * <pre>
070 * // Usually executed during bundle start...
071 * ServiceResolver resolver = new ServiceResolver(context);
072 * // Add dependencies
073 * resolver.addDependency(...);
074 * resolver.setOnResolved(() -&gt; ...);
075 * resolver.setOnDissolving(() -&gt; ...);
076 * resolver.setOnRebound(name -&gt; ...);
077 * resolver.open();
078 * 
079 * // Usually executed during bundle stop...
080 * resolver.close();
081 * </pre>
082 * 
083 * In addition to the management of mandatory dependencies, the
084 * {@code ServiceResolver} can also keep track of optional
085 * service dependencies. This allows the {@code ServiceResolver}
086 * to be provided as unified service access point. Apart from
087 * a single {@link #open()} and {@link #close()} function, the
088 * {@code ServiceProvider} does not implement additional functions
089 * for optional service dependencies compared to the underlying
090 * {@link ServiceCollector}s.
091 */
092@SuppressWarnings({ "PMD.GodClass", "PMD.TooManyMethods" })
093public class ServiceResolver implements AutoCloseable, BundleActivator {
094
095    /**
096     * The bundle context. Made available for derived classes.
097     */
098    protected BundleContext context;
099    @SuppressWarnings("PMD.AvoidUsingVolatile")
100    private volatile boolean isOpen;
101    @SuppressWarnings("PMD.UseConcurrentHashMap")
102    private final Map<String, ServiceCollector<?, ?>> dependencies
103        = new HashMap<>();
104    @SuppressWarnings("PMD.UseConcurrentHashMap")
105    private final Map<String, ServiceCollector<?, ?>> optDependencies
106        = new HashMap<>();
107    private int resolvedCount;
108    private Runnable onResolved;
109    private Runnable onDissolving;
110    private Consumer<String> onRebound;
111
112    /**
113     * Creates a new resolver that uses the given context.
114     *
115     * @param context the context
116     */
117    public ServiceResolver(BundleContext context) {
118        this.context = context;
119    }
120
121    /**
122     * Constructor for using the {@code ServiceResolver} as base
123     * class for a {@link BundleActivator}.
124     */
125    @SuppressWarnings("PMD.UncommentedEmptyConstructor")
126    protected ServiceResolver() {
127    }
128
129    /**
130     * Called by the framework when using the {@code ServiceResolver} as 
131     * base class for a bundle activator.
132     * <P>
133     * The implementation sets the {@link #context} attribute and calls 
134     * {@link #configure()} and {@link #open()}. 
135     *
136     * @param context the context
137     * @throws Exception if a problem occurs
138     */
139    @Override
140    public void start(BundleContext context) throws Exception {
141        this.context = context;
142        configure();
143        open();
144    }
145
146    /**
147     * Configures the {@code ServiceResolver}. Must be overridden 
148     * by the derived class when using the {@code ServiceResolver}
149     * as base class for a bundle activator.
150     * The derived class must configure the resolver with calls to
151     * at least one of the {@code addDependency} methods.
152     * <P>
153     * The default implementation does nothing.
154     */
155    protected void configure() {
156        // Default does nothing.
157    }
158
159    /**
160     * Called when all dependencies have been resolved. Overriding this
161     * method is an alternative to setting a callback with
162     * {@link #setOnResolved(Runnable)}.
163     */
164    protected void onResolved() {
165        // Default does nothing.
166    }
167
168    /**
169     * Called when the resolver is about to leave the resolved state,
170     * i.e. when one of the mandatory services is going to be unbound.
171     * Overriding this method is an alternative to setting a callback with
172     * {@link #setOnDissolving(Runnable)}.
173     */
174    protected void onDissolving() {
175        // Default does nothing.
176    }
177
178    /**
179     * Called when the preferred service of a resolved dependency 
180     * changes. The change may either be a change of properties 
181     * reported by the framework or the replacement of the preferred 
182     * service with another service. Overriding this method is an 
183     * alternative to setting a callback with
184     * {@link #setOnRebound(Consumer)}.
185     *
186     * @param dependency the dependency that has been rebound
187     */
188    protected void onRebound(String dependency) {
189        // Default does nothing.
190    }
191
192    /**
193     * Called by the framework when using the {@code ServiceResolver} as 
194     * base class for a bundle activator. The implementatoin calls
195     * {@link #close()}.
196     *
197     * @param context the context
198     * @throws Exception if a problem occurs
199     */
200    @Override
201    public void stop(BundleContext context) throws Exception {
202        close();
203    }
204
205    /**
206     * Sets the function to called when the resolver has entered the
207     * resolved state, i.e. when all mandatory services have been bound.
208     *
209     * @param onResolved the function
210     * @return the service resolver
211     */
212    public ServiceResolver setOnResolved(Runnable onResolved) {
213        this.onResolved = onResolved;
214        return this;
215    }
216
217    /**
218     * Sets the function to called when the resolver is about to leave the
219     * resolved state, i.e. when one of the mandatory services is going
220     * to be unbound.
221     *
222     * @param onDissolving the function
223     * @return the service resolver
224     */
225    public ServiceResolver setOnDissolving(Runnable onDissolving) {
226        this.onDissolving = onDissolving;
227        return this;
228    }
229
230    /**
231     * Sets the function to be called when the preferred service
232     * of a resolved dependency changes. The change may either be
233     * a change of properties reported by the framework or the
234     * replacement of the preferred service with another service. 
235     *
236     * @param onRebound the on rebound
237     * @return the service resolver
238     */
239    public ServiceResolver setOnRebound(Consumer<String> onRebound) {
240        this.onRebound = onRebound;
241        return this;
242    }
243
244    /**
245     * Adds a mandatory dependency on the service specified by the
246     * class. The name of the class is used as name of the
247     * dependency.
248     *
249     * @param clazz the class
250     * @return the service resolver
251     */
252    public ServiceResolver addDependency(Class<?> clazz) {
253        addDependency(clazz.getName(), clazz.getName());
254        return this;
255    }
256
257    /**
258     * Adds a mandatory dependency on the service specified by the
259     * filter.
260     *
261     * @param name the name of the dependency
262     * @param filter the filter
263     * @return the service resolver
264     */
265    @SuppressWarnings("PMD.AvoidDuplicateLiterals")
266    public ServiceResolver addDependency(String name, Filter filter) {
267        synchronized (this) {
268            if (isOpen) {
269                throw new IllegalStateException(
270                    "Cannot add dependencies to open reolver.");
271            }
272            dependencies.put(name, new ServiceCollector<>(context, filter)
273                .setOnBound(this::boundCb).setOnUnbinding(this::unbindingCb)
274                .setOnModfied((ref, svc) -> modifiedCb(name)));
275            return this;
276        }
277    }
278
279    /**
280     * Adds a mandatory dependency on the service specified by the
281     * service reference.
282     *
283     * @param name the name of the dependency
284     * @param reference the reference
285     * @return the service resolver
286     */
287    public ServiceResolver addDependency(String name,
288            ServiceReference<?> reference) {
289        synchronized (this) {
290            if (isOpen) {
291                throw new IllegalStateException(
292                    "Cannot add dependencies to open reolver.");
293            }
294            dependencies.put(name, new ServiceCollector<>(context, reference)
295                .setOnBound(this::boundCb).setOnUnbinding(this::unbindingCb)
296                .setOnModfied((ref, svc) -> modifiedCb(name)));
297            return this;
298        }
299    }
300
301    /**
302     * Adds a mandatory dependency on the service specified by the
303     * service reference.
304     *
305     * @param name the name of the dependency
306     * @param className the class name
307     * @return the service resolver
308     */
309    public ServiceResolver addDependency(String name, String className) {
310        synchronized (this) {
311            if (isOpen) {
312                throw new IllegalStateException(
313                    "Cannot add dependencies to open reolver.");
314            }
315            dependencies.put(name, new ServiceCollector<>(context, className)
316                .setOnBound(this::boundCb).setOnUnbinding(this::unbindingCb)
317                .setOnModfied((ref, svc) -> modifiedCb(name)));
318            return this;
319        }
320    }
321
322    /**
323     * Adds an optional dependency on the service specified by the
324     * class. The name of the class is used as identifier for the
325     * dependency.
326     *
327     * @param clazz the class
328     * @return the service resolver
329     */
330    public ServiceResolver addOptionalDependency(Class<?> clazz) {
331        addOptionalDependency(clazz.getName(), clazz.getName());
332        return this;
333    }
334
335    /**
336     * Adds an optional dependency on the service specified by the
337     * filter.
338     *
339     * @param name the name of the dependency
340     * @param filter the filter
341     * @return the service resolver
342     */
343    public ServiceResolver addOptionalDependency(String name, Filter filter) {
344        synchronized (this) {
345            if (isOpen) {
346                throw new IllegalStateException(
347                    "Cannot add dependencies to open resolver.");
348            }
349            optDependencies.put(name, new ServiceCollector<>(context, filter)
350                .setOnModfied((ref, svc) -> modifiedCb(name)));
351            return this;
352        }
353    }
354
355    /**
356     * Adds an optional dependency on the service specified by the
357     * service reference.
358     *
359     * @param name the name of the dependency
360     * @param reference the reference
361     * @return the service resolver
362     */
363    public ServiceResolver addOptionalDependency(String name,
364            ServiceReference<?> reference) {
365        synchronized (this) {
366            if (isOpen) {
367                throw new IllegalStateException(
368                    "Cannot add dependencies to open reolver.");
369            }
370            optDependencies.put(name,
371                new ServiceCollector<>(context, reference)
372                    .setOnModfied((ref, svc) -> modifiedCb(name)));
373            return this;
374        }
375    }
376
377    /**
378     * Adds an optional dependency on the service specified by the
379     * service reference.
380     *
381     * @param name the name of the dependency
382     * @param className the class name
383     * @return the service resolver
384     */
385    public ServiceResolver addOptionalDependency(String name,
386            String className) {
387        synchronized (this) {
388            if (isOpen) {
389                throw new IllegalStateException(
390                    "Cannot add dependencies to open reolver.");
391            }
392            optDependencies.put(name,
393                new ServiceCollector<>(context, className)
394                    .setOnModfied((ref, svc) -> modifiedCb(name)));
395            return this;
396        }
397    }
398
399    @SuppressWarnings("PMD.UnusedFormalParameter")
400    private void boundCb(ServiceReference<?> reference, Object service) {
401        synchronized (this) {
402            resolvedCount += 1;
403            if (resolvedCount == dependencies.size()) {
404                Optional.ofNullable(onResolved).ifPresent(cb -> cb.run());
405                onResolved();
406            }
407        }
408    }
409
410    @SuppressWarnings("PMD.UnusedFormalParameter")
411    private void unbindingCb(ServiceReference<?> reference, Object service) {
412        synchronized (this) {
413            if (resolvedCount == dependencies.size()) {
414                Optional.ofNullable(onDissolving).ifPresent(cb -> cb.run());
415                onDissolving();
416            }
417            resolvedCount -= 1;
418        }
419    }
420
421    private void modifiedCb(String dependency) {
422        synchronized (this) {
423            Optional.ofNullable(onRebound)
424                .ifPresent(cb -> cb.accept(dependency));
425            onRebound(dependency);
426        }
427    }
428
429    /**
430     * Starts the resolver. While open, the resolver attempts
431     * to obtain service implementation for all registered
432     * service dependencies.
433     */
434    public void open() {
435        synchronized (this) {
436            if (isOpen) {
437                return;
438            }
439            resolvedCount = 0;
440        }
441        for (ServiceCollector<?, ?> coll : dependencies.values()) {
442            coll.open();
443        }
444        for (ServiceCollector<?, ?> coll : optDependencies.values()) {
445            coll.open();
446        }
447        isOpen = true;
448    }
449
450    /**
451     * Stops the resolver. All maintained services are released.
452     */
453    public void close() {
454        synchronized (this) {
455            if (!isOpen) {
456                return;
457            }
458            isOpen = false;
459            for (ServiceCollector<?, ?> coll : dependencies.values()) {
460                coll.close();
461            }
462            for (ServiceCollector<?, ?> coll : optDependencies.values()) {
463                coll.close();
464            }
465            dependencies.clear();
466            optDependencies.clear();
467        }
468    }
469
470    /**
471     * Checks if the resolver is open.
472     *
473     * @return true, if open
474     */
475    public boolean isOpen() {
476        return isOpen;
477    }
478
479    /**
480     * Checks if the resolver is in the resolved state.
481     *
482     * @return the result
483     */
484    public boolean isResolved() {
485        synchronized (this) {
486            return resolvedCount == dependencies.size();
487        }
488    }
489
490    /**
491     * Returns the service found for the mandatory dependency with the
492     * given name. This method should only be called when the resolver
493     * is in state resolved.
494     * 
495     * Note that due to the threaded nature of the environment, 
496     * this method can return {@code null}, even if the resolver
497     * was in state resolved before its invocation, because the service
498     * can go away between the check and the invocation of this method.
499     * 
500     * If you want to be sure that services haven't been unregistered
501     * concurrently, the check for resolved and the invocation of this
502     * method must be in a block synchronized on this resolver.
503     * 
504     * Consider using {@link ServiceResolver#with(String, Function)}
505     * or {@link #with(Class, Function)} as an alternative.
506     * 
507     * @param <T> the type of the service
508     * @param name the name of the dependency
509     * @param clazz the class of the service
510     * @return the service or {@code null} if called in unresolved
511     *     state and the dependency is not resolved
512     */
513    @SuppressWarnings("unchecked")
514    public <T> T get(String name, Class<T> clazz) {
515        return Optional.ofNullable(dependencies.get(name))
516            .map(coll -> ((ServiceCollector<?, T>) coll).service().get())
517            .orElse(null);
518    }
519
520    /**
521     * Returns the service found for the mandatory dependency
522     * using the name of the class as name of the dependency. 
523     *
524     * @param <T> the type of the service
525     * @param clazz the class of the service
526     * @return the service or {@code null} if called in unresolved
527     *     state and the dependency is not resolved
528     * @see #get(String, Class)
529     */
530    public <T> T get(Class<T> clazz) {
531        return get(clazz.getName(), clazz);
532    }
533
534    /**
535     * Returns the service found for the optional dependency 
536     * with the given name, if it exists. 
537     * 
538     * @param <T> the type of the service
539     * @param name the name of the dependency
540     * @param clazz the class of the service
541     * @return the result
542     */
543    @SuppressWarnings("unchecked")
544    public <T> Optional<T> optional(String name, Class<T> clazz) {
545        return Optional.ofNullable(optDependencies.get(name))
546            .map(coll -> ((ServiceCollector<?, T>) coll).service())
547            .orElse(Optional.empty());
548    }
549
550    /**
551     * Returns the service found for the optional dependency
552     * using the name of the class as name of the dependency,
553     * if it exists. 
554     *
555     * @param <T> the type of the service
556     * @param clazz the class of the service
557     * @return the result
558     */
559    public <T> Optional<T> optional(Class<T> clazz) {
560        return optional(clazz.getName(), clazz);
561    }
562
563    /**
564     * Convenience method to invoke the a function with the
565     * service registered as mandatory or optional dependency
566     * while holding a lock on the underlying service collector. 
567     *
568     * @param <T> the type of the service
569     * @param <R> the result type
570     * @param name the name of the dependency
571     * @param function the function to invoke with the service as argument
572     * @return the result or {@link Optional#empty()} of the service
573     * was not available.
574     */
575    @SuppressWarnings("unchecked")
576    public <T, R> Optional<R> with(String name,
577            Function<T, ? extends R> function) {
578        return Optional.ofNullable(
579            dependencies.getOrDefault(name, optDependencies.get(name)))
580            .flatMap(
581                coll -> ((ServiceCollector<?, T>) coll).withService(function));
582    }
583
584    /**
585     * Convenience method to invoke the a function with the
586     * service registered as mandatory or optional dependency
587     * while holding a lock on the underlying service collector. 
588     *
589     * @param <T> the type of the service
590     * @param <R> the result type
591     * @param clazz the class of the service
592     * @param function the function to invoke with the service as argument
593     * @return the result or {@link Optional#empty()} of the service
594     * was not available.
595     */
596    public <T, R> Optional<R> with(Class<T> clazz,
597            Function<T, ? extends R> function) {
598        return with(clazz.getName(), function);
599    }
600
601}