Runtime, Process y Git

Hoy voy a intentar explicar cómo comunicar nuestros programas Java con cualquier proceso, y al  final pondré un ejemplo con Git.

En Java podemos lanzar procesos con la clase Process, cosa que viene perfectamente explicada en esta página:

http://www.chuidiang.com/java/ejemplos/Runtime/runtime.php

Como se explica en esa web podemos, además de lanzar un proceso, leer sus salidas.

Así que vamos a  darle una estructura mejor para poder usarlo de manera fácil y modular.

Primero vamos a hacer una clase para leer las salidas del proceso. Ahora mismo no he probado como responder a peticiones que nos haga un proceso pero lo estudiaré más adelante. Aclarar también que tenemos que ver las entradas y las salidas desde el punto de vista del proceso, quiero decir: Para nosotros cuando un evento nos  devuelve algo(por ejemplo un syso) es un output. Sin embargo desde la vista del proceso, esa salida de información le llega desde el programa al que llamamos, así que es una entrada, un Inputstream.

También tenemos que pensar en “cuando un proceso” ha  terminado, o cuando su Inputstream ha terminado de leer la información que le llegaba, por tanto tendremos que implementar 2 variables sincronizadas (ya que estamos trabajando con hilos) para saber cuando el proceso termina.

   private class ProcessStreamReader
            extends Thread
    {
        private BufferedReader _reader;
        private int _lvl;

        public ProcessStreamReader(InputStream is, int lvl)
        {
            _reader = new BufferedReader(new InputStreamReader(is));
            _lvl = lvl;
            if (_lvl == 0) // Encendenmos los semáforos. También lo podemos hacer con unos set fuera del
            // método si no vamos a usarlo inmediatamente.
            {
                _okSemaforo = true;
            }
            else
            {
                _errorSemaforo = true;
            }
        }

        public void run()
        {
            StringBuilder sb = null;
            try
            {
                String line = _reader.readLine();
                sb = new StringBuilder(line);

                while (line != null && sb.toString() != null)
                {
                    // Aquí podemos hacer operaciones, como mostrarlo en una ventana o procesar el texto.
                    line = _reader.readLine();
                    sb.append("\n");
                    sb.append(line);
                }
            }
            catch (IOException e)
            {
            }
            catch (NullPointerException e)
            {
            }
            finally // Finalmente tenemos que cerrar los buffers y apagar los semáforos.
            {
                if (_lvl == 0)
                {
                    if (sb != null)
                    {
                        _okLecture = sb.toString();
                    }
                    else
                    {
                        _okLecture = null;
                    }
                }
                else
                {
                    if (sb != null)
                    {
                        _errorLecture = sb.toString();
                    }
                    else
                    {
                        _errorLecture = null;
                    }
                }

                if (_lvl == 0)
                {
                    _okSemaforo = false;
                }
                else
                {
                    _errorSemaforo = false;
                }
                try
                {
                    _reader.close();
                }
                catch (IOException ex)
                {
                }
            }
        }
    }

Como vemos tenemos un flag lvl para distinguir entre el errorInputStream y el InputStream normal.

Ahora vamos a implementar el propio proceso.
Sólo vamos a añadir una consideración más, normalmente queremos que nuestro proceso se lance en la misma carpeta que  estamos trabajando, así que tendremos que pasarle una variable dir que será donde se ejecute el proceso.

  private class ProcessExecuter
            extends Thread
    {
        private String _process;
        private String _dir;

        public ProcessExecuter(String p, String dir)
        {
            _process = p;
            _dir = dir;
        }

        public void run()
        {
            try
            {
                Process p = Runtime.getRuntime().exec(_process, null, new File(_dir));

                new ProcessStreamReader(p.getInputStream(), 0).start();
                new ProcessStreamReader(p.getErrorStream(), 1).start();

                while (isOkSemaforo() || isErrorSemaforo())
                {
                    try // Dormimos mientras se esta llevando a cabo el proceso.
                    {
                        sleep(200);
                    }
                    catch (InterruptedException ex)
                    {
                    }
                }

            }
            catch (IOException ex)
            {
                // Manejamos las excepciones
            }
            finally
            {
                // Al finalizar tendremos que realizar alguna acción,
                // por ejemplo continuar a la siguiente fase
                _stage++;
                continueStage();
            }
        }
    }

