From 53beb78043b9eb4a0f81aaf7464b3a82ee9c1b5c Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Sun, 6 Feb 2022 21:41:45 +0100 Subject: [PATCH] Initial commit --- .gitignore | 28 ++++ DatabaseConnector.java | 148 +++++++++++++++++ List.java | 345 +++++++++++++++++++++++++++++++++++++++ QueryResult.java | 76 +++++++++ Queue.java | 142 ++++++++++++++++ Server.java | 359 +++++++++++++++++++++++++++++++++++++++++ package.bluej | 91 +++++++++++ wordle.db | Bin 0 -> 40960 bytes 8 files changed, 1189 insertions(+) create mode 100644 .gitignore create mode 100644 DatabaseConnector.java create mode 100644 List.java create mode 100644 QueryResult.java create mode 100644 Queue.java create mode 100644 Server.java create mode 100755 package.bluej create mode 100644 wordle.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..173985e --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# ---> Java +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt +Dokumente* + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Engine-Alpha files +engine-alpha.log diff --git a/DatabaseConnector.java b/DatabaseConnector.java new file mode 100644 index 0000000..3e48e1d --- /dev/null +++ b/DatabaseConnector.java @@ -0,0 +1,148 @@ +import java.sql.*; + +/** + *

+ * Materialien zu den zentralen NRW-Abiturpruefungen im Fach Informatik ab 2018 + *

+ *

+ * Klasse DatabaseConnector + *

+ *

+ * Ein Objekt der Klasse DatabaseConnector ermoeglicht die Abfrage und Manipulation + * einer SQLite-Datenbank. + * Beim Erzeugen des Objekts wird eine Datenbankverbindung aufgebaut, so dass + * anschließend SQL-Anweisungen an diese Datenbank gerichtet werden koennen. + *

