[JAVA/JsCh] Un client SSH pour Java

La mise en place d’un client SSH robuste pour Java n’est pas aussi simple qu’il n’y parait.

Il existe bon nombre d’API, mais peu de comparatifs. Qui plus est, les documentations sont, de manière générale, peu explicites.

Je vais vous fournir ici un client SSH prêt à être utilisé dans tout type de projet. Il est basé sur l’API JsCh (qui à mon avis est la plus rapide à prendre en main) ainsi qu’une classe utilitaire de Apache permettant d’implémenter l’échange de fichiers avec le protocole SCP (une petite présentation ici).

Ci-dessous donc :

  • la classe SSHClient qui contient  l’essentiel du code fonctionnel pour se connecter à un hôte SSH et dialoguer (envoi de commandes…)
  • la classe Apache ajoutant la couche Scp pour l’échange de fichiers

Il ne vous reste plus qu’à modifier, à l’intérieur de la classe SSHClient, les logins pour accéder à la machine distante.

Ce code est bien entendu libre et peut être modifié à volonté.

SSHClient.java

package com;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;

public class SSHClient {

    /** Channel modes */
    public static final String  EXEC_MODE               = "exec";

    public static final String  SHELL_MODE              = "shell";

    public static final String  SFTP_MODE               = "sftp";

    /** Authentication */
    private static String       USER;

    private static String       PWD;

    private static String       HOST;

    /** Utils */
    private String              CMD_INVITE;

    private PipedInputStream    fromServer;

    private OutputStream        toServer;

    private Channel             channel;

    private Session             session;

    private static final String TERMINATOR              = "zDonez";

    private String              lastCommand             = "";

    private Pattern             alphaNumeric            = Pattern
                                                            .compile("([^a-zA-z0-9])");

    private int                 nbChannel               = 0;

    private String              previousMode;

    private int                 SERVER_RESPONSE_TIMEOUT = 1500;

    /** Low-level layer to provide ssh functions */
    private Scp                 scp;

    /** Shell */
    private JSch                shell;

    public SSHClient() throws Exception {
        shell = new JSch();
        USER = "test";
        PWD = "test";
        HOST = "test";
        CMD_INVITE = HOST + ":" + USER + "#";
        initSession();
    }

    /**
     * Init SSH session
     * @throws Exception 
     * @throws JSchException
     */
    private void initSession() throws Exception {
        try {
            session = shell.getSession(USER, HOST, 22);
            MyUserInfo ui = new MyUserInfo();
            ui.setPassword(PWD);
            session.setUserInfo(ui);

            Properties config = new Properties();
            config.put("StrictHostKeyChecking", "no");
            session.setConfig(config);

            session.connect();
            // session.connect(30000); // making a connection with timeout.
            scp = new Scp(session);
        } catch (JSchException e) {
            e.printStackTrace();
            LOGGER.error(e);
            throw new Exception(e.getLocalizedMessage());
        }
    }

    /**
     * Connects to remote machine
     * @param mode Type of channel to open : can be EXEC_MODE, SHELL_MODE, SFTP_MODE
     * @throws Exception 
     * @throws JSchException
     * @throws IOException
     * @throws InterruptedException
     */
    private void connect(String mode) throws Exception {

        if (!isConnected()) {
            try {
                channel = session.openChannel(mode);
                previousMode = mode;
                // If Shell mode, customize I/O streams to get response
                if (mode.equals(SHELL_MODE)) {
                    initShellMode();
                    channel.connect();
                }
            } catch (Exception e) {
                e.printStackTrace();
                LOGGER.error(e);
                throw new Exception(e.getLocalizedMessage());
            }
        } else {
            // To avoid 10 files upload limitation in session
            if (nbChannel == 9) {
                disconnect();
                nbChannel = 0;
                connect(mode);
            }
            // Change channel
            if (!previousMode.equals(mode)) {
                previousMode = mode;
                channel.disconnect();
                connect(mode);
            }
            nbChannel++;
        }
    }

    /**
     * Init shell mode
     * @throws IOException
     */
    private void initShellMode() throws IOException {
        PipedOutputStream po = new PipedOutputStream();
        fromServer = new PipedInputStream(po);
        channel.setOutputStream(po);

        toServer = new PipedOutputStream();
        PipedInputStream pi = new PipedInputStream((PipedOutputStream) toServer);
        channel.setInputStream(pi);
    }

    /**
     * Check if client is connected
     * @return YES or NO
     */
    public boolean isConnected() {
        return (channel != null && channel.isConnected());
    }

