diff --git a/documentation/jetty/modules/programming-guide/pages/arch/bean.adoc b/documentation/jetty/modules/programming-guide/pages/arch/bean.adoc index 56598234bb28..5ec7bc2cf09c 100644 --- a/documentation/jetty/modules/programming-guide/pages/arch/bean.adoc +++ b/documentation/jetty/modules/programming-guide/pages/arch/bean.adoc @@ -108,6 +108,59 @@ If you need component tree features such as automatic xref:arch/jmx.adoc[export You can make the long-lived container efficient at adding/removing the short-lived components using a data structure that is not part of the component tree, and make the long-lived container handle the JMX and dump features for the short-lived components. ==== +[[bean-collections]] +== Bean Container Collections + +Jetty provides specialized collection classes that automatically manage their elements as beans in a `ContainerLifeCycle`. +When elements are added to these collections, they are registered as managed beans; when removed, they are unregistered. +This ensures that lifecycle events (start, stop) are properly propagated to collection elements. + +[[attribute-container-map]] +=== AttributeContainerMap + +`AttributeContainerMap` implements the `Attributes` interface while managing attribute values as beans. +When you set an attribute, the value is added as a managed bean; when you remove an attribute, the bean is removed. + +[,java,indent=0] +---- +AttributeContainerMap attributes = new AttributeContainerMap(); + +// Add a LifeCycle as an attribute - it becomes a managed bean +attributes.setAttribute("myService", new MyService()); + +// Start the container - all managed attribute values are started +attributes.start(); + +// Remove the attribute - the bean is stopped and removed +attributes.removeAttribute("myService"); +---- + +[[container-lifecycle-map]] +=== ContainerLifeCycleMap + +`ContainerLifeCycleMap` implements the `Map` interface while managing map values as beans. +This is useful when you need to maintain a keyed collection of lifecycle-managed components. + +[,java,indent=0] +---- +ContainerLifeCycleMap connectors = new ContainerLifeCycleMap<>(); + +// Add connectors - they become managed beans +connectors.put("http", new ServerConnector(server)); +connectors.put("https", new ServerConnector(server, sslContextFactory)); + +// Start the container - all connectors are started +connectors.start(); + +// Access connectors by key +Connector http = connectors.get("http"); + +// Remove a connector - it is stopped and removed as a bean +connectors.remove("http"); +---- + +The collection views returned by `keySet()`, `values()`, and `entrySet()` are wrapped to properly manage beans when elements are removed through these views. + [[listener]] == Jetty Component Listeners diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycleMap.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycleMap.java new file mode 100644 index 000000000000..f2d8e30dab49 --- /dev/null +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycleMap.java @@ -0,0 +1,462 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util.component; + +import java.io.IOException; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.thread.AutoLock; + +/** + * A {@link Map} implementation that manages its values as beans in a {@link ContainerLifeCycle}. + *

+ * When values are added to the map via {@link #put(Object, Object)} or {@link #putAll(Map)}, + * they are also added as managed beans. When values are removed via {@link #remove(Object)}, + * {@link #clear()}, or through collection views, they are removed as beans. + *

+ *

+ * The lifecycle of the values is tied to this container: when the container starts, + * all managed values are started; when it stops, they are stopped. + *

+ * + * @param the type of keys maintained by this map + * @param the type of mapped values, must extend {@link LifeCycle} + */ +public class ContainerLifeCycleMap extends ContainerLifeCycle implements Map +{ + private final AutoLock _lock = new AutoLock(); + private final Map _map = new HashMap<>(); + + @Override + public int size() + { + try (AutoLock l = _lock.lock()) + { + return _map.size(); + } + } + + @Override + public boolean isEmpty() + { + try (AutoLock l = _lock.lock()) + { + return _map.isEmpty(); + } + } + + @Override + public boolean containsKey(Object key) + { + try (AutoLock l = _lock.lock()) + { + return _map.containsKey(key); + } + } + + @Override + public boolean containsValue(Object value) + { + try (AutoLock l = _lock.lock()) + { + return _map.containsValue(value); + } + } + + @Override + public V get(Object key) + { + try (AutoLock l = _lock.lock()) + { + return _map.get(key); + } + } + + @Override + public V put(K key, V value) + { + try (AutoLock l = _lock.lock()) + { + V old = _map.put(key, value); + updateBean(old, value); + return old; + } + } + + @Override + public V remove(Object key) + { + try (AutoLock l = _lock.lock()) + { + V removed = _map.remove(key); + if (removed != null) + removeBean(removed); + return removed; + } + } + + @Override + public void putAll(Map m) + { + try (AutoLock l = _lock.lock()) + { + for (Entry entry : m.entrySet()) + { + V old = _map.put(entry.getKey(), entry.getValue()); + updateBean(old, entry.getValue()); + } + } + } + + @Override + public void clear() + { + try (AutoLock l = _lock.lock()) + { + _map.clear(); + removeBeans(); + } + } + + @Override + public Set keySet() + { + return new KeySet(); + } + + @Override + public Collection values() + { + return new Values(); + } + + @Override + public Set> entrySet() + { + return new EntrySet(); + } + + @Override + public void dump(Appendable out, String indent) throws IOException + { + Dumpable.dumpObject(out, this); + try (AutoLock l = _lock.lock()) + { + Dumpable.dumpMapEntries(out, indent, _map, true); + } + } + + @Override + public String toString() + { + try (AutoLock l = _lock.lock()) + { + return String.format("%s@%x{size=%d}", TypeUtil.toShortName(this.getClass()), hashCode(), _map.size()); + } + } + + /** + * A wrapped key set that intercepts removal operations to properly remove beans. + */ + private class KeySet extends AbstractSet + { + @Override + public Iterator iterator() + { + return new KeyIterator(); + } + + @Override + public int size() + { + return ContainerLifeCycleMap.this.size(); + } + + @Override + public boolean contains(Object o) + { + return ContainerLifeCycleMap.this.containsKey(o); + } + + @Override + public boolean remove(Object o) + { + return ContainerLifeCycleMap.this.remove(o) != null; + } + + @Override + public void clear() + { + ContainerLifeCycleMap.this.clear(); + } + } + + /** + * A wrapped values collection that intercepts removal operations to properly remove beans. + */ + private class Values extends AbstractCollection + { + @Override + public Iterator iterator() + { + return new ValueIterator(); + } + + @Override + public int size() + { + return ContainerLifeCycleMap.this.size(); + } + + @Override + public boolean contains(Object o) + { + return ContainerLifeCycleMap.this.containsValue(o); + } + + @Override + public void clear() + { + ContainerLifeCycleMap.this.clear(); + } + } + + /** + * A wrapped entry set that intercepts removal and setValue operations to properly manage beans. + */ + private class EntrySet extends AbstractSet> + { + @Override + public Iterator> iterator() + { + return new EntryIterator(); + } + + @Override + public int size() + { + return ContainerLifeCycleMap.this.size(); + } + + @Override + public boolean contains(Object o) + { + if (!(o instanceof Entry e)) + return false; + try (AutoLock l = _lock.lock()) + { + V value = _map.get(e.getKey()); + return value != null && value.equals(e.getValue()); + } + } + + @Override + public boolean remove(Object o) + { + if (!(o instanceof Entry e)) + return false; + try (AutoLock l = _lock.lock()) + { + V value = _map.get(e.getKey()); + if (value != null && value.equals(e.getValue())) + { + _map.remove(e.getKey()); + removeBean(value); + return true; + } + return false; + } + } + + @Override + public void clear() + { + ContainerLifeCycleMap.this.clear(); + } + } + + /** + * Iterator over keys that properly removes beans when iterator.remove() is called. + */ + private class KeyIterator implements Iterator + { + private final Iterator> _iterator; + private Entry _current; + + KeyIterator() + { + try (AutoLock l = _lock.lock()) + { + // Create a copy to avoid ConcurrentModificationException + _iterator = new HashMap<>(_map).entrySet().iterator(); + } + } + + @Override + public boolean hasNext() + { + return _iterator.hasNext(); + } + + @Override + public K next() + { + _current = _iterator.next(); + return _current.getKey(); + } + + @Override + public void remove() + { + if (_current == null) + throw new IllegalStateException(); + ContainerLifeCycleMap.this.remove(_current.getKey()); + _current = null; + } + } + + /** + * Iterator over values that properly removes beans when iterator.remove() is called. + */ + private class ValueIterator implements Iterator + { + private final Iterator> _iterator; + private Entry _current; + + ValueIterator() + { + try (AutoLock l = _lock.lock()) + { + // Create a copy to avoid ConcurrentModificationException + _iterator = new HashMap<>(_map).entrySet().iterator(); + } + } + + @Override + public boolean hasNext() + { + return _iterator.hasNext(); + } + + @Override + public V next() + { + _current = _iterator.next(); + return _current.getValue(); + } + + @Override + public void remove() + { + if (_current == null) + throw new IllegalStateException(); + ContainerLifeCycleMap.this.remove(_current.getKey()); + _current = null; + } + } + + /** + * Iterator over entries that properly manages beans when iterator.remove() or entry.setValue() is called. + */ + private class EntryIterator implements Iterator> + { + private final Iterator> _iterator; + private Entry _current; + + EntryIterator() + { + try (AutoLock l = _lock.lock()) + { + // Create a copy to avoid ConcurrentModificationException + _iterator = new HashMap<>(_map).entrySet().iterator(); + } + } + + @Override + public boolean hasNext() + { + return _iterator.hasNext(); + } + + @Override + public Entry next() + { + _current = _iterator.next(); + // Return a wrapped entry that intercepts setValue + return new WrappedEntry(_current.getKey()); + } + + @Override + public void remove() + { + if (_current == null) + throw new IllegalStateException(); + ContainerLifeCycleMap.this.remove(_current.getKey()); + _current = null; + } + } + + /** + * A wrapped entry that intercepts setValue to properly manage beans. + */ + private class WrappedEntry implements Entry + { + private final K _key; + + WrappedEntry(K key) + { + _key = key; + } + + @Override + public K getKey() + { + return _key; + } + + @Override + public V getValue() + { + return ContainerLifeCycleMap.this.get(_key); + } + + @Override + public V setValue(V value) + { + return ContainerLifeCycleMap.this.put(_key, value); + } + + @Override + public boolean equals(Object o) + { + if (!(o instanceof Entry e)) + return false; + return _key.equals(e.getKey()) && getValue().equals(e.getValue()); + } + + @Override + public int hashCode() + { + V value = getValue(); + return _key.hashCode() ^ (value == null ? 0 : value.hashCode()); + } + } +} diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/component/AttributeContainerMapTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/component/AttributeContainerMapTest.java new file mode 100644 index 000000000000..1b40a120adff --- /dev/null +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/component/AttributeContainerMapTest.java @@ -0,0 +1,260 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util.component; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AttributeContainerMapTest +{ + @Test + public void testSetGetRemoveAttribute() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + // Test setAttribute + assertNull(map.setAttribute("key1", bean1)); + assertNull(map.setAttribute("key2", bean2)); + + // Test getAttribute + assertEquals(bean1, map.getAttribute("key1")); + assertEquals(bean2, map.getAttribute("key2")); + assertNull(map.getAttribute("nonexistent")); + + // Test getAttributeNameSet + Set names = map.getAttributeNameSet(); + assertThat(names, containsInAnyOrder("key1", "key2")); + + // Test removeAttribute + assertEquals(bean1, map.removeAttribute("key1")); + assertNull(map.getAttribute("key1")); + assertNull(map.removeAttribute("nonexistent")); + + // Verify key1 is removed from names + names = map.getAttributeNameSet(); + assertThat(names, containsInAnyOrder("key2")); + } + + @Test + public void testSetAttributeReplacesOldValue() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + assertNull(map.setAttribute("key", bean1)); + assertEquals(bean1, map.setAttribute("key", bean2)); + assertEquals(bean2, map.getAttribute("key")); + } + + @Test + public void testClearAttributes() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.setAttribute("key1", bean1); + map.setAttribute("key2", bean2); + + map.clearAttributes(); + + assertNull(map.getAttribute("key1")); + assertNull(map.getAttribute("key2")); + assertThat(map.getAttributeNameSet(), empty()); + } + + @Test + public void testBeanAddedOnSetAttribute() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + TestLifeCycle bean = new TestLifeCycle("bean"); + + map.setAttribute("key", bean); + + // Bean should be added to the container + assertTrue(map.contains(bean)); + } + + @Test + public void testBeanRemovedOnRemoveAttribute() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + TestLifeCycle bean = new TestLifeCycle("bean"); + + map.setAttribute("key", bean); + assertTrue(map.contains(bean)); + + map.removeAttribute("key"); + assertFalse(map.contains(bean)); + } + + @Test + public void testBeanReplacedOnSetAttribute() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.setAttribute("key", bean1); + assertTrue(map.contains(bean1)); + assertFalse(map.contains(bean2)); + + map.setAttribute("key", bean2); + assertFalse(map.contains(bean1)); + assertTrue(map.contains(bean2)); + } + + @Test + public void testBeansRemovedOnClearAttributes() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.setAttribute("key1", bean1); + map.setAttribute("key2", bean2); + assertTrue(map.contains(bean1)); + assertTrue(map.contains(bean2)); + + map.clearAttributes(); + assertFalse(map.contains(bean1)); + assertFalse(map.contains(bean2)); + } + + @Test + public void testLifeCycleManagement() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.setAttribute("key1", bean1); + map.setAttribute("key2", bean2); + + // Beans should not be started yet + assertEquals(0, bean1.started.get()); + assertEquals(0, bean2.started.get()); + + // Start the container + map.start(); + + // Beans should now be started + assertEquals(1, bean1.started.get()); + assertEquals(1, bean2.started.get()); + assertTrue(bean1.isStarted()); + assertTrue(bean2.isStarted()); + + // Stop the container + map.stop(); + + // Beans should now be stopped + assertEquals(1, bean1.stopped.get()); + assertEquals(1, bean2.stopped.get()); + assertTrue(bean1.isStopped()); + assertTrue(bean2.isStopped()); + } + + @Test + public void testLifeCycleAddWhileRunning() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.setAttribute("key1", bean1); + map.start(); + + assertEquals(1, bean1.started.get()); + assertEquals(0, bean2.started.get()); + + // Add bean2 while container is running + map.setAttribute("key2", bean2); + + // bean2 is added as unmanaged since container is already started + // The bean needs to be started separately or managed explicitly + assertTrue(map.contains(bean2)); + } + + @Test + public void testNonLifeCycleAttribute() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + String stringValue = "hello"; + + map.setAttribute("key", stringValue); + assertEquals(stringValue, map.getAttribute("key")); + + map.start(); + map.stop(); + + // String value should still be accessible + assertEquals(stringValue, map.getAttribute("key")); + } + + @Test + public void testToString() throws Exception + { + AttributeContainerMap map = new AttributeContainerMap(); + map.setAttribute("key1", "value1"); + map.setAttribute("key2", "value2"); + + String str = map.toString(); + assertTrue(str.contains("size=2")); + } + + private static class TestLifeCycle extends AbstractLifeCycle + { + private final String _name; + private final AtomicInteger started = new AtomicInteger(); + private final AtomicInteger stopped = new AtomicInteger(); + + TestLifeCycle(String name) + { + _name = name; + } + + @Override + protected void doStart() throws Exception + { + started.incrementAndGet(); + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + stopped.incrementAndGet(); + super.doStop(); + } + + @Override + public String toString() + { + return _name; + } + } +} diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/component/ContainerLifeCycleMapTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/component/ContainerLifeCycleMapTest.java new file mode 100644 index 000000000000..a79ac097c0e1 --- /dev/null +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/component/ContainerLifeCycleMapTest.java @@ -0,0 +1,450 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util.component; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ContainerLifeCycleMapTest +{ + @Test + public void testPutGetRemove() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + // Test put + assertNull(map.put("key1", bean1)); + assertNull(map.put("key2", bean2)); + + // Test get + assertEquals(bean1, map.get("key1")); + assertEquals(bean2, map.get("key2")); + assertNull(map.get("nonexistent")); + + // Test size + assertEquals(2, map.size()); + assertFalse(map.isEmpty()); + + // Test containsKey/containsValue + assertTrue(map.containsKey("key1")); + assertTrue(map.containsValue(bean1)); + assertFalse(map.containsKey("nonexistent")); + + // Test remove + assertEquals(bean1, map.remove("key1")); + assertNull(map.get("key1")); + assertNull(map.remove("nonexistent")); + assertEquals(1, map.size()); + } + + @Test + public void testPutReplacesOldValue() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + assertNull(map.put("key", bean1)); + assertEquals(bean1, map.put("key", bean2)); + assertEquals(bean2, map.get("key")); + assertEquals(1, map.size()); + } + + @Test + public void testPutAll() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + Map other = new HashMap<>(); + other.put("key1", bean1); + other.put("key2", bean2); + + map.putAll(other); + + assertEquals(2, map.size()); + assertEquals(bean1, map.get("key1")); + assertEquals(bean2, map.get("key2")); + assertTrue(map.contains(bean1)); + assertTrue(map.contains(bean2)); + } + + @Test + public void testClear() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + map.clear(); + + assertTrue(map.isEmpty()); + assertEquals(0, map.size()); + assertNull(map.get("key1")); + assertNull(map.get("key2")); + } + + @Test + public void testBeanAddedOnPut() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean = new TestLifeCycle("bean"); + + map.put("key", bean); + + // Bean should be added to the container + assertTrue(map.contains(bean)); + } + + @Test + public void testBeanRemovedOnRemove() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean = new TestLifeCycle("bean"); + + map.put("key", bean); + assertTrue(map.contains(bean)); + + map.remove("key"); + assertFalse(map.contains(bean)); + } + + @Test + public void testBeanReplacedOnPut() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key", bean1); + assertTrue(map.contains(bean1)); + assertFalse(map.contains(bean2)); + + map.put("key", bean2); + assertFalse(map.contains(bean1)); + assertTrue(map.contains(bean2)); + } + + @Test + public void testBeansRemovedOnClear() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + assertTrue(map.contains(bean1)); + assertTrue(map.contains(bean2)); + + map.clear(); + assertFalse(map.contains(bean1)); + assertFalse(map.contains(bean2)); + } + + @Test + public void testLifeCycleManagement() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + // Beans should not be started yet + assertEquals(0, bean1.started.get()); + assertEquals(0, bean2.started.get()); + + // Start the container + map.start(); + + // Beans should now be started + assertEquals(1, bean1.started.get()); + assertEquals(1, bean2.started.get()); + assertTrue(bean1.isStarted()); + assertTrue(bean2.isStarted()); + + // Stop the container + map.stop(); + + // Beans should now be stopped + assertEquals(1, bean1.stopped.get()); + assertEquals(1, bean2.stopped.get()); + assertTrue(bean1.isStopped()); + assertTrue(bean2.isStopped()); + } + + @Test + public void testKeySet() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + Set keys = map.keySet(); + assertEquals(2, keys.size()); + assertTrue(keys.contains("key1")); + assertTrue(keys.contains("key2")); + } + + @Test + public void testKeySetRemove() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + // Remove via keySet + map.keySet().remove("key1"); + + assertNull(map.get("key1")); + assertFalse(map.contains(bean1)); + assertEquals(1, map.size()); + } + + @Test + public void testKeySetIteratorRemove() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + // Remove via keySet iterator + Iterator iter = map.keySet().iterator(); + while (iter.hasNext()) + { + String key = iter.next(); + if (key.equals("key1")) + { + iter.remove(); + } + } + + assertNull(map.get("key1")); + assertFalse(map.contains(bean1)); + assertEquals(1, map.size()); + } + + @Test + public void testValuesIteratorRemove() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + // Remove via values iterator + Iterator iter = map.values().iterator(); + while (iter.hasNext()) + { + TestLifeCycle value = iter.next(); + if (value == bean1) + { + iter.remove(); + } + } + + assertNull(map.get("key1")); + assertFalse(map.contains(bean1)); + assertEquals(1, map.size()); + } + + @Test + public void testEntrySet() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + Set> entries = map.entrySet(); + assertEquals(2, entries.size()); + } + + @Test + public void testEntrySetIteratorRemove() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + // Remove via entrySet iterator + Iterator> iter = map.entrySet().iterator(); + while (iter.hasNext()) + { + Map.Entry entry = iter.next(); + if (entry.getKey().equals("key1")) + { + iter.remove(); + } + } + + assertNull(map.get("key1")); + assertFalse(map.contains(bean1)); + assertEquals(1, map.size()); + } + + @Test + public void testEntrySetValue() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + + // Set value via entry + for (Map.Entry entry : map.entrySet()) + { + if (entry.getKey().equals("key1")) + { + entry.setValue(bean2); + } + } + + assertEquals(bean2, map.get("key1")); + assertFalse(map.contains(bean1)); + assertTrue(map.contains(bean2)); + } + + @Test + public void testKeySetClear() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + map.keySet().clear(); + + assertTrue(map.isEmpty()); + assertFalse(map.contains(bean1)); + assertFalse(map.contains(bean2)); + } + + @Test + public void testValuesClear() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + map.values().clear(); + + assertTrue(map.isEmpty()); + assertFalse(map.contains(bean1)); + assertFalse(map.contains(bean2)); + } + + @Test + public void testEntrySetClear() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + TestLifeCycle bean1 = new TestLifeCycle("bean1"); + TestLifeCycle bean2 = new TestLifeCycle("bean2"); + + map.put("key1", bean1); + map.put("key2", bean2); + + map.entrySet().clear(); + + assertTrue(map.isEmpty()); + assertFalse(map.contains(bean1)); + assertFalse(map.contains(bean2)); + } + + @Test + public void testToString() throws Exception + { + ContainerLifeCycleMap map = new ContainerLifeCycleMap<>(); + map.put("key1", new TestLifeCycle("bean1")); + map.put("key2", new TestLifeCycle("bean2")); + + String str = map.toString(); + assertTrue(str.contains("size=2")); + } + + private static class TestLifeCycle extends AbstractLifeCycle + { + private final String _name; + private final AtomicInteger started = new AtomicInteger(); + private final AtomicInteger stopped = new AtomicInteger(); + + TestLifeCycle(String name) + { + _name = name; + } + + @Override + protected void doStart() throws Exception + { + started.incrementAndGet(); + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + stopped.incrementAndGet(); + super.doStop(); + } + + @Override + public String toString() + { + return _name; + } + } +}