org.sakaiproject.content.chh.file.ContentHostingHandlerImplFileSystem.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.content.chh.file.ContentHostingHandlerImplFileSystem.java

Source

/**********************************************************************************
 * $URL: https://source.sakaiproject.org/svn/rwiki/trunk/rwiki-tool/tool/src/java/uk/ac/cam/caret/sakai/rwiki/tool/ModelMigrationContextListener.java $
 * $Id: ModelMigrationContextListener.java 20354 2007-01-17 10:30:57Z ian@caret.cam.ac.uk $
 ***********************************************************************************
 *
 * Copyright (c) 2003, 2004, 2005, 2006, 2007 University of Cambridge.
 *
 * Licensed under the Educational Community License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.opensource.org/licenses/ecl1.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 **********************************************************************************/
package org.sakaiproject.content.chh.file;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidObjectException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Collection;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.content.api.ContentCollection;
import org.sakaiproject.content.api.ContentCollectionEdit;
import org.sakaiproject.content.api.ContentEntity;
import org.sakaiproject.content.api.ContentHostingHandler;
import org.sakaiproject.content.api.ContentHostingHandlerResolver;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.content.api.ContentResourceEdit;
import org.sakaiproject.content.cover.ContentHostingService;
import org.sakaiproject.entity.api.Edit;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.entity.api.ResourcePropertiesEdit;
import org.sakaiproject.exception.ServerOverloadException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Provides a read/write view of a filesystem through virtual content hosting.
 * 
 * @author johnf
 */
public class ContentHostingHandlerImplFileSystem implements ContentHostingHandler {
    private static final String HANDLER_NAME = "FSHandler";
    private static final Log log = LogFactory.getLog(ContentHostingHandlerImplFileSystem.class);

    public final static String XML_NODE_NAME = "mountpoint";

    public final static String XML_ATTRIBUTE_NAME = "path";
    public final static String XML_ATTRIBUTE_HIDDEN = "showHiddenFiles";
    public final static String XML_ATTRIBUTE_SEARCHABLE = "searchable";

    public final static boolean SHOW_FULL_PATHS = false; /* list full paths in the file list view */

    /* ---------------------------------------------------- */
    private ContentHostingHandlerResolver contentHostingHandlerResolver = null;

    /**
     * @return the contentHandlerResover
     */
    public ContentHostingHandlerResolver getContentHostingHandlerResolver() {
        return contentHostingHandlerResolver;
    }

    /**
     * @param contentHandlerResover the contentHandlerResover to set
     */
    public void setContentHostingHandlerResolver(ContentHostingHandlerResolver contentHostingHandlerResolver) {
        this.contentHostingHandlerResolver = contentHostingHandlerResolver;
    }
    /* ---------------------------------------------------- */

    public void cancel(ContentCollectionEdit edit) { /* no work required -- no temporary changes to reverse */
    }

    public void cancel(ContentResourceEdit edit) { /* no work required -- no temporary changes to reverse */
    }

    public void commit(ContentCollectionEdit edit) {
        ContentCollectionFileSystem ccfs = null;
        if (edit instanceof ContentCollectionFileSystem)
            ccfs = (ContentCollectionFileSystem) edit;
        else {
            ContentEntity tmp = edit.getVirtualContentEntity();
            if (tmp instanceof ContentCollectionFileSystem)
                ccfs = (ContentCollectionFileSystem) tmp;
            else if (tmp instanceof ContentResourceFileSystem)
                ccfs = ((ContentResourceFileSystem) tmp).convertToCollection();
        }
        if (ccfs == null)
            return; // can't do anything if the resource isn't a file system resource!

        // save directory
        String path = ccfs.basePath + ccfs.relativePath;
        try {
            new File(path).mkdirs();
        } catch (SecurityException e) {
            log.warn(
                    "Content Hosting Handler File System was unable to save the contents of a collection to disk because the JVM SecurityManager refused the operation.");
            return; // permissions error -- operation cannot be performed
        }

        // save properties (in a file in the new directory)
        saveProperties(edit, getPropertiesFileName(ccfs),
                ccfs.basePath + propertiesFileFieldSeparator + ccfs.relativePath + propertiesFileFieldSeparator);
    }

