org.olat.core.util.vfs.version.VersionsFileManager.java Source code

Java tutorial

Introduction

Here is the source code for org.olat.core.util.vfs.version.VersionsFileManager.java

Source

/**
 * <a href="http://www.openolat.org">
 * OpenOLAT - Online Learning and Training</a><br>
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); <br>
 * you may not use this file except in compliance with the License.<br>
 * You may obtain a copy of the License at the
 * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
 * <p>
 * Unless required by applicable law or agreed to in writing,<br>
 * software distributed under the License is distributed on an "AS IS" BASIS, <br>
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
 * See the License for the specific language governing permissions and <br>
 * limitations under the License.
 * <p>
 * Initial code contributed and copyrighted by<br>
 * frentix GmbH, http://www.frentix.com
 * <p>
 */
package org.olat.core.util.vfs.version;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.zip.Adler32;
import java.util.zip.Checksum;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.olat.core.commons.modules.bc.FolderConfig;
import org.olat.core.commons.modules.bc.meta.MetaInfo;
import org.olat.core.commons.modules.bc.meta.tagged.MetaTagged;
import org.olat.core.configuration.Initializable;
import org.olat.core.id.Identity;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.util.StringHelper;
import org.olat.core.util.async.ProgressDelegate;
import org.olat.core.util.vfs.LocalFileImpl;
import org.olat.core.util.vfs.LocalFolderImpl;
import org.olat.core.util.vfs.LocalImpl;
import org.olat.core.util.vfs.MergeSource;
import org.olat.core.util.vfs.NamedContainerImpl;
import org.olat.core.util.vfs.OlatRelPathImpl;
import org.olat.core.util.vfs.VFSConstants;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.VFSManager;
import org.olat.core.util.vfs.filters.VFSItemSuffixFilter;
import org.olat.core.util.vfs.filters.VFSLeafFilter;
import org.olat.core.util.vfs.restapi.SystemItemFilter;
import org.olat.core.util.xml.XStreamHelper;

import com.thoughtworks.xstream.XStream;

/**
 * 
 * Description:<br>
 * This implementation of the VersionsManager saved the revisions of a file in a
 * file with the same name as the original + ".xml". This xml file is saved in
 * a parallel folder .version under the root defined in FolderConfig. Every revision'file
 * have a name made of a generated unique id + the name of the original file.
 * 
 * <P>
 * Initial Date:  21 sept. 2009 <br>
 *
 * @author srosse
 */
public class VersionsFileManager extends VersionsManager implements Initializable {
    private static final OLog log = Tracing.createLoggerFor(VersionsFileManager.class);

    private static final Versions NOT_VERSIONED = new NotVersioned();
    private static final Pattern TAG_PATTERN = Pattern.compile("\\s*[<>]\\s*");
    private static XStream mystream;

    private File rootFolder;
    private File rootVersionFolder;
    private VFSContainer rootVersionsContainer;

    private FolderVersioningConfigurator versioningConfigurator;

    /**
     * [spring]
     */
    private VersionsFileManager() {
        INSTANCE = this;
    }

    /**
     * [used by Spring]
     * @param versioningConfigurator
     */
    public void setVersioningConfigurator(FolderVersioningConfigurator versioningConfigurator) {
        this.versioningConfigurator = versioningConfigurator;
    }

    @Override
    public Versions createVersionsFor(VFSLeaf leaf) {
        return createVersionsFor(leaf, false);
    }

    @Override
    public Versions createVersionsFor(VFSLeaf leaf, boolean force) {
        if (!(leaf instanceof Versionable)) {
            return NOT_VERSIONED;
        } else if (isVersionFile(leaf)) {
            return NOT_VERSIONED;
        }

        Versions versions = readVersions(leaf, false);
        return versions;
    }

    @Override
    public List<Versions> getDeletedFiles(VFSContainer container) {
        List<Versions> deletedRevisions = new ArrayList<Versions>();

        VFSContainer versionContainer = getCanonicalVersionFolder(container, false);
        if (versionContainer != null) {
            Set<String> currentNames = new HashSet<String>();
            for (VFSItem item : container.getItems(new VFSLeafFilter())) {
                currentNames.add(item.getName() + ".xml");
            }

            List<VFSItem> versionItems = versionContainer.getItems(new VFSItemSuffixFilter(new String[] { "xml" }));
            for (VFSItem versionItem : versionItems) {
                String name = versionItem.getName();
                if (versionItem instanceof VFSLeaf && !currentNames.contains(name)
                        && isVersionsXmlFile((VFSLeaf) versionItem)) {
                    Versions versions = readVersions(null, (VFSLeaf) versionItem);
                    if (versions != null) {
                        List<VFSRevision> revisions = versions.getRevisions();
                        if (!revisions.isEmpty()) {
                            deletedRevisions.add(versions);
                        }
                    }
                }
            }
        }
        return deletedRevisions;
    }

