/*******************************************************************************
 * Copyright (c) 2004, 2014 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.storagemanager;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SyncFailedException;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.eclipse.osgi.framework.internal.reliablefile.ReliableFile;
import org.eclipse.osgi.framework.internal.reliablefile.ReliableFileInputStream;
import org.eclipse.osgi.framework.internal.reliablefile.ReliableFileOutputStream;
import org.eclipse.osgi.internal.location.LocationHelper;
import org.eclipse.osgi.internal.location.Locker;
import org.eclipse.osgi.internal.messages.Msg;

/**
 * Storage managers provide a facility for tracking the state of a group of files having
 * relationship with each others and being updated by several entities at the same time.
 * The typical usecase is in shared configuration data areas.
 * <p>
 * The facilities provided here are cooperative. That is, all participants must
 * agree to the conventions and to calling the given API. There is no capacity
 * to enforce these conventions or prohibit corruption.
 * </p>
 * <p>
 * Clients can not extend this class
 * </p>
 * <p>
 * Example:</p>
 * <pre>
 * //Open the storage manager
 * org.eclipse.osgi.storagemanager.StorageManager cacheStorageManager = new StorageManager("d:/sharedFolder/bar/", false); //$NON-NLS-1$
 * try {
 *	 cacheStorageManager.open(true);
 * } catch (IOException e) {
 * // Ignore the exception. The registry will be rebuilt from source.
 * }
 *
 * //To read from a file
 * java.io.File fileA = cacheStorageManager.lookup("fileA", false));
 * java.io.File fileB = cacheStorageManager.lookup("fileB", false));
 * //Do the reading code
 * new java.io.FileOutputStream(fileA);
 *
 * //To write in files
 * cacheStorageManager.add("fileC"); //add the file to the filemanager (in this case we assume it is not already here)
 * cacheStorageManager.add("fileD");
 *
 * // The file is never written directly into the file name, so we create some temporary file
 * java.io.File fileC = cacheStorageManager.createTempFile("fileC");
 * java.io.File fileD = cacheStorageManager.createTempFile("fileD");
 *
 * //Do the actual writing here...
 *
 * //Finally update the storagemanager with the actual file to manage.
 * cacheStorageManager.update(new String[] {"fileC", "fileD"}, new String[] {fileC.getName(), fileD.getName()};
 *
 * //Close the file manager at the end
 * cacheStorageManager.close();
 * </pre>
 * <p>
 * Implementation details <br>
 * The following implementation details are provided to help with understanding the
 * behavior of this class.
 * The general principle is to maintain a table which maps user-level file names
 * onto an actual disk files.  If a file needs to be modified,
 * it is stored into a new file.  The old content is not removed from disk until all entities
 * have closed there instance of the storage manager.
 * Once the instance has been created, open() must be called before performing any other operation.
 * On open the storage manager obtains a snapshot of the current managed files contents. If an
 * entity updates a managed file, the storage manager will save the content for that instance of the
 * storage manager, all other storage manager instances will still have access to that managed file's
 * content as it was when the instance was first opened.
 * </p>
 * @since 3.2
 */

// Note the implementation of this class originated from the following deprecated classes:
// /org.eclipse.osgi/eclipseAdaptor/src/org/eclipse/core/runtime/adaptor/FileManager.java
// /org.eclipse.osgi/eclipseAdaptor/src/org/eclipse/core/runtime/adaptor/StreamManager.java
public final class StorageManager {
	private static final int FILETYPE_STANDARD = 0;
	private static final int FILETYPE_RELIABLEFILE = 1;
	private static final String MANAGER_FOLDER = ".manager"; //$NON-NLS-1$
	private static final String TABLE_FILE = ".fileTable"; //$NON-NLS-1$
	private static final String LOCK_FILE = ".fileTableLock"; //$NON-NLS-1$
	private static final int MAX_LOCK_WAIT = 5000; // 5 seconds
	// these should be static but the tests expect to be able to create new managers after changing this setting dynamically
	private final boolean useReliableFiles = Boolean.valueOf(System.getProperty("osgi.useReliableFiles")).booleanValue(); //$NON-NLS-1$
	private final boolean tempCleanup = Boolean.valueOf(System.getProperty("osgi.embedded.cleanTempFiles")).booleanValue(); //$NON-NLS-1$
	private final boolean openCleanup = Boolean.valueOf(System.getProperty("osgi.embedded.cleanupOnOpen")).booleanValue(); //$NON-NLS-1$
	private final boolean saveCleanup = Boolean.valueOf(System.getProperty("osgi.embedded.cleanupOnSave")).booleanValue(); //$NON-NLS-1$

