/****************************************************************************/
/* File: FileSystemStorage.java */
/* Author: F. Georges - H2O Consulting */
/* Date: 2010-10-07 */
/* Tags: */
/* Copyright (c) 2010 Florent Georges (see end of file.) */
/* ------------------------------------------------------------------------ */
package org.expath.pkg.repo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URI;
import java.util.Set;
import javax.xml.transform.stream.StreamSource;
import org.expath.pkg.repo.tools.Logger;
import org.expath.pkg.repo.tools.PackagesTxtFile;
import org.expath.pkg.repo.tools.PackagesXmlFile;
/**
* Storage using the file system.
*
* @author Florent Georges
*/
public class FileSystemStorage
extends Storage
{
public FileSystemStorage(File root)
throws PackageException
{
// the repository root directory
if ( root == null ) {
throw new NullPointerException("The repository root directory is null");
}
if ( ! root.exists() ) {
String msg = "The repository root directory does not exist: " + root;
throw new PackageException(msg);
}
if ( ! root.isDirectory() ) {
String msg = "The repository root directory is not a directory: " + root;
throw new PackageException(msg);
}
myRoot = root;
File dir = new File(root, ".expath-pkg");
FileHelper.ensureDir(dir);
myPrivate = dir;
File xmlfile = new File(dir, "packages.xml");
myXmlFile = new PackagesXmlFile(xmlfile);
File txtfile = new File(dir, "packages.txt");
myTxtFile = new PackagesTxtFile(txtfile);
}
public File getRootDirectory()
{
return myRoot;
}
@Override
public boolean isReadOnly()
{
return false;
}
@Override
public PackageResolver makePackageResolver(String rsrc_name, String abbrev)
throws PackageException
{
File pkg_root = rsrc_name == null ? null : new File(myRoot, rsrc_name);
return new FileSystemResolver(pkg_root, abbrev, rsrc_name);
}
@Override
public Set<String> listPackageDirectories()
throws PackageException
{
return myTxtFile.parseDirectories();
}
@Override
public void beforeInstall(boolean force, UserInteractionStrategy interact)
throws PackageException
{
// nothing
}
@Override
public File makeTempDir(String prefix)
throws PackageException
{
return FileHelper.makeTempDir(prefix, myPrivate);
}
@Override
public boolean packageKeyExists(String key)
throws PackageException
{
File f = new File(myRoot, key);
return f.exists();
}
@Override
public void storeInstallDir(File dir, String key, Package pkg)
throws PackageException
{
// move the temporary dir content to the repository
File dest = new File(myRoot, key);
FileHelper.renameTmpDir(dir, dest);
FileSystemResolver resolver = getResolver(pkg);
resolver.setPkgDir(dest);
}
@Override
public void updatePackageLists(Package pkg)
throws PackageException
{
FileSystemResolver resolver = getResolver(pkg);
String dir = resolver.getDirName();
myXmlFile.addPackage(pkg, dir);
File txt_file = new File(myPrivate, "packages.txt");
myTxtFile.addPackage(pkg, dir);
}
@Override
public void remove(Package pkg)
throws PackageException
{
FileSystemResolver resolver = getResolver(pkg);
// remove the entries from the packages.* files
String dir = resolver.getDirName();
myXmlFile.removePackageByDir(dir);
myTxtFile.removePackageByDir(dir);
// actually delete the files
deleteDirRecurse(resolver.myPkgDir);
}
@Override
public String toString()
{
return "File system storage in " + myRoot.getAbsolutePath();
}
/**
* If true (the default), an error is thrown if there is no content dir in the package.
*/
public void setErrorIfNoContentDir(boolean value)
{
myErrorIfNoContentDir = value;
}
private FileSystemResolver getResolver(Package pkg)
throws PackageException
{
Storage.PackageResolver base_resolver = pkg.getResolver();
if ( ! (base_resolver instanceof FileSystemResolver) ) {
throw new PackageException("The package has not been installed in this storage.");
}
return (FileSystemResolver) base_resolver;
}
/**
* Delete a complete directory (with its descendants).
*/
private void deleteDirRecurse(File target)
throws PackageException
{
// recurse if needed
File[] children = target.listFiles();
if ( children != null ) {
for ( File child : children ) {
deleteDirRecurse(child);
}
}
// delete target (if it is a dir, it is empty after recursing)
for ( int i = 3; i >= 0; --i ) {
boolean success = shallowDelete(target, i);
if ( success ) {
break;
}
}
}
/**
* Delete a file or a directory.
*
* @param target The file or directory to delete. The function does not
* recurse, so if {@code target} is a directory, it must be empty.
*
* @param remain The remaining attempts at trying. If it is 0, this function
* throws an error if the deletion fails.
*
* @throws PackageException If {@code remain} is 0 and deleting the target
* fails.
*/
@SuppressWarnings("SleepWhileInLoop")
private boolean shallowDelete(File target, int remain)
throws PackageException
{
if ( ! target.exists() ) {
// trying to delete a non-existing file or dir
return true;
}
boolean success = target.delete();
if ( success ) {
return true;
}
// because of Windows file management, we need to try to collect the
// garbage before failing, in case deleting a file fails
// TODO: Do it only on Windows?
// TODO: Is there a better way?
System.gc();
try {
Thread.sleep(100);
}
catch ( InterruptedException ex ) {
throw new PackageException("Interrupted while deleting: " + target);
}
if ( remain <= 0 ) {
if ( target.isDirectory() ) {
throw new PackageException("Error deleting a dir: " + target);
}
else {
throw new PackageException("Error deleting a file: " + target);
}
}
return false;
}
/** The logger. */
private static final Logger LOG = Logger.getLogger(FileSystemStorage.class);
/** The root dir of the repo. */
private File myRoot;
/** The private area. Must be used only through getPrivateFile(). */
private File myPrivate;
/** The package list, XML format, in [repo]/.expath-pkg/packages.xml. */
private final PackagesXmlFile myXmlFile;
/** The package list, text format, in [repo]/.expath-pkg/packages.txt. */
private final PackagesTxtFile myTxtFile;
/** Throw an error if none content dir exist? */
private boolean myErrorIfNoContentDir = true;
public class FileSystemResolver
extends PackageResolver
{
public FileSystemResolver(File pkg_dir, String abbrev, String rsrc_name)
throws PackageException
{
myPkgAbbrev = abbrev;
myRsrcName = rsrc_name;
setPkgDir(pkg_dir);
}
@Override
public String getResourceName()
{
return myRsrcName;
}
@Override
public URI getContentDirBaseURI()
{
return myContentDir.toURI();
}
private void setPkgDir(File dir)
throws PackageException
{
myPkgDir = dir;
if ( dir == null || myPkgAbbrev == null ) {
myContentDir = null;
}
else {
myContentDir = getContenDir(dir, myPkgAbbrev);
}
}
private File getContenDir(File pkg_dir, String abbrev)
throws PackageException
{
File old_style = new File(pkg_dir, abbrev);
File new_style = new File(pkg_dir, "content");
boolean old_exists = old_style.exists();
boolean new_exists = new_style.exists();
boolean old_isdir = old_style.isDirectory();
boolean new_isdir = new_style.isDirectory();
LOG.finer("Content dir ''{0}'' (exists:{1}/isdir:{2}), and ''{3}'' (exists:{4}/isdir:{5})",
new_style, new_exists, new_isdir, old_style, old_exists, old_isdir);
if ( ! old_exists && ! new_exists ) {
String msg = "None of content dirs exist: '" + new_style + "' and '" + old_style + "'";
LOG.info(msg);
if ( myErrorIfNoContentDir ) {
throw new PackageException(msg);
}
return null;
}
else if ( old_exists && new_exists ) {
String msg = "Both content dirs exist: '" + new_style + "' and '" + old_style + "'";
LOG.info(msg);
throw new PackageException(msg);
}
else if ( new_exists ) {
if ( ! new_isdir ) {
String msg = "Content dir is not a directory: '" + new_style + "'";
LOG.info(msg);
throw new PackageException(msg);
}
return new_style;
}
else {
if ( ! old_isdir ) {
String msg = "Content dir is not a directory: '" + old_style + "'";
LOG.info(msg);
throw new PackageException(msg);
}
LOG.info("Warning: package uses old-style content dir: ''{0}''", old_style);
return old_style;
}
}
public File resolveResourceAsFile(String path)
{
return new File(myPkgDir, path);
}
public File resolveComponentAsFile(String path)
{
if ( myContentDir == null ) {
return null;
}
return new File(myContentDir, path);
}
@Override
public StreamSource resolveResource(String path)
throws PackageException
, NotExistException
{
return resolveWithin(path, myPkgDir);
}
@Override
public StreamSource resolveComponent(String path)
throws PackageException
, NotExistException
{
if ( myContentDir == null ) {
return null;
}
return resolveWithin(path, myContentDir);
}
private StreamSource resolveWithin(String path, File dir)
throws PackageException
, NotExistException
{
LOG.fine("Trying to resolve ''{0}'' within ''{1}''", path, dir);
File f = new File(dir, path);
if ( ! f.exists() ) {
String msg = "File '" + f + "' does not exist";
LOG.fine(msg);
throw new NotExistException(msg);
}
try {
InputStream in = new FileInputStream(f);
StreamSource src = new StreamSource(in);
src.setSystemId(f.toURI().toString());
return src;
}
catch ( FileNotFoundException ex ) {
String msg = "File '" + f + "' exists but is not found";
LOG.severe(msg);
throw new PackageException(msg, ex);
}
}
private String getDirName()
{
return myPkgDir.getName();
}
private final String myRsrcName;
private final String myPkgAbbrev;
private File myPkgDir;
private File myContentDir;
}
}
/* ------------------------------------------------------------------------ */
/* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS COMMENT. */
/* */
/* The contents of this file are subject to the Mozilla Public License */
/* Version 1.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.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 Original Code is: all this file. */
/* */
/* The Initial Developer of the Original Code is Florent Georges. */
/* */
/* Contributor(s): none. */
/* ------------------------------------------------------------------------ */