    private Versions readVersions(VFSLeaf leaf, boolean create) {
        VFSLeaf fVersions = getCanonicalVersionXmlFile(leaf, create);
        if (!create && fVersions == null) {
            VersionsFileImpl versions = new VersionsFileImpl();
            versions.setCurrentVersion((Versionable) leaf);
            versions.setVersioned(isVersioned(leaf));
            versions.setRevisionNr(getNextRevisionNr(versions));
            return versions;
        }
        return readVersions(leaf, fVersions);
    }

    private boolean isVersionsXmlFile(VFSLeaf fVersions) {
        if (fVersions == null || !fVersions.exists()) {
            return false;
        }
        InputStream in = fVersions.getInputStream();
        if (in == null) {
            return false;
        }

        Scanner scanner = new Scanner(in);
        scanner.useDelimiter(TAG_PATTERN);

        boolean foundVersionsTag = false;
        while (scanner.hasNext()) {
            String tag = scanner.next();
            if ("versions".equals(tag)) {
                foundVersionsTag = true;
                break;
            }
        }

        scanner.close();
        IOUtils.closeQuietly(in);
        return foundVersionsTag;
    }

    private Versions readVersions(VFSLeaf leaf, VFSLeaf fVersions) {
        if (fVersions == null) {
            return new NotVersioned();
        }

        try {
            VFSContainer fVersionContainer = fVersions.getParentContainer();
            VersionsFileImpl versions = (VersionsFileImpl) XStreamHelper.readObject(mystream, fVersions);
            versions.setVersionFile(fVersions);
            versions.setCurrentVersion((Versionable) leaf);
            if (versions.getRevisionNr() == null || versions.getRevisionNr().length() == 0) {
                versions.setRevisionNr(getNextRevisionNr(versions));
            }

            for (VFSRevision revision : versions.getRevisions()) {
                RevisionFileImpl revisionImpl = (RevisionFileImpl) revision;
                revisionImpl.setContainer(fVersionContainer);
            }
            return versions;
        } catch (Exception e) {
            log.warn("This file is not a versions XML file: " + fVersions, e);
            fVersions.delete();
            VersionsFileImpl versions = new VersionsFileImpl();
            versions.setCurrentVersion((Versionable) leaf);
            versions.setVersioned(isVersioned(leaf));
            versions.setRevisionNr(getNextRevisionNr(versions));
            log.warn("Deleted corrupt version XML file and created new version XML file: " + versions);
            // the old revisions can not be restored automatically. They are still on disk, you could recover them
            // manually. This is not a perfect solution, but at least the user does not get an RS
            return versions;
        }
    }

    @Override
    public boolean addVersion(Versionable currentVersion, Identity identity, String comment, InputStream newFile) {
        VFSLeaf currentFile = (VFSLeaf) currentVersion;
        if (addToRevisions(currentVersion, identity, comment)) {
            // copy the content of the new file to the old
            boolean closeInputStream = !(newFile instanceof net.sf.jazzlib.ZipInputStream
                    || newFile instanceof java.util.zip.ZipInputStream);
            if (VFSManager.copyContent(newFile, currentFile, closeInputStream)) {
                return true;
            }
        } else {
            log.error("Cannot create a version of this file: " + currentVersion);
        }
        return false;
    }

    @Override
    public boolean move(VFSLeaf currentFile, VFSLeaf targetFile, Identity author) {
        VFSLeaf fCurrentVersions = getCanonicalVersionXmlFile(currentFile, true);
        Versions currentVersions = readVersions(currentFile, fCurrentVersions);

        boolean brandNewVersionFile = false;
        VFSLeaf fTargetVersions = getCanonicalVersionXmlFile(targetFile, false);
        if (fTargetVersions == null) {
            brandNewVersionFile = true;
            fTargetVersions = getCanonicalVersionXmlFile(targetFile, true);
        }

        Versions targetVersions = readVersions(targetFile, fTargetVersions);
        if (!(currentVersions instanceof VersionsFileImpl) || !(targetVersions instanceof VersionsFileImpl)) {
            return false;
        }

        VersionsFileImpl targetVersionsImpl = (VersionsFileImpl) targetVersions;
        if (author != null) {
            targetVersionsImpl.setAuthor(author.getName());
        }
        if (brandNewVersionFile) {
            targetVersionsImpl.setCreator(currentVersions.getCreator());
            targetVersionsImpl.setComment(currentVersions.getComment());
        }

        boolean allOk = true;
        for (VFSRevision revision : currentVersions.getRevisions()) {
            allOk &= copyRevision(revision, fTargetVersions, targetVersionsImpl);
        }

        targetVersionsImpl.setRevisionNr(getNextRevisionNr(targetVersionsImpl));
        XStreamHelper.writeObject(mystream, fTargetVersions, targetVersionsImpl);

        return allOk;
    }

