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