package io.github.xhanin.jarup;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Enumeration;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.newBufferedReader;
import static java.nio.file.Files.newBufferedWriter;
/**
* Date: 10/1/14
* Time: 17:40
*/
public class WorkingCopy implements AutoCloseable {
private static final int BUFFER = 2048;
private static final long TS = System.currentTimeMillis();
private static final long R = new Random().nextLong();
private static final AtomicLong C = new AtomicLong();
private static final String MANIFEST = JarFile.MANIFEST_NAME;
private static final String MANIFEST_DIR = "META-INF/";
public static WorkingCopy prepareFor(Path jarPath) throws IOException {
String id = TS + "-" + R + "-" + C.incrementAndGet();
File root = new File(System.getProperty("java.io.tmpdir") + "/jarup/" + id + "/" + jarPath.getFileName());
unzip(jarPath, root);
return new WorkingCopy(jarPath, root);
}
private final Path jarPath;
private final File root;
private boolean updated;
private WorkingCopy(Path jarPath, File root) {
this.jarPath = jarPath;
this.root = root;
}
public String readFile(String filePath, String encoding) throws IOException {
return IOUtils.toString(getFile(filePath, FileOperation.READ), Charset.forName(encoding));
}
// for tests only
File getFile(String filePath) {
return new File(root, filePath);
}
public void deleteFile(String path) throws IOException {
getFile(path, FileOperation.DELETE);
}
private File getFile(String filePath, FileOperation operation) throws IOException {
Path archiveRoot = root.toPath();
String entryName = filePath;
while (filePath.contains(":/")) {
String subPath = filePath.substring(0, filePath.indexOf(":/"));
Path explodedPath = getExplodedPath(root.toPath().resolve(subPath));
if (!explodedPath.toFile().exists()) {
unzip(root.toPath().resolve(subPath), explodedPath.toFile());
}
archiveRoot = explodedPath;
entryName = filePath.substring(filePath.indexOf(":/") + 2);
filePath = root.toPath().relativize(explodedPath) + "/" + entryName;
}
File file = new File(root, filePath);
if (operation.requiresUpdate()) {
updated = true;
operation.updateEntry(file, archiveRoot, entryName);
}
return file;
}
private static Path getExplodedPath(Path file) {
return file.resolveSibling(file.getFileName().toString() + ".$");
}
private static void addEntry(Path archiveRoot, String entryName) throws IOException {
try (BufferedWriter entries = newBufferedWriter(entriesPath(archiveRoot),
UTF_8, StandardOpenOption.APPEND)) {
entries.write(entryName(archiveRoot.resolve(entryName), entryName));
entries.newLine();
}
}
private static void removeEntry(Path archiveRoot, String entryName) throws IOException {
Path path = entriesPath(archiveRoot);
File temp = Files.createTempFile(archiveRoot, "temp", "jarup").toFile();
try (BufferedReader reader = newBufferedReader(path, UTF_8);
BufferedWriter writer = newBufferedWriter(temp.toPath(), UTF_8)) {
String entryLine = entryName(archiveRoot.resolve(entryName), entryName);
String currentLine;
while ((currentLine = reader.readLine()) != null) {
String current = currentLine.trim();
if (current.equals(entryLine)) {
continue;
}
writer.write(current);
writer.newLine();
}
}
File target = path.toFile();
if (!temp.renameTo(target)) {
throw new FileSystemException(
String.format("Could not copy temporary entries files <%s> to <%s>", temp.getPath(), target.getPath())
);
}
}
public WorkingCopy writeFile(String path, String encoding, String content) throws IOException {
File file = getFile(path, FileOperation.UPDATE);
IOUtils.write(file, Charset.forName(encoding), content);
return this;
}
public WorkingCopy copyFileFrom(String from, String to) throws IOException {
File toFile = getFile(to, FileOperation.UPDATE);
mkdir(toFile.getParentFile());
Files.copy(Paths.get(from), toFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
return this;
}
public WorkingCopy copyFileTo(String from, String to) throws IOException {
File toFile = Paths.get(to).toFile();
mkdir(toFile.getParentFile());
Files.copy(getFile(from, FileOperation.UPDATE).toPath(), toFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
return this;
}
public String getDefaultCharsetFor(String path) {
if (path.endsWith(".properties")) {
return "ISO-8859-1";
} else {
return "UTF-8";
}
}
@Override
public void close() throws Exception {
if (updated) {
zip(root, jarPath);
}
IOUtils.delete(root);
}
private static void zip(final File from, Path to) throws IOException {
try (ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(to.toFile())))) {
final Path root = from.toPath();
// repackaging uncompressed archives
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (!root.equals(dir) && dir.getFileName().toString().endsWith(".$")) {
Path dest = dir.resolveSibling(dir.getFileName().toString().replaceAll("\\.\\$$", ""));
zip(dir.toFile(), dest);
IOUtils.delete(dir.toFile());
return FileVisitResult.SKIP_SUBTREE;
} else {
return FileVisitResult.CONTINUE;
}
}
});
// process all entries as listed in entries file
for (String entry : Files.readAllLines(entriesPath(root), UTF_8)) {
addZipEntry(out, root, root.resolve(entry));
}
}
}
/*
* Adds a new file entry to the ZIP output stream.
*/
private static void addZipEntry(ZipOutputStream out, Path root, Path path) throws IOException {
String name = entryName(root, path);
File file = path.toFile();
boolean isDir = file.isDirectory();
if (name.equals("") || name.equals(".")) {
return;
}
long size = isDir ? 0 : file.length();
ZipEntry e = new ZipEntry(name);
e.setTime(file.lastModified());
if (size == 0) {
e.setMethod(ZipEntry.STORED);
e.setSize(0);
e.setCrc(0);
}
out.putNextEntry(e);
if (!isDir) {
byte[] buf = new byte[1024];
int len;
try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
while ((len = is.read(buf, 0, buf.length)) != -1) {
out.write(buf, 0, len);
}
}
}
out.closeEntry();
}
private static String entryName(Path root, Path path) {
String name = root.relativize(path).toString();
return entryName(path, name);
}
private static String entryName(Path path, String name) {
name = name.replace(File.separatorChar, '/');
if (Files.isDirectory(path)) {
name = name.endsWith(File.separator) ? name :
(name + File.separator);
}
return name;
}
private static void unzip(Path from, File to) throws IOException {
mkdir(to);
try (ZipFile zip = new ZipFile(from.toFile());
BufferedWriter entries = newBufferedWriter(entriesPath(to.toPath()), UTF_8)
) {
Enumeration zipFileEntries = zip.entries();
while (zipFileEntries.hasMoreElements()) {
ZipEntry entry = (ZipEntry) zipFileEntries.nextElement();
entries.write(entry.getName());
entries.newLine();
String currentEntry = entry.getName();
File destFile = new File(to, currentEntry);
mkdir(destFile.getParentFile());
if (!entry.isDirectory()) {
try (BufferedInputStream is = new BufferedInputStream(zip.getInputStream(entry));
BufferedOutputStream dest = new BufferedOutputStream(new FileOutputStream(destFile), BUFFER)) {
byte data[] = new byte[BUFFER];
int currentByte;
while ((currentByte = is.read(data, 0, BUFFER)) != -1) {
dest.write(data, 0, currentByte);
}
}
} else {
mkdir(destFile);
}
destFile.setLastModified(entry.getTime());
}
}
}
private static Path entriesPath(Path root) {
return root.resolve("___jarup___entries");
}
private static void mkdir(File to) throws IOException {
if (to == null) {
return;
}
if (to.exists()) {
if (!to.isDirectory()) {
throw new IOException("can't create directory " + to.getAbsolutePath() + ": a file of same name already exists");
} else {
return;
}
}
if (!to.mkdirs()) {
throw new IOException("can't create directory " + to.getAbsolutePath());
}
}
private enum FileOperation {
READ {
@Override
public boolean requiresUpdate() {
return false;
}
@Override
public void updateEntry(File file, Path archiveRoot, String entryName) {
throw new UnsupportedOperationException();
}
},
UPDATE {
@Override
public boolean requiresUpdate() {
return true;
}
@Override
public void updateEntry(File file, Path archiveRoot, String entryName) throws IOException {
if (!file.exists()) {
addEntry(archiveRoot, entryName);
}
}
},
DELETE {
@Override
public boolean requiresUpdate() {
return true;
}
@Override
public void updateEntry(File file, Path archiveRoot, String entryName) throws IOException {
if (file.exists() && file.delete()) {
removeEntry(archiveRoot, entryName);
}
}
};
public abstract boolean requiresUpdate();
public abstract void updateEntry(File file, Path archiveRoot, String entryName) throws IOException;
}
}