001: /* ====================================================================
002: * The JRefactory License, Version 1.0
003: *
004: * Copyright (c) 2001 JRefactory. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions
008: * are met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in
015: * the documentation and/or other materials provided with the
016: * distribution.
017: *
018: * 3. The end-user documentation included with the redistribution,
019: * if any, must include the following acknowledgment:
020: * "This product includes software developed by the
021: * JRefactory (http://www.sourceforge.org/projects/jrefactory)."
022: * Alternately, this acknowledgment may appear in the software itself,
023: * if and wherever such third-party acknowledgments normally appear.
024: *
025: * 4. The names "JRefactory" must not be used to endorse or promote
026: * products derived from this software without prior written
027: * permission. For written permission, please contact seguin@acm.org.
028: *
029: * 5. Products derived from this software may not be called "JRefactory",
030: * nor may "JRefactory" appear in their name, without prior written
031: * permission of Chris Seguin.
032: *
033: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
034: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
035: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036: * DISCLAIMED. IN NO EVENT SHALL THE CHRIS SEGUIN OR
037: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
038: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
039: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
040: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
042: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
043: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
044: * SUCH DAMAGE.
045: * ====================================================================
046: *
047: * This software consists of voluntary contributions made by many
048: * individuals on behalf of JRefactory. For more information on
049: * JRefactory, please see
050: * <http://www.sourceforge.org/projects/jrefactory>.
051: */
052: package org.acm.seguin.ant;
053:
054: import java.io.*;
055: import java.text.*;
056: import java.util.*;
057: import org.acm.seguin.awt.ExceptionPrinter;
058: import org.acm.seguin.awt.TextExceptionPrinter;
059:
060: /**
061: * @author Ara Abrahamian (ara_e@email.com)
062: * @created June 21, 2001
063: * @version $Revision: 1.5 $
064: */
065: public final class CVSUtil {
066: private HashMap entries;
067:
068: private static TimeZone tz;
069:
070: /**
071: * Constructor for the CVSUtil object
072: *
073: */
074: public CVSUtil() {
075: entries = new HashMap();
076: }
077:
078: /**
079: * Sets the timeZone attribute of the CVSUtil object
080: *
081: * @param timeZone The new timeZone value
082: * @since 2.9.12
083: */
084: public void setTimeZone(String timeZone) {
085: tz = TimeZone.getTimeZone(timeZone);
086: }
087:
088: /**
089: * Gets the fileModified attribute of the CVSUtil object
090: *
091: * @param file Description of the Parameter
092: * @return The fileModified value
093: */
094: public boolean isFileModified(File file) {
095: CVSEntry entry = (CVSEntry) entries.get(file.toString()
096: .replace(File.separatorChar, '/'));
097:
098: if (entry == null) {
099: //either a new file or the Entries file not loaded yet
100:
101: //try loading the Entries file
102: entry = loadEntriesFileFor(file);
103:
104: if (entry == null) {
105: //new file, not yet placed at cvs
106:
107: return true;
108: }
109: }
110:
111: return !entry.equalsTime(file.lastModified());
112: }
113:
114: /**
115: * Description of the Method
116: *
117: * @param file Description of the Parameter
118: * @return Description of the Return Value
119: */
120: private CVSEntry loadEntriesFileFor(File file) {
121: File workingDirectory = file.getParentFile();
122: int linenum = 0;
123: String line = null;
124: BufferedReader in = null;
125: File entriesFile = new File(workingDirectory + "/CVS/Entries");
126:
127: if (!entriesFile.exists()) {
128: return null;
129: }
130:
131: try {
132: in = new BufferedReader(new FileReader(entriesFile));
133:
134: for (linenum = 1;; ++linenum) {
135: try {
136: line = in.readLine();
137: } catch (IOException ex) {
138: line = null;
139: break;
140: }
141:
142: if (line == null) {
143: break;
144: }
145:
146: if (line.startsWith("/")) {
147: try {
148: CVSEntry entry = CVSEntry.parseEntryLine(
149: workingDirectory, line);
150:
151: entries.put(entry.getFileName(), entry);
152: } catch (ParseException ex) {
153: System.err.println("Bad 'Entries' line "
154: + linenum + ", '" + line + "' - "
155: + ex.getMessage());
156: }
157: }
158: }
159: } catch (IOException ex) {
160: in = null;
161: } finally {
162: if (in != null) {
163: try {
164: in.close();
165: } catch (IOException ex) {
166: }
167: }
168: }
169:
170: return (CVSEntry) entries.get(file.toString().replace(
171: File.separatorChar, '/'));
172: }
173:
174: /**
175: * Description of Class
176: *
177: * @author Ara Abrahamian
178: * @created October 18, 2001
179: */
180: public static class CVSEntry {
181: private Date date;
182: private String fileName;
183:
184: /**
185: * Sets the timestamp attribute of the CVSEntry object
186: *
187: * @param timeStamp The new timestamp value
188: * @since 2.9.12
189: */
190: public void setTimestamp(String timeStamp) {
191: String tstamp = new String(timeStamp);
192: String conflict = null;
193:
194: if (tstamp.length() < 1) {
195: this .date = null;
196: } else if (tstamp.startsWith("+")) {
197: // We have received a "+conflict" format, which
198: // typically only comes from the server.
199: conflict = tstamp.substring(1);
200:
201: if (conflict.equals("=")) {
202: // In this case, the server is indicating that the
203: // file is "going to be equal" once the 'Merged' handling
204: // is completed. To retain the "inConflict" nature of
205: // the entry, we will simply set the conflict to an
206: // empty string (not null), as the conflict will be
207: // set very shortly as a result of the 'Merged' handling.
208: //
209: conflict = "";
210: }
211: } else {
212: int index = tstamp.indexOf('+');
213:
214: if (index < 0) {
215: this .date = null;
216: } else {
217: // The "timestamp+conflict" case.
218: // This should <em>only</em> comes from an Entries
219: // file, and should never come from the server.
220: conflict = tstamp.substring(index + 1);
221: tstamp = tstamp.substring(0, index);
222:
223: this .date = null;
224: // signal need to parse!
225:
226: if (tstamp.equals("Result of merge")) {
227: // REVIEW should we always set to conflict?
228: // If timestamp is empty, use the conflict...
229: if ((timeStamp == null || timeStamp.length() == 0)
230: && conflict.length() > 0) {
231: timeStamp = conflict;
232: }
233: } else {
234: timeStamp = tstamp;
235: }
236: }
237: }
238:
239: // If tsCache is set to null, we need to update it...
240: if (this .date == null && timeStamp.length() > 0) {
241: try {
242: this .date = parseTimestamp(timeStamp);
243: } catch (ParseException ex) {
244: this .date = null;
245: }
246: }
247: }
248:
249: /**
250: * Gets the fileName attribute of the CVSEntry object
251: *
252: * @return The fileName value
253: */
254: public String getFileName() {
255: return fileName;
256: }
257:
258: /**
259: * Description of the Method
260: *
261: * @param src Description of the Parameter
262: * @return Description of the Return Value
263: */
264: public boolean equals(Object src) {
265: if (src instanceof CVSEntry) {
266: return fileName.equals(((CVSEntry) src).getFileName());
267: } else {
268: return false;
269: }
270: }
271:
272: /**
273: * Determines if this timestamp is considered equivalent to the time represented by the parameter we are passed.
274: * Note that we allow up to, but not including, one second of time difference, since Java allows millisecond time
275: * resolution while CVS stores second resolution timestamps. Further, we allow the resolution difference on
276: * either side of the second because we can not be sure of the rounding.
277: *
278: * @param time Description of the Parameter
279: * @return Description of the Return Value
280: */
281: public boolean equalsTime(long time) {
282: //System.out.println("time="+time);
283: //System.out.println("date.getTime()="+date.getTime());
284: if (date == null) {
285: return false;
286: } else {
287: return (date.getTime() > time) ? ((date.getTime() - time) < 1000)
288: : ((time - date.getTime()) < 1000);
289: }
290: }
291:
292: /**
293: * Description of the Method
294: *
295: * @return Description of the Return Value
296: */
297: public int hashCode() {
298: return fileName.hashCode();
299: }
300:
301: /**
302: * Description of the Method
303: *
304: * @param source Description of the Parameter
305: * @return Description of the Return Value
306: * @exception ParseException Description of the Exception
307: */
308: public Date parseTimestamp(String source) throws ParseException {
309: SimpleDateFormat dateFormat = new SimpleDateFormat(
310: "EEE MMM dd HH:mm:ss yyyy", Locale.US);
311:
312: dateFormat.setTimeZone(CVSUtil.tz);
313:
314: Date result = dateFormat
315: .parse(source, new ParsePosition(0));
316:
317: if (result == null) {
318: throw new ParseException("invalid timestamp '" + source
319: + "'", 0);
320: }
321:
322: return result;
323: }
324:
325: /**
326: * Description of the Method
327: *
328: * @return Description of the Return Value
329: */
330: public String toString() {
331: return "fileName=" + fileName + " date=" + date;
332: }
333:
334: /**
335: * Gets the date attribute of the CVSEntry object
336: *
337: * @return The date value
338: */
339: private Date getDate() {
340: return date;
341: }
342:
343: /**
344: * Description of the Method
345: *
346: * @param parent_dir Description of the Parameter
347: * @param parseLine Description of the Parameter
348: * @return Description of the Return Value
349: * @exception ParseException Description of the Exception
350: */
351: public static CVSEntry parseEntryLine(File parent_dir,
352: String parseLine) throws ParseException {
353: String token = null;
354: String nameToke = null;
355: String versionToke = null;
356: String conflictToke = null;
357: String optionsToke = null;
358: String tagToke = null;
359: StringTokenizer toker = new StringTokenizer(parseLine, "/",
360: true);
361: int tokeCount = toker.countTokens();
362:
363: if (tokeCount < 6) {
364: throw new ParseException(
365: "not enough tokens in entries line "
366: + "(min 6, parsed " + tokeCount + ")",
367: 0);
368: }
369:
370: token = parseAToken(toker);
371: //the starting slash
372:
373: nameToke = parseAToken(toker);
374:
375: if (nameToke == null) {
376: throw new ParseException("could not parse entry name",
377: 0);
378: } else if (nameToke.equals("/")) {
379: throw new ParseException("entry has an empty name", 0);
380: } else {
381: token = parseAToken(toker);
382:
383: if (token == null || !token.equals("/")) {
384: throw new ParseException(
385: "could not parse version's starting slash",
386: 0);
387: }
388: }
389:
390: versionToke = parseAToken(toker);
391:
392: if (versionToke == null) {
393: throw new ParseException(
394: "out of tokens getting version field", 0);
395: } else if (versionToke.equals("/")) {
396: versionToke = "";
397: } else {
398: token = parseAToken(toker);
399:
400: if (token == null || !token.equals("/")) {
401: throw new ParseException(
402: "could not parse conflict's starting slash",
403: 0);
404: }
405: }
406:
407: conflictToke = parseAToken(toker);
408:
409: if (conflictToke == null) {
410: throw new ParseException(
411: "out of tokens getting conflict field", 0);
412: } else if (conflictToke.equals("/")) {
413: conflictToke = "";
414: } else {
415: token = parseAToken(toker);
416:
417: if (token == null || !token.equals("/")) {
418: throw new ParseException(
419: "could not parse options' starting slash",
420: 0);
421: }
422: }
423:
424: optionsToke = parseAToken(toker);
425:
426: if (optionsToke == null) {
427: throw new ParseException(
428: "out of tokens getting options field", 0);
429: } else if (optionsToke.equals("/")) {
430: optionsToke = "";
431: } else {
432: token = parseAToken(toker);
433:
434: if (token == null || !token.equals("/")) {
435: throw new ParseException(
436: "could not parse tag's starting slash", 0);
437: }
438: }
439:
440: tagToke = parseAToken(toker);
441:
442: if (tagToke == null || tagToke.equals("/")) {
443: tagToke = "";
444: }
445:
446: CVSEntry entry = new CVSEntry();
447:
448: nameToke = parent_dir.toString() + "/" + nameToke;
449: entry.fileName = nameToke.replace(File.separatorChar, '/');
450: entry.setTimestamp(conflictToke);
451:
452: return entry;
453: }
454:
455: /**
456: * Description of the Method
457: *
458: * @param toker Description of the Parameter
459: * @return Description of the Return Value
460: */
461: private static String parseAToken(StringTokenizer toker) {
462: String token = null;
463:
464: try {
465: token = toker.nextToken();
466: } catch (NoSuchElementException ex) {
467: token = null;
468: }
469:
470: return token;
471: }
472: }
473:
474: static {
475: tz = TimeZone.getTimeZone("GMT");
476: ExceptionPrinter.register(new TextExceptionPrinter());
477: }
478: }
|