    public void commit(ContentResourceEdit edit) {
        ContentResourceFileSystem crfs = null;
        if (edit instanceof ContentResourceFileSystem)
            crfs = (ContentResourceFileSystem) edit;
        else {
            ContentEntity tmp = edit.getVirtualContentEntity();
            if (tmp instanceof ContentResourceFileSystem)
                crfs = (ContentResourceFileSystem) tmp;
        }
        if (crfs == null)
            return; // can't do anything if the resource isn't a file system resource!

        // save properties
        saveProperties(edit, getPropertiesFileName(crfs),
                crfs.basePath + propertiesFileFieldSeparator + crfs.relativePath + propertiesFileFieldSeparator);

        // save file contents
        String path = crfs.basePath + crfs.relativePath;
        InputStream is = null;
        OutputStream os = null;
        try {
            byte b[] = new byte[1024];
            is = edit.streamContent();

            // If the source is another FILE input stream, then this is a copy/paste operation (and not an upload or an
            // edit of a file through the content-tool interface).  Rename target to "Copy of ..." to avoid overwrites.
            if (is instanceof java.io.FileInputStream) {
                while (new File(path).exists()) {
                    int p = path.lastIndexOf("/") + 1;
                    path = path.substring(0, p) + "Copy of " + path.substring(p);
                }
            }

            if (is != null) {
                os = new FileOutputStream(path);
                while (is.available() > 0) {
                    int l = is.read(b);
                    if (l > 0)
                        os.write(b, 0, l);
                    else
                        break;
                }
            }
        } catch (IOException e) {
            log.warn(
                    "Content Hosting Handler File System was unable to save the contents of a resource to disk because the file system refused the operation.");
            return; // file system error -- operation cannot be performed
        } catch (SecurityException e) {
            log.warn(
                    "Content Hosting Handler File System was unable to save the contents of a resource to disk because the JVM SecurityManager refused the operation.");
            return; // permissions error -- operation cannot be performed
        } catch (ServerOverloadException e) {
            log.warn(
                    "Content Hosting Handler File System was unable to save the contents of a resource to disk because the server threw a ServerOverloadException and was unable to stream the file contents.");
            return; // sakai failed to deliver the contents of the file; saving it is obviously impossible
        } finally {
            if (is != null)
                try {
                    is.close();
                } catch (IOException e) {
                }
            if (os != null)
                try {
                    os.flush();
                    os.close();
                } catch (IOException e) {
                }
        }
    }

    public void commitDeleted(ContentResourceEdit edit,
            String uuid) { /* No need to do anything -- removeResource/removeCollection does the work */
    }

    public List getCollections(ContentCollection collection) {
        ContentEntity cc = collection.getVirtualContentEntity();
        if (!(cc instanceof ContentCollectionFileSystem)) {
            return null; // this is not the correct handler for this resource -- serious problems!
        }
        ContentCollectionFileSystem ccfs = (ContentCollectionFileSystem) cc;
        List l = ccfs.getMembers();
        ArrayList<Edit> collections = new ArrayList<Edit>(l.size());
        for (Iterator i = l.listIterator(); i.hasNext();) {
            String id = (String) i.next();
            ContentEntityFileSystem cefs = resolveToFileOrDirectory(ccfs.realParent, ccfs.basePath,
                    id.substring(ccfs.realParent.getId().length() + 1), this, ccfs.showHiddenFiles,
                    ccfs.searchable);
            if (cefs instanceof ContentCollectionFileSystem)
                collections.add(cefs.wrap());
        }
        return collections;
    }

    public List getFlatResources(ContentEntity ce) {
        System.out.println("getFlatResources");
        return null;
    }

