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}