    /**
     * Disconnect channel and session
     */
    public void disconnect() {
        if (isConnected()) {
            channel.disconnect();
            session.disconnect();
        }
    }

    /**
     * Disconnect client and open new session
     * @throws Exception 
     */
    public void reconnect() throws Exception {
        disconnect();
        initSession();
    }

    /**
     * Send simple SHELL command
     * @param command
     * @return server response
     * @throws Exception 
     * @throws IOException
     * @throws InterruptedException
     */
    public String sendShell(String command) throws Exception {
        connect(SHELL_MODE);
        lastCommand = command;
        command += "; echo \"" + TERMINATOR + "\" \n";
        try {
            toServer.write(command.getBytes());
            // To ensure server answer reception
            // and manage a communication time out.
            try {
                Thread.sleep(SERVER_RESPONSE_TIMEOUT);
            } catch (InterruptedException e) {
            }
        } catch (IOException e) {
            e.printStackTrace();
            LOGGER.error(e);
            throw new Exception(e.getLocalizedMessage());
        }
        return getServerResponse();
    }

    /**
     * Retrieve server response to SHELL command
     * @return the response
     * @throws Exception 
     * @throws IOException
     * @throws InterruptedException
     */
    private String getServerResponse() throws Exception {
        StringBuffer builder = new StringBuffer();
        String result = null;
        try {
            int count = 0;
            String line = "";

            BufferedReader reader = new BufferedReader(new InputStreamReader(
                fromServer));
            if (reader.ready()) {
                for (int i = 0; true; i++) {
                    try {
                        line = reader.readLine();
                    } catch (IOException e) {
                        LOGGER.warn("Communication seems to be closed...");
                        break;
                    }
                    builder.append(line).append("\n");
                    if (line.indexOf(TERMINATOR) != -1 && (++count > 1)) {
                        break;
                    }
                }
                result = builder.toString();
                int beginIndex = result.indexOf(TERMINATOR + "\"")
                    + ((TERMINATOR + "\"").length());
                result = result.substring(beginIndex);
                result = result.replaceAll(escape(TERMINATOR), "").trim();
                System.out.println("Command : " + lastCommand + " -> Result : "
                    + result);
                return result;
            } else {
                throw new Exception("Server did not answer in the time ("
                    + SERVER_RESPONSE_TIMEOUT + "ms) to command ("
                    + lastCommand + ")");
            }
        } catch (IOException e) {
            e.printStackTrace();
            LOGGER.error(e);
            throw new Exception(e.getLocalizedMessage());
        }
    }

    private String escape(String subjectString) {
        return alphaNumeric.matcher(subjectString).replaceAll("\\\\$1");
    }

    /**
     * Execute remote script
     * @param scriptPath
     * @throws Exception
     */
    public String executeScript(String scriptPath, String[] params)
        throws Exception {
        String commandLine = scriptPath;
        for (String p : params) {
            commandLine += " " + p;
        }
        return sendShell(commandLine);
    }

    /**
     * Send a file on server (implementation of exec 'scp -t rfile' command)
     * @param localFilePath
     * @param remoteFilePath
     * @throws Exception
     */
    public void sendFile(String localFilePath, String remoteFilePath)
        throws Exception {
        String remoteTargetDir = remoteFilePath.substring(0, remoteFilePath
            .lastIndexOf("/") + 1);
        String remoteTargetName = remoteFilePath.substring(remoteTargetDir
            .length());
        if (nbChannel == 9) {
            nbChannel = 0;
            reconnect();
        }
        nbChannel++;
        scp.put(localFilePath, remoteTargetDir, remoteTargetName, "0777");
    }

    /**
     * Get files 
     * @param remotePath
     * @param localDestinationPath
     * @throws Exception 
     * @throws IOException 
     */
    public List getResultsFiles(String remotePath,
        String localDestinationPath) throws Exception {
        List filesPaths = new ArrayList();
        // To ensure well connection
        reconnect();
        // List files
        String list = sendShell("ls " + remotePath);
        if (list != null) {
            if (list.indexOf(CMD_INVITE) != -1) {
                list = list.substring(list.indexOf(CMD_INVITE)
                    + CMD_INVITE.length());
            }
            if (list.indexOf("not found") != -1) {
                throw new Exception(
                    "No output files!");
            }
            List files = getFilesList(list);
            if (files.size() > 0) {
                File tmp = null;
                for (String f : files) {
                    tmp = new File(localDestinationPath, f);
                    if (tmp.exists()) {
                        tmp.delete();
                    }
                    if (nbChannel == 9) {
                        nbChannel = 0;
                        reconnect();
                    }
                    nbChannel++;
                    String absPath = tmp.getAbsolutePath();
                    filesPaths.add(absPath);
                    scp.get(remotePath + f, absPath);
                }
                return filesPaths;
            } else {
                throw new Exception(
                    "No output files!");
            }
        } else {
            throw new Exception(
                "Impossible to retreive files list!");
        }
    }

