/* * ==================================================================== * Copyright (c) 2004-2012 TMate Software Ltd. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at http://svnkit.com/license.html * If newer versions of this license are posted there, you may use a * newer version instead, at your option. * ==================================================================== */ package org.tmatesoft.svn.core.wc; import java.text.DateFormat; import java.util.Calendar; import java.util.Collection; import java.util.Date; import org.tmatesoft.svn.core.internal.util.SVNHashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * <b>SVNRevision</b> is a revision wrapper used for an abstract representation * of revision information. * * <p> * Most of high-level API classes' methods receive revision parameters as * <b>SVNRevision</b> objects to get information on SVN revisions and use it * in version control operations. * * <p> * This class provides advantages of specifying revisions either as just * <span class="javakeyword">long</span> numbers or dated revisions (when a * revision is determined according to a particular timestamp) or SVN compatible * keywords denoting the latest revision (HEAD), Working Copy pristine * revision (BASE) and so on. And one more feature is that <b>SVNRevision</b> * can parse strings (that can be anything: string representations of numbers, * dates, keywords) to construct an <b>SVNRevision</b> to use. * * @version 1.3 * @author TMate Software Ltd. * @since 1.2 */ public class SVNRevision { /** * Denotes the latest repository revision. SVN's analogue keyword: HEAD. */ public static final SVNRevision HEAD = new SVNRevision("HEAD", 0); /** * Denotes an item's working (current) revision. This is a SVNKit constant * that should be provided to mean working revisions (what the native SVN * client assumes by default). */ public static final SVNRevision WORKING = new SVNRevision("WORKING", 1); /** * Denotes the revision just before the one when an item was last * changed (technically, <i>COMMITTED - 1</i>). SVN's analogue keyword: PREV. */ public static final SVNRevision PREVIOUS = new SVNRevision("PREV", 3); /** * Denotes the 'pristine' revision of a Working Copy item. * SVN's analogue keyword: BASE. */ public static final SVNRevision BASE = new SVNRevision("BASE", 2); /** * Denotes the last revision in which an item was changed before (or * at) BASE. SVN's analogue keyword: COMMITTED. */ public static final SVNRevision COMMITTED = new SVNRevision("COMMITTED", 4); /** * Used to denote that a revision is undefined (not available or not * valid). */ public static final SVNRevision UNDEFINED = new SVNRevision("UNDEFINED", 30); private static final Map ourValidRevisions = new SVNHashMap(); static { ourValidRevisions.put(HEAD.getName(), HEAD); ourValidRevisions.put(WORKING.getName(), WORKING); ourValidRevisions.put(PREVIOUS.getName(), PREVIOUS); ourValidRevisions.put(BASE.getName(), BASE); ourValidRevisions.put(COMMITTED.getName(), COMMITTED); } private static Pattern ISO_8601_EXTENDED_DATE_ONLY_PATTERN = Pattern.compile("(\\d{4})-(\\d{1,2})-(\\d{1,2})"); private static Pattern ISO_8601_EXTENDED_UTC_PATTERN = Pattern.compile("(\\d{4})-(\\d{1,2})-(\\d{1,2})T(\\d{1,2}):(\\d{2})(:(\\d{2})([.,](\\d{1,6}))?)?(Z)?"); private static Pattern ISO_8601_EXTENDED_OFFSET_PATTERN = Pattern.compile("(\\d{4})-(\\d{1,2})-(\\d{1,2})T(\\d{1,2}):(\\d{2})(:(\\d{2})([.,](\\d{1,6}))?)?([+-])(\\d{2})(:(\\d{2}))?"); private static Pattern ISO_8601_BASIC_DATE_ONLY_PATTERN = Pattern.compile("(\\d{4})(\\d{2})(\\d{2})"); private static Pattern ISO_8601_BASIC_UTC_PATTERN = Pattern.compile("(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})((\\d{2})([.,](\\d{1,6}))?)?(Z)?"); private static Pattern ISO_8601_BASIC_OFFSET_PATTERN = Pattern.compile("(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})((\\d{2})([.,](\\d{1,6}))?)?([+-])(\\d{2})((\\d{2}))?"); private static Pattern ISO_8601_GNU_FORMAT_PATTERN = Pattern.compile("(\\d{4})-(\\d{1,2})-(\\d{1,2})T(\\d{1,2}):(\\d{2})(:(\\d{2})([.,](\\d{1,6}))?)?([+-])(\\d{2})((\\d{2}))?"); private static Pattern SVN_LOG_DATE_FORMAT_PATTERN = Pattern.compile("(\\d{4})-(\\d{1,2})-(\\d{1,2}) (\\d{1,2}):(\\d{2})(:(\\d{2})([.,](\\d{1,6}))?)?( ([+-])(\\d{2})(\\d{2})?)?"); private static Pattern TIME_ONLY_PATTERN = Pattern.compile("(\\d{1,2}):(\\d{2})(:(\\d{2})([.,](\\d{1,6}))?)?"); private static final Collection ourTimeFormatPatterns = new LinkedList(); static { ourTimeFormatPatterns.add(ISO_8601_EXTENDED_DATE_ONLY_PATTERN); ourTimeFormatPatterns.add(ISO_8601_EXTENDED_UTC_PATTERN); ourTimeFormatPatterns.add(ISO_8601_EXTENDED_OFFSET_PATTERN); ourTimeFormatPatterns.add(ISO_8601_BASIC_DATE_ONLY_PATTERN); ourTimeFormatPatterns.add(ISO_8601_BASIC_UTC_PATTERN); ourTimeFormatPatterns.add(ISO_8601_BASIC_OFFSET_PATTERN); ourTimeFormatPatterns.add(SVN_LOG_DATE_FORMAT_PATTERN); ourTimeFormatPatterns.add(ISO_8601_GNU_FORMAT_PATTERN); ourTimeFormatPatterns.add(TIME_ONLY_PATTERN); } private long myRevision; private String myName; private Date myDate; private int myID; private SVNRevision(long number) { myRevision = number; myName = null; myID = 10; } private SVNRevision(String name, int id) { this(-1); myName = name; myID = id; } private SVNRevision(Date date) { this(-1); Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")); calendar.setTime(date); myDate = calendar.getTime(); myID = 20; } /** * Gets the revision keyword name. Each of <b>SVNRevision</b>'s * constant fields that represent revision keywords also have * its own name. * * @return a revision keyword name */ public String getName() { return myName; } /** * Gets the revision number represented by this object. * * @return a revision number; -1 is returned when this object * represents a revision information not using a revision * number. * */ public long getNumber() { return myRevision; } /** * Gets the timestamp used to specify a revision. * * @return a timestamp if any specified for this object */ public Date getDate() { return myDate; } /** * Checks if the revision information represented by this object * is valid. * <p> * {@link #UNDEFINED} is not a valid revision. * * @return <span class="javakeyword">true</span> if valid, otherwise * <span class="javakeyword">false</span> */ public boolean isValid() { return this != UNDEFINED && (myDate != null || myRevision >= 0 || myName != null); } /** * Gets the identifier of the revision information kind this * object represents. * * @return this object's id */ public int getID() { return myID; } /** * Evaluates the hash code for this object. * A hash code is evaluated in this way: * <ul> * <li>if this object represents revision info as a revision number * then * <code>hash code = (<span class="javakeyword">int</span>) revisionNumber & 0xFFFFFFFF</code>; * <li>if this object represents revision info as a timestamp then * {@link java.util.Date#hashCode()} is used; * <li>if this object represents revision info as a keyword * then {@link java.lang.String#hashCode()} is used for the keyword name; * </ul> * * @return this object's hash code */ public int hashCode() { if (myRevision >= 0) { return (int) myRevision & 0xFFFFFFFF; } else if (myDate != null) { return myDate.hashCode(); } else if (myName != null) { return myName.hashCode(); } return -1; } /** * Compares this object with another <b>SVNRevision</b> object. * * @param o an object to be compared with; if it's not an * <b>SVNRevision</b> then this method certainly returns * <span class="javakeyword">false</span> * @return <span class="javakeyword">true</span> if equal, otherwise * <span class="javakeyword">false</span> */ public boolean equals(Object o) { if (o == null || o.getClass() != SVNRevision.class) { return false; } SVNRevision r = (SVNRevision) o; if (myRevision >= 0) { return myRevision == r.getNumber(); } else if (myDate != null) { return myDate.equals(r.getDate()); } else if (myName != null) { return myName.equals(r.getName()); } return !r.isValid(); } /** * Checks whether a revision number is valid. * * @param revision a revision number * @return <span class="javakeyword">true</span> if valid (<code>>=0</code>), * otherwise <span class="javakeyword">false</span> */ public static boolean isValidRevisionNumber(long revision) { return revision >= 0; } /** * Creates an <b>SVNRevision</b> object given a revision number. * * @param revisionNumber a definite revision number * @return the constructed <b>SVNRevision</b> object */ public static SVNRevision create(long revisionNumber) { if (revisionNumber < 0) { return SVNRevision.UNDEFINED; } return new SVNRevision(revisionNumber); } /** * Creates an <b>SVNRevision</b> object given a particular timestamp. * * @param date a timestamp represented as a Date instance * @return the constructed <b>SVNRevision</b> object */ public static SVNRevision create(Date date) { return new SVNRevision(date); } /** * Determines if the revision represented by this abstract object is * Working Copy specific - that is one of {@link #BASE} or {@link #WORKING}. * * @return <span class="javakeyword">true</span> if this object represents * a kind of a local revision, otherwise <span class="javakeyword">false</span> */ public boolean isLocal() { boolean remote = !isValid() || this == SVNRevision.HEAD || getNumber() >= 0 || getDate() != null; return !remote; } /** * Parses an input string and be it a representation of either * a revision number, or a timestamp, or a revision keyword, constructs * an <b>SVNRevision</b> representation of the revision. * * @param value a string to be parsed * @return an <b>SVNRevision</b> object that holds the revision * information parsed from <code>value</code>; however * if an input string is not a valid one which can be * successfully transformed to an <b>SVNRevision</b> the * return value is {@link SVNRevision#UNDEFINED} */ public static SVNRevision parse(String value) { if (value == null) { return SVNRevision.UNDEFINED; } if (value.startsWith("-r")) { value = value.substring("-r".length()); } value = value.trim(); if (value.startsWith("{") && value.endsWith("}")) { value = value.substring(1); value = value.substring(0, value.length() - 1); try { Calendar date = Calendar.getInstance(); for (Iterator patterns = ourTimeFormatPatterns.iterator(); patterns.hasNext();) { Pattern pattern = (Pattern) patterns.next(); Matcher matcher = pattern.matcher(value); if (matcher.matches()) { if (pattern == ISO_8601_EXTENDED_DATE_ONLY_PATTERN || pattern == ISO_8601_BASIC_DATE_ONLY_PATTERN) { int year = Integer.parseInt(matcher.group(1)); int month = Integer.parseInt(matcher.group(2)); int day = Integer.parseInt(matcher.group(3)); date.clear(); date.set(year, month - 1, day); } else if (pattern == ISO_8601_EXTENDED_UTC_PATTERN || pattern == ISO_8601_EXTENDED_OFFSET_PATTERN || pattern == ISO_8601_BASIC_UTC_PATTERN || pattern == ISO_8601_BASIC_OFFSET_PATTERN || pattern == ISO_8601_GNU_FORMAT_PATTERN || pattern == SVN_LOG_DATE_FORMAT_PATTERN) { int year = Integer.parseInt(matcher.group(1)); int month = Integer.parseInt(matcher.group(2)); int day = Integer.parseInt(matcher.group(3)); int hours = Integer.parseInt(matcher.group(4)); int minutes = Integer.parseInt(matcher.group(5)); int seconds = 0; int milliseconds = 0; if (matcher.group(6) != null) { seconds = Integer.parseInt(matcher.group(7)); if (matcher.group(8) != null) { String millis = matcher.group(9); millis = millis.length() <= 3 ? millis : millis.substring(0, 3); milliseconds = Integer.parseInt(millis); } } date.clear(); date.set(year, month - 1, day, hours, minutes, seconds); date.set(Calendar.MILLISECOND, milliseconds); if (pattern == ISO_8601_EXTENDED_OFFSET_PATTERN || pattern == ISO_8601_BASIC_OFFSET_PATTERN || pattern == ISO_8601_GNU_FORMAT_PATTERN) { int zoneOffsetInMillis = "+".equals(matcher.group(10)) ? +1 : -1; int hoursOffset = Integer.parseInt(matcher.group(11)); int minutesOffset = matcher.group(12) != null ? Integer.parseInt(matcher.group(13)) : 0; zoneOffsetInMillis = zoneOffsetInMillis * ((hoursOffset*3600 + minutesOffset*60)*1000); date.set(Calendar.ZONE_OFFSET, zoneOffsetInMillis); date.set(Calendar.DST_OFFSET, 0); } else if (pattern == SVN_LOG_DATE_FORMAT_PATTERN && matcher.group(10) != null) { int zoneOffsetInMillis = "+".equals(matcher.group(11)) ? +1 : -1; int hoursOffset = Integer.parseInt(matcher.group(12)); int minutesOffset = matcher.group(13) != null ? Integer.parseInt(matcher.group(13)) : 0; zoneOffsetInMillis = zoneOffsetInMillis * ((hoursOffset*3600 + minutesOffset*60)*1000); date.set(Calendar.ZONE_OFFSET, zoneOffsetInMillis); date.set(Calendar.DST_OFFSET, 0); } else if (((pattern == ISO_8601_EXTENDED_UTC_PATTERN) || (pattern == ISO_8601_BASIC_UTC_PATTERN)) && "Z".equals(matcher.group(10))) { date.set(Calendar.ZONE_OFFSET, 0); date.set(Calendar.DST_OFFSET, 0); } } else if (pattern == TIME_ONLY_PATTERN) { int hours = Integer.parseInt(matcher.group(1)); int minutes = Integer.parseInt(matcher.group(2)); int seconds = 0; int milliseconds = 0; if (matcher.group(3) != null) { seconds = Integer.parseInt(matcher.group(4)); if (matcher.group(5) != null) { String millis = matcher.group(6); millis = millis.length() <= 3 ? millis : millis.substring(0, 3); milliseconds = Integer.parseInt(millis); } } date.set(Calendar.HOUR_OF_DAY, hours); date.set(Calendar.MINUTE, minutes); date.set(Calendar.SECOND, seconds); date.set(Calendar.MILLISECOND, milliseconds); } return SVNRevision.create(date.getTime()); } } return SVNRevision.UNDEFINED; } catch (NumberFormatException e) { return SVNRevision.UNDEFINED; } } try { long number = Long.parseLong(value); return SVNRevision.create(number); } catch (NumberFormatException nfe) { } SVNRevision revision = (SVNRevision) ourValidRevisions.get(value.toUpperCase()); if (revision == null) { return UNDEFINED; } return revision; } /** * Gives a string representation of this object. * * @return a string representing this object */ public String toString() { if (myRevision >= 0) { return Long.toString(myRevision); } else if (myName != null) { return myName; } else if (myDate != null) { return DateFormat.getDateTimeInstance().format(myDate); } return "{invalid revision}"; } }