]> gitweb.hamatoma.de Git - jatty/commitdiff
Initial state master
authorhm <hm@hawk>
Sun, 20 Mar 2016 19:19:47 +0000 (20:19 +0100)
committerhm <hm@hawk>
Wed, 23 Mar 2016 22:36:51 +0000 (23:36 +0100)
15 files changed:
.classpath [new file with mode: 0755]
.project [new file with mode: 0755]
.settings/org.eclipse.jdt.core.prefs [new file with mode: 0755]
bin/.gitignore [new file with mode: 0644]
resources/jatty.de.properties [new file with mode: 0644]
resources/jatty.en.properties [new file with mode: 0644]
resources/talk_schema.sql [new file with mode: 0755]
resources/user_schema.sql [new file with mode: 0755]
src/de/hamatoma/jatty/Client.java [new file with mode: 0755]
src/de/hamatoma/jatty/ClientGui.java [new file with mode: 0644]
src/de/hamatoma/jatty/I18N.java [new file with mode: 0644]
src/de/hamatoma/jatty/Jatty.java [new file with mode: 0755]
src/de/hamatoma/jatty/JattyDb.java [new file with mode: 0755]
src/de/hamatoma/jatty/Scrambler.java [new file with mode: 0644]
src/de/hamatoma/jatty/Server.java [new file with mode: 0755]

