001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2016-2018 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.core;
020
021import java.lang.ref.ReferenceQueue;
022import java.lang.ref.WeakReference;
023import java.time.Duration;
024import java.time.Instant;
025import java.util.Collections;
026import java.util.Comparator;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.Iterator;
030import java.util.Map;
031import java.util.PriorityQueue;
032import java.util.Queue;
033import java.util.Set;
034import java.util.WeakHashMap;
035import java.util.concurrent.ExecutorService;
036import java.util.concurrent.Executors;
037import java.util.concurrent.ThreadFactory;
038import java.util.concurrent.atomic.AtomicLong;
039import java.util.logging.Level;
040import org.jgrapes.core.annotation.ComponentManager;
041import org.jgrapes.core.events.Start;
042import org.jgrapes.core.events.Started;
043import org.jgrapes.core.internal.ComponentVertex;
044import org.jgrapes.core.internal.CoreUtils;
045import org.jgrapes.core.internal.GeneratorRegistry;
046
047/**
048 * This class provides some utility functions.
049 */
050@SuppressWarnings({ "PMD.TooManyMethods", "PMD.ClassNamingConventions",
051    "PMD.ExcessivePublicCount", "PMD.ExcessiveClassLength",
052    "PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal",
053    "PMD.CouplingBetweenObjects" })
054public class Components {
055
056    private static ExecutorService defaultExecutorService
057        = useVirtualThreads() ? Executors.newVirtualThreadPerTaskExecutor()
058            : Executors.newCachedThreadPool(
059                new ThreadFactory() {
060                    @SuppressWarnings({ "PMD.CommentRequired",
061                        "PMD.MissingOverride" })
062                    public Thread newThread(Runnable runnable) {
063                        Thread thread
064                            = Executors.defaultThreadFactory()
065                                .newThread(runnable);
066                        thread.setDaemon(true);
067                        return thread;
068                    }
069                });
070
071    private static ExecutorService timerExecutorService
072        = defaultExecutorService;
073
074    /**
075     * JGrapes uses virtual thread by default. However, as of
076     * 2024, some debuggers still have problems with virtual
077     * threads. Therefore it is possible to switch back to
078     * platform threads by starting the JVM with property
079     * `-Djgrapes.useVirtualThreads=false`.
080     *
081     * @return true, if successful
082     */
083    public static boolean useVirtualThreads() {
084        return Boolean.parseBoolean(
085            System.getProperty("jgrapes.useVirtualThreads", "true"));
086    }
087
088    private Components() {
089    }
090
091    /**
092     * Return the default executor service for the framework.
093     * 
094     * @return the defaultExecutorService
095     */
096    public static ExecutorService defaultExecutorService() {
097        return defaultExecutorService;
098    }
099
100    /**
101     * Set the default executor service for the framework. The default 
102     * value is a cached thread pool (see @link 
103     * {@link Executors#newCachedThreadPool()}) with daemon threads.
104     * 
105     * @param defaultExecutorService the executor service to set
106     */
107    @SuppressWarnings("PMD.CompareObjectsWithEquals")
108    public static void setDefaultExecutorService(
109            ExecutorService defaultExecutorService) {
110        // If the timer executor service is set to the default
111        // executor service, adjust it to the new value as well.
112        if (timerExecutorService == Components.defaultExecutorService) {
113            timerExecutorService = defaultExecutorService;
114        }
115        Components.defaultExecutorService = defaultExecutorService;
116    }
117
118    /**
119     * Returns a component's manager. For a component that inherits
120     * from {@link org.jgrapes.core.Component} this method simply returns
121     * the component as it is its own manager.
122     * 
123     * For components that implement {@link ComponentType} but don't inherit from 
124     * {@link org.jgrapes.core.Component} the method returns the value of 
125     * the attribute annotated as manager slot. If this attribute is still
126     * empty, this method makes the component the root
127     * of a new tree and returns its manager.
128     * 
129     * @param component the component
130     * @return the component (with its manager attribute set)
131     */
132    public static Manager manager(ComponentType component) {
133        return ComponentVertex.componentVertex(component, null);
134    }
135
136    /**
137     * Returns a component's manager like {@link #manager(ComponentType)}.
138     * If the manager slot attribute is empty, the component is initialized
139     * with its component channel set to the given parameter. Invoking
140     * this method overrides any channel set in the
141     * {@link ComponentManager} annotation.
142     * 
143     * This method is usually invoked by the constructor of a class
144     * that implements {@link ComponentType}.
145     * 
146     * @param component the component
147     * @param componentChannel the channel that the component's 
148     * handlers listen on by default and that 
149     * {@link Manager#fire(Event, Channel...)} sends the event to 
150     * @return the component (with its manager attribute set)
151     * @see Component#Component(Channel)
152     */
153    public static Manager manager(
154            ComponentType component, Channel componentChannel) {
155        return ComponentVertex.componentVertex(component, componentChannel);
156    }
157
158    /**
159     * Fires a {@link Start} event with an associated
160     * {@link Started} completion event on the broadcast channel
161     * of the given application and wait for the completion of the
162     * <code>Start</code> event.
163     * 
164     * @param application the application to start
165     * @throws InterruptedException if the execution was interrupted
166     */
167    public static void start(ComponentType application)
168            throws InterruptedException {
169        manager(application).fire(new Start(), Channel.BROADCAST).get();
170    }
171
172    /**
173     * Wait until all generators and event queues are exhausted. When this
174     * stage is reached, nothing can happen anymore unless a new event is
175     * sent from an external thread.
176     * 
177     * @throws InterruptedException if the current thread was interrupted
178     * while waiting
179     */
180    public static void awaitExhaustion() throws InterruptedException {
181        GeneratorRegistry.instance().awaitExhaustion();
182    }
183
184    /**
185     * Wait until all generators and event queues are exhausted or
186     * the maximum wait time has expired.
187     * 
188     * @param timeout the wait time in milliseconds
189     * @return {@code true} if exhaustion state was reached
190     * @throws InterruptedException if the execution was interrupted 
191     * @see #awaitExhaustion()
192     */
193    public static boolean awaitExhaustion(long timeout)
194            throws InterruptedException {
195        return GeneratorRegistry.instance().awaitExhaustion(timeout);
196    }
197
198    /**
199     * Utility method that checks if an assertion error has occurred
200     * while executing handlers. If so, the error is thrown and
201     * the assertion error store is reset.
202     * <P>
203     * This method is intended for junit tests. It enables easy propagation
204     * of assertion failures to the main thread.
205     * 
206     * @throws AssertionError if an assertion error occurred while
207     * executing the application
208     */
209    public static void checkAssertions() {
210        CoreUtils.checkAssertions();
211    }
212
213    /**
214     * Returns the full name of the object's class together with an id (see 
215     * {@link #objectId(Object)}). The result can be used as a unique
216     * human readable identifier for arbitrary objects.
217     * 
218     * @param object
219     *            the object
220     * @return the object's name
221     */
222    public static String fullObjectName(Object object) {
223        if (object == null) {
224            return "<null>";
225        }
226        StringBuilder builder = new StringBuilder();
227        builder.append(object.getClass().getName())
228            .append('#')
229            .append(objectId(object));
230        return builder.toString();
231    }
232
233    /**
234     * Returns the simple name of the object's class together with an id 
235     * (see {@link #objectId(Object)}). Can be used to create a human
236     * readable, though not necessarily unique, label for an object.
237     * 
238     * @param object
239     *            the object
240     * @return the object's name
241     */
242    public static String simpleObjectName(Object object) {
243        if (object == null) {
244            return "<null>";
245        }
246        StringBuilder builder = new StringBuilder();
247        builder.append(simpleClassName(object.getClass()))
248            .append('#')
249            .append(objectId(object));
250        return builder.toString();
251    }
252
253    /**
254     * Returns the name of the object's class together with an id (see 
255     * {@link #objectId(Object)}). May be used to implement {@code toString()}
256     * with identifiable objects. If the log level is "finer", the full
257     * class name will be used for the returned value, else the simple name.
258     * 
259     * @param object
260     *            the object
261     * @return the object's name
262     */
263    public static String objectName(Object object) {
264        if (object == null) {
265            return "<null>";
266        }
267        StringBuilder builder = new StringBuilder();
268        builder.append(className(object.getClass()))
269            .append('#')
270            .append(objectId(object));
271        return builder.toString();
272    }
273
274    private static Map<Object, String> objectIds // NOPMD
275        = new WeakHashMap<>();
276    private static Map<Class<?>, AtomicLong> idCounters // NOPMD
277        = new WeakHashMap<>();
278
279    private static String getId(Class<?> scope, Object object) {
280        if (object == null) {
281            return "?";
282        }
283        synchronized (objectIds) {
284            return objectIds.computeIfAbsent(object,
285                key -> Long.toString(idCounters
286                    .computeIfAbsent(scope, newKey -> new AtomicLong())
287                    .incrementAndGet()));
288
289        }
290    }
291
292    /**
293     * Returns the full name or simple name of the class depending
294     * on the log level.
295     * 
296     * @param clazz the class
297     * @return the name
298     */
299    public static String className(Class<?> clazz) {
300        if (CoreUtils.classNames.isLoggable(Level.FINER)) {
301            return clazz.getName();
302        } else {
303            return simpleClassName(clazz);
304        }
305    }
306
307    /**
308     * Returns the simple name of a class. Contrary to 
309     * {@link Class#getSimpleName()}, this method returns
310     * the last segement of the full name for anonymous
311     * classes (instead of an empty string).
312     * 
313     * @param clazz the class
314     * @return the name
315     */
316    public static String simpleClassName(Class<?> clazz) {
317        if (!clazz.isAnonymousClass()) {
318            return clazz.getSimpleName();
319        }
320        // Simple name of anonymous class is empty
321        String name = clazz.getName();
322        int lastDot = name.lastIndexOf('.');
323        if (lastDot <= 0) {
324            return name;
325        }
326        return name.substring(lastDot + 1);
327    }
328
329    /**
330     * Returns an id of the object that is unique within a specific scope. Ids
331     * are generated and looked up in the scope of the object's class unless the
332     * class implements {@link IdInfoProvider}.
333     * 
334     * @param object
335     *            the object
336     * @return the object's name
337     */
338    public static String objectId(Object object) {
339        if (object == null) {
340            return "?";
341        }
342        if (object instanceof IdInfoProvider) {
343            return getId(((IdInfoProvider) object).idScope(),
344                ((IdInfoProvider) object).idObject());
345        } else {
346            return getId(object.getClass(), object);
347        }
348    }
349
350    /**
351     * Implemented by classes that want a special class (scope) to be used
352     * for looking up their id or want to map to another object for getting the
353     * id (see {@link Components#objectId(Object)}).
354     */
355    public interface IdInfoProvider {
356
357        /**
358         * Returns the scope.
359         * 
360         * @return the scope
361         */
362        Class<?> idScope();
363
364        /**
365         * Returns the object to be used for generating the id.
366         * 
367         * @return the object (defaults to {@code this})
368         */
369        default Object idObject() {
370            return this;
371        }
372    }
373
374    /**
375     * Instances are added to the scheduler in order to be invoked
376     * at a given time.
377     */
378    @FunctionalInterface
379    public interface TimeoutHandler {
380
381        /**
382         * Invoked when the timeout occurs.
383         * 
384         * @param timer the timer that has timed out and needs handling
385         */
386        void timeout(Timer timer);
387    }
388
389    /**
390     * Represents a timer as created by 
391     * {@link Components#schedule(TimeoutHandler, Instant)}.
392     */
393    public static class Timer {
394        private final Scheduler scheduler;
395        private final TimeoutHandler timeoutHandler;
396        private Instant scheduledFor;
397
398        private Timer(Scheduler scheduler,
399                TimeoutHandler timeoutHandler, Instant scheduledFor) {
400            this.scheduler = scheduler;
401            this.timeoutHandler = timeoutHandler;
402            this.scheduledFor = scheduledFor;
403        }
404
405        /**
406         * Reschedules the timer for the given instant.
407         * 
408         * @param scheduledFor the instant
409         */
410        public void reschedule(Instant scheduledFor) {
411            scheduler.reschedule(this, scheduledFor);
412        }
413
414        /**
415         * Reschedules the timer for the given duration after now.
416         * 
417         * @param scheduledFor the timeout
418         */
419        public void reschedule(Duration scheduledFor) {
420            reschedule(Instant.now().plus(scheduledFor));
421        }
422
423        /**
424         * Returns the timeout handler of this timer.
425         * 
426         * @return the handler
427         */
428        public TimeoutHandler timeoutHandler() {
429            return timeoutHandler;
430        }
431
432        /**
433         * Returns the instant that this handler is scheduled for.
434         * 
435         * @return the instant or `null` if the timer has been cancelled.
436         */
437        public Instant scheduledFor() {
438            return scheduledFor;
439        }
440
441        /**
442         * Cancels this timer.
443         */
444        public void cancel() {
445            scheduler.cancel(this);
446        }
447    }
448
449    /**
450     * Returns the executor service used for executing timers.
451     * 
452     * @return the timer executor service
453     */
454    public static ExecutorService timerExecutorService() {
455        return timerExecutorService;
456    }
457
458    /**
459     * Sets the executor service used for executing timers.
460     * Defaults to the {@link #defaultExecutorService()}.
461     * 
462     * @param timerExecutorService the timerExecutorService to set
463     */
464    public static void setTimerExecutorService(
465            ExecutorService timerExecutorService) {
466        Components.timerExecutorService = timerExecutorService;
467    }
468
469    /**
470     * A general purpose scheduler.
471     */
472    private static class Scheduler extends Thread {
473
474        private final Queue<Timer> timers = new PriorityQueue<>(10,
475            Comparator.comparing(Timer::scheduledFor));
476
477        /**
478         * Instantiates a new scheduler.
479         */
480        @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
481        public Scheduler() {
482            (useVirtualThreads() ? ofVirtual() : ofPlatform())
483                .name("Components.Scheduler").start(this);
484        }
485
486        /**
487         * Schedule the handler and return the resulting timer.
488         *
489         * @param timeoutHandler the timeout handler
490         * @param scheduledFor the scheduled for
491         * @return the timer
492         */
493        public Timer schedule(
494                TimeoutHandler timeoutHandler, Instant scheduledFor) {
495            @SuppressWarnings("PMD.AccessorClassGeneration")
496            Timer timer = new Timer(this, timeoutHandler, scheduledFor);
497            synchronized (timers) {
498                timers.add(timer);
499                timers.notifyAll();
500            }
501            return timer;
502        }
503
504        private void reschedule(Timer timer, Instant scheduledFor) {
505            synchronized (timers) {
506                timers.remove(timer);
507                timer.scheduledFor = scheduledFor;
508                timers.add(timer);
509                timers.notifyAll();
510            }
511        }
512
513        private void cancel(Timer timer) {
514            synchronized (timers) {
515                if (timers.remove(timer)) {
516                    timers.notifyAll();
517                }
518                timer.scheduledFor = null;
519            }
520        }
521
522        @Override
523        public void run() {
524            while (true) {
525                while (true) {
526                    @SuppressWarnings("PMD.AvoidFinalLocalVariable")
527                    final Timer first;
528                    synchronized (timers) {
529                        first = timers.peek();
530                        if (first == null
531                            || first.scheduledFor().isAfter(Instant.now())) {
532                            break;
533                        }
534                        timers.poll();
535                    }
536                    timerExecutorService.submit(
537                        () -> first.timeoutHandler().timeout(first));
538                }
539                try {
540                    synchronized (timers) {
541                        if (timers.isEmpty()) {
542                            timers.wait();
543                        } else {
544                            timers
545                                .wait(Math.max(1,
546                                    Duration.between(Instant.now(),
547                                        timers.peek().scheduledFor())
548                                        .toMillis()));
549                        }
550                    }
551                } catch (Exception e) { // NOPMD
552                    // Keep running.
553                }
554            }
555        }
556    }
557
558    @SuppressWarnings("PMD.FieldDeclarationsShouldBeAtStartOfClass")
559    private static Scheduler scheduler = new Scheduler();
560
561    /**
562     * Schedules the given timeout handler for the given instance. 
563     * 
564     * @param timeoutHandler the handler
565     * @param scheduledFor the instance in time
566     * @return the timer
567     */
568    public static Timer schedule(
569            TimeoutHandler timeoutHandler, Instant scheduledFor) {
570        return scheduler.schedule(timeoutHandler, scheduledFor);
571    }
572
573    /**
574     * Schedules the given timeout handler for the given 
575     * offset from now. 
576     * 
577     * @param timeoutHandler the handler
578     * @param scheduledFor the time to wait
579     * @return the timer
580     */
581    public static Timer schedule(
582            TimeoutHandler timeoutHandler, Duration scheduledFor) {
583        return scheduler.schedule(
584            timeoutHandler, Instant.now().plus(scheduledFor));
585    }
586
587    /**
588     * Puts the given key and value in the given {@link Map} and
589     * returns the map. Looks ugly when nested, but comes in handy 
590     * sometimes.
591     *
592     * @param <K> the key type
593     * @param <V> the value type
594     * @param map the map
595     * @param key the key
596     * @param value the value
597     * @return the map
598     */
599    public static <K, V> Map<K, V> put(Map<K, V> map, K key, V value) {
600        map.put(key, value);
601        return map;
602    }
603
604    /**
605     * Provisional replacement for method available in Java 9. 
606     * 
607     * @return an empty map
608     */
609    @Deprecated
610    public static <K, V> Map<K, V> mapOf() {
611        return new HashMap<>();
612    }
613
614    /**
615     * Provisional replacement for method available in Java 9. 
616     * 
617     * @return an immutable map filled with the given values
618     */
619    @Deprecated
620    @SuppressWarnings({ "PMD.ShortVariable", "PMD.AvoidDuplicateLiterals" })
621    public static <K, V> Map<K, V> mapOf(K k1, V v1) {
622        @SuppressWarnings("PMD.UseConcurrentHashMap")
623        Map<K, V> result = new HashMap<>();
624        result.put(k1, v1);
625        return Collections.unmodifiableMap(result);
626    }
627
628    /**
629     * Provisional replacement for method available in Java 9. 
630     * 
631     * @return an immutable map filled with the given values
632     */
633    @Deprecated
634    @SuppressWarnings("PMD.ShortVariable")
635    public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2) {
636        @SuppressWarnings("PMD.UseConcurrentHashMap")
637        Map<K, V> result = new HashMap<>();
638        result.put(k1, v1);
639        result.put(k2, v2);
640        return Collections.unmodifiableMap(result);
641    }
642
643    /**
644     * Provisional replacement for method available in Java 9. 
645     * 
646     * @return an immutable map filled with the given values
647     */
648    @Deprecated
649    @SuppressWarnings("PMD.ShortVariable")
650    public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2, K k3, V v3) {
651        @SuppressWarnings("PMD.UseConcurrentHashMap")
652        Map<K, V> result = new HashMap<>();
653        result.put(k1, v1);
654        result.put(k2, v2);
655        result.put(k3, v3);
656        return Collections.unmodifiableMap(result);
657    }
658
659    /**
660     * Provisional replacement for method available in Java 9. 
661     * 
662     * @return an immutable map filled with the given values
663     */
664    @Deprecated
665    @SuppressWarnings("PMD.ShortVariable")
666    public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2, K k3, V v3,
667            K k4, V v4) {
668        @SuppressWarnings("PMD.UseConcurrentHashMap")
669        Map<K, V> result = new HashMap<>();
670        result.put(k1, v1);
671        result.put(k2, v2);
672        result.put(k3, v3);
673        result.put(k4, v4);
674        return Collections.unmodifiableMap(result);
675    }
676
677    /**
678     * Provisional replacement for method available in Java 9. 
679     * 
680     * @return an immutable map filled with the given values
681     */
682    @Deprecated
683    @SuppressWarnings({ "PMD.ExcessiveParameterList", "PMD.ShortVariable",
684        "PMD.AvoidDuplicateLiterals" })
685    public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2, K k3, V v3,
686            K k4, V v4, K k5, V v5) {
687        @SuppressWarnings("PMD.UseConcurrentHashMap")
688        Map<K, V> result = new HashMap<>();
689        result.put(k1, v1);
690        result.put(k2, v2);
691        result.put(k3, v3);
692        result.put(k4, v4);
693        result.put(k5, v5);
694        return Collections.unmodifiableMap(result);
695    }
696
697    /**
698     * Provisional replacement for method available in Java 9. 
699     * 
700     * @return an immutable map filled with the given values
701     */
702    @Deprecated
703    @SuppressWarnings({ "PMD.ExcessiveParameterList", "PMD.ShortVariable" })
704    public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2, K k3, V v3,
705            K k4, V v4, K k5, V v5, K k6, V v6) {
706        @SuppressWarnings("PMD.UseConcurrentHashMap")
707        Map<K, V> result = new HashMap<>();
708        result.put(k1, v1);
709        result.put(k2, v2);
710        result.put(k3, v3);
711        result.put(k4, v4);
712        result.put(k5, v5);
713        result.put(k6, v6);
714        return Collections.unmodifiableMap(result);
715    }
716
717    /**
718     * Provisional replacement for method available in Java 9. 
719     * 
720     * @return an immutable map filled with the given values
721     */
722    @Deprecated
723    @SuppressWarnings({ "PMD.ExcessiveParameterList", "PMD.ShortVariable" })
724    public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2, K k3, V v3,
725            K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7) {
726        @SuppressWarnings("PMD.UseConcurrentHashMap")
727        Map<K, V> result = new HashMap<>();
728        result.put(k1, v1);
729        result.put(k2, v2);
730        result.put(k3, v3);
731        result.put(k4, v4);
732        result.put(k5, v5);
733        result.put(k6, v6);
734        result.put(k7, v7);
735        return Collections.unmodifiableMap(result);
736    }
737
738    /**
739     * Provisional replacement for method available in Java 9. 
740     * 
741     * @return an immutable map filled with the given values
742     */
743    @Deprecated
744    @SuppressWarnings({ "PMD.ExcessiveParameterList", "PMD.ShortVariable" })
745    public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2, K k3, V v3,
746            K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8) {
747        @SuppressWarnings("PMD.UseConcurrentHashMap")
748        Map<K, V> result = new HashMap<>();
749        result.put(k1, v1);
750        result.put(k2, v2);
751        result.put(k3, v3);
752        result.put(k4, v4);
753        result.put(k5, v5);
754        result.put(k6, v6);
755        result.put(k7, v7);
756        result.put(k8, v8);
757        return Collections.unmodifiableMap(result);
758    }
759
760    /**
761     * Provisional replacement for method available in Java 9. 
762     * 
763     * @return an immutable map filled with the given values
764     */
765    @Deprecated
766    @SuppressWarnings({ "PMD.ExcessiveParameterList", "PMD.ShortVariable" })
767    public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2, K k3, V v3,
768            K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8,
769            K k9, V v9) {
770        @SuppressWarnings("PMD.UseConcurrentHashMap")
771        Map<K, V> result = new HashMap<>();
772        result.put(k1, v1);
773        result.put(k2, v2);
774        result.put(k3, v3);
775        result.put(k4, v4);
776        result.put(k5, v5);
777        result.put(k6, v6);
778        result.put(k7, v7);
779        result.put(k8, v8);
780        result.put(k9, v9);
781        return Collections.unmodifiableMap(result);
782    }
783
784    /**
785     * Provisional replacement for method available in Java 9. 
786     * 
787     * @return an immutable map filled with the given values
788     */
789    @Deprecated
790    @SuppressWarnings({ "PMD.ExcessiveParameterList", "PMD.ShortVariable" })
791    public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2, K k3, V v3,
792            K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8,
793            K k9, V v9, K k10, V v10) {
794        @SuppressWarnings("PMD.UseConcurrentHashMap")
795        Map<K, V> result = new HashMap<>();
796        result.put(k1, v1);
797        result.put(k2, v2);
798        result.put(k3, v3);
799        result.put(k4, v4);
800        result.put(k5, v5);
801        result.put(k6, v6);
802        result.put(k7, v7);
803        result.put(k8, v8);
804        result.put(k9, v9);
805        result.put(k10, v10);
806        return Collections.unmodifiableMap(result);
807    }
808
809    /**
810     * An index of pooled items. Each key is associated with a set
811     * of values. Values can be added or retrieved from the set.
812     *
813     * @param <K> the key type
814     * @param <V> the value type
815     */
816    public static class PoolingIndex<K, V> {
817
818        @SuppressWarnings("PMD.UseConcurrentHashMap")
819        private final Map<K, Set<ValueReference>> backing = new HashMap<>();
820        private final ReferenceQueue<V> orphanedEntries
821            = new ReferenceQueue<>();
822
823        @SuppressWarnings("PMD.CommentRequired")
824        private class ValueReference extends WeakReference<V> {
825
826            private final K key;
827
828            public ValueReference(K key, V referent) {
829                super(referent, orphanedEntries);
830                this.key = key;
831            }
832
833            /*
834             * (non-Javadoc)
835             * 
836             * @see java.lang.Object#hashCode()
837             */
838            @Override
839            public int hashCode() {
840                V value = get();
841                if (value == null) {
842                    return 0;
843                }
844                return value.hashCode();
845            }
846
847            /*
848             * (non-Javadoc)
849             * 
850             * @see java.lang.Object#equals(java.lang.Object)
851             */
852            @Override
853            public boolean equals(Object obj) {
854                if (obj == null) {
855                    return false;
856                }
857                if (!obj.getClass().equals(ValueReference.class)) {
858                    return false;
859                }
860                V value1 = get();
861                @SuppressWarnings("unchecked")
862                V value2 = ((ValueReference) obj).get();
863                if (value1 == null || value2 == null) {
864                    return false;
865                }
866                return value1.equals(value2);
867            }
868        }
869
870        @SuppressWarnings("PMD.CompareObjectsWithEquals")
871        private void cleanOrphaned() {
872            while (true) {
873                @SuppressWarnings("unchecked")
874                ValueReference orphaned
875                    = (ValueReference) orphanedEntries.poll();
876                if (orphaned == null) {
877                    return;
878                }
879                synchronized (this) {
880                    Set<ValueReference> set = backing.get(orphaned.key);
881                    if (set == null) {
882                        continue;
883                    }
884                    Iterator<ValueReference> iter = set.iterator();
885                    while (iter.hasNext()) {
886                        ValueReference ref = iter.next();
887                        if (ref == orphaned) {
888                            iter.remove();
889                            if (set.isEmpty()) {
890                                backing.remove(orphaned.key);
891                            }
892                            break;
893                        }
894                    }
895                }
896            }
897        }
898
899        /**
900         * Remove all entries.
901         */
902        public void clear() {
903            backing.clear();
904            cleanOrphaned();
905        }
906
907        /**
908         * Checks if the key is in the index.
909         *
910         * @param key the key
911         * @return true, if successful
912         */
913        public boolean containsKey(Object key) {
914            return backing.containsKey(key);
915        }
916
917        /**
918         * Checks if the index is empty.
919         *
920         * @return true, if is empty
921         */
922        public boolean isEmpty() {
923            return backing.isEmpty();
924        }
925
926        /**
927         * Returns all keys.
928         *
929         * @return the sets the
930         */
931        public Set<K> keySet() {
932            synchronized (this) {
933                return backing.keySet();
934            }
935        }
936
937        /**
938         * Adds the value to the pool of values associated with the key.
939         *
940         * @param key the key
941         * @param value the value
942         * @return the v
943         */
944        public V add(K key, V value) {
945            synchronized (this) {
946                cleanOrphaned();
947                backing.computeIfAbsent(key, k -> new HashSet<>())
948                    .add(new ValueReference(key, value));
949                return value;
950            }
951        }
952
953        /**
954         * Retrives and removes an item from the pool associated with the key.
955         *
956         * @param key the key
957         * @return the removed item or `null` if the pool is empty
958         */
959        @SuppressWarnings("PMD.AvoidBranchingStatementAsLastInLoop")
960        public V poll(K key) {
961            synchronized (this) {
962                cleanOrphaned();
963                Set<ValueReference> set = backing.get(key);
964                if (set == null) {
965                    return null;
966                }
967                Iterator<ValueReference> iter = set.iterator();
968                while (iter.hasNext()) {
969                    ValueReference ref = iter.next();
970                    V value = ref.get();
971                    iter.remove();
972                    if (value == null) {
973                        continue;
974                    }
975                    if (set.isEmpty()) {
976                        backing.remove(key);
977                    }
978                    return value;
979                }
980            }
981            return null;
982        }
983
984        /**
985         * Removes all values associated with the key.
986         *
987         * @param key the key
988         */
989        public void removeAll(K key) {
990            synchronized (this) {
991                cleanOrphaned();
992                backing.remove(key);
993            }
994        }
995
996        /**
997         * Removes the given value from the pool associated with the given key.
998         *
999         * @param key the key
1000         * @param value the value
1001         * @return the v
1002         */
1003        @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
1004        public V remove(K key, V value) {
1005            synchronized (this) {
1006                cleanOrphaned();
1007                Set<ValueReference> set = backing.get(key);
1008                if (set == null) {
1009                    return null;
1010                }
1011                Iterator<ValueReference> iter = set.iterator();
1012                while (iter.hasNext()) {
1013                    ValueReference ref = iter.next();
1014                    V stored = ref.get();
1015                    boolean found = false;
1016                    if (stored == null) {
1017                        iter.remove();
1018                    }
1019                    if (stored.equals(value)) {
1020                        iter.remove();
1021                        found = true;
1022                    }
1023                    if (set.isEmpty()) {
1024                        backing.remove(key);
1025                    }
1026                    if (found) {
1027                        return value;
1028                    }
1029                }
1030            }
1031            return null;
1032        }
1033
1034        /**
1035         * Removes the value from the first pool in which it is found.
1036         *
1037         * @param value the value
1038         * @return the value or `null` if the value is not found
1039         */
1040        public V remove(V value) {
1041            synchronized (this) {
1042                for (Set<ValueReference> set : backing.values()) {
1043                    Iterator<ValueReference> iter = set.iterator();
1044                    while (iter.hasNext()) {
1045                        ValueReference ref = iter.next();
1046                        V stored = ref.get();
1047                        if (stored == null) {
1048                            iter.remove();
1049                        }
1050                        if (stored.equals(value)) {
1051                            iter.remove();
1052                            return value;
1053                        }
1054                    }
1055                }
1056            }
1057            return null;
1058        }
1059
1060        /**
1061         * Returns the number of keys in the index.
1062         *
1063         * @return the numer of keys
1064         */
1065        public int keysSize() {
1066            return backing.size();
1067        }
1068
1069    }
1070}