/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library 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;
* version 2.1 of the License.
*
* This library 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.
*/
package org.geotoolkit.internal.io;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.HashMap;
import java.util.logging.Level;
import java.lang.ref.PhantomReference;
import org.apache.sis.util.Disposable;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.nio.IOUtilities;
import org.geotoolkit.resources.Loggings;
import org.geotoolkit.internal.ReferenceQueueConsumer;
/**
* Manages temporary files created by the Geotk library. This class provides a {@link #createTempFile}
* method which similar to the one provided in the standard {@link File} class, except that the file
* may be deleted earlier. More specifically if the {@link File} object has been garbage-collected,
* then the corresponding file in the file system is deleted immediately instead than waiting for
* the JVM shutdown.
*
* {@note This class extends <code>PhantomReference</code> for implementation convenience.
* Callers should ignore that detail.}
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.16
*
* @since 3.03
* @module
*/
public final class TemporaryFile extends PhantomReference<Path> implements Disposable {
/**
* The temporary files not yet deleted. Keys are the string returned by {@link File#getPath()}.
*/
private static final Map<String,TemporaryFile> REFERENCES = new HashMap<>();
/**
* The temporary directory, or {@code null} if none.
*/
private static Path sharedTemporaryDirectory;
/**
* Registers a shutdown hook which will delete every files not yet deleted.
*/
static {
try {
Class.forName("org.geotoolkit.factory.ShutdownHook", true, TemporaryFile.class.getClassLoader())
.getMethod("registerFileDeletor", Runnable.class).invoke(null, new Runnable() {
@Override public void run() {
while (TemporaryFile.deleteAll()) {
Thread.yield();
// The loop exists as a paranoiac action in case TemporaryFile.deleteOnExit(...)
// is being invoked concurrently, but it should never happen.
}
}
});
} catch (Exception e) {
Logging.unexpectedException(null, TemporaryFile.class, "<init>", e);
}
}
/**
* The path to the file to delete. We do not keep directly a reference to {@link File}
* since we want to allow the garbage collector to collect the file. Note that Sun
* implementation of {@link File#deleteOnExit()} in JDK 6 also retains the path instead
* than the {@code File}.
*/
private final String path;
/**
* Creates a new reference to a temporary file.
*/
private TemporaryFile(final Path file) {
super(file, ReferenceQueueConsumer.DEFAULT.queue);
path = file.toString();
}
/**
* Returns the temporary directory.
*
* @return The temporary directory to use.
*/
public static synchronized Path getSharedTemporaryDirectory() {
Path directory = sharedTemporaryDirectory;
if (directory == null) {
directory = Paths.get(System.getProperty("java.io.tmpdir", "/tmp"), "Geotoolkit.org");
if (!Files.isDirectory(directory)) {
try {
Files.createDirectories(directory);
} catch (IOException e) {
// If we can't create the Geotoolkit subdirectory,
// stay in the usual tmp directory.
directory = directory.getParent();
}
}
sharedTemporaryDirectory = directory;
}
return directory;
}
/**
* Creates a new temporary file and register it immediately for deletion on JVM shutdown.
*
* @param prefix The prefix string to be used in generating the file's name.
* @param suffix The suffix string to be used in generating the file's name
* (usually with a leading dot), or {@code null} for {@code ".tmp"}.
* @param directory The directory in which the file is to be created, or {@code null}.
* @return The temporary file.
* @throws IOException If the file can not be created.
*/
public static Path createTempFile(String prefix, String suffix, Path directory) throws IOException {
Path tmpFile;
if (directory == null) {
tmpFile = Files.createTempFile(prefix, suffix);
} else {
tmpFile = Files.createTempFile(directory, prefix, suffix);
}
final TemporaryFile ref = new TemporaryFile(tmpFile);
synchronized (REFERENCES) {
if (REFERENCES.put(ref.path, ref) != null) {
// Should never happen since File.createTempFile ensures unique filename.
// Not that there is a slight risks that this failure happens if the user
// invoked File.delete() instead than TemporaryFile.delete(tmpFile).
throw new AssertionError(ref);
}
}
return tmpFile;
}
/**
* Deletes the given temporary file. This method should be invoked instead of
* {@link File#delete()} in order to unregister the given file.
*
* @param file The file to delete.
* @return {@code true} if the file has been deleted.
*/
public static boolean delete(final File file) {
synchronized (REFERENCES) {
final TemporaryFile ref = REFERENCES.remove(file.getPath());
if (ref != null) {
ref.clear();
}
}
return file.delete(); // Must be after the removal from the list.
}
/**
* Deletes the given temporary file. This method should be invoked instead of
* {@link File#delete()} in order to unregister the given file.
*
* @param file The file to delete.
* @return {@code true} if the file has been deleted.
*/
public static boolean delete(final Path file) {
synchronized (REFERENCES) {
final TemporaryFile ref = REFERENCES.remove(file.toString());
if (ref != null) {
ref.clear();
}
}
return IOUtilities.deleteSilently(file);
}
/**
* Deletes the current file. This is invoked by {@link #dispose()}
* when a {@link File} object has been garbage-collected.
*
* @return {@code true} if the file has been successfully deleted.
*/
private boolean delete() {
synchronized (REFERENCES) {
if (REFERENCES.remove(path) == this) {
final Path filePath = Paths.get(this.path);
IOUtilities.deleteSilently(filePath);
return new File(this.path).delete();
}
}
return false; // Already deleted by the shutdown hook.
}
/**
* Deletes every files, no matter if they have been garbage-collected or not.
* This method should be invoking during shutdown only.
*
* @return {@code true} if at least one file has been successfully deleted.
*/
public static boolean deleteAll() {
boolean deleted = false;
final Map<String,TemporaryFile> references = REFERENCES;
if (references != null) { // Safety check against weird behavior at shutdown time.
synchronized (references) {
for (final TemporaryFile ref : references.values()) {
deleted |= IOUtilities.deleteSilently(Paths.get(ref.path));
ref.clear();
}
references.clear();
}
}
return deleted;
}
/**
* Deletes this file. This method is invoked automatically when
* a {@link File} object has been garbage-collected.
*/
@Override
public void dispose() {
if (delete()) {
/*
* Logs the message at the WARNING level because execution of this code
* means that the application failed to delete itself the temporary file.
*/
Logging.log(TemporaryFile.class, "delete",
Loggings.format(Level.WARNING, Loggings.Keys.TemporaryFileGc_1, this));
}
}
/**
* Returns the file path.
*
* @return The file path.
*/
@Override
public String toString() {
return path;
}
}