Implement ContentStore
diff --git a/src/main/java/com/intel/jndn/utils/ContentStore.java b/src/main/java/com/intel/jndn/utils/ContentStore.java
new file mode 100644
index 0000000..aad8a0d
--- /dev/null
+++ b/src/main/java/com/intel/jndn/utils/ContentStore.java
@@ -0,0 +1,66 @@
+/*
+ * jndn-utils
+ * Copyright (c) 2016, Intel Corporation.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 3, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ * more details.
+ */
+
+package com.intel.jndn.utils;
+
+import net.named_data.jndn.Face;
+import net.named_data.jndn.Name;
+import net.named_data.jndn.util.Blob;
+
+import java.io.IOException;
+
+/**
+ * TODO merge with Repository
+ *
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public interface ContentStore {
+ /**
+ * Store some content under a name
+ *
+ * @param name the name of the content
+ * @param content the bytes of data
+ */
+ void put(Name name, Blob content);
+
+ /**
+ * Check if the content exists
+ *
+ * @param name the name of the content
+ * @return true if the content exists
+ */
+ boolean has(Name name);
+
+ /**
+ * Retrieve the content by name
+ *
+ * @param name the name of the content
+ * @return the content if it exists or null otherwise TODO throw instead? Optional?
+ */
+ Blob get(Name name);
+
+ /**
+ * Write the data under a given name to the face
+ *
+ * @param face the face to write to
+ * @param name the name of the data to write
+ * @throws IOException if the writing fails
+ */
+ void push(Face face, Name name) throws IOException;
+
+ /**
+ * Remove all stored content
+ */
+ void clear();
+}
diff --git a/src/main/java/com/intel/jndn/utils/Publisher.java b/src/main/java/com/intel/jndn/utils/Publisher.java
index 7755b5c..4a93084 100644
--- a/src/main/java/com/intel/jndn/utils/Publisher.java
+++ b/src/main/java/com/intel/jndn/utils/Publisher.java
@@ -16,9 +16,15 @@
import net.named_data.jndn.util.Blob;
+import java.io.IOException;
+
/**
* @author Andrew Brown, andrew.brown@intel.com
*/
-public interface Publisher {
- void publish(Blob message);
+public interface Publisher extends AutoCloseable {
+ /**
+ * @param message a binary blob to publish to a topic
+ * @throws IOException if the publication fails
+ */
+ void publish(Blob message) throws IOException;
}
diff --git a/src/main/java/com/intel/jndn/utils/pubsub/ContentStore.java b/src/main/java/com/intel/jndn/utils/PushableRepository.java
similarity index 63%
rename from src/main/java/com/intel/jndn/utils/pubsub/ContentStore.java
rename to src/main/java/com/intel/jndn/utils/PushableRepository.java
index 34c6ac0..4da471a 100644
--- a/src/main/java/com/intel/jndn/utils/pubsub/ContentStore.java
+++ b/src/main/java/com/intel/jndn/utils/PushableRepository.java
@@ -1,35 +1,35 @@
-/*
- * jndn-utils
- * Copyright (c) 2016, Intel Corporation.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms and conditions of the GNU Lesser General Public License,
- * version 3, as published by the Free Software Foundation.
- *
- * This program is distributed in the hope it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
- * more details.
- */
-
-package com.intel.jndn.utils.pubsub;
-
-import net.named_data.jndn.Face;
-import net.named_data.jndn.Name;
-
-import java.io.IOException;
-
-/**
- * @author Andrew Brown, andrew.brown@intel.com
- */
-interface ContentStore<T> {
- void put(Name name, T data);
-
- boolean has(Name name);
-
- T get(Name name);
-
- void push(Face face, Name name) throws IOException;
-
- void clear();
-}
+/*
+ * jndn-utils
+ * Copyright (c) 2016, Intel Corporation.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 3, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ * more details.
+ */
+
+package com.intel.jndn.utils;
+
+import net.named_data.jndn.Face;
+import net.named_data.jndn.Name;
+
+import java.io.IOException;
+
+/**
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public interface PushableRepository extends Repository {
+ /**
+ * Write data to a face. Each name must correspond to one datum, but the implementation may choose to write these
+ * as separate data packets (e.g. as segments of a file).
+ *
+ * @param face the face to write the data to
+ * @param name the name of the data to write
+ * @throws IOException if the writing fails
+ */
+ void push(Face face, Name name) throws IOException;
+}
diff --git a/src/main/java/com/intel/jndn/utils/Subscriber.java b/src/main/java/com/intel/jndn/utils/Subscriber.java
index 4949826..89e5168 100644
--- a/src/main/java/com/intel/jndn/utils/Subscriber.java
+++ b/src/main/java/com/intel/jndn/utils/Subscriber.java
@@ -21,6 +21,11 @@
/**
* @author Andrew Brown, andrew.brown@intel.com
*/
-public interface Subscriber {
+public interface Subscriber extends AutoCloseable {
+ /**
+ * @param onMessage called every time a new message is received
+ * @param onError called every time an error occurs
+ * @return a cancellation token for stopping the subscription
+ */
Cancellation subscribe(On<Blob> onMessage, On<Exception> onError);
}
diff --git a/src/main/java/com/intel/jndn/utils/server/impl/SegmentedServerHelper.java b/src/main/java/com/intel/jndn/utils/impl/SegmentedServerHelper.java
similarity index 97%
rename from src/main/java/com/intel/jndn/utils/server/impl/SegmentedServerHelper.java
rename to src/main/java/com/intel/jndn/utils/impl/SegmentedServerHelper.java
index 68ab3eb..8fd4cdb 100644
--- a/src/main/java/com/intel/jndn/utils/server/impl/SegmentedServerHelper.java
+++ b/src/main/java/com/intel/jndn/utils/impl/SegmentedServerHelper.java
@@ -1,6 +1,6 @@
/*
* jndn-utils
- * Copyright (c) 2015, Intel Corporation.
+ * Copyright (c) 2016, Intel Corporation.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU Lesser General Public License,
@@ -11,7 +11,8 @@
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
* more details.
*/
-package com.intel.jndn.utils.server.impl;
+
+package com.intel.jndn.utils.impl;
import net.named_data.jndn.Data;
import net.named_data.jndn.Name;
diff --git a/src/main/java/com/intel/jndn/utils/pubsub/BlobContentStore.java b/src/main/java/com/intel/jndn/utils/pubsub/BlobContentStore.java
index 823bd85..375fae5 100644
--- a/src/main/java/com/intel/jndn/utils/pubsub/BlobContentStore.java
+++ b/src/main/java/com/intel/jndn/utils/pubsub/BlobContentStore.java
@@ -14,6 +14,7 @@
package com.intel.jndn.utils.pubsub;
+import com.intel.jndn.utils.ContentStore;
import net.named_data.jndn.Face;
import net.named_data.jndn.Name;
import net.named_data.jndn.util.Blob;
@@ -21,20 +22,26 @@
/**
* @author Andrew Brown, andrew.brown@intel.com
*/
-class BlobContentStore implements ContentStore<Blob> {
+class BlobContentStore implements ContentStore {
+ private final BoundedLinkedMap<Name, Blob> store;
+
+ BlobContentStore(int maxSize) {
+ this.store = new BoundedLinkedMap<>(maxSize);
+ }
+
@Override
public void put(Name name, Blob data) {
- throw new UnsupportedOperationException();
+ store.put(name, data);
}
@Override
public boolean has(Name name) {
- return false;
+ return store.containsKey(name);
}
@Override
public Blob get(Name name) {
- throw new UnsupportedOperationException();
+ return store.get(name);
}
@Override
@@ -44,6 +51,6 @@
@Override
public void clear() {
- throw new UnsupportedOperationException();
+ store.clear();
}
}
diff --git a/src/main/java/com/intel/jndn/utils/pubsub/BoundedLinkedMap.java b/src/main/java/com/intel/jndn/utils/pubsub/BoundedLinkedMap.java
new file mode 100644
index 0000000..8211fe1
--- /dev/null
+++ b/src/main/java/com/intel/jndn/utils/pubsub/BoundedLinkedMap.java
@@ -0,0 +1,159 @@
+/*
+ * jndn-utils
+ * Copyright (c) 2016, Intel Corporation.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 3, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ * more details.
+ */
+
+package com.intel.jndn.utils.pubsub;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Linked hash map exposing the earliest and latest entries added; it is bounded to a configurable size to save memory.
+ * When benchmarked against CircularBufferCache, this class had slower insertions but faster reads and was therefore
+ * retained for use.
+ * <p>
+ * It limits the amount of memory by replacing the oldest added element; this involves overriding the LinkedHashMap
+ * implementation's removeEldestEntry() method; see the <a href="https://docs.oracle.com/javase/8/docs/api/java/util/LinkedHashMap.html#removeEldestEntry-java.util.Map.Entry-">Javadoc
+ * entry</a> for more information.
+ * <p>
+ * Additionally, we implemented Josh Bloch's item 16 of Effective Java so that calls are forwarded to the underlying
+ * LinkedHashMap. This allows us to decorate with some custom behavior and synchronize as we need.
+ * <p>
+ * This class is coarsely thread-safe; every public method is synchronized for one-at-a-time access to the underlying
+ * map.
+ *
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public class BoundedLinkedMap<K, V> implements Map<K, V> {
+ private final LinkedHashMap<K, V> map;
+ private final int maxSize;
+ private K latest;
+
+ /**
+ * @param maxSize the maximum allowed number of records to store
+ */
+ public BoundedLinkedMap(int maxSize) {
+ this.maxSize = maxSize;
+ this.map = new LinkedHashMap<K, V>(this.maxSize) {
+ @Override
+ public boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ return size() > maxSize;
+ }
+ };
+ }
+
+ /**
+ * @return the earliest key added to this set or null if none are added
+ */
+ public synchronized K earliest() {
+ for (K key : map.keySet()) {
+ return key; // the LinkedHashMap guarantees iteration in order of insertion
+ }
+ return null;
+ }
+
+ /**
+ * @return the latest key added to this set or null if none are added
+ */
+ public synchronized K latest() {
+ return latest;
+ }
+
+ @Override
+ public synchronized V put(K key, V value) {
+ latest = key;
+ return map.put(key, value);
+ }
+
+ @Override
+ public synchronized int size() {
+ return map.size();
+ }
+
+ @Override
+ public synchronized boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ @Override
+ public synchronized boolean containsKey(Object key) {
+ return map.containsKey(key);
+ }
+
+ @Override
+ public synchronized boolean containsValue(Object value) {
+ return map.containsValue(value);
+ }
+
+ @Override
+ public synchronized V get(Object key) {
+ return map.get(key);
+ }
+
+
+ @Override
+ public synchronized V remove(Object key) {
+ V value = map.remove(key);
+ if (key == latest) latest = findLatest(map);
+ return value;
+ }
+
+ @Override
+ public synchronized void putAll(Map<? extends K, ? extends V> m) {
+ map.putAll(m);
+ latest = findLatest(map);
+ }
+
+ @Override
+ public synchronized void clear() {
+ map.clear();
+ latest = null;
+ }
+
+ @Override
+ public synchronized Set<K> keySet() {
+ return map.keySet();
+ }
+
+ @Override
+ public synchronized Collection<V> values() {
+ return map.values();
+ }
+
+ @Override
+ public synchronized Set<java.util.Map.Entry<K, V>> entrySet() {
+ return map.entrySet();
+ }
+
+ @Override
+ public String toString() {
+ return map.toString();
+ }
+
+ /**
+ * To find the latest key in a LinkedHashMap, iterate and return the last one found. The LinkedHashMap guarantees
+ * iteration in order of insertion
+ *
+ * @param m the map to inspect
+ * @return the latest key added to the map
+ */
+ private K findLatest(LinkedHashMap<K, V> m) {
+ K newLatest = null;
+ for (K key : m.keySet()) {
+ newLatest = key;
+ }
+ return newLatest;
+ }
+}
diff --git a/src/main/java/com/intel/jndn/utils/pubsub/NdnPublisher.java b/src/main/java/com/intel/jndn/utils/pubsub/NdnPublisher.java
index 50469ea..f68e62b 100644
--- a/src/main/java/com/intel/jndn/utils/pubsub/NdnPublisher.java
+++ b/src/main/java/com/intel/jndn/utils/pubsub/NdnPublisher.java
@@ -1,11 +1,11 @@
/*
* jndn-utils
* Copyright (c) 2016, Intel Corporation.
- *
+ *
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU Lesser General Public License,
* version 3, as published by the Free Software Foundation.
- *
+ *
* This program is distributed in the hope it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
@@ -14,6 +14,7 @@
package com.intel.jndn.utils.pubsub;
+import com.intel.jndn.utils.ContentStore;
import com.intel.jndn.utils.Publisher;
import net.named_data.jndn.Data;
import net.named_data.jndn.Face;
@@ -39,17 +40,17 @@
*/
class NdnPublisher implements Publisher, OnInterestCallback {
private static final Logger LOGGER = Logger.getLogger(NdnPublisher.class.getName());
- private final Face face; // TODO only needed in start, remove?
- private final Name prefix; // TODO only needed in start, remove?
+ private final Face face;
+ private final Name prefix;
private final AnnouncementService announcementService;
private final PendingInterestTable pendingInterestTable;
- private final ContentStore<Blob> contentStore;
+ private final ContentStore contentStore;
private final long publisherId;
private volatile long latestMessageId = 0;
private long registrationId;
- // TODO need pit
+ private boolean started = false;
- NdnPublisher(Face face, Name prefix, long publisherId, AnnouncementService announcementService, PendingInterestTable pendingInterestTable, ContentStore<Blob> contentStore) {
+ NdnPublisher(Face face, Name prefix, long publisherId, AnnouncementService announcementService, PendingInterestTable pendingInterestTable, ContentStore contentStore) {
this.face = face;
this.prefix = prefix;
this.publisherId = publisherId;
@@ -58,7 +59,8 @@
this.contentStore = contentStore;
}
- public void start() throws RegistrationFailureException {
+ void start() throws RegistrationFailureException {
+ started = true;
CompletableFuture<Void> future = new CompletableFuture<>();
OnRegistration onRegistration = new OnRegistration(future);
@@ -71,7 +73,8 @@
}
}
- public void stop() throws IOException {
+ @Override
+ public void close() throws IOException {
face.removeRegisteredPrefix(registrationId);
contentStore.clear();
announcementService.announceExit(publisherId);
@@ -79,18 +82,26 @@
// TODO should throw IOException?
@Override
- public void publish(Blob message) {
+ public void publish(Blob message) throws IOException {
+ if (!started) {
+ try {
+ start();
+ } catch (RegistrationFailureException e) {
+ throw new IOException(e);
+ }
+ }
+
long id = latestMessageId++; // TODO synchronize?
Name name = PubSubNamespace.toMessageName(prefix, publisherId, id);
contentStore.put(name, message);
LOGGER.log(Level.INFO, "Published message {0} to content store", id);
- if(pendingInterestTable.has(new Interest(name))){
+ if (pendingInterestTable.has(new Interest(name))) {
try {
contentStore.push(face, name);
} catch (IOException e) {
- LOGGER.log(Level.SEVERE, "Failed to send message {0} for pending interests: {1}", new Object[]{id, name});
+ LOGGER.log(Level.SEVERE, "Failed to send message {0} for pending interests: {1}", new Object[]{id, name, e});
}
}
}
@@ -98,7 +109,7 @@
@Override
public void onInterest(Name name, Interest interest, Face face, long registrationId, InterestFilter interestFilter) {
try {
- if(contentStore.has(interest.getName())){
+ if (contentStore.has(interest.getName())) {
contentStore.push(face, interest.getName());
} else {
pendingInterestTable.add(interest);
@@ -116,5 +127,4 @@
LOGGER.log(Level.SEVERE, "Failed to decode message ID for interest: " + interest.toUri(), e);
}
}
-
}
diff --git a/src/main/java/com/intel/jndn/utils/pubsub/NdnSubscriber.java b/src/main/java/com/intel/jndn/utils/pubsub/NdnSubscriber.java
index 83e3f28..e2deebd 100644
--- a/src/main/java/com/intel/jndn/utils/pubsub/NdnSubscriber.java
+++ b/src/main/java/com/intel/jndn/utils/pubsub/NdnSubscriber.java
@@ -23,12 +23,12 @@
import net.named_data.jndn.encoding.EncodingException;
import net.named_data.jndn.util.Blob;
-import java.util.HashSet;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
-import java.util.stream.Collectors;
/**
* @author Andrew Brown, andrew.brown@intel.com
@@ -39,7 +39,7 @@
private final Name prefix;
private final AnnouncementService announcementService;
private final Client client;
- private final Set<Context> known = new HashSet<>();
+ private final Map<Long, Context> known = new HashMap<>();
private Cancellation newAnnouncementCancellation;
private Cancellation existingAnnouncementsCancellation;
private volatile boolean started = false;
@@ -54,21 +54,26 @@
private void start() throws RegistrationFailureException {
LOGGER.log(Level.INFO, "Starting subscriber");
- existingAnnouncementsCancellation = announcementService.discoverExistingAnnouncements(this::add, null, e -> stop());
- newAnnouncementCancellation = announcementService.observeNewAnnouncements(this::add, this::remove, e -> stop());
+ existingAnnouncementsCancellation = announcementService.discoverExistingAnnouncements(this::add, null, e -> close());
+ newAnnouncementCancellation = announcementService.observeNewAnnouncements(this::add, this::remove, e -> close());
started = true;
}
private void add(long publisherId) {
- known.add(new Context(publisherId));
+ if (known.containsKey(publisherId)) {
+ LOGGER.log(Level.WARNING, "Duplicate publisher ID {} received from announcement service; this should not happen and will be ignored", publisherId);
+ } else {
+ known.put(publisherId, new Context(publisherId));
+ }
}
private void remove(long publisherId) {
- known.remove(publisherId); // TODO incorrect
+ known.remove(publisherId);
}
- private void stop() {
+ @Override
+ public void close() {
LOGGER.log(Level.INFO, "Stopping subscriber, knows of {0} publishers: {1} ", new Object[]{known.size(), known});
if (newAnnouncementCancellation != null) {
@@ -79,7 +84,7 @@
existingAnnouncementsCancellation.cancel();
}
- for (Context c : known) {
+ for (Context c : known.values()) {
c.cancel();
}
@@ -87,7 +92,7 @@
}
Set<Long> knownPublishers() {
- return known.stream().map(c -> c.publisherId).collect(Collectors.toSet());
+ return known.keySet();
}
// TODO repeated calls?
@@ -103,11 +108,11 @@
}
}
- for (Context c : known) {
+ for (Context c : known.values()) {
c.subscribe(onMessage, onError);
}
- return this::stop;
+ return this::close;
}
private class Context implements Cancellation {
@@ -180,8 +185,12 @@
@Override
public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
Context context = (Context) o;
return publisherId == context.publisherId;
}
diff --git a/src/main/java/com/intel/jndn/utils/pubsub/Topic.java b/src/main/java/com/intel/jndn/utils/pubsub/Topic.java
index 71c7d9f..4a60b1c 100644
--- a/src/main/java/com/intel/jndn/utils/pubsub/Topic.java
+++ b/src/main/java/com/intel/jndn/utils/pubsub/Topic.java
@@ -20,6 +20,8 @@
import net.named_data.jndn.Face;
import net.named_data.jndn.Name;
+import java.util.Random;
+
/**
* @author Andrew Brown, andrew.brown@intel.com
*/
@@ -41,6 +43,6 @@
// TODO move to PubSubFactory?
public Publisher newPublisher(Face face) {
- return new NdnPublisher(face, name, 23, new NdnAnnouncementService(face, name), new ForLoopPendingInterestTable(), new BlobContentStore());
+ return new NdnPublisher(face, name, new Random().nextLong(), new NdnAnnouncementService(face, name), new ForLoopPendingInterestTable(), new BlobContentStore(1024));
}
}
diff --git a/src/test/java/com/intel/jndn/utils/pubsub/BoundedLinkedMapTest.java b/src/test/java/com/intel/jndn/utils/pubsub/BoundedLinkedMapTest.java
new file mode 100644
index 0000000..3107db6
--- /dev/null
+++ b/src/test/java/com/intel/jndn/utils/pubsub/BoundedLinkedMapTest.java
@@ -0,0 +1,177 @@
+/*
+ * jndn-utils
+ * Copyright (c) 2016, Intel Corporation.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 3, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ * more details.
+ */
+
+package com.intel.jndn.utils.pubsub;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test BoundedLinkedMapTest
+ *
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public class BoundedLinkedMapTest {
+ private static final Logger LOGGER = Logger.getLogger(BoundedLinkedMapTest.class.getName());
+ private BoundedLinkedMap<String, Object> instance;
+
+ @Before
+ public void beforeTest() {
+ instance = new BoundedLinkedMap<>(2);
+ }
+
+ @Test
+ public void testUsage() {
+ Object object0 = new Object();
+ Object object1 = new Object();
+ Object object2 = new Object();
+
+ instance.put("0", object0);
+ assertEquals(1, instance.size());
+
+ instance.put("1", object1);
+ assertEquals(2, instance.size());
+
+ instance.put("2", object2);
+ assertEquals(2, instance.size());
+
+ assertNull(instance.get("0"));
+ assertEquals("2", instance.latest());
+ }
+
+ @Test
+ public void testEarliestLatest() {
+ assertNull(instance.earliest());
+ assertNull(instance.latest());
+
+ instance.put(".", new Object());
+ assertEquals(instance.earliest(), instance.latest());
+
+ instance.put("..", new Object());
+ assertEquals(".", instance.earliest());
+ assertEquals("..", instance.latest());
+
+ instance.put("...", new Object());
+ assertEquals("..", instance.earliest());
+ assertEquals("...", instance.latest());
+ }
+
+ @Test
+ public void testIsEmpty() {
+ assertTrue(instance.isEmpty());
+ instance.put("...", new Object());
+ assertFalse(instance.isEmpty());
+ }
+
+ @Test
+ public void testContainsKey() {
+ assertFalse(instance.containsKey("..."));
+ instance.put("...", new Object());
+ assertTrue(instance.containsKey("..."));
+ }
+
+ @Test
+ public void testContainsValue() {
+ Object o = new Object();
+ assertFalse(instance.containsValue(o));
+ instance.put("...", o);
+ assertTrue(instance.containsValue(o));
+ }
+
+ @Test
+ public void testRemove() {
+ Object o = new Object();
+ String key = "...";
+
+ instance.put(key, o);
+ assertTrue(instance.containsKey(key));
+ assertTrue(instance.containsValue(o));
+
+ instance.remove(key);
+ assertFalse(instance.containsKey(key));
+ assertFalse(instance.containsValue(o));
+ assertEquals(0, instance.size());
+ }
+
+ @Test
+ public void testPutAll() {
+ HashMap<String, Object> map = new HashMap<>();
+ map.put("1", new Object());
+ map.put("2", new Object());
+ map.put("99", new Object());
+
+ instance.putAll(map);
+ LOGGER.log(Level.FINE, "Map passed to putAll(): {0}", map.toString());
+ LOGGER.log(Level.FINE, "Resulting bounded map after putAll(): {0}", instance.toString());
+
+ assertEquals(2, instance.size()); // note: this is not 3 because the max size is bounded
+ assertEquals("2", instance.latest()); // note: put all doesn't do the FIFO replacement
+ }
+
+ @Test
+ public void testClear() {
+ instance.put("...", new Object());
+
+ instance.clear();
+
+ assertEquals(0, instance.size());
+ assertNull(instance.get("..."));
+ assertNull(instance.latest());
+ assertNull(instance.earliest());
+ }
+
+ @Test
+ public void testConversions() {
+ instance.put("...", new Object());
+
+ assertEquals(1, instance.keySet().size());
+ assertEquals(1, instance.values().size());
+ assertEquals(1, instance.entrySet().size());
+ }
+
+ @Test
+ public void testPerformanceAgainstArrayList() {
+ int numMessages = 10000;
+ BoundedLinkedMap<Integer, Object> map = new BoundedLinkedMap<>(numMessages);
+ ArrayList<Object> list = new ArrayList<>(numMessages);
+
+ long mapPutTime = measure(numMessages, i -> map.put(i, new Object()));
+ long listPutTime = measure(numMessages, i -> list.add(i, new Object()));
+ LOGGER.log(Level.FINE, "Custom map put has overhead of {0}% versus list put", toPercent((mapPutTime - listPutTime) / (double) listPutTime));
+
+ long mapGetTime = measure(numMessages, map::get);
+ long listGetTime = measure(numMessages, list::get);
+ LOGGER.log(Level.FINE, "Custom map get has overhead of {0}% versus list get", toPercent((mapGetTime - listGetTime) / (double) listPutTime));
+ }
+
+ private long measure(int numTimes, Consumer<Integer> work) {
+ long start = System.nanoTime();
+ for (int i = 0; i < numTimes; i++) {
+ work.accept(i);
+ }
+ return System.nanoTime() - start;
+ }
+
+ private double toPercent(double number) {
+ return Math.round(number * 100);
+ }
+}
\ No newline at end of file