/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This 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 software 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 software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package com.xpn.xwiki.doc; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import org.suigeneris.jrcs.rcs.Version; import org.suigeneris.jrcs.util.ToString; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.rcs.XWikiPatch; import com.xpn.xwiki.doc.rcs.XWikiRCSArchive; import com.xpn.xwiki.doc.rcs.XWikiRCSNodeContent; import com.xpn.xwiki.doc.rcs.XWikiRCSNodeId; import com.xpn.xwiki.doc.rcs.XWikiRCSNodeInfo; /** * Contains document history. Allows to load any version of document. * * @version $Id: 1db510dfc58e53efa166d170bbbedc4413ea8e56 $ */ public class XWikiDocumentArchive { /** =docId. */ private long id; /** SortedMap from Version to XWikiRCSNodeInfo. */ private SortedMap<Version, XWikiRCSNodeInfo> versionToNode = new TreeMap<Version, XWikiRCSNodeInfo>(); /** * SortedSet of Version - versions which has full document, not patch. Latest version is always full. */ private SortedSet<Version> fullVersions = new TreeSet<Version>(); // store-specific information /** Set of {@link XWikiRCSNodeInfo} which need to delete. */ private Set<XWikiRCSNodeInfo> deletedNodes = new TreeSet<XWikiRCSNodeInfo>(); /** Set of {@link XWikiRCSNodeInfo} which need to saveOrUpdate. */ private Set<XWikiRCSNodeInfo> updatedNodeInfos = new TreeSet<XWikiRCSNodeInfo>(); /** Set of {@link XWikiRCSNodeContent} which need to update. */ private Set<XWikiRCSNodeContent> updatedNodeContents = new TreeSet<XWikiRCSNodeContent>(); /** @param id = {@link XWikiDocument#getId()} */ public XWikiDocumentArchive(long id) { this(); setId(id); } /** default constructor. */ public XWikiDocumentArchive() { } // helper methods /** * @param cur - current version * @param isMinor - is modification is minor * @return next version */ protected Version createNextVersion(Version cur, boolean isMinor) { Version result; if (cur == null) { result = new Version(1, 1); } else if (!isMinor) { result = cur.getBase(1).next().newBranch(1); } else { result = cur.next(); } return result; } /** @param node - node added to versionToNode and fullNodes */ protected void updateNode(XWikiRCSNodeInfo node) { Version ver = node.getId().getVersion(); this.versionToNode.put(ver, node); if (!node.isDiff()) { this.fullVersions.add(ver); } else { this.fullVersions.remove(ver); } } /** * Make a patch. It is store only modified nodes(latest). New nodes need be saved after. * * @param newnode - new node information * @param doc - document for that patch created * @param context - used for loading node contents and generating xml * @return node content for newnode * @throws XWikiException if exception while loading content */ protected XWikiRCSNodeContent makePatch(XWikiRCSNodeInfo newnode, XWikiDocument doc, XWikiContext context) throws XWikiException { XWikiRCSNodeContent result = new XWikiRCSNodeContent(); result.setPatch(new XWikiPatch().setFullVersion(doc, context)); newnode.setContent(result); XWikiRCSNodeInfo latestNode = getLatestNode(); if (latestNode != null) { int nodesCount = getNodes().size(); int nodesPerFull = context.getWiki() == null ? 5 : Integer.parseInt(context.getWiki().getConfig() .getProperty("xwiki.store.rcs.nodesPerFull", "5")); if (nodesPerFull <= 0 || (nodesCount % nodesPerFull) != 0) { XWikiRCSNodeContent latestContent = latestNode.getContent(context); latestContent.getPatch().setDiffVersion(latestContent.getPatch().getContent(), doc, context); latestNode.setContent(latestContent); updateNode(latestNode); getUpdatedNodeContents().add(latestContent); } } return result; } /** @return {@link XWikiDocument#getId()} - primary key */ public long getId() { return this.id; } /** @param id = {@link XWikiDocument#getId()} */ public void setId(long id) { this.id = id; } /** @return collection of XWikiRCSNodeInfo order by version desc */ public Collection<XWikiRCSNodeInfo> getNodes() { return this.versionToNode.values(); } /** * @return collection of XWikiRCSNodeInfo where vfrom >= version >= vto order by version desc * @param vfrom - start version * @param vto - end version */ public Collection<XWikiRCSNodeInfo> getNodes(Version vfrom, Version vto) { int[] ito = vto.getNumbers(); ito[1]--; return this.versionToNode.subMap(vfrom, new Version(ito)).values(); } /** @param versions - collection of XWikiRCSNodeInfo */ public void setNodes(Collection<XWikiRCSNodeInfo> versions) { resetArchive(); for (XWikiRCSNodeInfo node : versions) { updateNode(node); } if (getNodes().size() > 0) { // ensure latest version is full getLatestNode().setDiff(false); updateNode(getLatestNode()); } } /** * @param context - used for load nodes content * @return serialization of class used in {@link com.xpn.xwiki.plugin.packaging.PackagePlugin}. * @throws XWikiException if any error */ public String getArchive(XWikiContext context) throws XWikiException { XWikiRCSArchive archive = new XWikiRCSArchive(getNodes(), context); return archive.toString(); } /** * Deserialize class. Used in {@link com.xpn.xwiki.plugin.packaging.PackagePlugin}. * * @param text - archive in JRCS format * @throws XWikiException if parse error */ public void setArchive(String text) throws XWikiException { try { XWikiRCSArchive archive = new XWikiRCSArchive(text); resetArchive(); Collection nodes = archive.getNodes(getId()); for (Iterator it = nodes.iterator(); it.hasNext();) { XWikiRCSNodeInfo nodeInfo = (XWikiRCSNodeInfo) it.next(); XWikiRCSNodeContent nodeContent = (XWikiRCSNodeContent) it.next(); updateNode(nodeInfo); this.updatedNodeInfos.add(nodeInfo); this.updatedNodeContents.add(nodeContent); } } catch (Exception e) { Object[] args = { text, Long.valueOf(getId()) }; throw new XWikiException(XWikiException.MODULE_XWIKI_DIFF, XWikiException.ERROR_XWIKI_DIFF_CONTENT_ERROR, "Exception while constructing archive for JRCS string [{0}] for document [{1}]", e, args); } } /** * Update history with new document version. * * @param doc - document for this version * @param author - author of version * @param date - date of version * @param comment - version comment * @param version - preferably document version in history * @param context - used for loading nodes content * @throws XWikiException in any error */ public void updateArchive(XWikiDocument doc, String author, Date date, String comment, Version version, XWikiContext context) throws XWikiException { Version oldLatestVer = getLatestVersion(); Version newVer = version; if (newVer == null || oldLatestVer != null && newVer.compareVersions(oldLatestVer) <= 0) { newVer = createNextVersion(oldLatestVer, doc.isMinorEdit()); } XWikiRCSNodeInfo newNode = new XWikiRCSNodeInfo(new XWikiRCSNodeId(getId(), newVer)); newNode.setAuthor(author); newNode.setComment(comment); newNode.setDate(date); XWikiRCSNodeContent newContent = makePatch(newNode, doc, context); updateNode(newNode); this.updatedNodeInfos.add(newNode); this.updatedNodeContents.add(newContent); } /** * Remove document versions from vfrom to vto, inclusive. * * @param newerVersion - start version * @param olderVersion - end version * @param context - used for loading nodes content * @throws XWikiException if any error */ public void removeVersions(Version newerVersion, Version olderVersion, XWikiContext context) throws XWikiException { Version upperBound = newerVersion; Version lowerBound = olderVersion; if (upperBound.compareVersions(lowerBound) < 0) { Version tmp = upperBound; upperBound = lowerBound; lowerBound = tmp; } Version firstVersionAfter = getNextVersion(upperBound); Version firstVersionBefore = getPrevVersion(lowerBound); if (firstVersionAfter == null && firstVersionBefore == null) { resetArchive(); return; } if (firstVersionAfter == null) { // Deleting the most recent version. // Store full version in firstVersionBefore String xmlBefore = getVersionXml(firstVersionBefore, context); XWikiRCSNodeInfo niBefore = getNode(firstVersionBefore); XWikiRCSNodeContent ncBefore = niBefore.getContent(context); ncBefore.getPatch().setFullVersion(xmlBefore); niBefore.setContent(ncBefore); updateNode(niBefore); getUpdatedNodeContents().add(ncBefore); } else if (firstVersionBefore != null) { // We're not deleting from the first version, so we must make a new diff jumping over // the deleted versions. String xmlAfter = getVersionXml(firstVersionAfter, context); String xmlBefore = getVersionXml(firstVersionBefore, context); XWikiRCSNodeInfo niBefore = getNode(firstVersionBefore); XWikiRCSNodeContent ncBefore = niBefore.getContent(context); ncBefore.getPatch().setDiffVersion(xmlBefore, xmlAfter, ""); niBefore.setContent(ncBefore); updateNode(niBefore); getUpdatedNodeContents().add(ncBefore); } // if (firstVersionBefore == null) => nothing else to do, except delete for (Iterator<XWikiRCSNodeInfo> it = getNodes(upperBound, lowerBound).iterator(); it.hasNext();) { XWikiRCSNodeInfo ni = it.next(); this.fullVersions.remove(ni.getId().getVersion()); this.deletedNodes.add(ni); it.remove(); } } /** * @return selected version of document, null if version is not found. * @param version - which version to load * @param context - used for loading * @throws XWikiException if any error */ public XWikiDocument loadDocument(Version version, XWikiContext context) throws XWikiException { XWikiRCSNodeInfo nodeInfo = getNode(version); if (nodeInfo == null) { return null; } try { String content = getVersionXml(version, context); XWikiDocument doc = new XWikiDocument(); doc.fromXML(content); doc.setRCSVersion(version); doc.setComment(nodeInfo.getComment()); doc.setAuthor(nodeInfo.getAuthor()); doc.setMinorEdit(nodeInfo.isMinorEdit()); doc.setMostRecent(version.equals(getLatestVersion())); return doc; } catch (Exception e) { Object[] args = { version.toString(), Long.valueOf(getId()) }; throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.ERROR_XWIKI_STORE_RCS_READING_REVISIONS, "Exception while reading version [{0}] for document id [{1,number}]", e, args); } } /** * Return the XML corresponding to a version. If the version node contains just a diff, then restore the complete * XML by applying all patches from the nearest full version to the requested version. * * @param version The version to retrieve. * @param context The {@link com.xpn.xwiki.XWikiContext context}. * @return The XML corresponding to the version. * @throws XWikiException If any exception occured. */ public String getVersionXml(Version version, XWikiContext context) throws XWikiException { Version nearestFullVersion = getNearestFullVersion(version); List<XWikiRCSNodeContent> lstContent = loadRCSNodeContents(nearestFullVersion, version, context); List<String> origText = new ArrayList<String>(); for (XWikiRCSNodeContent nodeContent : lstContent) { nodeContent.getPatch().patch(origText); } return ToString.arrayToString(origText.toArray()); } /** * @return {@link XWikiRCSNodeInfo} by version. null if none. * @param version which version to get */ public XWikiRCSNodeInfo getNode(Version version) { return version == null ? null : (XWikiRCSNodeInfo) this.versionToNode.get(version); } /** @return latest version in history for document. null if none. */ public Version getLatestVersion() { return this.versionToNode.size() == 0 ? null : (Version) this.versionToNode.firstKey(); } /** @return latest node in history for document. null if none. */ public XWikiRCSNodeInfo getLatestNode() { return getNode(getLatestVersion()); } /** * @return next version in history. null if none * @param ver - current version */ public Version getNextVersion(Version ver) { // headMap is exclusive SortedMap<Version, XWikiRCSNodeInfo> headmap = this.versionToNode.headMap(ver); return (headmap.size() == 0) ? null : headmap.lastKey(); } /** * @return previous version in history. null if none * @param ver - current version */ public Version getPrevVersion(Version ver) { // tailMap is inclusive SortedMap<Version, XWikiRCSNodeInfo> tailmap = this.versionToNode.tailMap(ver); if (tailmap.size() <= 1) { return null; } Iterator<Version> it = tailmap.keySet().iterator(); it.next(); return it.next(); } /** * @param ver - for what version find nearest * @return nearest version which contain full information (not patch) */ public Version getNearestFullVersion(Version ver) { if (this.fullVersions.contains(ver)) { return ver; } SortedSet<Version> headSet = this.fullVersions.headSet(ver); return (headSet.size() == 0) ? null : headSet.last(); } /** * @return List of {@link XWikiRCSNodeContent} where vfrom<=version<=vto order by version * @param vfrom - start version * @param vto - end version * @param context - used everywhere * @throws XWikiException if any error */ private List<XWikiRCSNodeContent> loadRCSNodeContents(Version vfrom, Version vto, XWikiContext context) throws XWikiException { List<XWikiRCSNodeContent> result = new ArrayList<XWikiRCSNodeContent>(); for (XWikiRCSNodeInfo nodeInfo : getNodes(vfrom, vto)) { XWikiRCSNodeContent nodeContent = nodeInfo.getContent(context); result.add(nodeContent); } return result; } /** reset history. history becomes empty. */ public void resetArchive() { this.versionToNode.clear(); this.fullVersions.clear(); this.deletedNodes.addAll(this.updatedNodeInfos); this.updatedNodeInfos.clear(); this.updatedNodeContents.clear(); } /** @return mutable Set of {@link XWikiRCSNodeInfo} which are need for delete */ public Set<XWikiRCSNodeInfo> getDeletedNodeInfo() { return this.deletedNodes; } /** @return mutable Set of {@link XWikiRCSNodeInfo} which are need for saveOrUpdate */ public Set<XWikiRCSNodeInfo> getUpdatedNodeInfos() { return this.updatedNodeInfos; } /** @return mutable Set of {@link XWikiRCSNodeContent} which are need for update */ public Set<XWikiRCSNodeContent> getUpdatedNodeContents() { return this.updatedNodeContents; } /** * @return full copy of this archive with specified docId * @param docId - new {@link #getId()} * @param context - used for loading content * @throws XWikiException if any error */ public XWikiDocumentArchive clone(long docId, XWikiContext context) throws XWikiException { XWikiDocumentArchive result = new XWikiDocumentArchive(docId); result.setArchive(getArchive(context)); return result; } }