/**
 * (C) 2020 - 2024 ScalAgent Distributed Technologies
 * All rights reserved
 */
package com.scalagent.jakartatool;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;

import jakarta.jms.BytesMessage;
import jakarta.jms.Connection;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.Destination;
import jakarta.jms.JMSException;
import jakarta.jms.MapMessage;
import jakarta.jms.Message;
import jakarta.jms.MessageProducer;
import jakarta.jms.ObjectMessage;
import jakarta.jms.StreamMessage;
import jakarta.jms.TextMessage;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.swing.JOptionPane;
import javax.swing.JPasswordField;

import org.ow2.joram.jakarta.jms.admin.AdminException;
import org.ow2.joram.jakarta.jms.admin.AdminWrapper;
import org.ow2.joram.jakarta.jms.admin.User;

import fr.dyade.aaa.common.BinaryDump;

import org.ow2.joram.jakarta.jms.Queue;
import org.ow2.joram.jakarta.jms.Session;

/**
 * This client test a Joram JMS connector using the Jakarta JMS 3 API.
 * 
 * Options, set by Java environment variable ("-Dproperty=value" in command line)
 *  - JNDI_FILE: Path of JNDI properties file. If not defined, Joram's default are
 *    used. "fr.dyade.aaa.jndi2.client.NamingContextFactory" for JNDI Factory,
 *    "localhost", and 16400 for host and port.
 *    These values can be overloaded by specific properties below.
 *  - JNDI_FACTORY: Classname of the JNDI factory (cf java.naming.factory.initial
 *    property).
 *  - JNDI_URL: URL of JNDI server (cf java.naming.provider.url property).
 *  - CF: JNDI name of the ConnectionFactory to use, by default "cf".
 *  - QUEUE: JNDI name of JMS queue to handle, by default "queue".
 *  - ADMIN: User name for authentication, by default "root.
 *  - PASS: Password for authentication, by default "root".
 *  - FORWARD: JNDI Name of queue to forward messages, no default.
 *  - MSGID: Unique identifier of message to handle, no default.
 *  - ALL: If defined true, all messages are automatically handled. With the
 *    browse command all messages are displayed, with the forward command
 *    all messages are forwarded to the new destination. If false each message
 *    is handled in an interactive way.
 *  - DUMP: If defined true, try to dump (hex+ascii) the content of Stream and
 *    Object messages.
 *  - BODY_AS_TEXT: Try to print message body as a String using a proposed
 *    charset (by default UTF-8). This option is only available with BytesMessage.
 *  - CHARSET: ALlows to fix the charset used to save TextMessage, and to
 *    display BytesMessage in text. By default UTF-8.
 *  - SILENT: If defined true, the output is minimum.
 *  
 *  Commands: help, browse, purge, forward, remove, pause_on/pause_off, create,
 *  delete and statistics.
 *   - help: prints the usage message.
 *   - browse: prints successively each message of the queue, allowing to delete
 *     or forward it. If ALL option is defined, all messages are automatically
 *     handled.
 *   - purge: removes all messages in the queue.
 *   - forward: forwards the message identified by MSGID option.
 *   - remove: removes the message identified by MSGID option.
 *   - pause_on/pause_off: pause (resp. restart) the queue.
 *   - create: creates the queue and registers it in JNDI. The rights set allow
 *     any user to send or receive messages on this queue.
 *   - delete: deletes the queue in the JMS server.
 *   - statistics: prints all statistics about the queue.
 *   - lookup: gets the registered object in JNDI (name asked on terminal).
 *   - unbind: removes the binding in JNDI (name asked on terminal).
 *   - password: updates a user password (parameters asked in command line).
 *     
 *  The client returns 0 if the command is ok, -1 otherwise.
 */
public final class Tool {
  public final static String HELP_CMD = "help";
  
  public final static String BROWSE_CMD = "browse";
  public final static String PURGE_CMD = "purge";
  
  public final static String FORWARD_CMD = "forward";
  public final static String REMOVE_CMD = "remove";
  
  public final static String CREATE_CMD = "create";
  public final static String DELETE_CMD = "delete";

  public final static String PAUSE_ON_CMD = "pause_on";
  public final static String PAUSE_OFF_CMD = "pause_off";
  public final static String STAT_CMD = "statistics";
  
  public final static String LOOKUP_CMD = "lookup";
  public final static String UNBIND_CMD = "unbind";
  
  public final static String PASSWORD_CMD = "password";
  
  public final static String ALL = "ALL";
  
