/* * Copyright 2008 Fedora Commons, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.mulgara.store.stringpool.xa11; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.net.URI; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.util.LinkedList; import java.util.List; import java.util.WeakHashMap; import org.apache.log4j.Logger; import org.mulgara.query.Constraint; import org.mulgara.query.Cursor; import org.mulgara.query.TuplesException; import org.mulgara.query.Variable; import org.mulgara.store.StoreException; import org.mulgara.store.nodepool.NewNodeListener; import org.mulgara.store.nodepool.NodePool; import org.mulgara.store.nodepool.NodePoolException; import org.mulgara.store.statement.StatementStore; import org.mulgara.store.stringpool.SPComparator; import org.mulgara.store.stringpool.SPObject; import org.mulgara.store.stringpool.SPObjectFactory; import org.mulgara.store.stringpool.SPTypedLiteral; import org.mulgara.store.stringpool.StringPoolException; import org.mulgara.store.stringpool.SPObject.TypeCategory; import org.mulgara.store.stringpool.xa.SPObjectFactoryImpl; import org.mulgara.store.tuples.Annotation; import org.mulgara.store.tuples.RowComparator; import org.mulgara.store.tuples.SimpleTuplesFormat; import org.mulgara.store.tuples.Tuples; import org.mulgara.store.tuples.TuplesOperations; import org.mulgara.store.xa.AVLComparator; import org.mulgara.store.xa.AVLFile; import org.mulgara.store.xa.AVLNode; import org.mulgara.store.xa.AbstractBlockFile; import org.mulgara.store.xa.Block; import org.mulgara.store.xa.BlockFile; import org.mulgara.store.xa.LockFile; import org.mulgara.store.xa.SimpleXAResourceException; import org.mulgara.store.xa.XANodePool; import org.mulgara.store.xa.XAStringPool; import org.mulgara.store.xa.XAUtils; import org.mulgara.util.Constants; import org.mulgara.util.LongMapper; import org.mulgara.util.functional.Pair; import org.mulgara.util.io.LBufferedFile; import org.mulgara.util.io.MappingUtil; import static org.mulgara.store.stringpool.xa11.DataStruct.*; /** * This is a WORM transactional string pool. The only write operations that are permitted * are insertions. Deletions are ignored. The only exception to this is that rollback * on a transaction results in a set of writes being abandoned. * * @created Aug 11, 2008 * @author Paula Gearon * @copyright © 2008 <a href="http://www.fedora-commons.org/">Fedora Commons</a> */ public class XA11StringPoolImpl implements XAStringPool, XANodePool { /** Logger. */ private static final Logger logger = Logger.getLogger(XA11StringPoolImpl.class); /** The number of metaroots in the metaroot file. */ private static final int NR_METAROOTS = 2; /** The variables to use when pretending that the string pool is a Tuples. */ static final Variable[] VARIABLES = new Variable[] { StatementStore.VARIABLES[0] }; /** A factory for this class. */ static final SPObjectFactory SPO_FACTORY = SPObjectFactoryImpl.getInstance(); /** The main data structures are rooted on this filename. */ private String mainFilename; /** The flat data structures are rooted on this filename. */ private String flatDataFilename; /** The index file for mapping data to a gNode. */ private AVLFile dataToGNode; /** The file reader for mapping gNodes to data. */ private LBufferedFile gNodeToDataFile; /** The object for creating the output appender. */ private FileOutputStream gNodeToDataOutputStream; /** A writing object for the flat file for mapping gNodes to data. */ private FileChannel gNodeToDataAppender; /** Indicates that the current phase has been written to. */ private boolean dirty = true; /** The next-gNode value. This corresponds to the end of the flat file. */ private long nextGNodeValue; /** The object for handling blank node allocation. */ private BlankNodeAllocator blankNodeAllocator = new BlankNodeAllocator(); /** The latest phase in the index tree. */ private TreePhase currentPhase = null; /** The next-gNode for the committed phase. */ private long committedNextGNode; /** The LockFile that protects the string pool from being opened twice. */ private LockFile lockFile; /** The BlockFile for the node pool metaroot file. */ private BlockFile metarootFile = null; /** The metaroot info in the metaroot file. */ private Metaroot[] metaroots = new Metaroot[NR_METAROOTS]; /** A Token on the last committed phase. */ private TreePhase.Token committedPhaseToken = null; /** Object used for locking on synchronized access to the committed phase. */ private Object committedPhaseLock = new Object(); /** Phase reference for when the phase is being written. */ private TreePhase.Token recordingPhaseToken = null; /** Indicates that the phase is written but not yet acknowledged as valid. */ private boolean prepared = false; /** The valid phase index on file to use. Must always be 0 or 1. */ private int phaseIndex = 0; /** The number of the current phase. These increase monotonically. */ private int phaseNumber = 0; /** A list of listeners to inform whenever a new node is created. */ private List<NewNodeListener> newNodeListeners = new LinkedList<NewNodeListener>(); /** A flag used to delay throwing an exception on the file version until it is needed. */ private boolean wrongFileVersion = false; /** Cache the mapping of node IDs to objects */ private WeakHashMap<Long,SPObject> nodeCache = new WeakHashMap<Long,SPObject>(); /** * Create a string pool instance using a set of directories. * @param basenames A list of paths for creating string pool files in. * Each path is expected to be on a separate file system. * @throws IOException The files cannot be created or read. */ public XA11StringPoolImpl(String[] basenames) throws IOException { distributeFilenames(basenames); lockFile = LockFile.createLockFile(mainFilename + ".sp.lock"); wrongFileVersion = false; try { try { wrongFileVersion = !Metaroot.check(mainFilename + ".sp"); } catch (FileNotFoundException ex) { // no-op } dataToGNode = new AVLFile(mainFilename + ".sp_avl", PAYLOAD_SIZE); gNodeToDataOutputStream = new FileOutputStream(flatDataFilename, true); gNodeToDataAppender = gNodeToDataOutputStream.getChannel(); gNodeToDataFile = LBufferedFile.createReadOnly(flatDataFilename); } catch (IOException ex) { try { close(); } catch (StoreException ex2) { // no-op } throw ex; } // clear the cache whenever the whole file is mapped gNodeToDataFile.registerRemapListener(new Runnable() { public void run() { nodeCache.clear(); } }); } /** * Returns the most recent successful committed phase. * @see org.mulgara.store.xa.SimpleXARecoveryHandler#recover() */ public int[] recover() throws SimpleXAResourceException { if (currentPhase != null) return new int[0]; if (wrongFileVersion) throw new SimpleXAResourceException("Wrong metaroot file version."); try { openMetarootFile(false); } catch (IOException ex) { throw new SimpleXAResourceException("I/O error", ex); } // Count the number of valid phases. int phaseCount = 0; if (metaroots[0].getValid() != 0) ++phaseCount; if (metaroots[1].getValid() != 0) ++phaseCount; // Read the phase numbers. int[] phaseNumbers = new int[phaseCount]; int index = 0; if (metaroots[0].getValid() != 0) phaseNumbers[index++] = metaroots[0].getPhaseNr(); if (metaroots[1].getValid() != 0) phaseNumbers[index++] = metaroots[1].getPhaseNr(); return phaseNumbers; } /** * @see org.mulgara.store.stringpool.StringPool#put(long, org.mulgara.store.stringpool.SPObject) */ public void put(long node, SPObject spObject) throws StringPoolException { throw new UnsupportedOperationException("Cannot manually allocate a gNode for this string pool."); } /** * Stores an spObject and allocates a gNode to go with it. * @param spObject The object to store. * @return The new gNode associated with this object. * @throws StringPoolException If the string pool could not allocate space. */ public synchronized long put(SPObject spObject) throws StringPoolException { try { long gNode = nextGNodeValue; DataStruct spObjectData = new DataStruct(spObject, nextGNodeValue); // this is the secret sauce - gNodes allocation moves up by size of the data nextGNodeValue += spObjectData.writeTo(gNodeToDataAppender); mapObjectToGNode(spObjectData, spObject.getSPComparator()); informNodeListeners(gNode); nodeCache.put(gNode, spObject); return gNode; } catch (IOException e) { throw new StringPoolException("Unable to write to data files.", e); } } /** * Sets the node pool for this string pool. Not used for this implementation. * @param nodePool The node pool being set. Ignored. */ public void setNodePool(XANodePool nodePool) { if (nodePool != this) throw new IllegalArgumentException("XA 1.1 data pool requires an integrated node pool."); if (logger.isDebugEnabled()) logger.debug("Setting a node pool for the XA 1.1 string pool. Ignored."); } /** * @see org.mulgara.store.stringpool.StringPool#findGNode(org.mulgara.store.stringpool.SPObject) */ public long findGNode(SPObject spObject) throws StringPoolException { checkInitialized(); return currentPhase.findGNode(spObject, false); } /** * @deprecated The <var>nodePool</var> parameter must equal this. Use {@link #findGNode(SPObject, boolean)} with a true <var>create</var> parameter instead. * @see org.mulgara.store.stringpool.StringPool#findGNode(org.mulgara.store.stringpool.SPObject, org.mulgara.store.nodepool.NodePool) */ public long findGNode(SPObject spObject, NodePool nodePool) throws StringPoolException { if (nodePool != this) throw new IllegalStateException("The XA11 data store must manage its own nodes"); checkInitialized(); return currentPhase.findGNode(spObject, true); } /** * @see org.mulgara.store.stringpool.StringPool#findGNode(org.mulgara.store.stringpool.SPObject, org.mulgara.store.nodepool.NodePool) */ public long findGNode(SPObject spObject, boolean create) throws StringPoolException { checkInitialized(); return currentPhase.findGNode(spObject, create); } /** * @see org.mulgara.store.stringpool.StringPool#findGNodes(org.mulgara.store.stringpool.SPObject, boolean, org.mulgara.store.stringpool.SPObject, boolean) */ public Tuples findGNodes(SPObject lowValue, boolean inclLowValue, SPObject highValue, boolean inclHighValue) throws StringPoolException { checkInitialized(); dirty = false; return currentPhase.findGNodes(lowValue, inclLowValue, highValue, inclHighValue); } /** * @see org.mulgara.store.stringpool.StringPool#findGNodes(org.mulgara.store.stringpool.SPObject.TypeCategory, java.net.URI) */ public Tuples findGNodes(TypeCategory typeCategory, URI typeURI) throws StringPoolException { checkInitialized(); dirty = false; return currentPhase.findGNodes(typeCategory, typeURI); } /** * @see org.mulgara.store.stringpool.StringPool#findSPObject(long) */ public SPObject findSPObject(long node) throws StringPoolException { // blank nodes don't get loaded up as an SPObject if (BlankNodeAllocator.isBlank(node)) return null; // outside of the allocated range if (node >= nextGNodeValue) return null; // Look aside into the cache first SPObject cached = nodeCache.get(node); if (cached != null) return cached; try { return new DataStruct(gNodeToDataFile, node).getSPObject(); } catch (IllegalArgumentException iae) { throw new StringPoolException("Bad node data. gNode = " + node, iae); } catch (IOException ioe) { throw new StringPoolException("Unable to load data from data pool.", ioe); } } /** * @see org.mulgara.store.stringpool.StringPool#getSPObjectFactory() */ public SPObjectFactory getSPObjectFactory() { return SPO_FACTORY; } /** * @see org.mulgara.store.stringpool.StringPool#remove(long) * Nodes are never removed. * @return Always true, so that anyone thinking it should have been removed will * get the answer they were expecting. */ public boolean remove(long node) throws StringPoolException { return true; } /** * @see org.mulgara.store.xa.XAStringPool#close() */ public void close() throws StoreException { try { close(false); } catch (IOException ex) { throw new StringPoolException("I/O error closing string pool.", ex); } } /** * @see org.mulgara.store.xa.XAStringPool#delete() */ public void delete() throws StoreException { try { close(true); } catch (IOException ex) { throw new StringPoolException("I/O error deleting string pool.", ex); } finally { gNodeToDataFile = null; gNodeToDataAppender = null; dataToGNode = null; metarootFile = null; } } /** * @see org.mulgara.store.xa.XAStringPool#newReadOnlyStringPool() */ public XAStringPool newReadOnlyStringPool() { return new ReadOnlyStringPool(); } /** * @see org.mulgara.store.xa.XAStringPool#newWritableStringPool() */ public XAStringPool newWritableStringPool() { return this; } /** * @see org.mulgara.store.xa.SimpleXAResource#commit() */ public void commit() throws SimpleXAResourceException { synchronized (this) { if (!prepared) throw new SimpleXAResourceException("commit() called without previous prepare()."); // Perform a commit. try { // New phase is now marked valid. Invalidate the metaroot of the old phase. Metaroot mr = metaroots[1 - phaseIndex]; mr.setValid(0); mr.write(); metarootFile.force(); // Release the token for the previously committed phase. synchronized (committedPhaseLock) { if (committedPhaseToken != null) committedPhaseToken.release(); committedPhaseToken = recordingPhaseToken; } recordingPhaseToken = null; blankNodeAllocator.commit(); } catch (IOException ex) { logger.fatal("I/O error while performing commit.", ex); throw new SimpleXAResourceException("I/O error while performing commit.", ex); } finally { prepared = false; if (recordingPhaseToken != null) { // Something went wrong! recordingPhaseToken.release(); recordingPhaseToken = null; logger.error("Commit failed. Calling close()."); try { close(); } catch (Throwable t) { logger.error("Exception on forced close()", t); } } } } } /** * @see org.mulgara.store.xa.SimpleXAResource#getPhaseNumber() */ public synchronized int getPhaseNumber() throws SimpleXAResourceException { checkInitialized(); return phaseNumber; } /** * Writes all transactional data to disk, in preparation for a full commit. * @throws SimpleXAResourceException Occurs due to an IO error when writing data to disk. */ public void prepare() throws SimpleXAResourceException { // TODO: This synchronization is possibly redundant due to the global lock in StringPoolSession synchronized(this) { checkInitialized(); if (prepared) { // prepare already performed. throw new SimpleXAResourceException("prepare() called twice."); } try { // Perform a prepare. recordingPhaseToken = currentPhase.new Token(); TreePhase recordingPhase = currentPhase; currentPhase = new TreePhase(); // Ensure that all data associated with the phase is on disk. dataToGNode.force(); gNodeToDataAppender.force(true); // Write the metaroot. int newPhaseIndex = 1 - phaseIndex; int newPhaseNumber = phaseNumber + 1; Metaroot metaroot = metaroots[newPhaseIndex]; metaroot.setValid(0); metaroot.setPhaseNr(newPhaseNumber); metaroot.setFlatFileSize(DataStruct.toOffset(nextGNodeValue)); blankNodeAllocator.prepare(metaroot); if (logger.isDebugEnabled()) logger.debug("Writing data pool metaroot for phase: " + newPhaseNumber); metaroot.addPhase(recordingPhase); metaroot.write(); metarootFile.force(); metaroot.setValid(1); metaroot.write(); metarootFile.force(); phaseIndex = newPhaseIndex; phaseNumber = newPhaseNumber; committedNextGNode = nextGNodeValue; prepared = true; } catch (IOException ex) { logger.error("I/O error while performing prepare.", ex); throw new SimpleXAResourceException("I/O error while performing prepare.", ex); } finally { if (!prepared) { logger.error("Prepare failed."); if (recordingPhaseToken != null) { recordingPhaseToken.release(); recordingPhaseToken = null; } } } } } /** * Drops all data in the current transaction, recovering any used resources. * @throws SimpleXAResourceException Caused by any IO errors. */ public void rollback() throws SimpleXAResourceException { // TODO: This synchronization is probably redundant due to the global lock in StringPoolSession SimpleXAResourceException xaEx = null; // variable to hold the first thrown exception. synchronized (this) { checkInitialized(); try { if (prepared) { // Restore phaseIndex and phaseNumber to their previous values. phaseIndex = 1 - phaseIndex; --phaseNumber; recordingPhaseToken = null; prepared = false; // Invalidate the metaroot of the other phase. Metaroot mr = metaroots[1 - phaseIndex]; mr.setValid(0); mr.write(); metarootFile.force(); } } catch (IOException ex) { xaEx = new SimpleXAResourceException("I/O error while performing rollback (invalidating metaroot)", ex); } finally { try { try { blankNodeAllocator.rollback(); nextGNodeValue = committedNextGNode; long offset = DataStruct.toOffset(nextGNodeValue); gNodeToDataAppender.position(offset); // tell the read-only access to prepare for truncation gNodeToDataFile.truncate(offset); MappingUtil.truncate(gNodeToDataAppender, offset); } catch (IOException ioe) { String msg = "I/O error while performing rollback (new committed phase)"; if (xaEx == null) xaEx = new SimpleXAResourceException(msg, ioe); // this is the first exception. else logger.info(msg, ioe); // another exception already occurred, log the suppressed exception. } } finally { try { currentPhase = new TreePhase(committedPhaseToken.getPhase()); } catch (IOException ex) { String msg = "I/O error while performing rollback (new committed phase)"; if (xaEx == null) xaEx = new SimpleXAResourceException(msg, ex); // this is the first exception. else logger.info(msg, ex); // another exception already occurred, log the suppressed exception. } finally { // This is the last thing to execute; re-throw any previously caught exception now. if (xaEx != null) throw xaEx; } } } } } public void refresh() throws SimpleXAResourceException { /* no-op */ } public void release() throws SimpleXAResourceException { /* no-op */ } /** * @see org.mulgara.store.xa.SimpleXARecoveryHandler#clear() */ public synchronized void clear() throws IOException, SimpleXAResourceException { if (currentPhase == null) clear(0); } /** * @see org.mulgara.store.xa.SimpleXARecoveryHandler#clear(int) */ public void clear(int phaseNumber) throws IOException, SimpleXAResourceException { if (currentPhase != null) throw new IllegalStateException("StringPool already has a current phase."); openMetarootFile(true); synchronized (committedPhaseLock) { committedPhaseToken = new TreePhase().new Token(); } this.phaseNumber = phaseNumber; phaseIndex = 1; dataToGNode.clear(); blankNodeAllocator.clear(); // clear the flat file nextGNodeValue = NodePool.MIN_NODE; committedNextGNode = NodePool.MIN_NODE; // this forces a seek to 0 gNodeToDataFile.truncate(0); MappingUtil.truncate(gNodeToDataAppender, 0); currentPhase = new TreePhase(); } /** * This gets called after {@link #recover()}. * It selects the active phase to use, and sets all the internal data related to a phase. * @see org.mulgara.store.xa.SimpleXARecoveryHandler#selectPhase(int) */ public void selectPhase(int phaseNumber) throws IOException, SimpleXAResourceException { // check if this was already called if (currentPhase != null) { if (phaseNumber != this.phaseNumber) throw new SimpleXAResourceException("selectPhase() called on initialized StringPoolImpl."); return; } if (metarootFile == null) throw new SimpleXAResourceException("String pool metaroot file is not open."); // Locate the metaroot corresponding to the given phase number. if (metaroots[0].getValid() != 0 && metaroots[0].getPhaseNr() == phaseNumber) { phaseIndex = 0; // A new phase will be saved in the other metaroot. } else if (metaroots[1].getValid() != 0 && metaroots[1].getPhaseNr() == phaseNumber) { phaseIndex = 1; // A new phase will be saved in the other metaroot. } else { throw new SimpleXAResourceException("Phase number [" + phaseNumber + "] is not present in the metaroot file. Found [" + metaroots[0].getPhaseNr() + "], [" + metaroots[1].getPhaseNr() + "]"); } Metaroot metaroot = metaroots[phaseIndex]; // Load a duplicate of the selected phase. The duplicate will have a // phase number which is one higher than the original phase. try { synchronized (committedPhaseLock) { committedPhaseToken = new TreePhase(metaroot.block).new Token(); } this.phaseNumber = phaseNumber; } catch (IllegalStateException ex) { throw new SimpleXAResourceException("Cannot construct initial phase.", ex); } // load all the remaining state for this phase blankNodeAllocator.setCurrentState(metaroot.getNextBlankNode()); long fileSize = metaroot.getFlatFileSize(); committedNextGNode = DataStruct.toGNode(fileSize); nextGNodeValue = committedNextGNode; updateAppender(fileSize); currentPhase = new TreePhase(); // Invalidate the on-disk metaroot that the new phase will be saved to. Metaroot mr = metaroots[1 - phaseIndex]; mr.setValid(0); mr.write(); metarootFile.force(); } public void newNode(long node) throws Exception { /* no-op: This was already allocated by this object */ } public void releaseNode(long node) { /* no-op */ } /** * @see org.mulgara.store.xa.XANodePool#addNewNodeListener(org.mulgara.store.nodepool.NewNodeListener) */ public void addNewNodeListener(NewNodeListener l) { if (l != this) newNodeListeners.add(l); } /** * @see org.mulgara.store.xa.XANodePool#newReadOnlyNodePool() */ public XANodePool newReadOnlyNodePool() { return this; } /** * @see org.mulgara.store.xa.XANodePool#newWritableNodePool() */ public XANodePool newWritableNodePool() { return this; } /** * @see org.mulgara.store.xa.XANodePool#removeNewNodeListener(org.mulgara.store.nodepool.NewNodeListener) */ public void removeNewNodeListener(NewNodeListener l) { newNodeListeners.remove(l); } /** * Allocate a new blank node. This interface was defined for allocating all nodes * but standard node allocation is now handled internally within this data pool * rather than calling back into this method. * @see org.mulgara.store.nodepool.NodePool#newNode() */ public long newNode() throws NodePoolException { long node = blankNodeAllocator.allocate(); return informNodeListeners(node); } /** @see org.mulgara.store.xa.XANodePool#getNodeMapper() */ public LongMapper getNodeMapper() throws Exception { return new BlankNodeMapper("n2n"); } /** * Inform all listeners that a new node was just allocated. * @param newNode The newly allocated node. * @return The node that was passed to all the listeners. */ private long informNodeListeners(long newNode) { for (NewNodeListener l: newNodeListeners) { try { l.newNode(newNode); } catch (Exception e) { logger.error("Error informing object [" + l.getClass() + ":" + l + "] of a new node", e); } } return newNode; } /** * Inserts an object into an index, so it can be looked up to find a gNode. * @param spObjectData The data for the object used as a key to the index. * @param comparator The SPComparator used for compararing data of the provided type. */ private void mapObjectToGNode(DataStruct spObjectData, SPComparator comparator) throws StringPoolException, IOException { checkInitialized(); if (!dirty && currentPhase.isInUse()) { currentPhase = new TreePhase(); dirty = true; } if (logger.isDebugEnabled()) logger.debug("put(" + spObjectData.getGNode() + ", " + spObjectData + ")"); try { currentPhase.put(spObjectData, comparator); } catch (RuntimeException ex) { if (logger.isDebugEnabled()) logger.debug("RuntimeException in put()", ex); throw ex; } catch (Error e) { if (logger.isDebugEnabled()) logger.debug("Error in put()", e); throw e; } catch (StringPoolException ex) { if (logger.isDebugEnabled()) logger.debug("StringPoolException in put()", ex); throw ex; } } /** * Checks that the phase for the tree index has been set. * @throws IllegalStateException If the currentPhase is not initialized. */ private void checkInitialized() { if (currentPhase == null) { throw new IllegalStateException("No current phase. Object Pool has not been initialized or has been closed."); } } /** * Remove all mappings of files, so we can close them, and possibly delete them. */ public synchronized void unmap() { if (committedPhaseToken != null) { recordingPhaseToken = null; prepared = false; try { new TreePhase(committedPhaseToken.getPhase()); } catch (Throwable t) { logger.warn("Exception while rolling back in unmap()", t); } currentPhase = null; synchronized (committedPhaseLock) { committedPhaseToken.release(); committedPhaseToken = null; } } if (dataToGNode != null) dataToGNode.unmap(); if (metarootFile != null) { if (metaroots[0] != null) metaroots[0] = null; if (metaroots[1] != null) metaroots[1] = null; metarootFile.unmap(); } } /** * Closes all the files involved with a data pool * @param deleteFiles Remove files after closing them. * @throws IOException There was an error accessing the filesystem. */ private void close(boolean deleteFiles) throws IOException { try { unmap(); } finally { try { if (gNodeToDataFile != null) gNodeToDataFile.close(); } finally { try { if (gNodeToDataAppender != null) gNodeToDataAppender.close(); } finally { try { if (deleteFiles) new File(flatDataFilename).delete(); } finally { try { if (dataToGNode != null) { if (deleteFiles) dataToGNode.delete(); else dataToGNode.close(); } } finally { try { if (metarootFile != null) { if (deleteFiles) metarootFile.delete(); else metarootFile.close(); } } finally { if (lockFile != null) { lockFile.release(); lockFile = null; } } } } } } } } /** * Reads the data from the metaroot file, and initializes the files. * * @param clear If <code>true</code> then empties all files. * @throws IOException An error reading or writing files. * @throws SimpleXAResourceException An internal error, often caused by an IOException */ private void openMetarootFile(boolean clear) throws IOException, SimpleXAResourceException { if (metarootFile == null) { metarootFile = AbstractBlockFile.openBlockFile( mainFilename + ".sp", Metaroot.getSize() * Constants.SIZEOF_LONG, BlockFile.IOType.EXPLICIT ); // check that the file is the right size long nrBlocks = metarootFile.getNrBlocks(); if (nrBlocks != NR_METAROOTS) { if (nrBlocks > 0) { logger.info("String pool metaroot file for triple store \"" + mainFilename + "\" has invalid number of blocks: " + nrBlocks); // rewrite the file if (nrBlocks < NR_METAROOTS) { clear = true; metarootFile.clear(); } } else { // Empty file, so initialize it clear = true; } // expand or contract the file as necessary metarootFile.setNrBlocks(NR_METAROOTS); } metaroots[0] = new Metaroot(this, metarootFile.readBlock(0)); metaroots[1] = new Metaroot(this, metarootFile.readBlock(1)); } if (clear) { // Invalidate the metaroots on disk. metaroots[0].clear().write(); metaroots[1].clear().write(); metarootFile.force(); } } /** * Update the data appender. This moves to the end of the file if neccessary, and * truncates the file to this endpoint if it is too long (due to an abandoned transaction). * @param fileSize The end position of the file * @throws IOException Error moving in the file or changing its length. */ private void updateAppender(long fileSize) throws IOException { // truncate if the file is longer than the appending position gNodeToDataFile.truncate(fileSize); MappingUtil.truncate(gNodeToDataAppender, fileSize); gNodeToDataAppender.position(fileSize); } /** * Makes the best available use of the provided paths, to reduce seek contention * where possible. Directories in this list are built from the head, with each * extra directory in the list being used to optimize operations on certain files. * The presumption of multiple directories is that each one will occur on * different filesystems. * @param basenames An array of paths available for storing this string pool. */ private void distributeFilenames(String[] basenames) { if (basenames == null || basenames.length == 0) { throw new IllegalArgumentException("At least one directory must be provided for storing the string pool"); } mainFilename = basenames[0]; flatDataFilename = mainFilename; if (basenames.length > 1) { flatDataFilename = basenames[1]; } flatDataFilename += ".sp_nd"; } /** * This is a struct for holding metaroot information. */ final static class Metaroot { /** Unique int value in metaroot to mark this file as a string pool. */ static final int FILE_MAGIC = 0xa5f3f4f6; /** The current int version of this file format, stored in the metaroot. */ static final int FILE_VERSION = 1; /** Index of the file magic number (integer) within each of the two on-disk metaroots. */ static final int IDX_MAGIC = 0; /** Index of the file version number (integer) within each of the two on-disk metaroots. */ static final int IDX_VERSION = 1; /** Index of the valid flag (integer) within each of the two on-disk metaroots. */ static final int IDX_VALID = 2; /** The index of the phase number (integer) in the on-disk phase. */ static final int IDX_PHASE_NUMBER = 3; /** The integer index of long with the committed flat file size. */ static final int IDX_FLAT_FILE_SIZE = 4; /** The long index of long with the committed flat file size. */ static final int IDX_L_FLAT_FILE_SIZE = 2; /** The integer index of long with the committed next blank node. */ static final int IDX_NEXT_BLANK = 6; /** The long index of long with the committed next blank node. */ static final int IDX_L_NEXT_BLANK = 3; /** The size of the header of a metaroot in longs. */ static final int HEADER_SIZE_LONGS = IDX_L_NEXT_BLANK + 1; /** The size of the header of a metaroot in ints. */ static final int HEADER_SIZE_INTS = HEADER_SIZE_LONGS * 2; /** The size of a metaroot in longs. This is the metaroot header, plus the rest of the data given to the metaroot. */ static final int METAROOT_SIZE_LONGS = HEADER_SIZE_LONGS + TreePhase.RECORD_SIZE; /** The size of a metaroot in longs. */ static final int METAROOT_SIZE_INTS = METAROOT_SIZE_LONGS * 2; /** The VALID flag for a metaroot. */ int valid; /** The phase number for a metaroot. */ int phaseNr; /** The size of the flat file described by this metaroot. */ long flatFileSize; /** The metaroot description of the next blank node to be allocated. */ long nextBlankNode; /** The block this data structure sits on top of. */ final Block block; /** * Creates a new metaroot around a block. * @param block The block to build the structure around. */ public Metaroot(XA11StringPoolImpl currentPool, Block block) throws IOException { this.block = block; read(currentPool); } /** Gets the total size of this block, in LONG values. */ public static int getSize() { return METAROOT_SIZE_LONGS; } /** Gets the size of the header portion of this block, in LONG values. */ public static int getHeaderSize() { return HEADER_SIZE_LONGS; } /** * Clears out a block holding metaroot information. */ public Metaroot clear() { block.putInt(IDX_MAGIC, FILE_MAGIC); block.putInt(IDX_VERSION, FILE_VERSION); block.putInt(IDX_VALID, 0); block.putInt(IDX_PHASE_NUMBER, 0); block.putLong(IDX_L_FLAT_FILE_SIZE, 0); block.putLong(IDX_L_NEXT_BLANK, BlankNodeAllocator.FIRST); int[] empty = new int[METAROOT_SIZE_INTS - HEADER_SIZE_INTS]; block.put(HEADER_SIZE_INTS, empty); valid = 0; phaseNr = 0; flatFileSize = 0; return this; } /** * Writes this metaroot information to a block, sans phase information */ public Metaroot writeAllToBlock() { block.putInt(IDX_MAGIC, FILE_MAGIC); block.putInt(IDX_VERSION, FILE_VERSION); block.putInt(IDX_VALID, valid); block.putInt(IDX_PHASE_NUMBER, phaseNr); block.putLong(IDX_L_FLAT_FILE_SIZE, flatFileSize); block.putLong(IDX_L_NEXT_BLANK, nextBlankNode); // phase information is not written return this; } /** * Writes the metaroot block out. * @return The current metaroot. */ public Metaroot write() throws IOException { block.write(); return this; } /** * Reads metaroot information out of a block and into this structure. * @param currentPool Unused. */ public Metaroot read(XA11StringPoolImpl currentPool) throws IOException { valid = block.getInt(IDX_VALID); phaseNr = block.getInt(IDX_PHASE_NUMBER); flatFileSize = block.getLong(IDX_L_FLAT_FILE_SIZE); nextBlankNode = block.getLong(IDX_L_NEXT_BLANK); return this; } /** * Tests if this metaroot contains appropriate metaroot information. * @return <code>true</code> for a block with an appropriate header, <code>false</code> otherwise. */ public boolean check() { return FILE_MAGIC == block.getInt(IDX_MAGIC) && FILE_VERSION == block.getInt(IDX_VERSION); } /** * Tests if a block contains appropriate metaroot information. * @param block The block to test. * @return <code>true</code> for a block with an appropriate header, <code>false</code> otherwise. */ public static boolean check(Block block) { return FILE_MAGIC == block.getInt(IDX_MAGIC) && FILE_VERSION == block.getInt(IDX_VERSION); } /** * Tests if a raw file starts with appropriate metaroot information. * @param filename The name of the file to test. * @return <code>true</code> for a file with an appropriate header, <code>false</code> otherwise. */ public static boolean check(String filename) throws IOException { boolean failed = false; RandomAccessFile file = new RandomAccessFile(filename, "r"); try { if (file.length() < 2 * Constants.SIZEOF_INT) return false; int fileMagic = file.readInt(); int fileVersion = file.readInt(); if (AbstractBlockFile.byteOrder != ByteOrder.BIG_ENDIAN) { fileMagic = XAUtils.bswap(fileMagic); fileVersion = XAUtils.bswap(fileVersion); } if (FILE_MAGIC != fileMagic || FILE_VERSION != fileVersion) return false; } catch (IOException e) { failed = true; throw e; } finally { try { file.close(); } catch (IOException e) { if (!failed) throw e; else logger.info("I/O exception closing a failed file", e); } } return true; } public int getVersion() { return FILE_MAGIC; } public int getMagicNumber() { return FILE_VERSION; } public int getValid() { return valid; } public int getPhaseNr() { return phaseNr; } public long getFlatFileSize() { return flatFileSize; } public long getNextBlankNode() { return nextBlankNode; } public void setValid(int valid) { this.valid = valid; block.putInt(IDX_VALID, valid); } public void setPhaseNr(int phaseNr) { this.phaseNr = phaseNr; block.putInt(IDX_PHASE_NUMBER, phaseNr); } public void setFlatFileSize(long flatFileSize) { this.flatFileSize = flatFileSize; block.putLong(IDX_L_FLAT_FILE_SIZE, flatFileSize); } public void setNextBlankNode(long nextBlankNode) { this.nextBlankNode = nextBlankNode; block.putLong(IDX_L_NEXT_BLANK, nextBlankNode); } public void addPhase(TreePhase phase) { phase.avlFilePhase.writeToBlock(block, HEADER_SIZE_LONGS); } } /** * An internal read-only view of the current string pool. */ final class ReadOnlyStringPool implements XAStringPool { /** Releases resources held by the string pool. Not used. */ public void close() throws StringPoolException { throw new UnsupportedOperationException("Trying to close a read-only string pool."); } /** Deletes files used by the string pool. Not used. */ public void delete() throws StringPoolException { throw new UnsupportedOperationException("Trying to delete a read-only string pool."); } public XAStringPool newReadOnlyStringPool() { throw new UnsupportedOperationException("Read-only string pools are not used to manage other string pools."); } public XAStringPool newWritableStringPool() { throw new UnsupportedOperationException("Read-only string pools are not used to manage other string pools."); } public int[] recover() throws SimpleXAResourceException { throw new UnsupportedOperationException("Attempting to recover ReadOnlyStringPool"); } public void selectPhase(int phaseNumber) throws IOException, SimpleXAResourceException { throw new UnsupportedOperationException("Attempting to selectPhase of ReadOnlyStringPool"); } public void newNode(long node) throws Exception { throw new UnsupportedOperationException("Cannot write to a read-only string pool."); } public void releaseNode(long node) throws Exception { throw new UnsupportedOperationException("Cannot write to a read-only string pool."); } public void put(long node, SPObject spObject) throws StringPoolException { throw new UnsupportedOperationException("Cannot write to a read-only string pool."); } public boolean remove(long node) throws StringPoolException { throw new UnsupportedOperationException("Cannot write to a read-only string pool."); } public void commit() throws SimpleXAResourceException { } public void prepare() throws SimpleXAResourceException { } public void rollback() throws SimpleXAResourceException { } public void clear() throws IOException, SimpleXAResourceException { } public void clear(int phaseNumber) throws IOException, SimpleXAResourceException { } public void refresh() throws SimpleXAResourceException { /* no-op */ } public void release() throws SimpleXAResourceException { } public int getPhaseNumber() throws SimpleXAResourceException { return phaseNumber; } public long findGNode(SPObject spObject) throws StringPoolException { return XA11StringPoolImpl.this.findGNode(spObject); } public long findGNode(SPObject spObject, NodePool nodePool) throws StringPoolException { throw new UnsupportedOperationException("Cannot manually set the node pool for an XA 1.1 store."); } public Tuples findGNodes(SPObject lowValue, boolean inclLowValue, SPObject highValue, boolean inclHighValue) throws StringPoolException { return XA11StringPoolImpl.this.findGNodes(lowValue, inclLowValue, highValue, inclHighValue); } public Tuples findGNodes(TypeCategory typeCategory, URI typeURI) throws StringPoolException { return XA11StringPoolImpl.this.findGNodes(typeCategory, typeURI); } public SPObject findSPObject(long node) throws StringPoolException { return XA11StringPoolImpl.this.findSPObject(node); } public SPObjectFactory getSPObjectFactory() { return SPO_FACTORY; } public long put(SPObject spObject) throws StringPoolException, NodePoolException { throw new UnsupportedOperationException("Cannot write to a read-only string pool."); } public void setNodePool(XANodePool nodePool) { // NO-OP } public long findGNode(SPObject spObject, boolean create) throws StringPoolException { if (create) throw new UnsupportedOperationException("Trying to modify a read-only string pool."); return XA11StringPoolImpl.this.findGNode(spObject, false); } } /** * Represents the root of an index tree. This root is updated for each new phase. */ private class TreePhase { /** The size of a phase record, in Longs. */ static final int RECORD_SIZE = AVLFile.Phase.RECORD_SIZE; /** The underlying tree to manage. */ private AVLFile.Phase avlFilePhase; /** * Create a new phase for the tree. */ public TreePhase() throws IOException { avlFilePhase = dataToGNode.new Phase(); } /** * A copy constructor for a phase. * @param p The existing phase to build this * @throws IOException Caused by an IO error in the under AVL tree. */ TreePhase(TreePhase p) throws IOException { assert p != null; avlFilePhase = dataToGNode.new Phase(p.avlFilePhase); // current phase should be set to this dirty = true; } /** * A constructor from a block on disk. * @param b The block to read from. * @throws IOException Caused by an IO error reading the block. */ TreePhase(Block b) throws IOException { avlFilePhase = dataToGNode.new Phase(b, Metaroot.HEADER_SIZE_LONGS); // current phase should be set to this dirty = true; } /** * Indicates if there are any remaining readers on the current phase. * @return <code>true</code> if the phase is in use. */ public boolean isInUse() { return avlFilePhase.isInUse(); } /** * Inserts a node into the tree, mapping data onto a long. * @param objectData The node to insert. * @param spComparator The comparison mechanism to use to search the tree. */ public void put(DataStruct objectData, SPComparator spComparator) throws StringPoolException { if (objectData.getGNode() < NodePool.MIN_NODE) throw new IllegalArgumentException("gNode < MIN_NODE. Object = " + objectData); AVLNode[] findResult = null; try { AVLComparator avlComparator = new DataAVLComparator(spComparator, objectData, gNodeToDataFile); // Find the adjacent nodes. findResult = avlFilePhase.find(avlComparator, null); if (findResult != null && findResult.length == 1) { throw new StringPoolException("SPObject already exists. (existing graph node: " + findResult[0].getPayloadLong(IDX_GRAPH_NODE) + ")"); } put(objectData, findResult); } catch (IOException ex) { throw new StringPoolException("I/O Error", ex); } finally { if (findResult != null) AVLFile.release(findResult); } } /** * Inserts data into the tree, allocating a new node to store the data in. * @param objectData The data to store. * @param findResult A pair of nodes that the new node must fit between, * or <code>null</code> if the tree is empty. * @throws StringPoolException If the data is already in the tree. * @throws IOException If the tree could not be written to. */ private void put(DataStruct objectData, AVLNode[] findResult) throws StringPoolException, IOException { // Create the new AVLNode. AVLNode newNode = avlFilePhase.newAVLNodeInstance(); objectData.writeTo(newNode); newNode.write(); if (findResult == null) { avlFilePhase.insertFirst(newNode); } else { // Insert the node into the tree. int li = AVLFile.leafIndex(findResult); findResult[li].insert(newNode, 1 - li); } newNode.release(); } /** * Finds a graph node matching a given SPObject. * @param spObject The SPObject to search on. * @param create If <code>true</code> then new nodes are to be allocated when an SPObject * is not found. * @return The graph node. <code>Graph.NONE</code> if not found and <var>create</var> * is <code>false</code>. * @throws StringPoolException For an internal search error. */ long findGNode(SPObject spObject, boolean create) throws StringPoolException { if (spObject == null) throw new StringPoolException("spObject parameter is null"); long gNode; AVLNode[] findResult = null; try { SPComparator spComparator = spObject.getSPComparator(); DataStruct objectData = new DataStruct(spObject); AVLComparator avlComparator = new DataAVLComparator(spComparator, objectData, gNodeToDataFile); // Find the SPObject. findResult = avlFilePhase.find(avlComparator, null); if (findResult != null && findResult.length == 1) { gNode = findResult[0].getPayloadLong(IDX_GRAPH_NODE); } else { if (create) { gNode = nextGNodeValue; objectData.setGNode(gNode); // allocated gNodes move up by the size of the data between them nextGNodeValue += objectData.writeTo(gNodeToDataAppender); put(objectData, findResult); informNodeListeners(gNode); } else { // Not found. gNode = NodePool.NONE; } } } catch (IOException ex) { throw new StringPoolException("I/O Error", ex); } catch (RuntimeException ex) { if (logger.isDebugEnabled()) logger.debug("RuntimeException in findGNode(" + spObject + ")", ex); throw ex; } catch (Error e) { if (logger.isDebugEnabled()) logger.debug("Error in findGNode(" + spObject + ")", e); throw e; } finally { if (findResult != null) AVLFile.release(findResult); } if (logger.isDebugEnabled()) logger.debug("findGNode(" + spObject + ") = " + gNode); return gNode; } /** * Finds a range of SPObjects. * @param lowValue The low end of the range. * @param inclLowValue If the low end value is to be included in the results. * @param highValue The high end of the range. * @param inclHighValue If the high end value is to be included in the results. * @return A range of values, in a Tuples. * @throws StringPoolException Any kind of error, both due to internal structure and IO errors. */ Tuples findGNodes(SPObject lowValue, boolean inclLowValue, SPObject highValue, boolean inclHighValue) throws StringPoolException { SPObject.TypeCategory typeCategory; int typeId; AVLNode lowAVLNode; long highAVLNodeId; if (lowValue == null && highValue == null) { // Return all nodes in the index. typeCategory = null; typeId = SPObjectFactory.INVALID_TYPE_ID; lowAVLNode = avlFilePhase.getRootNode(); if (lowAVLNode != null) lowAVLNode = lowAVLNode.getMinNode_R(); highAVLNodeId = Block.INVALID_BLOCK_ID; } else { // Get the type category. SPObject typeValue = lowValue != null ? lowValue : highValue; typeCategory = typeValue.getTypeCategory(); typeId = typeCategory == SPObject.TypeCategory.TYPED_LITERAL ? ((SPTypedLiteral)typeValue).getTypeId() : SPObjectFactory.INVALID_TYPE_ID; // Check that the two SPObjects are of the same type. if (lowValue != null && highValue != null) { if ( typeCategory != highValue.getTypeCategory() || ( typeCategory == SPObject.TypeCategory.TYPED_LITERAL && ((SPTypedLiteral)lowValue).getTypeId() != ((SPTypedLiteral)highValue).getTypeId() ) ) { // Type mismatch. throw new StringPoolException("lowValue and highValue are not of the same type"); } if (lowValue != null && highValue != null) { // Check for lowValue being higher than highValue. // Also check for lowValue being equal to highValue but excluded // by either inclLowValue or inclHighValue being false. int c = lowValue.compareTo(highValue); if (c > 0 || c == 0 && (!inclLowValue || !inclHighValue)) { return new GNodeTuplesImpl( null, SPObjectFactory.INVALID_TYPE_ID, null, null, null, Block.INVALID_BLOCK_ID ); } } } // Compute the comparator for lowValue. AVLComparator lowComparator; if (lowValue != null) { DataStruct lowData = new DataStruct(lowValue); SPComparator spComparator = lowValue.getSPComparator(); // lowComparator = new SPAVLComparator(spComparator, typeCategory, typeId, data); lowComparator = new DataAVLComparator(spComparator, lowData, gNodeToDataFile); } else { // Select the first node with the current type. if (typeCategory == SPObject.TypeCategory.TYPED_LITERAL) { lowComparator = new DataCategoryTypeAVLComparator(typeCategory.ID, typeId); } else { lowComparator = new DataCategoryAVLComparator(typeCategory.ID); } } // Compute the comparator for highValue. AVLComparator highComparator; if (highValue != null) { DataStruct highData = new DataStruct(highValue); SPComparator spComparator = highValue.getSPComparator(); highComparator = new DataAVLComparator(spComparator, highData, gNodeToDataFile); } else { // Select the first node past the last one that has the current type. if (typeCategory == SPObject.TypeCategory.TYPED_LITERAL) { highComparator = new DataCategoryTypeAVLComparator(typeCategory.ID, typeId + 1); } else { highComparator = new DataCategoryAVLComparator(typeCategory.ID + 1); } } AVLNode[] findResult = avlFilePhase.find(lowComparator, null); if (findResult == null) { // Empty store. lowAVLNode = null; highAVLNodeId = Block.INVALID_BLOCK_ID; } else { if (findResult.length == 1) { // Found the node exactly. lowAVLNode = findResult[0]; // Handle inclLowValue. if (!inclLowValue) { lowAVLNode = lowAVLNode.getNextNode_R(); // The lowValue passed to the GNodeTuplesImpl constructor // is always inclusive but inclLowValue is false. // Recalculate lowValue. if (lowAVLNode != null) lowValue = loadSPObject(typeCategory, typeId, lowAVLNode); } } else { // Did not find the node but found the location where the node // would be if it existed. if (findResult[0] != null) findResult[0].release(); lowAVLNode = findResult[1]; } if (lowAVLNode != null) { // Find the high node. findResult = avlFilePhase.find(highComparator, null); if (findResult.length == 1) { // Found the node exactly. AVLNode highAVLNode = findResult[0]; // Handle inclHighValue. if (inclHighValue) { // Step past this node so that it is included in the range. highAVLNode = highAVLNode.getNextNode(); if (highAVLNode != null) { highAVLNodeId = highAVLNode.getId(); // The highValue passed to the GNodeTuplesImpl constructor // is always exclusive but inclHighValue is true. // Recalculate highValue. highValue = loadSPObject(typeCategory, typeId, highAVLNode); highAVLNode.release(); } else { highAVLNodeId = Block.INVALID_BLOCK_ID; highValue = null; } } else { highAVLNodeId = highAVLNode.getId(); } } else { // Did not find the node but found the location where the node would be if it existed. highAVLNodeId = findResult[1] != null ? findResult[1].getId() : Block.INVALID_BLOCK_ID; } AVLFile.release(findResult); } else { highAVLNodeId = Block.INVALID_BLOCK_ID; } } } return new GNodeTuplesImpl(typeCategory, typeId,lowValue, highValue, lowAVLNode, highAVLNodeId); } /** * Get the entire set of GNodes that match a given type. * @param typeCategory The category of type to match. * @param typeURI The specific type to search for. * @return A tuples containing all GNodes of the requested type. * @throws StringPoolException Caused by a structural error or an IO exception. */ Tuples findGNodes(SPObject.TypeCategory typeCategory, URI typeURI) throws StringPoolException { // null paramaters mean we want all GNodes if (typeCategory == null) { if (typeURI != null) throw new StringPoolException("typeCategory is null and typeURI is not null"); return findAllGNodes(); } // Convert the type URI to a type ID. int typeId; try { typeId = (typeURI == null) ? SPObjectFactory.INVALID_TYPE_ID : SPO_FACTORY.getTypeId(typeURI); } catch (IllegalArgumentException ex) { throw new StringPoolException("Unsupported XSD type: " + typeURI, ex); } // get the appropriate comparators for the requested type Pair<AVLComparator,AVLComparator> comparators = getTypeComparators(typeCategory, typeId); AVLComparator lowComparator = comparators.first(); AVLComparator highComparator = comparators.second(); AVLNode lowAVLNode; long highAVLNodeId; AVLNode[] findResult = avlFilePhase.find(lowComparator, null); if (findResult == null) { // Empty store. lowAVLNode = null; highAVLNodeId = Block.INVALID_BLOCK_ID; } else { assert findResult.length == 2; lowAVLNode = findResult[1]; if (findResult[0] != null) findResult[0].release(); if (lowAVLNode != null) { // Find the high node. findResult = avlFilePhase.find(highComparator, null); assert findResult.length == 2; highAVLNodeId = findResult[1] != null ? findResult[1].getId() : Block.INVALID_BLOCK_ID; AVLFile.release(findResult); } else { highAVLNodeId = Block.INVALID_BLOCK_ID; } } return new GNodeTuplesImpl(typeCategory, typeId, null, null, lowAVLNode, highAVLNodeId); } /** * Constructs a pair of comparators for finding the lowest and highest AVL nodes for a given type specification. * @param typeCategory The category of nodes from the data pool. * @param typeId The ID of the type, if it is a literal. * @return A pair of comparators for the given type. */ private Pair<AVLComparator,AVLComparator> getTypeComparators(SPObject.TypeCategory typeCategory, int typeId) { AVLComparator lowComparator; AVLComparator highComparator; if (typeCategory == SPObject.TypeCategory.TYPED_LITERAL && typeId != SPObjectFactory.INVALID_TYPE_ID) { // Return nodes of the specified category and type node. lowComparator = new DataCategoryTypeAVLComparator(typeCategory.ID, typeId); highComparator = new DataCategoryTypeAVLComparator(typeCategory.ID, typeId + 1); } else { // Return nodes of the specified category. lowComparator = new DataCategoryAVLComparator(typeCategory.ID); highComparator = new DataCategoryAVLComparator(typeCategory.ID + 1); } return new Pair<AVLComparator,AVLComparator>(lowComparator, highComparator); } /** * Retrieves all nodes in the index. * @return A Tuples for all the nodes. */ private Tuples findAllGNodes() { AVLNode lowAVLNode = avlFilePhase.getRootNode(); if (lowAVLNode != null) lowAVLNode = lowAVLNode.getMinNode_R(); return new GNodeTuplesImpl(null, SPObjectFactory.INVALID_TYPE_ID, null, null, lowAVLNode, Block.INVALID_BLOCK_ID); } /** * Load an SPObject with some type checking. * @param typeCategory The category of the object. * @param typeId The ID for the object type * @param avlNode The node to load the data from. * @return The requested object, or <code>null</code> if the object is not compatible with the request. */ private SPObject loadSPObject(SPObject.TypeCategory typeCategory, int typeId, AVLNode avlNode) throws StringPoolException { DataStruct data = new DataStruct(avlNode); try { data.getRemainingBytes(gNodeToDataFile); } catch (IOException e) { throw new StringPoolException("Unable to read data pool", e); } int typeCategoryId = data.getTypeCategoryId(); if ( typeCategoryId == SPObject.TypeCategory.TCID_FREE || // blank node // type mismatch typeCategoryId != typeCategory.ID || ( typeCategory == SPObject.TypeCategory.TYPED_LITERAL && typeId != avlNode.getPayloadByte(IDX_TYPE_ID_B) ) ) { return null; } return data.getSPObject(); } /** * Attaches a token to a phase. Used to maintain a reference. */ final class Token { private AVLFile.Phase.Token avlFileToken; /** Constructs a token on the current phase */ Token() { avlFileToken = avlFilePhase.use(); } public TreePhase getPhase() { assert avlFileToken != null : "Invalid Token"; return TreePhase.this; } public void release() { assert avlFileToken != null : "Invalid Token"; avlFileToken.release(); avlFileToken = null; } } /** * An internal representation of the data structures as a singe Tuples. * It would be nice to have this in an external class, but it is imtimately tied * into the current phase and the data pool itself. */ private class GNodeTuplesImpl implements Tuples { private static final int INVALID_CARDINALITY = -1; /** A cache for the calculated row cardinality. */ private int rowCardinality = INVALID_CARDINALITY; /** * The low value of the range (inclusive) or null to indicate the lowest possible value * within the type defined by the typeCategory and typeId fields. */ private SPObject lowValue; /** * The high value of the range (exclusive) or null to indicate the highest possible value * within the type defined by the typeCategory and typeId fields. */ private SPObject highValue; /** The first index node in the range (inclusive) or null to indicate an empty Tuples. */ private AVLNode lowAVLNode; /** * The last index node in the range (exclusive) or Block.INVALID_BLOCK_ID to indicate all * nodes following lowAVLNode in the index. */ private long highAVLNodeId; /** The current node. */ private AVLNode avlNode = null; /** Maintains a hold on the phase of the structure being accessed. */ AVLFile.Phase.Token avlFileToken = null; /** The number of nodes. */ private long nrGNodes; /** This is set to true once the number of nodes is known. */ private boolean nrGNodesValid = false; private boolean beforeFirst = false; private long[] prefix = null; private boolean onPrefixNode = false; private Variable[] variables = (Variable[])VARIABLES.clone(); /** * Constructs a GNodeTuplesImpl that represents nodes in the AVLFile * index that range from lowAVLNode up to but not including the node with * ID highAVLNodeId. * @param typeCategory The type of data this Tuples returns. * @param typeId The ID of the data being returned. * @param lowValue The lower bound of the sequence in this tuples. * @param highValue The upper bound of the sequence in this tuples. * @param lowAVLNode the AVLNode that has the first graph node that is * included in the Tuples. * @param highAVLNodeId the ID of the AVLNode that has the first graph * node that is not included in the Tuples. */ GNodeTuplesImpl( SPObject.TypeCategory typeCategory, int typeId, SPObject lowValue, SPObject highValue, AVLNode lowAVLNode, long highAVLNodeId ) { if (lowAVLNode != null && lowAVLNode.getId() == highAVLNodeId) { // Low and High are equal - Empty. lowAVLNode.release(); lowAVLNode = null; highAVLNodeId = Block.INVALID_BLOCK_ID; } if (lowAVLNode == null) { // Empty tuples. typeCategory = null; lowValue = null; highValue = null; if (highAVLNodeId != Block.INVALID_BLOCK_ID) { if (logger.isDebugEnabled()) { logger.debug("lowAVLNode is null but highAVLNodeId is not " +Block.INVALID_BLOCK_ID); } highAVLNodeId = Block.INVALID_BLOCK_ID; } nrGNodes = 0; nrGNodesValid = true; } else { avlFileToken = avlFilePhase.use(); } if (typeCategory != SPObject.TypeCategory.TYPED_LITERAL) typeId = SPObjectFactory.INVALID_TYPE_ID; this.lowValue = lowValue; this.highValue = highValue; this.lowAVLNode = lowAVLNode; this.highAVLNodeId = highAVLNodeId; } /** * Get the gNode of from the cursor at this point. This is read directly from the AVLNode. * @see org.mulgara.store.tuples.Tuples#getColumnValue(int) */ public long getColumnValue(int column) throws TuplesException { if (column != 0) throw new TuplesException("Column index out of range: " + column); // Handle the prefix. if (onPrefixNode) return prefix[0]; if (avlNode == null) throw new TuplesException("No current row"); return avlNode.getPayloadLong(DataStruct.IDX_GRAPH_NODE); } /** * @see #getColumnValue(int) */ public long getRawColumnValue(int column) throws TuplesException { return getColumnValue(column); } /** * Returns the single variable name for this data. */ public Variable[] getVariables() { // Clone the variables array in case the caller changes the returned array. return (Variable[])variables.clone(); } /** @return 1, indicating the single column from this data. */ public int getNumberOfVariables() { return 1; } /** * Accumulates the size of this data and returns the number of nodes. * This has scope for improvement, if nodes start storing the numbers of decendants. * @see org.mulgara.store.tuples.Tuples#getRowCount() */ public long getRowCount() throws TuplesException { if (!nrGNodesValid) { assert lowAVLNode != null; AVLNode n = lowAVLNode; n.incRefCount(); long count = 0; while (n != null && (highAVLNodeId == Block.INVALID_BLOCK_ID || n.getId() != highAVLNodeId)) { ++count; n = n.getNextNode_R(); } if (n != null) n.release(); nrGNodes = count; nrGNodesValid = true; } return nrGNodes; } /** Delegates this work to {@link #getRowCount()} */ public long getRowUpperBound() throws TuplesException { return getRowCount(); } /** Delegates this work to {@link #getRowCount()} */ public long getRowExpectedCount() throws TuplesException { return getRowCount(); } /** * Return the cardinality of the tuples. * * @return <code>Cursor.ZERO</code> if the size of this tuples is 0, * <code>Cursor.ONE</code> if the size is 1, * <code>Cursor.MANY</code> if the size of this tuples is 2 or more. * @throws TuplesException If there is an error accessing the underlying data. */ public int getRowCardinality() throws TuplesException { if (rowCardinality != INVALID_CARDINALITY) return rowCardinality; long count = 0; if (nrGNodesValid) { count = nrGNodes; } else { assert lowAVLNode != null; AVLNode n = lowAVLNode; n.incRefCount(); while (count < 2 && n != null && (highAVLNodeId == Block.INVALID_BLOCK_ID || n.getId() != highAVLNodeId)) { ++count; n = n.getNextNode_R(); } if (n != null) n.release(); } rowCardinality = count == 0 ? Cursor.ZERO : count == 1 ? Cursor.ONE : Cursor.MANY; return rowCardinality; } /* (non-Javadoc) * @see org.mulgara.query.Cursor#isEmpty() */ public boolean isEmpty() throws TuplesException { return lowAVLNode == null; } /** @see org.mulgara.store.tuples.Tuples#getColumnIndex(org.mulgara.query.Variable) */ public int getColumnIndex(Variable variable) throws TuplesException { if (variable == null) throw new IllegalArgumentException("variable is null"); if (variable.equals(variables[0])) return 0; throw new TuplesException("variable doesn't match any column: " + variable); } /** @see org.mulgara.store.tuples.Tuples#isColumnEverUnbound(int) */ public boolean isColumnEverUnbound(int column) { return false; } /** @see org.mulgara.store.tuples.Tuples#isMaterialized() */ public boolean isMaterialized() { return true; } /** @see org.mulgara.store.tuples.Tuples#isUnconstrained() */ public boolean isUnconstrained() { return false; } /** @see org.mulgara.store.tuples.Tuples#hasNoDuplicates() */ public boolean hasNoDuplicates() { return true; } /** @see org.mulgara.store.tuples.Tuples#getComparator() */ public RowComparator getComparator() { return null; // Unsorted } /** @see org.mulgara.store.tuples.Tuples#getOperands() */ public java.util.List<Tuples> getOperands() { return java.util.Collections.emptyList(); } /** @see org.mulgara.store.tuples.Tuples#beforeFirst(long[], int) */ public void beforeFirst(long[] prefix, int suffixTruncation) throws TuplesException { assert prefix != null; if (prefix.length > 1) throw new TuplesException("prefix.length (" + prefix.length + ") > nrColumns (1)"); if (suffixTruncation != 0) throw new TuplesException("suffixTruncation not supported"); beforeFirst = true; onPrefixNode = false; this.prefix = prefix; // check if this had been iterating, if so then forget where we were if (avlNode != null) { avlNode.release(); avlNode = null; } } /** @see org.mulgara.query.Cursor#beforeFirst() */ public void beforeFirst() throws TuplesException { beforeFirst(Tuples.NO_PREFIX, 0); } /** @see org.mulgara.store.tuples.Tuples#next() */ public boolean next() throws TuplesException { if (beforeFirst) { assert prefix != null; assert avlNode == null; assert !onPrefixNode; beforeFirst = false; // Handle the prefix. if (prefix.length == 1) { // If there are no nodes this Tuples can't contain the prefix node. if (lowAVLNode == null) return false; SPObject spObject; try { // FIXME check the type category and type node. spObject = findSPObject(prefix[0]); } catch (StringPoolException ex) { throw new TuplesException("Exception while loading SPObject", ex); } // Check that the SPObject is within range. onPrefixNode = spObject != null && (lowValue == null || spObject.compareTo(lowValue) >= 0) && (highValue == null || spObject.compareTo(highValue) < 0); return onPrefixNode; } if (lowAVLNode != null) { lowAVLNode.incRefCount(); avlNode = lowAVLNode; } } else if (avlNode != null) { avlNode = avlNode.getNextNode_R(); if (avlNode != null) { // Check if this is the highNode. if (highAVLNodeId != Block.INVALID_BLOCK_ID && avlNode.getId() == highAVLNodeId ) { avlNode.release(); avlNode = null; } } } onPrefixNode = false; return avlNode != null; } /** * Release the resources reserved by having this tuples refering to the phase. * @see org.mulgara.query.Cursor#close() */ public void close() throws TuplesException { if (lowAVLNode != null) { if (avlNode != null) { avlNode.release(); avlNode = null; } lowAVLNode.release(); lowAVLNode = null; avlFileToken.release(); avlFileToken = null; } } /** @see org.mulgara.store.tuples.Tuples#renameVariables(org.mulgara.query.Constraint) */ public void renameVariables(Constraint constraint) { variables[0] = (Variable)constraint.getElement(0); } /** Duplicate this tuples and its resources. */ public Object clone() { try { GNodeTuplesImpl t = (GNodeTuplesImpl)super.clone(); t.variables = (Variable[])variables.clone(); if (t.lowAVLNode != null) { t.lowAVLNode.incRefCount(); t.avlFileToken = avlFilePhase.use(); // Allocate a new token. if (t.avlNode != null) t.avlNode.incRefCount(); } return t; } catch (CloneNotSupportedException e) { throw new Error(getClass() + " doesn't support clone, which it must", e); } } /** * Iterate over this object and see it looks the same as the comparing object. */ public boolean equals(Object o) { boolean isEqual = false; // Make sure it's not null if (o != null) { try { // Try and cast the passed object - if not then they aren't equal. Tuples testTuples = (Tuples) o; // Ensure that the row count is the same if (getRowCount() == testTuples.getRowCount()) { // Ensure that the variable lists are equal if (java.util.Arrays.asList(getVariables()).equals( java.util.Arrays.asList(testTuples.getVariables()))) { // Clone tuples to be compared Tuples t1 = (Tuples) clone(); Tuples t2 = (Tuples) testTuples.clone(); try { // Put them at the start. t1.beforeFirst(); t2.beforeFirst(); boolean finished = false; boolean tuplesEqual = true; // Repeat until there are no more rows or we find an unequal row. while (!finished) { // Assume that if t1 has next so does t2. finished = !t1.next(); t2.next(); // If we're not finished compare the row. if (!finished) { // Check if the elements in both rows are equal. for (int variableIndex = 0; variableIndex < t1.getNumberOfVariables(); variableIndex++) { // If they're not equal quit the loop and set tuplesEqual to false. if (t1.getColumnValue(variableIndex) != t2.getColumnValue(variableIndex)) { tuplesEqual = false; finished = true; } } } } isEqual = tuplesEqual; } finally { t1.close(); t2.close(); } } } } catch (ClassCastException cce) { // Not of the correct type return false. } catch (TuplesException ex) { throw new RuntimeException(ex.toString(), ex); } } return isEqual; } /** * Added to match {@link #equals(Object)}. */ public int hashCode() { // This works with the above defined equals method return TuplesOperations.hashCode(this); } /** @see java.lang.Object#toString() */ public String toString() { return SimpleTuplesFormat.format(this); } /** * Copied from AbstractTuples */ public Annotation getAnnotation(Class<? extends Annotation> annotationClass) throws TuplesException { return null; } } // end of TreePhase.GNodeTuplesImpl } // end of TreePhase }