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 * Based on the ServiceTracker implementation from OSGi.
017 * 
018 * Copyright (c) OSGi Alliance (2000, 2014). All Rights Reserved.
019 * 
020 * Licensed under the Apache License, Version 2.0 (the "License").
021 */
022
023package de.mnl.osgi.coreutils;
024
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.HashSet;
029import java.util.Iterator;
030import java.util.List;
031import java.util.NoSuchElementException;
032import java.util.Optional;
033import java.util.Set;
034import java.util.SortedMap;
035import java.util.TreeMap;
036import java.util.function.BiConsumer;
037import java.util.function.BiFunction;
038import java.util.function.Function;
039import org.osgi.framework.AllServiceListener;
040import org.osgi.framework.BundleContext;
041import org.osgi.framework.Constants;
042import org.osgi.framework.Filter;
043import org.osgi.framework.InvalidSyntaxException;
044import org.osgi.framework.ServiceEvent;
045import org.osgi.framework.ServiceListener;
046import org.osgi.framework.ServiceReference;
047
048/**
049 * Maintains a collection of services matching some criteria.
050 * <P>
051 * An instance can be created with different criteria for matching
052 * services of a given type. While the instance is open, it 
053 * maintains a collection of all matching services provided
054 * by the framework. Changes of the collection are reported using
055 * the registered handlers.
056 * <P>
057 * Because OSGi is a threaded environment, the registered services 
058 * can vary any time. Results from queries on the collection are 
059 * therefore only reliable while synchronizing on the 
060 * {@code ServiceCollector} instance. The synchronization puts 
061 * other threads that attempt to change the collection on hold. 
062 * Note that this implies a certain risk of creating deadlocks.
063 * Also note that this does not prevent modifications by the
064 * holder of the lock.
065 * <P>
066 * Callbacks are always synchronized on this collector, else
067 * the state of the collector during the execution of the
068 * callback might no longer be the state that triggered
069 * the callback.
070 * <P>
071 * Instead of using the services from the framework directly,
072 * they may optionally be adapted to another type by setting a
073 * service getter function. The invocation of this function is 
074 * also synchronized on this collector.
075 *
076 * @param <S> the type of the service
077 * @param <T> the type of service instances returned by queries, 
078 *      usually the same type as the type of the service
079 *      (see {@link #setServiceGetter(BiFunction)})
080 */
081@SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.GodClass",
082    "PMD.TooManyFields", "PMD.DataflowAnomalyAnalysis", "PMD.TooManyMethods" })
083public class ServiceCollector<S, T> implements AutoCloseable {
084    /**
085     * The Bundle Context used by this {@code ServiceCollector}.
086     */
087    private final BundleContext context;
088    /**
089     * The Filter used by this {@code ServiceCollector} which 
090     * specifies the search criteria for the services to collect.
091     */
092    private final Filter filter;
093    /**
094     * Filter string for use when adding the ServiceListener. If this field is
095     * set, then certain optimizations can be taken since we don't have a user
096     * supplied filter.
097     */
098    private final String listenerFilter;
099    /**
100     * The registered listener.
101     */
102    private ServiceListener listener;
103    /**
104     * Class name to be collected. If this field is set, then we are 
105     * collecting by class name.
106     */
107    private final String collectClass;
108    /**
109     * Reference to be collected. If this field is set, then we are 
110     * collecting a single ServiceReference.
111     */
112    private final ServiceReference<S> collectReference;
113    /**
114     * Initial service references, processed in open.
115     */
116    private final Set<ServiceReference<S>> initialReferences = new HashSet<>();
117    /**
118     * Collected services: {@code ServiceReference} -> customized Object and
119     * {@code ServiceListener} object
120     */
121    private final SortedMap<ServiceReference<S>, T> collected
122        = new TreeMap<>(Collections.reverseOrder());
123    /**
124     * Can be used for waiting on.
125     */
126    @SuppressWarnings("PMD.AvoidUsingVolatile")
127    private volatile int[] modificationCount = { -1 };
128    // The callbacks.
129    private BiConsumer<ServiceReference<S>, T> onBound;
130    private BiConsumer<ServiceReference<S>, T> onAdded;
131    private BiConsumer<ServiceReference<S>, T> onRemoving;
132    private BiConsumer<ServiceReference<S>, T> onUnbinding;
133    private BiConsumer<ServiceReference<S>, T> onModified;
134    private BiFunction<BundleContext, ServiceReference<S>, T> svcGetter;
135    // Speed up getService.
136    @SuppressWarnings("PMD.AvoidUsingVolatile")
137    private volatile T cachedService;
138
139    /**
140     * Instantiates a new {@code ServiceCollector} that collects services
141     * of the specified class.
142     *
143     * @param context the bundle context used to interact with the framework
144     * @param clazz the clazz
145     */
146    public ServiceCollector(BundleContext context, Class<S> clazz) {
147        this(context, clazz.getName());
148    }
149
150    /**
151     * Instantiates a new {@code ServiceCollector} that collects services
152     * matches by the specified filter.
153     *
154     * @param context the bundle context used to interact with the framework
155     * @param filter the filter
156     */
157    @SuppressWarnings("PMD.AvoidThrowingNullPointerException")
158    public ServiceCollector(BundleContext context, Filter filter) {
159        this.context = context;
160        this.filter = filter;
161        collectReference = null;
162        collectClass = null;
163        listenerFilter = filter.toString();
164        if (context == null || filter == null) {
165            /*
166             * we throw a NPE here to be consistent with the other constructors
167             */
168            throw new NullPointerException();
169        }
170    }
171
172    /**
173     * Instantiates a new {@code ServiceCollector} that collects services
174     * on the specified service reference.
175     *
176     * @param context the bundle context used to interact with the framework
177     * @param reference the reference
178     */
179    public ServiceCollector(BundleContext context,
180            ServiceReference<S> reference) {
181        this.context = context;
182        this.collectReference = reference;
183        collectClass = null;
184        listenerFilter = "(" + Constants.SERVICE_ID + "="
185            + reference.getProperty(Constants.SERVICE_ID).toString() + ")";
186        try {
187            filter = context.createFilter(listenerFilter);
188        } catch (InvalidSyntaxException e) {
189            /*
190             * we could only get this exception if the ServiceReference was
191             * invalid
192             */
193            IllegalArgumentException iae = new IllegalArgumentException(
194                "unexpected InvalidSyntaxException: " + e.getMessage());
195            iae.initCause(e);
196            throw iae;
197        }
198    }
199
200    /**
201     * Instantiates a new {@code ServiceCollector} that collects services
202     * on the specified class name.
203     *
204     * @param context the bundle context used to interact with the framework
205     * @param className the class name
206     */
207    public ServiceCollector(BundleContext context, String className) {
208        this.context = context;
209        collectReference = null;
210        collectClass = className;
211        // we call clazz.toString to verify clazz is non-null!
212        listenerFilter
213            = "(" + Constants.OBJECTCLASS + "=" + className + ")";
214        try {
215            filter = context.createFilter(listenerFilter);
216        } catch (InvalidSyntaxException e) {
217            /*
218             * we could only get this exception if the clazz argument was
219             * malformed
220             */
221            IllegalArgumentException iae = new IllegalArgumentException(
222                "unexpected InvalidSyntaxException: " + e.getMessage());
223            iae.initCause(e);
224            throw iae;
225        }
226    }
227
228    /**
229     * Sets the service getter function. Instead of simply getting the
230     * service from the bundle using the given {@link ServiceReference}
231     * some additional processing may be performed such as adapting
232     * the service obtained to another interface.
233     * <P>
234     * A possible use case scenario for this feature consists of two 
235     * service types with different APIs but overlapping functionality.
236     * If the consumer needs only the common functionality, it
237     * may collect both service types and adapt the services obtained.
238     * <P>
239     * If the function returns {@code null}, no service is added
240     * to the collection. The function can thus also be used
241     * as a filter.
242     *
243     * @param serviceGetter the function
244     * @return the service collector
245     */
246    public ServiceCollector<S, T> setServiceGetter(
247            BiFunction<BundleContext, ServiceReference<S>, T> serviceGetter) {
248        this.svcGetter = serviceGetter;
249        return this;
250    }
251
252    /**
253     * Sets a function to be called when the first service 
254     * instance was added to the collection. The reference
255     * to the new service and the service instance are passed as 
256     * arguments.
257     * <P>
258     * The function is called before a callback set by 
259     * {@link #setOnAdded(BiConsumer)}.
260     *
261     * @param onBound the function to be called
262     * @return the {@code ServiceCollector}
263     */
264    public ServiceCollector<S, T> setOnBound(
265            BiConsumer<ServiceReference<S>, T> onBound) {
266        this.onBound = onBound;
267        return this;
268    }
269
270    /**
271     * Sets a function to be called when a new service instance
272     * was added to the collection. The reference to the new 
273     * service and the service instance are passed as arguments.
274     *
275     * @param onAdded the function to be called
276     * @return the {@code ServiceCollector}
277     */
278    public ServiceCollector<S, T> setOnAdded(
279            BiConsumer<ServiceReference<S>, T> onAdded) {
280        this.onAdded = onAdded;
281        return this;
282    }
283
284    /**
285     * Sets a function to be called before one of the collected service
286     * instances becomes unavailable. The reference to the service to 
287     * be removed and the service instance are passed as arguments.
288     *
289     * @param onRemoving the function to call
290     * @return the {@code ServiceCollector}
291     */
292    public ServiceCollector<S, T> setOnRemoving(
293            BiConsumer<ServiceReference<S>, T> onRemoving) {
294        this.onRemoving = onRemoving;
295        return this;
296    }
297
298    /**
299     * Sets a function to be called before the last of the collected 
300     * service instances becomes unavailable. The reference to 
301     * the service to be removed and the service instance are 
302     * passed as arguments.
303     * <P>
304     * The function is called after a callback set by 
305     * {@link #setOnRemoving(BiConsumer)}).
306     *
307     * @param onUnbinding the function to call
308     * @return the {@code ServiceCollector}
309     */
310    public ServiceCollector<S, T> setOnUnbinding(
311            BiConsumer<ServiceReference<S>, T> onUnbinding) {
312        this.onUnbinding = onUnbinding;
313        return this;
314    }
315
316    /**
317     * Sets a function to be called when the preferred service
318     * changes. This may either be a change of the preferred service's
319     * properties (reported by the framework) or the replacement of
320     * the preferred service by another service. The service reference 
321     * to the modified service and the service are passed as arguments.
322     * <P>
323     * If the preferred service is replaced by another service, this
324     * function is called after the "onAdded" or "onRemoved" callback.
325     *
326     * @param onModified the function to call
327     * @return the {@code ServiceCollector}
328     */
329    public ServiceCollector<S, T> setOnModfied(
330            BiConsumer<ServiceReference<S>, T> onModified) {
331        this.onModified = onModified;
332        return this;
333    }
334
335    /**
336     * Returns the context passed to the constructor.
337     * 
338     * @return the context
339     */
340    public BundleContext getContext() {
341        return context;
342    }
343
344    private void modified() {
345        synchronized (modificationCount) {
346            modificationCount[0] = modificationCount[0] + 1;
347            cachedService = null;
348            modificationCount.notifyAll();
349        }
350    }
351
352    /**
353     * Starts collecting of service providers. Short for calling 
354     * {@code open(false)}.
355     *
356     * @throws IllegalStateException If the {@code BundleConetxt}
357     *     with which this {@code ServiceCollector} was created is 
358     *     no longer valid.
359     */
360    @SuppressWarnings("PMD.AvoidUncheckedExceptionsInSignatures")
361    public void open() throws IllegalStateException {
362        open(false);
363    }
364
365    /**
366     * Starts collecting services. Short for calling {@code open(false)}.
367     * 
368     * @param collectAllServices if <code>true</code>, then this 
369     *     {@code ServiceCollector} will collect all matching services 
370     *     regardless of class loader
371     *     accessibility. If <code>false</code>, then 
372     *     this {@code ServiceCollector} will only collect matching services 
373     *     which are class loader
374     *     accessible to the bundle whose <code>BundleContext</code> is 
375     *     used by this {@code ServiceCollector}.
376     * @throws IllegalStateException If the {@code BundleConetxt}
377     *     with which this {@code ServiceCollector} was created is no 
378     *     longer valid.
379     */
380    @SuppressWarnings({ "PMD.AvoidUncheckedExceptionsInSignatures",
381        "PMD.ConfusingTernary", "PMD.AvoidThrowingRawExceptionTypes" })
382    public void open(boolean collectAllServices) throws IllegalStateException {
383        synchronized (this) {
384            if (isOpen()) {
385                // Already open, don't treat as error.
386                return;
387            }
388            modificationCount[0] = 0;
389            try {
390                registerListener(collectAllServices);
391                if (collectClass != null) {
392                    initialReferences.addAll(getInitialReferences(
393                        collectAllServices, collectClass, null));
394                } else if (collectReference != null) {
395                    if (collectReference.getBundle() != null) {
396                        initialReferences.add(collectReference);
397                    }
398                } else { /* user supplied filter */
399                    initialReferences.addAll(getInitialReferences(
400                        collectAllServices, null, listenerFilter));
401                }
402                processInitial();
403            } catch (InvalidSyntaxException e) {
404                throw new RuntimeException(
405                    "unexpected InvalidSyntaxException: " + e.getMessage(),
406                    e);
407            }
408        }
409    }
410
411    private void registerListener(boolean collectAllServices)
412            throws InvalidSyntaxException {
413        if (collectAllServices) {
414            listener = new AllServiceListener() {
415                @Override
416                public void serviceChanged(ServiceEvent event) {
417                    ServiceCollector.this.serviceChanged(event);
418                }
419            };
420        } else {
421            listener = new ServiceListener() {
422
423                @Override
424                public void serviceChanged(ServiceEvent event) {
425                    ServiceCollector.this.serviceChanged(event);
426                }
427
428            };
429        }
430        context.addServiceListener(listener, listenerFilter);
431    }
432
433    /**
434     * Returns the list of initial {@code ServiceReference}s that will be
435     * collected by this {@code ServiceCollector}.
436     * 
437     * @param collectAllServices If {@code true}, use
438     *        {@code getAllServiceReferences}.
439     * @param className The class name with which the service was registered, or
440     *        {@code null} for all services.
441     * @param filterString The filter criteria or {@code null} for all services.
442     * @return The list of initial {@code ServiceReference}s.
443     * @throws InvalidSyntaxException If the specified filterString has an
444     *         invalid syntax.
445     */
446    private List<ServiceReference<S>> getInitialReferences(
447            boolean collectAllServices, String className, String filterString)
448            throws InvalidSyntaxException {
449        @SuppressWarnings("unchecked")
450        ServiceReference<S>[] result
451            = (ServiceReference<S>[]) (collectAllServices
452                ? context.getAllServiceReferences(className, filterString)
453                : context.getServiceReferences(className, filterString));
454        if (result == null) {
455            return Collections.emptyList();
456        }
457        return Arrays.asList(result);
458    }
459
460    private void processInitial() {
461        while (true) {
462            // Process one by one so that we do not keep holding the lock.
463            synchronized (this) {
464                if (!isOpen() || initialReferences.isEmpty()) {
465                    return; /* we are done */
466                }
467                // Get one...
468                Iterator<ServiceReference<S>> iter
469                    = initialReferences.iterator();
470                ServiceReference<S> reference = iter.next();
471                iter.remove();
472                // Process (as if it had been registered).
473                addToCollected(reference);
474            }
475        }
476    }
477
478    private void serviceChanged(ServiceEvent event) {
479        /*
480         * Check if we had a delayed call (which could happen when we
481         * close).
482         */
483        if (!isOpen()) {
484            return;
485        }
486        @SuppressWarnings("unchecked")
487        final ServiceReference<S> reference
488            = (ServiceReference<S>) event.getServiceReference();
489
490        switch (event.getType()) {
491        case ServiceEvent.REGISTERED:
492            synchronized (this) {
493                initialReferences.remove(reference);
494                addToCollected(reference);
495            }
496            break;
497        case ServiceEvent.MODIFIED:
498            synchronized (this) {
499                T service = collected.get(reference);
500                if (service == null) {
501                    // Probably still in initialReferences, ignore.
502                    return;
503                }
504                modified();
505                if (onModified != null
506                    && reference.equals(collected.firstKey())) {
507                    onModified.accept(reference, service);
508                }
509            }
510            break;
511        case ServiceEvent.MODIFIED_ENDMATCH:
512        case ServiceEvent.UNREGISTERING:
513            synchronized (this) {
514                // May occur while processing the initial set of references.
515                initialReferences.remove(reference);
516                removeFromCollected(reference);
517            }
518            break;
519        default:
520            break;
521        }
522    }
523
524    /**
525     * Add the given reference to the collected services.
526     * Must be called from synchronized block.
527     * 
528     * @param reference reference to be collected.
529     */
530    private void addToCollected(final ServiceReference<S> reference) {
531        if (!isOpen()) {
532            // We have been closed.
533            return;
534        }
535        if (collected.get(reference) != null) {
536            /* if we are already collecting this reference */
537            return; /* skip this reference */
538        }
539
540        @SuppressWarnings("unchecked")
541        T service = (svcGetter == null)
542            ? (T) context.getService(reference)
543            : svcGetter.apply(context, reference);
544        if (service == null) {
545            // Has either vanished in the meantime (should not happen
546            // when processing a REGISTERED event, but may happen when
547            // processing a reference from initialReferences) or was
548            // filtered by the service getter.
549            return;
550        }
551        boolean wasEmpty = collected.isEmpty();
552        collected.put(reference, service);
553        modified();
554        if (wasEmpty) {
555            Optional.ofNullable(onBound)
556                .ifPresent(cb -> cb.accept(reference, service));
557        }
558        Optional.ofNullable(onAdded)
559            .ifPresent(cb -> cb.accept(reference, service));
560        // If added is first, first has changed
561        if (onModified != null && collected.size() > 1
562            && collected.firstKey().equals(reference)) {
563            onModified.accept(reference, service);
564        }
565    }
566
567    /**
568     * Removes a service reference from the collection.
569     * Must be called from synchronized block.
570     *
571     * @param reference the reference
572     */
573    @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
574    private void removeFromCollected(ServiceReference<S> reference) {
575        synchronized (this) {
576            // Only proceed if collected
577            T service = collected.get(reference);
578            if (service == null) {
579                return;
580            }
581            Optional.ofNullable(onRemoving)
582                .ifPresent(cb -> cb.accept(reference, service));
583            if (collected.size() == 1) {
584                Optional.ofNullable(onUnbinding)
585                    .ifPresent(cb -> cb.accept(reference, service));
586            }
587            context.ungetService(reference);
588            boolean firstChanges = reference.equals(collected.firstKey());
589            collected.remove(reference);
590            if (isOpen()) {
591                modified();
592            }
593            // Has first changed?
594            if (onModified != null && firstChanges && !collected.isEmpty()) {
595                onModified.accept(reference, service);
596            }
597        }
598
599    }
600
601    /**
602     * Stops collecting services.
603     */
604    public void close() {
605        synchronized (this) {
606            context.removeServiceListener(listener);
607            synchronized (modificationCount) {
608                modificationCount[0] = -1;
609                modificationCount.notifyAll();
610            }
611        }
612        while (!collected.isEmpty()) {
613            synchronized (this) {
614                removeFromCollected(collected.lastKey());
615            }
616        }
617        cachedService = null;
618    }
619
620    /**
621     * Wait for at least one service to be collected by this
622     * {@code ServiceCollector}. This method will also return when this
623     * {@code ServiceCollector} is closed from another thread.
624     * <p>
625     * It is strongly recommended that {@code waitForService} is not used 
626     * during the calling of the {@code BundleActivator} methods.
627     * {@code BundleActivator} methods are expected to complete in a short
628     * period of time.
629     * 
630     * @param timeout The time interval in milliseconds to wait. If zero, the
631     *     method will wait indefinitely.
632     * @return Returns the result of {@link #service()}.
633     * @throws InterruptedException If another thread has interrupted the
634     *     current thread.
635     * @throws IllegalArgumentException If the value of timeout is negative.
636     */
637    public Optional<T> waitForService(long timeout)
638            throws InterruptedException {
639        if (timeout < 0) {
640            throw new IllegalArgumentException("timeout value is negative");
641        }
642
643        T service = service().orElse(null);
644        if (service != null) {
645            return Optional.of(service);
646        }
647
648        @SuppressWarnings("PMD.UselessParentheses")
649        final long endTime
650            = (timeout == 0) ? 0 : (System.currentTimeMillis() + timeout);
651        while (true) {
652            synchronized (modificationCount) {
653                if (modificationCount() < 0) {
654                    return Optional.empty();
655                }
656                modificationCount.wait(timeout);
657            }
658            Optional<T> found = service();
659            if (found.isPresent()) {
660                return found;
661            }
662            if (endTime > 0) { // if we have a timeout
663                timeout = endTime - System.currentTimeMillis();
664                if (timeout <= 0) { // that has expired
665                    return Optional.empty();
666                }
667            }
668        }
669    }
670
671    /**
672     * Return the current set of {@code ServiceReference}s for all services 
673     * collected by this {@code ServiceCollector}.
674     * 
675     * @return the set.
676     */
677    public List<ServiceReference<S>> serviceReferences() {
678        synchronized (this) {
679            return Collections
680                .unmodifiableList(new ArrayList<>(collected.keySet()));
681        }
682    }
683
684    /**
685     * Returns a {@code ServiceReference} for one of the services collected
686     * by this {@code ServiceCollector}.
687     * <p>
688     * If multiple services have been collected, the service with the highest
689     * ranking (as specified in its {@code service.ranking} property) is
690     * returned. If there is a tie in ranking, the service with the lowest
691     * service id (as specified in its {@code service.id} property); that is,
692     * the service that was registered first is returned. This is the same
693     * algorithm used by {@code BundleContext.getServiceReference}.
694     * 
695     * @return an optional {@code ServiceReference}
696     */
697    public Optional<ServiceReference<S>> serviceReference() {
698        try {
699            synchronized (this) {
700                return Optional.of(collected.firstKey());
701            }
702        } catch (NoSuchElementException e) {
703            return Optional.empty();
704        }
705    }
706
707    /**
708     * Returns the service object for the specified {@code ServiceReference} 
709     * if the specified referenced service has been collected
710     * by this {@code ServiceCollector}.
711     * 
712     * @param reference the reference to the desired service.
713     * @return an optional service object
714     */
715    public Optional<T> service(ServiceReference<S> reference) {
716        synchronized (this) {
717            return Optional.ofNullable(collected.get(reference));
718        }
719    }
720
721    /**
722     * Returns a service object for one of the services collected by this
723     * {@code ServiceCollector}. This is effectively the same as 
724     * {@code serviceReference().flatMap(ref -> service(ref))}.
725     * <P>
726     * The result value is cached, so refrain from implementing another
727     * cache in the invoking code.
728     * 
729     * @return an optional service object
730     */
731    public Optional<T> service() {
732        final T cached = cachedService;
733        if (cached != null) {
734            return Optional.of(cached);
735        }
736        synchronized (this) {
737            Iterator<T> iter = collected.values().iterator();
738            if (iter.hasNext()) {
739                cachedService = iter.next();
740                return Optional.of(cachedService);
741            }
742        }
743        return Optional.empty();
744    }
745
746    /**
747     * Convenience method to invoke the a function with the
748     * result from {@link #service()} (if it is available)
749     * while holding a lock on this service collector. 
750     *
751     * @param <R> the result type
752     * @param function the function to invoke with the service as argument
753     * @return the result or {@link Optional#empty()} of the service
754     * was not available.
755     */
756    public <R> Optional<R> withService(Function<T, ? extends R> function) {
757        synchronized (this) {
758            return service().map(function);
759        }
760    }
761
762    /**
763     * Return the list of service objects for all services collected by this
764     * {@code ServiceCollector}. This is effectively a mapping of
765     * {@link #serviceReferences()} to services.
766     * 
767     * @return a list of service objects which may be empty
768     */
769    public List<T> services() {
770        synchronized (this) {
771            return Collections
772                .unmodifiableList(new ArrayList<>(collected.values()));
773        }
774    }
775
776    /**
777     * Return the number of services collected by this
778     * {@code ServiceCollector}. This value may not be correct during the
779     * execution of handlers.
780     * 
781     * @return The number of services collected
782     */
783    public int size() {
784        synchronized (this) {
785            return collected.size();
786        }
787    }
788
789    /**
790     * Returns the modification count for this {@code ServiceCollector}.
791     * 
792     * The modification count is initialized to 0 when this 
793     * {@code ServiceCollector} is opened. Every time a service is added, 
794     * modified or removed from this {@code ServiceCollector}, 
795     * the modification count is incremented.
796     * <p>
797     * The modification count can be used to determine if this
798     * {@code ServiceCollector} has added, modified or removed a service by
799     * comparing a modification count value previously collected with the 
800     * current modification count value. If the value has not changed, 
801     * then no service has been added, modified or removed from this 
802     * {@code ServiceCollector} since the previous modification count 
803     * was collected.
804     * 
805     * @return The modification count for this {@code ServiceCollector} or
806     *      -1 if this {@code ServiceCollector} is not open.
807     */
808    public int modificationCount() {
809        return modificationCount[0];
810    }
811
812    /**
813     * Checks if this {@code ServiceCollector} is open.
814     *
815     * @return true, if is open
816     */
817    public boolean isOpen() {
818        return modificationCount[0] >= 0;
819    }
820
821    /**
822     * Return a {@code SortedMap} of the {@code ServiceReference}s and service
823     * objects for all services collected by this {@code ServiceCollector}.
824     * The map is sorted in reverse natural order of {@code ServiceReference}.
825     * That is, the first entry is the service with the highest ranking and the
826     * lowest service id.
827     * 
828     * @return A {@code SortedMap} with the {@code ServiceReference}s and
829     *         service objects for all services collected by this
830     *         {@code ServiceCollector}. If no services have been collected,
831     *         then the returned map is empty.
832     */
833    public SortedMap<ServiceReference<S>, T> collected() {
834        synchronized (this) {
835            if (collected.isEmpty()) {
836                return new TreeMap<>(Collections.reverseOrder());
837            }
838            return Collections.unmodifiableSortedMap(new TreeMap<>(collected));
839        }
840    }
841
842    /**
843     * Return if this {@code ServiceCollector} is empty.
844     * 
845     * @return {@code true} if this {@code ServiceCollector} 
846     *     has not collected any services.
847     */
848    public boolean isEmpty() {
849        synchronized (this) {
850            return collected.isEmpty();
851        }
852    }
853
854    /**
855     * Remove a service from this {@code ServiceCollector}.
856     * 
857     * The specified service will be removed from this {@code ServiceCollector}.
858     * 
859     * @param reference The reference to the service to be removed.
860     */
861    public void remove(ServiceReference<S> reference) {
862        synchronized (this) {
863            removeFromCollected(reference);
864        }
865    }
866
867    /*
868     * (non-Javadoc)
869     * 
870     * @see java.lang.Object#toString()
871     */
872    @Override
873    public String toString() {
874        return "ServiceCollector [filter=" + filter + ", bundle="
875            + context.getBundle() + "]";
876    }
877
878}