001: /*
002: * ====================================================================
003: * Copyright (c) 2004-2008 TMate Software Ltd. All rights reserved.
004: *
005: * This software is licensed as described in the file COPYING, which
006: * you should have received as part of this distribution. The terms
007: * are also available at http://svnkit.com/license.html
008: * If newer versions of this license are posted there, you may use a
009: * newer version instead, at your option.
010: * ====================================================================
011: */
012: package org.tmatesoft.svn.core.wc;
013:
014: import java.text.DateFormat;
015: import java.util.Calendar;
016: import java.util.Collection;
017: import java.util.Date;
018: import java.util.HashMap;
019: import java.util.Iterator;
020: import java.util.LinkedList;
021: import java.util.Map;
022: import java.util.regex.Matcher;
023: import java.util.regex.Pattern;
024:
025: /**
026: * <b>SVNRevision</b> is a revision wrapper used for an abstract representation
027: * of revision information.
028: *
029: * <p>
030: * Most of high-level API classes' methods receive revision parameters as
031: * <b>SVNRevision</b> objects to get information on SVN revisions and use it
032: * in version control operations.
033: *
034: * <p>
035: * This class provides advantages of specifying revisions either as just
036: * <span class="javakeyword">long</span> numbers or dated revisions (when a
037: * revision is determined according to a particular timestamp) or SVN compatible
038: * keywords denoting the latest revision (HEAD), Working Copy pristine
039: * revision (BASE) and so on. And one more feature is that <b>SVNRevision</b>
040: * can parse strings (that can be anything: string representations of numbers,
041: * dates, keywords) to construct an <b>SVNRevision</b> to use.
042: *
043: * @version 1.1.1
044: * @author TMate Software Ltd.
045: */
046: public class SVNRevision {
047: /**
048: * Denotes the latest repository revision. SVN's analogue keyword: HEAD.
049: */
050: public static final SVNRevision HEAD = new SVNRevision("HEAD", 0);
051:
052: /**
053: * Denotes an item's working (current) revision. This is a SVNKit constant
054: * that should be provided to mean working revisions (what the native SVN
055: * client assumes by default).
056: */
057: public static final SVNRevision WORKING = new SVNRevision(
058: "WORKING", 1);
059:
060: /**
061: * Denotes the revision just before the one when an item was last
062: * changed (technically, <i>COMMITTED - 1</i>). SVN's analogue keyword: PREV.
063: */
064: public static final SVNRevision PREVIOUS = new SVNRevision("PREV",
065: 3);
066:
067: /**
068: * Denotes the 'pristine' revision of a Working Copy item.
069: * SVN's analogue keyword: BASE.
070: */
071: public static final SVNRevision BASE = new SVNRevision("BASE", 2);
072:
073: /**
074: * Denotes the last revision in which an item was changed before (or
075: * at) BASE. SVN's analogue keyword: COMMITTED.
076: */
077: public static final SVNRevision COMMITTED = new SVNRevision(
078: "COMMITTED", 4);
079:
080: /**
081: * Used to denote that a revision is undefined (not available or not
082: * valid).
083: */
084: public static final SVNRevision UNDEFINED = new SVNRevision(
085: "UNDEFINED", 30);
086:
087: private static final Map ourValidRevisions = new HashMap();
088:
089: static {
090: ourValidRevisions.put(HEAD.getName(), HEAD);
091: ourValidRevisions.put(WORKING.getName(), WORKING);
092: ourValidRevisions.put(PREVIOUS.getName(), PREVIOUS);
093: ourValidRevisions.put(BASE.getName(), BASE);
094: ourValidRevisions.put(COMMITTED.getName(), COMMITTED);
095: }
096:
097: private static Pattern ISO_8601_EXTENDED_DATE_ONLY_PATTERN = Pattern
098: .compile("(\\d{4})-(\\d{1,2})-(\\d{1,2})");
099: private static Pattern ISO_8601_EXTENDED_UTC_PATTERN = Pattern
100: .compile("(\\d{4})-(\\d{1,2})-(\\d{1,2})T(\\d{1,2}):(\\d{2})(:(\\d{2})([.,](\\d{1,6}))?)?(?:Z)?");
101: private static Pattern ISO_8601_EXTENDED_OFFSET_PATTERN = Pattern
102: .compile("(\\d{4})-(\\d{1,2})-(\\d{1,2})T(\\d{1,2}):(\\d{2})(:(\\d{2})([.,](\\d{1,6}))?)?([+-])(\\d{2})(:(\\d{2}))?");
103: private static Pattern ISO_8601_BASIC_DATE_ONLY_PATTERN = Pattern
104: .compile("(\\d{4})(\\d{2})(\\d{2})");
105: private static Pattern ISO_8601_BASIC_UTC_PATTERN = Pattern
106: .compile("(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})((\\d{2})([.,](\\d{1,6}))?)?(?:Z)?");
107: private static Pattern ISO_8601_BASIC_OFFSET_PATTERN = Pattern
108: .compile("(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})((\\d{2})([.,](\\d{1,6}))?)?([+-])(\\d{2})((\\d{2}))?");
109: private static Pattern ISO_8601_GNU_FORMAT_PATTERN = Pattern
110: .compile("(\\d{4})-(\\d{1,2})-(\\d{1,2})T(\\d{1,2}):(\\d{2})(:(\\d{2})([.,](\\d{1,6}))?)?([+-])(\\d{2})((\\d{2}))?");
111: private static Pattern SVN_LOG_DATE_FORMAT_PATTERN = Pattern
112: .compile("(\\d{4})-(\\d{1,2})-(\\d{1,2}) (\\d{1,2}):(\\d{2})(:(\\d{2})([.,](\\d{1,6}))?)?( ([+-])(\\d{2})(\\d{2})?)?");
113: private static Pattern TIME_ONLY_PATTERN = Pattern
114: .compile("(\\d{1,2}):(\\d{2})(:(\\d{2})([.,](\\d{1,6}))?)?");
115: private static final Collection ourTimeFormatPatterns = new LinkedList();
116:
117: static {
118: ourTimeFormatPatterns.add(ISO_8601_EXTENDED_DATE_ONLY_PATTERN);
119: ourTimeFormatPatterns.add(ISO_8601_EXTENDED_UTC_PATTERN);
120: ourTimeFormatPatterns.add(ISO_8601_EXTENDED_OFFSET_PATTERN);
121: ourTimeFormatPatterns.add(ISO_8601_BASIC_DATE_ONLY_PATTERN);
122: ourTimeFormatPatterns.add(ISO_8601_BASIC_UTC_PATTERN);
123: ourTimeFormatPatterns.add(ISO_8601_BASIC_OFFSET_PATTERN);
124: ourTimeFormatPatterns.add(SVN_LOG_DATE_FORMAT_PATTERN);
125: ourTimeFormatPatterns.add(ISO_8601_GNU_FORMAT_PATTERN);
126: ourTimeFormatPatterns.add(TIME_ONLY_PATTERN);
127: }
128:
129: private long myRevision;
130:
131: private String myName;
132:
133: private Date myDate;
134:
135: private int myID;
136:
137: private SVNRevision(long number) {
138: myRevision = number;
139: myName = null;
140: myID = 10;
141: }
142:
143: private SVNRevision(String name, int id) {
144: this (-1);
145: myName = name;
146: myID = id;
147: }
148:
149: private SVNRevision(Date date) {
150: this (-1);
151: myDate = date;
152: myID = 20;
153: }
154:
155: /**
156: * Gets the revision keyword name. Each of <b>SVNRevision</b>'s
157: * constant fields that represent revision keywords also have
158: * its own name.
159: *
160: * @return a revision keyword name
161: */
162: public String getName() {
163: return myName;
164: }
165:
166: /**
167: * Gets the revision number represented by this object.
168: *
169: * @return a revision number; -1 is returned when this object
170: * represents a revision information not using a revision
171: * number.
172: *
173: */
174: public long getNumber() {
175: return myRevision;
176: }
177:
178: /**
179: * Gets the timestamp used to specify a revision.
180: *
181: * @return a timestamp if any specified for this object
182: */
183: public Date getDate() {
184: return myDate;
185: }
186:
187: /**
188: * Checks if the revision information represented by this object
189: * is valid.
190: * <p>
191: * {@link #UNDEFINED} is not a valid revision.
192: *
193: * @return <span class="javakeyword">true</span> if valid, otherwise
194: * <span class="javakeyword">false</span>
195: */
196: public boolean isValid() {
197: return this != UNDEFINED
198: && (myDate != null || myRevision >= 0 || myName != null);
199: }
200:
201: /**
202: * Gets the identifier of the revision information kind this
203: * object represents.
204: *
205: * @return this object's id
206: */
207: public int getID() {
208: return myID;
209: }
210:
211: /**
212: * Evaluates the hash code for this object.
213: * A hash code is evaluated in this way:
214: * <ul>
215: * <li>if this object represents revision info as a revision number
216: * then
217: * <code>hash code = (<span class="javakeyword">int</span>) revisionNumber & 0xFFFFFFFF</code>;
218: * <li>if this object represents revision info as a timestamp then
219: * {@link java.util.Date#hashCode()} is used;
220: * <li>if this object represents revision info as a keyword
221: * then {@link java.lang.String#hashCode()} is used for the keyword name;
222: * </ul>
223: *
224: * @return this object's hash code
225: */
226: public int hashCode() {
227: if (myRevision >= 0) {
228: return (int) myRevision & 0xFFFFFFFF;
229: } else if (myDate != null) {
230: return myDate.hashCode();
231: } else if (myName != null) {
232: return myName.hashCode();
233: }
234: return -1;
235: }
236:
237: /**
238: * Compares this object with another <b>SVNRevision</b> object.
239: *
240: * @param o an object to be compared with; if it's not an
241: * <b>SVNRevision</b> then this method certainly returns
242: * <span class="javakeyword">false</span>
243: * @return <span class="javakeyword">true</span> if equal, otherwise
244: * <span class="javakeyword">false</span>
245: */
246: public boolean equals(Object o) {
247: if (o == null || o.getClass() != SVNRevision.class) {
248: return false;
249: }
250: SVNRevision r = (SVNRevision) o;
251: if (myRevision >= 0) {
252: return myRevision == r.getNumber();
253: } else if (myDate != null) {
254: return myDate.equals(r.getDate());
255: } else if (myName != null) {
256: return myName.equals(r.getName());
257: }
258: return !r.isValid();
259: }
260:
261: /**
262: * Checks whether a revision number is valid.
263: *
264: * @param revision a revision number
265: * @return <span class="javakeyword">true</span> if valid,
266: * otherwise false
267: */
268: public static boolean isValidRevisionNumber(long revision) {
269: return revision >= 0;
270: }
271:
272: /**
273: * Creates an <b>SVNRevision</b> object given a revision number.
274: *
275: * @param revisionNumber a definite revision number
276: * @return the constructed <b>SVNRevision</b> object
277: */
278: public static SVNRevision create(long revisionNumber) {
279: if (revisionNumber < 0) {
280: return SVNRevision.UNDEFINED;
281: }
282: return new SVNRevision(revisionNumber);
283: }
284:
285: /**
286: * Creates an <b>SVNRevision</b> object given a particular timestamp.
287: *
288: * @param date a timestamp represented as a Date instance
289: * @return the constructed <b>SVNRevision</b> object
290: */
291: public static SVNRevision create(Date date) {
292: return new SVNRevision(date);
293: }
294:
295: /**
296: * Determines if the revision represented by this abstract object is
297: * Working Copy specific - that is one of {@link #BASE} or {@link #WORKING}.
298: *
299: * @return <span class="javakeyword">true</span> if this object represents
300: * a kind of a local revision, otherwise <span class="javakeyword">false</span>
301: */
302: public boolean isLocal() {
303: boolean remote = !isValid() || this == SVNRevision.HEAD
304: || getNumber() >= 0 || getDate() != null;
305: return !remote;
306: }
307:
308: /**
309: * Parses an input string and be it a representation of either
310: * a revision number, or a timestamp, or a revision keyword, constructs
311: * an <b>SVNRevision</b> representation of the revision.
312: *
313: * @param value a string to be parsed
314: * @return an <b>SVNRevision</b> object that holds the revision
315: * information parsed from <code>value</code>; however
316: * if an input string is not a valid one which can be
317: * successfully transformed to an <b>SVNRevision</b> the
318: * return value is {@link SVNRevision#UNDEFINED}
319: */
320: public static SVNRevision parse(String value) {
321: if (value == null) {
322: return SVNRevision.UNDEFINED;
323: }
324: if (value.startsWith("-r")) {
325: value = value.substring("-r".length());
326: }
327: value = value.trim();
328: if (value.startsWith("{") && value.endsWith("}")) {
329: value = value.substring(1);
330: value = value.substring(0, value.length() - 1);
331:
332: try {
333: Calendar date = Calendar.getInstance();
334: for (Iterator patterns = ourTimeFormatPatterns
335: .iterator(); patterns.hasNext();) {
336: Pattern pattern = (Pattern) patterns.next();
337: Matcher matcher = pattern.matcher(value);
338: if (matcher.matches()) {
339: if (pattern == ISO_8601_EXTENDED_DATE_ONLY_PATTERN
340: || pattern == ISO_8601_BASIC_DATE_ONLY_PATTERN) {
341: int year = Integer.parseInt(matcher
342: .group(1));
343: int month = Integer.parseInt(matcher
344: .group(2));
345: int day = Integer
346: .parseInt(matcher.group(3));
347: date.clear();
348: date.set(year, month - 1, day);
349: } else if (pattern == ISO_8601_EXTENDED_UTC_PATTERN
350: || pattern == ISO_8601_EXTENDED_OFFSET_PATTERN
351: || pattern == ISO_8601_BASIC_UTC_PATTERN
352: || pattern == ISO_8601_BASIC_OFFSET_PATTERN
353: || pattern == ISO_8601_GNU_FORMAT_PATTERN
354: || pattern == SVN_LOG_DATE_FORMAT_PATTERN) {
355: int year = Integer.parseInt(matcher
356: .group(1));
357: int month = Integer.parseInt(matcher
358: .group(2));
359: int day = Integer
360: .parseInt(matcher.group(3));
361: int hours = Integer.parseInt(matcher
362: .group(4));
363: int minutes = Integer.parseInt(matcher
364: .group(5));
365: int seconds = 0;
366: int milliseconds = 0;
367: if (matcher.group(6) != null) {
368: seconds = Integer.parseInt(matcher
369: .group(7));
370: if (matcher.group(8) != null) {
371: String millis = matcher.group(9);
372: millis = millis.length() <= 3 ? millis
373: : millis.substring(0, 3);
374: milliseconds = Integer
375: .parseInt(millis);
376: }
377: }
378: date.clear();
379: date.set(year, month - 1, day, hours,
380: minutes, seconds);
381: date
382: .set(Calendar.MILLISECOND,
383: milliseconds);
384:
385: if (pattern == ISO_8601_EXTENDED_OFFSET_PATTERN
386: || pattern == ISO_8601_BASIC_OFFSET_PATTERN
387: || pattern == ISO_8601_GNU_FORMAT_PATTERN) {
388: int zoneOffsetInMillis = "+"
389: .equals(matcher.group(10)) ? +1
390: : -1;
391: int hoursOffset = Integer
392: .parseInt(matcher.group(11));
393: int minutesOffset = matcher.group(12) != null ? Integer
394: .parseInt(matcher.group(13))
395: : 0;
396: zoneOffsetInMillis = zoneOffsetInMillis
397: * ((hoursOffset * 3600 + minutesOffset * 60) * 1000);
398: date.set(Calendar.ZONE_OFFSET,
399: zoneOffsetInMillis);
400: } else if (pattern == SVN_LOG_DATE_FORMAT_PATTERN
401: && matcher.group(10) != null) {
402: int zoneOffsetInMillis = "+"
403: .equals(matcher.group(11)) ? +1
404: : -1;
405: int hoursOffset = Integer
406: .parseInt(matcher.group(12));
407: int minutesOffset = matcher.group(13) != null ? Integer
408: .parseInt(matcher.group(13))
409: : 0;
410: zoneOffsetInMillis = zoneOffsetInMillis
411: * ((hoursOffset * 3600 + minutesOffset * 60) * 1000);
412: date.set(Calendar.ZONE_OFFSET,
413: zoneOffsetInMillis);
414: }
415: } else if (pattern == TIME_ONLY_PATTERN) {
416: int hours = Integer.parseInt(matcher
417: .group(1));
418: int minutes = Integer.parseInt(matcher
419: .group(2));
420: int seconds = 0;
421: int milliseconds = 0;
422: if (matcher.group(3) != null) {
423: seconds = Integer.parseInt(matcher
424: .group(4));
425: if (matcher.group(5) != null) {
426: String millis = matcher.group(6);
427: millis = millis.length() <= 3 ? millis
428: : millis.substring(0, 3);
429: milliseconds = Integer
430: .parseInt(millis);
431: }
432: }
433: date.set(Calendar.HOUR_OF_DAY, hours);
434: date.set(Calendar.MINUTE, minutes);
435: date.set(Calendar.SECOND, seconds);
436: date
437: .set(Calendar.MILLISECOND,
438: milliseconds);
439: }
440: return SVNRevision.create(date.getTime());
441: }
442: }
443: return SVNRevision.UNDEFINED;
444: } catch (NumberFormatException e) {
445: return SVNRevision.UNDEFINED;
446: }
447: }
448: try {
449: long number = Long.parseLong(value);
450: return SVNRevision.create(number);
451: } catch (NumberFormatException nfe) {
452: }
453: SVNRevision revision = (SVNRevision) ourValidRevisions
454: .get(value.toUpperCase());
455: if (revision == null) {
456: return UNDEFINED;
457: }
458: return revision;
459: }
460:
461: /**
462: * Gives a string representation of this object.
463: *
464: * @return a string representing this object
465: */
466: public String toString() {
467: if (myRevision >= 0) {
468: return Long.toString(myRevision);
469: } else if (myName != null) {
470: return myName;
471: } else if (myDate != null) {
472: return DateFormat.getDateTimeInstance().format(myDate);
473: }
474: return "{invalid revision}";
475: }
476:
477: }
|