server.Folder.java Source code

Java tutorial

Introduction

Here is the source code for server.Folder.java

Source

// cinnamon - the Open Enterprise CMS project
// Copyright (C) 2007-2009 Horner GmbH (http://www.horner-project.eu)
// 
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// 
// This library 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
// Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
// (or visit: http://www.gnu.org/licenses/lgpl.html)

package server;

import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Node;
import org.hibernate.Hibernate;
import org.hibernate.annotations.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.dao.*;
import server.data.ObjectSystemData;
import server.data.Validator;
import server.data.ZippedFolder;
import server.exceptions.CinnamonException;
import server.global.Conf;
import server.global.ConfThreadLocal;
import server.global.Constants;
import server.helpers.MetasetService;
import server.helpers.ObjectTreeCopier;
import server.index.IndexAction;
import server.index.IndexJob;
import server.index.Indexable;
import server.index.LuceneBridge;
import server.interfaces.IMetasetJoin;
import server.interfaces.IMetasetOwner;
import server.interfaces.XmlConvertable;
import utils.HibernateSession;
import utils.ParamParser;

import javax.persistence.*;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;

@Entity
@Table(name = "folders", uniqueConstraints = { @UniqueConstraint(columnNames = { "name", "parent_id" }) })
public class Folder implements Serializable, Ownable, Indexable, XmlConvertable, IMetasetOwner {

    private static final long serialVersionUID = 1L;
    static final DAOFactory daoFactory = DAOFactory.instance(DAOFactory.HIBERNATE);

    @Transient
    private transient Logger log = LoggerFactory.getLogger(this.getClass());

    @Id
    @GeneratedValue //(strategy=GenerationType.SEQUENCE)
    @Column(name = "id")
    private long id;

    @Column(name = "name", length = Constants.NAME_LENGTH, nullable = false)
    private String name;

    @Column(name = "metadata", length = Constants.METADATA_SIZE, nullable = false)
    @Type(type = "text")
    private String metadata = "<meta/>";

    @ManyToOne
    @JoinColumn(name = "owner_id", nullable = false)
    private User owner;

    @ManyToOne
    @JoinColumn(name = "parent_id", nullable = true)
    private Folder parent;

    @OneToMany(mappedBy = "parent")
    @OrderBy("name")
    private Set<Folder> children = new LinkedHashSet<Folder>();

    @ManyToOne
    @JoinColumn(name = "type_id", nullable = false)
    private FolderType type;

    @ManyToOne
    @JoinColumn(name = "acl_id", nullable = false)
    private Acl acl;

    @OneToMany(mappedBy = "folder", cascade = { CascadeType.PERSIST, CascadeType.REMOVE })
    private Set<FolderMetaset> folderMetasets = new HashSet<FolderMetaset>();

    @Version
    @Column(name = "obj_version")
    @SuppressWarnings("unused")
    private Long obj_version = 0L;

    public Folder() {

    }

    public Folder(Folder that) {
        name = that.name;
        owner = that.owner;
        parent = that.parent;
        type = that.type;
        acl = that.acl;
        metadata = that.metadata;
    }

    public Folder(String name, String metadata, Acl acl, Folder parent, User owner, FolderType type) {
        this.name = name;
        if (metadata != null) {
            setMetadata(metadata);
        }
        this.acl = acl;
        this.parent = parent;
        this.owner = owner;
        this.type = type;
    }

