package org.epics.archiverappliance.utils.nio;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.spi.FileSystemProvider;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.log4j.Logger;
/**
* This is a replacement for NIO Paths that caters to our syntax rules.
* Normal file system paths are all similar to linux file names.
* The compressed use the jar:file:syntax.
* For example, <code>jar:file:///ziptest/alltext.zip!/SomeTextFile.txt</code>
*
* @author luofeng
* @author mshankar
*
*/
public class ArchPaths implements Closeable {
public static final String ZIP_PREFIX = "jar:file://";
private static Logger logger = Logger.getLogger(ArchPaths.class.getName());
private static FileSystemProvider zipFSProvider = getZipFSProvider();
private ConcurrentHashMap<String, FileSystem> fileSystemList = new ConcurrentHashMap<String, FileSystem>();
/**
* Return a path based on a varargs list of path components.
* Each path component is separated by the file separator.
* @param first
* @param more
* @return Path
* @throws IOException
*/
public Path get(String first, String... more) throws IOException {
return get(false, first, more);
}
/**
* Return a path based on a varargs list of path components.
* Each path component is separated by the file separator.
* @param createParent If this is true, we create the parent automatically when requesting the path
* @param first
* @param more
* @return Path
* @throws IOException
*/
public Path get(boolean createParent, String first, String... more) throws IOException {
String path;
if (more.length == 0) {
path = first;
} else {
StringBuilder sb = new StringBuilder();
sb.append(first);
for (String segment: more) {
if (segment.length() > 0) {
if (sb.length() > 0) {
sb.append('/');
}
sb.append(segment);
}
}
path = sb.toString();
}
return this.get(path, createParent);
}
/**
* @param uriPathOrDefautFilePath
* @return Path
* @throws IOException
*/
public Path get(String uriPathOrDefautFilePath) throws IOException {
return get(uriPathOrDefautFilePath, false);
}
/**
* Return a path based on the full URI representation of the path.
*
* @param uriPathOrDefautFilePath
* @param createParent If this is true, we create the parent automatically when requesting the path
* @return Path
* @throws IOException
*/
public Path get(String uriPathOrDefautFilePath, boolean createParent) throws IOException {
if(uriPathOrDefautFilePath.startsWith(ZIP_PREFIX)){
// We are dealing with zip files here.
int sep=uriPathOrDefautFilePath.indexOf("!/");
if (sep == -1){
int sep2=uriPathOrDefautFilePath.indexOf(ZIP_PREFIX);
if(sep2!=-1){
String defaultFile=uriPathOrDefautFilePath.substring(sep2+11);
return FileSystems.getDefault().getPath(defaultFile);
}else{
IOException e=new IOException("the uri path doesn't include the file path in zip fie. the url path should be like these:" +
" jar:file:///D:/ziptest/alltext.zip!/SomeTextFile.txt or jar:file:///ziptest/alltext.zip!/SomeTextFile.txt,or jar:file:///ziptest/, but your path is "+uriPathOrDefautFilePath);
logger.error("the uri path doesn't include the file path in zip fie. the url path should be like these:" +
" jar:file:///D:/ziptest/alltext.zip!/SomeTextFile.txt or jar:file:///ziptest/alltext.zip!/SomeTextFile.txt,or jar:file:///ziptest/, but your path is "+uriPathOrDefautFilePath,e);
throw e;
}
}
if(logger.isDebugEnabled()) logger.debug("Asking for " + uriPathOrDefautFilePath + (createParent ? " with and option to create parent folder" : " without the option to create the parent folder"));
String zipPathStr = uriPathOrDefautFilePath.substring(ZIP_PREFIX.length(), sep);
if(logger.isDebugEnabled()) logger.debug("The path to the zip file is " + zipPathStr);
String innerFilePath = uriPathOrDefautFilePath.substring(sep+1);
if(logger.isDebugEnabled()) logger.debug("The path to the file within the zip file is " + innerFilePath);
FileSystem zipfs = null;
if(fileSystemList.get(zipPathStr) != null) {
logger.debug("We already have the zip file open in this context " + zipPathStr);
zipfs = fileSystemList.get(zipPathStr);
} else {
Path zipPath = FileSystems.getDefault().getPath(zipPathStr);
if(!Files.exists(zipPath)) {
if(createParent) {
Files.createDirectories(zipPath.getParent());
logger.debug("Creating the zip file.");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
zipfs = zipFSProvider.newFileSystem(zipPath, env);
if(zipfs == null) throw new IOException("Unable to get a new file system from the provider.");
fileSystemList.put(zipPathStr, zipfs);
} else {
throw new NoSuchFileException("The zip file " + zipPathStr + " does not exist and we do not have the createParent set to true");
}
} else {
Map<String, String> env = new HashMap<>();
env.put("create", "false");
zipfs = zipFSProvider.newFileSystem(zipPath, env);
if(zipfs == null) throw new IOException("Unable to get a new file system from the provider.");
fileSystemList.put(zipPathStr, zipfs);
}
}
Path pathWithinZipFile = zipfs.getPath(innerFilePath);
Path parent = pathWithinZipFile.getParent();
if (createParent && parent != null && Files.notExists(parent)) {
if(logger.isDebugEnabled()) logger.debug("Creating parent folder " + parent.toString());
Files.createDirectories(parent);
}
return pathWithinZipFile;
} else {
if(uriPathOrDefautFilePath.contains(":") && uriPathOrDefautFilePath.indexOf(":") < uriPathOrDefautFilePath.indexOf("/") && !uriPathOrDefautFilePath.startsWith("file:///")) {
try {
URI uri = new URI(uriPathOrDefautFilePath);
FileSystem fs = FileSystems.newFileSystem(uri, System.getenv(), Thread.currentThread().getContextClassLoader());
Path normalFilePath = fs.getPath(uri.getPath());
Path parent = normalFilePath.getParent();
if (createParent && parent != null && Files.notExists(parent)) {
if(logger.isDebugEnabled()) logger.debug("Creating parent folder " + parent.toString());
Files.createDirectories(parent);
}
return normalFilePath;
} catch(URISyntaxException ex) {
throw new IOException(ex);
}
} else {
// We are dealing with normal file system paths.
Path normalFilePath = FileSystems.getDefault().getPath(uriPathOrDefautFilePath);
Path parent = normalFilePath.getParent();
if (createParent && parent != null && Files.notExists(parent)) {
if(logger.isDebugEnabled()) logger.debug("Creating parent folder " + parent.toString());
Files.createDirectories(parent);
}
return normalFilePath;
}
}
}
/**
* Returns a seekable byte channel.
* In case of file systems, this is the raw SeekableByteChannel as returned by the provider.
* In case of zip files, we wrap the InputStream using WrappedSeekableByteChannel (which is a read only byte channel for now).
* @param path Path
* @param options OpenOption
* @return a new seekabel byte channel
* @throws IOException
*/
public static SeekableByteChannel newByteChannel(Path path, OpenOption...options) throws IOException {
String pathURI = path.toUri().toString();
if(pathURI.startsWith(ZIP_PREFIX)) {
return new WrappedSeekableByteChannel(path);
} else {
return Files.newByteChannel(path, options);
}
}
private static FileSystemProvider getZipFSProvider() {
for (FileSystemProvider provider : FileSystemProvider.installedProviders()) {
if ("jar".equals(provider.getScheme()))
return provider;
}
logger.fatal("For some reason we do not have the zip file system provider on this JVM.");
return null;
}
@Override
public void close() throws IOException {
for(String key:fileSystemList.keySet()){
if(logger.isDebugEnabled()) logger.debug("Closing file system for " + key);
fileSystemList.get(key).close();
}
}
/**
* The path used for backing up the data using ETL.
* When enabling compression with packing, we backup the packed file itself as opposed to the files within the packed file.
* @param path Path
* @return Path for backup
* @throws IOException
*/
public String getPathForBackup(Path path) throws IOException {
String uriStr = path.toUri().toString();
if(uriStr.startsWith(ZIP_PREFIX)) {
int sep = uriStr.indexOf("!/");
if(sep == -1) {
throw new IOException("Malformed URI :" + uriStr);
}
String zipPathStr = uriStr.substring(ZIP_PREFIX.length(), sep);
return zipPathStr;
} else {
// For regular file systems, the container path is the same as the regular path
return path.toFile().getAbsolutePath();
}
}
}