package com.mastfrog.healthtracker;
import com.google.inject.name.Named;
import com.mastfrog.acteur.resources.ExpiresPolicy;
import com.mastfrog.acteur.resources.FileResources;
import com.mastfrog.acteur.resources.MimeTypes;
import com.mastfrog.acteur.resources.StaticResources;
import com.mastfrog.giulius.DeploymentMode;
import com.mastfrog.giulius.ShutdownHookRegistry;
import com.mastfrog.settings.Settings;
import com.mastfrog.util.GUIDFactory;
import com.mastfrog.util.Streams;
import io.netty.buffer.ByteBufAllocator;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.xeustechnologies.jtar.TarEntry;
import org.xeustechnologies.jtar.TarInputStream;
/**
* Looks up static html files to be served by acteur-resources and provides an
* instance of {@link StaticResources} over them. Note you will need to bind
* {@link StaticResources.Resource} in the request scope for this to work. Uses
* the following algorithm:
* <ul>
* <li>Looks up the value of SETTINGS_KEY_JAR_RELATIVE_FOLDER_NAME
* ("html.jar.relative.path") in the settings, defaulting it to
* DEFAULT_JAR_RELATIVE_FOLDER_NAME ("html").</li>
* <li>Searches for that folder relative to the location of the JAR file or
* classes directory the passed class lives in, as returned by
* <code>application.getClass().getProtectionDomain().getCodeSource().getLocation()</code>.
* This makes it possible to run an application during development while editing
* html files live.</li>
* <li>If no such folder exists, look in settings for SETTINGS_KEY_HTML_PATH
* ("html.path") which should be a path to a folder on disk where the html files
* to serve already are.</li>
* <li>If the key is not set or it is set but the folder does not exist, look
* for an gzipped tar archive of HTML files (you can create one with the maven
* assembly plugin) in the same package as the application class - by default it
* will look for <code>html-files.tar.gz</code> in that package, or you can set
* the name using the settings key SETTINGS_KEY_HTML_ARCHIVE_TAR_GZ_NAME
* ("archive.tar.gz.name") - the file <em>must</em> have a <code>.tar.gz</code>
* extension. If that is found:
* <ul>
* <li>Create a directory <code>html-$RANDOM_ID</code> in the system temporary
* directory</li>
* <li>Unpack the archive there and serve those files</li>
* </ul>
* If no archive is found and no place to serve files from as found an
* IOException will be thrown (if you want to make serving files optional, don't
* bind StaticResources or add ResourcePage to your application - do that
* conditionally in that case).
* </li>
*
* @author Tim Boudreau
*/
@Singleton
public class MarkupFiles implements Provider<StaticResources> {
private final StaticResources resources;
/**
* Settings key for an explicit path to a directory containing markup files.
*/
public static final String SETTINGS_KEY_HTML_PATH = "html.path";
/**
* Settings key for the path relative to location of the project or jar file
* shich, if it exists, should be used for markup.
*/
public static final String SETTINGS_KEY_JAR_RELATIVE_FOLDER_NAME = "html.jar.relative.path";
public static final String DEFAULT_JAR_RELATIVE_FOLDER_NAME = "html";
/**
* Name of an archive of markup files which should be used if no folder can
* be found relative to the project.
*/
public static final String SETTINGS_KEY_HTML_ARCHIVE_TAR_GZ_NAME = "archive.tar.gz.name";
public static final String DEFAULT_HTML_ARCHIVE_TAR_GZ_NAME = "html-files";
/**
* Name for the @Named binding of a <code>Class</code> object which
* should be used for locating the html directory and the markup archive.
*/
public static final String GUICE_BINDING_CLASS_RELATIVE_MARKUP = "markupRelativeTo";
private final ShutdownHookRegistry onShutdown;
@Inject
@SuppressWarnings("unchecked")
public MarkupFiles(@Named(GUICE_BINDING_CLASS_RELATIVE_MARKUP) Class type, Settings settings, MimeTypes types, DeploymentMode mode, ByteBufAllocator allocator, ExpiresPolicy policy, ShutdownHookRegistry onShutdown) throws Exception {
String jarRelativeFolderName = settings.getString(SETTINGS_KEY_JAR_RELATIVE_FOLDER_NAME, DEFAULT_JAR_RELATIVE_FOLDER_NAME);
this.onShutdown = onShutdown;
// Find where we're running from and try to look up ../html
File file = findFolderRelativeToJAR(type, jarRelativeFolderName);
// If that isn't there, see if there is a setting html.path that
// points to it
if (file == null) {
String markupPath = settings.getString(SETTINGS_KEY_HTML_PATH);
if (markupPath != null) {
File f = new File(markupPath);
if (f.exists() && f.isDirectory()) {
file = f;
}
}
}
// If that fails, unpack the embedded archive of html into a unique
// subdir of /tmp and serve from there
if (file == null) {
String archiveName = settings.getString(SETTINGS_KEY_HTML_ARCHIVE_TAR_GZ_NAME, DEFAULT_HTML_ARCHIVE_TAR_GZ_NAME);
file = unpackMarkupArchive(type, archiveName);
}
resources = new FileResources(file, types, mode, allocator, settings, policy);
}
public File findFolderRelativeToJAR(Class<?> jarClass, String relPath) throws URISyntaxException, IOException {
// Get the location this JAR is in on disk, to set up paths relative to it
ProtectionDomain protectionDomain = jarClass.getProtectionDomain();
URL location = protectionDomain.getCodeSource().getLocation();
// See if an explicitly provided assets folder was passed to us; if
// not we will look for an "assets" folder belonging to this application
File codebaseDir = new File(location.toURI()).getParentFile();
if ("target".equals(codebaseDir.getName()) || "build".equals(codebaseDir.getName())) {
codebaseDir = codebaseDir.getParentFile();
}
File f = new File(codebaseDir, relPath);
File result = f.exists() && f.isDirectory() ? f : null;
if (result == null) {
result = new File(codebaseDir, "src" + File.separator + "main" + File.separator + relPath);
if (!result.exists() || !result.isDirectory()) {
result = null;
}
}
if (result != null) {
System.err.println("Using markup files in " + result.getAbsolutePath());
}
return result;
}
private final File unpackMarkupArchive(Class<?> relativeTo, String archiveName) throws IOException, FileNotFoundException {
File tmp = new File(System.getProperty("java.io.tmpdir"));
String uniq = GUIDFactory.get().newGUID(1, 7);
if (!archiveName.endsWith(".tar.gz")) {
archiveName += ".tar.gz";
}
File destDir = new File(tmp, "html-" + uniq);
System.err.println("Decompressing markup archive to " + destDir);
try (InputStream in = relativeTo.getResourceAsStream(archiveName)) {
if (in == null) {
throw new IOException("Markup files missing from archive: " + archiveName + " in " + relativeTo.getPackage().getName().replace('.', '/'));
}
if (!destDir.mkdirs()) {
throw new IOException("Could not create " + destDir.getAbsolutePath());
}
unTar(in, destDir);
}
return destDir;
}
@Override
public StaticResources get() {
return resources;
}
private List<File> unTar(InputStream raw, final File outputDir) throws FileNotFoundException, IOException {
final List<File> untaredFiles = new LinkedList<>();
GZIPInputStream is = new GZIPInputStream(raw);
try (TarInputStream debInputStream = new TarInputStream(is)) {
TarEntry entry = null;
while ((entry = (TarEntry) debInputStream.getNextEntry()) != null) {
final File outputFile = new File(outputDir, entry.getName());
if (entry.isDirectory()) {
if (!outputFile.exists()) {
if (!outputFile.mkdirs()) {
throw new IllegalStateException(String.format("Couldn't create directory %s.", outputFile.getAbsolutePath()));
}
}
} else {
try (OutputStream outputFileStream = new FileOutputStream(outputFile)) {
Streams.copy(debInputStream, outputFileStream);
}
outputFile.setLastModified(entry.getModTime().getTime());
}
untaredFiles.add(outputFile);
}
}
try {
onShutdown.add(new MarkupDeleter(outputDir, untaredFiles));
} catch (Exception e) {
e.printStackTrace();
}
return untaredFiles;
}
static class MarkupDeleter implements Runnable {
private final File markupDir;
private final Set<File> files;
public MarkupDeleter(File markupDir, List<File> files) {
this.markupDir = markupDir;
this.files = new HashSet<>(files);
}
@Override
public void run() {
System.err.println("Deleting unarchived markup in " + markupDir);
delete(markupDir);
}
private void delete(File file) {
if (file.isFile()) {
if (files.contains(file)) {
if (!file.delete()) {
System.err.println("Could not delete " + file);
}
}
} else if (file.isDirectory()) {
for (File f : file.listFiles()) {
delete(f);
}
file.delete();
}
}
}
}