Coverage Report - com.aragost.javahg.internals.AbstractCommand
 
Classes in this File Line Coverage Branch Coverage Complexity
AbstractCommand
93%
116/124
76%
32/42
2.29
AbstractCommand$State
100%
1/1
N/A
2.29
 
 1  
 /*
 2  
  * #%L
 3  
  * JavaHg
 4  
  * %%
 5  
  * Copyright (C) 2011 aragost Trifork ag
 6  
  * %%
 7  
  * Permission is hereby granted, free of charge, to any person obtaining a copy
 8  
  * of this software and associated documentation files (the "Software"), to deal
 9  
  * in the Software without restriction, including without limitation the rights
 10  
  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 11  
  * copies of the Software, and to permit persons to whom the Software is
 12  
  * furnished to do so, subject to the following conditions:
 13  
  * 
 14  
  * The above copyright notice and this permission notice shall be included in
 15  
  * all copies or substantial portions of the Software.
 16  
  * 
 17  
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 18  
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 19  
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 20  
  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 21  
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 22  
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 23  
  * THE SOFTWARE.
 24  
  * #L%
 25  
  */
 26  
 package com.aragost.javahg.internals;
 27  
 
 28  
 import java.io.ByteArrayOutputStream;
 29  
 import java.io.IOException;
 30  
 import java.io.InputStream;
 31  
 import java.io.OutputStream;
 32  
 import java.nio.charset.CharsetDecoder;
 33  
 import java.util.ArrayList;
 34  
 import java.util.Arrays;
 35  
 import java.util.ConcurrentModificationException;
 36  
 import java.util.List;
 37  
 import java.util.concurrent.atomic.AtomicReference;
 38  
 
 39  
 import com.aragost.javahg.Changeset;
 40  
 import com.aragost.javahg.DateTime;
 41  
 import com.aragost.javahg.Repository;
 42  
 import com.aragost.javahg.UnknownCommandException;
 43  
 import com.aragost.javahg.commands.CancelledExecutionException;
 44  
 import com.aragost.javahg.commands.ExecutionException;
 45  
 import com.google.common.collect.Lists;
 46  
 import com.google.common.io.ByteStreams;
 47  
 
 48  
 /**
 49  
  * Base class for the command classes.
 50  
  * 
 51  
  * Each Mercurial command (e.g., "log", "commit", etc) is mapped to a command
 52  
  * class, which is a subclass of this class. The command classes will provide
 53  
  * methods for setting command line flags and for actually executing the
 54  
  * command.
 55  
  * 
 56  
  * Concurrency: Instances of this class should be accessed only on a single
 57  
  * thread. The only exception is {@link #cancel()}.
 58  
  * 
 59  
  * States: Normally: READY->QUEUED->RUNNING->READY. If cancelled: From READY,
 60  
  * QUEUED, or RUNNING -> CANCELLING -> READY. If cancelled the executing thread
 61  
  * will have a CancelledExecutionException thrown and state will be READY.
 62  
  */
 63  
 public abstract class AbstractCommand {
 64  
 
 65  5
     public enum State { READY, QUEUED, RUNNING, CANCELING }; 
 66  
 
 67  7773
     private AtomicReference<State> state = new AtomicReference<AbstractCommand.State>(State.READY);
 68  
 
 69  
     private final List<String> cmdLine;
 70  
 
 71  
     private final Repository repository;
 72  
 
 73  
     private OutputChannelInputStream outputChannelStream;
 74  
 
 75  7773
     private ByteArrayOutputStream error = new ByteArrayOutputStream();
 76  
 
 77  7773
     private int returnCode = Integer.MIN_VALUE;
 78  
 
 79  
     private int lineChannelLength;
 80  
 
 81  
     /**
 82  
      * Not null while in {@link State#RUNNING}.
 83  
      */
 84  
     private volatile Server server;
 85  
 
 86  7732
     protected AbstractCommand(Repository repository) {
 87  7732
         this.repository = repository;
 88  7732
         this.cmdLine = Lists.newArrayList(getCommandName());
 89  7732
     }
 90  
 
 91  41
     protected AbstractCommand(Repository repository, String commandName) {
 92  41
         this.repository = repository;
 93  41
         this.cmdLine = Lists.newArrayList(commandName);
 94  41
     }
 95  
 
 96  
     /**
 97  
      * @return the name of this Mercurial command, i.e., "add", "log",
 98  
      *         etc
 99  
      */
 100  
     public abstract String getCommandName();
 101  
 
 102  
     public void cmdAppend(String option) {
 103  7684
         this.cmdLine.add(option);
 104  7684
     }
 105  
 
 106  
     public void cmdAppend(String option, String arg) {
 107  2139
         if (arg == null) {
 108  1
             throw new NullPointerException("cannot pass null for " + option + " flag");
 109  
         }
 110  2138
         this.cmdLine.add(option);
 111  2138
         this.cmdLine.add(arg);
 112  2138
     }
 113  
 
 114  
     public void cmdAppend(String option, String[] args) {
 115  145
         for (String arg : args) {
 116  73
             cmdAppend(option, arg);
 117  
         }
 118  72
     }
 119  
 
 120  
     public void cmdAppend(String option, int arg) {
 121  4751
         this.cmdLine.add(option);
 122  4751
         this.cmdLine.add("" + arg);
 123  4751
     }
 124  
 
 125  
     public void cmdAppend(String option, DateTime date) {
 126  6
         if (date == null) {
 127  1
             throw new NullPointerException("cannot pass null for " + option + " flag");
 128  
         }
 129  5
         this.cmdLine.add(option);
 130  5
         this.cmdLine.add(date.getHgString());
 131  5
     }
 132  
 
 133  
     /**
 134  
      * Launch the command and return stdout as a String.
 135  
      * 
 136  
      * @param args
 137  
      *            extra command line arguments (optional).
 138  
      * @return stdout as a String.
 139  
      * @throws IOException
 140  
      */
 141  
     protected final String launchString(String... args) {
 142  1839
         InputStream stdout = launchStream(args);
 143  
         try {
 144  1816
             return Utils.readStream(stdout, getRepository().newDecoder());
 145  
         } finally {
 146  1814
             cleanUp();
 147  
         }
 148  
     }
 149  
 
 150  
     /**
 151  
      * Launch the command and return stdout as a InputStream.
 152  
      * 
 153  
      * @param args
 154  
      *            extra command line arguments (optional).
 155  
      * @return stdout stream
 156  
      */
 157  
     protected final HgInputStream launchStream(String... args) {
 158  3810
         clear();
 159  3810
         changeState(State.READY, State.QUEUED, true);
 160  
 
 161  
         try {
 162  3809
             server = repository.getServerPool().take(this);
 163  3808
             changeState(State.QUEUED, State.RUNNING, true);
 164  1
         } catch (InterruptedException e1) {
 165  1
             changeState(State.CANCELING, State.READY, false);
 166  1
             throw new CancelledExecutionException(this);
 167  3808
         }
 168  
 
 169  3808
         List<String> commandLine = new ArrayList<String>(this.cmdLine);
 170  3808
         boolean ok = false;
 171  
 
 172  3808
         getRepository().addToCommandLine(commandLine);
 173  3808
         commandLine.addAll(Arrays.asList(args));
 174  
 
 175  
         try {
 176  3808
             this.outputChannelStream = server.runCommand(commandLine, this);
 177  3778
             HgInputStream stream = new HgInputStream(outputChannelStream, this.repository.newDecoder());
 178  3778
             ok = true;
 179  3778
             return stream;
 180  13
         } catch (UnexpectedServerTerminationException e) {
 181  13
             if (state.get() == State.CANCELING) {
 182  1
                 throw new CancelledExecutionException(this);
 183  
             }
 184  12
             throw e;
 185  0
         } catch (IOException e) {
 186  0
             throw new RuntimeIOException(e);
 187  
         } finally {
 188  
             // If an exception is thrown there is no chance the command will
 189  
             // finish normally so abort the server. E.g protocol error
 190  3808
             if (!ok && state.get() != State.READY) {
 191  18
                 state.set(State.READY);
 192  18
                 repository.getServerPool().abort(server);
 193  18
                 server = null;
 194  
             }
 195  
         }
 196  
     }
 197  
 
 198  
     /**
 199  
      * Changes state checking for cancellation
 200  
      * 
 201  
      * @param current
 202  
      *            The expected state
 203  
      * @param next
 204  
      *            The state to change to
 205  
      * @param strict
 206  
      *            If true a {@link ConcurrentModificationException} is thrown if
 207  
      *            in unexpected state
 208  
      * @throws CancelledExecutionException
 209  
      *             If in cancelled state
 210  
      * @throws ConcurrentModificationException
 211  
      *             If in another unexpected state and if strict is true
 212  
      */
 213  
     private void changeState(State current, State next, boolean strict) {
 214  11406
         if (!state.compareAndSet(current, next)) {
 215  1
             if (state.compareAndSet(State.CANCELING, State.READY)) {
 216  1
                 throw new CancelledExecutionException(this);                
 217  
             }
 218  0
             if (strict) {
 219  0
                 throw new ConcurrentModificationException("Unexpected command state");
 220  
             }
 221  
         }
 222  11405
     }
 223  
 
 224  
     protected final LineIterator launchIterator(String... args) {
 225  1745
         return new LineIterator(launchStream(args));
 226  
     }
 227  
 
 228  
     /**
 229  
      * Open the output stream again after sending input to the command
 230  
      * server. When the server alternates between sending output on
 231  
      * the 'o' channel and reading input lines, the output channel
 232  
      * will run dry several times. Call this method after sending
 233  
      * input to the command server in response to reading from the 'L'
 234  
      * channel. New output from the server will then appear on the
 235  
      * output channel.
 236  
      */
 237  
     public void reopenOutputChannelStream() {
 238  3
         this.outputChannelStream.reopen();
 239  3
     }
 240  
 
 241  
     /**
 242  
      * Send input line to command server in response to reading a
 243  
      * block on the 'L' channel.
 244  
      * 
 245  
      * @param s
 246  
      *            line of input.
 247  
      */
 248  
     public void sendLine(String s) {
 249  3
         if (this.lineChannelLength == 0) {
 250  0
             throw new IllegalStateException("No input expected");
 251  
         }
 252  
 
 253  3
         server.sendLine(s);
 254  3
         this.lineChannelLength = 0;
 255  3
     }
 256  
 
 257  
     /**
 258  
      * @return true if we have read a 'L' channel from the server.
 259  
      */
 260  
     public boolean needsInputLine() {
 261  0
         return this.lineChannelLength > 0;
 262  
     }
 263  
 
 264  
     /**
 265  
      * Finish the request by consuming any remaining input on the
 266  
      * output channel from the server. The server wont respond to new
 267  
      * commands until this is done.
 268  
      */
 269  
     void cleanUp() {
 270  1816
         if (this.outputChannelStream != null) {
 271  
             try {
 272  1816
                 Utils.consumeAll(this.outputChannelStream);
 273  1
             } catch (IOException e) {
 274  1
                 throw new RuntimeIOException(e);
 275  1815
             }
 276  
         }
 277  1815
     }
 278  
 
 279  
     protected void clear() {
 280  3810
         this.error.reset();
 281  3810
         this.returnCode = Integer.MIN_VALUE;
 282  3810
     }
 283  
 
 284  
     /**
 285  
      * Check if the command ended with a zero return code. Subclasses
 286  
      * can override this to accept other return codes as successful.
 287  
      * 
 288  
      * @return true if the command ended successfully.
 289  
      */
 290  
     protected boolean isSuccessful() {
 291  3787
         return getReturnCode() == 0;
 292  
     }
 293  
 
 294  
     private String streamAsString(OutputStream stream) {
 295  50
         if (stream instanceof ByteArrayOutputStream) {
 296  50
             byte[] bytes = ((ByteArrayOutputStream) stream).toByteArray();
 297  50
             CharsetDecoder decoder = getRepository().newDecoder();
 298  50
             return Utils.decodeBytes(bytes, decoder);
 299  
         } else {
 300  0
             throw new IllegalStateException();
 301  
         }
 302  
     }
 303  
 
 304  
     /**
 305  
      * @return data read on the 'e' channel
 306  
      */
 307  
     public String getErrorString() {
 308  50
         return streamAsString(this.error);
 309  
     }
 310  
 
 311  
     /**
 312  
      * @return the return code read from the 'r' channel
 313  
      */
 314  
     public int getReturnCode() {
 315  7439
         if (this.returnCode == Integer.MIN_VALUE) {
 316  0
             throw new IllegalStateException("cmdserver is still executing request");
 317  
         }
 318  7439
         return this.returnCode;
 319  
     }
 320  
 
 321  
     /**
 322  
      * @return the {@link Repository} associated with this command.
 323  
      */
 324  
     public Repository getRepository() {
 325  7764
         return repository;
 326  
     }
 327  
 
 328  
     @Override
 329  
     public String toString() {
 330  1
         return getCommandName();
 331  
     }
 332  
 
 333  
     /**
 334  
      * @param lineChannelLength
 335  
      *            the line length requested by the server on the 'L'
 336  
      *            channel.
 337  
      */
 338  
     void setLineChannelLength(int lineChannelLength) {
 339  3
         this.lineChannelLength = lineChannelLength;
 340  3
     }
 341  
 
 342  
     /**
 343  
      * Add to the stderr data stored in this command.
 344  
      * 
 345  
      * @param cin
 346  
      *            stderr of the command server.
 347  
      * @throws IOException
 348  
      */
 349  
     void addToError(BlockInputStream cin) throws IOException {
 350  391
         ByteStreams.copy(cin, this.error);
 351  391
     }
 352  
 
 353  
     protected void withDebugAndChangesetStyle() {
 354  32
         withDebugFlag();
 355  32
         cmdAppend("--style", Changeset.CHANGESET_STYLE_PATH);
 356  32
     }
 357  
 
 358  
     protected void withDebugFlag() {
 359  7574
         cmdAppend("--debug");
 360  7574
     }
 361  
 
 362  
     /**
 363  
      * Called exactly once when this command finishes executing. The server is
 364  
      * freed and the state is changed to DONE.
 365  
      * 
 366  
      * @param returnCode The exit code of the completed command
 367  
      */
 368  
     final void handleReturnCode(int returnCode) {
 369  3789
         this.returnCode = returnCode;
 370  3789
         server.clearCurrentCommand(this);
 371  3787
         repository.getServerPool().put(server);
 372  3787
         server = null;
 373  3787
         changeState(State.RUNNING, State.READY, false);
 374  
 
 375  3787
         if (returnCode == -1) {
 376  
             // This can for example happens for an unknown command
 377  16
             String errorString = getErrorString();
 378  16
             if (errorString.startsWith("hg: unknown command '")) {
 379  1
                 throw new UnknownCommandException(this);
 380  
             }
 381  
         }
 382  
 
 383  3786
         doneHook();
 384  
 
 385  3786
         if (!isSuccessful()) {
 386  14
             throw new ExecutionException(this);
 387  
         }
 388  3772
     }
 389  
 
 390  
     /**
 391  
      * Cancel a running command. May be called from a different thread. Returns
 392  
      * immediately. The thread executing the command should return soon after
 393  
      * with a {@link CancelledExecutionException} thrown.
 394  
      */
 395  
     public final void cancel() {
 396  4
         State oldState = state.getAndSet(State.CANCELING);
 397  
 
 398  4
         if (oldState != State.READY) {
 399  
             // There is a small chance the final state of a command will be
 400  
             // CANCELLING rather than DONE but this doesn't actually matter.
 401  3
             Server server = this.server;
 402  
             Process process;
 403  
 
 404  3
             if (server != null && (process = server.getProcess()) != null) {
 405  2
                 process.destroy();
 406  
             }
 407  
         }
 408  4
     }
 409  
 
 410  
     /**
 411  
      * @return The current state of the command.
 412  
      */
 413  
     State getState() {
 414  10758
         return state.get();
 415  
     }
 416  
 
 417  
     /**
 418  
      * This method is called when the processing of a command is
 419  
      * finished. More precise it is called just after the 'r' channel
 420  
      * is read
 421  
      * <p>
 422  
      * It can be overridden in subclasses.
 423  
      */
 424  
     protected void doneHook() {
 425  
 
 426  3781
     }
 427  
 }