    private boolean copyRevision(VFSRevision revision, VFSLeaf fNewVersions, VersionsFileImpl targetVersions) {
        if (!(revision instanceof RevisionFileImpl)) {
            logWarn("Copy only copy persisted revisions", null);
        }

        RevisionFileImpl revisionImpl = (RevisionFileImpl) revision;
        String revUuid = revisionImpl.getUuid();
        for (VFSRevision rev : targetVersions.getRevisions()) {
            if (rev instanceof RevisionFileImpl) {
                RevisionFileImpl fRev = (RevisionFileImpl) rev;
                if (StringHelper.containsNonWhitespace(fRev.getUuid()) && fRev.getUuid().equals(revUuid)) {
                    return true;
                }
            }
        }

        String uuid = UUID.randomUUID().toString().replace("-", "") + "_" + revision.getName();

        RevisionFileImpl newRevision = new RevisionFileImpl();
        newRevision.setName(revision.getName());
        newRevision.setFilename(uuid);
        newRevision.setRevisionNr(getNextRevisionNr(targetVersions));
        newRevision.setComment(revision.getComment());
        newRevision.setAuthor(revision.getAuthor());
        newRevision.setLastModified(revision.getLastModified());
        newRevision.setUuid(revUuid);

        //copy -> the files revision
        InputStream revisionIn = revision.getInputStream();

        VFSLeaf target = fNewVersions.getParentContainer().createChildLeaf(uuid);
        if (VFSManager.copyContent(revisionIn, target)) {
            targetVersions.setComment(revision.getComment());
            targetVersions.getRevisions().add(newRevision);
            targetVersions.setRevisionNr(getNextRevisionNr(targetVersions));
            targetVersions.setAuthor(revision.getAuthor());
            return true;
        }
        return false;
    }

    @Override
    public boolean move(Versionable currentVersion, VFSContainer container) {
        VFSLeaf currentFile = (VFSLeaf) currentVersion;
        VFSLeaf fVersions = getCanonicalVersionXmlFile(currentFile, true);
        Versions versions = readVersions(currentFile, fVersions);

        VFSContainer versionContainer = getCanonicalVersionFolder(container, true);

        boolean allOk = VFSConstants.YES.equals(versionContainer.copyFrom(fVersions));
        for (VFSRevision revision : versions.getRevisions()) {
            RevisionFileImpl revisionImpl = (RevisionFileImpl) revision;
            VFSLeaf revisionFile = revisionImpl.getFile();
            if (revisionFile != null) {
                allOk &= VFSConstants.YES.equals(versionContainer.copyFrom(revisionFile));
            }
        }

        allOk &= VFSConstants.YES.equals(fVersions.delete());
        for (VFSRevision revision : versions.getRevisions()) {
            VFSLeaf revisionFile = ((RevisionFileImpl) revision).getFile();
            if (revisionFile != null) {
                allOk &= VFSConstants.YES.equals(revisionFile.delete());
            }
        }
        return allOk;
    }

    @Override
    public boolean restore(Versionable currentVersion, VFSRevision version, String comment) {
        VFSLeaf currentFile = (VFSLeaf) currentVersion;
        if (!VFSManager.exists(currentFile)) {
            return false;
        }

        // add current version to versions file
        if (addToRevisions(currentVersion, null, comment)) {
            // copy the content of the new file to the old
            if (VFSManager.copyContent(version.getInputStream(), currentFile)) {
                return true;
            }
        } else {
            log.error("Cannot create a version of this file: " + currentVersion);
        }

        return false;
    }