diff --git a/.classpath b/.classpath
new file mode 100755 (executable)
index 0000000..d639a08
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<classpath>\r
+       <classpathentry kind="src" path="src"/>\r
+       <classpathentry kind="src" path="resources"/>\r
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>\r
+       <classpathentry kind="output" path="bin"/>\r
+</classpath>\r
diff --git a/.project b/.project
new file mode 100755 (executable)
index 0000000..180dd81
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<projectDescription>\r
+       <name>jatty</name>\r
+       <comment></comment>\r
+       <projects>\r
+       </projects>\r
+       <buildSpec>\r
+               <buildCommand>\r
+                       <name>org.eclipse.jdt.core.javabuilder</name>\r
+                       <arguments>\r
+                       </arguments>\r
+               </buildCommand>\r
+       </buildSpec>\r
+       <natures>\r
+               <nature>org.eclipse.jdt.core.javanature</nature>\r
+       </natures>\r
+</projectDescription>\r
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100755 (executable)
index 0000000..ace45ce
--- /dev/null
@@ -0,0 +1,12 @@
+eclipse.preferences.version=1\r
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled\r
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate\r
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8\r
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve\r
+org.eclipse.jdt.core.compiler.compliance=1.8\r
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate\r
+org.eclipse.jdt.core.compiler.debug.localVariable=generate\r
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate\r
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error\r
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error\r
+org.eclipse.jdt.core.compiler.source=1.8\r
diff --git a/bin/.gitignore b/bin/.gitignore
new file mode 100644 (file)
index 0000000..c9adce7
--- /dev/null
@@ -0,0 +1,5 @@
+/de/
+/jatty.de.properties
+/jatty.en.properties
+/talk_schema.sql
+/user_schema.sql
diff --git a/resources/jatty.de.properties b/resources/jatty.de.properties
new file mode 100644 (file)
index 0000000..b418a71
--- /dev/null
@@ -0,0 +1,13 @@
+0=Gebrauch:
+1=Startet einen Chat-Server, einen GUI-Chat-Client oder einen Kommandozeilen-Chat-Client.
+2=Adresse des Chat-Servers, z.B. localhost oder
+3=Voreinstellung:
+4=Port des Servers.
+5=Voreinstellung:
+6=Beispiele:
+7=Modus fehlt:
+8=Server angehalten
+9=Sprache der Oberfläche. Voreinstellung: Systemeinstellung
+10=Willkommen bei
+11=Willkommen %s
+12=Es fehlen Benutzername und Passwort: z.B.
\ No newline at end of file
diff --git a/resources/jatty.en.properties b/resources/jatty.en.properties
new file mode 100644 (file)
index 0000000..1fab3ac
--- /dev/null
@@ -0,0 +1,13 @@
+0=Usage:
+1=Starts a chat server, a GUI chat client or a commandline chat client.
+2=address of the chat server, e.g. localhost or
+3=Default:
+4=port of the server.
+5=Default:
+6=Examples:
+7=missing mode:
+8=server stopped
+9=language of the GUI. Default: system setting
+10=Welcome to
+11=Welcome %s
+12=missing username and password: e.g.
\ No newline at end of file
diff --git a/resources/talk_schema.sql b/resources/talk_schema.sql
new file mode 100755 (executable)
index 0000000..9ec5269
--- /dev/null
@@ -0,0 +1,7 @@
+CREATE TABLE talk (\r
+       talkId int identity,\r
+    time timestamp,\r
+    user varchar(64),\r
+    message varchar(254)\r
+);\r
+insert into talk (time, user, message) values('2016-03-16 08:10:32', 'hm', 'Hi all');
\ No newline at end of file
diff --git a/resources/user_schema.sql b/resources/user_schema.sql
new file mode 100755 (executable)
index 0000000..49eaab2
--- /dev/null
@@ -0,0 +1,7 @@
+CREATE TABLE user (\r
+    user varchar(64),\r
+    passwdHash varchar(256),\r
+    lastLogin TIMESTAMP,\r
+    UNIQUE (user)\r
+);\r
+insert into user (user, passwdHash, lastLogin) values('hm', '', '2016-03-16 08:24:33');\r
diff --git a/src/de/hamatoma/jatty/Client.java b/src/de/hamatoma/jatty/Client.java
new file mode 100755 (executable)
index 0000000..8092b6a
--- /dev/null
@@ -0,0 +1,118 @@
+/**\r
+ *\r
+ */\r
+package de.hamatoma.jatty;\r
+\r
+import java.io.BufferedReader;\r
+import java.io.DataOutputStream;\r
+import java.io.IOException;\r
+import java.io.InputStreamReader;\r
+import java.net.Socket;\r
+import java.net.UnknownHostException;\r
+import java.util.LinkedList;\r
+import java.util.List;\r
+\r
+/**\r
+ * A TCP chat client.\r
+ *\r
+ * @author hm\r
+ *\r
+ */\r
+class Client {\r
+    String address;\r
+    int port = 0;\r
+    Socket clientSocket = null;\r
+    BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));\r
+    DataOutputStream outToServer = null;\r
+    BufferedReader inFromServer = null;\r
+    Scrambler scrambler;\r
+\r
+    /**\r
+     * Constructor.\r
+     *\r
+     * @param address\r
+     *            the address of the server, e.g. "localhost" or "192.168.2.1"\r
+     * @param port\r
+     *            the port of the server\r
+     */\r
+    public Client(final String address, final int port,\r
+        final Scrambler scrambler) {\r
+        this.port = port;\r
+        this.address = address;\r
+        this.scrambler = scrambler;\r
+        try {\r
+            this.clientSocket = new Socket(address, port);\r
+            this.outToServer = new DataOutputStream(this.clientSocket.getOutputStream());\r
+            this.inFromServer = new BufferedReader(new InputStreamReader(this.clientSocket.getInputStream()));\r
+        } catch (final UnknownHostException e) {\r
+            e.printStackTrace();\r
+            this.clientSocket = null;\r
+        } catch (final IOException e) {\r
+            e.printStackTrace();\r
+            this.clientSocket = null;\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Writes a message to the user interface.\r
+     *\r
+     * @param msg\r
+     *            the message to write\r
+     */\r
+    public void log(final String msg) {\r
+        System.out.println(msg);\r
+    }\r
+\r
+    /**\r
+     * Sends a message and receives the answer.\r
+     *\r
+     * @param message\r
+     *            the message to send\r
+     * @return the answer of the server a a string list\r
+     */\r
+    public List<String> sendAndReceive(String message) {\r
+        final List<String> answer = new LinkedList<String>();\r
+        try {\r
+            final boolean isUpdate = message.startsWith("/UPDATE");\r
+            if (!isUpdate)\r
+                log("> " + message);\r
+            message = this.scrambler.encode(message + "\n");\r
+            this.outToServer.writeBytes(message);\r
+            while (this.inFromServer.ready()) {\r
+                String line = this.inFromServer.readLine();\r
+                line = this.scrambler.decode(line);\r
+                if (isUpdate && answer.size() == 0 && line.length() > 0)\r
+                    log("> " + message);\r
+                log("< " + line);\r
+                answer.add(line);\r
+            }\r
+        } catch (final IOException e) {\r
+            e.printStackTrace();\r
+        }\r
+        return answer;\r
+    }\r
+\r
+    /**\r
+     * Runs the client untils the server sends "/EXIT".\r
+     *\r
+     * Only used by the command line client.\r
+     */\r
+    public void run() {\r
+        String message;\r
+        List<String> answer = null;\r
+        try {\r
+            while (answer == null || answer.size() == 0\r
+                    || answer.get(answer.size() - 1).startsWith("/EXIT")) {\r
+                message = this.inFromUser.readLine();\r
+                answer = sendAndReceive(message);\r
+            }\r
+        } catch (final IOException e) {\r
+            e.printStackTrace();\r
+        }\r
+        try {\r
+            this.clientSocket.close();\r
+        } catch (final IOException e) {\r
+            e.printStackTrace();\r
+        }\r
+    }\r
+}\r
diff --git a/src/de/hamatoma/jatty/ClientGui.java b/src/de/hamatoma/jatty/ClientGui.java
new file mode 100644 (file)
index 0000000..bf6875c
--- /dev/null
@@ -0,0 +1,220 @@
+package de.hamatoma.jatty;
+
+import java.awt.BorderLayout;
+import java.awt.EventQueue;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.util.List;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JFrame;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.JTextArea;
+import javax.swing.ListSelectionModel;
+import javax.swing.Timer;
+
+/**
+ * A chat client with a Swing GUI.
+ *
+ * @author hm
+ *
+ */
+public class ClientGui extends Client implements KeyListener, ActionListener {
+
+    private JFrame frame;
+    private JTextArea textAreaCommand;
+    private JTextArea textAreaStatusLine;
+    private JTextArea textAreaHeader;
+    private final DefaultListModel<String> talkListModel = new DefaultListModel<String>();
+
+    /**
+     * A main program for testing.
+     *
+     * @param args
+     *            program arguments
+     */
+    public static void main(final String[] args) {
+        final String addr = args.length < 1 ? "localhost" : args[0];
+        final int port = args.length < 2 ? 58123 : Integer.parseInt(args[1]);
+        final Scrambler scrambler = new Scrambler("a", "b", "c");
+        final ClientGui client = new ClientGui(addr, port, scrambler);
+        client.run();
+    }
+    /**
+     * Launch the application.
+     */
+    @Override
+    public void run() {
+        EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    ClientGui.this.frame.setVisible(true);
+                } catch (final Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        });
+    }
+
+    Timer timer = null;
+    private JScrollPane scrollPaneTalk;
+    private JList<String> listTalk;
+    private JScrollPane scrollPaneUser;
+    private JList<String> listUser;
+
+    /**
+     * Create the application.
+     *
+     * @param address
+     *            the server address, e.g. "localhost" or "123.2.4.8"
+     * @param port
+     *            the server port
+     */
+    public ClientGui(final String address, final int port,
+        final Scrambler scrambler) {
+        super(address, port, scrambler);
+        initialize();
+        this.timer = new Timer(1000, this);
+        this.timer.start();
+    }
+
+    /**
+     * Initialize the contents of the frame.
+     */
+    private void initialize() {
+        this.frame = new JFrame();
+        this.frame
+        .setTitle("jatty client [" + this.address + ":"
+                + this.port + "]");
+        this.frame.setBounds(100, 100, 450, 300);
+        this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        this.frame.getContentPane().setLayout(new BorderLayout(0, 0));
+
+        final JPanel panel = new JPanel();
+        this.frame.getContentPane().add(panel);
+        panel.setLayout(new BorderLayout(0, 0));
+
+        final JSplitPane splitPane = new JSplitPane();
+        splitPane.setDividerLocation(0.80);
+        splitPane.setResizeWeight(0.9);
+        splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT);
+        panel.add(splitPane, BorderLayout.CENTER);
+
+        this.textAreaCommand = new JTextArea();
+        this.textAreaCommand.setTabSize(3);
+        this.textAreaCommand.addKeyListener(this);
+        this.textAreaCommand.setText("/LOG ");
+        splitPane.setRightComponent(this.textAreaCommand);
+
+        final JSplitPane splitPaneOutput = new JSplitPane();
+        splitPaneOutput.setDividerLocation(0.80);
+        splitPaneOutput.setResizeWeight(0.8);
+        splitPane.setLeftComponent(splitPaneOutput);
+
+        this.scrollPaneTalk = new JScrollPane();
+        splitPaneOutput.setLeftComponent(this.scrollPaneTalk);
+
+        this.listTalk = new JList<String>(this.talkListModel);
+        this.listTalk.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        this.scrollPaneTalk.setViewportView(this.listTalk);
+
+        this.scrollPaneUser = new JScrollPane();
+        splitPaneOutput.setRightComponent(this.scrollPaneUser);
+
+        this.listUser = new JList<String>();
+        this.listUser.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        this.scrollPaneUser.setViewportView(this.listUser);
+
+        this.textAreaStatusLine = new JTextArea();
+        this.textAreaStatusLine.setEditable(false);
+        this.textAreaStatusLine.setText("Started");
+        this.frame.getContentPane().add(this.textAreaStatusLine, BorderLayout.SOUTH);
+
+        this.textAreaHeader = new JTextArea();
+        this.textAreaHeader.setEditable(false);
+        this.textAreaHeader.setText(
+            I18N.tr(I18N.WELCOME, "Welcome to") + " jatty " + Jatty.VERSION);
+        this.frame.getContentPane().add(this.textAreaHeader, BorderLayout.NORTH);
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
+     */
+    @Override
+    public void keyTyped(final KeyEvent e) {
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
+     */
+    @Override
+    public void keyPressed(final KeyEvent e) {
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
+     */
+    @Override
+    public void keyReleased(final KeyEvent e) {
+        if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+            if (e.getSource() == this.textAreaCommand){
+                final String text =  this.textAreaCommand.getText();
+                final List<String> answer = this.sendAndReceive(text);
+                handleAnswer(answer);
+                this.textAreaCommand.setText("");
+            }
+        }
+    }
+
+    /**
+     * Evaluates the server answer.
+     *
+     * @param answer
+     *            the answer string list
+     */
+    void handleAnswer(final List<String> answer) {
+        for (final String line : answer) {
+            if (line.startsWith("/USER")) {
+                final String[] users = line.substring(6).split(" ");
+                this.listUser.setListData(users);
+            } else if (line.startsWith("/STATUS")) {
+                this.textAreaStatusLine.setText(line.substring(7));
+            } else if (line.startsWith("/ERROR")) {
+                this.textAreaStatusLine.setText("+++ " + line.substring(7));
+            } else {
+                this.talkListModel.addElement(line);
+                this.listTalk
+                .ensureIndexIsVisible(this.talkListModel.getSize() - 1);
+            }
+        }
+
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see
+     * java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
+     */
+    @Override
+    public void actionPerformed(final ActionEvent e) {
+        if (e.getSource() == this.timer) {
+            final List<String> answer = this.sendAndReceive("/UPDATE");
+            if (!answer.isEmpty()) {
+                handleAnswer(answer);
+            }
+        }
+    }
+}
diff --git a/src/de/hamatoma/jatty/I18N.java b/src/de/hamatoma/jatty/I18N.java
new file mode 100644 (file)
index 0000000..77fdebf
--- /dev/null
@@ -0,0 +1,107 @@
+/**
+ *
+ */
+package de.hamatoma.jatty;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Internationalization: translations for the UI texts.
+ *
+ * @author hm
+ *
+ */
+public class I18N {
+    static public final int USAGE = 0;
+    static public final int USAGE2 = 1;
+    static public final int USAGE3 = 2;
+    static public final int USAGE4 = 3;
+    static public final int USAGE5 = 4;
+    static public final int USAGE6 = 5;
+    static public final int USAGE7 = 6;
+    static public final int MAIN1 = 7;
+    static public final int SERVER1 = 8;
+    static public final int USAGE8 = 9;
+    static public final int WELCOME = 10;
+    static public final int WELCOME2 = 11;
+    static public final int USERNAME = 12;
+
+    static I18N classLoader = new I18N();
+    static List<String> translations = new ArrayList<String>();
+
+    /**
+     * Reads the translations from a resource.
+     *
+     * @param prefix
+     *            prefix of the file with relative path. Will be completed by
+     *            the locale and ".properties"
+     * @param locale
+     *            the locale shortcut, e.g. "de" or "en-us"
+     * @return <code>true</code>: the resource has been read
+     */
+    public static boolean switchLanguage(final String prefix,
+        final String locale) {
+        boolean rc = false;
+        final String fn = "/" + prefix + "." + locale + ".properties";
+        BufferedReader reader = null;
+        try {
+            final InputStream input = classLoader.getClass()
+                    .getResourceAsStream(fn);
+            if (input != null) {
+                reader = new BufferedReader(new InputStreamReader(input, "UTF-8"));
+                if (reader != null) {
+                    translations.clear();
+                    String line;
+                    try {
+                        while ((line = reader.readLine()) != null) {
+                            if (line.matches("^\\d+=.*")) {
+                                translations
+                                .add(line.substring(line.indexOf('=') + 1));
+                            }
+                        }
+                        rc = true;
+                        System.out
+                        .println("successfully loaded: " + fn.substring(1));
+                    } catch (final IOException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        } catch (final UnsupportedEncodingException e) {
+            e.printStackTrace();
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close();
+                } catch (final IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return rc;
+    }
+
+    /**
+     * Translates a message.
+     *
+     * @param index
+     *            index of the message
+     * @param message
+     *            default value of the message
+     * @return the translated message or - if not found - the default message
+     */
+    public static String tr(final int index, final String message) {
+        final String rc;
+        if (index < translations.size())
+            rc = translations.get(index);
+        else
+            rc = message;
+        return rc;
+    }
+}
diff --git a/src/de/hamatoma/jatty/Jatty.java b/src/de/hamatoma/jatty/Jatty.java
new file mode 100755 (executable)
index 0000000..7ff45ae
--- /dev/null
@@ -0,0 +1,146 @@
+/**\r
+ *\r
+ */\r
+package de.hamatoma.jatty;\r
+\r
+import java.util.Locale;\r
+\r
+/**\r
+ * Starts a chat server or a chat client depending on the program arguments.\r
+ *\r
+ * @author wk\r
+ *\r
+ */\r
+public class Jatty {\r
+    public static final String VERSION = "v1.0.0 (2016.03.23)";\r
+\r
+    /**\r
+     * Extract the address from an argument.\r
+     *\r
+     * @param arg\r
+     *            the argument. Syntax: <address>[:<port>]<br>\r
+     *            If <code>null "localhost"</code> is returned\r
+     * @return the address of the IP expression\r
+     */\r
+    static String getAddr(final String arg) {\r
+        String rc = arg;\r
+        final int pos = arg == null ? -1 : arg.indexOf(':');\r
+        if (pos >= 0)\r
+            rc = arg.substring(0, pos);\r
+        return rc;\r
+    }\r
+\r
+    /**\r
+     * Extract the port from an argument.\r
+     *\r
+     * @param arg\r
+     *            the argument. Syntax: <address>[:<port>]<br>\r
+     *            If <code>null "localhost"</code> is returned\r
+     * @return the address of the IP expression\r
+     */\r
+    static int getPort(final String arg) {\r
+        int port = 58123;\r
+        final int pos = arg == null ? -1 : arg.indexOf(':');\r
+        if (pos >= 0)\r
+            port = Integer.parseInt(arg.substring(pos + 1));\r
+        return port;\r
+    }\r
+\r
+    /**\r
+     * Writes a message to the output media.\r
+     *\r
+     * @param message message to write\r
+     */\r
+    public static void log(final String message) {\r
+        System.out.println(message);\r
+    }\r
+\r
+    /**\r
+     * Main program.\r
+     *\r
+     * Evaluates the program arguments and start a server or a client.\r
+     *\r
+     * @param args\r
+     *            the program arguments\r
+     */\r
+    public static void main(final String[] args) {\r
+        if (args.length == 0) {\r
+            usage(I18N.tr(I18N.MAIN1, "missing mode:")\r
+                + " server | client | clientcl");\r
+        } else {\r
+            int pos0 = 0;\r
+            String arg0 = args[pos0];\r
+            String locale = Locale.getDefault().getLanguage();\r
+            I18N.switchLanguage("jatty", locale);\r
+            while (arg0 != null && arg0.startsWith("-")) {\r
+                if (arg0.startsWith("--locale=")) {\r
+                    locale = arg0.substring(9);\r
+                    if (!I18N.switchLanguage("jatty", locale)) {\r
+                        log("+++ not supported locale " + locale\r
+                            + ". Try --locale=de");\r
+                    }\r
+                } else {\r
+                    usage("unknown option: " + arg0);\r
+                }\r
+                pos0++;\r
+                arg0 = args.length < pos0 ? args[pos0] : null;\r
+            }\r
+            if (arg0 == null) {\r
+                usage(I18N.tr(I18N.MAIN1, "missing mode:")\r
+                    + " server | client | clientcl");\r
+            } else {\r
+                final Scrambler scrambler = new Scrambler("Modern Times",\r
+                    "Juwayakani", "2*5^8/3%");\r
+                final String arg1 = pos0 + 1 >= args.length ? null\r
+                    : args[pos0 + 1];\r
+                if (arg0.equals("client")) {\r
+                    final Client client = new ClientGui(getAddr(arg1),\r
+                        getPort(arg1), scrambler);\r
+                    client.run();\r
+                } else if (arg0.equals("clientcl")) {\r
+                    final Client client = new Client(getAddr(arg1),\r
+                        getPort(arg1), scrambler);\r
+                    client.run();\r
+                } else if (arg0.equals("server")) {\r
+                    final int port = arg1 == null ? getPort(null)\r
+                        : Integer.parseInt(arg1);\r
+                    final Server server = new Server(port, scrambler);\r
+                    server.run();\r
+                } else {\r
+                    usage("unknown mode: " + arg0);\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Prints a "usage" message.\r
+     *\r
+     * @param message\r
+     *            <code>null</code> or the error message\r
+     */\r
+    public static void usage(final String message) {\r
+        System.out.println(I18N.tr(I18N.USAGE, "Usage:")\r
+            + " jatty <opts> { server <port> | { client | clientcl } [<ip>[:<port>]] }\n"\r
+            + I18N.tr(I18N.USAGE2,\r
+                    "Starts a chat server, a GUI chat client or a commandline chat client.")\r
+            + "\n" + "<ip>\n\t"\r
+            + I18N.tr(I18N.USAGE3,\r
+                    "address of the chat server, e.g. localhost or")\r
+            + " 192.168.3.4\n" + "        " + I18N.tr(I18N.USAGE4, "Default:")\r
+            + " localhost\n" + "<port>\n\t"\r
+            + I18N.tr(I18N.USAGE5, "port of the server.") + "\n\t"\r
+            + I18N.tr(I18N.USAGE6, "Default:") + "58123\n"\r
+            + "<opts>:\n--locale=<locale>\n\t"\r
+            + I18N.tr(I18N.USAGE8,\r
+                    "language of the GUI. Default: system setting")\r
+            + I18N.tr(I18N.USAGE7, "Examples:") + "\n" + "jatty server\n"\r
+            + "jatty --locale=de server 8888\n"\r
+            + "jatty client localhost:8888\n"\r
+            + "jatty --locale=de client 123.4.3.55\n"\r
+            + "jatty clientcl 194.168.22.4:8888");\r
+        if (message != null)\r
+            System.out.println("+++ " + message);\r
+    }\r
+\r
+}\r
diff --git a/src/de/hamatoma/jatty/JattyDb.java b/src/de/hamatoma/jatty/JattyDb.java
new file mode 100755 (executable)
index 0000000..553c5ef
--- /dev/null
@@ -0,0 +1,17 @@
+/**\r
+ * \r
+ */\r
+package de.hamatoma.jatty;\r
+\r
+/**\r
+ * @author wk\r
+ *\r
+ */\r
+public class JattyDb {\r
+       public JattyDb(){\r
+               \r
+       }\r
+       public void initTables(){\r
+               \r
+       }\r
+}\r
diff --git a/src/de/hamatoma/jatty/Scrambler.java b/src/de/hamatoma/jatty/Scrambler.java
new file mode 100644 (file)
index 0000000..6025e92
--- /dev/null
@@ -0,0 +1,261 @@
+/**
+ *
+ */
+package de.hamatoma.jatty;
+
+/**
+ *
+ * Encoding/decoding of strings.
+ *
+ * @author hm
+ *
+ */
+public class Scrambler implements Cloneable {
+    static long[] primes = { //
+        4794328877860651L, // 11086aadb3872b
+        4958110297415603L, // 119d600308dfb3
+        4623102848326049L, // 106cb00308d9a1
+        4891108807597633L, // 1160700308de41
+        4578435188447437L, // 1044100308d8cd
+        5546234485817489L, // 13b445585e4091
+        5389897676242243L, // 132615585e3d43
+        4786884267880873L, // 1101a5585e31a9
+        5241005476646707L, // 129eaaadb38f33
+        4734771998022511L, // 10d2400308db6f
+        5278228526545621L, // 12c085585e3ad5
+        4652881288245079L, // 1087c5585e2f57
+        5181448596808493L, // 1268800308e32d
+        4920887247516739L, // 117b85585e3443
+        5360119236323197L, // 130b000308e77d
+        5471788386019861L, // 1370900308ea15
+        4645436678265319L, // 1081000308d9e7
+        4481655258710579L, // fec0aadb38233
+    };
+    public static void log(final String text) {
+        System.out.println(text);
+    }
+    public static void main(final String[] args) {
+        final Scrambler scrambler = new Scrambler("a", "b", "c");
+        test("What a wonderful world!", scrambler);
+        test(
+            ",.-;:_^\\!\"§$%&/()=?<>|ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890abcdefghijklmnopqrstuvwxyz 1234567890ßöäüÖÄÜ",
+            scrambler);
+        String text = "";
+        for (int ix = 0; ix < 5; ix++) {
+            text = "xxx" + (char) ('a' + ix);
+            log(String.format("%s: %x", text, scrambler.hash(text)));
+        }
+        text = "";
+        for (int ix = 0; ix < 5; ix++) {
+            text = text + "a";
+            log(String.format("%6s: %x", text, scrambler.hash(text)));
+        }
+        for (int ix = 0; ix < 5; ix++) {
+            text = "" + (char) ('a' + ix);
+            log(String.format("%s: %x", text, scrambler.hash(text)));
+        }
+        for (int ix = 0; ix < 5; ix++) {
+            text = (char) ('a' + ix) + "xxx";
+            log(String.format("%s: %x", text, scrambler.hash(text)));
+        }
+        scrambler.setSeed(0);
+        for (int ix = 0; ix < 5; ix++) {
+            log(String.format("%d: %x", ix, scrambler.nextLong()));
+        }
+        log("");
+    }
+    private static void test(final String text, final Scrambler scrambler) {
+        final String text2 = scrambler.encode(text);
+        final String text3 = scrambler.decode(text2);
+        if (!text.equals(text3))
+            log("not equal:\n" + text + "\n" + text3);
+
+    }
+    int callNumber = 0;
+    long factor = 7;
+    long mask = 0xabcd2233L;
+    long offset = 0x123321293456545L;
+    long savedSeed;
+    long seed = 0x19580312;
+    long trueRandomSeed = System.currentTimeMillis()
+            ^ (primes[0]);
+
+    /**
+     * Constructor.
+     *
+     * @param password1
+     *            first pass phrase
+     * @param password2
+     *            second pass phrase
+     * @param password3
+     *            3rd pass phrase
+     */
+    public Scrambler(final String password1, final String password2,
+        final String password3) {
+        if ((this.factor = hash(password1)) == 0)
+            this.factor = 0x17023947a2b3e477L;
+        this.offset = hash(password2);
+        this.mask = hash(password3);
+        // use an unitialized value:
+        final long random = Runtime.getRuntime().freeMemory();
+        this.trueRandomSeed += primes[(int) (random % 0xffffffffL)
+            % primes.length] ^ Runtime.getRuntime().maxMemory();
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#clone()
+     */
+    @Override
+    public Object clone() throws CloneNotSupportedException {
+        return super.clone();
+    }
+
+    /**
+     * Decodes a string encoded by <code>encode()</code>.
+     *
+     * @param text
+     *            encoded text
+     * @return the clear text
+     */
+    public String decode(final String text) {
+        final StringBuilder rc = new StringBuilder();
+        setSeed(text.charAt(0), text.charAt(1), text.charAt(2), text.charAt(3));
+        for (int ix = 4; ix < text.length(); ix++){
+            int cc = text.charAt(ix);
+            if (cc >= 32) {
+                final int offset = nextInt(0, 256);
+                cc = cc - 32 - offset;
+                if (cc < 0)
+                    cc += (0x1000 + 32);
+                cc += 32;
+            }
+            rc.append((char) cc);
+        }
+        return rc.toString();
+    }
+
+    /**
+     * Encodes a string.
+     *
+     * @param text
+     *            clear text
+     * @return the encoded text
+     */
+    public String encode(final String text) {
+        final StringBuilder rc = new StringBuilder();
+        // salt: 4 char:
+        switchToTrueRandom(true);
+        rc.append((char) nextInt(32, 256)).append((char) nextInt(32, 256));
+        rc.append((char) nextInt(32, 256)).append((char) nextInt(32, 256));
+        switchToTrueRandom(false);
+        setSeed(rc.charAt(0), rc.charAt(1), rc.charAt(2), rc.charAt(3));
+        for (int ix = 0; ix < text.length(); ix++){
+            int cc = text.charAt(ix);
+            if (cc >= 32) {
+                final int offset = nextInt(0, 256);
+                cc = 32 + ((cc - 32 + offset) % (0x10000 - 32));
+            }
+            rc.append((char) cc);
+        }
+        return rc.toString();
+    }
+
+    /**
+     * Converts a string into a long value.
+     *
+     * @param text
+     *            text to convert
+     * @return a long value
+     */
+    public long hash(final String text) {
+        long rc = primes[0];
+        int ixPrimes = primes.length - 1;
+        for (int ix = text.length() - 1; ix >= 0; ix--) {
+            rc = (((rc << 1) | (rc >> 63)) + primes[ixPrimes] * text.charAt(ix)
+                    + primes[ixPrimes - 1]
+                            + primes[text.charAt(ix == 0 ? 0 : ix - 1) % primes.length])
+                    ^ (primes[(text.length() - ix) % primes.length]);
+            if ((ixPrimes -= 2) <= 0)
+                ixPrimes = primes.length - 1;
+        }
+        return rc;
+    }
+
+    /**
+     * Returns a pseudo random value in a given range.
+     *
+     * @param min
+     *            the minimum value (inclusive)
+     * @param max
+     *            the maximum value (exclusive)
+     * @return a pseudo random value x with <code>min <= x < max</code>
+     */
+    public int nextInt(final int min, final int max) {
+        final int rc = min + (int) ((nextLong() >> 12) % (max - min + 1));
+        return rc;
+
+    }
+
+    /**
+     * Returns the next pseudo random value.
+     *
+     * @return a pseudo random value as long value (always not negative)
+     */
+    public long nextLong() {
+        this.seed = this.seed * this.factor + this.offset;
+        this.seed = (this.seed << 41) | (this.seed >> (64 - 41));
+        final long rc = ((this.seed ^ this.mask)
+                ^ primes[++this.callNumber % primes.length]) & 0x7fffffffffffffL;
+        return rc;
+    }
+
+    /**
+     * Resets the pseudo random generator.
+     *
+     * @param seed1
+     *            first seed value
+     * @param seed2
+     *            2nd seed value
+     * @param seed3
+     *            3rd seed value
+     * @param seed4
+     *            4th seed value
+     */
+    public void setSeed(final int seed1, final int seed2, final int seed3,
+        final int seed4) {
+        this.callNumber = 0;
+        setSeed((((long) seed1) << 56) | (((long) seed2) << 48)
+            | (((long) seed1) << 40) | (((long) seed1) << 32)
+            | (((long) seed1 + seed2) << 24) | (((long) seed2 + seed3) << 16)
+            | (((long) seed3 + seed4) << 8) + (seed1 + seed2 + seed3 + seed4));
+    }
+
+    /**
+     * Resets the pseudo random generator.
+     */
+    public void setSeed(final long seed) {
+        this.seed = primes[8] ^ seed;
+    }
+
+    /**
+     * Switch random generator from pseudo to true or vice versa.
+     *
+     * @param nowTrueRandom
+     *            <code>true</code>: the random generator will be return "true"
+     *            random now
+     */
+    private void switchToTrueRandom(final boolean nowTrueRandom) {
+        if (nowTrueRandom) {
+            this.savedSeed = this.seed;
+            this.trueRandomSeed = (this.trueRandomSeed << 17)
+                    + System.currentTimeMillis()
+                    + (this.trueRandomSeed >> (63 - 17));
+            this.seed = this.trueRandomSeed;
+        } else {
+            this.seed = this.savedSeed;
+        }
+
+    }
+}
diff --git a/src/de/hamatoma/jatty/Server.java b/src/de/hamatoma/jatty/Server.java
new file mode 100755 (executable)
index 0000000..b788d70
--- /dev/null
@@ -0,0 +1,369 @@
+/**\r
+ *\r
+ */\r
+package de.hamatoma.jatty;\r
+\r
+import java.io.BufferedReader;\r
+import java.io.DataOutputStream;\r
+import java.io.IOException;\r
+import java.io.InputStreamReader;\r
+import java.net.ServerSocket;\r
+import java.net.Socket;\r
+import java.net.SocketException;\r
+import java.util.ArrayList;\r
+import java.util.Calendar;\r
+import java.util.List;\r
+\r
+/**\r
+ * Stores the properties of a connection for one client.\r
+ *\r
+ * @author hm\r
+ *\r
+ */\r
+class Connection extends Thread {\r
+    Socket connectionSocket;\r
+    Scrambler scrambler;\r
+    Server server;\r
+    UpdateStatus updateStatus = new UpdateStatus();\r
+    String user;\r
+\r
+    public Connection(final Socket connectionSocket, final Server server) {\r
+        this.connectionSocket = connectionSocket;\r
+        this.user = this.connectionSocket.getInetAddress().getHostAddress();\r
+        this.scrambler = server.cloneScrambler();\r
+        log("new connection: " + this.user);\r
+        this.server = server;\r
+        start();\r
+    }\r
+\r
+    /**\r
+     * @return the user name of the connection\r
+     */\r
+    public String getUser() {\r
+        return this.user;\r
+    }\r
+\r
+    public void log(final String message) {\r
+        System.out.println(message);\r
+    }\r
+\r
+    /*\r
+     * (non-Javadoc)\r
+     *\r
+     * @see java.lang.Thread#run()\r
+     */\r
+    @Override\r
+    public void run() {\r
+        boolean again = true;\r
+        while (again) {\r
+            try {\r
+                final BufferedReader inFromClient = new BufferedReader(\r
+                    new InputStreamReader(\r
+                        this.connectionSocket.getInputStream()));\r
+                final DataOutputStream outToClient = new DataOutputStream(\r
+                    this.connectionSocket.getOutputStream());\r
+                try {\r
+                    String clientSentence = inFromClient.readLine();\r
+                    clientSentence = this.scrambler.decode(clientSentence);\r
+                    String answer = null;\r
+                    if (clientSentence != null) {\r
+                        if (!clientSentence.startsWith("/UPDATE"))\r
+                            log("> " + clientSentence);\r
+                        if (clientSentence.startsWith("/EXIT")) {\r
+                            again = false;\r
+                        } else if (clientSentence.startsWith("/LOG")) {\r
+                            final String[] args = clientSentence.split("\\s+");\r
+                            if (args.length > 1) {\r
+\r
+                                this.user = this.server.findUnusedUser(args[1]);\r
+                                this.server.setUpdateNoUser();\r
+                                answer = this.server.getUpdate(this.updateStatus,\r
+                                    "/STATUS " + String.format(\r
+                                        I18N.tr(I18N.WELCOME2, "welcome"),\r
+                                        this.user));\r
+                            } else {\r
+                                answer = "/ERROR "\r
+                                        + I18N.tr(I18N.USERNAME,\r
+                                                "missing username and password: e.g.")\r
+                                        + " /LOG jonny s!ekret\n";\r
+                            }\r
+                        } else if (clientSentence.startsWith("/UPDATE")) {\r
+                            answer = this.server.getUpdate(this.updateStatus, null);\r
+                        } else {\r
+                            final Calendar cal = Calendar.getInstance();\r
+                            final String line = String.format(\r
+                                "%02d:%02d:%02d %s: %s",\r
+                                cal.get(Calendar.HOUR_OF_DAY),\r
+                                cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND),\r
+                                this.user, clientSentence);\r
+                            this.server.putTalkLine(line);\r
+                            answer = this.server.getUpdate(this.updateStatus, null);\r
+                            log("< " + answer.substring(0, answer.length()));\r
+                        }\r
+                        if (answer != null)\r
+                            outToClient.writeBytes(answer);\r
+                    }\r
+                } catch (final SocketException e2) {\r
+                    log("+++ connection lost");\r
+                    again = false;\r
+                }\r
+            } catch (final IOException e) {\r
+                e.printStackTrace();\r
+                again = false;\r
+            }\r
+        }\r
+    }\r
+}\r
+\r
+/**\r
+ * A chat server.\r
+ *\r
+ * @author wk\r
+ *\r
+ */\r
+public class Server {\r
+    List<Connection> connections = new ArrayList<Connection>();\r
+    int maxTalkLines = 1024;\r
+    int port = 58123;\r
+    Scrambler scrambler;\r
+    List<String> talkLines = new ArrayList<String>();\r
+    int talkLinesOffset = 0;\r
+    /// stores the update number when the last login has been done\r
+    int updateNoUser = 0;\r
+\r
+    /**\r
+     * Constructor.\r
+     *\r
+     * @param port\r
+     *            the port listened by the server\r
+     */\r
+    public Server(final int port, final Scrambler scrambler) {\r
+        this.scrambler = scrambler;\r
+        this.port = port;\r
+    }\r
+\r
+    /**\r
+     * Search a name which is not used by another connection\r
+     *\r
+     * @param user\r
+     *            the wanted user name\r
+     * @return <code>user</code> if this name is not used<br>\r
+     *         otherwise: a modification of the username\r
+     */\r
+    public String findUnusedUser(final String user) {\r
+        String rc = user;\r
+        if (userExists(user)) {\r
+            int no = 1;\r
+            while (userExists(rc = user + no))\r
+                no++;\r
+        }\r
+        return rc;\r
+    }\r
+\r
+    /**\r
+     * @return a clone of the the scrambler\r
+     */\r
+    public Scrambler cloneScrambler() {\r
+        Scrambler rc = null;\r
+        try {\r
+            rc = (Scrambler) this.scrambler.clone();\r
+        } catch (final CloneNotSupportedException e) {\r
+            e.printStackTrace();\r
+        }\r
+        return rc;\r
+    }\r
+\r
+    /**\r
+     * Returns the data not yet reported to the client.\r
+     *\r
+     * @param updateStatus\r
+     *            IN/OUT: the "ages" of the resources\r
+     * @param message\r
+     *            <code>null</code> or an additional message to send\r
+     * @return the lines not yet sent as a string\r
+     */\r
+    public String getUpdate(final UpdateStatus updateStatus,\r
+        final String message) {\r
+        final StringBuilder buffer = new StringBuilder();\r
+        synchronized (this) {\r
+            final int count = this.talkLines.size();\r
+            final int start = updateStatus.getMessageNo()\r
+                    - this.talkLinesOffset;\r
+            if (start < count) {\r
+                for (int ix = start; ix < count; ix++) {\r
+                    buffer.append(this.scrambler.encode(this.talkLines.get(ix)))\r
+                    .append('\n');\r
+                }\r
+                updateStatus.setMessageNo(this.talkLinesOffset + count);\r
+            }\r
+            if (updateStatus.getUserNo() < this.updateNoUser) {\r
+                final StringBuilder buffer2 = new StringBuilder();\r
+                buffer2.append("/USER");\r
+                for (final Connection connection : this.connections) {\r
+                    buffer2.append(' ').append(connection.getUser());\r
+                }\r
+                buffer2.append('\n');\r
+                buffer.append(this.scrambler.encode(buffer2.toString()));\r
+                updateStatus.setUserNo(this.updateNoUser);\r
+            }\r
+        }\r
+        if (message != null) {\r
+            buffer.append(this.scrambler.encode(message)).append('\n');\r
+        }\r
+        return buffer.toString();\r
+    }\r
+\r
+    /**\r
+     * Writes a message to stdout.\r
+     *\r
+     * @param message\r
+     *            the message to write\r
+     */\r
+    public void log(final String message) {\r
+        System.out.println(message);\r
+    }\r
+\r
+    /**\r
+     * Collects the answer string list.\r
+     *\r
+     * Only the missing information will be returned.\r
+     *\r
+     * @param updateNo\r
+     *            the "age" of the message list since the last query of this\r
+     *            connection\r
+     * @param message\r
+     *            <code>null</code> or an additional line of the answer string\r
+     *            list\r
+     * @return\r
+     */\r
+    public String prepareAnswer(final int updateNo, final String message) {\r
+        final StringBuilder buffer = new StringBuilder();\r
+        synchronized (this) {\r
+\r
+        }\r
+        return buffer.toString();\r
+    }\r
+\r
+    /**\r
+     * Stores a chat message into the message list.\r
+     *\r
+     * @param line\r
+     *            the line to store\r
+     */\r
+    public void putTalkLine(final String line) {\r
+        synchronized (this) {\r
+            if (this.talkLines.size() >= this.maxTalkLines) {\r
+                this.talkLines.remove(0);\r
+                this.talkLinesOffset++;\r
+            }\r
+            this.talkLines.add(line);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Removes a connection from the connection list.\r
+     *\r
+     * @param connection\r
+     */\r
+    public void removeConnection(final Connection connection) {\r
+        this.connections.remove(connection);\r
+    }\r
+\r
+    /**\r
+     * Runs the chat server.\r
+     */\r
+    public void run() {\r
+        ServerSocket welcomeSocket = null;\r
+        log("jatty " + Jatty.VERSION);\r
+        log("0.0.0.0:" + this.port);\r
+        try {\r
+            welcomeSocket = new ServerSocket(this.port);\r
+            while (true) {\r
+                final Socket socket = welcomeSocket.accept();\r
+                final Connection connection = new Connection(socket, this);\r
+                this.connections.add(connection);\r
+            }\r
+        } catch (final Exception e) {\r
+            e.printStackTrace();\r
+        } finally {\r
+            if (welcomeSocket != null)\r
+                try {\r
+                    welcomeSocket.close();\r
+                } catch (final IOException e) {\r
+                    e.printStackTrace();\r
+                }\r
+        }\r
+        log(I18N.tr(I18N.SERVER1, "server stopped"));\r
+    }\r
+\r
+    /**\r
+     * Increments the "age" of user list.\r
+     */\r
+    public void setUpdateNoUser() {\r
+        synchronized (this) {\r
+            this.updateNoUser++;\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Tests whether a given user exists.\r
+     *\r
+     * @param user\r
+     *            username to test\r
+     * @return <code>true</code>: a user with the given name exists\r
+     */\r
+    public boolean userExists(final String user) {\r
+        boolean rc = false;\r
+        synchronized (this) {\r
+            for (final Connection connection : this.connections) {\r
+                if (connection.getUser().equals(user)) {\r
+                    rc = true;\r
+                    break;\r
+                }\r
+            }\r
+        }\r
+        return rc;\r
+    }\r
+}\r
+\r
+/**\r
+ * A utility class for storing "ages" of the changeable resources.\r
+ *\r
+ * @author hm\r
+ *\r
+ */\r
+class UpdateStatus {\r
+    int messageNo = 0;\r
+\r
+    int userNo = 0;\r
+\r
+    /**\r
+     * @return the messageNo\r
+     */\r
+    public int getMessageNo() {\r
+        return this.messageNo;\r
+    }\r
+\r
+    /**\r
+     * @return the userNo\r
+     */\r
+    public int getUserNo() {\r
+        return this.userNo;\r
+    }\r
+\r
+    /**\r
+     * @param messageNo\r
+     *            the messageNo to set\r
+     */\r
+    public void setMessageNo(final int messageNo) {\r
+        this.messageNo = messageNo;\r
+    }\r
+\r
+    /**\r
+     * @param userNo\r
+     *            the userNo to set\r
+     */\r
+    public void setUserNo(final int userNo) {\r
+        this.userNo = userNo;\r
+    }\r
+}
\ No newline at end of file