    public byte[] getResourceBody(ContentResource resource) throws ServerOverloadException {
        if (!(resource instanceof ContentResourceFileSystem))
            return null;
        ContentResourceFileSystem crfs = (ContentResourceFileSystem) resource;
        return crfs.getContent();
    }

    public List getResources(ContentCollection collection) {
        ContentEntity cc = collection.getVirtualContentEntity();
        if (!(cc instanceof ContentCollectionFileSystem)) {
            return null; // this is not the correct handler for this resource -- serious problems!
        }
        ContentCollectionFileSystem ccfs = (ContentCollectionFileSystem) cc;
        List l = ccfs.getMemberResources();
        return l;
    }

    protected ContentEntityFileSystem resolveToFileOrDirectory(ContentEntity realParent, String basePath,
            String relativePath, ContentHostingHandlerImplFileSystem chh, boolean showHiddenFiles,
            boolean searchable) {
        // return a file (resource) or a directory (collection) as appropriate
        while (relativePath.length() > 0 && relativePath.charAt(0) == '/')
            relativePath = relativePath.substring(1);
        while (relativePath.length() > 0 && relativePath.charAt(relativePath.length() - 1) == '/')
            relativePath = relativePath.substring(0, relativePath.length() - 1);
        relativePath = "/" + relativePath;
        if (basePath.charAt(basePath.length() - 1) == '/')
            basePath = basePath.substring(0, basePath.length() - 1);
        String newpath = basePath + relativePath;
        File f = new File(newpath);
        if (f.isDirectory()) {
            ContentEntityFileSystem cefs = new ContentCollectionFileSystem(realParent, basePath, relativePath, chh,
                    contentHostingHandlerResolver, showHiddenFiles, searchable);
            cefs.wrap();
            return cefs;
        } else {
            ContentEntityFileSystem cefs = new ContentResourceFileSystem(realParent, basePath, relativePath, chh,
                    contentHostingHandlerResolver, showHiddenFiles, searchable);
            cefs.wrap();
            return cefs;
        }
    }

    public ContentEntity getVirtualContentEntity(ContentEntity edit, String finalId) {
        // Algorithm: get the mount point from the XML file represented by 'edit'
        // construct a new ContentEntityFileSystem and return it
        try {
            boolean showHiddenFiles = false;
            boolean searchable = false; // allow the sakai-search tool to index the virtual hierarchy?
            byte[] xml = ((ContentResource) edit).getContent();
            if (xml == null)
                return null;
            DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            if (db == null)
                return null;
            Document d = db.parse(new ByteArrayInputStream(xml));
            if (d == null)
                return null;
            Node node_mountpoint = null;
            NodeList nl = d.getChildNodes();
            for (int j = 0; j < nl.getLength(); ++j)
                if (nl.item(j).getNodeName() != null && nl.item(j).getNodeName().equals(XML_NODE_NAME)) {
                    node_mountpoint = nl.item(j);
                    break;
                }
            if (node_mountpoint == null)
                return null;

            Node node_basepath = node_mountpoint.getAttributes().getNamedItem(XML_ATTRIBUTE_NAME);
            if (node_basepath == null)
                return null;
            final String basepath = node_basepath.getNodeValue();

            Node node_hiddenFiles = node_mountpoint.getAttributes().getNamedItem(XML_ATTRIBUTE_HIDDEN);
            if (node_hiddenFiles != null)
                showHiddenFiles = Boolean.parseBoolean(node_hiddenFiles.getNodeValue());

            Node node_searchable = node_mountpoint.getAttributes().getNamedItem(XML_ATTRIBUTE_SEARCHABLE);
            if (node_searchable != null)
                searchable = Boolean.parseBoolean(node_searchable.getNodeValue());

            if (basepath == null || basepath.equals(""))
                return null; // invalid mountpoint specification

            String relativePath = finalId.substring(edit.getId().length());
            ContentEntityFileSystem cefs = resolveToFileOrDirectory(edit, basepath, relativePath, this,
                    showHiddenFiles, searchable);
            if ("/".equals(finalId.substring(finalId.length() - 1)) && (cefs instanceof ContentResourceFileSystem))
                cefs = ((ContentResourceFileSystem) cefs).convertToCollection();
            Edit ce = cefs.wrap();
            if (ce == null)
                return null; // happens when the requested URL requires a log on but the user is not logged on
            return (ContentEntity) ce;
        } catch (Exception e) {
            log.warn("Invalid XML for the mountpoint [" + edit.getId() + "], error is [" + e + "]");
            return (ContentEntity) (new ContentResourceFileSystem(edit, "/ERROR:invalid_sakai_mount_point", "/",
                    this, contentHostingHandlerResolver, false, false).wrap());
        }
    }