	private class Entry {
		int readId;
		int writeId;
		int fileType;

		Entry(int readId, int writeId, int type) {
			this.readId = readId;
			this.writeId = writeId;
			this.fileType = type;
		}

		int getReadId() {
			return readId;
		}

		int getWriteId() {
			return writeId;
		}

		int getFileType() {
			return fileType;
		}

		void setReadId(int value) {
			readId = value;
		}

		void setWriteId(int value) {
			writeId = value;
		}

		void setFileType(int type) {
			fileType = type;
		}
	}

	private final File base; //The folder managed
	private final File managerRoot; //The folder that will contain all the file related to the functionning of the manager (typically a subdir of base)

	private final String lockMode;
	private final File tableFile;
	private final File lockFile; // The lock file for the table (this file is the same for all the instances)
	private Locker locker; // The locker for the lock

	private File instanceFile; //The file representing the running instance. It is created when the table file is read.
	private Locker instanceLocker = null; //The locker for the instance file.
	private final boolean readOnly; // Whether this storage manager is in read-only mode
	private boolean open; // Whether this storage manager is open for use

	// locking related fields
	private int tableStamp = -1;

	private final Properties table = new Properties();

	/**
	 * Returns a new storage manager for the area identified by the given base
	 * directory.
	 *
	 * @param base the directory holding the files to be managed
	 * @param lockMode the lockMode to use for the storage manager. It can have one the 3 values: none, java.io, java.nio
	 * and also supports null in which case the lock strategy will be the global one.
	 */
	public StorageManager(File base, String lockMode) {
		this(base, lockMode, false);
	}

	/**
	 * Returns a new storage manager for the area identified by the given base
	 * directory.
	 *
	 * @param base the directory holding the files to be managed
	 * @param lockMode the lockMode to use for the storage manager. It can have one the 3 values: none, java.io, java.nio
	 * and also supports null in which case the lock strategy will be the global one.
	 * @param readOnly true if the managed files are read-only
	 */
	public StorageManager(File base, String lockMode, boolean readOnly) {
		this.base = base;
		this.lockMode = lockMode;
		this.managerRoot = new File(base, MANAGER_FOLDER);
		this.tableFile = new File(managerRoot, TABLE_FILE);
		this.lockFile = new File(managerRoot, LOCK_FILE);
		this.readOnly = readOnly;
		open = false;
	}

	private void initializeInstanceFile() throws IOException {
		if (instanceFile != null || readOnly)
			return;
		this.instanceFile = File.createTempFile(".tmp", ".instance", managerRoot); //$NON-NLS-1$//$NON-NLS-2$
		this.instanceFile.deleteOnExit();
		instanceLocker = LocationHelper.createLocker(instanceFile, lockMode, false);
		instanceLocker.lock();
	}

	private String getAbsolutePath(String file) {
		return new File(base, file).getAbsolutePath();
	}

	/**
	 * Add the given managed file name to the list of files managed by this manager.
	 *
	 * @param managedFile name of the file to manage
	 * @throws IOException if there are any problems adding the given file name to the manager
	 */
	public void add(String managedFile) throws IOException {
		add(managedFile, FILETYPE_STANDARD);
	}