Las variables que usamos:

    private boolean _okSemaforo;
    public synchronized boolean isOkSemaforo()
    {
        return _okSemaforo;
    }

    private boolean _errorSemaforo;
    public synchronized boolean isErrorSemaforo()
    {
        return _errorSemaforo;
    }

    private static int _stage;

Y ahora como ejemplo práctico pongo mi implementación de comandos para Git, en el que se puede ver como se avanza por distintas fases y se llaman a los procesos:

  public synchronized void continueStage()
    {
        try
        {
            switch (_stage)
            {
                case 0: // Inicialiamos el workspace
                    new ProcessExecuter("cmd /c git init", _root).start();
                    break;

                case 1: // Config workspace
                    new ProcessExecuter("cmd /c git config --global user.email = \"ouro_17@hotmail.com\"", _root).start();
                    break;

                case 2: // Config workspace
                    new ProcessExecuter("cmd /c git config --global user.name \"Antonio Vidal Carrasco\"", _root).start();
                    break;

                case 3: // Inicializamos el repositorio
                    new ProcessExecuter("cmd /c git --bare init", _dirGit).start();
                    break;

                case 4: // Configuramos

                    if (_okLecture != null && _okLecture.contains("Reinitialized existing shared Git repository in"))
                    {
                        // Ya esta configurado, saltamos al 6
                        _stage = 6;
                        continueStage();
                    }
                    else
                    {
                        new ProcessExecuter("cmd /c git config core.sharedrepository 1", _dirGit).start();
                    }
                    break;

                case 5: // Siguiente config
                    new ProcessExecuter("cmd /c git config receive.denyNonFastforwards true", _dirGit).start();
                    break;

                case 6: // Ligamos con el repositorio(puede que ya lo esté, ignoramos el error)
                    new ProcessExecuter("cmd /c git remote add origin " + _dirGit, _root).start();

                    break;

                case 7:
                {
                    File file = new File(_root + "\\.git\\index.lock");
                    if (file.exists())
                    {
                        file.delete();
                    }
                }
                _stage = 8;
                continueStage();
                break;

                case 8: // Estado durmiente. Vendremos aquí cada vez que no haya que hacer nada.
                {
                    File file = new File(_root + "\\.git\\index.lock");
                    if (file.exists())
                    {
                        file.delete();
                    }
                }
                TreePath parent = jTree1.getPathForRow(0);

                Enumeration paths = jTree1.getExpandedDescendants(parent);

                _fileBrowser = new FileBrowser(_root);
                jTree1.setModel(_fileBrowser);

                while (paths.hasMoreElements())
                {
                    jTree1.expandPath(paths.nextElement());
                }

                break;

                case 9: // Add
                    for (String s : _listClicked)
                    {
                        new ProcessExecuter("cmd /c git add \"" + s + "\"", _root).start();
                    }
                    _adding = true;

                    break;

                case 10: // Respuesta de add
                    _stage = 8;
                    continueStage();
                    break;

                case 11: // Commit
                {
                    File file = new File(_root + "\\.git\\index.lock");
                    if (file.exists())
                    {
                        file.delete();
                    }
                }
                VentanaCommit.main(this);
                break;
                case 12: // Respuesta de commit
                    _stage = 8;
                    continueStage();
                    break;

                case 13: // Push
                    new ProcessExecuter("cmd /c git push origin master", _root).start();
                    _commiting = false;
                    break;

                case 14: // Respuesta push
                    _stage = 8;
                    continueStage();
                    break;

                case 15: // Pull
                    new ProcessExecuter("cmd /c git pull origin master", _root).start();
                    break;

                case 16: // Respuesta de pull
                    _stage = 8;
                    continueStage();
                    break;

                case 17: // descartar
                    new ProcessExecuter("cmd /c git checkout -- .", _root).start();
                    break;

                case 18: // Respuesta de descartar
                    _stage = 8;
                    continueStage();
                    break;

                case 19: // Clone: NO FUNCIONA BIEN NO USAR
                    if (!isGitDirectory())
                    {
                        VentanaConfirmacion.main(_ventana, 0);
                    }
                    new ProcessExecuter("cmd /c git clone " + _dirGit, _root).start();

                    break;

                case 20: // Respuesta de clone
                    _stage = 8;
                    continueStage();
                    break;

                case 21: // Continue commit all
                {
                    File file = new File(_root + "\\.git\\index.lock");
                    if (file.exists())
                    {
                        file.delete();
                    }
                }
                new ProcessExecuter("cmd /c git commit -a -m \"" + _mensaje + "\"", _root).start();
                break;
                case 22: // Respuesta al commit all
                    _stage = 8;
                    continueStage();
                    break;

                default:
                    _stage = 8;
                    continueStage();
            }
        }
        catch (NullPointerException e)
        {
        }
    }