    public void removeCollection(ContentCollectionEdit edit) {
        ContentCollectionFileSystem ccfs = null;
        if (edit instanceof ContentCollectionFileSystem)
            ccfs = (ContentCollectionFileSystem) edit;
        else {
            ContentEntity tmp = edit.getVirtualContentEntity();
            if (tmp instanceof ContentCollectionFileSystem)
                ccfs = (ContentCollectionFileSystem) tmp;
        }
        if (ccfs == null)
            return; // can't do anything if the resource isn't a file system resource!

        // remove meta properties (so new files/dirs created with same name don't 'inherit' them)
        removeProperties(ccfs); // not necessary -- properties file is IN the directory which is about to be removed.
        // even so, i call this incase someone decides to move the properties file later.

        String path = ccfs.basePath + ccfs.relativePath;
        try {
            // remove the properties file (otherwise directory is non-empty and directory removal fails)
            File f = new File(path + propertiesFileName);
            if (f.exists())
                f.delete();

            new File(path).delete();
        } catch (SecurityException e) {
            log.warn(
                    "Content Hosting Handler File System was unable to delete a directory because the JVM SecurityManager refused the operation.");
            return; // permissions error -- operation cannot be performed
        }
    }

    public void removeResource(ContentResourceEdit edit) {
        ContentResourceFileSystem crfs = null;
        if (edit instanceof ContentResourceFileSystem)
            crfs = (ContentResourceFileSystem) edit;
        else {
            ContentEntity tmp = edit.getVirtualContentEntity();
            if (tmp instanceof ContentResourceFileSystem)
                crfs = (ContentResourceFileSystem) tmp;
        }
        if (crfs == null)
            return; // can't do anything if the resource isn't a file system resource!

        // remove meta properties (so new files/dirs created with same name don't 'inherit' them)
        removeProperties(crfs);

        String path = crfs.basePath + crfs.relativePath;
        try {
            new File(path).delete();
        } catch (SecurityException e) {
            log.warn(
                    "Content Hosting Handler File System was unable to delete a file because the JVM SecurityManager refused the operation.");
            return; // permissions error -- operation cannot be performed
        }
    }

    public InputStream streamResourceBody(ContentResource resource) throws ServerOverloadException {
        ContentEntity ce = resource.getVirtualContentEntity();
        if (ce instanceof ContentResourceFileSystem) {
            ContentResourceFileSystem crfs = (ContentResourceFileSystem) ce;
            InputStream is = crfs.streamContent();
            if (is != null)
                return is;
        }
        return resource.streamContent();
    }

    public int getMemberCount(ContentEntity edit) {
        if (edit instanceof ContentCollectionFileSystem)
            return ((ContentCollectionFileSystem) edit).getMemberCount();
        if (edit.getVirtualContentEntity() instanceof ContentCollectionFileSystem)
            return ((ContentCollectionFileSystem) (edit.getVirtualContentEntity())).getMemberCount();
        return 0;
    }

    /* ------------------------------------------------------------------------ */
    /* Support for saving resource/collection properties in a meta file on disk */
    private static final String propertiesFileFieldSeparator = "|";
    private static final String propertiesFileFieldSeparatorAsRegExp = "\\|";
    private static final String propertiesFileName = ".sakai.chhprops";