	/* (non-Javadoc
	 * Add the given file name to the list of files managed by this manager.
	 *
	 * @param managedFile name of the file to manage.
	 * @param fileType the file type.
	 * @throws IOException if there are any problems adding the given file to the manager
	 */
	private void add(String managedFile, int fileType) throws IOException {
		if (!open)
			throw new IOException(Msg.fileManager_notOpen);
		if (readOnly)
			throw new IOException(Msg.fileManager_illegalInReadOnlyMode);
		if (!lock(true))
			throw new IOException(Msg.fileManager_cannotLock);
		try {
			updateTable();
			Entry entry = (Entry) table.get(managedFile);
			if (entry == null) {
				entry = new Entry(0, 1, fileType);
				table.put(managedFile, entry);
				// if this managed file existed before, ensure there is not an old
				// version on the disk to avoid name collisions. If version found,
				// us the oldest generation+1 for the write ID.
				int oldestGeneration = findOldestGeneration(managedFile);
				if (oldestGeneration != 0)
					entry.setWriteId(oldestGeneration + 1);
				save();
			} else {
				if (entry.getFileType() != fileType) {
					entry.setFileType(fileType);
					updateTable();
					save();
				}
			}
		} finally {
			release();
		}
	}

	/* (non-Javadoc)
	 * Find the oldest generation of a file still available on disk
	 * @param file the file from which to obtain the oldest generation.
	 * @return the oldest generation of the file or 0 if the file does
	 * not exist.
	 */
	private int findOldestGeneration(String managedFile) {
		String[] files = base.list();
		int oldestGeneration = 0;
		if (files != null) {
			String name = managedFile + '.';
			int len = name.length();
			for (String file : files) {
				if (!file.startsWith(name)) {
					continue;
				}
				try {
					int generation = Integer.parseInt(file.substring(len));
					if (generation > oldestGeneration)
						oldestGeneration = generation;
				}catch (NumberFormatException e) {
					continue;
				}
			}
		}
		return oldestGeneration;
	}

	/**
	 * Update the given managed files with the content in the given source files.
	 * The managedFiles is a list of managed file names which are currently managed.
	 * If a managed file name is not currently managed it will be added as a
	 * managed file for this storage manager.
	 * The sources are absolute (or relative to the current working directory)
	 * file paths containing the new content for the corresponding managed files.
	 *
	 * @param managedFiles the managed files to update
	 * @param sources the new content for the managed files
	 * @throws IOException if there are any problems updating the given managed files
	 */
	public void update(String[] managedFiles, String[] sources) throws IOException {
		if (!open)
			throw new IOException(Msg.fileManager_notOpen);
		if (readOnly)
			throw new IOException(Msg.fileManager_illegalInReadOnlyMode);
		if (!lock(true))
			throw new IOException(Msg.fileManager_cannotLock);
		try {
			updateTable();
			int[] originalReadIDs = new int[managedFiles.length];
			boolean error = false;
			for (int i = 0; i < managedFiles.length; i++) {
				originalReadIDs[i] = getId(managedFiles[i]);
				if (!update(managedFiles[i], sources[i]))
					error = true;
			}
			if (error) {
				// restore the original readIDs to avoid inconsistency for this group
				for (int i = 0; i < managedFiles.length; i++) {
					Entry entry = (Entry) table.get(managedFiles[i]);
					entry.setReadId(originalReadIDs[i]);
				}
				throw new IOException(Msg.fileManager_updateFailed);
			}
			save(); //save only if no errors
		} finally {
			release();
		}
	}

	/**
	 * Returns a list of all the managed files currently being managed.
	 *
	 * @return the names of the managed files
	 */
	public String[] getManagedFiles() {
		if (!open)
			return null;
		Set<Object> set = table.keySet();
		@SuppressWarnings("cast")
		String[] keys = (String[]) set.toArray(new String[set.size()]);
		String[] result = new String[keys.length];
		for (int i = 0; i < keys.length; i++)
			result[i] = new String(keys[i]);
		return result;
	}

	/**
	 * Returns the directory containing the files being managed by this storage
	 * manager.
	 *
	 * @return the directory containing the managed files
	 */
	public File getBase() {
		return base;
	}