Anuncios

Acciones y más acciones

Hoy nos vamos a ocupar de las “Action” de Java aplicadas a Swing.
Las Action nos ayudan a realizar, valga la redundancia, acciones sobre distintos elementos Swing. Vamos a concentrarnos en entrada de teclado.

Primer ejemplo, lectura del carácter ESCAPE para cerrar nuestra aplicación.

Para empezar deberíamos tener la ventana principal(el JFrame sobre el que trabajamos) en un SingletonHolder.

private static VentanaPrincipal _ventana;
public static VentanaPrincipal getInstance(){ return _ventana;}

De esta manera podremos acceder a él más adelante.

Ahora vamos a asignar 2 valores al panel principal de nuestra ventana. En el constructor de nuestra clase deberíamos hacer:

        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "CERRAR");
        getRootPane().getActionMap().put("CERRAR", new AbstractAction()
        {
            @Override
            public void actionPerformed(ActionEvent ae)
            {
                _ventana.dispose();
            }
        });

Esto hará que cuando pulsemos ESCAPE se cierre la ventana. Notar que el 0 en

put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0)

sirve para controlar el pulsado de teclas como ALT, CONTROL y SHIFT

Ahora voy a exponer otro ejemplo, abrir otra ventana con la combinación Control + K:

        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).
                put(KeyStroke.getKeyStroke(KeyEvent.VK_K,
                    KeyEvent.CTRL_DOWN_MASK), "KONSOLE");
        getRootPane().getActionMap().put("KONSOLE", new AbstractAction()
        {
            @Override
            public void actionPerformed(ActionEvent ae)
            {
                _ventanaKonsole.setVisible(!_ventanaKonsole.isVisible());

            }
        });

Ahora siempre que pulsemos la combinación de teclas la nueva ventana se abrirá o cerrará.

¿Qué más cosas podemos hacer con las Action? Podemos usarla como Binding Keys.
Para ello tenemos que tener creado un boton:

Button jb1 = new JButton();

Una vez que lo hemos creado vamos a crear y asociar una acción.
Primero creamos una clase que implemente nuestra acción:

    private class addAction
            extends AbstractAction
    {
        public addAction(String text, String desc, Integer mnemonic)
        {
            super(text);
            putValue(SHORT_DESCRIPTION, desc);
            putValue(MNEMONIC_KEY, mnemonic);
        }

        public void actionPerformed(ActionEvent e)
        {
            System.out.println("Add action is running");
        }
    }

Se puede observar que le añadido más cosas que antes a esta nueva Action. Le he añadido una regla mnemotécnica. Esto lo que hará será que podamos “pulsar” el botón usando la combianción “ALT + TECLA”, además de subrayar la letra asociada a la tecla si esta diponible(ahora entederás esto).

Por último creamos y asociamos la Action:

 addAction _addAction = new addAction("Add",
               "Marca un archivo para ser subido al repositorio.", new Integer(KeyEvent.VK_A));
jb1.addAction(_addAction);

Si os fijais la letra “A” será la tecla para la combinación “ALT + TECLA” antes mencionada, por lo que en nuestro botón la letra A estará subrayada.
También podemos crear la Action primero y luego usar el constructor de JButton para asociarlo:

JButton jb1 = new JButton(_addAction);

Espero que os haya servido de ayuda.