Package com.aragost.javahg.internals

Source Code of com.aragost.javahg.internals.AbstractCommand

/*
* #%L
* JavaHg
* %%
* Copyright (C) 2011 aragost Trifork ag
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* #L%
*/
package com.aragost.javahg.internals;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.CharsetDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import com.aragost.javahg.Changeset;
import com.aragost.javahg.DateTime;
import com.aragost.javahg.Repository;
import com.aragost.javahg.UnknownCommandException;
import com.aragost.javahg.commands.CancelledExecutionException;
import com.aragost.javahg.commands.ExecutionException;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;

/**
* Base class for the command classes.
*
* Each Mercurial command (e.g., "log", "commit", etc) is mapped to a command
* class, which is a subclass of this class. The command classes will provide
* methods for setting command line flags and for actually executing the
* command.
*
* Concurrency: Instances of this class should be accessed only on a single
* thread. The only exception is {@link #cancel()}.
*
* States: Normally: READY->QUEUED->RUNNING->READY. If cancelled: From READY,
* QUEUED, or RUNNING -> CANCELLING -> READY. If cancelled the executing thread
* will have a CancelledExecutionException thrown and state will be READY.
*/
public abstract class AbstractCommand {

    public enum State { READY, QUEUED, RUNNING, CANCELING };

    private AtomicReference<State> state = new AtomicReference<AbstractCommand.State>(State.READY);

    private final List<String> cmdLine;

    private final Repository repository;

    private OutputChannelInputStream outputChannelStream;

    private ByteArrayOutputStream error = new ByteArrayOutputStream();

    private int returnCode = Integer.MIN_VALUE;

    private int lineChannelLength;

    /**
     * Not null while in {@link State#RUNNING}.
     */
    private volatile Server server;

    protected AbstractCommand(Repository repository) {
        this.repository = repository;
        this.cmdLine = Lists.newArrayList(getCommandName());
    }

    protected AbstractCommand(Repository repository, String commandName) {
        this.repository = repository;
        this.cmdLine = Lists.newArrayList(commandName);
    }

    /**
     * @return the name of this Mercurial command, i.e., "add", "log",
     *         etc
     */
    public abstract String getCommandName();

    public void cmdAppend(String option) {
        this.cmdLine.add(option);
    }

    public void cmdAppend(String option, String arg) {
        if (arg == null) {
            throw new NullPointerException("cannot pass null for " + option + " flag");
        }
        this.cmdLine.add(option);
        this.cmdLine.add(arg);
    }

    public void cmdAppend(String option, String[] args) {
        for (String arg : args) {
            cmdAppend(option, arg);
        }
    }

    public void cmdAppend(String option, int arg) {
        this.cmdLine.add(option);
        this.cmdLine.add("" + arg);
    }

    public void cmdAppend(String option, DateTime date) {
        if (date == null) {
            throw new NullPointerException("cannot pass null for " + option + " flag");
        }
        this.cmdLine.add(option);
        this.cmdLine.add(date.getHgString());
    }

    /**
     * Launch the command and return stdout as a String.
     *
     * @param args
     *            extra command line arguments (optional).
     * @return stdout as a String.
     * @throws IOException
     */
    protected final String launchString(String... args) {
        InputStream stdout = launchStream(args);
        try {
            return Utils.readStream(stdout, getRepository().newDecoder());
        } finally {
            cleanUp();
        }
    }

    /**
     * Launch the command and return stdout as a InputStream.
     *
     * @param args
     *            extra command line arguments (optional).
     * @return stdout stream
     */
    protected final HgInputStream launchStream(String... args) {
        clear();
        changeState(State.READY, State.QUEUED, true);

        try {
            server = repository.getServerPool().take(this);
            changeState(State.QUEUED, State.RUNNING, true);
        } catch (InterruptedException e1) {
            changeState(State.CANCELING, State.READY, false);
            throw new CancelledExecutionException(this);
        }

        List<String> commandLine = new ArrayList<String>(this.cmdLine);
        boolean ok = false;

        getRepository().addToCommandLine(commandLine);
        commandLine.addAll(Arrays.asList(args));

        try {
            this.outputChannelStream = server.runCommand(commandLine, this);
            HgInputStream stream = new HgInputStream(outputChannelStream, this.repository.newDecoder());
            ok = true;
            return stream;
        } catch (UnexpectedServerTerminationException e) {
            if (state.get() == State.CANCELING) {
                throw new CancelledExecutionException(this);
            }
            throw e;
        } catch (IOException e) {
            throw new RuntimeIOException(e);
        } finally {
            // If an exception is thrown there is no chance the command will
            // finish normally so abort the server. E.g protocol error
            if (!ok && state.get() != State.READY) {
                state.set(State.READY);
                repository.getServerPool().abort(server);
                server = null;
            }
        }
    }

    /**
     * Changes state checking for cancellation
     *
     * @param current
     *            The expected state
     * @param next
     *            The state to change to
     * @param strict
     *            If true a {@link ConcurrentModificationException} is thrown if
     *            in unexpected state
     * @throws CancelledExecutionException
     *             If in cancelled state
     * @throws ConcurrentModificationException
     *             If in another unexpected state and if strict is true
     */
    private void changeState(State current, State next, boolean strict) {
        if (!state.compareAndSet(current, next)) {
            if (state.compareAndSet(State.CANCELING, State.READY)) {
                throw new CancelledExecutionException(this);               
            }
            if (strict) {
                throw new ConcurrentModificationException("Unexpected command state");
            }
        }
    }