	/**
	 * Returns the current numeric id (appendage) of the given managed file.
	 * <code>managedFile + "." + getId(target)</code>. A value of -1 is returned
	 * if the given name is not managed.
	 *
	 * @param managedFile the name of the managed file
	 * @return the id of the managed file
	 */
	public int getId(String managedFile) {
		if (!open)
			return -1;
		Entry entry = (Entry) table.get(managedFile);
		if (entry == null)
			return -1;
		return entry.getReadId();
	}

	/**
	 * Returns if readOnly state this storage manager is using.
	 *
	 * @return if this storage manager update state is read-only.
	 */
	public boolean isReadOnly() {
		return readOnly;
	}

	/* (non-Javadoc)
	 * Attempts to lock the state of this manager and returns <code>true</code>
	 * if the lock could be acquired.
	 * <p>
	 * Locking a manager is advisory only. That is, it does not prevent other
	 * applications from modifying the files managed by this manager.
	 * </p>
	 *
	 * @exception IOException
	 *                         if there was an unexpected problem while acquiring the
	 *                         lock.
	 */
	private boolean lock(boolean wait) throws IOException {
		if (readOnly)
			return false;
		if (locker == null) {
			locker = LocationHelper.createLocker(lockFile, lockMode, false);
			if (locker == null)
				throw new IOException(Msg.fileManager_cannotLock);
		}
		boolean locked = locker.lock();
		if (locked || !wait)
			return locked;
		//Someone else must have the directory locked, but they should release it quickly
		long start = System.currentTimeMillis();
		while (true) {
			try {
				Thread.sleep(200); // 5x per second
			} catch (InterruptedException e) {/*ignore*/
			}
			locked = locker.lock();
			if (locked)
				return true;
			// never wait longer than 5 seconds
			long time = System.currentTimeMillis() - start;
			if (time > MAX_LOCK_WAIT)
				return false;
		}
	}

	/**
	 * Returns the actual file location to use when reading the given managed file.
	 * A value of <code>null</code> can be returned if the given managed file name is not
	 * managed and add is set to false.
	 * <p>
	 * The returned file should be considered read-only.  Any updates to the content of this
	 * file should be done using {@link #update(String[], String[])}.
	 *
	 * @param managedFile the managed file to lookup
	 * @param add indicate whether the managed file name should be added to the manager if
	 * it is not already managed.
	 * @throws IOException if the add flag is set to true and the addition of the managed file failed
	 * @return the absolute file location to use for the given managed file or
	 *               <code>null</code> if the given managed file is not managed
	 */
	public File lookup(String managedFile, boolean add) throws IOException {
		if (!open)
			throw new IOException(Msg.fileManager_notOpen);
		Entry entry = (Entry) table.get(managedFile);
		if (entry == null) {
			if (add) {
				add(managedFile);
				entry = (Entry) table.get(managedFile);
			} else {
				return null;
			}
		}
		return new File(getAbsolutePath(managedFile + '.' + entry.getReadId()));
	}

	private boolean move(String source, String managedFile) {
		File original = new File(source);
		File targetFile = new File(managedFile);
		// its ok if the original does not exist. The table entry will capture
		// that fact. There is no need to put something in the filesystem.
		if (!original.exists() || targetFile.exists())
			return false;
		return original.renameTo(targetFile);
	}

	/**
	 * Saves the state of the storage manager and releases any locks held.
	 */
	private void release() {
		if (locker == null)
			return;
		locker.release();
	}

	/**
	 * Removes the given managed file from management by this storage manager.
	 *
	 * @param managedFile the managed file to remove
	 * @throws IOException if an error occured removing the managed file
	 */
	public void remove(String managedFile) throws IOException {
		if (!open)
			throw new IOException(Msg.fileManager_notOpen);
		if (readOnly)
			throw new IOException(Msg.fileManager_illegalInReadOnlyMode);
		// The removal needs to be done eagerly, so the value is effectively removed from the disktable.
		// Otherwise, an updateTable() caused by an update(,)  could cause the file to readded to the local table.
		if (!lock(true))
			throw new IOException(Msg.fileManager_cannotLock);
		try {
			updateTable();
			table.remove(managedFile);
			save();
		} finally {
			release();
		}
	}

