/*
 * Copyright (c) 2000, 2017 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package com.sun.messaging.jmq.io;

import java.io.*;
import java.util.Map;
import java.util.Iterator;
import java.util.Collections;
import java.util.StringTokenizer;

//import com.sun.messaging.jmq.resources.*;

/**
 * A table of PortMapperEntries. Knows how to parse and generate
 * the output used by the portmapper service.
 */
public class PortMapperTable {

    private static boolean DEBUG = false;

    public final static int PORTMAPPER_VERSION = 101;
    public final static String DOT = ".";
    public final static String NEWLINE = "\n";
    public final static String SPACE = " ";
    public final static byte NEWLINE_BYTE = 10;
    public final static byte DOT_BYTE = 46;
    public final static byte SPACE_BYTE = 32;

    private String    brokerInstance = "???";
    private String    brokerVersion = "???";
    private String    version = "???";

    private Map  table = null;

    /**
     * Construct an unititialized system message ID. It is assumed
     * the caller will set the fields either explicitly or via
     * readID()
     */
    public PortMapperTable() {
        try {
            Class c = Class.forName("java.util.LinkedHashMap");
            Map m = (Map)c.newInstance();
            table = Collections.synchronizedMap(m);
        } catch (Throwable ex) {
            table = Collections.synchronizedMap(new java.util.HashMap());
        }
        version = Integer.toString(PORTMAPPER_VERSION);
    }

    /**
     * Add a service
     */
    public void add(PortMapperEntry e) {
	table.remove(e.getName());
        table.put(e.getName(), e);
    }

    /**
     * get a service
     */

    public PortMapperEntry get(String name) {
        return (PortMapperEntry)table.get(name);
    }

    /**
     * Remove a service
     */
    public void remove(String name) {
        table.remove(name);
    }

    /**
     * Remove a service. 
     *
     * @param e PortMapperEntry to get service name from to delete.
     */
    public void remove(PortMapperEntry e) {
        table.remove(e.getName());
    }

    /**
     * Set the broker instance name
     */
    public void setBrokerInstanceName(String name) {
        brokerInstance = name;
    }

    /**
     * Set the broker version string
     */
    public void setBrokerVersion(String s) {
        brokerVersion = s;
    }

    /**
     * Get the broker instance name
     */
    public String getBrokerInstanceName() {
        return brokerInstance;
    }

    /**
     * Get the portmapper version number
     */
    public String getVersion() {
        return version;
    }

    /**
     * Get the broker version number
     */
    public String getBrokerVersion() {
        return brokerVersion;
    }

    /**
     * Get a hashtable containing the servicename/PortMapperEntry pairs
     */
    public Map getServices() {
        return table;
    }

    /**
     * Get the port number for a particular service.
     *
     * @param service   Name of service to get port number for
     * @returns         Port number, or -1 if port for service is not known.
     */
    public int getPortForService(String service) {

        PortMapperEntry pme = (PortMapperEntry)table.get(service);
        if (pme == null) {
            return -1;
        } else {
            return pme.getPort();
        }
    }

    public String toString() {
        return version + " " + brokerInstance + " " + brokerVersion +
            table.toString();
    }


    /**
     * Write the portmapper data to the specified DataOutputStream.
     * The formate of the data is:
     *
     *  <PRE>
     *  <portmapper version><SP><broker instance name><SP>broker version><NL>
     *  <service name><SP><protocol><SP><type><SP><port><NL>
     *  <.><NL>
     *
     *  Where:
     *
     *  <portmapper version>Portmapper numeric version string (ie "100").
     *  <broker version>    Broker version string (ie "2.0").
     *  <NL>                Newline character (octal 012)
     *  <service name>      Alphanumeric string. No embedded whitespace.
     *  <space>             A single space character
     *  <protocol>          Transport protocol. Typically "tcp" or "ssl"
     *  <service>           Service type. Typically "NORMAL", "ADMIN" or
     *                      "PORTMAPPER"
     *  <port>              Numeric string. Service port number
     *  <.>                 The '.' (dot) character
     *
     *  An example would be:
     *
     *  101 jmqbroker 2.0
     *  portmapper tcp PORTMAPPER 7575
     *  jms tcp NORMAL 59510
     *  admin tcp ADMIN 59997
     *  ssljms ssl NORMAL 42322
     *  .
     *
     *   </PRE>
     *
     * @param    out    OutputStream to write ID to
     *
     */
    public void write(OutputStream out)
	throws IOException {

        StringBuffer data = new StringBuffer();
        String  name;
        PortMapperEntry pme;
        Integer port;

        data.append(PORTMAPPER_VERSION + SPACE + brokerInstance + SPACE +
		    brokerVersion + NEWLINE);

        for (Iterator e = table.keySet().iterator(); e.hasNext() ;) {
            name = (String)e.next();
            pme = (PortMapperEntry)table.get(name);
            data.append(pme.toString() + NEWLINE);
        }

        data.append(DOT + NEWLINE);

        out.write(data.toString().getBytes("ASCII"));
        out.flush();
    }

    /**
     * Read the data from the specified DataInputStream. The format of
     * the data is assumed to match that generated by write.
     *
     * @param    in    InputStream to read from
     *
     */
    public void read(InputStream is)
	throws IOException {

        BufferedInputStream in = new BufferedInputStream(is);
	/*
	 * IH: Increased size of buffer from 128 to 2048. There
	 * shouldn't really be a hard limit here.
	 */
        byte[] buffer = new byte[2048];
        int nBytes = 0;

        if (DEBUG) {
            System.err.println(this.getClass().getName() +
                ".read():");
        }

        // Read first line
        nBytes = readLine(in, buffer);

	StringTokenizer st = new StringTokenizer(new String(buffer, "ASCII"));

        version = st.nextToken();
        if (Integer.parseInt(version) != PORTMAPPER_VERSION) {
        	//TODO I18N
        	//  Bad version for portmapper data
        	//  S4006=Portmapper version mismatch. Version={0}. Expecting {1}
        	String errorMessage="Portmapper version mismatch. Version="+String.valueOf(version)+". Expecting "+String.valueOf(PORTMAPPER_VERSION);
        	throw new IOException(errorMessage);
 
//            throw new IOException(
//                SharedResources.getResources().getString(
//                    SharedResources.X_BAD_PORTMAPPER_VERSION,
//                    String.valueOf(version),
//                    String.valueOf(PORTMAPPER_VERSION)));
        }

        brokerInstance = st.nextToken();

	brokerVersion = st.nextToken();

        // Read service name/port number value pairs
        while (true) {
            nBytes = readLine(in, buffer);

            if (nBytes <= 0 || (nBytes == 1 && buffer[0] == DOT_BYTE)) {
                break;
            }

            PortMapperEntry pme = PortMapperEntry.parse(
                                    new String(buffer, 0, nBytes, "ASCII"));
            this.add(pme);
        }
    }

    /**
     * Read an ASCII line from an input stream into a buffer. If
     * the input line is longer than the buffer then the bytes at
     * the end of the line are lost
     *
     * @returns Number of bytes in buffer
     */
    private int readLine(InputStream in, byte[] buffer)
	throws IOException {

        int b = 0;
        int n = 0;

        b = in.read();
        while (b != -1 && b != NEWLINE_BYTE) {
            if (n < buffer.length) {
                buffer[n] = (byte)b;
                n++;
            }
            b = in.read();
        }

        if (DEBUG) {
            try {
                System.err.println(new String(buffer, 0, n, "ASCII"));
            } catch (UnsupportedEncodingException e) {
            }
        }

        return n;

    }
}
