/* * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License, * Version 1.0, and under the Eclipse Public License, Version 1.0 * (http://h2database.com/html/license.html). * Initial Developer: H2 Group */ package org.h2.dev.store.btree; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * A stored map. * * @param <K> the key class * @param <V> the value class */ public class MVMap<K, V> extends AbstractMap<K, V> { /** * The store. */ protected final MVStore store; /** * The current root page (may not be null). */ protected volatile Page root; private final int id; private final String name; private final DataType keyType; private final DataType valueType; private final long createVersion; private ArrayList<Page> oldRoots = new ArrayList<Page>(); private boolean closed; private boolean readOnly; protected MVMap(MVStore store, int id, String name, DataType keyType, DataType valueType, long createVersion) { this.store = store; this.id = id; this.name = name; this.keyType = keyType; this.valueType = valueType; this.createVersion = createVersion; this.root = Page.createEmpty(this, createVersion); } /** * Add or replace a key-value pair. * * @param key the key (may not be null) * @param value the value (may not be null) * @return the old value if the key existed, or null otherwise */ @SuppressWarnings("unchecked") public V put(K key, V value) { checkWrite(); long writeVersion = store.getCurrentVersion(); Page p = root.copyOnWrite(writeVersion); if (p.getKeyCount() > store.getMaxPageSize()) { int at = p.getKeyCount() / 2; long totalCount = p.getTotalCount(); Object k = p.getKey(at); Page split = p.split(at); Object[] keys = { k }; long[] children = { p.getPos(), split.getPos() }; Page[] childrenPages = { p, split }; long[] counts = { p.getTotalCount(), split.getTotalCount() }; p = Page.create(this, writeVersion, 1, keys, null, children, childrenPages, counts, totalCount, 0); // now p is a node; insert continues } Object result = put(p, writeVersion, key, value); setRoot(p); return (V) result; } /** * Add or update a key-value pair. * * @param p the page * @param writeVersion the write version * @param key the key (may not be null) * @param value the value (may not be null) * @return the old value, or null */ protected Object put(Page p, long writeVersion, Object key, Object value) { if (p.isLeaf()) { int index = p.binarySearch(key); if (index < 0) { index = -index - 1; p.insertLeaf(index, key, value); return null; } return p.setValue(index, value); } // p is a node int index = p.binarySearch(key); if (index < 0) { index = -index - 1; } else { index++; } Page c = p.getChildPage(index).copyOnWrite(writeVersion); if (c.getKeyCount() >= store.getMaxPageSize()) { // split on the way down int at = c.getKeyCount() / 2; Object k = c.getKey(at); Page split = c.split(at); p.setChild(index, split); p.insertNode(index, k, c); // now we are not sure where to add return put(p, writeVersion, key, value); } Object result = put(c, writeVersion, key, value); p.setChild(index, c); return result; } /** * Get a value. * * @param key the key * @return the value, or null if not found */ @SuppressWarnings("unchecked") public V get(Object key) { checkOpen(); return (V) binarySearch(root, key); } /** * Go to the first element for the given key. * * @param p the current page * @param cursor the cursor * @param key the key * @return the cursor position */ protected CursorPos min(Page p, Cursor<K, V> cursor, Object key) { while (true) { if (p.isLeaf()) { int x = key == null ? 0 : p.binarySearch(key); if (x < 0) { x = -x - 1; } CursorPos c = new CursorPos(); c.page = p; c.index = x; return c; } int x = key == null ? -1 : p.binarySearch(key); if (x < 0) { x = -x - 1; } else { x++; } CursorPos c = new CursorPos(); c.page = p; c.index = x; cursor.push(c); p = p.getChildPage(x); } } /** * Get the next key. * * @param p the cursor position * @param cursor the cursor * @return the next key */ protected Object nextKey(CursorPos p, Cursor<K, V> cursor) { while (p != null) { int index = p.index++; Page x = p.page; if (index < x.getKeyCount()) { return x.getKey(index); } while (true) { p = cursor.pop(); if (p == null) { break; } index = ++p.index; x = p.page; if (index <= x.getKeyCount()) { cursor.push(p); p = cursor.visitChild(x, index); if (p != null) { break; } } } } return null; } /** * Get the value for the given key, or null if not found. * * @param p the page * @param key the key * @return the value or null */ protected Object binarySearch(Page p, Object key) { int x = p.binarySearch(key); if (!p.isLeaf()) { if (x < 0) { x = -x - 1; } else { x++; } p = p.getChildPage(x); return binarySearch(p, key); } if (x >= 0) { return p.getValue(x); } return null; } public boolean containsKey(Object key) { return get(key) != null; } /** * Get the page for the given value. * * @param key the key * @return the value, or null if not found */ protected Page getPage(K key) { return binarySearchPage(root, key); } /** * Get the value for the given key, or null if not found. * * @param p the parent page * @param key the key * @return the page or null */ protected Page binarySearchPage(Page p, Object key) { int x = p.binarySearch(key); if (!p.isLeaf()) { if (x < 0) { x = -x - 1; } else { x++; } p = p.getChildPage(x); return binarySearchPage(p, key); } if (x >= 0) { return p; } return null; } /** * Remove all entries. */ public void clear() { checkWrite(); root.removeAllRecursive(); setRoot(Page.createEmpty(this, store.getCurrentVersion())); } /** * Remove all entries, and close the map. */ public void removeMap() { checkWrite(); root.removeAllRecursive(); store.removeMap(name); close(); } /** * Close the map, making it read only and release the memory. */ public void close() { closed = true; readOnly = true; removeAllOldVersions(); root = null; } public boolean isClosed() { return closed; } /** * Remove a key-value pair, if the key exists. * * @param key the key (may not be null) * @return the old value if the key existed, or null otherwise */ public V remove(Object key) { checkWrite(); long writeVersion = store.getCurrentVersion(); Page p = root.copyOnWrite(writeVersion); @SuppressWarnings("unchecked") V result = (V) remove(p, writeVersion, key); setRoot(p); return result; } /** * Remove a key-value pair. * * @param p the page (may not be null) * @param writeVersion the write version * @param key the key * @return the old value, or null if the key did not exist */ protected Object remove(Page p, long writeVersion, Object key) { int index = p.binarySearch(key); Object result = null; if (p.isLeaf()) { if (index >= 0) { result = p.getValue(index); p.remove(index); if (p.getKeyCount() == 0) { removePage(p); } } return result; } // node if (index < 0) { index = -index - 1; } else { index++; } Page cOld = p.getChildPage(index); Page c = cOld.copyOnWrite(writeVersion); long oldCount = c.getTotalCount(); result = remove(c, writeVersion, key); if (oldCount == c.getTotalCount()) { return null; } // TODO merge if the c key count is below the threshold if (c.getTotalCount() == 0) { // this child was deleted if (p.getKeyCount() == 0) { p.setChild(index, c); removePage(p); } else { p.remove(index); } } else { p.setChild(index, c); } return result; } protected void setRoot(Page newRoot) { if (root != newRoot) { removeUnusedOldVersions(); if (root.getVersion() != newRoot.getVersion()) { ArrayList<Page> list = oldRoots; if (list.size() > 0) { Page last = list.get(list.size() - 1); if (last.getVersion() != root.getVersion()) { list.add(root); } } else { list.add(root); } store.markChanged(this); } root = newRoot; } } /** * Check whether this map has any unsaved changes. * * @return true if there are unsaved changes. */ public boolean hasUnsavedChanges() { return !oldRoots.isEmpty(); } /** * Compare two keys. * * @param a the first key * @param b the second key * @return -1 if the first key is smaller, 1 if bigger, 0 if equal */ int compare(Object a, Object b) { return keyType.compare(a, b); } /** * Get the key type. * * @return the key type */ DataType getKeyType() { return keyType; } /** * Get the value type. * * @return the value type */ DataType getValueType() { return valueType; } /** * Read a page. * * @param pos the position of the page * @return the page */ Page readPage(long pos) { return store.readPage(this, pos); } /** * Set the position of the root page. * * @param rootPos the position, 0 for empty */ void setRootPos(long rootPos) { root = rootPos == 0 ? Page.createEmpty(this, 0) : readPage(rootPos); } /** * Iterate over all keys. * * @param from the first key to return * @return the iterator */ public Iterator<K> keyIterator(K from) { checkOpen(); return new Cursor<K, V>(this, root, from); } /** * Iterate over all keys in changed pages. * This does not include deleted deleted pages. * * @param minVersion the minimum version * @return the iterator */ public Iterator<K> changeIterator(long minVersion) { checkOpen(); return new ChangeCursor<K, V>(this, root, null, minVersion); } public Set<Map.Entry<K, V>> entrySet() { HashMap<K, V> map = new HashMap<K, V>(); for (K k : keySet()) { map.put(k, get(k)); } return map.entrySet(); } public Set<K> keySet() { checkOpen(); final Page root = this.root; return new AbstractSet<K>() { @Override public Iterator<K> iterator() { return new Cursor<K, V>(MVMap.this, root, null); } @Override public int size() { return MVMap.this.size(); } @Override public boolean contains(Object o) { return MVMap.this.containsKey(o); } }; } /** * Get the root page. * * @return the root page */ public Page getRoot() { return root; } /** * Get the map name. * * @return the name */ String getName() { return name; } MVStore getStore() { return store; } int getId() { return id; } /** * Rollback to the given version. * * @param version the version */ void rollbackTo(long version) { checkWrite(); removeUnusedOldVersions(); if (version < createVersion) { removeMap(); } else if (root.getVersion() != version) { // iterating in descending order - // this is not terribly efficient if there are many versions ArrayList<Page> list = oldRoots; while (list.size() > 0) { int i = list.size() - 1; Page p = list.get(i); root = p; list.remove(i); if (p.getVersion() <= version) { break; } } } } /** * Forget all old versions. */ void removeAllOldVersions() { // create a new instance // because another thread might iterate over it oldRoots = new ArrayList<Page>(); } /** * Forget those old versions that are no longer needed. */ void removeUnusedOldVersions() { long oldest = store.getRetainVersion(); if (oldest == -1) { return; } int i = searchRoot(oldest); if (i < 0) { return; } // create a new instance // because another thread might iterate over it ArrayList<Page> list = new ArrayList<Page>(); list.addAll(oldRoots.subList(i, oldRoots.size())); oldRoots = list; } public void setReadOnly(boolean readOnly) { this.readOnly = readOnly; } public boolean isReadOnly() { return readOnly; } /** * Check whether the map is open. * * @throws IllegalStateException if the map is closed */ protected void checkOpen() { if (closed) { throw new IllegalStateException("This map is closed"); } } /** * Check whether writing is allowed. * * @throws IllegalStateException if the map is read-only */ protected void checkWrite() { if (readOnly) { checkOpen(); throw new IllegalStateException("This map is read-only"); } } public String toString() { StringBuilder buff = new StringBuilder(); buff.append("map:").append(name); if (readOnly) { buff.append(" readOnly"); } if (closed) { buff.append(" closed"); } return buff.toString(); } public int hashCode() { return id; } public boolean equals(Object o) { return this == o; } public int size() { long size = getSize(); return size > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) size; } public long getSize() { return root.getTotalCount(); } long getCreateVersion() { return createVersion; } /** * Remove the given page (make the space available). * * @param p the page */ protected void removePage(Page p) { store.removePage(p.getPos()); } /** * Open an old version for the given map. * * @param version the version * @return the map */ public MVMap<K, V> openVersion(long version) { if (readOnly) { throw new IllegalArgumentException("This map is read-only - need to call the method on the writable map"); } if (version < createVersion) { throw new IllegalArgumentException("Unknown version"); } Page newest = null; // need to copy because it can change Page r = root; if (r.getVersion() == version) { newest = r; } else { // find the newest page that has a getVersion() <= version int i = searchRoot(version); if (i < 0) { // not found if (i == -1) { // smaller than all in-memory versions return store.openMapVersion(version, name); } i = -i - 2; } newest = oldRoots.get(i); } MVMap<K, V> m = new MVMap<K, V>(store, id, name, keyType, valueType, createVersion); m.readOnly = true; m.root = newest; return m; } private int searchRoot(long version) { int low = 0, high = oldRoots.size() - 1; while (low <= high) { int x = (low + high) >>> 1; long v = oldRoots.get(x).getVersion(); if (v < version) { low = x + 1; } else if (version < v) { high = x - 1; } else { return x; } } return -(low + 1); } public long getVersion() { return root.getVersion(); } }