	private void updateTable() throws IOException {
		int stamp;
		stamp = ReliableFile.lastModifiedVersion(tableFile);
		if (stamp == tableStamp || stamp == -1)
			return;
		Properties diskTable = new Properties();
		InputStream input = new ReliableFileInputStream(tableFile);
		try {
			diskTable.load(input);
		} finally {
			try {
				input.close();
			} catch (IOException e) {
				// ignore
			}
		}
		tableStamp = stamp;
		for (Enumeration<Object> e = diskTable.keys(); e.hasMoreElements();) {
			String file = (String) e.nextElement();
			String value = diskTable.getProperty(file);
			if (value != null) {
				Entry entry = (Entry) table.get(file);
				// check front of value for ReliableFile
				int id;
				int fileType;
				int idx = value.indexOf(',');
				if (idx != -1) {
					id = Integer.parseInt(value.substring(0, idx));
					fileType = Integer.parseInt(value.substring(idx + 1));
				} else {
					id = Integer.parseInt(value);
					fileType = FILETYPE_STANDARD;
				}
				if (entry == null) {
					table.put(file, new Entry(id, id + 1, fileType));
				} else {
					entry.setWriteId(id + 1);
					//don't change type
				}
			}
		}
	}

	/*
	 * This method should be called while the manager is locked.
	 */
	private void save() throws IOException {
		if (readOnly)
			return;
		// if the table file has change on disk, update our data structures then
		// rewrite the file.
		updateTable();

		Properties props = new Properties();
		for (Enumeration<Object> e = table.keys(); e.hasMoreElements();) {
			String file = (String) e.nextElement();
			Entry entry = (Entry) table.get(file);
			String value;
			if (entry.getFileType() != FILETYPE_STANDARD) {
				value = Integer.toString(entry.getWriteId() - 1) + ',' + //In the table we save the write  number  - 1, because the read number can be totally different.
						entry.getFileType();
			} else {
				value = Integer.toString(entry.getWriteId() - 1); //In the table we save the write  number  - 1, because the read number can be totally different.
			}
			props.put(file, value);
		}
		ReliableFileOutputStream fileStream = new ReliableFileOutputStream(tableFile);
		boolean error = true;
		try {
			props.store(fileStream, "safe table"); //$NON-NLS-1$
			fileStream.close();
			error = false;
		} finally {
			if (error)
				fileStream.abort();
		}
		// bug 259981 we should clean up
		if (saveCleanup) {
			try {
				cleanup(false);
			} catch (IOException ex) {
				// If IOException is thrown from our custom method.
				// log and swallow for now.
				System.out.println("Unexpected IOException is thrown inside cleanupWithLock. Please look below for stacktrace"); //$NON-NLS-1$
				ex.printStackTrace(System.out);
			}
		}
		tableStamp = ReliableFile.lastModifiedVersion(tableFile);
	}

	private boolean update(String managedFile, String source) throws IOException {
		Entry entry = (Entry) table.get(managedFile);
		if (entry == null)
			add(managedFile);
		int newId = entry.getWriteId();
		// attempt to rename the file to the next generation
		boolean success = move(getAbsolutePath(source), getAbsolutePath(managedFile) + '.' + newId);
		if (!success) {
			//possible the next write generation file exists? Lets determine the largest
			//generation number, then use that + 1.
			newId = findOldestGeneration(managedFile) + 1;
			success = move(getAbsolutePath(source), getAbsolutePath(managedFile) + '.' + newId);
		}
		if (!success)
			return false;
		// update the entry. read and write ids should be the same since
		// everything is in sync
		entry.setReadId(newId);
		entry.setWriteId(newId + 1);
		return true;
	}

