/*
* (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.update;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Set;
import org.netbeans.lib.cvsclient.ClientServices;
import org.netbeans.lib.cvsclient.admin.Entry;
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.CommandUtils;
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.connection.AuthenticationException;
import org.netbeans.lib.cvsclient.event.EventManager;
import org.netbeans.lib.cvsclient.event.MessageEvent;
import org.netbeans.lib.cvsclient.file.FileUtils;
import org.netbeans.lib.cvsclient.request.ArgumentRequest;
import org.netbeans.lib.cvsclient.request.CommandRequest;
import org.netbeans.lib.cvsclient.request.EntryRequest;
import org.netbeans.lib.cvsclient.request.UnchangedRequest;
/**
* The Update command. Updates files that have previously been checked out from the repository with the checkout command. Modified files are
* not overwritten.
*
* @author Robert Greig
*/
public class UpdateCommand extends BasicCommand implements TemporaryFileCreator {
// This format possibly may be set by the user of the library later.
private static final String RENAME_FORMAT = "{0}/.#{1}.{2}"; // NOI18N
private static final Object[] FORMAT_PARAMETER = new Object[3]; // { path, filename, revision }
/**
* 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();
/**
* Whether to build directories, like checkout does (this is the -d option in command-line CVS).
*/
private boolean buildDirectories;
/**
* Determines whether to get a clean copy from the server. This overrides even locally modified files.
*/
private boolean cleanCopy;
/**
* 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;
/**
* Determines wheather the output of the command is processed on standard output. Default is false. If true, nothing is done to local
* working files.
*/
private boolean pipeToOutput;
/**
* 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;
/**
* equals the -D switch of command line cvs.
*/
private String updateByDate;
/**
* Equals the -r switch of command-line cvs.
*/
private String updateByRevision;
/**
* Use this keyword substitution for the command. does not include the -k switch part.
*/
private KeywordSubstitutionOptions keywordSubst;
/**
* First of the 2 possible -j switches that merge 2 different revisions. If only this property is set, the current working file is
* merged with the specified one.
*/
private String mergeRevision1;
/**
* Second of the 2 possible -j switches that merge 2 different revisions. Assumes the first -j switch (mergeRevision1 property) is set.
* Then the update commands merges the sources of these 2 revisons specified by the -j switches.
*/
private String mergeRevision2;
/**
* Construct a new update command.
*/
public UpdateCommand() {
// TODO: move up the hierarchy ?
resetCVSCommand();
}
/**
* Method that is called while the command is being executed. Descendants can override this method to return a Builder instance that
* will parse the server's output and create data structures.
*/
@Override
public Builder createBuilder(EventManager eventManager) {
if (isPipeToOutput()) {
return new PipedFilesBuilder(eventManager, this, this);
}
return new UpdateBuilder(eventManager, getLocalDirectory());
}
/**
* If <code>getCleanCopy()</code> returns true, the files will be treated as not existing.
*/
@Override
protected void sendEntryAndModifiedRequests(Entry entry, File file) {
if (isCleanCopy() && file != null && entry != null) {
if (!isPipeToOutput()) {
FORMAT_PARAMETER[0] = file.getParent();
FORMAT_PARAMETER[1] = file.getName();
FORMAT_PARAMETER[2] = entry.getRevision();
String filename = MessageFormat.format(RENAME_FORMAT, FORMAT_PARAMETER);
try {
FileUtils.copyFile(file, new File(filename));
} catch (IOException e) {
// backup copy will not be created
}
}
file = null;
}
super.sendEntryAndModifiedRequests(entry, file);
}
/**
* Set whether to build directories. This is the -d option in command-line CVS.
*/
public void setBuildDirectories(boolean buildDirectories) {
this.buildDirectories = buildDirectories;
}
/**
* Returns whether to build directories.
*
* @return true if directories are to be built, false otherwise
*/
public boolean isBuildDirectories() {
return buildDirectories;
}
/**
* Sets whether to get a clean copy from the server. Even locally modified files will not merged but overridden. This is the -C option
* in the command-line CVS.
*/
public void setCleanCopy(boolean cleanCopy) {
this.cleanCopy = cleanCopy;
}
/**
* Returns whether to get a clean copy from the server.
*/
public boolean isCleanCopy() {
return cleanCopy;
}
/**
* Set whether to prune directories. This is the -P option in the command- line CVS.
*/
public void setPruneDirectories(boolean pruneDirectories) {
this.pruneDirectories = pruneDirectories;
}
/**
* Returns whether to prune directories.
*
* @return true if directories should be removed if they contain no files, false otherwise.
*/
public boolean isPruneDirectories() {
return pruneDirectories;
}
/**
* Execute the 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 eventManager) throws CommandException, AuthenticationException {
client.ensureConnection();
super.execute(client, eventManager);
emptyDirectories.clear();
try {
// now add the request that indicates the working directory for the
// command
if (!isRecursive()) {
requests.add(1, new ArgumentRequest("-l")); // NOI18N
}
if (isBuildDirectories()) {
requests.add(1, new ArgumentRequest("-d")); // NOI18N
}
if (isCleanCopy() && !isPipeToOutput()) {
requests.add(1, new ArgumentRequest("-C")); // NOI18N
}
if (isPipeToOutput()) {
requests.add(1, new ArgumentRequest("-p")); // NOI18N
}
if (isResetStickyOnes()) {
requests.add(1, new ArgumentRequest("-A")); // NOI18N
}
if (isUseHeadIfNotFound()) {
requests.add(1, new ArgumentRequest("-f")); // NOI18N
}
if (getUpdateByDate() != null) {
requests.add(1, new ArgumentRequest("-D")); // NOI18N
requests.add(2, new ArgumentRequest(getUpdateByDate()));
} else if (getUpdateByRevision() != null) {
requests.add(1, new ArgumentRequest("-r")); // NOI18N
requests.add(2, new ArgumentRequest(getUpdateByRevision()));
}
if (getMergeRevision1() != null) {
requests.add(1, new ArgumentRequest("-j")); // NOI18N
requests.add(2, new ArgumentRequest(getMergeRevision1()));
if (getMergeRevision2() != null) {
requests.add(3, new ArgumentRequest("-j")); // NOI18N
requests.add(4, new ArgumentRequest(getMergeRevision2()));
}
}
if (getKeywordSubst() != null) {
requests.add(1, new ArgumentRequest("-k")); // NOI18N
requests.add(2, new ArgumentRequest(getKeywordSubst().toString()));
}
requests.add(1, new ArgumentRequest("-u")); // NOI18N
addRequestForWorkingDirectory(client);
addArgumentRequests();
addRequest(CommandRequest.UPDATE);
// hack - now check for the entry request for removed files
// only when p with -r or -D is on
if (isPipeToOutput() && (getUpdateByRevision() != null || getUpdateByDate() != null)) {
ListIterator it = requests.listIterator();
while (it.hasNext()) {
Object req = it.next();
if (req instanceof EntryRequest) {
EntryRequest eReq = (EntryRequest) req;
Entry entry = eReq.getEntry();
if (entry.getRevision().startsWith("-")) {// NOI18N
entry.setRevision(entry.getRevision().substring(1));
}
it.set(new EntryRequest(entry));
it.add(new UnchangedRequest(entry.getName()));
}
}
}
// end of hack..
client.processRequests(requests);
if (pruneDirectories && (getGlobalOptions() == null || !getGlobalOptions().isDoNoChanges())) {
pruneEmptyDirectories(client);
}
} 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());
} finally {
requests.clear();
}
}
/**
* 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 updateByDate.
*
* @return Value of property updateByDate.
*/
public String getUpdateByDate() {
return updateByDate;
}
/**
* Setter for property updateByDate.
*
* @param updateByDate
* New value of property updateByDate.
*/
public void setUpdateByDate(String updateByDate) {
this.updateByDate = getTrimmedString(updateByDate);
}
/**
* Getter for property updateByRevision.
*
* @return Value of property updateByRevision.
*/
public String getUpdateByRevision() {
return updateByRevision;
}
/**
* Setter for property updateByRevision.
*
* @param updateByRevision
* New value of property updateByRevision.
*/
public void setUpdateByRevision(String updateByRevision) {
this.updateByRevision = getTrimmedString(updateByRevision);
}
/**
* 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;
}
/**
* Method that creates a temporary file.
*/
@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("update "); // NOI18N
toReturn.append(getCVSArguments());
File[] files = getFiles();
if (files != null) {
for (int index = 0; index < files.length; index++) {
toReturn.append(files[index].getName());
toReturn.append(' ');
}
}
return toReturn.toString();
}
/**
* 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 (isPipeToOutput()) {
toReturn.append("-p "); // NOI18N
}
if (isCleanCopy()) {
toReturn.append("-C "); // NOI18N
}
if (!isRecursive()) {
toReturn.append("-l "); // NOI18N
}
if (isBuildDirectories()) {
toReturn.append("-d "); // NOI18N
}
if (isPruneDirectories()) {
toReturn.append("-P "); // NOI18N
}
if (isResetStickyOnes()) {
toReturn.append("-A "); // NOI18N
}
if (isUseHeadIfNotFound()) {
toReturn.append("-f "); // NOI18N
}
if (getKeywordSubst() != null) {
toReturn.append("-k"); // NOI18N
toReturn.append(getKeywordSubst().toString());
toReturn.append(' ');
}
if (getUpdateByRevision() != null) {
toReturn.append("-r "); // NOI18N
toReturn.append(getUpdateByRevision());
toReturn.append(' ');
}
if (getUpdateByDate() != null) {
toReturn.append("-D "); // NOI18N
toReturn.append(getUpdateByDate());
toReturn.append(' ');
}
if (getMergeRevision1() != null) {
toReturn.append("-j "); // NOI18N
toReturn.append(getMergeRevision1());
toReturn.append(' ');
if (getMergeRevision2() != null) {
toReturn.append("-j "); // NOI18N
toReturn.append(getMergeRevision2());
toReturn.append(' ');
}
}
return toReturn.toString();
}
/**
* Takes the arguments and by parsing them, sets the command. To be mainly used for automatic settings (like parsing the .cvsrc file)
*/
@Override
public boolean setCVSCommand(char opt, String optArg) {
if (opt == 'R') {
setRecursive(true);
} else if (opt == 'C') {
setCleanCopy(true);
} else if (opt == 'l') {
setRecursive(false);
} else if (opt == 'd') {
setBuildDirectories(true);
} else if (opt == 'P') {
setPruneDirectories(true);
} else if (opt == 'A') {
setResetStickyOnes(true);
} else if (opt == 'f') {
setUseHeadIfNotFound(true);
} else if (opt == 'D') {
setUpdateByDate(optArg.trim());
} else if (opt == 'r') {
setUpdateByRevision(optArg.trim());
} else if (opt == 'k') {
KeywordSubstitutionOptions keywordSubst = KeywordSubstitutionOptions.findKeywordSubstOption(optArg);
setKeywordSubst(keywordSubst);
} else if (opt == 'p') {
setPipeToOutput(true);
} else if (opt == 'j') {
if (getMergeRevision1() == null) {
setMergeRevision1(optArg);
} else {
setMergeRevision2(optArg);
}
} else {
// TODO - now silently ignores not recognized switches.
return false;
}
return true;
}
/**
* 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() {
setRecursive(true);
setCleanCopy(false);
setBuildDirectories(false);
setPruneDirectories(false);
setResetStickyOnes(false);
setUseHeadIfNotFound(false);
setUpdateByDate(null);
setUpdateByRevision(null);
setKeywordSubst(null);
setPipeToOutput(false);
setMergeRevision1(null);
setMergeRevision2(null);
}
/**
* 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) {
return;
}
final String relativePath = CommandUtils.getExaminedDirectory(e.getMessage(), UpdateBuilder.EXAM_DIR);
if (relativePath == null) {
return;
}
// dont delete the current directory, even if it's empty
if (relativePath.equals(".")) { // NOI18N
return;
}
emptyDirectories.add(new File(getLocalDirectory(), relativePath));
}
/**
* Prunes a directory, recursively pruning its subdirectories
*
* @param directory
* the directory to prune
*/
private boolean pruneEmptyDirectory(File directory, ClientServices client) throws IOException {
final File[] contents = directory.listFiles();
// should never be null, but just in case...
if (contents == null) {
return true;
}
for (int i = 0; i < contents.length; i++) {
if (contents[i].isFile()) {
return false;
}
// Skip the cvs directory
if (contents[i].getName().equals("CVS")) {
// NOI18N
continue;
}
if (!pruneEmptyDirectory(contents[i], client)) {
return false;
}
}
// check this is a CVS directory and not some directory the user
// has stupidly called CVS...
if (new File(directory, "CVS/Entries").isFile() && new File(directory, "CVS/Repository").isFile()) {
final File adminDir = new File(directory, "CVS"); // NOI18N
// we must NOT delete a directory if it contains valuable entries
for (Iterator i = clientServices.getEntries(directory); i.hasNext();) {
Entry entry = (Entry) i.next();
if (entry.getName() != null && entry.isUserFileToBeRemoved()) {
return false;
}
}
deleteRecursively(adminDir);
directory.delete();
// if the client still makes this directory's entries available, do not delete its entry
if (!client.exists(directory)) {
client.removeEntry(directory);
}
return true;
}
return false;
}
/**
* Deletes a directory and all files and directories inside the directory.
*
* @param dir
* directory to delete
*/
private void deleteRecursively(File dir) {
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isDirectory()) {
deleteRecursively(file);
} else {
file.delete();
}
}
dir.delete();
}
/**
* Remove any directories that don't contain any files
*/
private void pruneEmptyDirectories(ClientServices client) throws IOException {
for (Iterator it = emptyDirectories.iterator(); 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, client);
}
}
emptyDirectories.clear();
}
/**
* String returned by this method defines which options are available for this particular command
*/
@Override
public String getOptString() {
return "RCnldPAfD:r:pj:k:"; // NOI18N
}
/**
* Getter for property mergeRevision1.
*
* @return Value of property mergeRevision1.
*/
public String getMergeRevision1() {
return mergeRevision1;
}
/**
* Setter for property mergeRevision1.
*
* @param mergeRevision1
* New value of property mergeRevision1.
*/
public void setMergeRevision1(String mergeRevision1) {
this.mergeRevision1 = getTrimmedString(mergeRevision1);
}
/**
* Getter for property mergeRevision2.
*
* @return Value of property mergeRevision2.
*/
public String getMergeRevision2() {
return mergeRevision2;
}
/**
* Setter for property mergeRevision2.
*
* @param mergeRevision2
* New value of property mergeRevision2.
*/
public void setMergeRevision2(String mergeRevision2) {
this.mergeRevision2 = getTrimmedString(mergeRevision2);
}
}