+ * + * @author Qualitaets- und UnterstuetzungsAgentur - Landesinstitut fuer Schule + * @version 2016-01-24 + */ +public class DatabaseConnector{ + private java.sql.Connection connection; + private QueryResult currentQueryResult = null; + private String message = null; + + /** + * Ein Objekt vom Typ DatabaseConnector wird erstellt, und eine Verbindung zur Datenbank + * wird aufgebaut. Mit den Parametern pIP und pPort werden die IP-Adresse und die + * Port-Nummer uebergeben, unter denen die Datenbank mit Namen pDatabase zu erreichen ist. + * Mit den Parametern pUsername und pPassword werden Benutzername und Passwort fuer die + * Datenbank uebergeben. + */ + public DatabaseConnector(String pIP, int pPort, String pDatabase, String pUsername, String pPassword){ + //Eine Impementierung dieser Schnittstelle fuer SQLite ignoriert pID und pPort, da die Datenbank immer lokal ist. + //Auch pUsername und pPassword werden nicht verwendet, da SQLite sie nicht unterstuetzt. + try { + //Laden der Treiberklasse + Class.forName("org.sqlite.JDBC"); + + //Verbindung herstellen + connection = DriverManager.getConnection("jdbc:sqlite:"+pDatabase); + + } catch (Exception e) { + message = e.getMessage(); + } + } + + /** + * Der Auftrag schickt den im Parameter pSQLStatement enthaltenen SQL-Befehl an die + * Datenbank ab. + * Handelt es sich bei pSQLStatement um einen SQL-Befehl, der eine Ergebnismenge + * liefert, so kann dieses Ergebnis anschließend mit der Methode getCurrentQueryResult + * abgerufen werden. + */ + public void executeStatement(String pSQLStatement){ + //Altes Ergebnis loeschen + currentQueryResult = null; + message = null; + + try { + //Neues Statement erstellen + Statement statement = connection.createStatement(); + + //SQL Anweisung an die DB schicken. + if (statement.execute(pSQLStatement)) { //Fall 1: Es gibt ein Ergebnis + + //Resultset auslesen + ResultSet resultset = statement.getResultSet(); + + //Spaltenanzahl ermitteln + int columnCount = resultset.getMetaData().getColumnCount(); + + //Spaltennamen und Spaltentypen in Felder uebertragen + String[] resultColumnNames = new String[columnCount]; + String[] resultColumnTypes = new String[columnCount]; + for (int i = 0; i < columnCount; i++){ + resultColumnNames[i] = resultset.getMetaData().getColumnLabel(i+1); + resultColumnTypes[i] = resultset.getMetaData().getColumnTypeName(i+1); + } + + //Queue fuer die Zeilen der Ergebnistabelle erstellen + Queue rows = new Queue(); + + //Daten in Queue uebertragen und Zeilen zaehlen + int rowCount = 0; + while (resultset.next()){ + String[] resultrow = new String[columnCount]; + for (int s = 0; s < columnCount; s++){ + resultrow[s] = resultset.getString(s+1); + } + rows.enqueue(resultrow); + rowCount = rowCount + 1; + } + + //Ergebnisfeld erstellen und Zeilen aus Queue uebertragen + String[][] resultData = new String[rowCount][columnCount]; + int j = 0; + while (!rows.isEmpty()){ + resultData[j] = rows.front(); + rows.dequeue(); + j = j + 1; + } + + //Statement schließen und Ergebnisobjekt erstellen + statement.close(); + currentQueryResult = new QueryResult(resultData, resultColumnNames, resultColumnTypes); + + } else { //Fall 2: Es gibt kein Ergebnis. + //Statement ohne Ergebnisobjekt schliessen + statement.close(); + } + + } catch (Exception e) { + //Fehlermeldung speichern + message = e.getMessage(); + } + } + + /** + * Die Anfrage liefert das Ergebnis des letzten mit der Methode executeStatement an + * die Datenbank geschickten SQL-Befehls als Ob-jekt vom Typ QueryResult zurueck. + * Wurde bisher kein SQL-Befehl abgeschickt oder ergab der letzte Aufruf von + * executeStatement keine Ergebnismenge (z.B. bei einem INSERT-Befehl oder einem + * Syntaxfehler), so wird null geliefert. + */ + public QueryResult getCurrentQueryResult(){ + return currentQueryResult; + } + + /** + * Die Anfrage liefert null oder eine Fehlermeldung, die sich jeweils auf die letzte zuvor ausgefuehrte + * Datenbankoperation bezieht. + */ + public String getErrorMessage(){ + return message; + } + + /** + * Die Datenbankverbindung wird geschlossen. + */ + public void close(){ + try{ + connection.close(); + } catch (Exception e) { + message = e.getMessage(); + } + } + +} diff --git a/List.java b/List.java new file mode 100644 index 0000000..8c82b92 --- /dev/null +++ b/List.java @@ -0,0 +1,345 @@ + /** + *

+ * Materialien zu den zentralen NRW-Abiturpruefungen im Fach Informatik ab 2018 + *

+ *

+ * Generische Klasse List + *

+ *

+ * Objekt der generischen Klasse List verwalten beliebig viele linear + * angeordnete Objekte vom Typ ContentType. Auf hoechstens ein Listenobjekt, + * aktuellesObjekt genannt, kann jeweils zugegriffen werden.
+ * Wenn eine Liste leer ist, vollstaendig durchlaufen wurde oder das aktuelle + * Objekt am Ende der Liste geloescht wurde, gibt es kein aktuelles Objekt.
+ * Das erste oder das letzte Objekt einer Liste koennen durch einen Auftrag zum + * aktuellen Objekt gemacht werden. Ausserdem kann das dem aktuellen Objekt + * folgende Listenobjekt zum neuen aktuellen Objekt werden.
+ * Das aktuelle Objekt kann gelesen, veraendert oder geloescht werden. Ausserdem + * kann vor dem aktuellen Objekt ein Listenobjekt eingefuegt werden. + *

+ * + * @author Qualitaets- und UnterstuetzungsAgentur - Landesinstitut fuer Schule + * @version Generisch_06 2015-10-25 + */ +public class List { + + /* --------- Anfang der privaten inneren Klasse -------------- */ + + private class ListNode { + + private ContentType contentObject; + private ListNode next; + + /** + * Ein neues Objekt wird erschaffen. Der Verweis ist leer. + * + * @param pContent das Inhaltsobjekt vom Typ ContentType + */ + private ListNode(ContentType pContent) { + contentObject = pContent; + next = null; + } + + /** + * Der Inhalt des Knotens wird zurueckgeliefert. + * + * @return das Inhaltsobjekt des Knotens + */ + public ContentType getContentObject() { + return contentObject; + } + + /** + * Der Inhalt dieses Kontens wird gesetzt. + * + * @param pContent das Inhaltsobjekt vom Typ ContentType + */ + public void setContentObject(ContentType pContent) { + contentObject = pContent; + } + + /** + * Der Nachfolgeknoten wird zurueckgeliefert. + * + * @return das Objekt, auf das der aktuelle Verweis zeigt + */ + public ListNode getNextNode() { + return this.next; + } + + /** + * Der Verweis wird auf das Objekt, das als Parameter uebergeben + * wird, gesetzt. + * + * @param pNext der Nachfolger des Knotens + */ + public void setNextNode(ListNode pNext) { + this.next = pNext; + } + + } + + /* ----------- Ende der privaten inneren Klasse -------------- */ + + // erstes Element der Liste + ListNode first; + + // letztes Element der Liste + ListNode last; + + // aktuelles Element der Liste + ListNode current; + + /** + * Eine leere Liste wird erzeugt. + */ + public List() { + first = null; + last = null; + current = null; + } + + /** + * Die Anfrage liefert den Wert true, wenn die Liste keine Objekte enthaelt, + * sonst liefert sie den Wert false. + * + * @return true, wenn die Liste leer ist, sonst false + */ + public boolean isEmpty() { + // Die Liste ist leer, wenn es kein erstes Element gibt. + return first == null; + } + + /** + * Die Anfrage liefert den Wert true, wenn es ein aktuelles Objekt gibt, + * sonst liefert sie den Wert false. + * + * @return true, falls Zugriff moeglich, sonst false + */ + public boolean hasAccess() { + // Es gibt keinen Zugriff, wenn current auf kein Element verweist. + return current != null; + } + + /** + * Falls die Liste nicht leer ist, es ein aktuelles Objekt gibt und dieses + * nicht das letzte Objekt der Liste ist, wird das dem aktuellen Objekt in + * der Liste folgende Objekt zum aktuellen Objekt, andernfalls gibt es nach + * Ausfuehrung des Auftrags kein aktuelles Objekt, d.h. hasAccess() liefert + * den Wert false. + */ + public void next() { + if (this.hasAccess()) { + current = current.getNextNode(); + } + } + + /** + * Falls die Liste nicht leer ist, wird das erste Objekt der Liste aktuelles + * Objekt. Ist die Liste leer, geschieht nichts. + */ + public void toFirst() { + if (!isEmpty()) { + current = first; + } + } + + /** + * Falls die Liste nicht leer ist, wird das letzte Objekt der Liste + * aktuelles Objekt. Ist die Liste leer, geschieht nichts. + */ + public void toLast() { + if (!isEmpty()) { + current = last; + } + } + + /** + * Falls es ein aktuelles Objekt gibt (hasAccess() == true), wird das + * aktuelle Objekt zurueckgegeben, andernfalls (hasAccess() == false) gibt + * die Anfrage den Wert null zurueck. + * + * @return das aktuelle Objekt (vom Typ ContentType) oder null, wenn es + * kein aktuelles Objekt gibt + */ + public ContentType getContent() { + if (this.hasAccess()) { + return current.getContentObject(); + } else { + return null; + } + } + + /** + * Falls es ein aktuelles Objekt gibt (hasAccess() == true) und pContent + * ungleich null ist, wird das aktuelle Objekt durch pContent ersetzt. Sonst + * geschieht nichts. + * + * @param pContent + * das zu schreibende Objekt vom Typ ContentType + */ + public void setContent(ContentType pContent) { + // Nichts tun, wenn es keinen Inhalt oder kein aktuelles Element gibt. + if (pContent != null && this.hasAccess()) { + current.setContentObject(pContent); + } + } + + /** + * Falls es ein aktuelles Objekt gibt (hasAccess() == true), wird ein neues + * Objekt vor dem aktuellen Objekt in die Liste eingefuegt. Das aktuelle + * Objekt bleibt unveraendert.
+ * Wenn die Liste leer ist, wird pContent in die Liste eingefuegt und es + * gibt weiterhin kein aktuelles Objekt (hasAccess() == false).
+ * Falls es kein aktuelles Objekt gibt (hasAccess() == false) und die Liste + * nicht leer ist oder pContent gleich null ist, geschieht nichts. + * + * @param pContent + * das einzufuegende Objekt vom Typ ContentType + */ + public void insert(ContentType pContent) { + if (pContent != null) { // Nichts tun, wenn es keinen Inhalt gibt. + if (this.hasAccess()) { // Fall: Es gibt ein aktuelles Element. + + // Neuen Knoten erstellen. + ListNode newNode = new ListNode(pContent); + + if (current != first) { // Fall: Nicht an erster Stelle einfuegen. + ListNode previous = this.getPrevious(current); + newNode.setNextNode(previous.getNextNode()); + previous.setNextNode(newNode); + } else { // Fall: An erster Stelle einfuegen. + newNode.setNextNode(first); + first = newNode; + } + + } else { //Fall: Es gibt kein aktuelles Element. + + if (this.isEmpty()) { // Fall: In leere Liste einfuegen. + + // Neuen Knoten erstellen. + ListNode newNode = new ListNode(pContent); + + first = newNode; + last = newNode; + } + + } + } + } + + /** + * Falls pContent gleich null ist, geschieht nichts.
+ * Ansonsten wird ein neues Objekt pContent am Ende der Liste eingefuegt. + * Das aktuelle Objekt bleibt unveraendert.
+ * Wenn die Liste leer ist, wird das Objekt pContent in die Liste eingefuegt + * und es gibt weiterhin kein aktuelles Objekt (hasAccess() == false). + * + * @param pContent + * das anzuhaengende Objekt vom Typ ContentType + */ + public void append(ContentType pContent) { + if (pContent != null) { // Nichts tun, wenn es keine Inhalt gibt. + + if (this.isEmpty()) { // Fall: An leere Liste anfuegen. + this.insert(pContent); + } else { // Fall: An nicht-leere Liste anfuegen. + + // Neuen Knoten erstellen. + ListNode newNode = new ListNode(pContent); + + last.setNextNode(newNode); + last = newNode; // Letzten Knoten aktualisieren. + } + + } + } + + /** + * Falls es sich bei der Liste und pList um dasselbe Objekt handelt, + * pList null oder eine leere Liste ist, geschieht nichts.
+ * Ansonsten wird die Liste pList an die aktuelle Liste angehaengt. + * Anschliessend wird pList eine leere Liste. Das aktuelle Objekt bleibt + * unveraendert. Insbesondere bleibt hasAccess identisch. + * + * @param pList + * die am Ende anzuhaengende Liste vom Typ List + */ + public void concat(List pList) { + if (pList != this && pList != null && !pList.isEmpty()) { // Nichts tun, + // wenn pList und this identisch, pList leer oder nicht existent. + + if (this.isEmpty()) { // Fall: An leere Liste anfuegen. + this.first = pList.first; + this.last = pList.last; + } else { // Fall: An nicht-leere Liste anfuegen. + this.last.setNextNode(pList.first); + this.last = pList.last; + } + + // Liste pList loeschen. + pList.first = null; + pList.last = null; + pList.current = null; + } + } + + /** + * Wenn die Liste leer ist oder es kein aktuelles Objekt gibt (hasAccess() + * == false), geschieht nichts.
+ * Falls es ein aktuelles Objekt gibt (hasAccess() == true), wird das + * aktuelle Objekt geloescht und das Objekt hinter dem geloeschten Objekt + * wird zum aktuellen Objekt.
+ * Wird das Objekt, das am Ende der Liste steht, geloescht, gibt es kein + * aktuelles Objekt mehr. + */ + public void remove() { + // Nichts tun, wenn es kein aktuelle Element gibt oder die Liste leer ist. + if (this.hasAccess() && !this.isEmpty()) { + + if (current == first) { + first = first.getNextNode(); + } else { + ListNode previous = this.getPrevious(current); + if (current == last) { + last = previous; + } + previous.setNextNode(current.getNextNode()); + } + + ListNode temp = current.getNextNode(); + current.setContentObject(null); + current.setNextNode(null); + current = temp; + + //Beim loeschen des letzten Elements last auf null setzen. + if (this.isEmpty()) { + last = null; + } + } + } + + /** + * Liefert den Vorgaengerknoten des Knotens pNode. Ist die Liste leer, pNode + * == null, pNode nicht in der Liste oder pNode der erste Knoten der Liste, + * wird null zurueckgegeben. + * + * @param pNode + * der Knoten, dessen Vorgaenger zurueckgegeben werden soll + * @return der Vorgaenger des Knotens pNode oder null, falls die Liste leer ist, + * pNode == null ist, pNode nicht in der Liste ist oder pNode der erste Knoten + * der Liste ist + */ + private ListNode getPrevious(ListNode pNode) { + if (pNode != null && pNode != first && !this.isEmpty()) { + ListNode temp = first; + while (temp != null && temp.getNextNode() != pNode) { + temp = temp.getNextNode(); + } + return temp; + } else { + return null; + } + } + +} diff --git a/QueryResult.java b/QueryResult.java new file mode 100644 index 0000000..cb35dc4 --- /dev/null +++ b/QueryResult.java @@ -0,0 +1,76 @@ +/** + *

+ * Materialien zu den zentralen NRW-Abiturpruefungen im Fach Informatik ab 2018 + *

+ *

+ * Klasse QueryResult + *

+ *

+ * Ein Objekt der Klasse QueryResult stellt die Ergebnistabelle einer Datenbankanfrage mit Hilfe + * der Klasse DatabaseConnector dar. Objekte dieser Klasse werden nur von der Klasse DatabaseConnector erstellt. + * Die Klasse verfuegt ueber keinen oeffentlichen Konstruktor. + *

+ * + * @author Qualitaets- und UnterstuetzungsAgentur - Landesinstitut fuer Schule + * @version 2015-01-31 + */ +public class QueryResult{ + private String[][] data; + private String[] columnNames; + private String[] columnTypes; + + /** + * Paketinterner Konstruktor. + */ + QueryResult(String[][] pData, String[] pColumnNames, String[] pColumnTypes){ + data = pData; + columnNames = pColumnNames; + columnTypes = pColumnTypes; + } + + /** + * Die Anfrage liefert die Eintraege der Ergebnistabelle als zweidimensionales Feld + * vom Typ String. Der erste Index des Feldes stellt die Zeile und der zweite die + * Spalte dar (d.h. Object[zeile][spalte]). + */ + public String[][] getData(){ + return data; + } + + /** + * Die Anfrage liefert die Bezeichner der Spalten der Ergebnistabelle als Feld vom + * Typ String zurueck. + */ + public String[] getColumnNames(){ + return columnNames; + } + + /** + * Die Anfrage liefert die Typenbezeichnung der Spalten der Ergebnistabelle als Feld + * vom Typ String zurueck. Die Bezeichnungen entsprechen den Angaben in der MySQL-Datenbank. + */ + public String[] getColumnTypes(){ + return columnTypes; + } + + /** + * Die Anfrage liefert die Anzahl der Zeilen der Ergebnistabelle als Integer. + */ + public int getRowCount(){ + if (data != null ) + return data.length; + else + return 0; + } + + /** + * Die Anfrage liefert die Anzahl der Spalten der Ergebnistabelle als Integer. + */ + public int getColumnCount(){ + if (data != null && data.length > 0 && data[0] != null) + return data[0].length; + else + return 0; + } + +} \ No newline at end of file diff --git a/Queue.java b/Queue.java new file mode 100644 index 0000000..e7835c0 --- /dev/null +++ b/Queue.java @@ -0,0 +1,142 @@ +/** + *

+ * Materialien zu den zentralen NRW-Abiturpruefungen im Fach Informatik ab 2018 + *

+ *

+ * Generische Klasse Queue + *

+ *

+ * Objekte der generischen Klasse Queue (Warteschlange) verwalten beliebige + * Objekte vom Typ ContentType nach dem First-In-First-Out-Prinzip, d.h., das + * zuerst abgelegte Objekt wird als erstes wieder entnommen. Alle Methoden haben + * eine konstante Laufzeit, unabhaengig von der Anzahl der verwalteten Objekte. + *

+ * + * @author Qualitaets- und UnterstuetzungsAgentur - Landesinstitut fuer Schule + * @version Generisch_02 2014-02-21 + */ +public class Queue { + + /* --------- Anfang der privaten inneren Klasse -------------- */ + + private class QueueNode { + + private ContentType content = null; + private QueueNode nextNode = null; + + /** + * Ein neues Objekt vom Typ QueueNode wird erschaffen. + * Der Inhalt wird per Parameter gesetzt. Der Verweis ist leer. + * + * @param pContent das Inhaltselement des Knotens vom Typ ContentType + */ + public QueueNode(ContentType pContent) { + content = pContent; + nextNode = null; + } + + /** + * Der Verweis wird auf das Objekt, das als Parameter uebergeben wird, + * gesetzt. + * + * @param pNext der Nachfolger des Knotens + */ + public void setNext(QueueNode pNext) { + nextNode = pNext; + } + + /** + * Liefert das naechste Element des aktuellen Knotens. + * + * @return das Objekt vom Typ QueueNode, auf das der aktuelle Verweis zeigt + */ + public QueueNode getNext() { + return nextNode; + } + + /** + * Liefert das Inhaltsobjekt des Knotens vom Typ ContentType. + * + * @return das Inhaltsobjekt des Knotens + */ + public ContentType getContent() { + return content; + } + + } + + /* ----------- Ende der privaten inneren Klasse -------------- */ + + private QueueNode head; + private QueueNode tail; + + /** + * Eine leere Schlange wird erzeugt. + * Objekte, die in dieser Schlange verwaltet werden, muessen vom Typ + * ContentType sein. + */ + public Queue() { + head = null; + tail = null; + } + + /** + * Die Anfrage liefert den Wert true, wenn die Schlange keine Objekte enthaelt, + * sonst liefert sie den Wert false. + * + * @return true, falls die Schlange leer ist, sonst false + */ + public boolean isEmpty() { + return head == null; + } + + /** + * Das Objekt pContentType wird an die Schlange angehaengt. + * Falls pContentType gleich null ist, bleibt die Schlange unveraendert. + * + * @param pContent + * das anzuhaengende Objekt vom Typ ContentType + */ + public void enqueue(ContentType pContent) { + if (pContent != null) { + QueueNode newNode = new QueueNode(pContent); + if (this.isEmpty()) { + head = newNode; + tail = newNode; + } else { + tail.setNext(newNode); + tail = newNode; + } + } + } + + /** + * Das erste Objekt wird aus der Schlange entfernt. + * Falls die Schlange leer ist, wird sie nicht veraendert. + */ + public void dequeue() { + if (!this.isEmpty()) { + head = head.getNext(); + if (this.isEmpty()) { + head = null; + tail = null; + } + } + } + + /** + * Die Anfrage liefert das erste Objekt der Schlange. + * Die Schlange bleibt unveraendert. + * Falls die Schlange leer ist, wird null zurueckgegeben. + * + * @return das erste Objekt der Schlange vom Typ ContentType oder null, + * falls die Schlange leer ist + */ + public ContentType front() { + if (this.isEmpty()) { + return null; + } else { + return head.getContent(); + } + } +} diff --git a/Server.java b/Server.java new file mode 100644 index 0000000..710ecc0 --- /dev/null +++ b/Server.java @@ -0,0 +1,359 @@ +/** + *

+ * Materialien zu den zentralen NRW-Abiturpruefungen im Fach Informatik ab 2018 + *

+ *

+ * Klasse Server + *

+ *

+ * Objekte von Unterklassen der abstrakten Klasse Server ermoeglichen das + * Anbieten von Serverdiensten, so dass Clients Verbindungen zum Server mittels + * TCP/IP-Protokoll aufbauen koennen. Zur Vereinfachung finden Nachrichtenversand + * und -empfang zeilenweise statt, d. h., beim Senden einer Zeichenkette wird ein + * Zeilentrenner ergaenzt und beim Empfang wird dieser entfernt. + * Verbindungsannahme, Nachrichtenempfang und Verbindungsende geschehen + * nebenlaeufig. Auf diese Ereignisse muss durch Ueberschreiben der entsprechenden + * Ereignisbehandlungsmethoden reagiert werden. Es findet nur eine rudimentaere + * Fehlerbehandlung statt, so dass z.B. Verbindungsabbrueche nicht zu einem + * Programmabbruch fuehren. Einmal unterbrochene oder getrennte Verbindungen + * koennen nicht reaktiviert werden. + *

+ * + * @author Qualitaets- und UnterstuetzungsAgentur - Landesinstitut fuer Schule + * @version 30.08.2016 + */ +import java.net.*; +import java.io.*; + +public abstract class Server +{ + private NewConnectionHandler connectionHandler; + private List messageHandlers; + + private class NewConnectionHandler extends Thread + { + private ServerSocket serverSocket; + private boolean active; + + public NewConnectionHandler(int pPort) + { + try + { + serverSocket = new ServerSocket(pPort); + start(); + active = true; + } + catch (Exception e) + { + serverSocket = null; + active = false; + } + } + + public void run() + { + while (active) + { + try + { + //Warten auf Verbdinungsversuch durch Client: + Socket clientSocket = serverSocket.accept(); + // Eingehende Nachrichten vom neu verbundenen Client werden + // in einem eigenen Thread empfangen: + addNewClientMessageHandler(clientSocket); + processNewConnection(clientSocket.getInetAddress().getHostAddress(),clientSocket.getPort()); + } + + catch (IOException e) + { + /* + * Kann keine Verbindung zum anfragenden Client aufgebaut werden, + * geschieht nichts. + */ + } + } + } + + public void close() + { + active = false; + if(serverSocket != null) + try + { + serverSocket.close(); + } + catch (IOException e) + { + /* + * Befindet sich der ServerSocket im accept()-Wartezustand oder wurde + * er bereits geschlossen, geschieht nichts. + */ + } + } + } + + private class ClientMessageHandler extends Thread + { + private ClientSocketWrapper socketWrapper; + private boolean active; + + private class ClientSocketWrapper + { + private Socket clientSocket; + private BufferedReader fromClient; + private PrintWriter toClient; + + public ClientSocketWrapper(Socket pSocket) + { + try + { + clientSocket = pSocket; + toClient = new PrintWriter(clientSocket.getOutputStream(), true); + fromClient = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); + } + catch (IOException e) + { + clientSocket = null; + toClient = null; + fromClient = null; + } + } + + public String receive() + { + if(fromClient != null) + try + { + return fromClient.readLine(); + } + catch (IOException e) + { + } + return(null); + } + + public void send(String pMessage) + { + if(toClient != null) + { + toClient.println(pMessage); + } + } + + public String getClientIP() + { + if(clientSocket != null) + return(clientSocket.getInetAddress().getHostAddress()); + else + return(null); //Gemaess Java-API Rueckgabe bei nicht-verbundenen Sockets + } + + public int getClientPort() + { + if(clientSocket != null) + return(clientSocket.getPort()); + else + return(0); //Gemaess Java-API Rueckgabe bei nicht-verbundenen Sockets + } + + public void close() + { + if(clientSocket != null) + try + { + clientSocket.close(); + } + catch (IOException e) + { + /* + * Falls eine Verbindung getrennt werden soll, deren Endpunkt + * nicht mehr existiert bzw. ihrerseits bereits beendet worden ist, + * geschieht nichts. + */ + } + } + } + + private ClientMessageHandler(Socket pClientSocket) + { + socketWrapper = new ClientSocketWrapper(pClientSocket); + if(pClientSocket!=null) + { + start(); + active = true; + } + else + { + active = false; + } + } + + public void run() + { + String message = null; + while (active) + { + message = socketWrapper.receive(); + if (message != null) + processMessage(socketWrapper.getClientIP(), socketWrapper.getClientPort(), message); + else + { + ClientMessageHandler aMessageHandler = findClientMessageHandler(socketWrapper.getClientIP(), socketWrapper.getClientPort()); + if (aMessageHandler != null) + { + aMessageHandler.close(); + removeClientMessageHandler(aMessageHandler); + processClosingConnection(socketWrapper.getClientIP(), socketWrapper.getClientPort()); + } + } + } + } + + public void send(String pMessage) + { + if(active) + socketWrapper.send(pMessage); + } + + public void close() + { + if(active) + { + active=false; + socketWrapper.close(); + } + } + + public String getClientIP() + { + return(socketWrapper.getClientIP()); + } + + public int getClientPort() + { + return(socketWrapper.getClientPort()); + } + + } + + public Server(int pPort) + { + connectionHandler = new NewConnectionHandler(pPort); + messageHandlers = new List(); + } + + public boolean isOpen() + { + return(connectionHandler.active); + } + + public boolean isConnectedTo(String pClientIP, int pClientPort) + { + ClientMessageHandler aMessageHandler = findClientMessageHandler(pClientIP, pClientPort); + if (aMessageHandler != null) + return(aMessageHandler.active); + else + return(false); + } + + public void send(String pClientIP, int pClientPort, String pMessage) + { + ClientMessageHandler aMessageHandler = this.findClientMessageHandler(pClientIP, pClientPort); + if (aMessageHandler != null) + aMessageHandler.send(pMessage); + } + + public void sendToAll(String pMessage) + { + synchronized(messageHandlers) + { + messageHandlers.toFirst(); + while (messageHandlers.hasAccess()) + { + messageHandlers.getContent().send(pMessage); + messageHandlers.next(); + } + } + } + + public void closeConnection(String pClientIP, int pClientPort) + { + ClientMessageHandler aMessageHandler = findClientMessageHandler(pClientIP, pClientPort); + if (aMessageHandler != null) + { + processClosingConnection(pClientIP, pClientPort); + aMessageHandler.close(); + removeClientMessageHandler(aMessageHandler); + } + + } + + public void close() + { + connectionHandler.close(); + + synchronized(messageHandlers) + { + ClientMessageHandler aMessageHandler; + messageHandlers.toFirst(); + while (messageHandlers.hasAccess()) + { + aMessageHandler = messageHandlers.getContent(); + processClosingConnection(aMessageHandler.getClientIP(), aMessageHandler.getClientPort()); + aMessageHandler.close(); + messageHandlers.remove(); + } + } + + } + public abstract void processNewConnection(String pClientIP, int pClientPort); + + public abstract void processMessage(String pClientIP, int pClientPort, String pMessage); + + public abstract void processClosingConnection(String pClientIP, int pClientPort); + + private void addNewClientMessageHandler(Socket pClientSocket) + { + synchronized(messageHandlers) + { + messageHandlers.append(new Server.ClientMessageHandler(pClientSocket)); + } + } + + private void removeClientMessageHandler(ClientMessageHandler pClientMessageHandler) + { + synchronized(messageHandlers) + { + messageHandlers.toFirst(); + while (messageHandlers.hasAccess()) + { + if (pClientMessageHandler == messageHandlers.getContent()) + { + messageHandlers.remove(); + return; + } + else + messageHandlers.next(); + } + } + } + + private ClientMessageHandler findClientMessageHandler(String pClientIP, int pClientPort) + { + synchronized(messageHandlers) + { + ClientMessageHandler aMessageHandler; + messageHandlers.toFirst(); + + while (messageHandlers.hasAccess()) + { + aMessageHandler = messageHandlers.getContent(); + if (aMessageHandler.getClientIP().equals(pClientIP) && aMessageHandler.getClientPort() == pClientPort) + return (aMessageHandler); + messageHandlers.next(); + } + return (null); + } + } + +} \ No newline at end of file diff --git a/package.bluej b/package.bluej new file mode 100755 index 0000000..460a419 --- /dev/null +++ b/package.bluej @@ -0,0 +1,91 @@ +#BlueJ package file +dependency1.from=DecisionNode +dependency1.to=Dataset +dependency1.type=UsesDependency +dependency2.from=Classification +dependency2.to=Dataset +dependency2.type=UsesDependency +dependency3.from=Decision +dependency3.to=Dataset +dependency3.type=UsesDependency +dependency4.from=DecisionTreeBuilder +dependency4.to=BinaryTree +dependency4.type=UsesDependency +dependency5.from=DecisionTreeBuilder +dependency5.to=DecisionNode +dependency5.type=UsesDependency +dependency6.from=DecisionTreeBuilder +dependency6.to=Classification +dependency6.type=UsesDependency +dependency7.from=DecisionTreeBuilder +dependency7.to=Decision +dependency7.type=UsesDependency +dependency8.from=DecisionTreeBuilder +dependency8.to=Dataset +dependency8.type=UsesDependency +editor.fx.0.height=722 +editor.fx.0.width=800 +editor.fx.0.x=388 +editor.fx.0.y=50 +objectbench.height=66 +objectbench.width=1201 +package.divider.horizontal=0.6 +package.divider.vertical=0.8983286908077994 +package.editor.height=622 +package.editor.width=1078 +package.editor.x=39 +package.editor.y=24 +package.frame.height=776 +package.frame.width=1241 +package.numDependencies=8 +package.numTargets=6 +package.showExtends=true +package.showUses=true +project.charset=UTF-8 +readme.height=58 +readme.name=@README +readme.width=47 +readme.x=10 +readme.y=10 +target1.height=40 +target1.name=Classification +target1.showInterface=false +target1.type=ClassTarget +target1.width=210 +target1.x=40 +target1.y=460 +target2.height=50 +target2.name=Decision +target2.showInterface=false +target2.type=ClassTarget +target2.width=190 +target2.x=300 +target2.y=460 +target3.height=120 +target3.name=BinaryTree +target3.showInterface=false +target3.type=ClassTarget +target3.width=440 +target3.x=890 +target3.y=250 +target4.height=40 +target4.name=Dataset +target4.showInterface=false +target4.type=ClassTarget +target4.width=270 +target4.x=430 +target4.y=30 +target5.height=50 +target5.name=DecisionNode +target5.showInterface=false +target5.type=AbstractTarget +target5.width=110 +target5.x=210 +target5.y=290 +target6.height=60 +target6.name=DecisionTreeBuilder +target6.showInterface=false +target6.type=ClassTarget +target6.width=270 +target6.x=580 +target6.y=230 diff --git a/wordle.db b/wordle.db new file mode 100644 index 0000000000000000000000000000000000000000..051609c12fa92fa840824b4d6a82aa1069e084e9 GIT binary patch literal 40960 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