	/**
	 * This methods remove all the temporary files that have been created by the storage manager.
	 * This removal is only done if the instance of eclipse calling this method is the last instance using this storage manager.
	 * @throws IOException
	 */
	private void cleanup(boolean doLock) throws IOException {
		if (readOnly)
			return;
		//Lock first, so someone else can not start while we're in the middle of cleanup
		if (doLock && !lock(true))
			throw new IOException(Msg.fileManager_cannotLock);
		try {
			//Iterate through the temp files and delete them all, except the one representing this storage manager.
			String[] files = managerRoot.list();
			if (files != null) {
				for (String file : files) {
					if (file.endsWith(".instance") && (instanceFile == null || !file.equalsIgnoreCase(instanceFile.getName()))) { //$NON-NLS-1$
						Locker tmpLocker = LocationHelper.createLocker(new File(managerRoot, file), lockMode, false);
						if (tmpLocker.lock()) {
							//If I can lock it is a file that has been left behind by a crash
							tmpLocker.release();
							new File(managerRoot, file).delete();
						} else {
							tmpLocker.release();
							return; //The file is still being locked by somebody else
						}
					}
				}
			}

			//If we are here it is because we are the last instance running. After locking the table and getting its latest content, remove all the backup files and change the table
			updateTable();
			Collection<Map.Entry<Object, Object>> managedFiles = table.entrySet();
			for (Map.Entry<Object, Object> fileEntry : managedFiles) {
				String fileName = (String) fileEntry.getKey();
				Entry info = (Entry) fileEntry.getValue();
				if (info.getFileType() == FILETYPE_RELIABLEFILE) {
					ReliableFile.cleanupGenerations(new File(base, fileName));
				} else {
					//Because we are cleaning up, we are giving up the values from our table, and we must delete all the files that are not referenced by the table
					String readId = Integer.toString(info.getWriteId() - 1);
					deleteCopies(fileName, readId);
				}
			}

			if (tempCleanup) {
				files = base.list();
				if (files != null) {
					for (String file : files) {
						if (file.endsWith(ReliableFile.tmpExt)) {
							new File(base, file).delete();
						}
					}
				}
			}
		} finally {
			if (doLock)
				release();
		}
	}

	private void deleteCopies(String fileName, String exceptionNumber) {
		String notToDelete = fileName + '.' + exceptionNumber;
		String[] files = base.list();
		if (files == null)
			return;
		for (String file : files) {
			if (file.startsWith(fileName + '.') && !file.equals(notToDelete)) {
				new File(base, file).delete();
			}
		}
	}

	/**
	 * This method declares the storage manager as closed. From thereon, the instance can no longer be used.
	 * It is important to close the manager as it also cleans up old copies of the managed files.
	 */
	public void close() {
		if (!open)
			return;
		open = false;
		if (readOnly)
			return;
		try {
			cleanup(true);
		} catch (IOException e) {
			//Ignore and close.
		}
		if (instanceLocker != null)
			instanceLocker.release();

		if (instanceFile != null)
			instanceFile.delete();
	}

	/**
	 * This methods opens the storage manager.
	 * This method must be called before any operation on the storage manager.
	 * @param wait indicates if the open operation must wait in case of contention on the lock file.
	 * @throws IOException if an error occurred opening the storage manager
	 */
	public void open(boolean wait) throws IOException {
		if (!readOnly) {
			managerRoot.mkdirs();
			if (!managerRoot.isDirectory())
				throw new IOException(Msg.fileManager_cannotLock);
			if (openCleanup)
				cleanup(true);
			boolean locked = lock(wait);
			if (!locked && wait)
				throw new IOException(Msg.fileManager_cannotLock);
		}

		try {
			initializeInstanceFile();
			updateTable();
			open = true;
		} finally {
			release();
		}
	}

	/**
	 * Creates a new unique empty temporary-file in the storage manager base directory. The file name
	 * must be at least 3 characters. This file can later be used to update a managed file.
	 * <p>
	 * Note that {@link File#deleteOnExit()} is not called on the returned file.
	 * </p>
	 * @param file the file name to create temporary file from.
	 * @return the newly-created empty file.
	 * @throws IOException if the file can not be created.
	 * @see #update(String[], String[])
	 */
	public File createTempFile(String file) throws IOException {
		if (readOnly)
			throw new IOException(Msg.fileManager_illegalInReadOnlyMode);
		File tmpFile = File.createTempFile(file, ReliableFile.tmpExt, base);
		// bug 350106: do not use deleteOnExit()  If clients really want that the
		// they can call it themselves.
		return tmpFile;
	}