    public Folder(Map<String, String> cmd) {
        EntityManager em = HibernateSession.getLocalEntityManager();
        // TODO: change constructor to receive valid EM
        FolderDAO folderDao = daoFactory.getFolderDAO(em);

        log.debug("Entering Folder-Constructor");

        this.name = cmd.get("name");
        this.setMetadata(cmd.get("metadata"));
        Long parentId = ParamParser.parseLong(cmd.get("parentid"), "error.param.parent_id");

        log.debug("trying to find parent folder with id " + parentId);

        if (parentId == 0) {
            this.parent = folderDao.findRootFolder();
            log.debug("got root folder:  " + parent);
        } else {
            Folder myParent = folderDao.get(parentId);
            if (myParent == null) { // parent not found
                throw new CinnamonException("error.parent_folder.not_found");
            } else {
                this.parent = myParent;
            }
        }

        // typeid is optional
        if (!cmd.containsKey("typeid")) {
            cmd.put("typeid", "0");
        }
        Long folderTypeId = ParamParser.parseLong(cmd.get("typeid"), "error.param.type_id");
        FolderTypeDAO ftDao = daoFactory.getFolderTypeDAO(em);
        if (folderTypeId == 0) {
            this.type = ftDao.findByName(Constants.FOLDER_TYPE_DEFAULT);
        } else {
            this.type = ftDao.get(folderTypeId);
        }

        log.debug("check if folder already exists.");

        // check whether folder exists in parent folder
        List<Folder> folders = folderDao.findAllByNameAndParentID(name, parent);
        if (!folders.isEmpty()) {
            throw new CinnamonException("error.folder.exists");
        }

        log.debug("set acl for this folder");
        // Set ACL for this folder:
        if (cmd.containsKey("aclid")) {
            Long acl_id = ParamParser.parseLong(cmd.get("aclid"), "error.param.acl_id");
            AclDAO aclDao = daoFactory.getAclDAO(em);
            this.acl = aclDao.get(acl_id);
        } else {
            // a folder normally inherits the parent folder's acl.
            this.acl = parent.acl;
        }

        Long ownerId = ParamParser.parseLong(cmd.get("ownerid"), "error.param.owner_id");
        server.dao.UserDAO userDao = daoFactory.getUserDAO(em);
        User myOwner = userDao.get(ownerId);
        if (myOwner == null) {
            throw new CinnamonException("error.user.not_found");
        }
        owner = myOwner;
    }

    public Folder(Map<String, String> cmd, User owner) {
        this(cmd);
        this.owner = owner;
    }

    public long getId() {
        return id;
    }

    /**
     * @return the compiled metadata of this element (all metasets collected under one meta root element).
     */
    public String getMetadata() {
        // for compatibility: return non-empty metadata, otherwise try to compile metasets
        //       if(metadata.length() > 8 && getFolderMetasets().size() == 0){
        //            return metadata;
        //        }
        Document doc = DocumentHelper.createDocument();
        Element root = doc.addElement("meta");
        for (Metaset m : fetchMetasets()) {
            root.add(Metaset.asElement("metaset", m));
        }
        return doc.asXML();
    }

    /**
     * Set the Metadata on this folder. Tries to parse the submitted string
     * and throws an exception if it is not valid XML.<br/>
     * Note: this parses the meta-xml into metasets and stores them as such.
     * @param metadata the custom metadata
     */
    public void setMetadata(String metadata) {
        setMetadata(metadata, WritePolicy.BRANCH);
    }

    /**
     * Set the Metadata on this item. Tries to parse the submitted string
     * and throws an exception if it is not valid XML.<br/>
     * Note: this parses the meta-xml into metasets and stores them as such.
     * This method will unlink existing metasets if they are missing from the metadata,
     * that is, you cannot submit partial metadata to setMetadata. You must set <em>all</em>
     * metadata if you use this method. (see addMetaset to add an individual metaset) 
     * @param metadata the custom metadata
     * @param writePolicy the WritePolicy - how to treat existing metasets.
     */
    public void setMetadata(String metadata, WritePolicy writePolicy) {
        EntityManager em = HibernateSession.getLocalEntityManager();
        if (metadata == null || metadata.trim().length() == 0) {
            this.metadata = "<meta/>";
            for (FolderMetaset fm : getFolderMetasets()) {
                em.remove(fm);
            }
        } else {
            MetasetService metasetService = new MetasetService();
            Document doc = ParamParser.parseXmlToDocument(metadata, "error.param.metadata");
            List<Node> sets = doc.selectNodes("//metaset");
            if (sets.size() == 0) {
                this.metadata = metadata;
                if (folderMetasets.size() > 0) {
                    // delete obsolete metasets:
                    for (Metaset m : fetchMetasets()) {
                        new MetasetService().unlinkMetaset(this, m);
                    }
                }
                return;
            }

            Set<MetasetType> currentMetasetMap = new HashSet<MetasetType>();
            for (Metaset metaset : fetchMetasets()) {
                // create a set of the currently existing metasets.
                currentMetasetMap.add(metaset.getType());
            }

            for (Node metasetNode : sets) {
                String content = metasetNode.detach().asXML();
                String metasetTypeName = metasetNode.selectSingleNode("@type").getText();
                log.debug("metasetType: " + metasetTypeName);
                MetasetTypeDAO mtDao = daoFactory.getMetasetTypeDAO(em);
                MetasetType metasetType = mtDao.findByName(metasetTypeName);
                if (metasetType == null) {
                    throw new CinnamonException("error.unknown.metasetType", metasetTypeName);
                }
                metasetService.createOrUpdateMetaset(this, metasetType, content, writePolicy);
                currentMetasetMap.remove(metasetType);
            }
            for (MetasetType metasetType : currentMetasetMap) {
                // any metaset that was not found in the metadata parameter will be deleted.                    
                metasetService.unlinkMetaset(this, this.fetchMetaset(metasetType.getName())); // somewhat convoluted.
            }
        }
        // remove legacy metadata
        this.metadata = "<meta />";
    }

