/* * This file is part of ELKI: * Environment for Developing KDD-Applications Supported by Index-Structures * * Copyright (C) 2017 * ELKI Development Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.lmu.ifi.dbs.elki.index.tree; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.*; import de.lmu.ifi.dbs.elki.logging.Logging; import de.lmu.ifi.dbs.elki.logging.LoggingConfiguration; import de.lmu.ifi.dbs.elki.persistent.AbstractExternalizablePage; import de.lmu.ifi.dbs.elki.utilities.datastructures.BitsUtil; import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException; /** * Abstract superclass for nodes in an tree based index structure. * * @author Elke Achtert * @since 0.2 * @param <E> the type of Entry used in the index */ public abstract class AbstractNode<E extends Entry> extends AbstractExternalizablePage implements Node<E> { /** * The number of entries in this node. */ protected int numEntries; /** * The entries (children) of this node. */ protected Entry[] entries; /** * Indicates whether this node is a leaf node. */ protected boolean isLeaf; /** * Empty constructor for Externalizable interface. */ public AbstractNode() { super(); } /** * Creates a new Node with the specified parameters. * * @param capacity the capacity (maximum number of entries plus 1 for * overflow) of this node * @param isLeaf indicates whether this node is a leaf node * @param eclass Entry class, to initialize array storage */ public AbstractNode(int capacity, boolean isLeaf, Class<? super E> eclass) { super(); this.numEntries = 0; this.entries = new Entry[capacity]; this.isLeaf = isLeaf; } @Override public final Iterator<IndexTreePath<E>> children(final IndexTreePath<E> parentPath) { return new Iterator<IndexTreePath<E>>() { int count = 0; @Override public boolean hasNext() { return count < numEntries; } @Override public IndexTreePath<E> next() { synchronized(AbstractNode.this) { if(count < numEntries) { return new IndexTreePath<>(parentPath, getEntry(count), count++); } } throw new NoSuchElementException(); } }; } @Override public final int getNumEntries() { return numEntries; } @Override public final boolean isLeaf() { return isLeaf; } @SuppressWarnings("unchecked") @Override public final E getEntry(int index) { return (E) entries[index]; } /** * Calls the super method and writes the id of this node, the numEntries and * the entries array to the specified stream. */ @Override public void writeExternal(ObjectOutput out) throws IOException { super.writeExternal(out); out.writeBoolean(isLeaf); out.writeInt(numEntries); // Entries will be written in subclasses } /** * Reads the id of this node, the numEntries and the entries array from the * specified stream. * * @param in the stream to read data from in order to restore the object * @throws java.io.IOException if I/O errors occur * @throws ClassNotFoundException If the class for an object being restored * cannot be found. */ @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { super.readExternal(in); isLeaf = in.readBoolean(); numEntries = in.readInt(); // Entries will be read in subclasses } /** * Returns a string representation of this node. * * @return the type of this node (LeafNode or DirNode) followed by its id */ @Override public String toString() { return (isLeaf ? "LeafNode " : "DirNode ") + getPageID(); } /** * Adds a new leaf entry to this node's children and returns the index of the * entry in this node's children array. An UnsupportedOperationException will * be thrown if the entry is not a leaf entry or this node is not a leaf node. * * @param entry the leaf entry to be added * @return the index of the entry in this node's children array * @throws UnsupportedOperationException if entry is not a leaf entry or this * node is not a leaf node */ @Override public final int addLeafEntry(E entry) { // entry is not a leaf entry if(!(entry instanceof LeafEntry)) { throw new UnsupportedOperationException("Entry is not a leaf entry!"); } // this is a not a leaf node if(!isLeaf()) { throw new UnsupportedOperationException("Node is not a leaf node!"); } // leaf node return addEntry(entry); } /** * Adds a new directory entry to this node's children and returns the index of * the entry in this node's children array. An UnsupportedOperationException * will be thrown if the entry is not a directory entry or this node is not a * directory node. * * @param entry the directory entry to be added * @return the index of the entry in this node's children array * @throws UnsupportedOperationException if entry is not a directory entry or * this node is not a directory node */ @Override public final int addDirectoryEntry(E entry) { // entry is not a directory entry if(entry instanceof LeafEntry) { throw new UnsupportedOperationException("Entry is not a directory entry!"); } // this is a not a directory node if(isLeaf()) { throw new UnsupportedOperationException("Node is not a directory node!"); } return addEntry(entry); } /** * Deletes the entry at the specified index and shifts all entries after the * index to left. * * @param index the index at which the entry is to be deleted * @return true id deletion was successful */ public boolean deleteEntry(int index) { System.arraycopy(entries, index + 1, entries, index, numEntries - index - 1); entries[--numEntries] = null; return true; } /** * Deletes all entries in this node. */ public final void deleteAllEntries() { if(numEntries > 0) { Arrays.fill(entries, null); this.numEntries = 0; } } /** * Returns the capacity of this node (i.e. the length of the entries arrays). * * @return the capacity of this node */ public final int getCapacity() { return entries.length; } /** * Returns a list of the entries. * * @return a list of the entries * * @deprecated Using this method means an extra copy - usually at the cost of * performance. */ @SuppressWarnings("unchecked") @Deprecated public final List<E> getEntries() { List<E> result = new ArrayList<>(numEntries); for(Entry entry : entries) { if(entry != null) { result.add((E) entry); } } return result; } /** * Adds the specified entry to the entries array and increases the numEntries * counter. * * @param entry the entry to be added * @return the current number of entries */ private int addEntry(E entry) { entries[numEntries++] = entry; return numEntries - 1; } /** * Remove entries according to the given mask. * * @param mask Mask to remove */ public void removeMask(long[] mask) { int dest = BitsUtil.nextSetBit(mask, 0); if(dest < 0) { return; } int src = BitsUtil.nextSetBit(mask, dest); while(src < numEntries) { if(!BitsUtil.get(mask, src)) { entries[dest] = entries[src]; dest++; } src++; } int rm = src - dest; while(dest < numEntries) { entries[dest] = null; dest++; } numEntries -= rm; } /** * Redistribute entries according to the given sorting. * * @param newNode Node to split to * @param sorting Sorting to use * @param splitPoint Split point */ public final void splitTo(AbstractNode<E> newNode, List<E> sorting, int splitPoint) { assert (isLeaf() == newNode.isLeaf()); deleteAllEntries(); StringBuilder msg = LoggingConfiguration.DEBUG ? new StringBuilder("\n") : null; for(int i = 0; i < splitPoint; i++) { addEntry(sorting.get(i)); if(msg != null) { msg.append("n_").append(getPageID()).append(" "); msg.append(sorting.get(i)).append("\n"); } } for(int i = splitPoint; i < sorting.size(); i++) { newNode.addEntry(sorting.get(i)); if(msg != null) { msg.append("n_").append(newNode.getPageID()).append(" "); msg.append(sorting.get(i)).append("\n"); } } if(msg != null) { Logging.getLogger(this.getClass().getName()).fine(msg.toString()); } } /** * Splits the entries of this node into a new node using the given assignments * * @param newNode Node to split to * @param assignmentsToFirst the assignment to this node * @param assignmentsToSecond the assignment to the new node */ public final void splitTo(AbstractNode<E> newNode, List<E> assignmentsToFirst, List<E> assignmentsToSecond) { assert (isLeaf() == newNode.isLeaf()); deleteAllEntries(); StringBuilder msg = LoggingConfiguration.DEBUG ? new StringBuilder() : null; // assignments to this node for(E entry : assignmentsToFirst) { if(msg != null) { msg.append("n_").append(getPageID()).append(" ").append(entry).append("\n"); } addEntry(entry); } // assignments to the new node for(E entry : assignmentsToSecond) { if(msg != null) { msg.append("n_").append(newNode.getPageID()).append(" ").append(entry).append("\n"); } newNode.addEntry(entry); } if(msg != null) { Logging.getLogger(this.getClass()).fine(msg.toString()); } } /** * Splits the entries of this node into a new node using the given assignments * * @param newNode Node to split to * @param assignment Assignment mask */ public final void splitByMask(AbstractNode<E> newNode, long[] assignment) { assert (isLeaf() == newNode.isLeaf()); int dest = BitsUtil.nextSetBit(assignment, 0); if(dest < 0) { throw new AbortException("No bits set in splitting mask."); } // FIXME: use faster iteration/testing int pos = dest; while(pos < numEntries) { if(BitsUtil.get(assignment, pos)) { // Move to new node newNode.addEntry(getEntry(pos)); } else { // Move to new position entries[dest] = entries[pos]; dest++; } pos++; } final int rm = numEntries - dest; while(dest < numEntries) { entries[dest] = null; dest++; } numEntries -= rm; } }