    @Override
    public boolean restore(VFSContainer container, VFSRevision revision) {
        String filename = revision.getName();
        VFSItem restoredItem = container.resolve(filename);
        if (restoredItem == null) {
            restoredItem = container.createChildLeaf(filename);
        }
        if (restoredItem instanceof VFSLeaf) {
            VFSLeaf restoredLeaf = (VFSLeaf) restoredItem;
            InputStream inStream = revision.getInputStream();
            if (VFSManager.copyContent(inStream, restoredLeaf)) {
                VFSLeaf versionFile = getCanonicalVersionXmlFile(restoredLeaf, true);
                Versions versions = readVersions(restoredLeaf, versionFile);
                if (versions instanceof VersionsFileImpl) {
                    versions.getRevisions().remove(revision);
                    ((VersionsFileImpl) versions).setRevisionNr(getNextRevisionNr(versions));
                }
                XStreamHelper.writeObject(mystream, versionFile, versions);
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean deleteRevisions(Versionable currentVersion, List<VFSRevision> versionsToDelete) {
        VFSLeaf currentFile = (VFSLeaf) currentVersion;
        Versions versions = readVersions(currentFile, true);
        List<VFSRevision> allVersions = versions.getRevisions();

        Map<String, VFSLeaf> filenamesToDelete = new HashMap<String, VFSLeaf>(allVersions.size());
        for (VFSRevision versionToDelete : versionsToDelete) {
            RevisionFileImpl versionImpl = (RevisionFileImpl) versionToDelete;
            for (Iterator<VFSRevision> allVersionIt = allVersions.iterator(); allVersionIt.hasNext();) {
                RevisionFileImpl allVersionImpl = (RevisionFileImpl) allVersionIt.next();
                if (allVersionImpl.getFilename() != null
                        && allVersionImpl.getFilename().equals(versionImpl.getFilename())) {
                    allVersionIt.remove();
                    break;
                }
            }

            VFSLeaf fileToDelete = versionImpl.getFile();
            if (fileToDelete != null) {
                filenamesToDelete.put(fileToDelete.getName(), fileToDelete);
            }
        }

        for (VFSRevision survivingVersion : allVersions) {
            RevisionFileImpl survivingVersionImpl = (RevisionFileImpl) survivingVersion;
            VFSLeaf revFile = survivingVersionImpl.getFile();
            if (filenamesToDelete.containsKey(revFile.getName())) {
                filenamesToDelete.remove(revFile.getName());
            }
        }

        for (VFSLeaf fileToDelete : filenamesToDelete.values()) {
            fileToDelete.deleteSilently();
        }

        VFSLeaf versionFile = getCanonicalVersionXmlFile(currentFile, true);
        XStreamHelper.writeObject(mystream, versionFile, versions);
        if (currentVersion.getVersions() instanceof VersionsFileImpl) {
            ((VersionsFileImpl) currentVersion.getVersions()).update(versions);
        }
        return true;
    }

    @Override
    public boolean deleteVersions(List<Versions> versions) {
        for (Versions versionToDelete : versions) {
            if (versionToDelete instanceof VersionsFileImpl) {
                VersionsFileImpl versionsImpl = (VersionsFileImpl) versionToDelete;
                VFSLeaf versionFile = versionsImpl.getVersionFile();
                if (versionFile != null) {
                    //robust against manual file system manipulation
                    versionFile.deleteSilently();
                }
                for (VFSRevision revisionToDelete : versionsImpl.getRevisions()) {
                    RevisionFileImpl versionImpl = (RevisionFileImpl) revisionToDelete;
                    VFSLeaf fileToDelete = versionImpl.getFile();
                    if (fileToDelete != null) {
                        fileToDelete.deleteSilently();
                    }
                }
            }
        }
        return true;
    }

    @Override
    public boolean delete(VFSItem item, boolean force) {
        if (item instanceof VFSContainer) {
            if (force) {
                VFSContainer container = (VFSContainer) item;
                VFSContainer versionContainer = getCanonicalVersionFolder(container, false);
                if (versionContainer == null) {
                    return true;
                }
                return VFSConstants.YES.equals(versionContainer.delete());
            }
            return true;
        } else if (item instanceof VFSLeaf && item instanceof Versionable) {
            VFSLeaf leaf = (VFSLeaf) item;
            if (force || isTemporaryFile(leaf)) {
                cleanUp(leaf);
            } else {
                Identity identity = ThreadLocalUserActivityLogger.getLoggedIdentity();
                addToRevisions((Versionable) leaf, identity, null);
            }
        }
        return false;
    }

    /**
     * Some temporary/lock files of specific editors need to be force deleted
     * with all versions. Word can reuse older names.
     * @param leaf
     * @return
     */
    private boolean isTemporaryFile(VFSLeaf leaf) {
        String name = leaf.getName();
        //temporary files
        if (name.endsWith(".tmp")) {
            //Word 2010: ~WRD0002.tmp
            if (name.startsWith("~WRD") || name.startsWith("~WRL")) {
                return true;
            }
            //PowerPoint 2010: ppt5101.tmp 
            if (name.startsWith("ppt")) {
                return true;
            }
        }
        //lock files of Word 2010, Excel 2010, PowerPoint 2010:
        if (name.startsWith("~$") && (name.endsWith(".docx") || name.endsWith(".xlsx") || name.endsWith(".pptx"))) {
            return true;
        }

        //OpenOffice locks: .~lock.Versions_21.odt#
        if (name.startsWith(".~lock.") && (name.endsWith(".odt#") /* Writer */ || name.endsWith(".ods#") /* Calc */
                || name.endsWith(".odp#") /* Impress */ || name.endsWith("odf#") /* Math */
                || name.endsWith(".odg#") /* Draw */)) {
            return true;
        }
        //OpenOffice database lock
        if (name.endsWith(".odb.lck")) {
            return true;
        }

        return false;
    }

    /**
     * Clean up all revisions files, xml file
     * @param leaf
     */
    private void cleanUp(VFSLeaf leaf) {
        String relPath = getRelPath(leaf);
        if (relPath == null)
            return; // cannot handle

        File fVersion = new File(getRootVersionsFile(), relPath + ".xml");
        File fParentVersion = fVersion.getParentFile();
        if (!fParentVersion.exists())
            return; //already deleted

        VFSLeaf versionLeaf = null;
        if (fVersion.exists()) {
            LocalFolderImpl localVersionContainer = new LocalFolderImpl(fParentVersion);
            versionLeaf = (VFSLeaf) localVersionContainer.resolve(fVersion.getName());
        }

        if (versionLeaf == null)
            return; //already deleted
        Versions versions = readVersions(leaf, versionLeaf);
        for (VFSRevision versionToDelete : versions.getRevisions()) {
            RevisionFileImpl versionImpl = (RevisionFileImpl) versionToDelete;
            VFSLeaf fileToDelete = versionImpl.getFile();
            if (fileToDelete != null) {
                fileToDelete.delete();
            }
        }
        versionLeaf.delete();
    }

    @Override
    public boolean rename(VFSItem item, String newname) {
        if (item instanceof VFSLeaf) {
            VFSLeaf currentFile = (VFSLeaf) item;
            VFSLeaf versionFile = getCanonicalVersionXmlFile(currentFile, true);
            // infinite loop if rename is own versions file
            return VFSConstants.YES.equals(versionFile.rename(newname + ".xml"));
        } else if (item instanceof VFSContainer) {
            VFSContainer container = (VFSContainer) item;
            VFSContainer versionContainer = getCanonicalVersionFolder(container, false);
            if (versionContainer == null) {
                return true;
            }
            return VFSConstants.YES.equals(versionContainer.rename(newname));
        }
        return false;
    }

    /**
     * @see org.olat.core.util.vfs.version.VersionsManager#addToRevisions(org.olat.core.util.vfs.version.Versionable, org.olat.core.id.Identity, java.lang.String)
     */
    @Override
    public boolean addToRevisions(Versionable currentVersion, Identity identity, String comment) {
        int maxNumOfVersions = versioningConfigurator.getMaxNumOfVersionsAllowed();
        if (maxNumOfVersions == 0) {
            return true;//deactivated, return all ok
        }

        VFSLeaf currentFile = (VFSLeaf) currentVersion;

        VFSLeaf versionFile = getCanonicalVersionXmlFile(currentFile, true);
        if (versionFile == null) {
            return false;//cannot do something with the current file
        }

        VFSContainer versionContainer = versionFile.getParentContainer();

        String name = currentFile.getName();

        // read from the
        Versions v = readVersions(currentFile, versionFile);
        if (!(v instanceof VersionsFileImpl)) {
            log.error("Wrong implementation of Versions: " + v);
            return false;
        }
        VersionsFileImpl versions = (VersionsFileImpl) v;
        boolean sameFile = isSameFile(currentFile, versions);
        String uuid = sameFile ? getLastRevisionFilename(versions) : UUID.randomUUID().toString() + "_" + name;

        String versionNr = getNextRevisionNr(versions);
        String currentAuthor = versions.getAuthor();
        long lastModifiedDate = 0;
        if (currentFile instanceof MetaTagged) {
            MetaInfo metaInfo = ((MetaTagged) currentFile).getMetaInfo();
            if (metaInfo != null) {
                metaInfo.clearThumbnails();
                if (currentAuthor == null) {
                    currentAuthor = metaInfo.getAuthor();
                }
                lastModifiedDate = metaInfo.getLastModified();
            }
        }

        if (lastModifiedDate <= 0) {
            Calendar cal = Calendar.getInstance();
            cal.setTime(new Date());
            lastModifiedDate = cal.getTimeInMillis();
        }

        RevisionFileImpl newRevision = new RevisionFileImpl();
        newRevision.setUuid(UUID.randomUUID().toString());
        newRevision.setName(name);
        newRevision.setFilename(uuid);
        newRevision.setRevisionNr(versionNr);
        newRevision.setComment(versions.getComment());
        newRevision.setAuthor(currentAuthor);
        newRevision.setLastModified(lastModifiedDate);

        if (versions.getRevisions().isEmpty() && currentVersion instanceof MetaTagged) {
            MetaTagged metaTagged = (MetaTagged) currentVersion;
            versions.setCreator(metaTagged.getMetaInfo().getAuthor());
        }

        if (sameFile || VFSManager.copyContent(currentFile, versionContainer.createChildLeaf(uuid))) {
            if (identity != null) {
                versions.setAuthor(identity.getName());
            }

            if (maxNumOfVersions >= 0 && versions.getRevisions().size() >= maxNumOfVersions) {
                List<VFSRevision> revisions = versions.getRevisions();
                int numOfVersionsToDelete = Math.min(revisions.size(), (revisions.size() - maxNumOfVersions) + 1);
                if (numOfVersionsToDelete > 0) {
                    List<VFSRevision> versionsToDelete = revisions.subList(0, numOfVersionsToDelete);
                    deleteRevisions(currentVersion, versionsToDelete);
                    versions = (VersionsFileImpl) currentVersion.getVersions();
                }
            }
            versions.setComment(comment);
            versions.getRevisions().add(newRevision);
            versions.setRevisionNr(getNextRevisionNr(versions));
            XStreamHelper.writeObject(mystream, versionFile, versions);
            if (currentVersion.getVersions() instanceof VersionsFileImpl) {
                ((VersionsFileImpl) currentVersion.getVersions()).update(versions);
            }
            return true;
        } else {
            log.error("Cannot create a version of this file: " + currentVersion);
        }
        return false;
    }

    private boolean isSameFile(VFSLeaf currentFile, VersionsFileImpl versions) {
        boolean same = false;
        if (versions.getRevisions() != null && !versions.getRevisions().isEmpty()) {
            VFSRevision lastRevision = versions.getRevisions().get(versions.getRevisions().size() - 1);

            long lastSize = lastRevision.getSize();
            long currentSize = currentFile.getSize();
            if (currentSize == lastSize && currentSize > 0 && lastRevision instanceof RevisionFileImpl
                    && currentFile instanceof LocalFileImpl) {
                RevisionFileImpl lastRev = ((RevisionFileImpl) lastRevision);
                LocalFileImpl current = (LocalFileImpl) currentFile;
                //can be the same file
                try {
                    Checksum cm1 = FileUtils.checksum(((LocalFileImpl) lastRev.getFile()).getBasefile(),
                            new Adler32());
                    Checksum cm2 = FileUtils.checksum(current.getBasefile(), new Adler32());
                    same = cm1.getValue() == cm2.getValue();
                } catch (IOException e) {
                    log.debug("Error calculating the checksum of files");
                }
            }
        }
        return same;
    }

    public String getNextRevisionNr(Versions versions) {
        int maxNumber = 0;
        for (VFSRevision version : versions.getRevisions()) {
            String versionNr = version.getRevisionNr();
            if (versionNr != null && versionNr.length() > 0) {
                try {
                    int number = Integer.parseInt(versionNr);
                    maxNumber = Math.max(maxNumber, number);
                } catch (Exception ex) {
                    // if not a number, don't interest us
                }
            }
        }
        return Integer.toString(maxNumber + 1);
    }

    private String getLastRevisionFilename(Versions versions) {
        if (versions.getRevisions() == null || versions.getRevisions().isEmpty()) {
            return null;
        }

        VFSRevision revision = versions.getRevisions().get(versions.getRevisions().size() - 1);
        if (revision instanceof RevisionFileImpl) {
            return ((RevisionFileImpl) revision).getFilename();
        }
        return null;
    }

    /**
     * Get the canonical path to the file's meta file.
     * 
     * @param bcPath
     * @return String
     */
    private VFSLeaf getCanonicalVersionXmlFile(VFSItem item, boolean create) {
        File f = getOriginFile(item);
        if (!f.exists()) {
            return null;
        }

        String relPath = getRelPath(item);
        if (relPath == null) {
            // cannot handle
            return null;
        }

        File fVersion = new File(getRootVersionsFile(), relPath + ".xml");
        File fParentVersion = fVersion.getParentFile();
        if (!fParentVersion.exists() && create) {
            fParentVersion.mkdirs();
        }

        if (fVersion.exists()) {
            LocalFolderImpl localVersionContainer = new LocalFolderImpl(fParentVersion);
            return (VFSLeaf) localVersionContainer.resolve(fVersion.getName());
        } else if (create) {
            LocalFolderImpl localVersionContainer = new LocalFolderImpl(fParentVersion);
            VersionsFileImpl versions = new VersionsFileImpl();
            versions.setVersioned(isVersioned(item));
            versions.setRevisionNr(getNextRevisionNr(versions));
            VFSLeaf fVersions = localVersionContainer.createChildLeaf(fVersion.getName());
            XStreamHelper.writeObject(mystream, fVersions, versions);
            return fVersions;
        }
        return null;
    }

    protected VFSContainer getCanonicalVersionFolder(VFSContainer container, boolean create) {
        String relPath = getRelPath(container);
        File fVersion = new File(getRootVersionsFile(), relPath);
        if (fVersion.exists()) {
            return new LocalFolderImpl(fVersion);
        }
        if (create) {
            fVersion.mkdirs();
            return new LocalFolderImpl(fVersion);
        }
        return null;
    }

    private String getRelPath(VFSItem item) {
        String relPath = null;
        if (item instanceof NamedContainerImpl) {
            item = ((NamedContainerImpl) item).getDelegate();
        }
        if (item instanceof MergeSource) {
            item = ((MergeSource) item).getRootWriteContainer();
        }
        if (item instanceof OlatRelPathImpl) {
            relPath = ((OlatRelPathImpl) item).getRelPath();
        } else if (item instanceof LocalImpl) {
            LocalImpl impl = (LocalImpl) item;
            String absolutPath = impl.getBasefile().getAbsolutePath();
            if (absolutPath.startsWith(getCanonicalRoot())) {
                relPath = absolutPath.substring(getCanonicalRoot().length());
            }

            Path path = impl.getBasefile().toPath();
            Path relativePath = getCanonicalRootFile().toPath().relativize(path);
            String relPath2 = "/" + relativePath.toString();
            log.debug(relPath + " :: " + relPath2);
        }
        return relPath;
    }

    private boolean isVersionFile(VFSItem item) {
        File f = getOriginFile(item);
        if (f == null)
            return false;

        try {
            String path = f.getCanonicalPath();
            String vPath = getRootVersionsFile().getCanonicalPath();
            return path.startsWith(vPath);
        } catch (IOException e) {
            log.error("Cannot check if this file is a version file: " + item, e);
            return false;
        }
    }

    private boolean isVersioned(VFSItem item) {
        if (item == null)
            return false;
        VFSContainer parent = item.getParentContainer();
        return FolderConfig.versionsEnabled(parent);
    }

    private File getOriginFile(VFSItem item) {
        if (item instanceof LocalImpl) {
            LocalImpl localImpl = (LocalImpl) item;
            return localImpl.getBasefile();
        }
        if (item instanceof OlatRelPathImpl) {
            OlatRelPathImpl relPath = (OlatRelPathImpl) item;
            return new File(getCanonicalRoot(), relPath.getRelPath());
        }
        return null;
    }

    public File getCanonicalRootFile() {
        if (rootFolder == null) {
            rootFolder = new File(FolderConfig.getCanonicalRoot());
        }
        return rootFolder;
    }

    public String getCanonicalRoot() {
        return getCanonicalRootFile().getAbsolutePath();
    }

    public File getRootVersionsFile() {
        if (rootVersionsContainer == null) {
            rootVersionFolder = new File(FolderConfig.getCanonicalVersionRoot());
            if (!rootVersionFolder.exists()) {
                rootVersionFolder.mkdirs();
            }
            rootVersionsContainer = new LocalFolderImpl(rootVersionFolder);
        }
        return rootVersionFolder;
    }

    public VFSContainer getRootVersionsContainer() {
        if (rootVersionsContainer == null) {
            rootVersionFolder = new File(FolderConfig.getCanonicalVersionRoot());
            if (!rootVersionFolder.exists()) {
                rootVersionFolder.mkdirs();
            }
            rootVersionsContainer = new LocalFolderImpl(rootVersionFolder);
        }
        return rootVersionsContainer;
    }

    @Override
    public int countDirectories() {
        VFSContainer versionsContainer = getRootVersionsContainer();
        if (versionsContainer.exists()) {
            return countDirectories(versionsContainer);
        }
        return 0;
    }

    private int countDirectories(VFSContainer container) {
        int count = 1;//itself
        List<VFSItem> children = container.getItems(new SystemItemFilter());
        for (VFSItem child : children) {
            if (child instanceof VFSContainer) {
                count += countDirectories((VFSContainer) child);
            }
        }
        return count;
    }

    @Override
    public void pruneHistory(long maxHistoryLength, ProgressDelegate progress) {
        VFSContainer versionsContainer = getRootVersionsContainer();
        if (!versionsContainer.exists()) {
            return;
        }
        //delete folder without versioning first

        int count = 0;
        String[] excludedRootFolders = new String[] { "tmp", "scorm", "forum", "portfolio" };
        for (String excludedRootFolder : excludedRootFolders) {
            VFSItem excludedContainer = versionsContainer.resolve(excludedRootFolder);
            if (excludedContainer instanceof LocalFolderImpl) {
                File excludedFile = ((LocalFolderImpl) excludedContainer).getBasefile();
                FileUtils.deleteQuietly(excludedFile);
                if (progress != null)
                    progress.setInfo(excludedContainer.getName());
            }
            if (progress != null)
                progress.setActual(++count);
        }

        if (maxHistoryLength < 0) {
            //nothing to do
        } else if (maxHistoryLength == 0 && versionsContainer instanceof LocalFolderImpl) {
            //delete all the stuff
            FileUtils.deleteQuietly(((LocalFolderImpl) versionsContainer).getBasefile());
        } else {
            pruneVersionHistory(versionsContainer, maxHistoryLength, progress, count);
        }

        if (progress != null)
            progress.finished();
    }

    private void pruneVersionHistory(VFSContainer container, long maxHistoryLength, ProgressDelegate progress,
            int count) {
        List<VFSItem> children = container.getItems(new SystemItemFilter());
        for (VFSItem child : children) {
            if (child instanceof VFSContainer) {
                if (progress != null)
                    progress.setActual(++count);
                pruneVersionHistory((VFSContainer) child, maxHistoryLength, progress, count);
            }
            if (child instanceof VFSLeaf) {
                VFSLeaf versionsLeaf = (VFSLeaf) child;
                pruneVersionHistory(versionsLeaf, maxHistoryLength, progress);
            }
        }
    }

    private void pruneVersionHistory(VFSLeaf versionsLeaf, long maxHistoryLength, ProgressDelegate progress) {
        if (versionsLeaf.getName().endsWith(".xml") && isVersionsXmlFile(versionsLeaf)) {
            File originalFile = reversedOriginFile(versionsLeaf);
            if (originalFile.exists()) {
                VFSLeaf original = new LocalFileImpl(originalFile);
                if (progress != null)
                    progress.setInfo(original.getName());
                Versions versions = readVersions(original, versionsLeaf);
                List<VFSRevision> revisions = versions.getRevisions();
                if (revisions.size() > maxHistoryLength) {
                    List<VFSRevision> revisionsToDelete = revisions.subList(0,
                            revisions.size() - (int) maxHistoryLength);
                    deleteRevisions((Versionable) original, revisionsToDelete);
                }
            }
        }
    }

    @Override
    public boolean deleteOrphans(ProgressDelegate progress) {
        List<OrphanVersion> orphans = orphans();
        if (progress != null)
            progress.setMax(orphans.size());
        int count = 0;
        for (OrphanVersion orphan : orphans) {
            delete(orphan);
            if (progress != null) {
                progress.setActual(++count);
                progress.setInfo(orphan.getOriginalFilePath());
            }
        }
        if (progress != null)
            progress.finished();
        return true;
    }

    @Override
    public boolean delete(OrphanVersion orphan) {
        VFSLeaf versionLeaf = orphan.getVersionsLeaf();

        if (versionLeaf == null)
            return true; //already deleted
        Versions versions = orphan.getVersions();
        for (VFSRevision versionToDelete : versions.getRevisions()) {
            RevisionFileImpl versionImpl = (RevisionFileImpl) versionToDelete;
            versionImpl.setContainer(orphan.getVersionsLeaf().getParentContainer());
            VFSLeaf fileToDelete = versionImpl.getFile();
            if (fileToDelete != null) {
                fileToDelete.delete();
            }
        }
        versionLeaf.delete();
        return true;
    }

    @Override
    public List<OrphanVersion> orphans() {
        List<OrphanVersion> orphans = new ArrayList<OrphanVersion>();
        VFSContainer versionsContainer = getRootVersionsContainer();
        crawlForOrphans(versionsContainer, orphans);
        return orphans;
    }

    private void crawlForOrphans(VFSContainer container, List<OrphanVersion> orphans) {
        if (!container.exists()) {
            return;
        }

        List<VFSItem> children = container.getItems();
        for (VFSItem child : children) {
            if (child instanceof VFSContainer) {
                crawlForOrphans((VFSContainer) child, orphans);
            }
            if (child instanceof VFSLeaf) {
                VFSLeaf versionsLeaf = (VFSLeaf) child;
                if (child.getName().endsWith(".xml")) {
                    Versions versions = isOrphan(versionsLeaf);
                    if (versions == null) {
                        continue;
                    } else {
                        List<VFSRevision> revisions = versions.getRevisions();
                        if (revisions != null) {
                            for (VFSRevision revision : revisions) {
                                if (revision instanceof RevisionFileImpl) {
                                    ((RevisionFileImpl) revision).setContainer(container);
                                }
                            }
                        }
                    }
                    File originalFile = reversedOriginFile(child);
                    if (!originalFile.exists()) {
                        VFSLeaf orphan = new LocalFileImpl(originalFile);
                        orphans.add(new OrphanVersion(orphan, versionsLeaf, versions));
                    }
                }
            }
        }
    }

    private Versions isOrphan(VFSLeaf potentialOrphan) {
        try {
            if (potentialOrphan.exists()) {
                VersionsFileImpl versions = (VersionsFileImpl) XStreamHelper.readObject(mystream, potentialOrphan);
                return versions;
            }
            return null;
        } catch (Exception e) {
            return null;
        }
    }

    private File reversedOriginFile(VFSItem versionXml) {
        String path = File.separatorChar + versionXml.getName().substring(0, versionXml.getName().length() - 4);
        for (VFSContainer parent = versionXml.getParentContainer(); parent != null
                && !parent.isSame(getRootVersionsContainer()); parent = parent.getParentContainer()) {
            path = File.separatorChar + parent.getName() + path;
        }

        return new File(getCanonicalRoot(), path);
    }

    /**
     * 
     * @see org.olat.core.configuration.Initializable#init()
     */
    public void init() {
        mystream = XStreamHelper.createXStreamInstance();
        mystream.alias("versions", VersionsFileImpl.class);
        mystream.alias("revision", RevisionFileImpl.class);
        mystream.omitField(VersionsFileImpl.class, "currentVersion");
        mystream.omitField(VersionsFileImpl.class, "versionFile");
        mystream.omitField(RevisionFileImpl.class, "current");
        mystream.omitField(RevisionFileImpl.class, "container");
        mystream.omitField(RevisionFileImpl.class, "file");
    }
}