package org.rr.commons.mufs; import static org.rr.commons.utils.StringUtil.EMPTY; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.management.ManagementFactory; import java.lang.management.MemoryUsage; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.rr.commons.log.LoggerFactory; /** * The {@link AResourceHandler} provides some provider implementation independent methods. * It's not attendant to extends this class for creating new {@link IResourceHandler} types * but the {@link IResourceHandler} interface must be implemented. */ abstract class AResourceHandler implements IResourceHandler { private static final HashMap<String, Pattern> FILE_EXTENSION_PATTERNS = new HashMap<String, Pattern>() { { put(MimeUtils.MIME_JPEG, Pattern.compile("(.jpg|.jpeg)$")); put(MimeUtils.MIME_PNG, Pattern.compile("(.png)$")); put(MimeUtils.MIME_GIF, Pattern.compile("(.gif)$")); put(MimeUtils.MIME_TEXT, Pattern.compile("(.txt)$")); put(MimeUtils.MIME_EPUB, Pattern.compile("(.epub)$")); put(MimeUtils.MIME_PDF, Pattern.compile("(.pdf)$")); put(MimeUtils.MIME_CBZ, Pattern.compile("(.cbz)$")); put(MimeUtils.MIME_CBR, Pattern.compile("(.cbr)$")); put(MimeUtils.MIME_HTML, Pattern.compile("(.htm|.html|.xhtml)$")); put(MimeUtils.MIME_XML, Pattern.compile("(.xml)$")); put(MimeUtils.MIME_RTF, Pattern.compile("(.rtf)$")); put(MimeUtils.MIME_MOBI, Pattern.compile("(.mobi)$")); put(MimeUtils.MIME_AZW, Pattern.compile("(.azw\\d*)$")); put(MimeUtils.MIME_FB2, Pattern.compile("(.fb2)$")); put(MimeUtils.MIME_LIT, Pattern.compile("(.lit)$")); put(MimeUtils.MIME_PKG, Pattern.compile("(.pkg)$")); put(MimeUtils.MIME_RB, Pattern.compile("(.rb)$")); put(MimeUtils.MIME_DJVU, Pattern.compile("(.djvu)$")); put(MimeUtils.MIME_DOC, Pattern.compile("(.doc)$")); put(MimeUtils.MIME_DOCX, Pattern.compile("(.docx)$")); } }; /** * The file format. This is a cached value, so the format should not be determined each time. */ private String mime = null; /** * An empty implementation because it's not needed for all {@link IResourceHandler} implementations. */ @Override public void refresh() { this.mime = null; } /** * Tries to determine the format of the resource handled by this {@link IResourceHandler} instance. * The file format will firstly be determined by the resource extension and afterwards by it's content. */ @Override public String getMimeType(boolean force) { if (this.mime != null) { if(!this.mime.isEmpty()) { return this.mime; } return null; } else if(this.isDirectoryResource()) { this.mime = EMPTY; return null; } String mimeFromFileName = extractMimeTypeFromFileName(); if(mimeFromFileName != null) { return mimeFromFileName; } if(force) { try { final String guessedMime = ResourceHandlerUtils.guessFormat(this); if (guessedMime != null) { return this.mime = guessedMime; } } catch(FileNotFoundException e) { LoggerFactory.logInfo(this, "File not found " + this, e); return null; //No file, no reason to continue. } catch (IOException e1) { return null; //IO is not good. No reason to continue. } } return null; } /** * Get the mime type by the file extension. * @return The desired mime for the file or <code>null</code> if the extension is not known. */ private String extractMimeTypeFromFileName() { final String resourceString = this.getResourceString(); if (resourceString != null && resourceString.lastIndexOf('.') != -1) { final String lowerCasedResourceString = resourceString.toLowerCase(); for(String mime : FILE_EXTENSION_PATTERNS.keySet()) { Pattern pattern = FILE_EXTENSION_PATTERNS.get(mime); if(pattern.matcher(lowerCasedResourceString).find()) { return mime; } } } return null; } /** * Tells if this {@link AResourceHandler} instance file format is an image. * @return <code>true</code> if the resource is an image or <code>false</code> otherwise. */ @Override public boolean isImageFormat() { return getMimeType(true) != null && getMimeType(true).startsWith("image/"); } /** * Gets the file extension of the file resource. The last three characters behind the dot must not * really be the file extension! The format of the file will be determined and compared to * the file system file extension. Only of the format and the file system file extension * matches to each other, the extension of the file is returned. * * @return The file extension. If no extension is detected, an empty String is returned. */ public String getFileExtension() { final String fileName = this.getName().toLowerCase(); try { //test if a file extension was specified. if(fileName.indexOf('.') == -1 || isDirectoryResource()) { return EMPTY; } //test if the file ends with a default file extension. If the //format and the extension did not match, the string behind the dot //belong to the file name. final String mime = this.getMimeType(false); if (mime != null && mime.length() > 0 && mime.indexOf('/') != -1) { final String mimeFormatPart = mime.substring(mime.indexOf('/')+1); if(mimeFormatPart.equals("jpg") || mimeFormatPart.equals("jpeg")) { if(!fileName.endsWith(".jpg") && !fileName.endsWith(".jpeg")) { return EMPTY; //no jpeg extension. all after the dot belongs to the file name. } } else if(mimeFormatPart.equals("png")) { if(!fileName.endsWith(".png")) { return EMPTY; //no png extension. all after the dot belongs to the file name. } } else if(mimeFormatPart.equals("gif")) { if(!fileName.endsWith(".gif")) { return EMPTY; //no gif extension. all after the dot belongs to the file name. } } } //return with the chars behind the last dot. return fileName.substring(this.getName().lastIndexOf('.') + 1); } catch (Exception e) { return EMPTY; } } /** * Tells if the resource handled by this {@link AResourceHandler} is * a file resource. * * @return <code>true</code> if it's a file resource or <code>false</code> otherwise. */ public boolean isFileResource() { if(!this.exists()) { return false; } return !this.isDirectoryResource(); } /** * Reads the content of the {@link InputStream} provided by this {@link InputStreamResourceHandler} * and puts it into the target {@link IResourceHandler}. */ @Override public boolean copyTo(IResourceHandler targetRecourceLoader, boolean overwrite) throws IOException { //handle overwrite if(!overwrite && targetRecourceLoader.exists()) { return false; } //perform a slow stream copy. OutputStream contentOutputStream = null; try { contentOutputStream = targetRecourceLoader.getContentOutputStream(false); IOUtils.write(this.getContent(), contentOutputStream); return true; } finally { IOUtils.closeQuietly(contentOutputStream); } } /** * Perfrom also a {@link #copyTo(IResourceHandler, boolean)} because a stream could not be moved. */ @Override public void moveTo(IResourceHandler targetRecourceLoader, boolean overwrite) throws IOException { copyTo(targetRecourceLoader, overwrite); delete(); } /** * A general filter listResources method. Should be reimplemented if a {@link IResourceHandler} implementation * is able to perform a more performant way. * @param filter A filter to be used for filter the result child resources. * set it to <code>null</code> for no filter * @return all files and diretories matching to the given {@link ResourceNameFilter}. */ @Override public IResourceHandler[] listResources(final ResourceNameFilter filter) throws IOException { //get files and directories List<IResourceHandler> listFileResources = Arrays.asList(this.listFileResources()); List<IResourceHandler> listDirectoryResources = Arrays.asList(this.listDirectoryResources()); //create the result containing both, directories and files with the right size, so the list must ne be resized while copying into it. ArrayList<IResourceHandler> resultFileResources = new ArrayList<>(listFileResources.size()+listDirectoryResources.size()); //loop directory resources for (int i = 0; i < listDirectoryResources.size(); i++) { final IResourceHandler resourceHandler = listDirectoryResources.get(i); if(filter==null || filter.accept(resourceHandler)) { //add the resource. resultFileResources.add(resourceHandler); } } //loop file resources for (int i = 0; i < listFileResources.size(); i++) { final IResourceHandler resourceHandler = listFileResources.get(i); if(filter==null || filter.accept(resourceHandler)) { //add the resource. resultFileResources.add(resourceHandler); } } final IResourceHandler[] result = resultFileResources.toArray(new IResourceHandler[resultFileResources.size()]); return result; } /** * Gets the resource string as toString output. Use * {@link #getResourceString()} instead of {@link #toString()} if the * path for this {@link AResourceHandler} instance is needed. * @return The strign representation for this {@link AResourceHandler} instance. */ public String toString() { return this.getResourceString(); } /** * Gets the content of the resource handled by this {@link IResourceHandler} instance. * * @return The content of the resource. */ @Override public synchronized byte[] getContent() throws IOException { InputStream contentInputStream = this.getContentInputStream(); byte[] byteArray = IOUtils.toByteArray(contentInputStream); IOUtils.closeQuietly(contentInputStream); return byteArray; } /** * Sometimes, on heavy IO, the garbage collector isn't fast enough to free the heap. * To prevent this, the garbage collector is triggered if not enough space is * present. * @param heapRequired The amount of heap needed in the near future. * @throws IOException */ protected void cleanHeapIfNeeded(long heapRequired) throws IOException { final MemoryUsage heapMemoryUsage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); final long heapFreeSize = heapMemoryUsage.getCommitted() - heapMemoryUsage.getUsed(); if(heapFreeSize < (heapRequired * 1.2)) { // LoggerFactory.getLogger().log(Level.INFO , "Garbage collector triggered manually. " + heapFreeSize + " bytes remaining but " + heapRequired + " required for " + getName()); System.gc(); } } /** * Gets the content of the resource handled by this {@link IResourceHandler} instance. * * @return The content of the resource. */ @Override public synchronized byte[] getContent(int length) throws IOException { final InputStream contentInputStream = this.getContentInputStream(); final ByteArrayOutputStream output = new ByteArrayOutputStream(length); ResourceHandlerUtils.copy(contentInputStream, output, length); IOUtils.closeQuietly(contentInputStream); return output.toByteArray(); } public synchronized void setContent(byte[] content) throws IOException { OutputStream contentOutputStream = this.getContentOutputStream(false); IOUtils.write(content, contentOutputStream); IOUtils.closeQuietly(contentOutputStream); } public synchronized void setContent(CharSequence content) throws IOException { OutputStream contentOutputStream = this.getContentOutputStream(false); IOUtils.write(content, contentOutputStream); IOUtils.closeQuietly(contentOutputStream); } /** * Tests the resource string of the given object with this * instance for equalness. * @return <code>true</code> if the resources are equal and <code>false</code> otherwise. */ public boolean equals(Object o) { if(this == o) { return true; } else if(o instanceof IResourceHandler) { return ((IResourceHandler)o).getResourceString().equals(this.getResourceString()); } return false; } /** * Compares this object with the specified object for order. Returns a * negative integer, zero, or a positive integer as this object is less * than, equal to, or greater than the specified object. * * This is an alphanumeric comperator. * * @param o the object to be compared. * @return a negative integer, zero, or a positive integer as this object * is less than, equal to, or greater than the specified objec * * @see Comparable#compareTo(Object) */ public int compareTo(IResourceHandler o) { return ResourceHandlerUtils.compareTo(this, o, ResourceHandlerUtils.SORT_BY_NAME); } /** * Tests if this {@link AResourceHandler} instance is a root instance * by getting the parent resource. If the parent resource is <code>null</code> * this {@link AResourceHandler} instance is a root instance. * <br><br> * Implement a more effective way by overriding this method. */ public boolean isRoot() { return this.getParentResource()==null; } /** * Always returns <code>false</code>. Should overriden for local * file system implementations. */ public boolean isFloppyDrive() { return false; } /** * @return the root node for this {@link AResourceHandler} instance. * If multiple roots supported, this method should be overridden. */ public IResourceHandler[] getRoots() { IResourceHandler parent = this; while(!parent.isRoot()) { parent = parent.getParentResource(); } return new IResourceHandler[] {parent}; } /** * This default implementation uses the {@link FilenameFilter} to filter * these files starting with a '.'. */ public IResourceHandler[] listFileResources(final boolean showHidden) throws IOException { IResourceHandler[] listResources = this.listResources(new ResourceNameFilter() { @Override public boolean accept(IResourceHandler loader) { if(!loader.isFileResource()) { return false; } if(!showHidden && loader.getName().startsWith(".")) { return false; } return true; } }); return listResources; } /** * This default implementation uses the {@link FilenameFilter} to filter * these files starting with a '.'. */ @Override public IResourceHandler[] listDirectoryResources(final boolean showHidden) throws IOException { IResourceHandler[] listResources = this.listDirectoryResources(new ResourceNameFilter() { @Override public boolean accept(IResourceHandler loader) { if(!showHidden && loader.getName().startsWith(".")) { return false; } return true; } }); return listResources; } public final IResourceHandler[] listDirectoryResources() throws IOException { return listDirectoryResources(null); } /** * The default implementation did not support these feature. * The {@link #getName()} result is returned. */ @Override public String getSystemDisplayName() { return this.getName(); } /** * Creates a new folder with the name "New Folder". */ @Override public IResourceHandler createNewFolder() throws IOException { try { IResourceHandler addPathStatement = this.addPathStatement("New Folder"); addPathStatement.mkdirs(); return addPathStatement; } catch (IOException e) { throw e; } catch (Exception e) { throw new IOException(e); } } @Override public File toFile() { String name = getName(); String fileExtension = getFileExtension(); if(fileExtension != null && name.endsWith(fileExtension)) { name = name.substring(0, name.length() - fileExtension.length() - 1); } try { File createTempFile = File.createTempFile(name, fileExtension); IResourceHandler tmpResourceHandler = ResourceHandlerFactory.getResourceHandler(createTempFile); tmpResourceHandler.setContent(this.getContent()); tmpResourceHandler.dispose(); return createTempFile; } catch(Exception e) { throw new RuntimeException(e); } } @Override public List<String> getPathSegments() { List<String> emptyList = Collections.emptyList(); return emptyList; } @Override public boolean isHidden() { return false; } public void deleteOnExit() { ResourceHandlerFactory.deleteOnExit(this); } public boolean isEmpty() { if(!exists()) { return false; } try { boolean result; if(isDirectoryResource()) { result = listResources(null).length == 0; } else { result = size() == 0; } return result; } catch(Exception e) { return false; } } }