/** Copyright (C) 2012 Delcyon, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.delcyon.capo.resourcemanager.types; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Modifier; import java.net.URI; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; import java.util.Map; import java.util.logging.Level; import com.delcyon.capo.CapoApplication; import com.delcyon.capo.datastream.stream_attribute_filter.MD5FilterInputStream; import com.delcyon.capo.datastream.stream_attribute_filter.MD5FilterOutputStream; import com.delcyon.capo.datastream.stream_attribute_filter.SizeFilterInputStream; import com.delcyon.capo.resourcemanager.ResourceParameter; import com.delcyon.capo.resourcemanager.ResourceParameterBuilder; import com.delcyon.capo.resourcemanager.ResourceURI; import com.delcyon.capo.util.CloneControl; import com.delcyon.capo.util.CloneControl.Clone; /** * @author jeremiah * */ @CloneControl(filter=Clone.exclude, modifiers=Modifier.TRANSIENT) public class FileResourceContentMetaData extends AbstractContentMetaData { private String uri = null; private int currentDepth = 0; private ResourceParameter[] resourceParameters = new ResourceParameter[0]; private transient File file; public enum FileAttributes { absolutePath, canonicalPath, symlink, regular } @SuppressWarnings("unused") private FileResourceContentMetaData() //serialization only { } public FileResourceContentMetaData(String uri, ResourceParameter... resourceParameters) throws Exception { this.uri = uri; this.resourceParameters = resourceParameters; init(uri,0,resourceParameters); } public FileResourceContentMetaData(String uri, int currentDepth, ResourceParameter... resourceParameters) throws Exception { this.uri = uri; this.resourceParameters = resourceParameters; init(uri,currentDepth,resourceParameters); } @SuppressWarnings("rawtypes") @Override public Enum[] getAdditionalSupportedAttributes() { return new Enum[]{Attributes.exists,Attributes.executable,Attributes.readable,Attributes.writeable,Attributes.container,Attributes.lastModified,Attributes.MD5,FileAttributes.absolutePath,FileAttributes.canonicalPath,FileAttributes.symlink,FileAttributes.regular}; } public void refresh(ResourceParameter... resourceParameters) throws Exception { clearAttributes(); setInitialized(false); if (resourceParameters != null) { init(uri,currentDepth,resourceParameters); } else { init(uri,currentDepth,this.resourceParameters); } } //just initialize anything about ourselves, BUT NOTHING ABOUT OUR CHILDREN private void init(String uri,int currentDepth, ResourceParameter... resourceParameters) throws Exception { if (getBoolean(Parameters.USE_RELATIVE_PATHS,false,resourceParameters)) { if (getString(Attributes.path,null,resourceParameters) == null) { ResourceParameterBuilder resourceParameterBuilder = new ResourceParameterBuilder(); resourceParameterBuilder.addAll(resourceParameters); resourceParameterBuilder.addParameter(Attributes.path, uri.toString()); this.resourceParameters = resourceParameterBuilder.getParameters(); } else { String uriString = uri.toString(); uriString = uriString.replaceFirst(getString(Attributes.path,null,resourceParameters),""); setResourceURI(new ResourceURI(ResourceURI.removeURN(uriString))); } } else { setResourceURI(new ResourceURI(uri)); } file = new File(new URI(uri)); if(getResourceURI() == null) { setResourceURI(new ResourceURI(file.toURI().toString())); } } @Override public void init() throws RuntimeException { if(isInitialized() == false) { try { load(); } catch (Exception e) { throw new RuntimeException(e); } } } protected void load() throws Exception { if(file == null) { refresh(); } boolean exists = false; Map fileAttributes = null; try //an exists() check on a file is more expensive than catching the IO exception { fileAttributes = Files.readAttributes(file.toPath(), "*", LinkOption.NOFOLLOW_LINKS); } catch(IOException ioe) { setValue(Attributes.exists, exists+""); setValue(Attributes.executable, file.canExecute()); setValue(Attributes.readable, file.canRead()); setValue(Attributes.writeable, file.canWrite()); setValue(Attributes.container, "false"); setValue(Attributes.lastModified,""); setValue(SizeFilterInputStream.ATTRIBUTE_NAME, 0); setValue(FileAttributes.absolutePath, file.getAbsolutePath()); setValue(FileAttributes.canonicalPath, file.getCanonicalPath()); setValue(FileAttributes.symlink, "false"); setInitialized(true); return; } exists = true; setValue(Attributes.exists, exists); setValue(Attributes.executable, file.canExecute()); setValue(Attributes.readable, file.canRead()); setValue(Attributes.writeable, file.canWrite()); setValue(Attributes.container, fileAttributes.get("isDirectory")); setValue(Attributes.lastModified,file.lastModified()); setValue(SizeFilterInputStream.ATTRIBUTE_NAME, fileAttributes.get("size")); setValue(FileAttributes.absolutePath, file.getAbsolutePath()); setValue(FileAttributes.canonicalPath, file.getCanonicalPath()); boolean isSymlink = Boolean.parseBoolean(fileAttributes.get("isSymbolicLink").toString()); // System.out.println(file+"attr="+Files.readAttributes(file.toPath(), "*",LinkOption.NOFOLLOW_LINKS)); setValue(FileAttributes.symlink, isSymlink+""); if (exists == true && file.canRead() == true && (file.isDirectory() == false || isSymlink)) { if(Files.isRegularFile(file.toPath())) { //System.out.println(Files.probeContentType(file.toPath())); setValue(FileAttributes.regular, "true"); FileInputStream fileInputStream = new FileInputStream(file); try //some files are not actually readable when we get here even though java says yes. { readInputStream(fileInputStream,false); } catch (IOException ioException) { setValue(Attributes.readable, false); } finally { fileInputStream.close(); } } else { setValue(FileAttributes.regular, "false"); } } else if (file.isDirectory() == true && ContentMetaData.getIntValue(ContentMetaData.Parameters.DEPTH,1,resourceParameters) > currentDepth && isSymlink == false) { String[] fileList = file.list(); //check for permissions, cause if we can't read, well get a null list back if(fileList == null && file.canRead() == false) { CapoApplication.logger.log(Level.WARNING, "Can't read directory contents of "+file); fileList = new String[]{}; } if(isSymlink) { CapoApplication.logger.log(Level.WARNING, "Symlink skipping directory contents of "+file); fileList = new String[]{}; } MD5FilterOutputStream md5FilterOutputStream = new MD5FilterOutputStream(new ByteArrayOutputStream()); if(file != null && exists) { md5FilterOutputStream.write(file.getName()); md5FilterOutputStream.write(file.length()+""); md5FilterOutputStream.write(file.lastModified()+""); } else { md5FilterOutputStream.write(""); } Arrays.sort(fileList); int currentPreviousChildIndex = 0; for (int currentChildURIIndex =0; currentChildURIIndex < fileList.length; currentChildURIIndex++) { String childURI = fileList[currentChildURIIndex]; File childFile = new File(file,childURI); //we don't care if this is a directory, since we always just strip off the ending slash String tempChildURI = toURI(childFile, false);//.toString(); if (tempChildURI.endsWith(File.separator)) { tempChildURI = tempChildURI.substring(0, tempChildURI.length()-File.separator.length()); } FileResourceContentMetaData contentMetaData = new FileResourceContentMetaData(tempChildURI, currentDepth+1,resourceParameters); if(contentMetaData.file != null) //XXX we just got this from out parent, and checks are expensive && contentMetaData.file.exists()) { contentMetaData.file = childFile; //always reuse our original file object since the uri encoding of names can be problematic esp with regards to non breaking spaces in file names. BasicFileAttributes contentAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class); md5FilterOutputStream.write(contentMetaData.file.getName()); md5FilterOutputStream.write(contentAttributes.size()+""); md5FilterOutputStream.write(contentAttributes.lastModifiedTime().toMillis()+""); } boolean addedChild = false; for( ; currentPreviousChildIndex < childContentMetaDataLinkedList.size(); currentPreviousChildIndex++) { File currentPreviousChildFile = ((FileResourceContentMetaData)childContentMetaDataLinkedList.get(currentPreviousChildIndex)).file; int compare = currentPreviousChildFile.getName().compareTo(childFile.getName()); if(compare > 0) //previous after newChild { if((currentPreviousChildIndex +1) == childContentMetaDataLinkedList.size()) { addContainedResource(contentMetaData); addedChild = true; } else { childContentMetaDataLinkedList.add(currentPreviousChildIndex, contentMetaData); addedChild = true; } break; } else if (compare == 0) //previous before newChild { addedChild = true; break; } } if(addedChild == false) { addContainedResource(contentMetaData); } while(childFile.getName().equals(((FileResourceContentMetaData)childContentMetaDataLinkedList.get(currentChildURIIndex)).file.getName()) == false) { childContentMetaDataLinkedList.remove(currentChildURIIndex); } } //This may not be needed as it should effectively does again, what the removale of any non matches against the entire list // for(int currentNewFileIndex =0 ; currentNewFileIndex < fileList.length; currentNewFileIndex++) // { // while(new File(file,fileList[currentNewFileIndex]).getName().equals(((FileResourceContentMetaData)childContentMetaDataLinkedList.get(currentNewFileIndex)).file.getName()) == false) // { // childContentMetaDataLinkedList.remove(currentNewFileIndex); // } // // } //this truncates any files that were not in the new list, but are sorted to the end of the list while(childContentMetaDataLinkedList.size() != fileList.length) { childContentMetaDataLinkedList.removeLast(); } setValue(MD5FilterInputStream.ATTRIBUTE_NAME, md5FilterOutputStream.getMD5()); md5FilterOutputStream.close(); } setInitialized(true); } @Override public Boolean exists() { return Boolean.parseBoolean(getValue(Attributes.exists)); } @Override public Long getLastModified() { return Long.parseLong(getValue(Attributes.lastModified)); } @Override public Boolean isContainer() { return Boolean.parseBoolean(getValue(Attributes.container)); } @Override public Boolean isReadable() { return Boolean.parseBoolean(getValue(Attributes.readable)); } @Override public Boolean isWriteable() { return Boolean.parseBoolean(getValue(Attributes.writeable)); } private static String slashify(String path, boolean isDirectory) { String p = path; if (File.separatorChar != '/') p = p.replace(File.separatorChar, '/'); //replace separator chars with '/' if (!p.startsWith("/")) //add slash to front of path p = "/" + p; if (!p.endsWith("/") && isDirectory) //add / to end of path if directory p = p + "/"; return p; } public String toURI(File file,boolean isDirectory) { try { File f = file.getAbsoluteFile(); String sp = slashify(f.getPath(), isDirectory); if (sp.startsWith("//")) sp = "//" + sp; String mine = "file:"+encode(sp); return mine; } catch (Exception x) { throw new Error(x); // Can't happen } } public static String encode(String input) { StringBuilder resultStr = new StringBuilder(); for (char ch : input.toCharArray()) { if (isUnsafe(ch)) { resultStr.append('%'); resultStr.append(toHex(ch / 16)); resultStr.append(toHex(ch % 16)); } else { resultStr.append(ch); } } return resultStr.toString(); } private static char toHex(int ch) { return (char) (ch < 10 ? '0' + ch : 'A' + ch - 10); } private static boolean isUnsafe(char ch) { if((int)ch == 160) //nbsp { return true; } return " %;?<>#|\\[]{}\"\n\r\t".indexOf(ch) >= 0; } @Override public ResourceParameter[] getResourceParameters() { return resourceParameters; } }