    /**
     * Compute file list received from server ("ls" command) to a list of files
     * @param lsResult
     * @return
     */
    private List getFilesList(String lsResult) {
        List files = new ArrayList();
        String[] tabl = lsResult.split("\\n");
        for (String s : tabl) {
            s = s.trim();
            if (!"".equals(s)) {
                if (s.indexOf(" ") == -1) {
                    files.add(s);
                } else {
                    String[] tabl2 = s.split(" ");
                    for (String s2 : tabl2) {
                        s2 = s2.trim();
                        if (!"".equals(s2)) {
                            files.add(s2);
                        }
                    }
                }
            }
        }
        return files;
    }

    /**
     * Move command shell to specified directory
     * @param path
     * @throws Exception
     */
    public void moveToDir(String path) throws Exception {
        sendShell("cd " + path);
    }

    /**
     * Local class that implements logging credentials
     */
    private static class MyUserInfo implements UserInfo {

        private String password;

        public void setPassword(String password) {
            this.password = password;

        }

        public String getPassphrase() {
            return null;
        }

        public String getPassword() {
            return password;
        }

        public boolean promptPassword(String arg0) {
            return true;
        }

        public boolean promptPassphrase(String arg0) {
            return true;
        }

        public boolean promptYesNo(String arg0) {
            return true;
        }

        public void showMessage(String arg0) {
            System.out.println(arg0);
        }
    }
}

SCP.java

/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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 com;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.apache.log4j.Logger;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

