package service; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.util.Calendar; import java.util.Map; import java.util.NavigableMap; import java.util.TimeZone; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.atomic.AtomicInteger; import com.google.common.collect.ImmutableSortedMap; public class InMemoryEventTimeline<E> implements EventTimeline<String, E> { private static final String IDENTIFIER_FORMAT = "%0"+Long.toHexString(Long.MAX_VALUE).length()+"x"+ "%0"+Integer.toHexString(Integer.MAX_VALUE).length()+"x"; private final AtomicInteger counter = new AtomicInteger(); private final ConcurrentNavigableMap<String, Reference<E>> timeline = new ConcurrentSkipListMap<String, Reference<E>>(); // Initial lost event ID is lexigraphically less than any valid ID private String lastLostEventId = getIdentifier(getMillis(), 0); @Override public String getLastEventId() { if (timeline.isEmpty()) return lastLostEventId; return timeline.lastKey(); } @Override public NavigableMap<String, E> getKnown() { try { return resolveRefs(timeline); } catch (ForgottenEventException e) { // GC may have run, so cleanup and try again cleanup(); return getKnown(); } } @Override public NavigableMap<String, E> getSince(String id) throws ForgottenEventException { if (id.compareTo(lastLostEventId) < 0) { throw new ForgottenEventException(id + " is before known history."); } return resolveRefs(timeline.tailMap(id,false)); } @Override public synchronized String record(E event) { cleanup(); final String id = getNewIdentifier(); timeline.put(id, getReference(event)); return id; } protected NavigableMap<String, E> resolveRefs( final NavigableMap<String, Reference<E>> m) throws service.EventTimeline.ForgottenEventException { final ImmutableSortedMap.Builder<String, E> b = ImmutableSortedMap .<String, E> naturalOrder(); for (final Map.Entry<String, Reference<E>> e : m.entrySet()) { final E v = e.getValue().get(); if (v == null) throw new ForgottenEventException(e.getKey() + " has been forgotten."); b.put(e.getKey(), v); } return b.build(); } protected void cleanup() { for (String k : timeline.keySet()) { if (timeline.get(k).get() != null) return; timeline.remove(k); lastLostEventId = k; } } /* * This method can be overridden for testing purposes. */ protected Reference<E> getReference(E referent) { return new SoftReference<E>(referent); } private String getNewIdentifier() { return getIdentifier(getMillis(), counter.addAndGet(1)); } private static String getIdentifier(final long ms, final int c) { return String.format(IDENTIFIER_FORMAT, ms, c); } private static Long getMillis() { return Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTimeInMillis(); } }