/* * 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.rcs; import java.io.StringReader; import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.net.URLCodec; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.suigeneris.jrcs.diff.PatchFailedException; import org.suigeneris.jrcs.rcs.Archive; import org.suigeneris.jrcs.rcs.InvalidFileFormatException; import org.suigeneris.jrcs.rcs.InvalidTrunkVersionNumberException; import org.suigeneris.jrcs.rcs.Version; import org.suigeneris.jrcs.rcs.impl.Node; import org.suigeneris.jrcs.rcs.impl.NodeNotFoundException; import org.suigeneris.jrcs.rcs.impl.TrunkNode; import org.suigeneris.jrcs.rcs.parse.ParseException; import org.suigeneris.jrcs.util.ToString; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiDocument; /** * Class for String [de]serialization for {@link com.xpn.xwiki.doc.XWikiDocumentArchive}. * * @version $Id: 5069347a725bd0534b735fa5fb9d64f616bbf2f3 $ * @since 1.2M1 */ public class XWikiRCSArchive extends Archive { /** logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(XWikiRCSArchive.class); /** * Used to serialize {@link com.xpn.xwiki.doc.XWikiDocumentArchive}. * * @param nodeInfos - collection of {@link XWikiRCSNodeInfo} in any order * @param context - for loading nodes content * @throws XWikiException if can't load nodes content */ public XWikiRCSArchive(Collection<XWikiRCSNodeInfo> nodeInfos, XWikiContext context) throws XWikiException { super(new Object[0], ""); this.nodes.clear(); this.head = null; if (nodeInfos.size() > 0) { for (XWikiRCSNodeInfo nodeInfo : nodeInfos) { XWikiJRCSNode node = new XWikiJRCSNode(nodeInfo.getId().getVersion(), null); node.setAuthor(nodeInfo.getAuthor()); node.setDate(nodeInfo.getDate()); node.setLog(nodeInfo.getComment()); XWikiRCSNodeContent content = nodeInfo.getContent(context); // Ensure we never set the text to NULL since this can cause errors on some DB such as Oracle. node.setText(StringUtils.defaultString(content.getPatch().getContent())); node.setDiff(nodeInfo.isDiff()); this.nodes.put(node.getVersion(), node); } XWikiJRCSNode last = null; for (Iterator it = this.nodes.keySet().iterator(); it.hasNext();) { Version ver = (Version) it.next(); XWikiJRCSNode node = (XWikiJRCSNode) this.nodes.get(ver); if (last != null) { last.setRCSNext(node); } last = node; if (this.head == null) { this.head = node; } } } } /** * Used to deserialize {@link com.xpn.xwiki.doc.XWikiDocumentArchive}. * * @param archiveText - archive text in JRCS format * @throws ParseException if syntax errors */ public XWikiRCSArchive(String archiveText) throws ParseException { super("", new StringReader(archiveText)); } /** * Helper class for convert from {@link XWikiRCSNodeInfo} to JRCS {@link Node}. */ private static class XWikiJRCSNode extends TrunkNode { /** bug if author=="". see http://www.suigeneris.org/issues/browse/JRCS-24 */ public static String sauthorIfEmpty = "_"; /** mark that node contains full text, not diff. */ public static String sfullVersion = "full"; /** mark that node contains diff. */ public static String sdiffVersion = "diff"; /** * @param vernum - version of node * @param next - next node (with smaller version) in history * @throws InvalidTrunkVersionNumberException if version is invalid */ public XWikiJRCSNode(Version vernum, TrunkNode next) throws InvalidTrunkVersionNumberException { super(vernum, next); } /** @param other - create class from copying this node */ public XWikiJRCSNode(Node other) { this(other.version, null); this.setDate(other.getDate()); this.author = other.getAuthor(); // setAuthor is encoding this.setState(other.getState()); this.setLog(other.getLog()); this.setLocker(other.getLocker()); this.setText(other.getText()); } /** * @param date - date of modification. * @see Node#setDate(int[]) */ public void setDate(Date date) { this.date = date; } /** bitset of chars allowed in author field */ static BitSet safeAuthorChars = new BitSet(); static { safeAuthorChars.set('-'); for (char c = 'A', c1 = 'a'; c <= 'Z'; c++, c1++) { safeAuthorChars.set(c); safeAuthorChars.set(c1); } } /** @param user - user of modification */ @Override public void setAuthor(String user) { // empty author is error in jrcs if (user == null || "".equals(user)) { super.setAuthor(sauthorIfEmpty); } else { byte[] enc = URLCodec.encodeUrl(safeAuthorChars, user.getBytes()); String senc = new String(enc).replace('%', '_'); super.setAuthor(senc); } } /** * @return user of modification can't override getAuthor, so getAuthor1 * @see Node#getAuthor() */ public String getAuthor1() { String result = super.getAuthor(); if (sauthorIfEmpty.equals(result)) { return ""; } else { result = result.replace('_', '%'); try { byte[] dec = URLCodec.decodeUrl(result.getBytes()); result = new String(dec); } catch (DecoderException e) { // Probably the archive was created before introducing this encoding (1.2M1/M2). result = super.getAuthor(); if (!result.matches("^(\\w|\\d|\\.)++$")) { // It's safer to use an empty author than to use an invalid value. result = ""; } } } return result; } /** @return is this node store diff or full version */ public boolean isDiff() { boolean isdiff = !sfullVersion.equals(getState()); if (getTextString() != null && isdiff != !getTextString().startsWith("<")) { LOGGER.warn("isDiff: Archive is inconsistent. Text and diff field are contradicting. version=" + getVersion()); isdiff = !isdiff; } return isdiff; } /** @param isdiff - true if node stores a diff, false - if full version */ public void setDiff(boolean isdiff) { if (getTextString() != null && isdiff != !getTextString().startsWith("<")) { LOGGER.warn("setDiff: Archive is inconsistent. Text and diff field are contradicting. version=" + getVersion()); isdiff = !isdiff; } setState(isdiff ? sdiffVersion : sfullVersion); } /** @return is this revision has old format. (xwiki-core<1.2, without author,comment,state fields) */ public boolean hasOldFormat() { return !sfullVersion.equals(getState()) && !sdiffVersion.equals(getState()); } /** * @return text of modification. * @see Node#getText() */ public String getTextString() { return mergedText()[0].toString(); } @Override public void patch(List original, boolean annotate) throws InvalidFileFormatException, PatchFailedException { if (isDiff()) { super.patch(original, annotate); } else { // there is full version, so simply copy. @see TrunkNode#patch0(..) // impossible because org.suigeneris.jrcs.rcs.impl.Line is final with default constructor. throw new IllegalArgumentException(); /* * original.clear(); Object[] lines = getText(); for (int it = 0; it < lines.length; it++) { * original.add(new Line(this, lines[it])); } */ } } } /** * @return Collection of pairs [{@link XWikiRCSNodeInfo}, {@link XWikiRCSNodeContent}] * @param docId - docId which will be wrote in {@link XWikiRCSNodeId#setDocId(long)} * @throws PatchFailedException * @throws InvalidFileFormatException * @throws NodeNotFoundException */ public Collection getNodes(long docId) throws NodeNotFoundException, InvalidFileFormatException, PatchFailedException { Collection result = new ArrayList(this.nodes.values().size()); for (Iterator it = this.nodes.values().iterator(); it.hasNext();) { XWikiJRCSNode node = new XWikiJRCSNode((Node) it.next()); XWikiRCSNodeInfo nodeInfo = new XWikiRCSNodeInfo(); nodeInfo.setId(new XWikiRCSNodeId(docId, node.getVersion())); nodeInfo.setDiff(node.isDiff()); if (!node.hasOldFormat()) { nodeInfo.setAuthor(node.getAuthor1()); nodeInfo.setComment(node.getLog()); nodeInfo.setDate(node.getDate()); } else { // If the archive node is old so there is no author, comment and date fields so we set them using the // ones from a XWikiDocment object that we construct using the archive content. try { String xml = getRevisionAsString(node.getVersion()); XWikiDocument doc = new XWikiDocument(); doc.fromXML(xml); // set this fields from old document nodeInfo.setAuthor(doc.getAuthor()); nodeInfo.setComment(doc.getComment()); nodeInfo.setDate(doc.getDate()); } catch (Exception e) { // 3 potential known errors: // 1) Revision 1.1 doesn't exist. Some time in the past there was a bug in XWiki where version // were starting at 1.2. When this happens the returned xml has a value of "\n". // 2) A Class property with an invalid XML name was created. // See https://jira.xwiki.org/browse/XWIKI-1855 // 3) Cannot get the revision as a string from a node version. Not sure why this // is happening though... See https://jira.xwiki.org/browse/XWIKI-2076 LOGGER.warn("Error in revision [" + node.getVersion().toString() + "]: [" + e.getMessage() + "]. Ignoring non-fatal error, the Author, Comment and Date are not set."); } } XWikiRCSNodeContent content = new XWikiRCSNodeContent(nodeInfo.getId()); content.setPatch(new XWikiPatch(node.getTextString(), node.isDiff())); nodeInfo.setContent(content); result.add(nodeInfo); result.add(content); } // Ensure that the latest revision is set set to have the full content and not a diff. if (result.size() > 0) { ((XWikiRCSNodeInfo) ((ArrayList) result).get(0)).setDiff(false); ((XWikiRCSNodeContent) ((ArrayList) result).get(1)).getPatch().setDiff(false); } return result; } /** * @return The text of the revision if found. * @param version - the version number. * @throws NodeNotFoundException if the revision could not be found. * @throws InvalidFileFormatException if any of the deltas cannot be parsed. * @throws PatchFailedException if any of the deltas could not be applied */ public String getRevisionAsString(Version version) throws NodeNotFoundException, InvalidFileFormatException, PatchFailedException { return ToString.arrayToString(super.getRevision(version), RCS_NEWLINE); } }