/**
 * This class is using the scp client to transfer data and information for the repository. 
 * 

* It is based on the SCPClient from the ganymed ssh library from Christian Plattner, * released under a BSD style license. *

* To minimize the dependency to the ssh library and because we needed some additional * functionality, we decided to copy'n'paste the single class rather than to inherit or * delegate it somehow. *

* Nevertheless credit should go to the original author. */ public class Scp { private static final int MODE_LENGTH = 4; private static final int SEND_FILE_BUFFER_LENGTH = 40000; private static final int SEND_BYTES_BUFFER_LENGTH = 512; private static final int MIN_TLINE_LENGTH = 8; private static final int CLINE_SPACE_INDEX2 = 5; private static final int CLINE_SPACE_INDEX1 = 4; private static final int MIN_C_LINE_LENGTH = 8; private static final int DEFAULT_LINE_BUFFER_LENGTH = 30; private static final int BUFFER_SIZE = 64 * 1024; /* * Maximum length authorized for scp lines. * This is a random limit - if your path names are longer, then adjust it. */ private static final int MAX_SCP_LINE_LENGTH = 8192; private Session session; public class FileInfo { private String filename; private long length; private long lastModified; /** * @param filename * The filename to set. */ public void setFilename(String filename) { this.filename = filename; } /** * @return Returns the filename. */ public String getFilename() { return filename; } /** * @param length * The length to set. */ public void setLength(long length) { this.length = length; } /** * @return Returns the length. */ public long getLength() { return length; } /** * @param lastModified * The lastModified to set. */ public void setLastModified(long lastModified) { this.lastModified = lastModified; } /** * @return Returns the lastModified. */ public long getLastModified() { return lastModified; } } public Scp(Session session) { if (session == null) { throw new IllegalArgumentException("Cannot accept null argument!"); } this.session = session; } private void readResponse(InputStream is) throws IOException, Exception { if (is.available() > 0) { int c = is.read(); if (c == 0) { return; } if (c == -1) { throw new Exception("Remote scp terminated unexpectedly."); } if ((c != 1) && (c != 2)) { throw new Exception("Remote scp sent illegal error code."); } if (c == 2) { throw new Exception("Remote scp terminated with error."); } String err = receiveLine(is); throw new Exception("Remote scp terminated with error (" + err + ")."); } } private String receiveLine(InputStream is) throws IOException, Exception { StringBuffer sb = new StringBuffer(DEFAULT_LINE_BUFFER_LENGTH); while (true) { if (sb.length() > MAX_SCP_LINE_LENGTH) { throw new Exception("Remote scp sent a too long line"); } int c = is.read(); if (c < 0) { throw new Exception("Remote scp terminated unexpectedly."); } if (c == '\n') { break; } sb.append((char) c); } return sb.toString(); } private void parseCLine(String line, FileInfo fileInfo) throws Exception { /* Minimum line: "xxxx y z" ---> 8 chars */ long len; if (line.length() < MIN_C_LINE_LENGTH) { throw new Exception( "Malformed C line sent by remote SCP binary, line too short."); } if ((line.charAt(CLINE_SPACE_INDEX1) != ' ') || (line.charAt(CLINE_SPACE_INDEX2) == ' ')) { throw new Exception( "Malformed C line sent by remote SCP binary."); } int lengthNameSep = line.indexOf(' ', CLINE_SPACE_INDEX2); if (lengthNameSep == -1) { throw new Exception( "Malformed C line sent by remote SCP binary."); } String lengthSubstring = line.substring(CLINE_SPACE_INDEX2, lengthNameSep); String nameSubstring = line.substring(lengthNameSep + 1); if ((lengthSubstring.length() <= 0) || (nameSubstring.length() <= 0)) { throw new Exception( "Malformed C line sent by remote SCP binary."); } if ((CLINE_SPACE_INDEX2 + 1 + lengthSubstring.length() + nameSubstring .length()) != line.length()) { throw new Exception( "Malformed C line sent by remote SCP binary."); } try { len = Long.parseLong(lengthSubstring); } catch (NumberFormatException e) { throw new Exception( "Malformed C line sent by remote SCP binary, cannot parse file length."); } if (len < 0) { throw new Exception( "Malformed C line sent by remote SCP binary, illegal file length."); } fileInfo.setLength(len); fileInfo.setFilename(nameSubstring); } private void parseTLine(String line, FileInfo fileInfo) throws Exception { /* Minimum line: "0 0 0 0" ---> 8 chars */ long modtime; long firstMsec; long atime; long secondMsec; if (line.length() < MIN_TLINE_LENGTH) { throw new Exception( "Malformed T line sent by remote SCP binary, line too short."); } int firstMsecBegin = line.indexOf(" ") + 1; if (firstMsecBegin == 0 || firstMsecBegin >= line.length()) { throw new Exception( "Malformed T line sent by remote SCP binary, line not enough data."); } int atimeBegin = line.indexOf(" ", firstMsecBegin + 1) + 1; if (atimeBegin == 0 || atimeBegin >= line.length()) { throw new Exception( "Malformed T line sent by remote SCP binary, line not enough data."); } int secondMsecBegin = line.indexOf(" ", atimeBegin + 1) + 1; if (secondMsecBegin == 0 || secondMsecBegin >= line.length()) { throw new Exception( "Malformed T line sent by remote SCP binary, line not enough data."); } try { modtime = Long.parseLong(line.substring(0, firstMsecBegin - 1)); firstMsec = Long.parseLong(line.substring(firstMsecBegin, atimeBegin - 1)); atime = Long.parseLong(line.substring(atimeBegin, secondMsecBegin - 1)); secondMsec = Long.parseLong(line.substring(secondMsecBegin)); } catch (NumberFormatException e) { LOGGER.error(e); throw new Exception( "Malformed C line sent by remote SCP binary, cannot parse file length."); } if (modtime < 0 || firstMsec < 0 || atime < 0 || secondMsec < 0) { throw new Exception( "Malformed C line sent by remote SCP binary, illegal file length."); } fileInfo.setLastModified(modtime); } private void sendFile(Channel channel, String localFile, String remoteName, String mode) throws IOException, Exception { byte[] buffer = new byte[BUFFER_SIZE]; OutputStream os = new BufferedOutputStream(channel.getOutputStream(), SEND_FILE_BUFFER_LENGTH); InputStream is = new BufferedInputStream(channel.getInputStream(), SEND_BYTES_BUFFER_LENGTH); try { if (channel.isConnected()) { channel.start(); } else { channel.connect(); } } catch (JSchException e1) { throw (IOException) new IOException("Channel connection problems") .initCause(e1); } readResponse(is); File f = new File(localFile); long remain = f.length(); String cMode = mode; if (cMode == null) { cMode = "0600"; } String cline = "C" + cMode + " " + remain + " " + remoteName + "\n"; os.write(cline.getBytes()); os.flush(); readResponse(is); FileInputStream fis = null; try { fis = new FileInputStream(f); while (remain > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { } int trans; if (remain > buffer.length) { trans = buffer.length; } else { trans = (int) remain; } if (fis.read(buffer, 0, trans) != trans) { throw new IOException("Cannot read enough from local file " + localFile); } os.write(buffer, 0, trans); remain -= trans; } fis.close(); } catch (Exception e) { if (fis != null) { fis.close(); } LOGGER.error(e); throw new Exception(e); } os.write(0); os.flush(); readResponse(is); os.write("E\n".getBytes()); os.flush(); } /** * Receive a file via scp and store it in a stream * * @param channel * ssh channel to use * @param file * to receive from remote * @param target * to store file into (if null, get only file info) * @return file information of the file we received * @throws IOException * in case of network or protocol trouble * @throws Exception * in case of problems on the target system (connection is fine) */ private FileInfo receiveStream(Channel channel, String file, OutputStream targetStream) throws IOException, Exception { byte[] buffer = new byte[BUFFER_SIZE]; OutputStream os = channel.getOutputStream(); InputStream is = channel.getInputStream(); try { if (channel.isConnected()) { channel.start(); } else { channel.connect(); } } catch (JSchException e1) { throw (IOException) new IOException("Channel connection problems") .initCause(e1); } os.write(0x0); os.flush(); FileInfo fileInfo = new FileInfo(); while (true) { int c = is.read(); if (c < 0) { throw new Exception("Remote scp terminated unexpectedly."); } String line = receiveLine(is); if (c == 'T') { parseTLine(line, fileInfo); os.write(0x0); os.flush(); continue; } if ((c == 1) || (c == 2)) { throw new Exception("Remote SCP error: " + line); } if (c == 'C') { parseCLine(line, fileInfo); break; } throw new Exception("Remote SCP error: " + ((char) c) + line); } if (targetStream != null) { os.write(0x0); os.flush(); try { long remain = fileInfo.getLength(); while (remain > 0) { int trans; if (remain > buffer.length) { trans = buffer.length; } else { trans = (int) remain; } int thisTimeReceived = is.read(buffer, 0, trans); if (thisTimeReceived < 0) { throw new IOException( "Remote scp terminated connection unexpectedly"); } targetStream.write(buffer, 0, thisTimeReceived); remain -= thisTimeReceived; } targetStream.close(); } catch (IOException e) { if (targetStream != null) { targetStream.close(); } LOGGER.error(e); throw (e); } readResponse(is); os.write(0x0); os.flush(); } return fileInfo; } /** * @return * @throws JSchException */ private ChannelExec getExecChannel() throws JSchException { ChannelExec channel; channel = (ChannelExec) session.openChannel("exec"); return channel; } /** * Copy a local file to a remote site, uses the specified mode when creating the file on the * remote side. * * @param localFile * Path and name of local file. Must be absolute. * @param remoteTargetDir * Remote target directory where the file has to end up (optional) * @param remoteTargetName * file name to use on the target system * @param mode * a four digit string (e.g., 0644, see "man chmod", "man open") * @throws IOException * in case of network problems * @throws Exception * in case of problems on the target system (connection ok) */ public void put(String localFile, String remoteTargetDir, String remoteTargetName, String mode) throws Exception { ChannelExec channel = null; if ((localFile == null) || (remoteTargetName == null)) { throw new IllegalArgumentException("Null argument."); } if (mode != null) { if (mode.length() != MODE_LENGTH) { throw new IllegalArgumentException("Invalid mode."); } for (int i = 0; i < mode.length(); i++) { if (!Character.isDigit(mode.charAt(i))) { throw new IllegalArgumentException("Invalid mode."); } } } String cmd = "scp -t "; if (mode != null) { cmd = cmd + "-p "; } if (remoteTargetDir != null && remoteTargetDir.length() > 0) { cmd = cmd + "-d " + remoteTargetDir; } try { channel = getExecChannel(); channel.setCommand(cmd); sendFile(channel, localFile, remoteTargetName, mode); channel.disconnect(); } catch (JSchException e) { if (channel != null) { channel.disconnect(); } e.printStackTrace(); LOGGER.error(e); throw new Exception("Error during SCP transfer." + e.getMessage()); } catch (Exception e) { e.printStackTrace(); LOGGER.error(e); throw new Exception(e.getLocalizedMessage()); } } /** * Download a file from the remote server to a local file. * * @param remoteFile * Path and name of the remote file. * @param localTarget * Local file where to store the data. Must be absolute. * @throws IOException * in case of network problems * @throws Exception * in case of problems on the target system (connection ok) */ public void get(String remoteFile, String localTarget) throws Exception { try { File f = new File(localTarget); FileOutputStream fop = new FileOutputStream(f); get(remoteFile, fop); } catch (IOException e) { e.printStackTrace(); LOGGER.error(e); throw new Exception(e.getLocalizedMessage()); } } /** * Download a file from the remote server into an OutputStream * * @param remoteFile * Path and name of the remote file. * @param localTarget * OutputStream to store the data. * @throws IOException * in case of network problems * @throws Exception * in case of problems on the target system (connection ok) */ private void get(String remoteFile, OutputStream localTarget) throws IOException, Exception { ChannelExec channel = null; if ((remoteFile == null) || (localTarget == null)) { throw new IllegalArgumentException("Null argument."); } String cmd = "scp -p -f " + remoteFile; try { channel = getExecChannel(); channel.setCommand(cmd); receiveStream(channel, remoteFile, localTarget); channel.disconnect(); } catch (JSchException e) { if (channel != null) { channel.disconnect(); } throw (IOException) new IOException("Error during SCP transfer." + e.getMessage()).initCause(e); } } /** * Initiates an SCP sequence but stops after getting fileinformation header * * @param remoteFile * to get information for * @return the file information got * @throws IOException * in case of network problems * @throws Exception * in case of problems on the target system (connection ok) */ public FileInfo getFileinfo(String remoteFile) throws IOException, Exception { ChannelExec channel = null; FileInfo fileInfo = null; if (remoteFile == null) { throw new IllegalArgumentException("Null argument."); } String cmd = "scp -p -f \"" + remoteFile + "\""; try { channel = getExecChannel(); channel.setCommand(cmd); fileInfo = receiveStream(channel, remoteFile, null); channel.disconnect(); } catch (JSchException e) { throw (IOException) new IOException("Error during SCP transfer." + e.getMessage()).initCause(e); } finally { if (channel != null) { channel.disconnect(); } } return fileInfo; } }