  public static final String QUEUE = "QUEUE";
  public static final String QUEUE_DFLT = "queue";

  public static final String FORWARD_QUEUE = "FORWARD";

  public static final String MSGID = "MSGID";

  public static final String INPUT = "INPUT";
  public static final String GUI = "GUI";

  public static final String SILENT = "SILENT";
  public static final boolean silent = Boolean.getBoolean(SILENT);
  
  public static final String DEBUG = "DEBUG";
  public static final boolean debug = Boolean.getBoolean(DEBUG);

  public static final String DUMP = "DUMP";
  public static final boolean dump = Boolean.getBoolean(DUMP);

  /**
   * Try to print message body as a String using a proposed charset (by default UTF-8).
   * This option is only available with BytesMessage.
   */
  public static final String BODY_AS_TEXT = "BODY_AS_TEXT";
  public static final boolean bodyAsText = Boolean.getBoolean(BODY_AS_TEXT);

  /**
   * Property allowing to fix charset used to save TextMessage, and to display
   * BytesMessage in text. By default UTF-8.
   */
  public static final String CHARSET = "CHARSET";
  public static final String DEFAULT_CHARSET = "UTF-8";
  public static final String charset = System.getProperty(CHARSET, DEFAULT_CHARSET);

  public static void usage() {
    System.out.println("usage:\njava -D" + JNDI_FILE + "=./jndi.properties -DSILENT=true -D" + ALL +"=true\n" +
        "    -D" + CF + "=cf -D" + QUEUE + "=queue -D" + FORWARD_QUEUE + "=forward -jar jakartatool.jar\n" +
        "    <command> [parameters]\n");

    System.err.println("Options and parameters, set by Java environment variable (\"-Dproperty=value\" in\n" +
        "  command line):");
    System.err.println("  - " + JNDI_FILE + ": Path of JNDI properties file. If not defined, Joram's default\n" + 
        "    are used. \"fr.dyade.aaa.jndi2.client.NamingContextFactory\" for JNDI\n" +
        "    Factory, \"localhost\", and 16400 for host and port.\n" +
        "    These values can be overloaded by specific properties below.");
    System.err.println("  - " + JNDI_FACTORY + ": Classname of the JNDI factory (cf " + InitialContext.INITIAL_CONTEXT_FACTORY + "\n" +
        "    property). By default \""  + JNDI_FACTORY_DFLT + "\".");
    System.err.println("  - " + JNDI_PROVIDER_URL + ": URL of JNDI server (cf " + InitialContext.PROVIDER_URL + "). By default\n" +
        "    \"" + JNDI_PROVIDER_URL_DFLT + "\".");
    System.err.println("  - " + CF + ": JNDI name of the ConnectionFactory to use, by default \"" + CF_DFLT + "\".");
    System.err.println("  - " + QUEUE + ": JNDI name of JMS queue to handle, by default \"" + QUEUE_DFLT + "\".");
    System.err.println("  - " + ADMIN + ": User name for authentication, by default \"" + ADMIN_DFLT + ".");
    System.err.println("  - " + PASS + ": Password for authentication, by default \"" + PASS_DFLT + "\".");
    System.err.println("  - " + FORWARD_QUEUE + ": JNDI Name of queue to forward messages, no default.");
    System.err.println("  - " + MSGID + ": Unique identifier of message to handle, no default.");
    System.err.println("  - " + INPUT + ": Pathname of file containing additionnal parameters (see\n" + 
        "     Parameters section below). Each line specfies a parameter.\n" +
        "     The additional command line parameters, and \"" + GUI + "\" option\n" +
        "     will then be ignored.");
    System.err.println("  - " + GUI + ": If set to true use a GUI dialog to get password, by default false.");
    System.err.println("  - " + ALL + ": If defined true, all messages are automatically handled. With the\n" +
        "    browse command all messages are displayed, with the forward command\n" +
        "    all messages are forwarded to the new destination. If false each message\n" +
        "    is handled in an interactive way.");
    System.err.println("  - " + DUMP + ": If defined true, try to dump (hex+ascii) the content of Stream and\n" +
        "    Object messages.");
    System.err.println("  - " + BODY_AS_TEXT + ": Try to print message body as a String using a proposed\n" +
        "    charset (by default UTF-8). This option is only available with BytesMessage.");
    System.err.println("  - " + CHARSET + ": ALlows to fix the charset used to save TextMessage, and to\n"
        + "    display BytesMessage in text. By default UTF-8.");
    System.err.println("  - " + SILENT + ": If defined true, the output is minimum.");
    System.err.println("\nCommands:\n" + 
        "  - " + HELP_CMD + ": prints the usage message.\n" + 
        "  - " + BROWSE_CMD + ":prints successively each message of the queue, allowing to delete\n" + 
        "    or forward it. If ALL option is defined, all messages are automatically\n" + 
        "    handled (only bodies of TextMessage are displayed).\n" + 
        "  - " +  PURGE_CMD + ": removes all messages in the queue.\n" + 
        "  - " + FORWARD_CMD + ": forwards the message identified by MSGID option.\n" + 
        "  - " + REMOVE_CMD + ": removes the message identified by MSGID option.\n" + 
        "  - " + PAUSE_ON_CMD + '/' + PAUSE_OFF_CMD + ": pause (resp. restart) the queue.\n" + 
        "  - " + CREATE_CMD + ": creates the queue and registers it in JNDI. The rights set allow\n" + 
        "    any user to send or receive messages on this queue.\n" + 
        "  - " + DELETE_CMD + ": deletes the queue in the JMS server.\n" + 
        "  - " + STAT_CMD + " statistics: prints all statistics about the queue.\n" +
        "  - " + LOOKUP_CMD + ": gets the registered object in JNDI (name asked on terminal).\n" +
        "  - " + UNBIND_CMD + ": removes the binding in JNDI (name asked on terminal).\n" +
        "  - " + PASSWORD_CMD+ ": updates a user password (parameters asked on terminal).");
    System.err.println("\nParameters: Some commands require additional parameters normally entered by\n" + 
        "  the user at the terminal. These parameters can be given in the command line\n" +
        "  in the order requested by the command:\n" + 
        "  - " + LOOKUP_CMD + '/' + UNBIND_CMD + ": <name>\n" + 
        "    These commands can take multiple parameters, to stipulate the last\n" +
        "    parameter and avoid a request on the terminal you can use the \"\"\n" +
        "    empty parameter.\n" +
        "  - " + PASSWORD_CMD + ": <user> <password>\n" +
        "    If a parameter contains white space characters it must be surrounded by \".");
  }
  
