/*******************************************************************************
* CogTool Copyright Notice and Distribution Terms
* CogTool 1.3, Copyright (c) 2005-2013 Carnegie Mellon University
* This software is distributed under the terms of the FSF Lesser
* Gnu Public License (see LGPL.txt).
*
* CogTool is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* CogTool is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CogTool; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* CogTool makes use of several third-party components, with the
* following notices:
*
* Eclipse SWT version 3.448
* Eclipse GEF Draw2D version 3.2.1
*
* Unless otherwise indicated, all Content made available by the Eclipse
* Foundation is provided to you under the terms and conditions of the Eclipse
* Public License Version 1.0 ("EPL"). A copy of the EPL is provided with this
* Content and is also available at http://www.eclipse.org/legal/epl-v10.html.
*
* CLISP version 2.38
*
* Copyright (c) Sam Steingold, Bruno Haible 2001-2006
* This software is distributed under the terms of the FSF Gnu Public License.
* See COPYRIGHT file in clisp installation folder for more information.
*
* ACT-R 6.0
*
* Copyright (c) 1998-2007 Dan Bothell, Mike Byrne, Christian Lebiere &
* John R Anderson.
* This software is distributed under the terms of the FSF Lesser
* Gnu Public License (see LGPL.txt).
*
* Apache Jakarta Commons-Lang 2.1
*
* This product contains software developed by the Apache Software Foundation
* (http://www.apache.org/)
*
* jopt-simple version 1.0
*
* Copyright (c) 2004-2013 Paul R. Holser, Jr.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Mozilla XULRunner 1.9.0.5
*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (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.mozilla.org/MPL/.
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
* The J2SE(TM) Java Runtime Environment version 5.0
*
* Copyright 2009 Sun Microsystems, Inc., 4150
* Network Circle, Santa Clara, California 95054, U.S.A. All
* rights reserved. U.S.
* See the LICENSE file in the jre folder for more information.
******************************************************************************/
package edu.cmu.cs.hcii.cogtool.util;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Implementation of persistence using the XML serialization support
* of ObjectSaver/ObjectLoader.
*
* @author mlh
*/
public class ObjectPersister
{
/**
* Name of the checkpoint file containing the XML serialization.
*/
private static final String PERSIST_FILE = "PERSIST";
public static final ObjectPersister ONLY = new ObjectPersister();
private ObjectPersister() { }
/**
* The information about objects that may be persisted;
* includes the object itself, the directory containing the checkpoint
* file, and the last file into which the object was saved, if any.
*/
private static class PersistInfo
{
public Object obj;
public File checkpointDir;
public File originalFile;
public PersistInfo(Object o, File chkptFile)
{
this(o, chkptFile, null);
}
public PersistInfo(Object o, File chkptFile, File origFile)
{
obj = o;
checkpointDir = chkptFile;
originalFile = origFile;
}
}
/**
* Location of the system temporary directory.
*/
private static final File tmpDir =
new File(System.getProperty("java.io.tmpdir"));
/**
* Prefix for CogTool temporary directories and files.
*/
private static String tmpFilePrefix = "CGT";
/**
* Suffix for CogTool temporary directories containing checkpoint files.
*/
private static String ckpFileSuffix = ".ckp";
/**
* Map from canonical file names to the persistence data for an object.
*/
private Map<String, PersistInfo> fileInfos =
new HashMap<String, PersistInfo>();
/**
* Map from persisted object to the persistence data for the object.
*/
private Map<Object, PersistInfo> objInfos =
new HashMap<Object, PersistInfo>();
/**
* Find the persistence information associated with the given object.
*
* @param obj object for which the persistence information is desired
* @return the persistence information for the given object;
* <code>null</code> if no information registered for the object
* @author mlh
*/
private PersistInfo getInfoByObject(Object obj)
{
return objInfos.get(obj);
}
/**
* Find the persistence information associated with the given
* saved file name (in canonical form).
*
* @param canonicalFileName the name for which the persistence
* information is desired
* @return the persistence information for the given name;
* <code>null</code> if no information registered for the name
* @author mlh
*/
private PersistInfo getInfoByName(String canonicalFileName)
{
return fileInfos.get(canonicalFileName);
}
/**
* Ensures that a desired amount of disk space is available for dstFile.
* This method helps implement a fail-early, recover-early policy for IO.
*
* @param dstFile the location to check for space
* @param size the amount of space desired
* @param msg error message for IOException when space is not available
* @throws FileNotFoundException if dstFile does not exist
* @throws IOException if size is not available for dstFile
*/
private static void checkDiskSpace(File dstFile, long size, String msg)
throws FileNotFoundException, IOException
{
RandomAccessFile rndFile = new RandomAccessFile(dstFile, "rw");
try {
rndFile.setLength(size);
}
catch (IOException e) {
// if not enough space, report problem to user
IOException newE = new IOException(msg);
newE.initCause(e);
throw newE;
}
finally {
rndFile.close();
}
}
/**
* Recursively computes the size on disk of a file or directory.
*
* @param file the file or directory to check
* @return sum of RandomAccessFile.length() for all files at this location
* @throws IOException if any error occurs while reading file sizes
*/
private static long diskSize(File file) throws IOException
{
if (file.isFile()) {
// Base case
long size;
RandomAccessFile f = null;
try {
f = new RandomAccessFile(file, "r");
size = f.length();
}
finally {
if (f != null) {
f.close();
}
}
return size;
}
// Recursive case
File[] files = file.listFiles();
long sum = 0;
for (File file2 : files) {
sum += diskSize(file2);
}
return sum;
}
/**
* Creates a directory with a unique name to hold checkpoint files
* in the system's temporary directory.
*
* @returns the abstract representation of the new directory
* @throws java.io.IOException if the directory cannot be created
* @author mlh
*/
private static File createTempDirectory()
throws IOException
{
// This actually creates a file on the file system!
File uniqueFileName =
File.createTempFile(tmpFilePrefix, ckpFileSuffix, tmpDir);
// Create internal representation only!
File tmpDirName = uniqueFileName.getCanonicalFile();
// Delete the stupid file since we want a directory.
if (! uniqueFileName.delete()) {
throw new IOException("Could not delete temp filename");
}
// Go the make the directory!
if (! tmpDirName.mkdir()) {
throw new IOException("Could not register temp directory");
}
return tmpDirName;
}
/**
* Delete all files referenced by the given abstract name;
* that is, delete the file if it is a single file or delete
* all of the directory's contents and the directory itself
* if it is a directory.
* <p>
* If any <code>delete</code> call fails, it uses <code>delteOnExit</code>.
*
* @param fileOrDir the abstract name of a file or directory
* @author mlh
*/
private static void deleteAll(File fileOrDir)
{
if (fileOrDir.exists()) {
// Base case
if (fileOrDir.isFile()) {
if (! fileOrDir.delete()) {
fileOrDir.deleteOnExit();
}
}
else if (fileOrDir.isDirectory()) {
File[] files = fileOrDir.listFiles();
for (File file : files) {
deleteAll(file);
}
if (! fileOrDir.delete()) {
fileOrDir.deleteOnExit();
}
}
}
} // deleteAll
/**
* Checks whether an Object has a permanent location registered with this
* ObjectPersister. If this returns true, save(project, null) has a well-defined
* destination.
*
* @param obj the Object to check for a permanent location
* @return true if obj has a known permanent location
*/
public boolean isPermanent(Object obj)
{
PersistInfo info = getInfoByObject(obj);
// Check for a null originalFile
return info.originalFile != null;
}
/**
* Create a temporary persistent representation for a new Object. This
* representation will be used for saving checkpoints and eventually to
* save the Object to a more permanent storage format.
*
* @param obj the Object to register for persistence
*/
public void registerForPersistence(Object obj) throws IOException
{
// Create checkpoint directory
File chkptFile = createTempDirectory();
// Keep track of this file for future lookup.
PersistInfo info = new PersistInfo(obj, chkptFile);
objInfos.put(obj, info);
} // registerForPersistence
/**
* Load an Object from a file. This method assumes that all error checking
* and prompting/correction for the validity of the file has already
* been completed.
*
* @param src the File location of the saved Object
* @return a new instantiated Object
* @throws java.io.IOException if any file operation fails or the SAX XML
* parser fails
*/
public Object load(File src) throws IOException
{
// See if an object is already loaded for the given file
String canonicalFileName = src.getCanonicalPath();
PersistInfo info = getInfoByName(canonicalFileName);
if (info != null) {
return info.obj;
}
// Attempt to create a file of 3x size in the temp directory
// to hold the expanded XML serialization.
File sizeCheckFile =
File.createTempFile(tmpFilePrefix, ".size", tmpDir);
try {
checkDiskSpace(sizeCheckFile,
3 * src.length(),
"Not enough temp space on disk to open file: "
+ src);
}
finally {
sizeCheckFile.delete();
}
// If we're here, no exception was thrown; create checkpoint directory
File chkptFile = createTempDirectory();
// Open file as ZipFile and decompress
ZipFile zip = null;
try {
zip = new ZipFile(src);
// Unzip the file to the checkpoint directory
ZipUtil.unzip(zip, chkptFile);
}
catch (ZipException ex) {
IOException newE =
new IOException("load encountered zip compression error");
newE.initCause(ex);
throw newE;
}
finally {
if (zip != null) {
zip.close();
}
}
// Load object from the expanded XML serialization
ObjectLoader l = new ObjectLoader();
Object obj = null;
Reader reader = null;
try {
reader =
new InputStreamReader(new FileInputStream(new File(chkptFile,
PERSIST_FILE)),
"UTF-8");
Collection<?> objSet = l.load(new InputSource(reader), null);
// There should be only one top-level object
Iterator<?> objs = objSet.iterator();
if (objs.hasNext()) {
obj = objs.next();
}
// Register this file for future lookup, both by object
// and by file name
info = new PersistInfo(obj, chkptFile, src);
objInfos.put(obj, info);
fileInfos.put(canonicalFileName, info);
}
catch (ParserConfigurationException e) {
IOException newE = new IOException("load encountered parser error");
newE.initCause(e);
throw newE;
}
catch (SAXException e) {
IOException newE = new IOException("load encountered SAX error");
newE.initCause(e);
throw newE;
}
finally {
if (reader != null) {
reader.close();
}
}
return obj;
} // load
/**
* Return whether the given file is already loaded.
*
* @param src the File location of a saved object
* @return the loaded object if loaded, <code>null</code> otherwise
*/
public Object isLoaded(File src) throws IOException
{
String canonicalFileName = src.getCanonicalPath();
PersistInfo info = getInfoByName(canonicalFileName);
if (info != null) {
return info.obj;
}
return null;
}
/**
* Helper class for recoverFiles, accepts only file names that start with
* tmpFilePrefix and end with ckpFileSuffix.
*/
private static class PrefixFilter implements FileFilter
{
public static final PrefixFilter ONLY = new PrefixFilter();
private PrefixFilter() {}
public boolean accept(File f)
{
String fName = f.getName();
return (f.isDirectory() && fName.startsWith(tmpFilePrefix)
&& fName.endsWith(ckpFileSuffix));
}
}
/**
* Look for checkpoint files that were not cleaned up properly by a
* previous application run.
*
* @return an array of recovered Objects
*/
public Object[] recoverFiles() throws IOException
{
// Look for possible checkpoint directories
File[] chkptDirs = tmpDir.listFiles(PrefixFilter.ONLY);
// Try to revive each
ObjectLoader l = new ObjectLoader();
Object[] recovered = new Object[chkptDirs.length];
for (int i = 0; i < recovered.length; i++) {
FileReader reader = null;
try {
reader = new FileReader(new File(chkptDirs[i], PERSIST_FILE));
recovered[i] = l.load(new InputSource(reader), null);
PersistInfo info = new PersistInfo(recovered[i], chkptDirs[i]);
objInfos.put(recovered[i], info);
// TODO ... keep track of original file name somehow!
}
catch (ParserConfigurationException e) {
IOException newE =
new IOException("load encountered parser error");
newE.initCause(e);
throw newE;
}
catch (SAXException e) {
IOException newE =
new IOException("load encountered SAX error");
newE.initCause(e);
throw newE;
}
finally {
if (reader != null) {
reader.close();
}
}
}
return recovered;
}
/**
* Flushes all in-memory changes for the given Object to disk as a
* checkpoint. The Object must have been created via this class (load or
* recoverFiles) or must be registered via the registerForPersistence
* method before it can be checkpointed.
*
* @param obj the Object to checkpoint
* @throws IllegalArgumentException if the given object had not been
* previously registered via a call to load, recoverFiles, or
* registerForPersistence
* @throws java.io.IOException if checkpointing or renaming fails
*/
public void checkpoint(Object obj) throws IOException
{
// The given object must have been previously registered
PersistInfo info = getInfoByObject(obj);
if (info == null) {
throw new IllegalArgumentException("Cannot find persistence info for given object");
}
// Create a file to hold the XML serialization in the checkpoint dir
File chkpt = new File(info.checkpointDir, PERSIST_FILE);
File oldChkpt = null;
// If a checkpoint already exists, rename it to PERSIST.old
if (chkpt.exists()) {
oldChkpt = new File(info.checkpointDir, PERSIST_FILE + ".old");
if (oldChkpt.exists()) {
oldChkpt.delete(); // try to recover from bad situation
}
// Use oldChkpt File object below because this rename doesn't
// actually change the name for existingChkpt (Java, grrrr!)
if (! chkpt.renameTo(oldChkpt)) {
throw new IOException("Cannot rename old checkpoint file");
}
}
// Create a sink into the file and serialize into XML
Writer writer = null;
try {
writer =
new OutputStreamWriter(new FileOutputStream(chkpt), "UTF-8");
ObjectSaver s = new ObjectSaver(writer);
s.saveObject(obj);
s.finish(); // ensures a flush!
}
finally {
if (writer != null) {
writer.close();
}
}
// If there was an existing checkpoint, delete the .old file
if (oldChkpt != null) {
if (! oldChkpt.delete()) {
throw new IOException("Cannot delete old checkpoint file");
}
}
} // checkpoint
/**
* Persist an Object to a file on the filesystem. The Object must have
* been created via this class (load or recoverFiles) or must be registered
* via the registerForPersistence method before it can be saved. This
* method assumes that all necessary error checking and
* prompting/correction for the destination has already been completed.
*
* @param obj the Object that will be saved to disk
* @param dst the destination File location, or null to reuse the
* load location of this Object
* @throws IllegalArgumentException if the given object had not been
* previously registered via a call to load, recoverFiles, or
* registerForPersistence
* @throws IOException if there isn't enough disk space to save obj to dst
* @throws IllegalStateException if (dst == null) && ! isPermanent(obj)
*/
public void save(Object obj, File dst) throws IOException
{
// The given object must have been previously registered
PersistInfo info = getInfoByObject(obj);
if (info == null) {
throw new IllegalArgumentException("Cannot find persistence info for given object");
}
// Use original file if destination not specified
if (dst == null) {
if (info.originalFile == null) {
throw new IllegalStateException("Unspecified save location!");
}
dst = info.originalFile;
}
// First, checkpoint
checkpoint(obj);
// Create a temp zip file in the same directory as dst
File tmp =
File.createTempFile(tmpFilePrefix, ".cgt", dst.getParentFile());
// Try to ensure the save will succeed
try {
checkDiskSpace(tmp,
diskSize(info.checkpointDir),
"Insufficient space to save project to file: "
+ dst);
}
finally {
tmp.delete();
}
// If we're here, no exception was thrown; compress the checkpoint
// file(s) into a temporary file in the destination file's directory
File[] files = info.checkpointDir.listFiles();
ZipUtil.zip(Arrays.asList(files), tmp);
// If all went well, delete dst and move tmp to dst
if (dst.exists()) {
if (! dst.delete()) {
throw new IOException("File cannot be deleted: "
+ dst.getAbsolutePath());
}
}
tmp.renameTo(dst);
// If all went well, reset original file to destination if necessary
String originalCanonicalPath =
(info.originalFile != null) ? info.originalFile.getCanonicalPath()
: "";
String dstCanonicalPath = dst.getCanonicalPath();
// Reset the file name registration if the name has changed
if (! dstCanonicalPath.equals(originalCanonicalPath)) {
fileInfos.remove(originalCanonicalPath); // works even if ""!
info.originalFile = new File(dstCanonicalPath);
fileInfos.put(dstCanonicalPath, info);
}
} // save
/**
* Releases any persistence-related resources attached to this Object.
*
* @param obj the obj whose persistence resources will be closed
* @throws IllegalArgumentException if the given object had not been
* previously registered via a call to load, recoverFiles, or
* registerForPersistence
* @throws java.io.IOException if any file manipulation files
*/
public void close(Object obj) throws IOException
{
// The given object must have been previously registered
PersistInfo info = getInfoByObject(obj);
if (info != null) {
// Delete the checkpoint directory
deleteAll(info.checkpointDir);
// Remove from registries
objInfos.remove(obj);
if (info.originalFile != null) {
fileInfos.remove(info.originalFile.getCanonicalPath());
}
}
} // close
/**
* Determines whether the given file is associated with a currently loaded
* object that is not the given object.
*
* @param dst the destination file to be used for a "save-as"
* @param obj the object to be saved; it is ok if the file specified is
* associated with the given object
* @return <code>true</code> if and only if the file is registered as
* associated with an object that is not the given object
* @throws IOException
*/
public boolean isRegistered(File dst, Object obj) throws IOException
{
// See if an object is already loaded for the given file
String canonicalFileName = dst.getCanonicalPath();
PersistInfo info = getInfoByName(canonicalFileName);
if (info != null) {
return info.obj != obj;
}
return false;
}
}