Fichier(s) joint(s) :



Eclipse (Swing) - Ajouter des composants dans un JTable

Afin d’améliorer l’ergonomie de certaines interfaces, il peut être utile d’ajouter des éléments interactifs (zones de texte, boutons radio…) directement dans les cellules d’un tableau.

Pour ce faire, l’API Swing permet de mettre rapidement en place des TableCellRenderer qui définissent le contenu des cellules. De base, une cellule est en fait un JLabel : on peut donc améliorer l’affichage en le remplaçant par une zone de texte srollable. C’est à partir de ce moment que les ennuis commencent…

Regardons tout d’abord le code nécessaire à la mise en place de cette zone de texte :

Définition du renderer personnalisé dans la dernière colonne du tableau :

Table table = new JTable() {
                    private static final long serialVersionUID = 3592907806336215067L;
                    public boolean isCellEditable(int rowIndex, int vColIndex) {
                        return (vColIndex==this.getColumnCount()-1);
                    }
                };
TableColumn col = table.getColumnModel().getColumn(resultColumnIndex);
TextAreaCellRenderer renderer = new TextAreaCellRenderer();
col.setCellRenderer(renderer);

Code du renderer :

import java.awt.Color;
import java.awt.Component;
 
import javax.swing.BorderFactory;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.table.DefaultTableCellRenderer;
 
