/*
* (c) Copyright 2010-2011 AgileBirds
*
* This file is part of OpenFlexo.
*
* OpenFlexo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenFlexo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenFlexo. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.netbeans.lib.cvsclient.command;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.netbeans.lib.cvsclient.ClientServices;
import org.netbeans.lib.cvsclient.admin.DateComparator;
import org.netbeans.lib.cvsclient.admin.Entry;
import org.netbeans.lib.cvsclient.connection.AuthenticationException;
import org.netbeans.lib.cvsclient.event.EventManager;
import org.netbeans.lib.cvsclient.request.ArgumentRequest;
import org.netbeans.lib.cvsclient.request.DirectoryRequest;
import org.netbeans.lib.cvsclient.request.EntryRequest;
import org.netbeans.lib.cvsclient.request.ModifiedRequest;
import org.netbeans.lib.cvsclient.request.QuestionableRequest;
import org.netbeans.lib.cvsclient.request.Request;
import org.netbeans.lib.cvsclient.request.RootRequest;
import org.netbeans.lib.cvsclient.request.StickyRequest;
import org.netbeans.lib.cvsclient.request.UnchangedRequest;
/**
* A class that provides common functionality for many of the CVS command that send similar sequences of requests.
*
* @author Robert Greig
*/
public abstract class BasicCommand extends BuildableCommand {
/**
* The requests that are sent and processed.
*/
protected List requests = new LinkedList();
/**
* The client services that are provided to this command.
*/
protected ClientServices clientServices;
/**
* Whether to update recursively.
*/
private boolean recursive = true;
/**
* The files and/or directories to operate on.
*/
protected File[] files;
/**
* Gets the value of the recursive option.
*
* @return true if recursive, false if not
* @deprecated use isRecursive instead
*/
@Deprecated
public boolean getRecursive() {
return recursive;
}
/**
* Gets the value of the recursive option.
*
* @return true if recursive, false if not
*/
public boolean isRecursive() {
return recursive;
}
/**
* Sets the value of the recursive option.
*
* @param recursive
* true if the command should recurse, false otherwise
*/
public void setRecursive(boolean recursive) {
this.recursive = recursive;
}
/**
* Set the files and/or directories on which to execute the command. The way these are processed is:
* <P>
* <UL>
* <LI>Default action (i.e. not setting the files explicitly or setting them to
*
* <pre>
* null
* </pre>
*
* ) is to use the directory in which the command was executed (see how directories are treated, below)</LI>
* <LI>Files are handled how you would expect</LI>
* <LI>For directories, all files within the directory are sent</LI>
* </UL>
*
* @param theFiles
* the files to operate on. May be null to indicate that the local directory specified in the client should be used. Full,
* absolute canonical pathnames <b>must</b> be supplied.
*/
public void setFiles(File[] theFiles) {
// sort array.. files first, directories follow
if (theFiles == null) {
files = theFiles;
return;
}
// assert theFiles.length > 0 : "Empty array causes random AIOOBEs!"; // ClientRuntime.java:119
files = new File[theFiles.length];
int fileCount = 0;
int dirCount = 0;
int totalCount = theFiles.length;
for (int index = 0; index < totalCount; index++) {
File currentFile = theFiles[index];
if (currentFile.isDirectory()) {
files[totalCount - (1 + dirCount)] = currentFile;
dirCount = dirCount + 1;
} else {
files[fileCount] = currentFile;
fileCount = fileCount + 1;
}
}
}
/**
* Get the files and/or directories specified for this command to operate on.
*
* @return the array of Files
*/
public File[] getFiles() {
return files;
}
/**
* Get a single file from the "files" list. returns only files, not directories. This method is used from within the builders, because
* for single file requests, the cvs server doesn't return us enough information to identify what file has been returned. Thus we sort
* the "files" array (files come before directories. Then the response froms erver comes in the same order and the files can be found
* this way.
*
* @param index
* the index of the file in the list.
*/
public File getXthFile(int index) {
if (index < 0 || index >= files.length) {
return null;
}
File file = files[index];
if (!file.isFile()) {
return null;
}
return file;
}
/**
* @param ending
* - the ending part of the file's pathname.. path separator is cvs's default '/'
*/
public File getFileEndingWith(String ending) {
String locEnding = ending.replace('\\', '/');
String localDir = getLocalDirectory().replace('\\', '/');
int index = 0;
for (index = 0; index < files.length; index++) {
String path = files[index].getAbsolutePath();
String parentPath = files[index].getParentFile().getAbsolutePath().replace('\\', '/');
path = path.replace('\\', '/');
if (path.endsWith(locEnding) && locEnding.indexOf('/') >= 0 || files[index].getName().equals(locEnding)
&& parentPath.equals(localDir)) {
return files[index];
}
}
return null;
}
/**
* Add the appropriate requests for a specified path. For a directory, process all the files within that directory and for a single
* file, just send it. For each directory, send a directory request. For each file send an Entry request followed by a Modified request.
*
* @param path
* the particular path to issue requests for. May be either a file or a directory.
*/
private void addRequests(File path) throws FileNotFoundException, IOException, CommandAbortedException {
if (path == null) {
throw new IllegalArgumentException("Cannot add requests for a " + "null path.");
}
if (!path.exists() || path.isFile()) {
addRequestsForFile(path);
} else {
addRequestsForDirectory(path);
}
}
/**
* Should return true if unchanged files should not be sent to server. If false is returned, all files will be sent to server This
* method is used by <code>sendEntryAndModifiedRequests</code>.
*/
protected boolean doesCheckFileTime() {
return true;
}
/**
* Send an Entry followed by a Modified or Unchanged request based on whether the file has been untouched on the local machine.
*
* @param entry
* the entry for the file
* @param file
* the file in question
*/
protected void sendEntryAndModifiedRequests(Entry entry, File file) {
if (entry == null) {
return;
}
// for deleted added files, don't send anything..
if (file != null && !file.exists() && entry.isNewUserFile()) {
return;
}
Date entryLastModified = entry.getLastModified();
boolean hadConflicts = entry.hadConflicts();
if (!hadConflicts) {
// we null out the conflict field if there is no conflict
// because we use that field to store the timestamp of the
// file (just like command-line CVS). There is no point
// in sending this information to the CVS server, even
// though it should be ignored by the server.
entry.setConflict(null);
} else if (fileRequiresConflictResolution(file, entry)) {
// send entry in wire conflict format
Entry clone = new Entry(entry.toString());
clone.setConflict(Entry.HAD_CONFLICTS_AND_TIMESTAMP_MATCHES_FILE);
entry = clone;
}
addRequest(new EntryRequest(entry));
if (file == null || !file.exists() || entry.isUserFileToBeRemoved()) {
return;
}
if (doesCheckFileTime() && !hadConflicts && entryLastModified != null) {
if (DateComparator.getInstance().equals(file.lastModified(), entryLastModified.getTime())) {
addRequest(new UnchangedRequest(file.getName()));
return;
}
}
addRequest(new ModifiedRequest(file, entry.isBinary()));
}
/**
* When committing, we need to perform a check that will prevent the unmodified conflicting files from being checked in. This is the
* behavior of command line CVS client. This method checks the Entry for the file against the time stamp. The user can optimally call
* this method only if the Entry for the file indicates a conflict
*
* @param entry
* The Entry object corresponding to the file
* @param file
* The File object representing the file on the filesystem
* @return boolean Returns true if the file's timestamp is same or less than the time when the conflicting merge was done by CVS update
* as indicated by the Entry.
*/
private final boolean fileRequiresConflictResolution(File file, Entry entry) {
if (file == null) {
return false; // null file is set by clean copy
}
boolean ret = false;
if (entry.hadConflicts()) {
// TODO introduce common timestamp comparation logic
// We only test accuracy of upto a second (1000ms) because that is
// the precision of the timestamp in the entries file
long mergedTime = entry.getLastModified().getTime() / 1000;
long timeStampOfFile = file.lastModified() / 1000;
ret = timeStampOfFile <= mergedTime;
}
return ret;
}
/**
* Adds the appropriate requests for a given directory. Sends a directory request followed by as many Entry and Modified requests as
* required
*
* @param directory
* the directory to send requests for
* @throws IOException
* if an error occurs constructing the requests
*/
protected void addRequestsForDirectory(File directory) throws IOException, CommandAbortedException {
if (!clientServices.exists(directory)) {
return;
}
if (clientServices.isAborted()) {
throw new CommandAbortedException("Command aborted during request generation", "Command aborted during request generation");
}
addDirectoryRequest(directory);
File[] dirFiles = directory.listFiles();
List localFiles;
if (dirFiles == null) {
localFiles = new ArrayList(0);
} else {
localFiles = new ArrayList(Arrays.asList(dirFiles));
localFiles.remove(new File(directory, "CVS"));
}
List subDirectories = null;
if (isRecursive()) {
subDirectories = new LinkedList();
}
// get all the entries we know about, and process them
for (Iterator it = clientServices.getEntries(directory); it.hasNext();) {
final Entry entry = (Entry) it.next();
final File file = new File(directory, entry.getName());
if (entry.isDirectory()) {
if (isRecursive()) {
subDirectories.add(new File(directory, entry.getName()));
}
} else {
addRequestForFile(file, entry);
}
localFiles.remove(file);
}
// In case that CVS folder does not exist, we need to process all
// directories that have CVS subfolders:
if (isRecursive() && !new File(directory, "CVS").exists()) {
File[] subFiles = directory.listFiles();
if (subFiles != null) {
for (int i = 0; i < subFiles.length; i++) {
if (subFiles[i].isDirectory() && new File(subFiles[i], "CVS").exists()) {
subDirectories.add(subFiles[i]);
}
}
}
}
for (Iterator it = localFiles.iterator(); it.hasNext();) {
String localFileName = ((File) it.next()).getName();
if (!clientServices.shouldBeIgnored(directory, localFileName)) {
addRequest(new QuestionableRequest(localFileName));
}
}
if (isRecursive()) {
for (Iterator it = subDirectories.iterator(); it.hasNext();) {
File subdirectory = (File) it.next();
File cvsSubDir = new File(subdirectory, "CVS"); // NOI18N
if (clientServices.exists(cvsSubDir)) {
addRequestsForDirectory(subdirectory);
}
}
}
}
/**
* This method is called for each explicit file and for files within a directory.
*/
protected void addRequestForFile(File file, Entry entry) {
sendEntryAndModifiedRequests(entry, file);
}
/**
* Add the appropriate requests for a single file. A directory request is sent, followed by an Entry and Modified request
*
* @param file
* the file to send requests for
* @throws IOException
* if an error occurs constructing the requests
*/
protected void addRequestsForFile(File file) throws IOException {
addDirectoryRequest(file.getParentFile());
try {
final Entry entry = clientServices.getEntry(file);
// a non-null entry means the file does exist in the
// Entries file for this directory
if (entry != null) {
addRequestForFile(file, entry);
} else if (file.exists()) {
// #50963 file exists locally without an entry AND the request is
// for the file explicitly
boolean unusedBinaryFlag = false;
addRequest(new ModifiedRequest(file, unusedBinaryFlag));
}
} catch (IOException ex) {
System.err.println("An error occurred getting the Entry " + "for file " + file + ": " + ex);
ex.printStackTrace();
}
}
/**
* Adds a DirectoryRequest (and maybe a StickyRequest) to the request list.
*/
protected final void addDirectoryRequest(File directory) {
// remove localPath prefix from directory. If left with
// nothing, use dot (".") in the directory request
String dir = getRelativeToLocalPathInUnixStyle(directory);
try {
String repository = clientServices.getRepositoryForDirectory(directory.getAbsolutePath());
addRequest(new DirectoryRequest(dir, repository));
String tag = clientServices.getStickyTagForDirectory(directory);
if (tag != null) {
addRequest(new StickyRequest(tag));
}
} catch (FileNotFoundException ex) {
// we can ignore this exception safely because it just means
// that the user has deleted a directory referenced in a
// CVS/Entries file
} catch (IOException ex) {
System.err.println("An error occurred reading the respository " + "for the directory " + dir + ": " + ex);
ex.printStackTrace();
}
}
/**
* Add the argument requests. The argument requests are created using the original set of files/directories passed in. Subclasses of
* this class should call this method at the appropriate point in their execute() method. Note that arguments are appended to the list.
*/
protected void addArgumentRequests() {
if (files == null) {
return;
}
for (int i = 0; i < files.length; i++) {
final File file = files[i];
String relativePath = getRelativeToLocalPathInUnixStyle(file);
addRequest(new ArgumentRequest(relativePath));
}
}
/**
* Execute a command. This implementation sends a Root request, followed by as many Directory and Entry requests as is required by the
* recurse setting and the file arguments that have been set. Subclasses should call this first, and tag on the end of the requests list
* any further requests and, finally, the actually request that does the command (e.g.
*
* <pre>
* update
* </pre>
*
* ,
*
* <pre>
* status
* </pre>
*
* etc.)
*
* @param client
* the client services object that provides any necessary services to this command, including the ability to actually process
* all the requests
* @throws CommandException
* if an error occurs executing the command
*/
@Override
public void execute(ClientServices client, EventManager em) throws CommandException, AuthenticationException {
requests.clear();
clientServices = client;
super.execute(client, em);
if (client.isFirstCommand()) {
addRequest(new RootRequest(client.getRepository()));
}
addFileRequests();
}
private void addFileRequests() throws CommandException {
try {
if (files != null && files.length > 0) {
for (int i = 0; i < files.length; i++) {
addRequests(files[i]);
}
} else {
// if no arguments have been specified, then specify the
// local directory - the "top level" for this command
if (assumeLocalPathWhenUnspecified()) {
addRequests(new File(getLocalDirectory()));
}
}
} catch (Exception ex) {
throw new CommandException(ex, ex.getLocalizedMessage());
}
}
/**
* The result from this command is used only when the getFiles() returns null or empty array. in such a case and when this method
* returns true, it is assumed the localpath should be taken as the 'default' file for the building of requests. Generally assumed to be
* true. Can be overriden by subclasses. However make sure you know what you are doing. :)
*/
protected boolean assumeLocalPathWhenUnspecified() {
return true;
}
/**
* Adds the specified request to the request list.
*/
protected final void addRequest(Request request) {
requests.add(request);
}
/**
* Adds the request for the current working directory.
*/
protected final void addRequestForWorkingDirectory(ClientServices clientServices) throws IOException {
addRequest(new DirectoryRequest(".", // NOI18N
clientServices.getRepositoryForDirectory(getLocalDirectory())));
}
/**
* If the specified value is true, add a ArgumentRequest for the specified argument.
*/
protected final void addArgumentRequest(boolean value, String argument) {
if (!value) {
return;
}
addRequest(new ArgumentRequest(argument));
}
/**
* Appends the file's names to the specified buffer.
*/
protected final void appendFileArguments(StringBuffer buffer) {
File[] files = getFiles();
if (files == null) {
return;
}
for (int index = 0; index < files.length; index++) {
if (index > 0) {
buffer.append(' ');
}
buffer.append(files[index].getName());
}
}
}