  static Context ictx = null;
  static Connection cnx = null;
  static AdminWrapper admin = null;

  static String[] values = null;
  static int idx = 0;
  
  public static void main(String[] args) throws Exception {
    if (args.length < 1) {
      usage();
      return;
    }
    
    String input = System.getProperty(INPUT);
    if (input != null) {
      try {
        stdin = new BufferedReader(new FileReader(input));
      } catch (Exception exc) {
        System.err.println("Error opening input file: " + input);
        if (debug) exc.printStackTrace(System.err);
        System.exit(-1);
      }
      if ((stdin != null) && (args.length > 1))
        System.err.println("WARN: an input file is specified, additional command line parameters will be ignored.");
    }
    
    gui = Boolean.getBoolean(GUI);
    if ((stdin != null) && gui) {
      System.err.println("WARN: an input file is specified, \"" + GUI + "\"option will be ignored.");
      gui = false;
    }
    
    values = args;
    idx = 1;
    
    // Verify that the command is valid
    String command = args[0];
    if (!BROWSE_CMD.equals(command) &&
        !PURGE_CMD.equals(command) && !CREATE_CMD.equals(command) && !DELETE_CMD.equals(command) &&
        !FORWARD_CMD.equals(command) && !REMOVE_CMD.equals(command) &&
        !PAUSE_ON_CMD.equals(command) && !PAUSE_OFF_CMD.equals(command) &&
        !STAT_CMD.equals(command) &&
        !UNBIND_CMD.equals(command) && !LOOKUP_CMD.equals(command) &&
        !PASSWORD_CMD.equals(command)) {
      usage();
      return;
    }
    
    // Get the JNDI initial context
    try {
      ictx = getJNDIInitialContext();
    } catch (Exception exc) {
      System.err.println("Error opening JNDI initial context: " + exc.getMessage());
      if (debug) exc.printStackTrace(System.err);
      System.exit(-1);
    }
    
    if (LOOKUP_CMD.equals(command)) {
      while (true) {
        String name = readLine("name: ");
        if ((name == null) || name.isEmpty())
          break;
        lookup(name);
      }
    } else if (UNBIND_CMD.equals(command)) {
      while (true) {
        String name = readLine("name: ");
        if ((name == null) || name.isEmpty())
          break;
        unbind(name);
      }
    }
    
    // Get the JMS administration connection
    try {
      cnx = getJMSConnection(ictx);
      cnx.start();
    } catch (NamingException exc) {
      System.err.println("Error opening JNDI connection: " + exc.getMessage());
      if (debug) exc.printStackTrace(System.err);
      if (ictx != null)
        ictx.close();
      if (cnx != null)
        cnx.close();
      System.exit(-1);
    } catch (JMSException exc) {
      System.err.println("Error opening JMS connection: " + exc.getMessage());
      if (debug) exc.printStackTrace(System.err);
      if (ictx != null)
        ictx.close();
      if (cnx != null)
        cnx.close();
      System.exit(-1);
    }
    
    // Initialize the administration API wrapper
    try {
      admin = new AdminWrapper(cnx);
    } catch (Exception exc) {
      System.err.println("Error opening Administration session: " + exc.getMessage());
      if (debug) exc.printStackTrace(System.err);
      if (ictx != null)
        ictx.close();
      if (cnx != null)
        cnx.close();
      System.exit(-1);
    }

    if (PASSWORD_CMD.equals(command)) {
      try {
        String userid = readLine("user: ");
        char[] userpass = readPassword("password: ");
        if ((userpass == null) || (userpass.length == 0)) {
          System.err.println("Cannot change user passord: no password");
          System.exit(-1);
        }
        User[] users = admin.getUsers();
        User user = null;
        for (User u : users) {
          if (u.getName().equals(userid)) {
            user = u;
            break;
          }
        }
        if (user == null) {
          System.err.println("ERROR: User \"" + userid + "\" unknown.");
          System.exit(-1);
        }
        user.setWrapper(admin);
        user.update(userid, new String(userpass));
      } catch (Exception exc) {
        System.err.println("Cannot change user passord: " + exc.getMessage());
        if (debug) exc.printStackTrace(System.err);
        System.exit(-1);
      } finally {
        if (ictx != null)
          ictx.close();
        if (admin != null)
          admin.close();
        if (cnx != null)
          cnx.close();
      }
      System.exit(0);
    }
    
    // Get the queue to handle
    String qname = System.getProperty(QUEUE, QUEUE_DFLT);
    Queue queue = null;
    if (!CREATE_CMD.equals(command)) {
      try {
        queue = (Queue) getJMSDestination(ictx, qname);
      } catch (Exception exc) {
        System.err.println("Can not get the handled queue: " + exc.getMessage());
        if (debug) exc.printStackTrace(System.err);
        if (ictx != null)
          ictx.close();
        if (admin != null)
          admin.close();
        if (cnx != null)
          cnx.close();
        System.exit(-1);
      }
    }
    // Set the administration wrapper
    if (queue != null)
      queue.setWrapper(admin);
    
    // Initialize the messageProducer to forward messages if any 
    MessageProducer forwarder = null;
    try {
      String forward_qname = System.getProperty(FORWARD_QUEUE);
      if (forward_qname != null) {
        Destination forward = getJMSDestination(ictx, forward_qname);
        Session session = (Session) cnx.createSession();
        forwarder = session.createProducer(forward);
      }
    } catch (Exception exc) {
      System.err.println("Cannot initialize message forwarder: " + exc.getMessage());
      if (debug) exc.printStackTrace(System.err);
    }
    
    // Execute the command
    try {
      if (BROWSE_CMD.equals(command)) {
        boolean all = Boolean.getBoolean(ALL);
        browse(queue, forwarder, !all);
      } else if (PURGE_CMD.equals(command)) {
        purge(queue);
      } else if (DELETE_CMD.equals(command)) {
        delete(queue);
      } else if (CREATE_CMD.equals(command)) {
        create(qname);
      } else if (FORWARD_CMD.equals(command)) {
        boolean all = Boolean.getBoolean(ALL);
        if (forwarder != null) {
          String msgid = System.getProperty(MSGID);
          if ((msgid != null) || all) {
            forward(queue, forwarder, msgid);
          } else {
            System.err.println("Identifier of message is not defined.");
            System.exit(-1);
          }
        } else {
          System.err.println("Forward queue is not defined.");
          System.exit(-1);
        }
      } else if (REMOVE_CMD.equals(command)) {
        String msgid = System.getProperty(MSGID);
        remove(queue, msgid);
      } else if (PAUSE_ON_CMD.equals(command)) {
        pause(queue, true);
      } else if (PAUSE_OFF_CMD.equals(command)) {
        pause(queue, false);
      } else if (STAT_CMD.equals(command)) {
        statistics(queue);
      }
    } catch (Exception exc) {
      System.exit(-1);
    } finally {
      if (ictx != null)
        ictx.close();
      if (admin != null)
        admin.close();
      if (cnx != null)
        cnx.close();
    }
    System.exit(0);
  }

