BUG-648: Create MapAdaptor and friends
[yangtools.git] / common / util / src / main / java / org / opendaylight / yangtools / util / MapAdaptor.java
diff --git a/common/util/src/main/java/org/opendaylight/yangtools/util/MapAdaptor.java b/common/util/src/main/java/org/opendaylight/yangtools/util/MapAdaptor.java
new file mode 100644 (file)
index 0000000..2ef038b
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import com.romix.scala.collection.concurrent.TrieMap;
+
+/*
+ * A simple layer on top of maps, which performs snapshot mediation and optimization of
+ * what the underlying implementation is.
+ */
+public final class MapAdaptor {
+    public static final int DEFAULT_COPY_MAX_ITEMS = 100;
+    public static final String COPY_MAX_ITEMS_MAX_PROP = "org.opendaylight.yangtools.util.mapadaptor.maxcopy";
+
+    public static final int DEFAULT_PERSIST_MIN_ITEMS = 50;
+    public static final String PERSIST_MIN_ITEMS_PROP = "org.opendaylight.yangtools.util.mapadaptor.minpersist";
+
+    private static final Logger LOG = LoggerFactory.getLogger(MapAdaptor.class);
+    private static final MapAdaptor DEFAULT_INSTANCE;
+
+    private final boolean useSingleton;
+    private final int persistMinItems;
+    private final int copyMaxItems;
+
+    static {
+        DEFAULT_INSTANCE = new MapAdaptor(true,
+                getProperty(COPY_MAX_ITEMS_MAX_PROP, DEFAULT_COPY_MAX_ITEMS),
+                getProperty(PERSIST_MIN_ITEMS_PROP, DEFAULT_PERSIST_MIN_ITEMS));
+        LOG.debug("Configured HashMap/TrieMap cutoff at {}/{} entries",
+                DEFAULT_INSTANCE.persistMinItems, DEFAULT_INSTANCE.copyMaxItems);
+    }
+
+    private static final int getProperty(final String name, final int defaultValue) {
+        try {
+            final String p = System.getProperty(name);
+            if (p != null) {
+                try {
+                    int pi = Integer.valueOf(p);
+                    if (pi <= 0) {
+                        LOG.warn("Ignoring illegal value of {}: has to be a positive number", name);
+                    } else {
+                        return pi;
+                    }
+                } catch (NumberFormatException e) {
+                    LOG.warn("Ignoring non-numerical value of {}", name, e);
+                }
+            }
+        } catch (Exception e) {
+            LOG.debug("Failed to get {}", name, e);
+        }
+        return defaultValue;
+    }
+
+    private MapAdaptor(final boolean useSingleton, final int copyMaxItems, final int persistMinItems) {
+        this.useSingleton = useSingleton;
+        this.copyMaxItems = copyMaxItems;
+        this.persistMinItems = persistMinItems;
+    }
+
+    /**
+     * Return the default-configured instance.
+     *
+     * @return the singleton global instance
+     */
+    public static MapAdaptor getDefaultInstance() {
+        return DEFAULT_INSTANCE;
+    }
+
+    public static MapAdaptor getInstance(final boolean useSingleton, final int copyMaxItems, final int persistMinItems) {
+        Preconditions.checkArgument(copyMaxItems >= 0, "copyMaxItems has to be a non-negative integer");
+        Preconditions.checkArgument(persistMinItems >= 0, "persistMinItems has to be a positive integer");
+        Preconditions.checkArgument(persistMinItems <= copyMaxItems, "persistMinItems must be less than or equal to copyMaxItems");
+        return new MapAdaptor(useSingleton, copyMaxItems, persistMinItems);
+    }
+
+    /**
+     * Input is treated is supposed to be left unmodified, result must be mutable.
+     *
+     * @param input
+     * @return
+     */
+    public <K, V> Map<K, V> takeSnapshot(final Map<K, V> input) {
+        if (input instanceof ReadOnlyTrieMap) {
+            return ((ReadOnlyTrieMap<K, V>)input).toReadWrite();
+        }
+
+        LOG.trace("Converting input {} to a HashMap", input);
+
+        // FIXME: be a bit smart about allocation based on observed size
+
+        final Map<K, V> ret = new HashMap<>(input);
+        LOG.trace("Read-write HashMap is {}", ret);
+        return ret;
+    }
+
+    /**
+     * Input will be thrown away, result will be retained for read-only access or
+     * {@link #takeSnapshot(Map)} purposes.
+     *
+     * @param input
+     * @return
+     */
+    public <K, V> Map<K, V> optimize(final Map<K, V> input) {
+        if (input instanceof ReadOnlyTrieMap) {
+            LOG.warn("Optimizing read-only map {}", input);
+        }
+
+        final int size = input.size();
+
+        /*
+         * No-brainer :)
+         */
+        if (size == 0) {
+            LOG.trace("Reducing input {} to an empty map", input);
+            return Collections.<K, V>emptyMap();
+        }
+
+        /*
+         * We retain the persistent map as long as it holds at least
+         * persistMinItems
+         */
+        if (input instanceof ReadWriteTrieMap && size >= persistMinItems) {
+            return ((ReadWriteTrieMap<K, V>)input).toReadOnly();
+        }
+
+        /*
+         * If the user opted to use singleton maps, use them. Except for the case
+         * when persistMinItems dictates we should not move off of the persistent
+         * map.
+         */
+        if (useSingleton && size == 1) {
+            final Entry<K, V> e = Iterables.getOnlyElement(input.entrySet());
+            final Map<K, V> ret = Collections.singletonMap(e.getKey(), e.getValue());
+            LOG.trace("Reducing input () to singleton map {}", input, ret);
+            return ret;
+        }
+
+        if (size <= copyMaxItems) {
+            /*
+             * Favor access speed: use a HashMap and copy it on modification.
+             */
+            if (input instanceof HashMap) {
+                return input;
+            }
+
+            LOG.trace("Copying input {} to a HashMap ({} entries)", input, size);
+            final Map<K, V> ret = new HashMap<>(input);
+            LOG.trace("Read-only HashMap is {}", ret);
+            return ret;
+        }
+
+        /*
+         * Favor isolation speed: use a TrieMap and perform snapshots
+         *
+         * This one is a bit tricky, as the TrieMap is concurrent and does not
+         * keep an uptodate size. Updating it requires a full walk -- which is
+         * O(N) and we want to avoid that. So we wrap it in an interceptor,
+         * which will maintain the size for us.
+         */
+        LOG.trace("Copying input {} to a TrieMap ({} entries)", input, size);
+        final TrieMap<K, V> map = TrieMap.empty();
+        map.putAll(input);
+        final Map<K, V> ret = new ReadOnlyTrieMap<>(map, size);
+        LOG.trace("Read-only TrieMap is {}", ret);
+        return ret;
+    }
+}