	/**
	 * Returns a managed <code>InputStream</code> for a managed file.
	 * <code>null</code> can be returned if the given name is not managed.
	 *
	 * @param managedFile the name of the managed file to open.
	 * @return an input stream to the managed file or
	 * <code>null</code> if the given name is not managed.
	 * @throws IOException if the content is missing, corrupt or an error occurs.
	 */
	public InputStream getInputStream(String managedFile) throws IOException {
		return getInputStream(managedFile, ReliableFile.OPEN_BEST_AVAILABLE);
	}

	/**
	 * Returns a managed input stream set for the managed file names.
	 * Elements of the returned set may be <code>null</code> if a given name is not managed.
	 * This method should be used for managed file sets which use the output streams returned
	 * by the {@link #getOutputStreamSet(String[])} to save data.
	 *
	 * @param managedFiles the names of the managed files to open.
	 * @return a set input streams to the given managed files.
	 * @throws IOException if the content of one of the managed files is missing, corrupt or an error occurs.
	 */
	public InputStream[] getInputStreamSet(String[] managedFiles) throws IOException {
		InputStream[] streams = new InputStream[managedFiles.length];
		for (int i = 0; i < streams.length; i++)
			streams[i] = getInputStream(managedFiles[i], ReliableFile.OPEN_FAIL_ON_PRIMARY);
		return streams;
	}

	private InputStream getInputStream(String managedFiles, int openMask) throws IOException {
		if (useReliableFiles) {
			int id = getId(managedFiles);
			if (id == -1)
				return null;
			return new ReliableFileInputStream(new File(getBase(), managedFiles), id, openMask);
		}
		File lookup = lookup(managedFiles, false);
		if (lookup == null)
			return null;
		return new FileInputStream(lookup);
	}

	/**
	 * Returns a <code>ManagedOutputStream</code> for a managed file.
	 * Closing the ouput stream will update the storage manager with the
	 * new content of the managed file.
	 *
	 * @param managedFile the name of the managed file to write.
	 * @return a managed output stream for the managed file.
	 * @throws IOException if an error occurs opening the managed file.
	 */
	public ManagedOutputStream getOutputStream(String managedFile) throws IOException {
		if (useReliableFiles) {
			ReliableFileOutputStream out = new ReliableFileOutputStream(new File(getBase(), managedFile));
			return new ManagedOutputStream(out, this, managedFile, null);
		}
		File tmpFile = createTempFile(managedFile);
		return new ManagedOutputStream(new FileOutputStream(tmpFile), this, managedFile, tmpFile);
	}

	/**
	 * Returns an array of <code>ManagedOutputStream</code> for a set of managed files.
	 * When all managed output streams in the set have been closed, the storage manager
	 * will be updated with the new content of the managed files.
	 * Aborting any one of the streams will cause the entire content of the set to abort
	 * and be discarded.
	 *
	 * @param managedFiles list of names of the managed file to write.
	 * @return an array of managed output streams respectively of managed files.
	 * @throws IOException if an error occurs opening the managed files.
	 */
	public ManagedOutputStream[] getOutputStreamSet(String[] managedFiles) throws IOException {
		int count = managedFiles.length;
		ManagedOutputStream[] streams = new ManagedOutputStream[count];
		int idx = 0;
		try {
			for (; idx < count; idx++) {
				ManagedOutputStream newStream = getOutputStream(managedFiles[idx]);
				newStream.setStreamSet(streams);
				streams[idx] = newStream;
			}
		} catch (IOException e) {
			// cleanup
			for (int jdx = 0; jdx < idx; jdx++)
				streams[jdx].abort();
			throw e;
		}
		return streams;
	}