    public Folder getParent() {
        return parent;
    }

    public void setParent(Folder parent) {
        this.parent = parent;
    }

    public void setParent(Long parentId) {
        EntityManager em = HibernateSession.getLocalEntityManager();
        log.debug("trying to find parent folder with id " + parentId);

        if (parentId == 0) {
            FolderDAO folderDao = daoFactory.getFolderDAO(em);
            parent = folderDao.findRootFolder();
        } else {
            Folder myParent = em.find(Folder.class, parentId);
            if (myParent == null) {
                throw new CinnamonException("error.parent_folder.not_found");
            }
            parent = myParent;
        }
    }

    public void setAcl(Long aclId) {
        EntityManager em = HibernateSession.getLocalEntityManager();
        AclDAO aclDAO = daoFactory.getAclDAO(em);
        acl = (aclId == 0) ? aclDAO.findByName(Constants.ACL_DEFAULT) : aclDAO.get(aclId);

        if (acl == null) {
            throw new CinnamonException("error.acl.not_found");
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @SuppressWarnings("unused")
    private void setId(long id) {
        this.id = id;
    }

    public Acl getAcl() {
        return acl;
    }

    public void setAcl(Acl acl) {
        this.acl = acl;
    }

    static public boolean folderExists(long id) {
        EntityManager em = HibernateSession.getLocalEntityManager();
        Folder f = em.find(Folder.class, id);
        return f != null;
    }

    static public boolean folderExists(Folder folder) {
        EntityManager em = HibernateSession.getLocalEntityManager();

        if (folder == null) {
            throw new CinnamonException("error.folder.not_found");
        }
        long id = folder.getId();
        Folder f = em.find(Folder.class, id);

        return f != null;
    }

    public boolean is_rootfolder() {
        if (getParent() == null) {
            return false;
        }
        return getParent().getId() == getId();
    }

    /**
     * @return Folder   The workflow.Server will store its Objects in this Folder.
     * @param emx an EntityManager, configured for the current repository.
     */
    // TODO: use DAO
    static public Folder findWorkflowFolder(EntityManager emx) {
        Logger log = LoggerFactory.getLogger("server.Folder");
        FolderDAO folderDao = daoFactory.getFolderDAO(emx);
        Folder root = folderDao.findRootFolder();
        log.debug("Root-Folder-Id is: " + root.getId());

        Folder f = null;
        try {
            Query q = emx.createQuery("select f from Folder f where f.name=:name and f.parent=:parent");
            q.setParameter("name", Constants.WORKFLOW_FOLDER_NAME);
            q.setParameter("parent", root);
            f = (Folder) q.getSingleResult();
        } catch (Exception e) {
            log.warn("Could not find workflow-Folder" + e.getMessage());
        }
        return f;
    }

    /**
     *
     * @return Folder for workflow objects.
     */
    static public Folder findWorkflowFolder() {
        EntityManager em = HibernateSession.getLocalEntityManager();
        return findWorkflowFolder(em);
    }

    public void setChildren(Set<Folder> children) {
        this.children = children;
    }

    public Set<Folder> getChildren() {
        return children;
    }

    /**
     * @return the owner
     */
    public User getOwner() {
        return owner;
    }

    /**
     * @param owner the owner to set
     */
    public void setOwner(User owner) {
        this.owner = owner;
    }

    /**
     * Add the folder as XML Element "folder" to the parameter Element.
     * @param root the element to which the folder element is appended as a child
      * @return the element which was added to the root param.
     */
    public Element toXmlElement(Element root) {
        Element folder = root.addElement("folder");
        folder.addElement("id").addText(String.valueOf(getId()));
        folder.addElement("name").addText(getName());
        folder.add(User.asElement("owner", getOwner()));
        folder.addElement("aclId").addText(String.valueOf(getAcl().getId()));
        folder.addElement("typeId").addText(String.valueOf(type.getId()));
        if (getParent() != null) {
            folder.addElement("parentId").addText(String.valueOf(getParent().getId()));
        } else {
            folder.addElement("parentId");
        }
        FolderDAO fDao = daoFactory.getFolderDAO(HibernateSession.getLocalEntityManager());
        if (fDao.getSubfolders(this).isEmpty()) {
            folder.addElement("hasChildren").addText("false");
        } else {
            folder.addElement("hasChildren").addText("true");
        }
        return folder;
    }

    /**
     * Create a list of folders right up to one step below the root folder (so the
     * root folder is not returned).
     * @return List of parent folders
     */
    List<Folder> getAncestors() {
        List<Folder> folderList = new ArrayList<Folder>();
        Folder folder = this;
        while (folder.getParent() != null && folder.getParent().id != folder.id) {
            folderList.add(folder.getParent());
            folder = folder.getParent();
        }
        return folderList;
    }

    @Override
    public String getContent(String repository) {
        /*
         * Folder.getContent could return a list of children and objects.
         * But what would be the purpose of this in a search context?
         * Is there a use case for "give me all folders which contain foo"
         * instead of searching for all objects/folders named "foo"?
         */
        return "<content/>";
    }

    @Override
    public byte[] getContentAsBytes(String repository) {
        return "<content/>".getBytes();
    }

    @Override
    public String getSystemMetadata() {
        Document doc = DocumentHelper.createDocument();
        Element root = doc.addElement("sysMeta");
        String className = Hibernate.getClass(this).getName();
        root.addAttribute("javaClass", className);
        root.addAttribute("hibernateId", String.valueOf(getId()));
        root.addAttribute("id", uniqueId());
        toXmlElement(root);
        return doc.asXML();
    }

    /**
     * Turn a collection of folders into an XML document. Any exceptions encountered during
     * serialization are turned into error-Elements which contain the exception's message.
     * @param results
     * @return Document
     */
    static public Document generateQueryFolderResultDocument(Collection<Folder> results) {
        Document doc = DocumentHelper.createDocument();
        Element root = doc.addElement("folders");
        Logger log = LoggerFactory.getLogger(ObjectSystemData.class);

        for (Folder folder : results) {
            Long id = folder.getId();
            log.debug("working on object: " + id);
            try {
                folder.toXmlElement(root);
            } catch (CinnamonException ex) {
                log.error("Error serializing folder: " + id + " - " + ex.getMessage());
                Element error = DocumentHelper.createElement("error").addText(ex.getLocalizedMessage());
                error.addElement("id").addText(id.toString());
                root.add(error);
            }
        }
        return doc;
    }

    /**
     *
     * @param recursive set to true if subfolders must be included in the list of OSDs.
     * @return a List of OSDs in this folder
     */
    public List<ObjectSystemData> getOSDList(Boolean recursive) {
        EntityManager em = HibernateSession.getLocalEntityManager();
        FolderDAO folderDao = daoFactory.getFolderDAO(em);

        return folderDao.getFolderContent(this, recursive);
    }

    /**
     * @return the type
     */
    public FolderType getType() {
        return type;
    }

    /**
     * @param type the type to set
     */
    public void setType(FolderType type) {
        this.type = type;
    }

    @Override
    /*
     * Implements Comparable interface based on id.
     * This is used to generate search results with a consistent ordering (where the
     * results are not ordered by any other parameter).
     *
     */
    public int compareTo(XmlConvertable o) {
        if (getId() > o.getId()) {
            return 1;
        } else if (getId() < o.getId()) {
            return -1;
        }
        return 0;
    }

    /**
     * Copy a source folder to a target folder. Copies recursively.
     * @param target the target folder in which the copy is created
     * @param croakOnError if true, do not continue past an error.
     * @param versions must be one of 'all', 'branch', 'head' - determines which OSDs inside the folder tree to copy.
     * @param user the user who issued the copyFolder command. This user is the new owner of the copy and he is also
     * used for permission checks.
     * @return a CopyResult object containing information about new folders and objects as well as error messages.
     */
    public CopyResult copyFolder(Folder target, Boolean croakOnError, String versions, User user) {
        /*
         * validate read permissions on source folder
         * validate write permissions on target folder
         * create folder in target folder
         * create a CopyResult object
         * this folder has sub folders? Copy them. Add their CopyResult to the existing one.
         * this folder has OSDs? Copy them.
         * 
         * 
         */
        CopyResult copyResult = new CopyResult();
        Validator validator = new Validator(user);
        try {
            // we need permission to browse this folder.
            validator.validateGetFolder(this);
        } catch (CinnamonException ce) {
            return copyResult.addFailure(this, ce);
        }

        try {
            // we need the permission to create a folder inside the target folder.
            validator.validateCreateFolder(target);

        } catch (CinnamonException ce) {
            return copyResult.addFailure(target, ce);
        }

        Folder copy = new Folder(this);
        copy.owner = user;
        copy.parent = target;

        EntityManager em = HibernateSession.getLocalEntityManager();
        FolderDAO fDao = daoFactory.getFolderDAO(em);
        fDao.makePersistent(copy);
        copyResult.addFolder(copy);

        // copy child folders
        List<Folder> children = fDao.getSubfolders(this);
        for (Folder child : children) {
            CopyResult cr = child.copyFolder(copy, croakOnError, versions, user);
            copyResult.addCopyResult(cr);
            if (copyResult.foundFailure() && croakOnError) {
                return copyResult;
            }
        }

        ObjectSystemDataDAO oDao = daoFactory.getObjectSystemDataDAO(em);
        // copy content
        Collection<ObjectSystemData> folderContent = oDao.findAllByParent(this);
        log.debug("folderContent contains " + folderContent.size() + " objects.");

        if (versions.equals("all")) {
            log.debug("copy all versions");
            // copy all versions
            ObjectTreeCopier otc = new ObjectTreeCopier(user, copy, validator, true);
            copyResult.addCopyResult(copyAllVersions(folderContent, otc, croakOnError));
        } else if (versions.equals("branch")) {
            log.debug("copy newest branch objects");
            Set<ObjectSystemData> branches = new HashSet<ObjectSystemData>();
            for (ObjectSystemData osd : folderContent) {
                branches.add(oDao.findLatestBranch(osd));
            }
            copyResult.addCopyResult(createNewVersionCopies(branches, copy, validator, user, croakOnError));
        } else {
            log.debug("copy head of object tree");
            // the default: copy head
            Set<ObjectSystemData> headSet = new HashSet<ObjectSystemData>();
            for (ObjectSystemData head : folderContent) {
                ObjectSystemData latestHead = oDao.findLatestHead(head);
                log.debug("latestHead found for " + head.getId());
                headSet.add(latestHead);
            }
            copyResult.addCopyResult(createNewVersionCopies(headSet, copy, validator, user, croakOnError));
        }
        log.debug("new folders: " + copyResult.newFolderCount());
        log.debug("new objects: " + copyResult.newObjectCount());
        return copyResult;
    }

    /**
     * Copy all versions of the objects found in a folder. This will create the complete object tree of
     * the objects, so if an object has ancestors or descendants in other folders, those will be copied, too.
     * @param folderContent the content of the folder which should be copied completely.
     * @param otc a ObjectTreeCopier which is configured with a validator and correct activeUser.
     * @param croakOnError if true, stop in case of an error and return a CopyResult which contains the events so far.
     * @return a copyResult containing a collection of all failed and successful attempts at copying the
     * folder's contents.
     */
    CopyResult copyAllVersions(Collection<ObjectSystemData> folderContent, ObjectTreeCopier otc,
            Boolean croakOnError) {
        ObjectSystemDataDAO oDao = daoFactory.getObjectSystemDataDAO(HibernateSession.getLocalEntityManager());
        CopyResult copyResult = new CopyResult();

        ConcurrentLinkedQueue<ObjectSystemData> conQueue = new ConcurrentLinkedQueue<ObjectSystemData>();
        conQueue.addAll(folderContent);
        log.debug("starting to copy " + conQueue.size() + " objects");

        for (ObjectSystemData source : conQueue) {
            //            otc.resetCopyResult();
            try {
                // create a full copy of the whole object tree:
                otc.createFullCopy(source);
                copyResult.addCopyResult(otc.getCopyResult());
            } catch (Exception ex) {
                log.debug("objectTreeCopy failed for id " + source.getId(), ex);
                // copy failed - now we have to cleanup and remove the already created copies:
                ObjectSystemData brokenCopy = otc.getCopyCache().get(source);
                if (brokenCopy != null) {
                    // we should nuke all other objects with the same root,
                    // as they won't be amendable to a copy operation either.
                    for (ObjectSystemData osd : conQueue) {
                        if (osd.getRoot().equals(brokenCopy.getRoot())) {
                            conQueue.remove(osd);
                        }
                    }

                    // recursively delete the broken object tree.
                    oDao.delete(brokenCopy.getRoot(), true, true);
                }

                log.debug("cleanup complete.");
                copyResult.addFailure(source, new CinnamonException(ex));
                if (croakOnError) {
                    return copyResult;
                }
            }
        }
        return copyResult;
    }

    CopyResult createNewVersionCopies(Collection<ObjectSystemData> sources, Folder target, Validator validator,
            User user, Boolean croakOnError) {
        CopyResult copyResult = new CopyResult();
        ObjectSystemDataDAO oDao = daoFactory.getObjectSystemDataDAO(HibernateSession.getLocalEntityManager());

        for (ObjectSystemData osd : sources) {
            log.debug("trying to copy " + osd.getId());
            // check permissions:
            try {
                validator.validateCopy(osd, target);
            } catch (Exception ex) {
                copyResult.addFailure(osd, new CinnamonException(ex));
                if (croakOnError) {
                    return copyResult;
                }
            }

            ObjectSystemData newCopy = new ObjectSystemData(osd, user);
            newCopy.setVersion("1");
            newCopy.setRoot(newCopy);
            newCopy.setParent(target);
            oDao.makePersistent(newCopy);
            osd.copyContent(newCopy);
            copyResult.addObject(newCopy);
        }
        return copyResult;
    }

    /**
     * Compute this folder's path, which is the combination of all parent folder's names,
     * for example: /system/workflows/templates.
     * @return the path of this folder.
     */
    public String fetchPath() {
        /*
         * getParentFolders returns: "c/b/a" for folders /a/b/c
        */
        FolderDAO folderDao = daoFactory.getFolderDAO(HibernateSession.getLocalEntityManager());
        List<Folder> folders = folderDao.getParentFolders(this);
        StringBuilder path = new StringBuilder();
        Collections.reverse(folders);
        folders.add(this);
        for (Folder f : folders) {
            path.append('/');
            path.append(f.getName());
        }
        return path.toString();
    }

    /**
     * Create a zipped folder containing those OSDs and subfolders (recursively) which the
     * validator allows. <br/>
     * Zip file encoding compatibility is difficult to achieve.<br/>
     * Using Cp437 as encoding will generate zip archives which can be unpacked with MS Windows XP
     * system utilities and also with the Linux unzip tool v6.0 (although the unzip tool will list them
     * as corrupted filenames with "?" in place for the special characters, it should unpack them
     * correctly). In tests, 7zip was unable to unpack those archives without messing up the filenames
     * - it requires UTF8 as encoding, as far as I can tell.<br/>
     * see: http://commons.apache.org/compress/zip.html#encoding<br/>
     * to manually test this, use: https://github.com/dewarim/GrailsBasedTesting
     * @param folderDao data access object for Folder objects
     * @param latestHead if set to true, only add objects with latestHead=true, if set to false include only
     *                   objects with latestHead=false, if set to null: include everything regardless of
     *                   latestHead status.
     * @param latestBranch if set to true, only add objects with latestBranch=true, if set to false include only
     *                   objects with latestBranch=false, if set to null: include everything regardless of
     *                     latestBranch status.
     * @param validator a Validator object which should be configured for the current user to check if access
     *                  to objects and folders inside the given folder is allowed. The content of this folder
     *                  will be filtered before it is added to the archive.
     * @return the zip archive of the given folder
     */
    public ZippedFolder createZippedFolder(FolderDAO folderDao, Boolean latestHead, Boolean latestBranch,
            Validator validator) {
        String repositoryName = HibernateSession.getLocalRepositoryName();
        final File sysTempDir = new File(System.getProperty("java.io.tmpdir"));
        File tempFolder = new File(sysTempDir, UUID.randomUUID().toString());
        if (!tempFolder.mkdirs()) {
            throw new CinnamonException(("error.create.tempFolder.fail"));
        }

        List<Folder> folders = new ArrayList<Folder>();
        folders.add(this);
        folders.addAll(folderDao.getSubfolders(this, true));
        folders = validator.filterUnbrowsableFolders(folders);
        log.debug("# of folders found: " + folders.size());
        // create zip archive:
        ZippedFolder zippedFolder;
        try {
            File zipFile = File.createTempFile("cinnamonArchive", "zip");
            zippedFolder = new ZippedFolder(zipFile, this);

            final OutputStream out = new FileOutputStream(zipFile);
            ZipArchiveOutputStream zos = (ZipArchiveOutputStream) new ArchiveStreamFactory()
                    .createArchiveOutputStream(ArchiveStreamFactory.ZIP, out);
            String encoding = ConfThreadLocal.getConf().getField("zipFileEncoding", "Cp437");

            log.debug("current file.encoding: " + System.getProperty("file.encoding"));
            log.debug("current Encoding for ZipArchive: " + zos.getEncoding() + "; will now set: " + encoding);
            zos.setEncoding(encoding);
            zos.setFallbackToUTF8(true);
            zos.setCreateUnicodeExtraFields(ZipArchiveOutputStream.UnicodeExtraFieldPolicy.ALWAYS);

            for (Folder folder : folders) {

                String path = folder.fetchPath().replace(fetchPath(), name); // do not include the parent folders up to root.
                log.debug("zipFolderPath: " + path);
                File currentFolder = new File(tempFolder, path);
                if (!currentFolder.mkdirs()) {
                    // log.warn("failed  to create folder for: "+currentFolder.getAbsolutePath());
                }
                List<ObjectSystemData> osds = validator.filterUnbrowsableObjects(
                        folderDao.getFolderContent(folder, false, latestHead, latestBranch));
                if (osds.size() > 0) {
                    zippedFolder.addToFolders(folder); // do not add empty folders as they are excluded automatically.
                }
                for (ObjectSystemData osd : osds) {
                    if (osd.getContentSize() == null) {
                        continue;
                    }
                    zippedFolder.addToObjects(osd);
                    File outFile = osd.createFilenameFromName(currentFolder);
                    // the name in the archive should be the path without the temp folder part prepended.
                    String zipEntryPath = outFile.getAbsolutePath().replace(tempFolder.getAbsolutePath(), "");
                    if (zipEntryPath.startsWith(File.separator)) {
                        zipEntryPath = zipEntryPath.substring(1);
                    }
                    log.debug("zipEntryPath: " + zipEntryPath);

                    zipEntryPath = zipEntryPath.replaceAll("\\\\", "/");
                    zos.putArchiveEntry(new ZipArchiveEntry(zipEntryPath));
                    IOUtils.copy(new FileInputStream(osd.getFullContentPath(repositoryName)), zos);
                    zos.closeArchiveEntry();
                }
            }
            zos.close();
        } catch (Exception e) {
            log.debug("Failed to create zipFolder:", e);
            throw new CinnamonException("error.zipFolder.fail", e.getLocalizedMessage());
        }
        return zippedFolder;
    }

    public Boolean hasXmlContent() {
        return false;
    }

    public Set<FolderMetaset> getFolderMetasets() {
        return folderMetasets;
    }

    public void setFolderMetasets(Set<FolderMetaset> folderMetasets) {
        this.folderMetasets = folderMetasets;
    }

    public Set<Metaset> fetchMetasets() {
        Set<Metaset> metasets = new HashSet<Metaset>(getFolderMetasets().size());
        for (FolderMetaset fm : getFolderMetasets()) {
            metasets.add(fm.getMetaset());
        }
        return metasets;
    }

    /**
     * Fetch a metaset by its given name. Returns null in case there is no such metaset
     * associated with this object.
     * @param name the name of the metaset
     * @return the metaset or null
     */
    public Metaset fetchMetaset(String name) {
        Metaset metaset = null;
        for (Metaset m : fetchMetasets()) {
            if (m.getType().getName().equals(name)) {
                metaset = m;
                break;
            }
        }
        return metaset;
    }

    public IMetasetJoin fetchMetasetJoin(MetasetType type) {
        EntityManager em = HibernateSession.getLocalEntityManager();
        Query q = em.createQuery(
                "select o from FolderMetaset o where o.metaset.type=:metasetType and o.folder=:folder");
        q.setParameter("metasetType", type);
        q.setParameter("folder", this);
        return (IMetasetJoin) q.getSingleResult();
    }

    public void addMetaset(Metaset metaset) {
        FolderMetaset om = new FolderMetaset(this, metaset);
        EntityManager em = HibernateSession.getLocalEntityManager();
        em.persist(om);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (!(o instanceof Folder))
            return false;

        Folder folder = (Folder) o;

        if (acl != null ? !acl.equals(folder.acl) : folder.acl != null)
            return false;
        if (metadata != null ? !metadata.equals(folder.metadata) : folder.metadata != null)
            return false;
        if (name != null ? !name.equals(folder.name) : folder.name != null)
            return false;
        if (owner != null ? !owner.equals(folder.owner) : folder.owner != null)
            return false;
        if (parent != null ? !parent.equals(folder.parent) : folder.parent != null)
            return false;
        if (type != null ? !type.equals(folder.type) : folder.type != null)
            return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (metadata != null ? metadata.hashCode() : 0);
        result = 31 * result + (owner != null ? owner.hashCode() : 0);
        return result;
    }

    public void updateIndex() {
        EntityManager em = HibernateSession.getLocalEntityManager();
        IndexJobDAO jobDAO = daoFactory.getIndexJobDAO(em);
        IndexJob indexJob = new IndexJob(this);
        jobDAO.makePersistent(indexJob);
    }

    @PostUpdate
    public void updateIndexOnCommit() {
        LocalRepository.addIndexable(this, IndexAction.UPDATE);
    }

    @PostPersist
    public void addToIndexOnCommit() {
        LocalRepository.addIndexable(this, IndexAction.ADD);
    }

    @PostRemove
    public void removeFromIndex() {
        LocalRepository.addIndexable(this, IndexAction.REMOVE);
    }

    @Override
    public Long myId() {
        return id;
    }

    @Override
    public Indexable reload() {
        EntityManager em = HibernateSession.getLocalEntityManager();
        em.refresh(this);
        return this;
    }

    @Override
    public String uniqueId() {
        String className = Hibernate.getClass(this).getName();
        return className + "@" + getId();
    }
}