package org.wyona.yarep.impl.repo.fs; import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Vector; import org.apache.commons.io.FileUtils; import org.apache.log4j.Category; import org.wyona.yarep.core.NoSuchRevisionException; import org.wyona.yarep.core.Node; import org.wyona.yarep.core.NodeStateException; import org.wyona.yarep.core.NodeType; import org.wyona.yarep.core.Path; import org.wyona.yarep.core.Property; import org.wyona.yarep.core.PropertyType; import org.wyona.yarep.core.RepositoryException; import org.wyona.yarep.core.Revision; import org.wyona.yarep.core.UID; import org.wyona.yarep.impl.AbstractNode; import org.wyona.yarep.impl.DefaultProperty; import org.wyona.yarep.impl.repo.fs.FileSystemRepository; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.Field.Index; import org.apache.lucene.document.Field.Store; import org.apache.lucene.index.IndexWriter; /** * This class represents a repository node. * A repository node may be either a collection ("directory") or a resource ("file"). */ public class FileSystemNode extends AbstractNode { private static Category log = Category.getInstance(FileSystemNode.class); protected static final String META_FILE_NAME = "meta"; protected static final String REVISIONS_BASE_DIR = "revisions"; protected static final String META_DIR_SUFFIX = ".yarep"; protected File contentDir; protected File contentFile; protected File metaDir; protected File metaFile; protected RevisionDirectoryFilter revisionDirectoryFilter = new RevisionDirectoryFilter(); /** * Constructor * @throws RepositoryException */ public FileSystemNode(FileSystemRepository repository, String path, String uuid) throws RepositoryException { super(repository, path, uuid); init(); } /** * Constructor * @throws RepositoryException */ protected FileSystemNode(FileSystemRepository repository, String path, String uuid, boolean doInit) throws RepositoryException { super(repository, path, uuid); if (doInit) { init(); } } /** * Init file system impl node */ protected void init() throws RepositoryException { this.contentDir = getRepository().getContentDir(); this.contentFile = determineContentFile(this.uuid); this.metaDir = determineMetaDir(this.uuid); this.metaFile = determineMetaFile(this.uuid); if (log.isDebugEnabled()) { log.debug("FileSystemNode: path=" + path + " uuid=" + uuid); log.debug("contentDir=" + contentDir); log.debug("contentFile=" + contentFile); log.debug("metaDir=" + metaDir); log.debug("metaFile=" + metaFile); } if (!metaFile.exists()) { createMetaFile(); } readProperties(); readRevisions(); } protected File determineContentFile(String uuid) { return new File(this.contentDir, uuid); } /** * Determine yarep meta directory */ protected File determineMetaDir(String uuid) { if (getRepository().getYarepMetaDir() != null) { return new File(getRepository().getYarepMetaDir(), uuid + META_DIR_SUFFIX); } else { return new File(this.contentDir, uuid + META_DIR_SUFFIX); } } /** * Determine yarep meta file of this node */ protected File determineMetaFile(String uuid) { return new File(this.metaDir, META_FILE_NAME); } /** * */ protected void createMetaFile() throws RepositoryException { if (!metaDir.exists()) { metaDir.mkdirs(); log.warn("Creating new meta directory: " + metaDir); } log.debug("Set node type property ..."); this.properties = new HashMap(); if (this.contentFile.isDirectory()) { this.setProperty(PROPERTY_TYPE, NodeType.TYPENAME_COLLECTION); } else { this.setProperty(PROPERTY_TYPE, NodeType.TYPENAME_RESOURCE); //this.setProperty(PROPERTY_SIZE, this.contentFile.length()); //this.setProperty(PROPERTY_LAST_MODIFIED, this.contentFile.lastModified()); } } /** * Read properties from meta file */ protected void readProperties() throws RepositoryException { try { log.debug("Reading meta file: " + this.metaFile); this.properties = new HashMap(); BufferedReader reader = new BufferedReader(new FileReader(this.metaFile)); String line; while ((line = reader.readLine()) != null) { line = line.trim(); String name; String typeName; String value; try { name = line.substring(0, line.indexOf("<")).trim(); typeName = line.substring(line.indexOf("<")+1, line.indexOf(">")).trim(); value = line.substring(line.indexOf(":")+1).trim(); } catch (StringIndexOutOfBoundsException e) { throw new RepositoryException("Error while parsing meta file: " + this.metaFile + " at line " + line); } Property property = new DefaultProperty(name, PropertyType.getType(typeName), this); property.setValueFromString(value); this.properties.put(name, property); } reader.close(); } catch (IOException e) { throw new RepositoryException("Error while reading meta file: " + metaFile + ": " + e.getMessage()); } } /** * FIXME: this implementation does not work correctly when a string property contains a line-break. * @throws RepositoryException */ protected void saveProperties() throws RepositoryException { try { // get repository FileSystemRepository fsRepo = getRepository(); // the lucene index location File propertiesSearchIndexFile = fsRepo.getPropertiesSearchIndexFile(); // get lucene index writer, create the index if it does not exist yet IndexWriter indexWriter = null; if (propertiesSearchIndexFile != null) { if (propertiesSearchIndexFile.isDirectory()) { indexWriter = new IndexWriter(propertiesSearchIndexFile.getAbsolutePath(), fsRepo.getWhitespaceAnalyzer(), false); } else { indexWriter = new IndexWriter(propertiesSearchIndexFile.getAbsolutePath(), fsRepo.getWhitespaceAnalyzer(), true); } } else { log.warn("Directory of search index for properties is not set!"); } // prepare the lucene document Document document = new Document(); log.debug("writing meta file: " + this.metaFile); PrintWriter writer = new PrintWriter(new FileOutputStream(this.metaFile)); Iterator iterator = this.properties.values().iterator(); while (iterator.hasNext()) { Property property = (Property)iterator.next(); writer.println(property.getName() + "<" + PropertyType.getTypeName(property.getType()) + ">:" + property.getValueAsString()); // add the property to the lucene document // TODO: write typed property value to index. possible? if (property.getValueAsString() != null) { document.add(new Field(property.getName(), property.getValueAsString(), Field.Store.YES, Field.Index.UN_TOKENIZED)); } else { log.warn("Property '" + property.getName() + "' has null as value and hence will not be indexed (path: " + this.getPath() + ")!"); } } writer.flush(); writer.close(); // store the lucene document document.add(new Field("_PATH", this.getPath(), Field.Store.YES, Field.Index.UN_TOKENIZED)); if (indexWriter != null) { indexWriter.updateDocument(new org.apache.lucene.index.Term("_PATH", this.getPath()), document); indexWriter.close(); } else { log.warn("Index writer for properties search is null!"); } } catch (IOException e) { throw new RepositoryException("Error while reading meta file: " + metaFile + ": " + e.getMessage()); } } /** * @see org.wyona.yarep.core.Node#getNodes() */ public Node[] getNodes() throws RepositoryException { Path[] childPaths = getRepository().getMap().getChildren(new Path(this.path)); FileSystemRepository repo = (FileSystemRepository) this.repository; Vector childNodes = new Vector(); for (int i = 0; i < childPaths.length; i++) { childNodes.addElement(repo.getNode(childPaths[i].toString())); } // Also add fallback nodes if fallback is enabled if (repo.isFallbackEnabled()) { log.warn("Fallback is enabled for repository '" + repo.getName() + "' and hence children will also retrieved from storage without being listed within map!"); File contentDir = repo.contentDir; String path = contentDir.getPath() + this.path; File currentDir = new File(path); if (currentDir.isDirectory()) { File[] files = null; files = currentDir.listFiles(new YarepMetaDataDirectoryFilter()); for (int i = 0; i < files.length; i++) { String fpath = this.path + "/" + files[i].getName(); boolean alreadyExists = false; for (int k = 0; k < childPaths.length; k++) { if (files[i].getName().equals(childPaths[k].getName())) { alreadyExists = true; break; } } if (!alreadyExists) { log.info("No UID! Fallback to : " + fpath); String uuid2 = new UID(fpath).toString(); childNodes.addElement(new FileSystemNode(repo, fpath, uuid2)); } } } } Node[] children = new Node[childNodes.size()]; for (int i = 0; i < children.length; i++) { children[i] = (Node) childNodes.elementAt(i); } return children; } /** * @see org.wyona.yarep.core.Node#addNode(java.lang.String, int) */ public Node addNode(String name, int type) throws RepositoryException { String newPath = getPath() + "/" + name; log.debug("Adding node: " + newPath); if (this.repository.existsNode(newPath)) { log.warn("Node already exists: " + newPath); throw new RepositoryException("Node exists already: " + newPath); } UID uid = getRepository().getMap().create(new Path(newPath), type); // create file: File file = determineContentFile(uid.toString()); try { if (type == NodeType.COLLECTION) { file.mkdirs(); log.warn("Directory created: " + file); } else if (type == NodeType.RESOURCE) { File parentFile = file.getParentFile(); if (!parentFile.exists()) { parentFile.mkdirs(); } file.createNewFile(); } else { throw new RepositoryException("Unknown node type: " + type); } return this.repository.getNode(newPath); } catch (IOException e) { throw new RepositoryException("Could not access file " + file, e); } } /** * @see org.wyona.yarep.core.Node#removeProperty(java.lang.String) */ public void removeProperty(String name) throws RepositoryException { this.properties.remove(name); saveProperties(); } /** * @see org.wyona.yarep.core.Node#setProperty(org.wyona.yarep.core.Property) */ public void setProperty(Property property) throws RepositoryException { this.properties.put(property.getName(), property); saveProperties(); } /** * @see org.wyona.yarep.core.Node#getInputStream() */ public InputStream getInputStream() throws RepositoryException { try { return new FileInputStream(this.contentFile); } catch (FileNotFoundException e) { throw new RepositoryException(e.getMessage(), e); } //return getProperty(PROPERTY_CONTENT).getInputStream(); } /** * @see org.wyona.yarep.core.Node#getOutputStream() */ public OutputStream getOutputStream() throws RepositoryException { try { //return new FileOutputStream(this.contentFile); return new FileSystemOutputStream(this, this.contentFile); } catch (FileNotFoundException e) { throw new RepositoryException(e.getMessage(), e); } //return getProperty(PROPERTY_CONTENT).getOutputStream(); } /** * @see org.wyona.yarep.core.Node#checkin() */ public Revision checkin() throws NodeStateException, RepositoryException { return checkin(""); } /** * @see org.wyona.yarep.core.Node#checkin() */ public Revision checkin(String comment) throws NodeStateException, RepositoryException { if (!isCheckedOut()) { throw new NodeStateException("Node " + path + " is not checked out."); } Revision revision = createRevision(comment); setProperty(PROPERTY_IS_CHECKED_OUT, false); setProperty(PROPERTY_CHECKIN_DATE, new Date()); return revision; } public void cancelCheckout() throws NodeStateException, RepositoryException { if (!isCheckedOut()) { throw new NodeStateException("Node " + path + " is not checked out."); } setProperty(PROPERTY_IS_CHECKED_OUT, false); setProperty(PROPERTY_CHECKIN_DATE, new Date()); } /** * @see org.wyona.yarep.core.Node#checkout(java.lang.String) */ public void checkout(String userID) throws NodeStateException, RepositoryException { // TODO: this should be somehow synchronized if (isCheckedOut()) { throw new NodeStateException("Node " + path + " is already checked out by: " + getCheckoutUserID()); } setProperty(PROPERTY_IS_CHECKED_OUT, true); setProperty(PROPERTY_CHECKOUT_USER_ID, userID); setProperty(PROPERTY_CHECKOUT_DATE, new Date()); /*if (getRevisions().length == 0) { // create a backup revision createRevision("initial revision"); }*/ } protected Revision createRevision(String comment) throws RepositoryException { try { File revisionsBaseDir = new File(this.metaDir, REVISIONS_BASE_DIR); String revisionName = String.valueOf(System.currentTimeMillis()); File revisionDir = new File(revisionsBaseDir, revisionName); File destContentFile = new File(revisionDir, FileSystemRevision.CONTENT_FILE_NAME); FileUtils.copyFile(this.contentFile, destContentFile); File destMetaFile = new File(revisionDir, META_FILE_NAME); FileUtils.copyFile(this.metaFile, destMetaFile); Revision revision = new FileSystemRevision(this, revisionName); revision.setProperty(PROPERTY_IS_CHECKED_OUT, false); ((FileSystemRevision)revision).setCreationDate(new Date()); ((FileSystemRevision)revision).setCreator(getCheckoutUserID()); ((FileSystemRevision)revision).setComment(comment); this.revisions.put(revisionName, revision); return revision; } catch (IOException e) { log.error(e.getMessage(), e); throw new RepositoryException(e.getMessage(), e); } } protected void readRevisions() throws RepositoryException { File revisionsBaseDir = new File(this.metaDir, REVISIONS_BASE_DIR); File[] revisionDirs = revisionsBaseDir.listFiles(this.revisionDirectoryFilter); this.revisions = new LinkedHashMap(); if (revisionDirs != null) { Arrays.sort(revisionDirs); for (int i=0; i<revisionDirs.length; i++) { String revisionName = revisionDirs[i].getName(); Revision revision = new FileSystemRevision(this, revisionName); this.revisions.put(revisionName, revision); } } } /** * @see org.wyona.yarep.core.Node#restore(java.lang.String) */ public void restore(String revisionName) throws NoSuchRevisionException, RepositoryException { try { File revisionsBaseDir = new File(this.metaDir, REVISIONS_BASE_DIR); File revisionDir = new File(revisionsBaseDir, revisionName); File srcContentFile = new File(revisionDir, FileSystemRevision.CONTENT_FILE_NAME); FileUtils.copyFile(srcContentFile, this.contentFile); File srcMetaFile = new File(revisionDir, META_FILE_NAME); FileUtils.copyFile(srcMetaFile, this.metaFile); setProperty(AbstractNode.PROPERTY_LAST_MODIFIED, this.contentFile.lastModified()); } catch (IOException e) { log.error(e.getMessage(), e); throw new RepositoryException(e.getMessage(), e); } } protected class RevisionDirectoryFilter implements FileFilter { public RevisionDirectoryFilter() { } public boolean accept(File pathname) { if (pathname.getName().matches("[0-9]+") && pathname.isDirectory()) { return true; } else { return false; } } } /** * Filter in order to filter ".yarep" nodes */ protected class YarepMetaDataDirectoryFilter implements FileFilter { public YarepMetaDataDirectoryFilter() { } public boolean accept(File pathname) { if (pathname.getName().endsWith(META_DIR_SUFFIX) && pathname.isDirectory()) { return false; } else { return true; } } } /** * Changing a property should update the last modified date. * FIXME: This implementation does not change the last modified date if a property changes. * @see org.wyona.yarep.impl.AbstractNode#getLastModified() */ public long getLastModified() throws RepositoryException { return this.contentFile.lastModified(); } /** * @see org.wyona.yarep.impl.AbstractNode#getSize() */ public long getSize() throws RepositoryException { return this.contentFile.length(); } protected FileSystemRepository getRepository() { return (FileSystemRepository)this.repository; } /** * @see org.wyona.yarep.core.Node#delete() */ public void delete() throws RepositoryException { deleteRec(this); } protected void deleteRec(Node node) throws RepositoryException { Node[] children = node.getNodes(); for (int i=0; i<children.length; i++) { deleteRec(children[i]); } boolean success = getRepository().getMap().delete(new Path(getPath())); try { if (this.contentFile.isDirectory()) { FileUtils.deleteDirectory(this.contentFile); } else { this.contentFile.delete(); } FileUtils.deleteDirectory(this.metaDir); } catch (IOException e) { throw new RepositoryException("Could not delete node: " + node.getPath() + ": " + e.toString(), e); } } }