/* * ==================================================================== * * The Apache Software License, Version 1.1 * * Copyright (c) 1999-2003 The Apache Software Foundation. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowledgement: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowledgement may appear in the software itself, * if and wherever such third-party acknowledgements normally appear. * * 4. The names "The Jakarta Project", "Commons", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact apache@apache.org. * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Software Foundation. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package org.apache.commons.jrcs.rcs; import java.text.DateFormat; import java.text.Format; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.StringTokenizer; import java.util.TreeMap; import org.apache.commons.jrcs.diff.AddDelta; import org.apache.commons.jrcs.diff.Chunk; import org.apache.commons.jrcs.diff.DeleteDelta; import org.apache.commons.jrcs.diff.Diff; import org.apache.commons.jrcs.diff.Revision; import org.apache.commons.jrcs.util.ToString; import org.apache.commons.jrcs.diff.PatchFailedException; /** * Ancestor to all nodes in a version control Archive. * <p>Nodes store the deltas between two revisions of the text.</p> * * This class is NOT thread safe. * * @see TrunkNode * @see BranchNode * @see Archive * * @author <a href="mailto:juanco@suigeneris.org">Juanco Anez</a> * @version $Id: Node.java 84222 2010-11-03 13:15:52Z david.horwitz@uct.ac.za $ */ public abstract class Node extends ToString implements Comparable { /** * The version number for this node. */ protected final Version version; protected Date date = new Date(); protected String author = System.getProperty("user.name"); protected String state = "Exp"; protected String log = ""; protected String locker = ""; protected Object[] text; protected Node rcsnext; protected Node parent; protected Node child; protected TreeMap branches = null; protected Phrases phrases = null; protected static final Format dateFormatter = new MessageFormat( "\t{0,number,##00}." + "{1,number,00}." + "{2,number,00}." + "{3,number,00}." + "{4,number,00}." + "{5,number,00}" ); protected static final DateFormat dateFormat = new SimpleDateFormat("yy.MM.dd.HH.mm.ss"); protected static final DateFormat dateFormat2K = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss"); /** * Creates a copy of a node. Only used internally. * @param other The node to copy. */ protected Node(Node other) { this(other.version, null); this.date = other.date; this.author = other.author; this.state = other.state; this.log = other.log; this.locker = other.locker; } /** * Creates a node with the given version number. * @param vernum The version number for the node. * @param rcsnext The next node in the RCS logical hierarchy. */ protected Node(Version vernum, Node rcsnext) { if (vernum == null) { throw new IllegalArgumentException(vernum.toString()); } this.version = (Version) vernum.clone(); this.setRCSNext(rcsnext); } /** * Creates a new node of the adequate type for the given version number. * @param vernum The version number for the node. * @param rcsnext The next node in the RCS logical hierarchy. * @return The newly created node. */ static Node newNode(Version vernum, Node rcsnext) throws InvalidVersionNumberException { if (vernum.isTrunk()) { return new TrunkNode(vernum, (TrunkNode) rcsnext); } else { return new BranchNode(vernum, (BranchNode) rcsnext); } } /** * Creates a new node of the adequate type for the given version number. * @param vernum The version number for the node. * @return The newly created node. */ static Node newNode(Version vernum) throws InvalidVersionNumberException { return newNode(vernum, null); } /** * Compares the version number of this node to that of another node. * @param other The node to compare two. * @return 0 if versions are equal, 1 if this version greather than the other, * and -1 otherwise. */ public int compareTo(Object other) { if (other == this) { return 0; } else if (!(other instanceof Node)) { return -1; } else { return version.compareTo(((Node) other).version); } } /** * Returns true if the node is a "ghost" node. * Ghost nodes have no associated text ot deltas. CVS uses * them to mark certain points in the node hierarchy. */ public boolean isGhost() { return version.isGhost() || text == null; } /** * Retrieve the branch node identified with * the given numer. * @param no The branch number. * @return The branch node. * @see BranchNode */ public BranchNode getBranch(int no) { if (branches == null) { return null; } else if (no == 0) { Integer branchNo = (Integer) branches.lastKey(); return (BranchNode) (branchNo == null ? null : branches.get(branchNo)); } else { return (BranchNode) branches.get(Integer.valueOf(no)); } } /** * Return the root node of the node hierarchy. * @return The root node. */ public Node root() { Node result = this; while (result.parent != null) { result = result.parent; } return result; } /** * Set the locker. * @param user A symbol that identifies the locker. */ public void setLocker(String user) { locker = user.intern(); } /** * Set the author of the node's revision. * @param user A symbol that identifies the author. */ public void setAuthor(String user) { author = user.intern(); } /** * Set the date of the node's revision. * @param value an array of 6 integers, corresponding to the * year, month, day, hour, minute, and second of this revision.<br> * If the year has two digits, it is interpreted as belonging to the 20th * century.<br> * The month is a number from 1 to 12. */ public void setDate(int[] value) { this.date = new GregorianCalendar(value[0] + (value[0] <= 99 ? 1900 : 0), value[1] - 1, value[2], value[3], value[4], value[5]).getTime(); } /** * Sets the state of the node's revision. * @param value A symbol that identifies the state. The most commonly * used value is Exp. */ public void setState(String value) { state = value; } /** * Sets the next node in the RCS logical hierarchy. * In the RCS hierarchy, a {@link TrunkNode TrunkNode} points * to the previous revision, while a {@link BranchNode BranchNode} * points to the next revision. * @param node The next node in the RCS logical hierarchy. */ public void setRCSNext(Node node) { rcsnext = node; } /** * Sets the log message for the node's revision. * The log message is usually used to explain why the revision took place. * @param value The message. */ public void setLog(String value) { // the last newline belongs to the file format if(value.endsWith(Archive.RCS_NEWLINE)) log = value.substring(0, value.length()-1); else log = value; } /** * Sets the text for the node's revision. * <p>For archives containing binary information, the text is an image * of the revision contents.</p> * <p>For ASCII archives, the text contains the delta between the * current revision and the next revision in the RCS logical hierarchy. * The deltas are codified in a format similar to the one used by Unix diff.</p> * <p> The passed string is converted to an array of objects * befored being stored as the revision's text</p> * @param value The revision's text. * @see ArchiveParser */ public void setText(String value) { this.text = org.apache.commons.jrcs.diff.Diff.stringToArray(value); } /** * Sets the text for the node's revision. * <p>For archives containing binary information, the text is an image * of the revision contents.</p> * <p>For ASCII archives, the text contains the delta between the * current revision and the next revision in the RCS logical hierarchy. * The deltas are codified in a format similar to the one used by Unix diff. * @param value The revision's text. * @see ArchiveParser */ public void setText(Object[] value) { this.text = Arrays.asList(value).toArray(); } /** * Adds a branch node to the current node. * @param node The branch node. * @throws InvalidVersionNumberException if the version number * is not a valid branch version number for the current node */ public void addBranch(BranchNode node) throws InvalidVersionNumberException { if (node.version.isLessThan(this.version) || node.version.size() != (this.version.size()+2)) { throw new InvalidVersionNumberException("version must be grater"); } int branchno = node.version.at(this.version.size()); if (branches == null) { branches = new TreeMap(); } branches.put(Integer.valueOf(branchno), node); node.parent = this; } /** * Returns the version number that should correspond to * the revision folowing this node. * @return The next version number. */ public Version nextVersion() { return this.version.next(); } /** * Returns the version number that should correspond to a newly * created branch of this node. * @return the new branch's version number. */ public Version newBranchVersion() { Version result = new Version(this.version); if (branches == null || branches.size() <= 0) { result.__addBranch(1); } else { result.__addBranch(((Integer) branches.lastKey()).intValue()); } result.__addBranch(1); return result; } /** * Return the next node in the RCS logical hierarchy. * @return the next node */ public Node getRCSNext() { return rcsnext; } /** * Returns the path from the current node to the node * identified by the given version. * @param vernum The version number of the last node in the path. * @return The path * @throws NodeNotFoundException if a node with the given version number * doesn't exist, or is not reachable following the RCS-next chain * from this node. * @see Path */ public Path pathTo(Version vernum) throws NodeNotFoundException { return pathTo(vernum, false); } /** * Returns the path from the current node to the node * identified by the given version. * @param vernum The version number of the last node in the path. * @param soft If true, no error is thrown if a node with the given * version doesn't exist. Use soft=true to find a apth to where a new * node should be added. * @return The path * @throws NodeNotFoundException if a node with the given version number * is not reachable following the RCS-next chain from this node. * If soft=false the exception is also thrown if a node with the given * version number doesn't exist. * @see Path */ public Path pathTo(Version vernum, boolean soft) throws NodeNotFoundException { Path path = new Path(); Node target = this; do { path.add(target); target = target.nextInPathTo(vernum, soft); } while (target != null); return path; } /** * Returns the next node in the path from the current node to the node * identified by the given version. * @param vernum The version number of the last node in the path. * @param soft If true, no error is thrown if a node with the given * version doesn't exist. Use soft=true to find a apth to where a new * node should be added. * @return The path * @throws NodeNotFoundException if a node with the given version number * is not reachable following the RCS-next chain from this node. * If soft=false the exception is also thrown if a node with the given * version number doesn't exist. * @see Path */ public abstract Node nextInPathTo(Version vernum, boolean soft) throws NodeNotFoundException; /** * Returns the Node with the version number that corresponds to * the revision to be obtained after the deltas in the current node * are applied. * <p>For a {@link BranchNode BranchNode} the deltaRevision is the * current revision; that is, after the deltas are applied, the text for * the current revision is obtained.</p> * <p>For a {@link TrunkNode TrunkNode} the deltaRevision is the * next revision; that is, after the deltas are applied, the text obtained * corresponds to the next revision in the chain.</p> * @return The node for the delta revision. */ public abstract Node deltaRevision(); /** * Apply the deltas in the current node to the given text. * @param original the text to be patched * @throws InvalidFileFormatException if the deltas cannot be parsed. * @throws PatchFailedException if the diff engine determines that * the deltas cannot apply to the given text. */ public void patch(List original) throws InvalidFileFormatException, PatchFailedException { patch(original, false); } /** * Apply the deltas in the current node to the given text. * @param original the text to be patched * @param annotate set to true to have each text line be a * {@link Line Line} object that identifies the revision in which * the line was changed or added. * @throws InvalidFileFormatException if the deltas cannot be parsed. * @throws PatchFailedException if the diff engine determines that * the deltas cannot apply to the given text. */ public void patch(List original, boolean annotate) throws InvalidFileFormatException, org.apache.commons.jrcs.diff.PatchFailedException { Revision revision = new Revision(); for (int it = 0; it < text.length; it++) { String cmd = text[it].toString(); java.util.StringTokenizer t = new StringTokenizer(cmd, "ad ", true); char action; int n; int count; try { action = t.nextToken().charAt(0); n = Integer.parseInt(t.nextToken()); t.nextToken(); // skip the space count = Integer.parseInt(t.nextToken()); } catch (Exception e) { throw new InvalidFileFormatException(version + ":line:" + ":" + e.getMessage()); } if (action == 'd') { revision.addDelta(new DeleteDelta(new Chunk(n - 1, count))); } else if (action == 'a') { revision.addDelta(new AddDelta(n, new Chunk(getTextLines(it + 1, it + 1 + count), 0, count, n - 1))); it += count; } else { throw new InvalidFileFormatException(version.toString()); } } revision.applyTo(original); } /** * Conver the current node and all of its branches * to their RCS string representation and * add it to the given StringBuffer. * @param s The string buffer to add the node's image to. */ public void toString(StringBuffer s) { toString(s, Archive.RCS_NEWLINE); } /** * Conver the current node and all of its branches * to their RCS string representation and * add it to the given StringBuffer using the given marker as * line separator. * @param s The string buffer to add the node's image to. * @param EOL The line separator to use. */ public void toString(StringBuffer s, String EOL) { String EOI = ";" + EOL; String NLT = EOL + "\t"; s.append(EOL); s.append(version.toString() + EOL); s.append("date"); if (date != null) { DateFormat formatter = dateFormat; Calendar cal = new GregorianCalendar(); cal.setTime(date); if (cal.get(Calendar.YEAR) > 1999) { formatter = dateFormat2K; } s.append("\t" + formatter.format(date)); } s.append(";\tauthor"); if (author != null) { s.append(" " + author); } s.append(";\tstate"); if (state != null) { s.append(" "); s.append(state); } s.append(EOI); s.append("branches"); if (branches != null) { for (Iterator i = branches.values().iterator(); i.hasNext();) { Node n = (Node) i.next(); if (n != null) { s.append(NLT + n.version); } } } s.append(EOI); s.append("next\t"); if (rcsnext != null) { s.append(rcsnext.version.toString()); } s.append(EOI); } /** * Conver the urrent node to its RCS string representation. * @return The string representation */ public String toText() { final StringBuffer s = new StringBuffer(); toText(s, Archive.RCS_NEWLINE); return s.toString(); } /** * Conver the urrent node to its RCS string representation and * add it to the given StringBuffer using the given marker as * line separator. * @param s The string buffer to add the node's image to. * @param EOL The line separator to use. */ public void toText(StringBuffer s, String EOL) { s.append(EOL + EOL); s.append(version.toString() + EOL); s.append("log" + EOL); if (log.length() == 0) s.append(Archive.quoteString("")); else // add a newline after the comment s.append(Archive.quoteString(log + EOL)); s.append(EOL); if (phrases != null) { s.append(phrases.toString()); } s.append("text" + EOL); s.append(Archive.quoteString(Diff.arrayToString(text, EOL) + EOL)); s.append(EOL); if (branches != null) { for (Iterator i = branches.values().iterator(); i.hasNext();) { Node n = (Node) i.next(); if (n != null) { n.toText(s, EOL); } } } } /** * Return a list with the lines of the node's text. * @return The list */ public List getTextLines() { return getTextLines(new LinkedList()); } /** * Return a list with a subset of the lines of the node's text. * @param from The offset of the first line to retrieve. * @param to The offset of the line after the last one to retrieve. * @return The list */ public List getTextLines(int from, int to) { return getTextLines(new LinkedList(), from, to); } /** * Add a subset of the lines of the node's text to the given list. * @return The given list after the additions have been made. */ public List getTextLines(List lines) { return getTextLines(lines, 0, text.length); } /** * Add a subset of the lines of the node's text to the given list. * @param from The offset of the first line to retrieve. * @param to The offset of the line after the last one to retrieve. * @return The given list after the additions have been made. */ public List getTextLines(List lines, int from, int to) { for (int i = from; i < to; i++) { lines.add(new Line(deltaRevision(), text[i])); } return lines; } public final Date getDate() { return date; } public final String getAuthor() { return author; } public final String getState() { return state; } public final String getLog() { return log; } public final String getLocker() { return locker; } public final Object[] getText() { return text; } public final Node getChild() { return child; } public final TreeMap getBranches() { return branches; } public final Node getParent() { return parent; } public final Version getVersion() { return version; } public Phrases getPhrases() { return phrases; } }