/*
* Copyright 2000-2009 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.community.intellij.plugins.communitycase.commands;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.EventDispatcher;
import com.intellij.util.Processor;
import org.community.intellij.plugins.communitycase.Util;
import org.community.intellij.plugins.communitycase.Vcs;
import org.community.intellij.plugins.communitycase.config.VcsSettings;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.*;
/**
* A handler for git commands
*/
public abstract class Handler {
protected final Project myProject;
protected final Command myCommand;
private final HashSet<Integer> myIgnoredErrorCodes = new HashSet<Integer>(); // Error codes that are ignored for the handler
private final List<VcsException> myErrors = Collections.synchronizedList(new ArrayList<VcsException>());
private static final Logger log = Logger.getInstance("#"+Handler.class.getName());
GeneralCommandLine myCommandLine;
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
Process myProcess;
private boolean myStdoutSuppressed; // If true, the standard output is not copied to version control console
private boolean myStderrSuppressed; // If true, the standard error is not copied to version control console
private final File myWorkingDirectory;
private boolean myEnvironmentCleanedUp = true; // the flag indicating that environment has been cleaned up, by default is true because there is nothing to clean
private int myHandlerNo;
private Processor<OutputStream> myInputProcessor; // The processor for stdin
// if true process might be cancelled
// note that access is safe because it accessed in unsynchronized block only after process is started, and it does not change after that
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
private boolean myIsCancellable = true;
private Integer myExitCode; // exit code or null if exit code is not yet available
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
@NonNls
private Charset myCharset = Charset.forName("UTF-8"); // Character set to use for IO
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
private boolean myRemoteFlag = false;
private final EventDispatcher<HandlerListener> myListeners = EventDispatcher.create(HandlerListener.class);
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
private boolean mySilent; // if true, the command execution is not logged in version control view
protected final Vcs myVcs;
private final Map<String, String> myEnv;
protected VcsSettings mySettings;
private VcsSettings myProjectSettings;
private Runnable mySuspendAction; // Suspend action used by {@link #suspendWriteLock()}
private Runnable myResumeAction; // Resume action used by {@link #resumeWriteLock()}
/**
* A constructor
*
* @param project a project
* @param directory a process directory
* @param command a command to execute (if empty string, the parameter is ignored)
*/
protected Handler(@NotNull Project project, @NotNull File directory, @NotNull Command command) {
myProject = project;
myCommand = command;
mySettings= VcsSettings.getInstance(myProject);
myEnv = new HashMap<String, String>(System.getenv());
if (!myEnv.containsKey("HOME")) {
String home = System.getProperty("user.home");
if (home != null) {
myEnv.put("HOME", home);
}
}
myVcs = Vcs.getInstance(project);
if (myVcs != null) {
myVcs.checkVersion();
}
myWorkingDirectory = directory;
myCommandLine = new GeneralCommandLine();
if (mySettings!= null) {
myCommandLine.setExePath(mySettings.getPathToExecutable());
}
myCommandLine.setWorkingDirectory(myWorkingDirectory);
if (command.name().length() > 0) {
addParameters(command.name());
}
}
/**
* A constructor
*
* @param project a project
* @param vcsRoot a process directory
* @param command a command to execute
*/
protected Handler(final Project project, final VirtualFile vcsRoot, final Command command) {
this(project, VfsUtil.virtualToIoFile(vcsRoot), command);
}
/**
* @return multicaster for listeners
*/
protected HandlerListener listeners() {
return myListeners.getMulticaster();
}
/**
* Add error code to ignored list
*
* @param code the code to ignore
*/
public void ignoreErrorCode(int code) {
myIgnoredErrorCodes.add(code);
}
/**
* Check if error code should be ignored
*
* @param code a code to check
* @return true if error code is ignorable
*/
public boolean isIgnoredErrorCode(int code) {
return myIgnoredErrorCodes.contains(code);
}
/**
* add error to the error list
*
* @param ex an error to add to the list
*/
public void addError(VcsException ex) {
myErrors.add(ex);
}
/**
* @return unmodifiable list of errors.
*/
public List<VcsException> errors() {
return Collections.unmodifiableList(myErrors);
}
/**
* @return a context project
*/
public Project project() {
return myProject;
}
/**
* @return the current working directory
*/
public File workingDirectory() {
return myWorkingDirectory;
}
/**
* @return the current working directory
*/
public VirtualFile workingDirectoryFile() {
final VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(workingDirectory());
if (file == null) {
throw new IllegalStateException("The working directly should be available: " + workingDirectory());
}
return file;
}
/**
* Set SSH flag. This flag should be set to true for commands that never interact with remote repositories.
*
* @param value if value is true, the custom ssh is not used for the command.
*/
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
public void setRemote(boolean value) {
checkNotStarted();
myRemoteFlag = value;
}
/**
* @return true if SSH is not invoked by this command.
*/
@SuppressWarnings({"WeakerAccess"})
public boolean isRemote() {
return myRemoteFlag;
}
/**
* Add listener to handler
*
* @param listener a listener
*/
protected void addListener(HandlerListener listener) {
myListeners.addListener(listener);
}
/**
* End option parameters and start file paths. The method adds {@code "--"} parameter.
*/
public void endOptions() {
myCommandLine.addParameter("--");
}
/**
* Add string parameters
*
* @param parameters a parameters to add
*/
@SuppressWarnings({"WeakerAccess"})
public void addParameters(@NonNls @NotNull String... parameters) {
checkNotStarted();
String[] fixedParameters = fixSpaces(parameters);
myCommandLine.addParameters(fixedParameters);
}
/**
* Add parameters from the list
*
* @param parameters the parameters to add
*/
public void addParameters(List<String> parameters) {
checkNotStarted();
myCommandLine.addParameters(parameters);
}
private String[] fixSpaces(String[] parameters) {
List<String> fixedParams = new ArrayList<String>();
for(String s : parameters) {
String[] separated = s.trim().split(" ");
fixedParams.addAll(Arrays.asList(separated)); //add all of the components separately
}
return fixedParams.toArray(new String[fixedParams.size()]);
}
/**
* Add file path parameters. The parameters are made relative to the working directory
*
* @param parameters a parameters to add
* @throws IllegalArgumentException if some path is not under root.
*/
@Deprecated
public void addRelativePaths(@NotNull FilePath... parameters) {
addRelativePaths(Arrays.asList(parameters));
}
/**
* Add file path parameters. The parameters are made relative to the working directory
*
* @param filePaths a parameters to add
* @throws IllegalArgumentException if some path is not under root.
*/
@SuppressWarnings({"WeakerAccess"})
@Deprecated
public void addRelativePaths(@NotNull final Collection<FilePath> filePaths) {
addRelativePaths(myCommandLine,myWorkingDirectory,filePaths);
}
/**
* Add file path parameters. The parameters are made relative to the working directory
*
* @param commandLine the command line to which the path will be added
* @param workingDirectory directory in which the command will be executed
* @param filePaths a parameters to add
* @throws IllegalArgumentException if some path is not under root.
*/
@Deprecated
private void addRelativePaths(@NotNull GeneralCommandLine commandLine,
@NotNull File workingDirectory,
@NotNull final Collection<FilePath> filePaths) {
checkNotStarted();
for (FilePath path : filePaths) {
commandLine.addParameter(Util.relativePath(workingDirectory, path));
}
}
/**
* Verifies if adding the paths as parameters would make the command line too long
*
* @param filePaths a parameters to add
* @throws IllegalArgumentException if some path is not under root.
* @return true if the command line would be too long, false otherwise
*/
public boolean isAddedPathSizeTooGreat(@NotNull final Collection<FilePath> filePaths) {
addRelativePaths(myCommandLine,myWorkingDirectory,filePaths);
return isLargeCommandLine(myCommandLine);
}
/**
* Add file path parameters. The parameters are made relative to the working directory
*
* @param files a parameters to add
* @throws IllegalArgumentException if some path is not under root.
*/
@Deprecated
public void addRelativePathsForFiles(@NotNull final Collection<File> files) {
checkNotStarted();
for (File file : files) {
myCommandLine.addParameter(Util.relativePath(myWorkingDirectory, file));
}
}
/**
* Add virtual file parameters. The parameters are made relative to the working directory
*
* @param files a parameters to add
* @throws IllegalArgumentException if some path is not under root.
*/
@SuppressWarnings({"WeakerAccess"})
public void addRelativeFiles(@NotNull final Collection<VirtualFile> files) {
checkNotStarted();
for (VirtualFile file : files) {
myCommandLine.addParameter(Util.relativePath(myWorkingDirectory, file));
}
}
/**
* check that process is not started yet
*
* @throws IllegalStateException if process has been already started
*/
private void checkNotStarted() {
if (isStarted()) {
throw new IllegalStateException("The process has been already started");
}
}
/**
* check that process is started
*
* @throws IllegalStateException if process has not been started
*/
protected final void checkStarted() {
if (!isStarted()) {
throw new IllegalStateException("The process is not started yet");
}
}
/**
* @return true if process is started
*/
public final synchronized boolean isStarted() {
return myProcess != null;
}
/**
* Set new value of cancellable flag (by default true)
*
* @param value a new value of the flag
*/
public void setCancellable(boolean value) {
checkNotStarted();
myIsCancellable = value;
}
/**
* @return cancellable state
*/
public boolean isCancellable() {
return myIsCancellable;
}
/**
* Start process
*/
public synchronized void start() {
checkNotStarted();
try {
// setup environment
if (!myProject.isDefault() && !mySilent && (myVcs != null)) {
myVcs.showCommandLine("cd " + myWorkingDirectory);
myVcs.showCommandLine(printableCommandLine());
}
if (log.isDebugEnabled()) {
log.debug("running: " + myCommandLine.getCommandLineString() + " in " + myWorkingDirectory);
}
myCommandLine.setEnvParams(myEnv);
// start process
myProcess = myCommandLine.createProcess();
startHandlingStreams();
}
catch (Throwable t) {
cleanupEnv();
myListeners.getMulticaster().startFailed(t);
}
}
/**
* Start handling streams for the handler
*/
protected abstract void startHandlingStreams();
/**
* @return a command line with full path to executable replace to "git"
*/
public String printableCommandLine() {
return myCommandLine.getCommandLineString();
}
/**
* Cancel activity
*/
public synchronized void cancel() {
checkStarted();
if (!myIsCancellable) {
throw new IllegalStateException("The process is not cancellable.");
}
destroyProcess();
}
/**
* Destroy process
*/
protected abstract void destroyProcess();
/**
* @return exit code for process if it is available
*/
public synchronized int getExitCode() {
if (myExitCode == null) {
throw new IllegalStateException("Exit code is not yet available");
}
return myExitCode.intValue();
}
/**
* @param exitCode a exit code for process
*/
protected synchronized void setExitCode(int exitCode) {
myExitCode = exitCode;
}
/**
* Cleanup environment
*/
protected synchronized void cleanupEnv() {
}
/**
* Wait for process termination
*/
public void waitFor() {
checkStarted();
try {
if (myInputProcessor != null) {
myInputProcessor.process(myProcess.getOutputStream());
}
}
finally {
waitForProcess();
}
}
/**
* Wait for process
*/
protected abstract void waitForProcess();
/**
* Set silent mode. When handler is silent, it does not logs command in version control console.
* Note that this option also suppresses stderr and stdout copying.
*
* @param silent a new value of the flag
* @see #setStderrSuppressed(boolean)
* @see #setStdoutSuppressed(boolean)
*/
@SuppressWarnings({"SameParameterValue"})
public void setSilent(final boolean silent) {
checkNotStarted();
//mySilent = silent;
//setStderrSuppressed(true);
//setStdoutSuppressed(true);
}
/**
* @return a character set to use for IO
*/
public Charset getCharset() {
return myCharset;
}
/**
* Set character set for IO
*
* @param charset a character set
*/
@SuppressWarnings({"SameParameterValue"})
public void setCharset(final Charset charset) {
myCharset = charset;
}
/**
* @return true if standard output is not copied to the console
*/
public boolean isStdoutSuppressed() {
return myStdoutSuppressed;
}
/**
* Set flag specifying if stdout should be copied to the console
*
* @param stdoutSuppressed true if output is not copied to the console
*/
public void setStdoutSuppressed(final boolean stdoutSuppressed) {
checkNotStarted();
//myStdoutSuppressed = stdoutSuppressed;
}
/**
* @return true if standard output is not copied to the console
*/
public boolean isStderrSuppressed() {
return myStderrSuppressed;
}
/**
* Set flag specifying if stderr should be copied to the console
*
* @param stderrSuppressed true if error output is not copied to the console
*/
public void setStderrSuppressed(final boolean stderrSuppressed) {
checkNotStarted();
//myStderrSuppressed = stderrSuppressed;
}
/**
* Set environment variable
*
* @param name the variable name
* @param value the variable value
*/
public void setEnvironment(String name, String value) {
myEnv.put(name, value);
}
/**
* Set processor for standard input. This is a place where input to the git application could be generated.
*
* @param inputProcessor the processor
*/
public void setInputProcessor(Processor<OutputStream> inputProcessor) {
myInputProcessor = inputProcessor;
}
/**
* Set suspend/resume actions
*
* @param suspend the suspend action
* @param resume the resume action
*/
synchronized void setSuspendResume(Runnable suspend, Runnable resume) {
mySuspendAction = suspend;
myResumeAction = resume;
}
/**
* Suspend write lock held by the handler
*/
public synchronized void suspendWriteLock() {
assert mySuspendAction != null;
mySuspendAction.run();
}
/**
* Resume write lock held by the handler
*/
public synchronized void resumeWriteLock() {
assert mySuspendAction != null;
myResumeAction.run();
}
/**
* @return true if the command line is too big
*/
public boolean isLargeCommandLine() {
return isLargeCommandLine(myCommandLine);
}
/**
* @return true if the command line is too big
*/
private static boolean isLargeCommandLine(GeneralCommandLine commandLine) {
return commandLine.getCommandLineString().length() > FileUtils.FILE_PATH_LIMIT;
}
}