    private synchronized void saveProperties(Edit edit, String propFileName, String fileIdPrefix) {
        /* First check to see if the resource being saved is the root of the virtual world.
         * If it is, save any changes to the mount point property to the real parent instead
         * of to the root virtual object.  In particular, this allows mount points to be
         * unmounted!
         */
        ContentEntityFileSystem cefs = (ContentEntityFileSystem) ((ContentEntity) edit).getVirtualContentEntity();
        if (cefs.relativePath.equals("/")) {
            // take out an edit object on the real parent...
            ContentResourceEdit cre = this.contentHostingHandlerResolver.editResource(cefs.realParent.getId());
            // ...take out an edit object on its properties...
            ResourcePropertiesEdit rpe = cre.getPropertiesEdit();
            // ...set the CHH Bean property from the virtual object...
            String prop = edit.getPropertiesEdit().getProperty(ContentHostingHandlerResolver.CHH_BEAN_NAME);
            if (prop == null || prop.equals(""))
                rpe.removeProperty(ContentHostingHandlerResolver.CHH_BEAN_NAME);
            else
                rpe.addProperty(ContentHostingHandlerResolver.CHH_BEAN_NAME, prop);
            // ...and save it back again to the storage.
            try {
                this.contentHostingHandlerResolver.commitResource(cre);
            } catch (Exception e) {
                log.warn(
                        "Content Hosting Handler File System was unable to save Sakai properties on the real parent of the virtual mountpoint: "
                                + e.toString());
            }
        }

        // Replace existing properties for content entity cefs with the new ones.
        // Things to take care about:
        //  1. concurrent access to the properties file
        //  2. the directory could be mountable/mounted by two different mountpoints
        //     simultaneously, with DIFFERENT properties on the same file in each case
        //  3. don't load the whole file of properties into RAM because it is HUGE, in many cases
        // Lines in the properties file have the format:
        //  [base mountpoint path] | [relative path of file] | [prop name] | [prop value]
        // the division of the absolute file path into the first two elements ensures that
        // different mount points can have different properties for the same file.
        BufferedReader r = null;
        Writer w = null;
        // Algorithm: write out properties for this file to a new file;
        //            copy properties from old file for everything except this filename;
        //            rename new properties file on top of old one.
        try {
            w = new BufferedWriter(new FileWriter(propFileName + ".new"));
            // write new properties
            ResourceProperties rp = edit.getPropertiesEdit();
            for (Iterator i = rp.getPropertyNames(); i.hasNext();) {
                String s = (String) i.next();
                //            if (!s.startsWith("http://purl.org/dc/")) continue;  // only save dublin core properties
                w.write(fileIdPrefix + s + propertiesFileFieldSeparator + rp.getProperty(s) + "\n");
            }

            // copy old properties (minus any existing ones for this file)
            try {
                r = new BufferedReader(new FileReader(propFileName));
                String line = null;
                while ((line = r.readLine()) != null)
                    if (!line.startsWith(fileIdPrefix))
                        w.write(line + "\n");
                r.close();
            } catch (InvalidObjectException e) {
                /* file doesn't exist / unreadable -- ignore */} catch (FileNotFoundException e) {
                /* file doesn't exist / unreadable -- ignore */}

            // move new file over old one
            w.flush();
            w.close();
            new File(propFileName).delete();
            new File(propFileName + ".new").renameTo(new File(propFileName));
        } catch (IOException e) {
            log.warn("Content Hosting Handler File System was unable to save Sakai properties: " + e.toString());
        } finally {
            if (w != null)
                try {
                    w.flush();
                    w.close();
                } catch (Exception e) {
                }
            if (r != null)
                try {
                    r.close();
                } catch (Exception e) {
                }
            r = null;
            w = null;
        }
    }