public class TextAreaCellRenderer extends DefaultTableCellRenderer {
 
    private static final long serialVersionUID = -3987688353662913146L;
 
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
        boolean isSelected, boolean hasFocus, int row, int column) {
 
        // Customize it
        JTextArea textArea = new JTextArea(value.toString());
        textArea.setEditable(false);
        textArea.setWrapStyleWord(true);
        textArea.setLineWrap(true);
        JScrollPane pane = new JScrollPane(textArea);
        pane.setBorder(BorderFactory.createLineBorder(Color.BLACK, 0));
        pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        pane
            .setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        return pane;
    }
}

Ces quelques lignes suffisent à faire apparaître une zone de texte dans les cellules en question. Cependant, impossible d’utiliser les barres de défilement!! En effet, comme expliqué dans ce forum, Swing n’ajoute pas réellement la zone de texte dans la cellule, mais plutôt une image de celle-ci.

Pour rendre les barres de défilement actives, voici le principe à mettre en place :

Au clic sur la cellule, il faut entrer en mode édition. Ainsi, une nouvelle zone de texte sera définie comme CellEditor et sera utilisable!

Voici donc le complément de code à mettre en place :

Définition de l’éditeur personnalisé :

Table table = new JTable() {
                    private static final long serialVersionUID = 3592907806336215067L;
                    public boolean isCellEditable(int rowIndex, int vColIndex) {
                        return (vColIndex==this.getColumnCount()-1);
                    }
                };