	/* (non-Javadoc)
	 * Instructs this manager to abort and discard a managed output stream.
	 * This method should be used if any errors occur after opening a managed
	 * output stream where the contents should not be saved.
	 * If this output stream is part of a set, all other managed output streams in this set
	 * will also be closed and aborted.
	 * @param out the managed output stream
	 * @see #getOutputStream(String)
	 * @see #getOutputStreamSet(String[])
	 */
	void abortOutputStream(ManagedOutputStream out) {
		ManagedOutputStream[] streamset = out.getStreamSet();
		if (streamset == null) {
			streamset = new ManagedOutputStream[] {out};
		}
		synchronized (streamset) {
			for (ManagedOutputStream stream : streamset) {
				out = stream;
				if (out.getOutputFile() == null) {
					// this is a ReliableFileOutpuStream
					ReliableFileOutputStream rfos = (ReliableFileOutputStream) out.getOutputStream();
					rfos.abort();
				} else {
					// plain FileOutputStream();
					if (out.getState() == ManagedOutputStream.ST_OPEN) {
						try {
							out.getOutputStream().close();
						} catch (IOException e) {/*do nothing*/
						}
					}
					out.getOutputFile().delete();
				}
				out.setState(ManagedOutputStream.ST_CLOSED);
			}
		}
	}

	/* (non-Javadoc)
	 * Close the managed output stream and update the new content to
	 * this manager. If this managed output stream is part of a set, only after closing
	 * all managed output streams in the set will storage manager be updated.
	 *
	 * @param smos the output stream.
	 * @throws IOException if an errors occur.
	 * @see #getOutputStream(String)
	 * @see #getOutputStreamSet(String[])
	 */
	void closeOutputStream(ManagedOutputStream smos) throws IOException {
		if (smos.getState() != ManagedOutputStream.ST_OPEN)
			return;
		ManagedOutputStream[] streamSet = smos.getStreamSet();
		if (smos.getOutputFile() == null) {
			// this is a ReliableFileOutputStream
			ReliableFileOutputStream rfos = (ReliableFileOutputStream) smos.getOutputStream();
			// manage file deletes
			File file = rfos.closeIntermediateFile();
			smos.setState(ManagedOutputStream.ST_CLOSED);
			String target = smos.getTarget();
			if (streamSet == null) {
				add(target, StorageManager.FILETYPE_RELIABLEFILE);
				update(new String[] {smos.getTarget()}, new String[] {file.getName()});
				ReliableFile.fileUpdated(new File(getBase(), smos.getTarget()));
			}
		} else {
			// this is a plain old file output steam
			OutputStream out = smos.getOutputStream();
			out.flush();
			try {
				((FileOutputStream) out).getFD().sync();
			} catch (SyncFailedException e) {/*ignore*/
			}
			out.close();
			smos.setState(ManagedOutputStream.ST_CLOSED);
			String target = smos.getTarget();
			if (streamSet == null) {
				add(target, StorageManager.FILETYPE_STANDARD);
				update(new String[] {target}, new String[] {smos.getOutputFile().getName()});
			}
		}

		if (streamSet != null) {
			synchronized (streamSet) {
				//check all the streams to see if there are any left open....
				for (ManagedOutputStream stream : streamSet) {
					if (stream.getState() == ManagedOutputStream.ST_OPEN) {
						return; //done
					}
				}
				//all streams are closed, we need to update storage manager
				String[] targets = new String[streamSet.length];
				String[] sources = new String[streamSet.length];
				for (int idx = 0; idx < streamSet.length; idx++) {
					smos = streamSet[idx];
					targets[idx] = smos.getTarget();
					File outputFile = smos.getOutputFile();
					if (outputFile == null) {
						// this is a ReliableFile
						add(smos.getTarget(), StorageManager.FILETYPE_RELIABLEFILE);
						ReliableFileOutputStream rfos = (ReliableFileOutputStream) smos.getOutputStream();
						File file = rfos.closeIntermediateFile(); //multiple calls to close() ok
						sources[idx] = file.getName();
						ReliableFile.fileUpdated(new File(getBase(), smos.getTarget()));
					} else {
						add(smos.getTarget(), StorageManager.FILETYPE_STANDARD);
						sources[idx] = outputFile.getName();
					}
				}
				update(targets, sources);
			}
		}
	}
}