  /* ********************************************************************************
   * Handle parameters about JNDI
   * ******************************************************************************** */

  public static final String JNDI_FILE = "JNDI_FILE";
  public static final String JNDI_FACTORY = "JNDI_FACTORY";
  public static final String JNDI_FACTORY_DFLT = "fr.dyade.aaa.jndi2.client.NamingContextFactory";
  public static final String JNDI_PROVIDER_URL = "JNDI_URL";
  public static final String JNDI_PROVIDER_URL_DFLT = "scn://localhost:16400";

  public static InitialContext getJNDIInitialContext() throws FileNotFoundException, IOException, NamingException {
    Properties jndiProps = new Properties();
    String jndiFile = System.getProperty(JNDI_FILE);
    if (jndiFile != null) {
      try (FileInputStream fis = new FileInputStream(jndiFile)) {
      jndiProps.load(fis);
      }
    } else {
      jndiProps.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY_DFLT);
      jndiProps.setProperty(InitialContext.PROVIDER_URL, JNDI_PROVIDER_URL_DFLT);
    }

    String jndiFactory = System.getProperty(JNDI_FACTORY);
    if (jndiFactory != null)
      jndiProps.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY, jndiFactory);

    String jndiURL = System.getProperty(JNDI_PROVIDER_URL);
    if (jndiURL != null)
      jndiProps.setProperty(InitialContext.PROVIDER_URL, jndiURL);

    InitialContext ictx = new InitialContext(jndiProps);
    return ictx;
  }

  /* ********************************************************************************
   * Handle parameters about JMS connection
   * ******************************************************************************** */

  public static final String CF = "CF";
  public static final String CF_DFLT = "cf";

  public static final String ADMIN = "ADMIN";
  public static final String ADMIN_DFLT = "root";
  public static final String PASS = "PASS";
  public static final String PASS_DFLT = "root";

  public static Connection getJMSConnection(Context ictx) throws NamingException, JMSException {
    ConnectionFactory cf = null;

    String cfname = System.getProperty(CF, CF_DFLT);
    try {
      cf = (ConnectionFactory) ictx.lookup(cfname);
    } catch (NameNotFoundException exc) {
      System.err.println("JNDI: " + cfname + " not found.");
      System.exit(-1);
    }
    if (debug)
      System.err.println("ConnectionFactory=" + cf);

    String user = System.getProperty(ADMIN, ADMIN_DFLT);
    String pass = System.getProperty(PASS, PASS_DFLT);

    Connection cnx = cf.createConnection(user, pass);
    return cnx;
  }

  public static Destination getJMSDestination(Context ictx, String destname) throws NamingException {
    try {
      return (Destination) ictx.lookup(destname);
    } catch (NameNotFoundException exc) {
      System.err.println("JNDI: " + destname + " not found.");
      throw exc;
    }
  }

  public final static String LINE_SEP1 = "+==================================================\n";
  public final static String LINE_SEP2 = "+------------------------------\n";
  
  public static String msgToString(Message msg) throws JMSException {
    StringBuilder strbuf = new StringBuilder();
    strbuf.append(LINE_SEP1);
    strbuf.append("| MessageID: \"").append(msg.getJMSMessageID()).append("\"\n");
    strbuf.append("| Class: \"").append(msg.getClass().getSimpleName()).append("\"\n");
    strbuf.append(LINE_SEP2);
    msgHeader(strbuf, msg);
    strbuf.append(LINE_SEP2);
    msgProperties(strbuf, msg);
    strbuf.append(LINE_SEP2);
    msgBody(strbuf, msg);
    strbuf.append(LINE_SEP2);
    
    return strbuf.toString();
  }

  public static void msgHeader(StringBuilder strbuf, Message msg) throws JMSException {
    strbuf.append("| To: ").append(msg.getJMSDestination()).append('\n');
    strbuf.append("| CorrelationId: ").append(msg.getJMSCorrelationID()).append('\n');
    strbuf.append("| DeliveryMode: ").append(msg.getJMSDeliveryMode()).append('\n');
    strbuf.append("| Expiration: ").append(msg.getJMSExpiration()).append('\n');
    strbuf.append("| Priority: ").append(msg.getJMSPriority()).append('\n');
    strbuf.append("| Redelivered: ").append(msg.getJMSRedelivered()).append('\n');
    strbuf.append("| ReplyTo: ").append(msg.getJMSReplyTo()).append('\n');
    strbuf.append("| Timestamp: ").append(msg.getJMSTimestamp())
      .append(" (").append(new Date(msg.getJMSTimestamp())).append(")\n");
    strbuf.append("| Type: ").append(msg.getJMSType()).append('\n');
  }
  
  public static void msgProperties(StringBuilder strbuf, Message msg) throws JMSException {
    // Handles JMS_JORAM_ERROR properties
    if (msg.propertyExists("JMS_JORAM_ERRORCOUNT")) {
      int nbErrors = msg.getIntProperty("JMS_JORAM_ERRORCOUNT");
      strbuf.append("| ").append("JMS_JORAM_ERRORCOUNT=").append(nbErrors).append('\n');

      for (int i=1; i<=nbErrors; i++) {
        strbuf.append("| ").append("JMS_JORAM_ERRORCODE_").append(i).append("=").append(msg.getStringProperty("JMS_JORAM_ERRORCODE_"+i)).append('\n');
        strbuf.append("| ").append("JMS_JORAM_ERRORCAUSE_").append(i).append("=").append(msg.getStringProperty("JMS_JORAM_ERRORCAUSE_"+i)).append('\n');
      }
      strbuf.append(LINE_SEP2);
    }
    // Handles other properties
    for (Enumeration<String> names = msg.getPropertyNames();names.hasMoreElements();) {
      String name = names.nextElement();
      if (! name.startsWith("JMS_JORAM_ERROR")) // Already handled
        strbuf.append("| ").append(name).append("=").append(msg.getStringProperty(name)).append('\n');
    }
  }
  
  public static void msgBody(StringBuilder strbuf, Message msg) throws JMSException {
    if (msg instanceof TextMessage) {
      // Handling of TextMessage
      strbuf.append(((TextMessage) msg).getText()).append('\n');
    } else if (msg instanceof BytesMessage) {
      // Handling of BytesMessage
      try {
        byte [] body = msg.getBody(byte[].class);
        if ((body == null) || (body.length == 0)) {
          strbuf.append("Empty body\n");
        } else {
          if (bodyAsText) {
            try {
              strbuf.append(new String(body, charset)).append('\n');
            } catch (UnsupportedEncodingException exc) {
              strbuf.append("/!\\ Could not writes body: " + exc.getMessage());
            }
          } else {
            strbuf.append(BinaryDump.dump(body, 0, 0));
          }
        }
      } catch (JMSException exc) {
        strbuf.append("/!\\ Error reading message body: " + exc.getMessage()).append('\n');
      }
    } else if (msg instanceof MapMessage) {
      // Handling of MapMessage
      try {
        Map map = msg.getBody(java.util.Map.class);
        if (map == null) {
          strbuf.append("Empty body\n");
        } else {
          map.forEach((k, v) -> strbuf.append(k).append('=').append(v).append('\n'));
        }
      } catch (JMSException exc) {
        strbuf.append("/!\\ Error reading message body: " + exc.getMessage()).append('\n');
      }
    } else if (msg instanceof ObjectMessage) {
      // Handling of ObjectMessage
      if (dump && (msg instanceof org.ow2.joram.jakarta.jms.Message)) {
        try {
          strbuf.append(((org.ow2.joram.jakarta.jms.Message) msg).dumpBody());
        } catch (IOException exc) {
          strbuf.append("/!\\ Error dumping message: " + exc.getMessage()).append('\n');
        }
      } else { 
        strbuf.append("/!\\ Message boddy cannot be displayed: " + msg.getClass().getName()).append('\n');
      }
    } else if (msg instanceof StreamMessage) {
      // Handling of StreamMessage
      if (dump && (msg instanceof org.ow2.joram.jakarta.jms.Message)) {
        try {
          strbuf.append(((org.ow2.joram.jakarta.jms.Message) msg).dumpBody());
        } catch (IOException exc) {
          strbuf.append("/!\\ Error dumping message: " + exc.getMessage()).append('\n');
        }
      } else { 
        strbuf.append("/!\\ Message boddy cannot be displayed: " + msg.getClass().getName()).append('\n');
      }
    } else {
      // Simple message without body
      strbuf.append("Empty body\n");
    }
  }

  public static void saveBody(String id, byte[] content) {
    File file = new File("Message_" + id.substring(3));
    try (FileOutputStream fos = new FileOutputStream(file)) {
      fos.write(content);
      fos.flush();
      System.out.println("Message saved in " + file.getPath());
    } catch (IOException exc) {
      System.err.println("/!\\ Cannot save message " + id + ": " + exc.getMessage());
    }
  }
  
  /* ********************************************************************************
   * Prints successively each message of the queue, allowing to delete or forward it.
   * If ALL option is defined, all messages are automatically handled.
   * ******************************************************************************** */
  
  public final static int QUIT = 'q';
  public final static int DELETE = 'd';
  public final static int FORWARD = 'f';
  public final static int SAVE = 's';
  public final static int NEXT = 'n';

  public static void browse(Queue queue, MessageProducer forwarder, boolean interactive) {
    try {
      String ids[] = queue.getMessageIds();
      for (String id : ids) {
        Message msg = queue.getMessage(id);
        System.out.println(msgToString(msg));
        
        if (interactive) {
          if (forwarder != null)
            System.out.println("d(elete), f(orward), s(ave), n(ext), q(uit)");
          else
            System.out.println("d(elete), n(ext), s(ave), q(uit)");

          int c;
          while (true) {
            try {
              c = System.in.read();
            } catch (IOException exc) {
              c = QUIT;
            }
            if ((c == QUIT) || (c == DELETE) || (c == SAVE)  || (c == NEXT)) break;
                        
            if ((forwarder != null) && (c == FORWARD)) break;
          }
          if (c == QUIT) break;
          if (c == NEXT) continue;
          if (c == DELETE) {
            queue.deleteMessage(id);
          } else if (c == SAVE) {
            if (msg instanceof TextMessage) {
              try {
                saveBody(msg.getJMSMessageID(), ((TextMessage) msg).getText().getBytes(charset));
              } catch (UnsupportedEncodingException exc) {
                System.err.println(" /!\\" + exc.getMessage());
              }
            } else if ((msg instanceof BytesMessage) || (msg instanceof MapMessage) || (msg instanceof ObjectMessage) || (msg instanceof StreamMessage)) {
              try {
                saveBody(msg.getJMSMessageID(), ((org.ow2.joram.jakarta.jms.Message) msg).getRawBody());
              } catch (IOException exc) {
                System.err.println(" /!\\" + exc.getMessage());
              }
            } else {
              System.out.println(" /!\\ Message body is empty: " + msg.getJMSMessageID());
            }
          } else if (c == FORWARD) {
            if (forwarder != null) {
              // Forwards this message.
              // TODO: Use QoS properties of initial message.
              forwarder.send(msg);
              // Then deletes it.
              queue.deleteMessage(id);
            }
          }
        }
      }
    } catch (ConnectException|AdminException exc) {
      System.err.println("Issue with administration connection: " + exc.getMessage());
      if (! silent || debug) exc.printStackTrace();
    } catch (JMSException exc) {
      System.err.println("Issue with JMS connection: " + exc.getMessage());
      if (! silent || debug) exc.printStackTrace();
    }
  }

  /* ********************************************************************************
   * Removes all messages in the queue.
   * ******************************************************************************** */

  public static void purge(Queue queue) {
    try {
      queue.clear();
    } catch (ConnectException|AdminException exc) {
      System.err.println("Issue with administration connection: " + exc.getMessage());
      if (! silent || debug) exc.printStackTrace();
    }
  }

  /* ********************************************************************************
   * Creates the queue and registers it in JNDI. The rights set allow any user to send
   * or receive messages on this queue.
   * ******************************************************************************** */

  public static void create(String name) {
    org.ow2.joram.jakarta.jms.Queue queue = null;
    try {
      queue = (Queue) admin.createQueue(name);
      queue.setFreeReading();
      queue.setFreeWriting();
    } catch (ConnectException|AdminException exc) {
      System.err.println("Issue with administration connection: " + exc.getMessage());
      if (! silent || debug) exc.printStackTrace();
      return;
    }
    try {
      ictx.bind(name, queue);
    } catch (NamingException exc) {
      System.err.println("Cannot bind queue in JNDI: " + exc.getMessage());
      if (! silent || debug) exc.printStackTrace();
    }
  }

  /* ********************************************************************************
   * Pause (or restart) the queue.
   * ******************************************************************************** */

  public static void pause(Queue queue, boolean pause) {
    try {
      queue.setPause(pause);
    } catch (ConnectException|AdminException exc) {
      System.err.println("Issue with administration connection: " + exc.getMessage());
      if (! silent || debug) exc.printStackTrace();
    }
  }

  /* ********************************************************************************
   * Get the queue statistics.
   * ******************************************************************************** */

  public static void statistics(Queue queue) {
    try {
      @SuppressWarnings("rawtypes") Hashtable statistics = queue.getStatistics();
      StringBuilder strbuf = new StringBuilder();
      for (Object key : statistics.keySet()) {
        strbuf.append("| ").append(key).append("=").append(statistics.get(key)).append('\n');
      }
      System.out.println(strbuf.toString());
    } catch (ConnectException|AdminException exc) {
      System.err.println("Issue with administration connection: " + exc.getMessage());
      if (! silent || debug) exc.printStackTrace();
    }
  }

  /* ********************************************************************************
   * Delete the queue.
   * ******************************************************************************** */

  public static void delete(Queue queue) {
    try {
      queue.delete();
    } catch (ConnectException|AdminException|JMSException exc) {
      System.err.println("Issue with administration connection: " + exc.getMessage());
      if (! silent || debug) exc.printStackTrace();
    }
  }

  /* ********************************************************************************
   * Remove a message in the queue.
   * ******************************************************************************** */

  public static void remove(Queue queue, String msgid) {
    try {
      Message msg = queue.getMessage(msgid);
      if (msg == null) {
        System.err.println("Message \"" + msgid +"\" unknown.");
        System.exit(-1);
      }
      queue.deleteMessage(msgid);
      System.out.println("Message \"" + msgid + "\" removed.");
    } catch (ConnectException|AdminException|JMSException exc) {
      System.err.println("Issue with administration connection: " + exc.getMessage());
      if (! silent || debug) exc.printStackTrace();
    }
  }

  /* ********************************************************************************
   * Forwards the message identified by MSGID option.
   * ******************************************************************************** */

  public static void forward(Queue queue, MessageProducer forwarder, String msgid) {
    try {
      if (msgid == null) {
        // forwards all messages
        String ids[] = queue.getMessageIds();
        for (String id : ids) {
          forward2(queue, forwarder, id);
        }
      } else {
        // forwards identified message
        forward2(queue, forwarder, msgid);
      }
    } catch (ConnectException|AdminException|JMSException exc) {
      System.err.println("Issue with administration connection: " + exc.getMessage());
      if (! silent || debug) exc.printStackTrace();
    }
  }

  private static void forward2(Queue queue, MessageProducer forwarder, String msgid) throws ConnectException, AdminException, JMSException {
    Message msg = queue.getMessage(msgid);
    if (msg == null) {
      System.err.println("Message \"" + msgid +"\" unknown.");
      System.exit(-1);
    }
    if (!silent || debug)
      System.out.println(msgToString(msg));
    // TODO: Use QoS properties of initial message.
    forwarder.send(msg);
    queue.deleteMessage(msgid);
    System.out.println("Message \"" + msgid + "\" forwarded.");
  }

  /* ********************************************************************************
   * Lookup in JNDI.
   * ******************************************************************************** */

  public static void lookup(String name) {
    try {
      Object obj = ictx.lookup(name);
      System.out.println("Name \"" + name + "\" bind to: " + obj);
    } catch (NamingException exc) {
      System.err.println("Name \"" + name +"\" unknown.");
      if (debug) exc.printStackTrace(System.err);
    }
  }

  /* ********************************************************************************
   * Unbind the queue.
   * ******************************************************************************** */

  public static void unbind(String qname) {
    try {
      ictx.unbind(qname);
    } catch (NamingException exc) {
      System.err.println("Name \"" + qname +"\" unknown.");
      if (debug) exc.printStackTrace(System.err);
    }
  }

  /* ********************************************************************************
   * Allows  to use System.console if possible but implements a workaround when the
   * console is not available (Eclipse or Mingw terminals).
   * ******************************************************************************** */

  static BufferedReader stdin = null;
  static boolean gui = false;
  
  public static char[] readPassword(String format, Object... args) throws IOException {
    if (gui) {
      final JPasswordField pf = new JPasswordField();
      if (JOptionPane.showConfirmDialog( null, pf, format, JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE ) == JOptionPane.OK_OPTION) {
        return pf.getPassword();
      } else {
        return null;
      }
    }
    if ((stdin == null) && (System.console() != null))
      return System.console().readPassword(format, args);
    return readLine(format, args).toCharArray();
  }

  public  static String readLine(String format, Object... args) throws IOException {
    if (stdin != null)
      return stdin.readLine();
 
    if (idx < values.length)
      return values[idx++];
    
    if (System.console() != null)
      return System.console().readLine(format, args);

    System.out.print(String.format(format, args));
    BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    return reader.readLine();
  }
}