TableColumn col = table.getColumnModel().getColumn(resultColumnIndex);
TextAreaCellRenderer renderer = new TextAreaCellRenderer();
TextAreaCellEditor editor = new TextAreaCellEditor();
col.setCellEditor(editor);
col.setCellRenderer(renderer);

Code de l’éditor :

import java.awt.Color;
import java.awt.Component;
 
import javax.swing.AbstractCellEditor;
import javax.swing.BorderFactory;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.table.TableCellEditor;
 
public class TextAreaCellEditor extends AbstractCellEditor implements
    TableCellEditor {
 
    private static final long serialVersionUID = -9030406143699523582L;
 
    private String editingValue;
 
    @Override
    public Component getTableCellEditorComponent(JTable table, Object value,
        boolean isSelected, int row, int column) {
 
        editingValue = value.toString();
 
        // Customize it
        JTextArea textArea = new JTextArea(valueStr);
        textArea.setEditable(false);
        textArea.setWrapStyleWord(true);
        textArea.setLineWrap(true);
        JScrollPane pane = new JScrollPane(textArea);
        pane.setBorder(BorderFactory.createLineBorder(Color.BLACK, 0));
        pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        pane
            .setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        return pane;
    }
 
    @Override
    public Object getCellEditorValue() {
        return editingValue;
    }
 
    @Override
    public void cancelCellEditing() {
        super.cancelCellEditing();
    }
}

Maintenant, le contenu de la cellule est défilant.


Fichier(s) joint(s) :



Eclipse (PDE) - Gérer les activités

Lors du développement d’applications Éclipse, on peut rapidement être confronté à la gestion des droits d’accès utilisateurs sur certaines fonctionnalités.

Heureusement, cela peut facilement être géré par les Activités : vous trouverez un très bon guide dans la documentation officielle, mais pour résumer, on peut dire qu’une activité est un groupe de fonctionnalités régies par des règles d’utilisation ou d’accès.

Pour créer ces règles, dans le fichier "plugin.xml", il faut utiliser le point d’extension

org.eclipse.ui.activities

C’est ensuite le "pattern" qui définira la règle à appliquer : sous forme d’expression régulière, son format est le suivant :

plug-in-identifier + "/" + local-identifier

Voici un exemple :

Dans le fichier "plugin.xml", on définit l’activité suivante : 

 Puis on lui affecte un pattern :

Ainsi, nous avons défini la règle suivante : le profil "developer" n’aura accès qu’aux fonctionnalités du plugin

com.client

dont le nom contient

ui.commands.developer

Pour faire appliquer cette règle à l’IDE, voici le code à implémenter :

public class ProfileManager {
    private static final String ACTIVITY_DEVELOPER_ID    = "com.client.ui.activities.profile.developer";
    public static void refreshGUIAccordingToProfile(){
        IWorkbenchActivitySupport activitySupport = PlatformUI.getWorkbench()
            .getActivitySupport();
        // List of activities to be activated
        Set enabledActivities = new HashSet();
 
        // Retrieve list of activities currently activated
        Set currentlyEnabledActivities = PlatformUI.getWorkbench()
            .getActivitySupport().getActivityManager().getEnabledActivityIds();
        // Remove activities which are related to user's profile
        // I.e. Keep activities activated by user via Windows > Preferences
        for (String anActivity : currentlyEnabledActivities) {
            if (!isAProfileRelatedActivity(anActivity)) {
                enabledActivities.add(anActivity);
            }
        }
        enabledActivities.add(ACTIVITY_DEVELOPER_ID);
        activitySupport.setEnabledActivityIds(enabledActivities);
   }
}

Ce code est à appeler au lancement du plugin dans l’IDE : il fait appel simplement à l’interface de gestion des activités par défaut et y ajoute celle que nous venons de créer!

Vous disposer maintenant de tout le nécessaire pour paramétrer l’interface de votre application selon différent profils utilisateurs.


Fichier(s) joint(s) :

Eclipse (SWT) : sélectionner une cellule dans une Table

Cela pourrait paraître enfantin et pourtant, on peut plutôt y perdre quelques cheveux!! Attention, je parle pas ici des JTable de swing qui sont facilement utilisables et extensibles, mais bien de l’objet simpliste Table de SWT.

Le principe basique des Tables est d’afficher une liste de données en ligne (les colonnes représentent les attributs). Le constructeur de l’objet permet facilement d’autoriser la sélection d’une ligne entière, mais rien ne permet d’activer à l’aide de la souris la sélection d’une cellule : il faut pour cela un peu d’imagination. Jetons d’abord un œil au code avant de le commenter :