    protected final LineIterator launchIterator(String... args) {
        return new LineIterator(launchStream(args));
    }

    /**
     * Open the output stream again after sending input to the command
     * server. When the server alternates between sending output on
     * the 'o' channel and reading input lines, the output channel
     * will run dry several times. Call this method after sending
     * input to the command server in response to reading from the 'L'
     * channel. New output from the server will then appear on the
     * output channel.
     */
    public void reopenOutputChannelStream() {
        this.outputChannelStream.reopen();
    }

    /**
     * Send input line to command server in response to reading a
     * block on the 'L' channel.
     *
     * @param s
     *            line of input.
     */
    public void sendLine(String s) {
        if (this.lineChannelLength == 0) {
            throw new IllegalStateException("No input expected");
        }

        server.sendLine(s);
        this.lineChannelLength = 0;
    }

    /**
     * @return true if we have read a 'L' channel from the server.
     */
    public boolean needsInputLine() {
        return this.lineChannelLength > 0;
    }

    /**
     * Finish the request by consuming any remaining input on the
     * output channel from the server. The server wont respond to new
     * commands until this is done.
     */
    void cleanUp() {
        if (this.outputChannelStream != null) {
            try {
                Utils.consumeAll(this.outputChannelStream);
            } catch (IOException e) {
                throw new RuntimeIOException(e);
            }
        }
    }

    protected void clear() {
        this.error.reset();
        this.returnCode = Integer.MIN_VALUE;
    }

    /**
     * Check if the command ended with a zero return code. Subclasses
     * can override this to accept other return codes as successful.
     *
     * @return true if the command ended successfully.
     */
    protected boolean isSuccessful() {
        return getReturnCode() == 0;
    }

    private String streamAsString(OutputStream stream) {
        if (stream instanceof ByteArrayOutputStream) {
            byte[] bytes = ((ByteArrayOutputStream) stream).toByteArray();
            CharsetDecoder decoder = getRepository().newDecoder();
            return Utils.decodeBytes(bytes, decoder);
        } else {
            throw new IllegalStateException();
        }
    }

    /**
     * @return data read on the 'e' channel
     */
    public String getErrorString() {
        return streamAsString(this.error);
    }

    /**
     * @return the return code read from the 'r' channel
     */
    public int getReturnCode() {
        if (this.returnCode == Integer.MIN_VALUE) {
            throw new IllegalStateException("cmdserver is still executing request");
        }
        return this.returnCode;
    }

    /**
     * @return the {@link Repository} associated with this command.
     */
    public Repository getRepository() {
        return repository;
    }

    @Override
    public String toString() {
        return getCommandName();
    }

    /**
     * @param lineChannelLength
     *            the line length requested by the server on the 'L'
     *            channel.
     */
    void setLineChannelLength(int lineChannelLength) {
        this.lineChannelLength = lineChannelLength;
    }

    /**
     * Add to the stderr data stored in this command.
     *
     * @param cin
     *            stderr of the command server.
     * @throws IOException
     */
    void addToError(BlockInputStream cin) throws IOException {
        ByteStreams.copy(cin, this.error);
    }

    protected void withDebugAndChangesetStyle() {
        withDebugFlag();
        cmdAppend("--style", Changeset.CHANGESET_STYLE_PATH);
    }

    protected void withDebugFlag() {
        cmdAppend("--debug");
    }

    /**
     * Called exactly once when this command finishes executing. The server is
     * freed and the state is changed to DONE.
     *
     * @param returnCode The exit code of the completed command
     */
    final void handleReturnCode(int returnCode) {
        this.returnCode = returnCode;
        server.clearCurrentCommand(this);
        repository.getServerPool().put(server);
        server = null;
        changeState(State.RUNNING, State.READY, false);

        if (returnCode == -1) {
            // This can for example happens for an unknown command
            String errorString = getErrorString();
            if (errorString.startsWith("hg: unknown command '")) {
                throw new UnknownCommandException(this);
            }
        }

        doneHook();

        if (!isSuccessful()) {
            throw new ExecutionException(this);
        }
    }

    /**
     * Cancel a running command. May be called from a different thread. Returns
     * immediately. The thread executing the command should return soon after
     * with a {@link CancelledExecutionException} thrown.
     */
    public final void cancel() {
        State oldState = state.getAndSet(State.CANCELING);

        if (oldState != State.READY) {
            // There is a small chance the final state of a command will be
            // CANCELLING rather than DONE but this doesn't actually matter.
            Server server = this.server;
            Process process;

            if (server != null && (process = server.getProcess()) != null) {
                process.destroy();
            }
        }
    }

    /**
     * @return The current state of the command.
     */
    State getState() {
        return state.get();
    }

    /**
     * This method is called when the processing of a command is
     * finished. More precise it is called just after the 'r' channel
     * is read
     * <p>
     * It can be overridden in subclasses.
     */
    protected void doneHook() {

    }
}
TOP

Related Classes of com.aragost.javahg.internals.AbstractCommand

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.