package org.rr.commons.mufs;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.regex.Pattern;
import javax.swing.filechooser.FileSystemView;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.rr.commons.log.LoggerFactory;
import org.rr.commons.utils.ListUtils;
/**
* The {@link FileResourceHandler} is able to handle all resources
* located in the file system.
*/
class FileResourceHandler extends AResourceHandler {
private static final String[] PATH_SEGMENT_SEPARATORS = new String[] { "/", "\\", File.separator};
/**
* The file url identifier.
*/
private static final String FILE_URL = "file:";
/**
* The resource String parsed into a file.
*/
private String resourceString;
/**
* the file to be handled with this {@link FileResourceHandler} instance.
*/
private File file;
/**
* The {@link FileResourceHandler} for the parent directory.
*/
private FileResourceHandler parentFileresourceLoader = null;
private Boolean isFloppyDrive = null;
private Boolean isDirectory = null;
/**
* Provide a shared FileSystemView but note that it's not synchronized.
*/
private static final FileSystemView fileSystemViewInstance = FileSystemView.getFileSystemView();
FileResourceHandler() {
super();
}
FileResourceHandler(File f) {
super();
this.setFile(f);
this.resourceString = normalizeDirectoryResourceString(f.getPath(), f);
}
/**
* Gets the file which is set to this {@link FileResourceHandler} instance.
*
* @return The desired file or <code>null</code> if no file was set.
*/
File getFile() {
return file;
}
/**
* Sets the {@link File} to be handled to the resource loader.
* @param file The file to be set.
*/
void setFile(File file) {
this.file = file;
}
@Override
public void refresh() {
super.refresh();
}
/**
* Tests if the given resource is a valid file system file.
*/
@Override
public boolean isValidResource(final String resourceString) {
if(resourceString.startsWith(FILE_URL)) {
try {
URLDecoder.decode(resourceString, "UTF-8");
} catch (UnsupportedEncodingException e) {
return false;
}
return true;
}
try {
if(isUnixFilePath(resourceString)) {
return true;
} else if (isWindowsFilePath(resourceString)) {
return true;
} else if (new File(resourceString).exists()) {
return true;
}
return false;
} catch (Exception e) {
return false;
}
}
/**
* Test if the given resource string match to a unix file- or file system path.
* @param resourceString A resource string to be tested if it look like a unix path
* @return <code>true</code> if the given resourceString is a unix path or <code>false</code> otherwise.
*/
private boolean isUnixFilePath(String resourceString) {
return resourceString.startsWith("/") && resourceString.indexOf("//")==-1 && resourceString.indexOf("\\\\")==-1 && !resourceString.startsWith("\\");
}
/**
* Tells if the given resource string matches to a valid windows path or file name.
* @param resourceString The resource string to be tested. Following examples matches with <code>true</code>.
* c:
* c:\
* c:\nv6vsa76A5v
* c:\nv6vsa76A5v\
* c:\nv6vsa76A5v\hvsdav
* c:\nv6vsa76A5v\hvsdav\hvsdav\hvsdav\hvsdav\
* c:\nv6vsa76A5v\hvsdav\hvsdav\hvsdav\hvsdav\web.config
* C:\abc\aabc6675bnvs.thgcsdcbsd
* d:\folder1\folder1\web.config
* d:\folder1\folder1\1.txt
*
* Not Accept
* C
* C::
* C:\\
* C:\abc\\
* C:\abc\ab%g
* C:\abc\aabc.txt\
* C:\abc\aabc.txt\ab
* @return <code>true</code> if the given resource matches to a windows file and <code>false</code> otherwise.
*/
private boolean isWindowsFilePath(String resourceString) {
return Pattern.matches("^[a-zA-Z]:\\\\.*", resourceString);
}
/**
* Creates a new {@link FileResourceHandler} instance for the given
* resource.
*
* @param resourceString The resource to be loaded.
*/
@Override
public IResourceHandler createInstance(String resourceString) {
final FileResourceHandler result = new FileResourceHandler();
if(resourceString.startsWith(FILE_URL)) {
String fileUrlResource = resourceString.substring(FILE_URL.length());
if(fileUrlResource.indexOf('%') != -1) {
try {
fileUrlResource = URLDecoder.decode(fileUrlResource, "UTF-8");
} catch (UnsupportedEncodingException e) {
LoggerFactory.getLogger().log(Level.SEVERE, "Failed to decode file string " + fileUrlResource);
}
}
result.setFile(new File(fileUrlResource));
} else {
result.setFile(new File(resourceString));
}
result.resourceString = normalizeDirectoryResourceString(resourceString, result.file);
return result;
}
private String normalizeDirectoryResourceString(String resource, File file) {
if(file.isDirectory()) {
//normalize that a directory resource is always returned with a trailing slash / backslash
if(!StringUtils.endsWithAny(resource, PATH_SEGMENT_SEPARATORS)) {
resource = resource + File.separator;
}
}
return resource;
}
public String getResourceString() {
return resourceString;
}
/**
* Reads the file specified for this {@link FileResourceHandler} instance into
* a byte[].
*
* @return The byte content of the file.
*/
@Override
public synchronized byte[] getContent() throws IOException {
try {
this.cleanHeapIfNeeded(this.file.length());
return FileUtils.readFileToByteArray(this.file);
} catch(Error e) {
if(e instanceof OutOfMemoryError) {
System.gc();
try {Thread.sleep(100);} catch (InterruptedException e1) {}
}
}
return FileUtils.readFileToByteArray(this.file);
}
/**
* Creates a new {@link BufferedInputStream} for the file specified for this {@link FileResourceHandler} instance.
*
* @return The desired {@link InputStream}.
*/
@Override
public ResourceHandlerInputStream getContentInputStream() throws IOException {
this.cleanHeapIfNeeded(this.file.length());
try {
FileInputStream fIn = new FileInputStream(this.file);
ResourceHandlerInputStream buffIn = new ResourceHandlerInputStream(this, fIn) ;
return buffIn;
} catch (Exception e) {
throw new IOException(e);
}
}
/**
* Creates a new {@link BufferedOutputStream} for the file specified for this {@link FileResourceHandler} instance.
*
* @param append Tells if the data written to the {@link OutputStream} is appended to the file or the file is overwritten.
* @return The desired {@link OutputStream}
*/
@Override
public OutputStream getContentOutputStream(boolean append) throws IOException {
final FileOutputStream fOut = new FileOutputStream(this.file, append);
final BufferedOutputStream buffOut = new BufferedOutputStream(fOut);
return buffOut;
}
/**
* Tells if the file exists
*/
@Override
public boolean exists() {
if(this.isFloppyDrive()) {
return true;
}
if(this.file.exists()) {
return true;
}
synchronized(fileSystemViewInstance) {
return fileSystemViewInstance.isDrive(this.file);
}
}
@Override
public IResourceHandler getParentResource() {
//must not be synchronized because it's not intendant if the
//cached parent FileResourceLoader is overwritten by another invokement
//at the nearly same time.
File parentFile = this.file.getParentFile();
if(parentFile==null) {
return null;
}
if(this.parentFileresourceLoader == null) {
this.parentFileresourceLoader = (FileResourceHandler) this.createInstance(parentFile.getPath());
}
return this.parentFileresourceLoader;
}
@Override
public void writeStringContent(String content, String encoding) throws IOException {
FileUtils.writeStringToFile(file, content, encoding);
}
@Override
public boolean mkdirs() {
resetIsDirectoryEvaluation();
return this.file.mkdirs();
}
/**
* Deletes the file handled with this {@link FileResourceHandler} instance.
* @return <code>true</code> if and only if the file or directory is
* successfully deleted; <code>false</code> otherwise
* @throws IOException
*/
@Override
public void delete() throws IOException {
if(this.isFileResource() && this.exists()) {
Path path = Paths.get(file.getAbsolutePath());
Files.delete(path);
} else {
FileUtils.deleteDirectory(this.file);
}
if(this.exists()) {
throw new IOException("could not delete resource " + String.valueOf(this.file));
}
//no need to delete this later. It's already done.
ResourceHandlerFactory.removeTemporaryResource(this);
resetIsDirectoryEvaluation();
}
private void resetIsDirectoryEvaluation() {
this.isDirectory = null;
}
@Override
public boolean moveToTrash() throws IOException {
try {
if(!ResourceHandlerUtils.moveToTrash(this)) {
com.sun.jna.platform.FileUtils.getInstance().moveToTrash(new File[] { new File(this.toString()) });
}
} catch(Exception e) {
e.printStackTrace();
}
return !exists();
}
public String toString() {
return this.getResourceString();
}
public IResourceHandler[] listDirectoryResources(final boolean showHidden) throws IOException {
return listDirectoryResources(new ResourceNameFilter() {
@Override
public boolean accept(IResourceHandler loader) {
if(!showHidden) {
if(loader.getName().startsWith(".")) {
return false;
} else if(((FileResourceHandler)loader).file.isHidden()) {
return false;
}
}
return true;
}
});
}
/**
* Lists all files and folders which are children of this {@link IResourceHandler} instance.
*
* @return all child {@link IResourceHandler} instances.
*/
@Override
public IResourceHandler[] listResources(final ResourceNameFilter filter) throws IOException {
final ArrayList<IResourceHandler> resourceResult = new ArrayList<>();
IResourceHandler[] listFileResources = this.listFileResources();
IResourceHandler[] listDirectoryResources = this.listDirectoryResources();
for (int i = 0; i < listFileResources.length; i++) {
if(filter==null) {
resourceResult.add(listFileResources[i]);
} else if(filter.accept(listFileResources[i])) {
//attach the accepted resource loader to the result list.
resourceResult.add(listFileResources[i]);
}
}
for (int i = 0; i < listDirectoryResources.length; i++) {
if(filter==null) {
resourceResult.add(listDirectoryResources[i]);
} else if(filter.accept(listDirectoryResources[i])) {
//attach the accepted resource loader to the result list.
resourceResult.add(listDirectoryResources[i]);
}
}
IResourceHandler[] sortedFileResourceHandlers = ResourceHandlerUtils.sortResourceHandlers(resourceResult.toArray(new IResourceHandler[resourceResult.size()]), ResourceHandlerUtils.SORT_BY_NAME, true);
return sortedFileResourceHandlers;
}
/**
* Lists all {@link File}s which are children of this {@link IResourceHandler} instance
* and which are directories.
*
* @return all child {@link IResourceHandler} instances.
*/
@Override
public IResourceHandler[] listDirectoryResources(ResourceNameFilter filter) {
final ArrayList<IResourceHandler> result = new ArrayList<>();
synchronized(fileSystemViewInstance) {
File[] files = FileResourceHandler.fileSystemViewInstance.getFiles(this.file, false);
for (int i = 0; i < files.length; i++) {
if(files[i].isDirectory()) {
IResourceHandler resourceLoader;
resourceLoader = ResourceHandlerFactory.getResourceHandler(files[i].getPath());
if(resourceLoader != null) {
if(filter != null && filter.accept(resourceLoader)) {
result.add(resourceLoader);
} else if(filter == null) {
result.add(resourceLoader);
}
}
}
}
}
return ResourceHandlerUtils.sortResourceHandlers(result.toArray(new IResourceHandler[result.size()]), ResourceHandlerUtils.SORT_BY_NAME, true);
}
/**
* Lists all {@link File}s which are children of this {@link IResourceHandler} instance.
* @return all child {@link IResourceHandler} instances.
*/
@Override
public IResourceHandler[] listFileResources() {
final ArrayList<IResourceHandler> result = new ArrayList<>();
synchronized(fileSystemViewInstance) {
// File[] files = this.file.listFiles();
//did not list files with invalid charset under ubuntu
File[] files = FileResourceHandler.fileSystemViewInstance.getFiles(this.file, false);
for (int i = 0; i < files.length; i++) {
if(!files[i].isDirectory()) {
IResourceHandler resourceLoader;
resourceLoader = ResourceHandlerFactory.getResourceHandler(files[i].getPath());
if(resourceLoader!=null) {
result.add(resourceLoader);
}
}
}
}
IResourceHandler[] sortedResourceHandlers = ResourceHandlerUtils.sortResourceHandlers(result.toArray(new IResourceHandler[result.size()]), ResourceHandlerUtils.SORT_BY_NAME, true);
return sortedResourceHandlers;
}
/**
* Gets the list of shown (i.e. not hidden) files.
*
* @param showHidden <code>true</code> if hidden files should be also shown and <code>false</code> otherwise.
* @return All these {@link IResourceHandler} which have this {@link IResourceHandler} as parent.
* @throws IOException
*/
@Override
public IResourceHandler[] listFileResources(boolean showHidden) throws IOException {
if(this.file.isFile()) {
return new IResourceHandler[0];
}
synchronized(fileSystemViewInstance) {
File[] files = fileSystemViewInstance.getFiles(this.file, !showHidden);
List<IResourceHandler> resultResources = new ArrayList<>(files.length);
for (int i = 0; i < files.length; i++) {
if(files[i].isFile()) {
resultResources.add(ResourceHandlerFactory.getResourceHandler(files[i]));
}
}
return resultResources.toArray(new IResourceHandler[resultResources.size()]);
}
}
/**
* Tells if the {@link File} hanlded by this {@link IResourceHandler} instance
* is a directory or not.
* @return <code>true</code> if it's a directory and <code>false</code> otherwise.
*/
@Override
public boolean isDirectoryResource() {
//use the cached information
if(this.isDirectory!=null) {
return this.isDirectory.booleanValue();
}
//ask if the folder is a dir. File.isDirectory will
//trigger the floppy motor. This takes time not needed.
if(this.isFloppyDrive()) {
return true;
}
if(this.file.isDirectory()) {
return (this.isDirectory = Boolean.TRUE);
}
if(!this.file.isFile()) {
synchronized(fileSystemViewInstance) {
boolean isDrive = fileSystemViewInstance.isDrive(this.file);
return (this.isDirectory = Boolean.valueOf(isDrive));
}
}
return false;
}
/**
* Gets the name of the file without any kind of path statement handled by this {@link FileResourceHandler} instance.
*/
@Override
public String getName() {
final String fileName = this.file.getName();
//Drive a Win32ShellFolder returns an empty String.
if(fileName.length()==0) {
return this.toString();
}
return this.file.getName();
}
/**
* Sets some local fields to <code>null</code>. It's not really important
* that the dispose is invoked to this {@link FileResourceHandler} instance.
*/
@Override
public void dispose() {
// this.parentFileresourceLoader = null;
// this.file = null;
// this.resource = null;
}
/**
* Gets the length of the file handled by this {@link FileResourceHandler} instance.
* @return size in bytes or 0 if the file did not exists.
*/
@Override
public long size() {
final File file = this.getFile();
if(file.isDirectory()) {
return 0;
}
return file.length();
}
@Override
public boolean copyTo(IResourceHandler targetRecourceLoader, boolean overwrite) throws IOException {
if(targetRecourceLoader instanceof FileResourceHandler) {
if(this.isDirectoryResource() && !targetRecourceLoader.exists()) {
//copy the source directory to the not existing target directory
targetRecourceLoader.mkdirs();
FileUtils.copyDirectory(this.file, ((FileResourceHandler)targetRecourceLoader).file);
return true;
} if(this.isDirectoryResource() && targetRecourceLoader.isDirectoryResource()) {
//copy the source directory to the target directory
FileUtils.copyDirectory(this.file, ((FileResourceHandler)targetRecourceLoader).file);
return true;
} else if(!this.isDirectoryResource()) {
//test if the target file already exists.
if(!overwrite && targetRecourceLoader.exists() && !targetRecourceLoader.isDirectoryResource()) {
throw new IOException("file already exists");
}
//try to copy using fast nio copy.
try {
return this.nioCopyFile(this.file, ((FileResourceHandler)targetRecourceLoader).file, overwrite);
} catch (IOException e) {
//copy the file to the target directory resource
FileUtils.copyFile(this.file, ((FileResourceHandler)targetRecourceLoader).file);
return true;
}
} else {
throw new IOException("could not copy the directory "+this.getResourceString()+" over the file " + targetRecourceLoader.getResourceString());
}
}
//perform a slow stream copy.
OutputStream contentOutputStream = null;
try {
contentOutputStream = targetRecourceLoader.getContentOutputStream(false);
IOUtils.write(this.getContent(), contentOutputStream);
return true;
} finally {
IOUtils.closeQuietly(contentOutputStream);
}
}
@Override
public void moveTo(IResourceHandler targetRecourceLoader, boolean overwrite) throws IOException {
resetIsDirectoryEvaluation();
if(targetRecourceLoader instanceof FileResourceHandler) {
if(this.equals(targetRecourceLoader)) {
return;
}
if(!this.isDirectoryResource()) {
//test if the target file already exists.
if(!overwrite && targetRecourceLoader.exists() && !targetRecourceLoader.isDirectoryResource()) {
throw new IOException("file already exists " + targetRecourceLoader.getResourceString());
} else if(targetRecourceLoader.isDirectoryResource()) {
throw new IOException("target is not a file " + targetRecourceLoader.getResourceString());
}
boolean deleted = FileUtils.deleteQuietly(((FileResourceHandler) targetRecourceLoader).file);
if(!deleted && ((FileResourceHandler) targetRecourceLoader).file.exists()) {
throw new IOException("Deleting file " + targetRecourceLoader + " has failed.");
}
FileUtils.moveFile(this.file, ((FileResourceHandler) targetRecourceLoader).file);
return;
}
super.moveTo(targetRecourceLoader, overwrite);
return;
}
super.moveTo(targetRecourceLoader, overwrite);
return;
}
/**
* Fast copy using nio.
*
* @param sourceFile source file
* @param destFile target file
* @param overwrite <code>true</code> if overwriting exiting target.
* @throws IOException
*/
public boolean nioCopyFile(File sourceFile, File destFile, boolean overwrite) throws IOException {
long position = 0;
if(!overwrite && destFile.exists()) {
return false;
}
if (!destFile.exists()) {
destFile.createNewFile();
}
try (FileInputStream in = new FileInputStream(sourceFile);
FileChannel source = in.getChannel();
FileOutputStream out = new FileOutputStream(destFile);
FileChannel destination = out.getChannel()) {
destination.transferFrom(source, position, source.size());
return true;
}
}
/**
* The file or directory modification date.
*/
@Override
public Date getModifiedAt() {
return new Date(this.file.lastModified());
}
/**
* Gets a new {@link IResourceHandler} having the given relative path statement attached.
* @param statement A relative path statement to be attached.
* @return the desired {@link IResourceHandler}.
*/
@Override
public IResourceHandler addPathStatement(String statement) throws ResourceHandlerException {
final File file = new File(this.file.getPath() + File.separatorChar + statement);
return ResourceHandlerFactory.getResourceHandler(file);
}
/**
* @return <code>false</code> in any case because this is a local resource handler.
*/
public boolean isRemoteResource() {
return false;
}
public boolean isRoot() {
synchronized(fileSystemViewInstance) {
return fileSystemViewInstance.isFloppyDrive(this.file) || fileSystemViewInstance.isRoot(this.file) || fileSystemViewInstance.isDrive(this.file);
}
}
public boolean isFloppyDrive() {
if(this.isFloppyDrive==null) {
synchronized(fileSystemViewInstance) {
this.isFloppyDrive = Boolean.valueOf(fileSystemViewInstance.isFloppyDrive(this.file));
}
}
return this.isFloppyDrive.booleanValue();
}
@Override
public RESOURCE_HANDLER_USER_TYPES getType() {
return RESOURCE_HANDLER_USER_TYPES.FILESYSTEM;
}
/**
* Returns all root partitions on this system. For example, on
* Windows, this would be the A: through Z: drives.
*/
@Override
public IResourceHandler[] getRoots() {
IResourceHandler[] fileSystemRoots = ResourceHandlerUtils.getFileSystemRoots();
return fileSystemRoots;
}
/**
* Name of a file, directory, or folder as it would be displayed in
* a system file browser. Example from Windows: the "M:\" directory
* displays as "CD-ROM (M:)"
*
* The default implementation gets information from the ShellFolder class.
*
* @return the file name as it would be displayed by a native file chooser
*/
@Override
public String getSystemDisplayName() {
synchronized(fileSystemViewInstance) {
try {
return fileSystemViewInstance.getSystemDisplayName(this.file);
} catch (Exception e) {
return this.getName();
}
}
}
@Override
public IResourceHandler createNewFolder() throws IOException {
synchronized(fileSystemViewInstance) {
return ResourceHandlerFactory.getResourceHandler(fileSystemViewInstance.createNewFolder(this.file));
}
}
@Override
public File toFile() {
return getFile();
}
@Override
public List<String> getPathSegments() {
final File file = getFile();
List<String> result = Collections.emptyList();
if(file != null) {
result = ListUtils.split(file.toString(), File.separator);
if(!result.isEmpty() && result.get(0).isEmpty()) {
result.set(0, "/");
}
}
return result;
}
@Override
public boolean isHidden() {
return this.file.isHidden() || this.file.getName().startsWith(".");
}
}