/*
* (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.checkout;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.netbeans.lib.cvsclient.ClientServices;
import org.netbeans.lib.cvsclient.command.BasicCommand;
import org.netbeans.lib.cvsclient.command.Builder;
import org.netbeans.lib.cvsclient.command.CommandException;
import org.netbeans.lib.cvsclient.command.KeywordSubstitutionOptions;
import org.netbeans.lib.cvsclient.command.PipedFilesBuilder;
import org.netbeans.lib.cvsclient.command.TemporaryFileCreator;
import org.netbeans.lib.cvsclient.command.update.UpdateBuilder;
import org.netbeans.lib.cvsclient.connection.AuthenticationException;
import org.netbeans.lib.cvsclient.event.EventManager;
import org.netbeans.lib.cvsclient.event.MessageEvent;
import org.netbeans.lib.cvsclient.event.ModuleExpansionEvent;
import org.netbeans.lib.cvsclient.request.ArgumentRequest;
import org.netbeans.lib.cvsclient.request.CommandRequest;
import org.netbeans.lib.cvsclient.request.DirectoryRequest;
import org.netbeans.lib.cvsclient.request.ExpandModulesRequest;
import org.netbeans.lib.cvsclient.request.RootRequest;
/**
* The checkout command. This handles the sending of the requests and the processing of the responses from the server.
*
* @author Robert Greig
*/
public class CheckoutCommand extends BasicCommand implements TemporaryFileCreator {
private static final String UPDATING = ": Updating "; // NOI18N
/**
* A store of potentially empty directories. When a directory has a file in it, it is removed from this set. This set allows the prune
* option to be implemented.
*/
private final Set emptyDirectories = new HashSet();
/**
* The modules to checkout. These names are unexpanded and will be passed to a module-expansion request.
*/
private final List modules = new LinkedList();
/**
* The expanded modules.
*/
private final List expandedModules = new LinkedList();
/**
* Will force the checkout command to display only a list of modules.
*/
private boolean showModules;
/**
* if set, will display just a list of modules with statuses.
*/
private boolean showModulesWithStatus;
/**
* if set, will redirect the output of the command to standard output.
*/
private boolean pipeToOutput;
/**
* Whether to prune directories, i.e. remove any directories that do not contain any files. This is the -P option in command-line CVS.
*/
private boolean pruneDirectories;
/**
* Resets any sticky tags/dates/options imposed on the updated file(s).
*/
private boolean resetStickyOnes;
/**
* Use head revision if a revision meeting criteria set by switches -r/-D (tag/date) is not found.
*/
private boolean useHeadIfNotFound;
/**
* Don't shorten module paths if -d specified.
*/
private boolean notShortenPaths;
/**
* Whether <code>notShortenPaths</code> was explicitly set.
*/
private boolean isNotShortenSet;
/**
* Forces a checkout of a revision that was current at specified date.
*/
private String checkoutByDate;
/**
* Forces a checkout of specified revision. Can be a number/tag/branch
*/
private String checkoutByRevision;
/**
* performs checkout to specified directory other then the module.
*/
private String checkoutDirectory;
/**
* Use this keyword substitution for the command. does not include the -k switch part.
*/
private KeywordSubstitutionOptions keywordSubst;
/**
* Do not run module program (if any).
*/
private boolean notRunModuleProgram;
/** Active during execute. */
private ClientServices client;
/**
* Construct a new checkout command.
*
* @param recursive
* whether to do a recursive checkout
* @param modules
* an array of modules names to checkout
*/
public CheckoutCommand(boolean recursive, String[] modules) {
resetCVSCommand();
setRecursive(recursive);
setModules(modules);
}
/**
* Construct a new checkout command.
*
* @param recursive
* whether to do a recursive checkout
* @param module
* the module to checkout
*/
public CheckoutCommand(boolean recursive, String module) {
resetCVSCommand();
setRecursive(recursive);
setModule(module);
}
/**
* Construct a checkout command, with default values for options.
*/
public CheckoutCommand() {
resetCVSCommand();
setRecursive(true);
}
/**
* Set the modules to checkout.
*
* @param theModules
* the names (it's like relative path) of the modules to checkout
*/
public void setModule(String module) {
modules.add(module);
}
/**
* clears the list of modules for checkout.
*/
public void clearModules() {
this.modules.clear();
}
/**
* Set the modules to checkout.
*
* @param theModules
* the names of the modules to checkout
*/
public void setModules(String[] modules) {
clearModules();
for (int i = 0; i < modules.length; i++) {
String module = modules[i];
this.modules.add(module);
}
}
public String[] getModules() {
String[] mods = new String[modules.size()];
mods = (String[]) modules.toArray(mods);
return mods;
}
/**
* Handle modules that are already checked out. We check whether a module has been checked out and if so we add it to the list of
* directories that the superclass must send Modified requests for etc.
*/
private void processExistingModules(String localPath) {
if (expandedModules.size() == 0) {
return;
}
List list = new ArrayList(expandedModules.size());
for (Iterator it = expandedModules.iterator(); it.hasNext();) {
String moduleName = (String) it.next();
if (moduleName.equals(".")) { // NOI18N
list.add(new File(localPath));
break;
}
File moduleDir = null;
final File moduleFile = new File(localPath, moduleName);
if (moduleFile.isFile()) {
moduleDir = moduleFile.getParentFile();
} else {
moduleDir = moduleFile;
}
final File moduleCVSDir = new File(moduleDir, "CVS/Repository"); // NOI18N
if (moduleCVSDir.exists()) {
list.add(moduleFile);
}
}
File[] directories = new File[list.size()];
directories = (File[]) list.toArray(directories);
setFiles(directories);
}
/**
* Execute this command.
*
* @param client
* the client services object that provides any necessary services to this command, including the ability to actually process
* all the requests
*/
@Override
public void execute(ClientServices client, EventManager em) throws CommandException, AuthenticationException {
client.ensureConnection();
this.client = client;
try {
requests = new LinkedList();
if (client.isFirstCommand()) {
requests.add(new RootRequest(client.getRepository()));
}
if (showModules || showModulesWithStatus) {
// we need to initialize the builder first (is done in BuildableCommand.execute()
// but we can't run it because of teh BasicCommand's execute and
// it's feature that adds the files request to the request list.
if (builder == null && !isBuilderSet()) {
builder = createBuilder(em);
}
// special handling for -c -s switches.
if (showModules) {
requests.add(new ArgumentRequest("-c")); // NOI18N
}
if (showModulesWithStatus) {
requests.add(new ArgumentRequest("-s")); // NOI18N
}
requests.add(CommandRequest.CHECKOUT);
try {
client.processRequests(requests);
requests.clear();
} catch (CommandException ex) {
throw ex;
} catch (EOFException ex) {
throw new CommandException(ex, CommandException.getLocalMessage("CommandException.EndOfFile", null)); // NOI18N
} catch (Exception ex) {
throw new CommandException(ex, ex.getLocalizedMessage());
}
return;
}
for (Iterator it = modules.iterator(); it.hasNext();) {
String module = (String) it.next();
requests.add(new ArgumentRequest(module));
}
expandedModules.clear();
requests.add(new DirectoryRequest(".", client.getRepository())); // NOI18N
requests.add(new RootRequest(client.getRepository()));
requests.add(new ExpandModulesRequest());
try {
client.processRequests(requests);
} catch (CommandException ex) {
throw ex;
} catch (Exception ex) {
throw new CommandException(ex, ex.getLocalizedMessage());
}
requests.clear();
postExpansionExecute(client, em);
} finally {
this.client = null;
}
}
/**
* 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. in checkout we operate
* with modules rather then files. This produces problems in the following situation. If you have something already checked out and want
* to checkout another module that is not checked out yet, then there's nothing to be translated from modules to files. and in such a
* case the localpathis assumed, which includes non-relevant already checked out directories..
*/
@Override
protected boolean assumeLocalPathWhenUnspecified() {
return false;
}
/**
* This is called when the server has responded to an expand-modules request.
*/
@Override
public void moduleExpanded(ModuleExpansionEvent e) {
expandedModules.add(e.getModule());
}
/**
* Execute this command
*
* @param client
* the client services object that provides any necessary services to this command, including the ability to actually process
* all the requests
*/
private void postExpansionExecute(ClientServices client, EventManager em) throws CommandException, AuthenticationException {
// we first test whether the modules specified actually exist
// checked out already. If so, we must work something like an update
// command and send modified files to the server.
processExistingModules(client.getLocalPath());
// the sending of the Modified requests and so on is handled in the
// superclass.
super.execute(client, em);
//
// moved modules code to the end of the other arguments --GAR
//
int index = requests.size();
final int FIRST_INDEX = 0;
final int SECOND_INDEX = 1;
if (!isRecursive()) {
requests.add(FIRST_INDEX, new ArgumentRequest("-l")); // NOI18N
}
if (pipeToOutput) {
requests.add(FIRST_INDEX, new ArgumentRequest("-p")); // NOI18N
}
if (resetStickyOnes) {
requests.add(FIRST_INDEX, new ArgumentRequest("-A")); // NOI18N
}
if (useHeadIfNotFound) {
requests.add(FIRST_INDEX, new ArgumentRequest("-f")); // NOI18N
}
if (isNotShortenPaths()) {
requests.add(FIRST_INDEX, new ArgumentRequest("-N")); // NOI18N
}
if (notRunModuleProgram) {
requests.add(FIRST_INDEX, new ArgumentRequest("-n")); // NOI18N
}
if (checkoutByDate != null && checkoutByDate.length() > 0) {
requests.add(FIRST_INDEX, new ArgumentRequest("-D")); // NOI18N
requests.add(SECOND_INDEX, new ArgumentRequest(getCheckoutByDate()));
}
if (checkoutByRevision != null && checkoutByRevision.length() > 0) {
requests.add(FIRST_INDEX, new ArgumentRequest("-r")); // NOI18N
requests.add(SECOND_INDEX, new ArgumentRequest(getCheckoutByRevision()));
}
if (checkoutDirectory != null && !checkoutDirectory.equals("")) {
requests.add(FIRST_INDEX, new ArgumentRequest("-d")); // NOI18N
requests.add(SECOND_INDEX, new ArgumentRequest(getCheckoutDirectory()));
}
if (getKeywordSubst() != null) {
requests.add(FIRST_INDEX, new ArgumentRequest("-k" + getKeywordSubst())); // NOI18N
}
index = requests.size() - index; // The end of our arguments
// Add a -- before the first file name just in case it looks like an option.
requests.add(index++, new ArgumentRequest("--")); // NOI18N
// Note that modules might be null and still be valid because
// for -c, -s switches no module has to be selected
// You might also think that we should pass in expandedModules here
// but according to the spec that would be wrong because of the -d
// flag.
for (Iterator it = modules.iterator(); it.hasNext();) {
String module = (String) it.next();
requests.add(index++, new ArgumentRequest(module));
}
requests.add(new DirectoryRequest(".", client.getRepository())); // NOI18N
requests.add(CommandRequest.CHECKOUT);
try {
client.processRequests(requests);
if (pruneDirectories) {
pruneEmptyDirectories();
}
requests.clear();
} catch (CommandException ex) {
throw ex;
} catch (Exception ex) {
throw new CommandException(ex, ex.getLocalizedMessage());
}
}
/**
* Getter for property showModules.
*
* @return Value of property showModules.
*/
public boolean isShowModules() {
return showModules;
}
/**
* Setter for property showModules.
*
* @param showModules
* New value of property showModules.
*/
public void setShowModules(boolean showModules) {
this.showModules = showModules;
}
/**
* Getter for property showModulesWithStatus.
*
* @return Value of property showModulesWithStatus.
*/
public boolean isShowModulesWithStatus() {
return showModulesWithStatus;
}
/**
* Setter for property showModulesWithStatus.
*
* @param showModulesWithStatus
* New value of property showModulesWithStatus.
*/
public void setShowModulesWithStatus(boolean showModulesWithStatus) {
this.showModulesWithStatus = showModulesWithStatus;
}
/**
* Set whether to prune directories. This is the -P option in the command-line CVS.
*/
public void setPruneDirectories(boolean pruneDirectories) {
this.pruneDirectories = pruneDirectories;
}
/**
* Get whether to prune directories.
*
* @return true if directories should be removed if they contain no files, false otherwise.
*/
public boolean getPruneDirectories() {
return pruneDirectories;
}
/**
* Getter for property pipeToOutput.
*
* @return Value of property pipeToOutput.
*/
public boolean isPipeToOutput() {
return pipeToOutput;
}
/**
* Setter for property pipeToOutput.
*
* @param pipeToOutput
* New value of property pipeToOutput.
*/
public void setPipeToOutput(boolean pipeToOutput) {
this.pipeToOutput = pipeToOutput;
}
/**
* Getter for property resetStickyOnes.
*
* @return Value of property resetStickyOnes.
*/
public boolean isResetStickyOnes() {
return resetStickyOnes;
}
/**
* Setter for property resetStickyOnes.
*
* @param resetStickyOnes
* New value of property resetStickyOnes.
*/
public void setResetStickyOnes(boolean resetStickyOnes) {
this.resetStickyOnes = resetStickyOnes;
}
/**
* Getter for property useHeadIfNotFound.
*
* @return Value of property useHeadIfNotFound.
*/
public boolean isUseHeadIfNotFound() {
return useHeadIfNotFound;
}
/**
* Setter for property useHeadIfNotFound.
*
* @param useHeadIfNotFound
* New value of property useHeadIfNotFound.
*/
public void setUseHeadIfNotFound(boolean useHeadIfNotFound) {
this.useHeadIfNotFound = useHeadIfNotFound;
}
/**
* Getter for property notShortenPaths.
*
* @return Value of property notShortenPaths.
*/
public boolean isNotShortenPaths() {
// Use the same logic as cvs from cvshome.org.
return notShortenPaths || !isNotShortenSet && checkoutDirectory == null;
}
/**
* Setter for property notShortenPaths.
*
* @param notShortenPaths
* New value of property notShortenPaths.
*/
public void setNotShortenPaths(boolean notShortenPaths) {
this.notShortenPaths = notShortenPaths;
isNotShortenSet = true;
}
/**
* Getter for property notRunModuleProgram.
*
* @return Value of property notRunModuleProgram.
*/
public boolean isNotRunModuleProgram() {
return notRunModuleProgram;
}
/**
* Setter for property notRunModuleProgram.
*
* @param notRunModuleProgram
* New value of property notRunModuleProgram.
*/
public void setNotRunModuleProgram(boolean notRunModuleProgram) {
this.notRunModuleProgram = notRunModuleProgram;
}
/**
* Getter for property checkoutByDate.
*
* @return Value of property checkoutByDate.
*/
public String getCheckoutByDate() {
return checkoutByDate;
}
/**
* Setter for property checkoutByDate.
*
* @param checkoutByDate
* New value of property checkoutByDate.
*/
public void setCheckoutByDate(String checkoutByDate) {
this.checkoutByDate = checkoutByDate;
}
/**
* Getter for property checkoutByRevision.
*
* @return Value of property checkoutByRevision.
*/
public String getCheckoutByRevision() {
return checkoutByRevision;
}
/**
* Setter for property checkoutByRevision.
*
* @param checkoutByRevision
* New value of property checkoutByRevision.
*/
public void setCheckoutByRevision(String checkoutByRevision) {
this.checkoutByRevision = checkoutByRevision;
}
/**
* Getter for property checkoutDirectory.
*
* @return Value of property checkoutDirectory.
*/
public String getCheckoutDirectory() {
return this.checkoutDirectory;
}
/**
* Setter for property checkoutDirectory.
*
* @param checkoutDirectory
* New value of property checkoutDirectory.
*/
public void setCheckoutDirectory(String checkoutDirectory) {
this.checkoutDirectory = checkoutDirectory;
}
/**
* Getter for property keywordSubst.
*
* @return Value of property keywordSubst.
*/
public KeywordSubstitutionOptions getKeywordSubst() {
return keywordSubst;
}
/**
* Setter for property keywordSubst.
*
* @param keywordSubst
* New value of property keywordSubst.
*/
public void setKeywordSubst(KeywordSubstitutionOptions keywordSubst) {
this.keywordSubst = keywordSubst;
}
@Override
public Builder createBuilder(EventManager eventMan) {
if (isShowModules() || isShowModulesWithStatus()) {
return new ModuleListBuilder(eventMan, this);
}
if (isPipeToOutput()) {
return new PipedFilesBuilder(eventMan, this, this);
}
return new UpdateBuilder(eventMan, getLocalDirectory());
}
@Override
public File createTempFile(String filename) throws IOException {
File temp = File.createTempFile("cvs", ".dff", getGlobalOptions().getTempDir()); // NOI18N
return temp;
}
/**
* This method returns how the command would looklike when typed on the command line. Each command is responsible for constructing this
* information.
*
* @returns <command's name> [<parameters>] files/dirs. Example: checkout -p CvsCommand.java
*/
@Override
public String getCVSCommand() {
StringBuffer toReturn = new StringBuffer("checkout "); // NOI18N
toReturn.append(getCVSArguments());
if (!isShowModules() && !isShowModulesWithStatus()) {
for (Iterator it = modules.iterator(); it.hasNext();) {
String module = (String) it.next();
toReturn.append(module);
toReturn.append(' ');
}
}
return toReturn.toString();
}
/**
* Takes the arguments and sets the command. To be mainly used for automatic settings (like parsing the .cvsrc file).
*
* @return true if the option (switch) was recognized and set
*/
@Override
public boolean setCVSCommand(char opt, String optArg) {
if (opt == 'c') {
setShowModules(true);
} else if (opt == 's') {
setShowModulesWithStatus(true);
} else if (opt == 'p') {
setPipeToOutput(true);
} else if (opt == 'R') {
setRecursive(true);
} else if (opt == 'l') {
setRecursive(false);
} else if (opt == 'A') {
setResetStickyOnes(true);
} else if (opt == 'f') {
setUseHeadIfNotFound(true);
} else if (opt == 'P') {
setPruneDirectories(true);
} else if (opt == 'D') {
setCheckoutByDate(optArg.trim());
} else if (opt == 'r') {
setCheckoutByRevision(optArg.trim());
} else if (opt == 'd') {
setCheckoutDirectory(optArg);
} else if (opt == 'N') {
setNotShortenPaths(true);
} else if (opt == 'n') {
setNotRunModuleProgram(true);
} else if (opt == 'k') {
KeywordSubstitutionOptions keywordSubst = KeywordSubstitutionOptions.findKeywordSubstOption(optArg);
setKeywordSubst(keywordSubst);
} else {
return false;
}
return true;
}
/**
* String returned by this method defines which options are available for this particular command
*/
@Override
public String getOptString() {
return "cnpslNPRAD:r:fk:d:"; // NOI18N
}
/**
* Resets all switches in the command. After calling this method, the command should have no switches defined and should behave
* defaultly.
*/
@Override
public void resetCVSCommand() {
setShowModules(false);
setShowModulesWithStatus(false);
setPipeToOutput(false);
setRecursive(true);
setResetStickyOnes(false);
setUseHeadIfNotFound(false);
setCheckoutByDate(null);
setCheckoutByRevision(null);
setKeywordSubst(null);
setPruneDirectories(false);
setNotShortenPaths(false);
isNotShortenSet = false;
setNotRunModuleProgram(false);
setCheckoutDirectory(null);
}
/**
* Returns the arguments of the command in the command-line style. Similar to getCVSCommand() however without the files and command's
* name
*/
@Override
public String getCVSArguments() {
StringBuffer toReturn = new StringBuffer(""); // NOI18N
if (isShowModules()) {
toReturn.append("-c "); // NOI18N
}
if (isShowModulesWithStatus()) {
toReturn.append("-s "); // NOI18N
}
if (isPipeToOutput()) {
toReturn.append("-p "); // NOI18N
}
if (!isRecursive()) {
toReturn.append("-l "); // NOI18N
}
if (isResetStickyOnes()) {
toReturn.append("-A "); // NOI18N
}
if (isUseHeadIfNotFound()) {
toReturn.append("-f "); // NOI18N
}
if (getPruneDirectories()) {
toReturn.append("-P "); // NOI18N
}
if (isNotShortenPaths()) {
toReturn.append("-N "); // NOI18N
}
if (isNotRunModuleProgram()) {
toReturn.append("-n "); // NOI18N
}
if (getKeywordSubst() != null) {
toReturn.append("-k"); // NOI18N
toReturn.append(getKeywordSubst());
toReturn.append(' ');
}
if (getCheckoutByRevision() != null && getCheckoutByRevision().length() > 0) {
toReturn.append("-r "); // NOI18N
toReturn.append(getCheckoutByRevision());
toReturn.append(' ');
}
if (getCheckoutByDate() != null && getCheckoutByDate().length() > 0) {
toReturn.append("-D "); // NOI18N
toReturn.append(getCheckoutByDate());
toReturn.append(' ');
}
if (getCheckoutDirectory() != null) {
toReturn.append("-d "); // NOI18N
toReturn.append(getCheckoutDirectory());
toReturn.append(" "); // NOI18N
}
return toReturn.toString();
}
/**
* Called when the server wants to send a message to be displayed to the user. The message is only for information purposes and clients
* can choose to ignore these messages if they wish.
*
* @param e
* the event
*/
@Override
public void messageSent(MessageEvent e) {
super.messageSent(e);
// we use this event to determine which directories need to be checked
// for updating
if (pruneDirectories && e.getMessage().indexOf(UPDATING) > 0) {
File file = new File(getLocalDirectory(), e.getMessage().substring(e.getMessage().indexOf(UPDATING) + UPDATING.length()));
emptyDirectories.add(file);
}
}
/**
* Prunes a directory, recursively pruning its subdirectories
*
* @param directory
* the directory to prune
*/
private boolean pruneEmptyDirectory(File directory) throws IOException {
boolean empty = true;
final File[] contents = directory.listFiles();
// should never be null, but just in case...
if (contents != null) {
for (int i = 0; i < contents.length; i++) {
if (contents[i].isFile()) {
empty = false;
} else {
if (!contents[i].getName().equals("CVS")) { // NOI18N
empty = pruneEmptyDirectory(contents[i]);
}
}
if (!empty) {
break;
}
}
if (empty) {
// check this is a CVS directory and not some directory the user
// has stupidly called CVS...
final File entriesFile = new File(directory, "CVS/Entries"); // NOI18N
if (entriesFile.exists()) {
final File adminDir = new File(directory, "CVS"); // NOI18N
final File[] adminFiles = adminDir.listFiles();
for (int i = 0; i < adminFiles.length; i++) {
adminFiles[i].delete();
}
adminDir.delete();
directory.delete();
client.removeEntry(directory);
}
}
}
return empty;
}
/**
* Remove any directories that don't contain any files
*/
private void pruneEmptyDirectories() throws IOException {
final Iterator it = emptyDirectories.iterator();
while (it.hasNext()) {
final File dir = (File) it.next();
// we might have deleted it already (due to recursive delete)
// so we need to check existence
if (dir.exists()) {
pruneEmptyDirectory(dir);
}
}
emptyDirectories.clear();
}
}