001: /*****************************************************************************
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is the CVS Client Library.
027: * The Initial Developer of the Original Software is Robert Greig.
028: * Portions created by Robert Greig are Copyright (C) 2000.
029: * All Rights Reserved.
030: *
031: * If you wish your version of this file to be governed by only the CDDL
032: * or only the GPL Version 2, indicate your decision by adding
033: * "[Contributor] elects to include this software in this distribution
034: * under the [CDDL or GPL Version 2] license." If you do not indicate a
035: * single choice of license, a recipient has the option to distribute
036: * your version of this file under either the CDDL, the GPL Version 2 or
037: * to extend the choice of license to its licensees as provided above.
038: * However, if you add GPL Version 2 code and therefore, elected the GPL
039: * Version 2 license, then the option applies only if the new code is
040: * made subject to such option by the copyright holder.
041: *
042: * Contributor(s): Robert Greig.
043: *****************************************************************************/package org.netbeans.lib.cvsclient.admin;
044:
045: import java.text.*;
046: import java.util.*;
047:
048: /**
049: * The class abstracts the CVS concept of an <i>entry line</i>. The entry
050: * line is textually of the form:<p>
051: * / name / version / conflict / options / tag_or_date
052: * <p>These are explained in section 5.1 of the CVS protocol 1.10 document.
053: * @author Robert Greig
054: */
055: public final class Entry {
056: /**
057: * The dummy timestamp set the conflict information for added or removed
058: * files.
059: */
060: public static final String DUMMY_TIMESTAMP = "dummy timestamp"; //NOI18N
061: public static final String DUMMY_TIMESTAMP_NEW_ENTRY = "dummy timestamp from new-entry"; //NOI18N
062:
063: public static final String MERGE_TIMESTAMP = "Result of merge"; //NOI18N
064:
065: /**
066: * Indicates a sticky tag.
067: */
068: private static final String TAG = "T"; //NOI18N
069:
070: /**
071: * Indicates a sticky date.
072: */
073: private static final String DATE = "D"; //NOI18N
074:
075: /**
076: * The instance of the date formatter for sticky dates.
077: */
078: private static SimpleDateFormat stickyDateFormatter;
079:
080: /**
081: * Returns the instance of the date formatter for sticky dates.
082: */
083: private static SimpleDateFormat getStickyDateFormatter() {
084: if (stickyDateFormatter == null) {
085: stickyDateFormatter = new SimpleDateFormat(
086: "yyyy.MM.dd.hh.mm.ss"); //NOI18N
087: }
088: return stickyDateFormatter;
089: }
090:
091: /**
092: * Indicates a binary file.
093: */
094: private static final String BINARY_FILE = "-kb"; //NOI18N
095:
096: /**
097: * Indicates that no user file is meant by the version details
098: */
099: private static final String NO_USER_FILE = ""; //NOI18N
100:
101: /**
102: * Indicates that a new user file is meant by the version details
103: */
104: private static final String NEW_USER_FILE = "0"; //NOI18N
105:
106: /**
107: * Indicates that the file is to be removed, in the version details
108: */
109: private static final String REMOVE_USER_FILE = "-"; //NOI18N
110:
111: /**
112: * Returns the instance of the Last-Modified-Date-Formatter.
113: */
114: public static SimpleDateFormat getLastModifiedDateFormatter() {
115: SimpleDateFormat df = new SimpleDateFormat(
116: "EEE MMM dd HH:mm:ss yyyy", Locale.US); //NOI18N
117: df.setTimeZone(getTimeZone());
118: return df;
119: }
120:
121: /**
122: * All entries times are by defaulf in Zulu/GMT0
123: */
124: public static TimeZone getTimeZone() {
125: return TimeZone.getTimeZone("GMT"); //NOI18N
126: }
127:
128: /**
129: * Indicates that the file had conflicts.
130: */
131: public static final char HAD_CONFLICTS = '+';
132:
133: /**
134: * Indicates that the timestamp matches the file.
135: */
136: public static final char TIMESTAMP_MATCHES_FILE = '=';
137:
138: /**
139: * Indicates that the file had conflicts and timestamp matches.
140: * It likely means unresolved conflict.
141: */
142: public static final String HAD_CONFLICTS_AND_TIMESTAMP_MATCHES_FILE = "+=";
143:
144: /**
145: * Initial letter that indicates a directory entry.
146: */
147: private static final String DIRECTORY_PREFIX = "D/";
148:
149: /**
150: * The name of the file.
151: */
152: private String name;
153:
154: /**
155: * The revision. There are constants defined for no user file, new user
156: * file and user file has to be removed.
157: */
158: private String revision;
159:
160: /**
161: * The conflict information. There are constants defined for indicating
162: * that conflicts occurred and that the timestamp matches the file
163: */
164: private String conflict;
165:
166: /**
167: * The last modified date of the file.
168: */
169: private Date lastModified;
170:
171: /**
172: * The options for signifying keyword expansion.
173: */
174: private String options;
175:
176: /**
177: * The tag. May be present in place of the date information.
178: */
179: private String tag;
180:
181: /**
182: * The date. May be present in place of the tag information.
183: */
184: private Date date;
185:
186: /**
187: * Indicates whether the entry is for a directory.
188: */
189: private boolean directory;
190:
191: /**
192: * Construct a new Entry from a given entry line.
193: */
194: public Entry(String entryLine) {
195: init(entryLine);
196: }
197:
198: /**
199: * Construct a new blank Entry.
200: */
201: public Entry() {
202: }
203:
204: /**
205: * Initialise the Entry by parsing an entry line.
206: * @param entryLine the entry line in standard CVS format
207: */
208: protected void init(String entryLine) {
209: //System.err.println("Constructing an entry line from: " + entryLine);
210: // try to parse the entry line, if we get stuck just
211: // throw an illegal argument exception
212:
213: if (entryLine.startsWith(DIRECTORY_PREFIX)) {
214: directory = true;
215: entryLine = entryLine.substring(1);
216: }
217:
218: // first character is a slash, so name is read from position 1
219: // up to the next slash
220: final int[] slashPositions = new int[5];
221:
222: try {
223: slashPositions[0] = 0;
224: for (int i = 1; i < 5; i++) {
225: slashPositions[i] = entryLine.indexOf('/',
226: slashPositions[i - 1] + 1);
227: }
228:
229: // Test if this is a D on its own, a special case indicating that
230: // directories are understood and there are no subdirectories
231: // in the current folder
232: if (slashPositions[1] > 0) {
233: // note that the parameters to substring are treated as follows:
234: // (inclusive, exclusive)
235: name = entryLine.substring(slashPositions[0] + 1,
236: slashPositions[1]);
237: revision = entryLine.substring(slashPositions[1] + 1,
238: slashPositions[2]);
239: if ((slashPositions[3] - slashPositions[2]) > 1) {
240: String conflict = entryLine.substring(
241: slashPositions[2] + 1, slashPositions[3]);
242: setConflict(conflict);
243: }
244: if ((slashPositions[4] - slashPositions[3]) > 1) {
245: options = entryLine.substring(
246: slashPositions[3] + 1, slashPositions[4]);
247: }
248: if (slashPositions[4] != (entryLine.length() - 1)) {
249: String tagOrDate = entryLine
250: .substring(slashPositions[4] + 1);
251: if (tagOrDate.startsWith(TAG)) {
252: setTag(tagOrDate.substring(1));
253: } else if (tagOrDate.startsWith(DATE)) {
254: //TODO process date into something useful
255: // MK - I didn't notice any time conversions (according to timezone)
256: // So I just convert it from String to Date and back.
257: try {
258: String dateString = tagOrDate
259: .substring(DATE.length());
260: Date stickyDate = getStickyDateFormatter()
261: .parse(dateString);
262: setDate(stickyDate);
263: } catch (ParseException exc) {
264: System.err
265: .println("We got another inconsistency in the library's date formatting."); //NOI18N
266: }
267: }
268: }
269: }
270: } catch (Exception e) {
271: System.err.println("Error parsing entry line: " + e); //NOI18N
272: e.printStackTrace();
273: throw new IllegalArgumentException("Invalid entry line: " + //NOI18N
274: entryLine);
275: }
276: }
277:
278: /**
279: * Get the name of the associated file.
280: * @return the file name
281: */
282: public String getName() {
283: return name;
284: }
285:
286: /**
287: * Set the name.
288: * @param theName the filename to set
289: */
290: public void setName(String name) {
291: this .name = name;
292: }
293:
294: /**
295: * Get the revision.
296: * @return the revision
297: */
298: public String getRevision() {
299: return revision;
300: }
301:
302: /**
303: * Set the revision.
304: * @param theVersion the revision to set
305: */
306: public void setRevision(String revision) {
307: this .revision = revision;
308: }
309:
310: /**
311: * Get the last modification time.
312: *
313: * @return date.getTime() compatible with File.lastModified()
314: */
315: public Date getLastModified() {
316: return lastModified;
317: }
318:
319: /**
320: * Get the conflict information.
321: * @return the conflict String
322: */
323: public String getConflict() {
324: return conflict;
325: }
326:
327: /**
328: * Set the conflict information.
329: * @param theConflict the conflict information
330: */
331: public void setConflict(String conflict) {
332: this .conflict = conflict;
333: this .lastModified = null;
334:
335: if (conflict == null || conflict.equals(DUMMY_TIMESTAMP)
336: || conflict.equals(MERGE_TIMESTAMP)
337: || conflict.equals(DUMMY_TIMESTAMP_NEW_ENTRY)) {
338: return;
339: }
340:
341: String dateString = conflict;
342:
343: // Look for the position of + which indicates a conflict
344: int conflictIndex = dateString.indexOf(HAD_CONFLICTS);
345: if (conflictIndex >= 0) {
346: // if the timestamp matches the file, there will be an = following
347: // the +
348: int timeMatchIndex = dateString
349: .indexOf(TIMESTAMP_MATCHES_FILE);
350: conflictIndex = Math.max(conflictIndex, timeMatchIndex);
351: }
352:
353: // At this point the conflict index tells us where the real conflict
354: // string starts
355: if (conflictIndex >= 0) {
356: dateString = dateString.substring(conflictIndex + 1);
357: }
358:
359: // if we have nothing after the = then don't try to parse it
360: if (dateString.length() == 0) {
361: return;
362: }
363:
364: try {
365: this .lastModified = getLastModifiedDateFormatter().parse(
366: dateString);
367: } catch (Exception ex) {
368: lastModified = null;
369: // System.err.println("[Entry] can't parse " + dateString); //NOI18N
370: }
371: }
372:
373: /**
374: * Get the options information.
375: * @return the options details
376: */
377: public String getOptions() {
378: return options;
379: }
380:
381: /**
382: * Set the options information.
383: * @param theOptions the options
384: */
385: public void setOptions(String options) {
386: this .options = options;
387: }
388:
389: /**
390: * Get the sticky information.
391: * It's either a tag, a date or null.
392: */
393: public String getStickyInformation() {
394: if (tag != null) {
395: return tag;
396: }
397: return getDateFormatted();
398: }
399:
400: /**
401: * Get the sticky tag information.
402: * May return null if no tag information was present. If so, you should
403: * check for date information. Note that tag and date information cannot
404: * both be present.
405: * @return the tag, or null if none is present
406: */
407: public String getTag() {
408: return tag;
409: }
410:
411: /**
412: * Set the sticky tag information.
413: * Setting this will remove any date information that is set.
414: * @param theTag the tag information
415: */
416: public void setTag(String tag) {
417: this .tag = tag;
418: date = null;
419: }
420:
421: /**
422: * Get sticky date information.
423: * May return null if no date information is available. If so, you should
424: * check for tag informaton. Note that tag and date information cannot both
425: * be present.
426: * @return the date, or null if none is present
427: */
428: public Date getDate() {
429: return date;
430: }
431:
432: /**
433: * Gets the sticky date information as a string in the appropriate format.
434: * Returns null if there ain't a sticky date assigned.
435: */
436: public String getDateFormatted() {
437: if (getDate() == null) {
438: return null;
439: }
440: SimpleDateFormat format = getStickyDateFormatter();
441: String dateFormatted = format.format(getDate());
442: return dateFormatted;
443: }
444:
445: /**
446: * Set the sticky date information.
447: * Note that setting this will remove any tag information that is currently set.
448: * @param theDate the date to use.
449: */
450: public void setDate(Date date) {
451: this .date = date;
452: tag = null;
453: }
454:
455: /**
456: * Determines whether the entry has a date (as opposed to a tag).
457: * @return true if the entry has a date, false otherwise
458: */
459: public boolean hasDate() {
460: return (date != null);
461: }
462:
463: /**
464: * Determines whether the entry has a tag (as opposed to a date).
465: * @return true if the entry has a tag, false otherwise
466: */
467: public boolean hasTag() {
468: return (tag != null);
469: }
470:
471: /**
472: * Determines whether the file is a binary file.
473: */
474: public boolean isBinary() {
475: return options != null && options.equals(BINARY_FILE);
476: }
477:
478: /**
479: * Determine whether there is no user file of that name.
480: * @return true if there is no user file of that name
481: */
482: public boolean isNoUserFile() {
483: return revision == null || revision.equals(NO_USER_FILE);
484: }
485:
486: /**
487: * Determine whether there is a new user file of that name.
488: * @return true if there is a new user file with that name
489: */
490: public boolean isNewUserFile() {
491: return revision != null && revision.startsWith(NEW_USER_FILE);
492: }
493:
494: /**
495: * Determine whether the user file of that name is to be removed.
496: * @return true if the user file with this name is to be removed
497: */
498: public boolean isUserFileToBeRemoved() {
499: return revision != null
500: && revision.startsWith(REMOVE_USER_FILE);
501: }
502:
503: /**
504: * Determines whether the entry is valid.
505: * A valid entry has at least a name.
506: */
507: public boolean isValid() {
508: return getName() != null && getName().length() > 0;
509: }
510:
511: /**
512: * Determine whether the entry refers to a directory.
513: */
514: public boolean isDirectory() {
515: return directory;
516: }
517:
518: /**
519: * Set whether the entry refers to a directory.
520: */
521: public void setDirectory(boolean directory) {
522: this .directory = directory;
523: }
524:
525: /**
526: * Determine whether there were any conflicts.
527: * @return true if there were conflicts, false otherwise
528: */
529: public boolean hadConflicts() {
530: if (conflict != null) {
531: return conflict.indexOf(HAD_CONFLICTS) >= 0;
532: } else {
533: return false;
534: }
535: }
536:
537: /**
538: * Determine whether the timestamp matches the file.
539: * @return true if the timpestamp does match the file, false otherwise
540: */
541: public boolean timestampMatchesFile() {
542: return (conflict.charAt(1) == TIMESTAMP_MATCHES_FILE);
543: }
544:
545: /**
546: * Create a string representation of the entry line.
547: * Create the standard CVS 1.10 entry line format.
548: * <p>
549: * Th eline format is suitable for writing into <tt>CVS/Entries</tt> file.
550: * Conflict one must be transformed before sending to wire
551: * {@link org.netbeans.lib.cvsclient.command.BasicCommand#sendEntryAndModifiedRequests}.
552: */
553: public String toString() {
554: StringBuffer buf = new StringBuffer();
555: if (directory) {
556: buf.append(DIRECTORY_PREFIX);
557: } else {
558: buf.append('/');
559: }
560: // if name is null, then this is a totally empty entry, so append
561: // nothing further
562: if (name != null) {
563: buf.append(name);
564: buf.append('/');
565: if (revision != null) {
566: buf.append(revision);
567: }
568: buf.append('/');
569: if (conflict != null) {
570: buf.append(conflict);
571: }
572: buf.append('/');
573: if (options != null) {
574: buf.append(options);
575: }
576: buf.append('/');
577: // TODO: put in tag_or_date section!!!
578: // MK - Added. Based on assumption "There can be only one"
579: if (tag != null && date == null) {
580: if ("HEAD".equals(tag) == false) {
581: buf.append(TAG);
582: buf.append(getTag());
583: }
584: } else if (tag == null && date != null) {
585: String dateString = getDateFormatted();
586: buf.append(DATE);
587: buf.append(dateString);
588: }
589: }
590: return buf.toString();
591: }
592: }
|