    protected synchronized Map loadProperties(ContentEntityFileSystem cefs) {
        Map<String, String> m = new HashMap<String, String>(); // the loaded properties (string -> string lookup table)

        String propFileName = getPropertiesFileName(cefs);
        BufferedReader r = null;
        String fileIdPrefix = cefs.basePath + propertiesFileFieldSeparator + cefs.relativePath
                + propertiesFileFieldSeparator;
        try {
            r = new BufferedReader(new FileReader(propFileName));
            String line = null;
            while ((line = r.readLine()) != null)
                if (line.startsWith(fileIdPrefix)) {
                    String fields[] = line.split(propertiesFileFieldSeparatorAsRegExp, 4);
                    m.put(fields[2], fields[3]);
                }
        } catch (IOException e) {
            /* file doesn't exist / unreadable -- ignore */
        } finally {
            if (r != null)
                try {
                    r.close();
                } catch (Exception e) {
                }
        }
        return m;
    }

    private synchronized void removeProperties(ContentEntityFileSystem cefs) {
        String propFileName = getPropertiesFileName(cefs);
        BufferedReader r = null;
        Writer w = null;
        // Algorithm: copy properties from old file for everything except this filename;
        //            rename new properties file on top of old one.
        String fileIdPrefix = cefs.basePath + propertiesFileFieldSeparator + cefs.relativePath
                + propertiesFileFieldSeparator;
        try {
            w = new BufferedWriter(new FileWriter(propFileName + ".new"));
            try {
                r = new BufferedReader(new FileReader(propFileName));
                String line = null;
                while ((line = r.readLine()) != null)
                    if (!line.startsWith(fileIdPrefix))
                        w.write(line + "\n");
                r.close();
            } catch (InvalidObjectException e) {
                /* file doesn't exist / unreadable -- ignore */} catch (FileNotFoundException e) {
                /* file doesn't exist / unreadable -- ignore */}

            // move new file over old one
            w.flush();
            w.close();
            new File(propFileName).delete();
            new File(propFileName + ".new").renameTo(new File(propFileName));
        } catch (IOException e) {
            log.warn("Content Hosting Handler File System was unable to remove Sakai properties: " + e.toString());
        } finally {
            if (w != null)
                try {
                    w.flush();
                    w.close();
                } catch (Exception e) {
                }
            if (r != null)
                try {
                    r.close();
                } catch (Exception e) {
                }
            r = null;
            w = null;
        }
    }

    private String getPropertiesFileName(ContentEntityFileSystem cefs) {
        return cefs.basePath + cefs.parentRelativePath + (!cefs.parentRelativePath.equals("/") ? "/" : "")
                + propertiesFileName;
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.content.api.ContentHostingHandler#getContentCollectionEdit(java.lang.String)
     */
    public ContentCollectionEdit getContentCollectionEdit(String id) {
        ContentCollectionEdit cce = (ContentCollectionEdit) this.contentHostingHandlerResolver
                .newCollectionEdit(id);
        cce.setContentHandler(this);
        return cce;
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.content.api.ContentHostingHandler#getContentResourceEdit(java.lang.String)
     */
    public ContentResourceEdit getContentResourceEdit(String id) {
        ContentResourceEdit cre = (ContentResourceEdit) this.contentHostingHandlerResolver.newResourceEdit(id);
        cre.setContentHandler(this);
        return cre;
    }

    /* (non-Javadoc)
     * @see org.sakaiproject.content.api.ContentHostingHandler#putDeleteResource(java.lang.String, java.lang.String, java.lang.String)
     */
    public ContentResourceEdit putDeleteResource(String id, String uuid, String userId) {
        ContentResourceEdit cre = (ContentResourceEdit) this.contentHostingHandlerResolver.newResourceEdit(id);
        cre.setContentHandler(this);
        return cre;
    }

    public void getUuid(String id) {
    }

    public void setResourceUuid(String resourceId, String uuid) {
    }

    public Collection<String> getMemberCollectionIds(ContentEntity ce) {
        return null;
    }

    public Collection<String> getMemberResourceIds(ContentEntity ce) {
        return null;
    }

    public String moveResource(ContentResourceEdit thisResource, String new_id) {
        return null;
    }

    public String moveCollection(ContentCollectionEdit thisCollection, String new_folder_id) {
        return null;
    }
}