TableViewer tableViewer = new TableViewer(parent, SWT.MULTI | SWT.H_SCROLL
            | SWT.V_SCROLL | SWT.FULL_SELECTION);
tableViewer.getTable().addListener(SWT.MouseDown, new Listener() {
            TableItem item = null;
            int       oldi = -1;
            public void handleEvent(Event event) {
                Point pt = new Point(event.x, event.y);
                if (item != null && !item.isDisposed()) {
                    item.setBackground(oldi, Display.getCurrent()
                        .getSystemColor(SWT.COLOR_WHITE));
                    item.setForeground(oldi, Display.getCurrent()
                        .getSystemColor(SWT.COLOR_BLACK));
                }
                item = table.getItem(pt);
                if (item == null)
                    return;
                for (int i = 0; i < table.getColumnCount(); i++) {
                    Rectangle rect = item.getBounds(i);
                    if (rect.contains(pt)) {
                        table.deselectAll();
                        item.setBackground(i, Display.getCurrent()
                            .getSystemColor(SWT.COLOR_LIST_SELECTION));
                        item.setForeground(i, Display.getCurrent()
                            .getSystemColor(SWT.COLOR_WHITE));
                        oldi = i;
                        int index = table.indexOf(item);
                        System.out.println("Sélection de la ligne " + index + " - colonne " + i + " : valeur '"+ item.getText(i) +"'");
                    }
                }
            }
        });
    }

Le principe est donc le suivant :

Lorsque l’utilisateur clic dans le tableau, on récupère la position x et y du clic et on lui demande de nous retourner l’élément situé sous la souris (l’objet item). Pas terrible n’est-ce pas?!

Au niveau du code on a donc bien récupéré notre objet, mais au niveau visuel, c’est toute la ligne du tableau qui est sélectionnée! Pour y remédier, on dé-sélectionne immédiatement tout, puis on affecte à notre cellule une couleur d’arrière-plan correspondant à la couleur d’un élément sélectionné… On garde en mémoire notre cellule pour lui redonner des couleurs normales lorsqu’une autre sera clicquée.

Je vous l’accorde, c’est du bricolage, mais l’objet Table de SWT est vraiment très basique et c’est le seul moyen d’arriver à ce résultat ( à moins que je ne me trompe…)


Fichier(s) joint(s) :



Eclipse (PDE) - Gérer les associations de fichiers

Cet article ouvre sur une nouvelle série de tutoriels consacrée au développement de plugins pour Eclipse.

L’environnement de développement utilisé est Eclipse RCP (l’IDE "par défaut" sans aucun plugin) et PDE (Plugin Environment Developement).

Je ne ferais pas de tutoriel complet sur la mise en place de cet environnement de développement, vous trouverez facilement tout ce qu’il faut sur le net, par exemple ici : http://www.eclipse.org/pde/

Ce premier sujet sera consacré aux méthodes à implémenter pour personnaliser les préférences de l’IDE.
Plus spécialement cette fois-ci, la personnalisation des associations de fichiers avec certains éditeurs. Deux cas se présentent :

L’extension de fichier considérée existe déjà dans la liste des extensions gérées dans les préférences (par défaut *.html ou *.htm) :

Dans ce cas le plus simple, le code suivant permet de modifier l’association par défaut :

IEditorRegistry registry = PlatformUI.getWorkbench().getEditorRegistry();
registry.setDefaultEditor("*.html", "org.eclipse.ui.DefaultTextEditor");
((EditorRegistry)registry).saveAssociations();

Ce code peut être implémenté dans une classe qui s’éxecute au lancement de l’IDE, ou dans tout autre classe, puisque la méthode saveAssociations() met à jour les registres.

L’extension de fichier considéré n’existe pas par défaut :

Dans ce cas, il est impossible d’ajouter cette nouvelle association au lancement de l’IDE en passant par les registres d’éditeurs.

Il faut créer un éditeur personnalisé et l’associer à la nouvelle extension à travers le fichier plugin.xml : il faut utiliser le point d’extension "org.eclipse.ui.editors" : la section extensions permet de spécifier toutes les nouvelles extensions de fichiers qui seront associées par défaut à cet éditeur.

Pour en savoir plus, un très bon tutoriel a été rédigé sur developpez.com, je vous conseille d’aller y jeter un oeil. Et pour quelques détails supplémentaires, un autre tutoriel Aptana.


Fichier(s) joint(s) :