// This software is released into the Public Domain. See copying.txt for details. package org.openstreetmap.osmosis.core.store; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import org.openstreetmap.osmosis.core.OsmosisRuntimeException; import org.openstreetmap.osmosis.core.lifecycle.Completable; /** * Provides a store for writing objects to a file for later retrieval. The * number of objects is limited only by disk space. * * @param <T> * The object type to be stored. * @author Brett Henderson */ public class RandomAccessObjectStore<T extends Storeable> implements Completable { private static final Logger LOG = Logger.getLogger(RandomAccessObjectStore.class.getName()); private ObjectSerializationFactory serializationFactory; private StorageStage stage; private String tempFilePrefix; private File tempFile; private File storageFile; private OffsetTrackingOutputStream offsetTrackingStream; private StoreClassRegister storeClassRegister; private ObjectWriter objectWriter; /** * Creates a new instance. * * @param serializationFactory * The factory defining the object serialisation implementation. * @param tempFilePrefix * The prefix of the temporary file. */ public RandomAccessObjectStore(ObjectSerializationFactory serializationFactory, String tempFilePrefix) { this.serializationFactory = serializationFactory; this.tempFilePrefix = tempFilePrefix; storeClassRegister = new DynamicStoreClassRegister(); stage = StorageStage.NotStarted; } /** * Creates a new instance. * * @param serializationFactory * The factory defining the object serialisation implementation. * @param storageFile * The storage file to use. */ public RandomAccessObjectStore(ObjectSerializationFactory serializationFactory, File storageFile) { this.serializationFactory = serializationFactory; this.storageFile = storageFile; storeClassRegister = new DynamicStoreClassRegister(); stage = StorageStage.NotStarted; } /** * Initialises the output file and configures the class for adding data. */ private void initializeAddStage() { // We can't add if we've passed the add stage. if (stage.compareTo(StorageStage.Add) > 0) { throw new OsmosisRuntimeException("Cannot add to storage in stage " + stage + "."); } // If we're not up to the add stage, initialise for adding. if (stage.compareTo(StorageStage.Add) < 0) { FileOutputStream fileStream = null; try { if (storageFile == null) { tempFile = File.createTempFile(tempFilePrefix, null); storageFile = tempFile; } fileStream = new FileOutputStream(storageFile); offsetTrackingStream = new OffsetTrackingOutputStream(new BufferedOutputStream(fileStream, 65536)); // Clear reference so that the stream doesn't get closed at the end of this method. fileStream = null; objectWriter = serializationFactory.createObjectWriter( new DataOutputStoreWriter(new DataOutputStream(offsetTrackingStream)), storeClassRegister ); stage = StorageStage.Add; } catch (IOException e) { throw new OsmosisRuntimeException( "Unable to create object stream writing to file " + storageFile + ".", e); } finally { if (fileStream != null) { try { fileStream.close(); } catch (IOException e) { // We are already in an error condition so log and continue. LOG.log(Level.WARNING, "Unable to close file stream.", e); } } } } } /** * Adds the specified object to the store. * * @param data * The object to be added. * @return The offset within the output file of the object written. */ public long add(T data) { long objectFileOffset; initializeAddStage(); objectFileOffset = offsetTrackingStream.getByteCount(); // Write the object to the store. objectWriter.writeObject(data); return objectFileOffset; } /** * Configures the state of this object instance for reading mode. If the * current state doesn't allow reading, an exception will be thrown. */ private void initializeReadingStage() { // If we're already in the reading stage there's nothing to do. if (stage.equals(StorageStage.Reading)) { return; } if (stage.equals(StorageStage.Add)) { throw new OsmosisRuntimeException( "Cannot begin reading in " + StorageStage.Add + " stage, must call complete first."); } // If we haven't reached the reading stage yet, configure for output // first to ensure a file is available for reading. if (stage.compareTo(StorageStage.Reading) < 0) { stage = StorageStage.Reading; } // If we've passed the reading stage, we can't continue. if (stage.compareTo(StorageStage.Reading) > 0) { throw new OsmosisRuntimeException("Cannot read from storage once we've reached stage " + stage + "."); } } /** * Creates a new reader capable of accessing the contents of this store. The * reader must be explicitly released when no longer required. Readers must * be released prior to this store. * * @return A store reader. */ public RandomAccessObjectStoreReader<T> createReader() { initializeReadingStage(); try { BufferedRandomAccessFileInputStream randomFileReader; randomFileReader = new BufferedRandomAccessFileInputStream(storageFile); return new RandomAccessObjectStoreReader<T>( randomFileReader, serializationFactory.createObjectReader( new DataInputStoreReader( new DataInputStream(randomFileReader)), storeClassRegister) ); } catch (FileNotFoundException e) { throw new OsmosisRuntimeException( "Unable to create object stream reading from file " + storageFile + ".", e); } } /** * {@inheritDoc} */ @Override public void complete() { // If we're already in the reading stage, we don't need to perform a // complete. if (stage.compareTo(StorageStage.Reading) != 0) { // We need to make sure we pass through the add stage to ensure an // output file is created. initializeAddStage(); try { offsetTrackingStream.close(); offsetTrackingStream = null; } catch (IOException e) { throw new OsmosisRuntimeException("Unable to close the file " + storageFile + "."); } stage = StorageStage.Reading; } } /** * {@inheritDoc} */ public void close() { if (offsetTrackingStream != null) { try { offsetTrackingStream.close(); } catch (Exception e) { // We cannot throw an exception within a release statement. LOG.log(Level.WARNING, "Unable to close offset tracking output stream.", e); } offsetTrackingStream = null; } if (tempFile != null) { if (!tempFile.delete()) { // We cannot throw an exception within a release statement. LOG.warning("Unable to delete file " + tempFile); } tempFile = null; } stage = StorageStage.Released; } }