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.command;
044:
045: import java.io.*;
046: import java.util.*;
047:
048: import org.netbeans.lib.cvsclient.*;
049: import org.netbeans.lib.cvsclient.admin.*;
050: import org.netbeans.lib.cvsclient.connection.*;
051: import org.netbeans.lib.cvsclient.event.*;
052: import org.netbeans.lib.cvsclient.request.*;
053:
054: /**
055: * A class that provides common functionality for many of the CVS command
056: * that send similar sequences of requests.
057: * @author Robert Greig
058: */
059: public abstract class BasicCommand extends BuildableCommand {
060: /**
061: * The requests that are sent and processed.
062: */
063: protected List requests = new LinkedList();
064:
065: /**
066: * The client services that are provided to this command.
067: */
068: protected ClientServices clientServices;
069:
070: /**
071: * Whether to update recursively.
072: */
073: private boolean recursive = true;
074:
075: /**
076: * The files and/or directories to operate on.
077: */
078: protected File[] files;
079:
080: /**
081: * Gets the value of the recursive option.
082: * @return true if recursive, false if not
083: * @deprecated use isRecursive instead
084: */
085: public boolean getRecursive() {
086: return recursive;
087: }
088:
089: /**
090: * Gets the value of the recursive option.
091: * @return true if recursive, false if not
092: */
093: public boolean isRecursive() {
094: return recursive;
095: }
096:
097: /**
098: * Sets the value of the recursive option.
099: * @param recursive true if the command should recurse, false otherwise
100: */
101: public void setRecursive(boolean recursive) {
102: this .recursive = recursive;
103: }
104:
105: /**
106: * Set the files and/or directories on which to execute the command.
107: * The way these are processed is:<P>
108: * <UL><LI>Default action (i.e. not setting the files explicitly or
109: * setting them to <pre>null</pre>) is to use the directory in which
110: * the command was executed (see how directories are treated, below)</LI>
111: * <LI>Files are handled how you would expect</LI>
112: * <LI>For directories, all files within the directory are sent</LI></UL>
113: * @param theFiles the files to operate on. May be null to indicate that the
114: * local directory specified in the client should be used. Full, absolute
115: * canonical pathnames <b>must</b> be supplied.
116: */
117: public void setFiles(File[] theFiles) {
118: // sort array.. files first, directories follow
119: if (theFiles == null) {
120: files = theFiles;
121: return;
122: }
123:
124: // assert theFiles.length > 0 : "Empty array causes random AIOOBEs!"; // ClientRuntime.java:119
125:
126: files = new File[theFiles.length];
127: int fileCount = 0;
128: int dirCount = 0;
129: int totalCount = theFiles.length;
130: for (int index = 0; index < totalCount; index++) {
131: File currentFile = theFiles[index];
132: if (currentFile.isDirectory()) {
133: files[totalCount - (1 + dirCount)] = currentFile;
134: dirCount = dirCount + 1;
135: } else {
136: files[fileCount] = currentFile;
137: fileCount = fileCount + 1;
138: }
139: }
140: }
141:
142: /**
143: * Get the files and/or directories specified for this command to operate
144: * on.
145: * @return the array of Files
146: */
147: public File[] getFiles() {
148: return files;
149: }
150:
151: /**
152: * Get a single file from the "files" list. returns only files, not directories.
153: * This method is used from within the builders, because for single file requests, the
154: * cvs server doesn't return us enough information to identify what file has been returned.
155: * Thus we sort the "files" array (files come before directories. Then the response froms erver comes in the same order
156: * and the files can be found this way.
157: *
158: * @param index the index of the file in the list.
159: */
160: public File getXthFile(int index) {
161: if (index < 0 || index >= files.length) {
162: return null;
163: }
164: File file = files[index];
165: if (!file.isFile()) {
166: return null;
167: }
168: return file;
169: }
170:
171: /**
172: * @param ending - the ending part of the file's pathname.. path separator is cvs's default '/'
173: */
174: public File getFileEndingWith(String ending) {
175: String locEnding = ending.replace('\\', '/');
176: String localDir = getLocalDirectory().replace('\\', '/');
177: int index = 0;
178: for (index = 0; index < files.length; index++) {
179: String path = files[index].getAbsolutePath();
180: String parentPath = files[index].getParentFile()
181: .getAbsolutePath().replace('\\', '/');
182: path = path.replace('\\', '/');
183: if ((path.endsWith(locEnding) && locEnding.indexOf('/') >= 0)
184: || (files[index].getName().equals(locEnding) && parentPath
185: .equals(localDir))) {
186: return files[index];
187: }
188: }
189: return null;
190: }
191:
192: /**
193: * Add the appropriate requests for a specified path. For a directory,
194: * process all the files within that directory and for a single file,
195: * just send it. For each directory, send a directory request. For each
196: * file send an Entry request followed by a Modified request.
197: * @param path the particular path to issue requests for. May be
198: * either a file or a directory.
199: */
200: private void addRequests(File path) throws FileNotFoundException,
201: IOException, CommandAbortedException {
202: if (path == null) {
203: throw new IllegalArgumentException(
204: "Cannot add requests for a " + "null path.");
205: }
206:
207: if (!path.exists() || path.isFile()) {
208: addRequestsForFile(path);
209: } else {
210: addRequestsForDirectory(path);
211: }
212: }
213:
214: /**
215: * Should return true if unchanged files should not be sent to server.
216: * If false is returned, all files will be sent to server
217: * This method is used by <code>sendEntryAndModifiedRequests</code>.
218: */
219: protected boolean doesCheckFileTime() {
220: return true;
221: }
222:
223: /**
224: * Send an Entry followed by a Modified or Unchanged request based on
225: * whether the file has been untouched on the local machine.
226: *
227: * @param entry the entry for the file
228: * @param file the file in question
229: */
230: protected void sendEntryAndModifiedRequests(Entry entry, File file) {
231: if (entry == null) {
232: return;
233: }
234:
235: // for deleted added files, don't send anything..
236: if (file != null && !file.exists() && entry.isNewUserFile()) {
237: return;
238: }
239:
240: Date entryLastModified = entry.getLastModified();
241: boolean hadConflicts = entry.hadConflicts();
242: if (!hadConflicts) {
243: // we null out the conflict field if there is no conflict
244: // because we use that field to store the timestamp of the
245: // file (just like command-line CVS). There is no point
246: // in sending this information to the CVS server, even
247: // though it should be ignored by the server.
248: entry.setConflict(null);
249: } else if (fileRequiresConflictResolution(file, entry)) {
250: // send entry in wire conflict format
251: Entry clone = new Entry(entry.toString());
252: clone
253: .setConflict(Entry.HAD_CONFLICTS_AND_TIMESTAMP_MATCHES_FILE);
254: entry = clone;
255: }
256: addRequest(new EntryRequest(entry));
257:
258: if (file == null || !file.exists()
259: || entry.isUserFileToBeRemoved()) {
260: return;
261: }
262:
263: if (doesCheckFileTime() && !hadConflicts
264: && entryLastModified != null) {
265: if (DateComparator.getInstance().equals(
266: file.lastModified(), entryLastModified.getTime())) {
267: addRequest(new UnchangedRequest(file.getName()));
268: return;
269: }
270: }
271:
272: addRequest(new ModifiedRequest(file, entry.isBinary()));
273: }
274:
275: /**
276: * When committing, we need to perform a check that will prevent the
277: * unmodified conflicting files from being checked in. This is the behavior
278: * of command line CVS client. This method checks the Entry for the file
279: * against the time stamp. The user can optimally call this method only if
280: * the Entry for the file indicates a conflict
281: * @param entry The Entry object corresponding to the file
282: * @param file The File object representing the file on the filesystem
283: * @return boolean Returns true if the file's timestamp is same or less than
284: * the time when the conflicting merge was done by CVS update as indicated
285: * by the Entry.
286: */
287: private final boolean fileRequiresConflictResolution(File file,
288: Entry entry) {
289:
290: if (file == null)
291: return false; // null file is set by clean copy
292:
293: boolean ret = false;
294:
295: if (entry.hadConflicts()) {
296: // TODO introduce common timestamp comparation logic
297: // We only test accuracy of upto a second (1000ms) because that is
298: // the precision of the timestamp in the entries file
299: long mergedTime = entry.getLastModified().getTime() / 1000;
300: long timeStampOfFile = file.lastModified() / 1000;
301: ret = timeStampOfFile <= mergedTime;
302: }
303:
304: return ret;
305: }
306:
307: /**
308: * Adds the appropriate requests for a given directory. Sends a
309: * directory request followed by as many Entry and Modified requests
310: * as required
311: * @param directory the directory to send requests for
312: * @throws IOException if an error occurs constructing the requests
313: */
314: protected void addRequestsForDirectory(File directory)
315: throws IOException, CommandAbortedException {
316: if (!clientServices.exists(directory)) {
317: return;
318: }
319: if (clientServices.isAborted()) {
320: throw new CommandAbortedException(
321: "Command aborted during request generation",
322: "Command aborted during request generation");
323: }
324:
325: addDirectoryRequest(directory);
326:
327: File[] dirFiles = directory.listFiles();
328: List localFiles;
329: if (dirFiles == null) {
330: localFiles = new ArrayList(0);
331: } else {
332: localFiles = new ArrayList(Arrays.asList(dirFiles));
333: localFiles.remove(new File(directory, "CVS"));
334: }
335:
336: List subDirectories = null;
337: if (isRecursive()) {
338: subDirectories = new LinkedList();
339: }
340:
341: // get all the entries we know about, and process them
342: for (Iterator it = clientServices.getEntries(directory); it
343: .hasNext();) {
344: final Entry entry = (Entry) it.next();
345: final File file = new File(directory, entry.getName());
346: if (entry.isDirectory()) {
347: if (isRecursive()) {
348: subDirectories.add(new File(directory, entry
349: .getName()));
350: }
351: } else {
352: addRequestForFile(file, entry);
353: }
354: localFiles.remove(file);
355: }
356:
357: // In case that CVS folder does not exist, we need to process all
358: // directories that have CVS subfolders:
359: if (isRecursive() && !new File(directory, "CVS").exists()) {
360: File[] subFiles = directory.listFiles();
361: if (subFiles != null) {
362: for (int i = 0; i < subFiles.length; i++) {
363: if (subFiles[i].isDirectory()
364: && new File(subFiles[i], "CVS").exists()) {
365: subDirectories.add(subFiles[i]);
366: }
367: }
368: }
369: }
370:
371: for (Iterator it = localFiles.iterator(); it.hasNext();) {
372: String localFileName = ((File) it.next()).getName();
373: if (!clientServices.shouldBeIgnored(directory,
374: localFileName)) {
375: addRequest(new QuestionableRequest(localFileName));
376: }
377: }
378:
379: if (isRecursive()) {
380: for (Iterator it = subDirectories.iterator(); it.hasNext();) {
381: File subdirectory = (File) it.next();
382: File cvsSubDir = new File(subdirectory, "CVS"); //NOI18N
383: if (clientServices.exists(cvsSubDir)) {
384: addRequestsForDirectory(subdirectory);
385: }
386: }
387: }
388: }
389:
390: /**
391: * This method is called for each explicit file and for files within a
392: * directory.
393: */
394: protected void addRequestForFile(File file, Entry entry) {
395: sendEntryAndModifiedRequests(entry, file);
396: }
397:
398: /**
399: * Add the appropriate requests for a single file. A directory request
400: * is sent, followed by an Entry and Modified request
401: * @param file the file to send requests for
402: * @throws IOException if an error occurs constructing the requests
403: */
404: protected void addRequestsForFile(File file) throws IOException {
405: addDirectoryRequest(file.getParentFile());
406:
407: try {
408: final Entry entry = clientServices.getEntry(file);
409: // a non-null entry means the file does exist in the
410: // Entries file for this directory
411: if (entry != null) {
412: addRequestForFile(file, entry);
413: } else if (file.exists()) {
414: // #50963 file exists locally without an entry AND the request is
415: // for the file explicitly
416: boolean unusedBinaryFlag = false;
417: addRequest(new ModifiedRequest(file, unusedBinaryFlag));
418: }
419: } catch (IOException ex) {
420: System.err.println("An error occurred getting the Entry "
421: + "for file " + file + ": " + ex);
422: ex.printStackTrace();
423: }
424: }
425:
426: /**
427: * Adds a DirectoryRequest (and maybe a StickyRequest) to the request list.
428: */
429: protected final void addDirectoryRequest(File directory) {
430: // remove localPath prefix from directory. If left with
431: // nothing, use dot (".") in the directory request
432: String dir = getRelativeToLocalPathInUnixStyle(directory);
433:
434: try {
435: String repository = clientServices
436: .getRepositoryForDirectory(directory
437: .getAbsolutePath());
438: addRequest(new DirectoryRequest(dir, repository));
439: String tag = clientServices
440: .getStickyTagForDirectory(directory);
441: if (tag != null) {
442: addRequest(new StickyRequest(tag));
443: }
444: } catch (FileNotFoundException ex) {
445: // we can ignore this exception safely because it just means
446: // that the user has deleted a directory referenced in a
447: // CVS/Entries file
448: } catch (IOException ex) {
449: System.err
450: .println("An error occurred reading the respository "
451: + "for the directory " + dir + ": " + ex);
452: ex.printStackTrace();
453: }
454: }
455:
456: /**
457: * Add the argument requests. The argument requests are created using
458: * the original set of files/directories passed in. Subclasses of this
459: * class should call this method at the appropriate point in their
460: * execute() method. Note that arguments are appended to the list.
461: */
462: protected void addArgumentRequests() {
463: if (files == null) {
464: return;
465: }
466:
467: for (int i = 0; i < files.length; i++) {
468: final File file = files[i];
469: String relativePath = getRelativeToLocalPathInUnixStyle(file);
470: addRequest(new ArgumentRequest(relativePath, true));
471: }
472: }
473:
474: /**
475: * Execute a command. This implementation sends a Root request, followed
476: * by as many Directory and Entry requests as is required by the recurse
477: * setting and the file arguments that have been set. Subclasses should
478: * call this first, and tag on the end of the requests list any further
479: * requests and, finally, the actually request that does the command (e.g.
480: * <pre>update</pre>, <pre>status</pre> etc.)
481: * @param client the client services object that provides any necessary
482: * services to this command, including the ability to actually process
483: * all the requests
484: * @throws CommandException if an error occurs executing the command
485: */
486: public void execute(ClientServices client, EventManager em)
487: throws CommandException, AuthenticationException {
488: requests.clear();
489: clientServices = client;
490: super .execute(client, em);
491:
492: if (client.isFirstCommand()) {
493: addRequest(new RootRequest(client.getRepository()));
494: }
495:
496: addFileRequests();
497: }
498:
499: private void addFileRequests() throws CommandException {
500: try {
501: if (files != null && files.length > 0) {
502: for (int i = 0; i < files.length; i++) {
503: addRequests(files[i]);
504: }
505: } else {
506: // if no arguments have been specified, then specify the
507: // local directory - the "top level" for this command
508: if (assumeLocalPathWhenUnspecified()) {
509: addRequests(new File(getLocalDirectory()));
510: }
511: }
512: } catch (Exception ex) {
513: throw new CommandException(ex, ex.getLocalizedMessage());
514: }
515: }
516:
517: /**
518: * The result from this command is used only when the getFiles() returns null or empty array.
519: * in such a case and when this method returns true, it is assumed the localpath should be taken
520: * as the 'default' file for the building of requests.
521: * Generally assumed to be true. Can be overriden by subclasses. However make sure you know what you are doing. :)
522: */
523: protected boolean assumeLocalPathWhenUnspecified() {
524: return true;
525: }
526:
527: /**
528: * Adds the specified request to the request list.
529: */
530: protected final void addRequest(Request request) {
531: requests.add(request);
532: }
533:
534: /**
535: * Adds the request for the current working directory.
536: */
537: protected final void addRequestForWorkingDirectory(
538: ClientServices clientServices) throws IOException {
539: addRequest(new DirectoryRequest(".", //NOI18N
540: clientServices
541: .getRepositoryForDirectory(getLocalDirectory())));
542: }
543:
544: /**
545: * If the specified value is true, add a ArgumentRequest for the specified
546: * argument.
547: */
548: protected final void addArgumentRequest(boolean value,
549: String argument) {
550: if (!value) {
551: return;
552: }
553:
554: addRequest(new ArgumentRequest(argument));
555: }
556:
557: /**
558: * Appends the file's names to the specified buffer.
559: */
560: protected final void appendFileArguments(StringBuffer buffer) {
561: File[] files = getFiles();
562: if (files == null) {
563: return;
564: }
565:
566: for (int index = 0; index < files.length; index++) {
567: if (index > 0) {
568: buffer.append(' ');
569: }
570: buffer.append(files[index].getName());
571: }
572: }
573: }
|