/**
 *  Copyright © 2025, Luis Andrés Lange <https://javacomm.net>
 *
 *  Previously released under Apache License 2.0; now licensed under MPL 2.0.
 *
 *  This Source Code Form is subject to the terms of the Mozilla Public
 *  License, v. 2.0. If a copy of the MPL was not distributed with this
 *  file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 *  ----------------------------------------------------------------------------
 *
 *  Exhibit B - "Incompatible With Secondary Licenses" Notice
 *
 *  This Source Code Form is "Incompatible With Secondary Licenses",
 *  as defined by the Mozilla Public License, v. 2.0.
 *
 *  In short:
 *  - This file may be used, modified, and distributed under MPL 2.0 only.
 *  - It may NOT be relicensed under GPL, LGPL, AGPL, or any other Secondary License.
 *
 *  Rationale:
 *  - Ensures that the code remains MPL-2.0.
 *  - Avoids legal conflicts with GPL-licensed libraries (e.g., VideoLAN).
 *  - Maximizes usability for commercial and security-critical applications.
 *
 */
package net.javacomm.server;

import static net.javacomm.protocol.HEADER.CONFIRM;
import static net.javacomm.protocol.HEADER.ERROR;
import static net.javacomm.protocol.HEADER.REQUEST;
import static net.javacomm.protocol.HEADER.RESPONSE;
import jakarta.websocket.CloseReason;
import jakarta.websocket.CloseReason.CloseCodes;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.MessageHandler;
import jakarta.websocket.SendHandler;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.PrivateKey;
import java.sql.SQLException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.crypto.SecretKey;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import net.javacomm.database.DatabaseException;
import net.javacomm.database.WebdatabaseImpl;
import net.javacomm.database.entity.EntityUser;
import net.javacomm.database.entity.Konferenzraum;
import net.javacomm.database.entity.Online;
import net.javacomm.database.entity.PrivateChatfile;
import net.javacomm.database.entity.Record;
import net.javacomm.database.entity.Screencast;
import net.javacomm.database.entity.ViewSenderSession;
import net.javacomm.facade.DatabaseService;
import net.javacomm.multilingual.MultilingualString;
import net.javacomm.multilingual.schema.ISO639;
import net.javacomm.multilingual.schema.KEY;
import net.javacomm.protocol.Agent;
import net.javacomm.protocol.BEAMEROFF;
import net.javacomm.protocol.BREAKROOMS;
import net.javacomm.protocol.BlacklistTypes;
import net.javacomm.protocol.CALLPRIVATECHAT;
import net.javacomm.protocol.CALLREMOTEUSER;
import net.javacomm.protocol.CANDIDATETOPICMEMBER;
import net.javacomm.protocol.CHANGETOPICMEMBER;
import net.javacomm.protocol.CHATMESSAGE;
import net.javacomm.protocol.CHATONLINELIST;
import net.javacomm.protocol.CHATUSERLIST;
import net.javacomm.protocol.CONFERENCE;
import net.javacomm.protocol.CONFERENCEAUDIO;
import net.javacomm.protocol.CONFERENCEMUTE;
import net.javacomm.protocol.CONFERENCEVIDEO;
import net.javacomm.protocol.CREATETEMPROOM;
import net.javacomm.protocol.ChatUser;
import net.javacomm.protocol.Command;
import net.javacomm.protocol.DELETEROOM;
import net.javacomm.protocol.DELETEUPLOADFILES;
import net.javacomm.protocol.DIAL;
import net.javacomm.protocol.DOWNLOAD;
import net.javacomm.protocol.ENTERROOM;
import net.javacomm.protocol.FILETYPES;
import net.javacomm.protocol.HISTORYMESSAGE;
import net.javacomm.protocol.IMAGE;
import net.javacomm.protocol.INCOMINGCALL;
import net.javacomm.protocol.LEAVEPRIVATECHAT;
import net.javacomm.protocol.LEAVEROOM;
import net.javacomm.protocol.MESSAGE;
import net.javacomm.protocol.MICROERROR;
import net.javacomm.protocol.MessageDecoder;
import net.javacomm.protocol.MessageEncoder;
import net.javacomm.protocol.ONCALL;
import net.javacomm.protocol.ONHOOK;
import net.javacomm.protocol.PRIVATECHATFILE;
import net.javacomm.protocol.PRIVATEMESSAGE;
import net.javacomm.protocol.PROYECTORCLOSING;
import net.javacomm.protocol.Protocol;
import net.javacomm.protocol.READROOMS;
import net.javacomm.protocol.READTOPICROOMOWNER;
import net.javacomm.protocol.ROOMLIST;
import net.javacomm.protocol.SCREENCAST;
import net.javacomm.protocol.SEARCHFILES;
import net.javacomm.protocol.STOPSENDINGVIDEO;
import net.javacomm.protocol.Searchfile;
import net.javacomm.protocol.TOPICMEMBER;
import net.javacomm.protocol.Token;
import net.javacomm.protocol.UPDATEFILETYPES;
import net.javacomm.protocol.UPDATEPHONE;
import net.javacomm.protocol.UPDATEPORT;
import net.javacomm.protocol.UPDATEUSER;
import net.javacomm.protocol.UPLOADFILES;
import net.javacomm.protocol.USERONLINELIST;
import net.javacomm.protocol.USRLOGIN;
import net.javacomm.protocol.crypto.Crypto;
import net.javacomm.restserver.Screenshot;

//Die zentrale Warteschlange für ALLES (Text & Audio)
//Wir nutzen Object, damit MESSAGE und byte[] nebeneinander existieren können.
//private static final Map<String, ConcurrentLinkedQueue<Object>> messageQueues = new ConcurrentHashMap<>();
//
////Die Zündschlösser (Besen-Logik)
//private static final Map<String, AtomicBoolean> running = new ConcurrentHashMap<>();
//
////Fehlerzähler für das automatische Schließen bei Verbindungsabbruch
//private static final Map<String, AtomicInteger> errorCounter = new ConcurrentHashMap<>();
//
////Der Threadpool, der die Worker bereitstellt
//private static final ExecutorService senderFIFO = Executors.newCachedThreadPool();



@ServerEndpoint(value = "/portal", encoders = {MessageEncoder.class}, decoders = {MessageDecoder.class})
public class Webserver extends Endpoint {

  private final static Logger log = LogManager.getLogger(Webserver.class);

  /**
   * key/value ---> (sessionId/Session) sessionId ist ein Datenbankwert
   */
  private static final ConcurrentHashMap<String, Session> sessionmap = new ConcurrentHashMap<>();

  /**
   * Nachrichtenwarteschlangen pro Session
   */
  private static final ConcurrentHashMap<String, ConcurrentLinkedQueue<Object>> messageQueues = new ConcurrentHashMap<>();
  /**
   * Ein Executor-Threadpool für die Sender
   */
  private static final ConcurrentHashMap<String, AtomicBoolean> running = new ConcurrentHashMap<>();
  /**
   * Maximale Anzahl Fehler, bevor eine Session geschlossen wird
   */
  private static final int MAX_ERRORS_BEFORE_CLOSE = 50;

  /**
   * Für CONFERENCEAUDIO
   */
  private static final ConcurrentHashMap<String, CONFERENCEAUDIO> activeStreams = new ConcurrentHashMap<>();

  /**
   * Fehlerzähler pro Session
   */
  private static final ConcurrentHashMap<String, AtomicInteger> errorCounter = new ConcurrentHashMap<>();

  // Nachrichten FIFO pro Client garantiert.
  private static ExecutorService senderFIFO;

  private static ScheduledExecutorService executorKeepAlive;
  static Runnable keepalvieRunnable = () -> {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();

    int minutos = databaseservice.forumCycle();
    int result = databaseservice.changeToForum(minutos); // in ein Forum
                                                         // wandeln 150
                                                         // Minuten

    if (result > 0) {
      // wenn gewandelt, dann Raumliste aktualisieren
      // kann auch wieder einen Zähler einbauen
      // Für Testzwecke auf 5 Minuten eingestellt
      // Roomliste übergeben
      try {
        ROOMLIST roomlist = databaseservice.fetchRoomlist(); // Alle vorhandenen Chaträume werden ermittelt
        // sende eine vollständige Raumliste an alle Anwender
        // ROMMLIST beschreibt die Räume(Pausenraum, Forum etc.) und nnicht die Anzahl
        // der User und deren Attribute JLauncher Alle Räume anzeigen
        broadcastAll(roomlist);
      }
      catch (DatabaseException e2) {
        log.warn(e2.getMessage(), e2.getCause());
        return;
      }
    }

    try {
      DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG)
          .withLocale(Locale.getDefault());

      CloseReason reason = new CloseReason(CloseCodes.NORMAL_CLOSURE, "KeepAlive exprired");

      List<Online> online = Collections.synchronizedList(databaseservice.deleteOnlineKeepalive());
      online.forEach(user -> {

        ZonedDateTime zdtKeepalive = user.getKeepalive().withZoneSameInstant(ZoneId.systemDefault());
        String datumKeepAlive = zdtKeepalive.format(formatter);

        log.info(datumKeepAlive);
        log.info(
            "keepalive Zeitüberschreitung vom user mit der Session = " + user.getSession()
                + " / "
                + user.getUid()
        );

        Screencast screencast = databaseservice.getScreencastByReceiverSession(user.getSession());
        if (screencast.getSenderSession() != null) {
          // Empfänger verliert Verbindung
          String senderuid = databaseservice.getUserBySession(screencast.getSenderSession());
          if (senderuid != null) {
            log.info("BEAMER senderuid=" + user.getUid());
            // BEAMEROFF senden, wenn eine Bildschirmübertragung läuft, an wen senden?
            // Wenn der user in der Tabelle TB_SCREENCAST als RECEIVER eingetragen ist, dann
            // BEMEROFF an den SENDER schicken
            BEAMEROFF beamer = new BEAMEROFF();
            beamer.setCommand(Command.BEAMEROFF);
            beamer.setHeader(REQUEST);
            beamer.setDataset(Protocol.DATASET);
            beamer.setUserid(senderuid);
            sendMessage(beamer, screencast.getSenderSession());
          }
        }
        else {
//          log.info("PROYECTORCLOSING senderuid=" + user.getUid());
          String callerSession = databaseservice.getSessionidByUserid(user.getUid());
          Screencast screencast2 = databaseservice.getScreencastByCallerSession(callerSession);
//          log.info("callerSession=" + callerSession);
          String receiverSession = screencast2.getReceiverSession();
//          log.info("receiverSession=" + receiverSession);
          if (receiverSession != null) {
            // Sender verliert Verbindung
            // muss PROYECTORCLOSING sein
            // PROYECTORCLOSING senden
            PROYECTORCLOSING proyector = new PROYECTORCLOSING();
            proyector.setCommand(Command.PROYECTORCLOSING);
            proyector.setHeader(REQUEST);
            proyector.setDataset(Protocol.DATASET);
            proyector.setUserid(user.getUid());
            log.info(proyector.toString());
            sendMessage(proyector, receiverSession);
            databaseservice.deleteScreencastUser(receiverSession); // Lösche aus TB_SCREENCAST
          }
        }
        // darf nicht vor getScreencastByReceiverSession stehen, weil die
        // RECEIVERSESSION schon gelöscht wurde
        // muss zuletzt gelöscht werden, wegen xxxSessionidByUserid()

        cleanUser(user.getUid(), user.getSession());

        Session closeSession = sessionmap.get(user.getSession());
        try {
          closeSession.close(reason);
        }
        catch (IOException e) {
          log.info(e.getMessage(), e);
        }
      });
    }
    catch (SQLException e) {
      log.error(e);
    }
  };

  static Runnable activeStreamsCleaner = () -> {
    try {
      for (Map.Entry<String, CONFERENCEAUDIO> entry : activeStreams.entrySet()) {
        CONFERENCEAUDIO meta = entry.getValue();
        if (meta.getReceiverSessions() == null) continue;

        List<String> validReceivers = new ArrayList<>();
        for (String receiver : meta.getReceiverSessions()) {
          Session s = sessionmap.get(receiver);
          boolean closed = s == null || Boolean.TRUE.equals(s.getUserProperties().get("closed"));
          if (!closed) {
            validReceivers.add(receiver);
          }
          else {
            log.debug("Cleaner: entferne verwaiste Receiver-Session {}", receiver);
          }
        }

        if (validReceivers.isEmpty()) {
          log.debug("Cleaner: entferne kompletten Stream {}", entry.getKey());
          activeStreams.remove(entry.getKey());
        }
        else if (validReceivers.size() != meta.getReceiverSessions().length) {
          meta.setReceiverSessions(validReceivers.toArray(new String[0]));
        }
      }
    }
    catch (Exception e) {
      log.warn("Fehler im activeStreamsCleaner: {}", e.getMessage(), e);
    }
  };

  /**
   * Sende eine Nachricht an alle Anwender.
   * 
   * @param message
   *                diese Nachricht wird gesendet
   */
  public static void broadcastAll(MESSAGE message) {
    ArrayList<Session> snapshot = new ArrayList<>(sessionmap.values());
    for (Session client : snapshot) {
      sendMessage(message, client.getId());
    }
  }



  /**
   * Lösche den User aus einer Reihe von Tabellen.
   * 
   * @param userid
   *                  dieser Anwender wird bereinigt
   * @param sessionid
   *                  unter dieser Sessionid war der User angemeldet
   */
  private synchronized static void cleanUser(String userid, String sessionid) {

    // muss geprüft werden
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();

    databaseservice.deleteUserChatrooms(userid);
    databaseservice.deleteTransferLib(userid);

    // War der der der letzte in einem Temproom
    // Durchsuche alle Temprooms, ob sie leer sind
    // Wenn ja, lösche den Temproom und alle Messages

    // select all TEMPROOMS
    List<String> temproom = databaseservice.fetchTemproomlist();
    for (String room : temproom) {
      int countUser = databaseservice.countTemproomUser(room);
      // deleteChatroom
      if (countUser == 0) {
        databaseservice.deleteTemproom(room);
      }
      // User wird aus einem Pausenraum/Group herausgenommen
      databaseservice.remove(userid, room);
    }

    // die privatechats müssen auch gelöscht werden!
    // die Gegenseite muss benachrichtigt werden
    // input sessionID dieser User ist gegangen
    String pseudoid = databaseservice.fetchPseudoidBySessionid(sessionid);
    List<String> counterIds = databaseservice.deleteAllPrivateChats(pseudoid); // pseudoid statt sessionid
    LEAVEPRIVATECHAT leavePrivatechat = new LEAVEPRIVATECHAT();
    leavePrivatechat.setCommand(Command.LEAVEPRIVATECHAT);
    leavePrivatechat.setHeader(REQUEST);
    leavePrivatechat.setDataset(Protocol.DATASET);
    leavePrivatechat.setRemotePseudoid(pseudoid);
    log.info("----->" + leavePrivatechat.toString());

    List<String> sessionIds = counterIds.stream()
        .map(pseudo -> databaseservice.fetchSessionByPseudoid(pseudo)).collect(Collectors.toList());
    sendMessage(leavePrivatechat, sessionIds);

//  Telefonate
    // Hole den Partner von uid
    String partneruid = databaseservice.findPartnerUserid(userid);
    if (partneruid != null) {
      databaseservice.setOnBusy(partneruid, false);
      log.debug("partneruid benachrichtigen, dass die Verbindung weg ist");

      // uid hat aufgelegt
      // uid!=partneruid
      boolean isCaller = databaseservice.isPartnerCaller(partneruid); // true partneruid ist calleruid
      ONHOOK onhook = new ONHOOK();
      onhook.setCommand(Command.ONHOOK);
      onhook.setHeader(REQUEST);
      onhook.setDataset(Protocol.DATASET);
      onhook.setCallerUserid(isCaller ? partneruid : userid); // Wer ist Anrufer?
      onhook.setReceiverUserid(isCaller ? userid : partneruid); // Wer ist Empfänger
      onhook.setTo(isCaller ? false : true); // Wer hat aufgelegt? // true der Caller hat aufgelegt
      onhook.setSignal(true);
      log.info("-----> " + onhook.toString());
      String key = databaseservice.getSessionidByUserid(partneruid);
      sendMessage(onhook, key);
    }

    // Der Gegenüber müsste benachrichtigt werden, dass ein plötzlicher Ausfall
    // vorliegt. Die Gegenseite muss benachrichtig werden, dass der Gesprächspartner
    // nicht mehr online ist

    Konferenzraum konferenzraum = databaseservice.fetchOnlineParticipant(userid);
    if (konferenzraum != null) {
      // ich habe einen Anwender gefunden, der bis jetzt online war.
      databaseservice.updateKonferenzraumOnlinestatus(
          konferenzraum.getKonferenzraum(), konferenzraum.getOrganisator(), userid, false
      );

      try {

        // CONFERENCEVIDEO Finde alle Empfänger von USERID; falls gefunden
        // CONFERENECEVIDEO SHARING=FALSE
        List<String> receivers = databaseservice.fetchReceiversBySender(userid);
        if (!receivers.isEmpty()) {
          String nickname = databaseservice.fetchNickname(userid);
          CONFERENCEVIDEO conferencevideo = new CONFERENCEVIDEO();
          conferencevideo.setCommand(Command.CONFERENCEVIDEO);
          conferencevideo.setHeader(REQUEST);
          conferencevideo.setDataset(Protocol.DATASET);
          conferencevideo.setKonferenzraum(konferenzraum.getKonferenzraum());
          conferencevideo.setNickname(nickname);
          // TODO As of version 260501-PRE3
//          conferencevideo.setUserid(userid);
          conferencevideo.setReceiverSessions(receivers.toArray(String[]::new));
          conferencevideo.setShared(false);
          log.info("-----> {}", conferencevideo.toString());
          sendMessage(conferencevideo, receivers);
        }

        String senderFromReceiver = databaseservice.senderFromReceiver(userid);
        // senderFromReceiver kann null sein
        boolean genau1x = databaseservice.countVideosender(senderFromReceiver);
        log.info("genau1x=" + genau1x);
        if (genau1x) {
          // Lösche aus TB_VIDEO_SENDER wird von STOPSENDINGVIDEO über den Client
          // ausgelöst
          // databaseservice.deleteVideoSenderUser(userid);

          // Sende STOPSENDINGVIDEO
          STOPSENDINGVIDEO stopSendingVideo = new STOPSENDINGVIDEO();
          stopSendingVideo.setCommand(Command.STOPSENDINGVIDEO);
          stopSendingVideo.setHeader(REQUEST);
          stopSendingVideo.setDataset(Protocol.DATASET);
          stopSendingVideo.setUserid(senderFromReceiver);

          log.info("-----> " + stopSendingVideo.toString());

          // Hole Sessionid vom Bildersender
          String bildersender = databaseservice.getSessionidByUserid(senderFromReceiver);
          sendMessage(stopSendingVideo, bildersender);
        }
      }
      catch (DatabaseException e) {
        log.info(e.getMessage());
      }
      databaseservice.deleteVideoReceiver(userid);
      // hier kann der Projektor gelöscht werden, wenn offhook=false ist. Nur
      // ein Receiver kann einen eingeschalteten Projektor haben

      // Lösche alle verwaisten Sender aus TB_VIDEO_SENDER
      // Wenn der Sender verwaist zurückbleibt, dann lösche ihn und alle anderen, die
      // du findest
      databaseservice.deleteOrphainedSender();

      databaseservice.switchProjektor(false, userid);

      // CONFERENCE
      CONFERENCE response = databaseservice
          .readKonferenzuser(konferenzraum.getKonferenzraum(), konferenzraum.getOrganisator());
      response.setOffhook(false);
      response.setUserid(userid);

      // in der RESPONSE muss die Session für die uerserid fehlen, weil der User
      // gegangen ist. Die Session wird später aus der TB_ONLINE gelöscht.

      // TODO muss geprüft werden, weil userid fehlt, deshalb ist der Filter
      // wirkungslos
      Arrays.stream(response.getKonferenzraumUser())
          .filter(konferenzuser -> userid.equals(konferenzuser.getUserid()))
          .forEach(found -> found.setSession(null));

      // an alle Sessions eine Antwort senden
      // response ---> CONFERENCE sendet an alle Teilnehmer in einem Konferenzraum
      // in offhook steht derjenige, der aufgelegt oder abgelegt hat
      List<String> sessions = databaseservice
          .fetchKonferenzraumSessions(konferenzraum.getKonferenzraum(), konferenzraum.getOrganisator());
      log.info("Ausgang <----- " + response.toString());
      sendMessage(response, sessions);

    }
    databaseservice.deleteScreencastUser(sessionid);
    databaseservice.switchProjektor(false, userid);
    databaseservice.deleteVideoUser(userid); // Lösche aus TB_VIDEO
    databaseservice.deleteVideoSenderUser(userid); // Lösche aus TB_VIDEO_SENDER
    databaseservice.deletePartner(userid); // TB_TOPHONE
    // muss zuletzt gelöscht werden, wegen xxxSessionidByUserid()
    databaseservice.deleteOnline(userid); // Lösche user aus der TB_ONLINE
    // Alle Einträge in transfer_lib vom User löschen
    databaseservice.deleteUserChatfiles(userid);
    Screenshot.removeScreenshot(userid);

  }



  /**
   * 
   * @param sessionId
   *                  diese sessionId
   */
  private static void handleError(String sessionId) {
    log.error("[Dispatcher] Session {} entfernt (geschlossen oder tot)", sessionId);
    sessionmap.remove(sessionId);
    messageQueues.remove(sessionId);
    running.remove(sessionId);
  }



  /**
   * Der Empfänger wird in einen FIFO gestellt. Sendewarteschlange pro Session.
   *
   * @param client
   *               an diesen Client wird gesendet
   */
  /**
   * Der Empfänger wird in einen FIFO gestellt. Sendewarteschlange pro Session.
   * Verarbeitet nun sowohl MESSAGE-Objekte (Text) als auch byte[] (Audio).
   *
   * @param client
   *               an diesen Client wird gesendet
   */
  /**
   * Der Empfänger wird in einen FIFO gestellt. Sendewarteschlange pro Session.
   * Verarbeitet nun sowohl MESSAGE-Objekte (Text) als auch byte[] (Audio).
   *
   * @param client
   *               an diesen Client wird gesendet
   */
  private static void processQueue(Session client) {
    String sessionId = client.getId();

    // Aktuelle Session validieren
    Session current = sessionmap.get(sessionId);
    if (current == null || !current.isOpen()) {
      log.warn("[Dispatcher] Stale or closed session {} – Queue wird verworfen", sessionId);
      messageQueues.remove(sessionId);
      AtomicBoolean runningFlag = running.get(sessionId);
      if (runningFlag != null) runningFlag.set(false);
      return;
    }

    ConcurrentLinkedQueue<Object> queue = messageQueues.get(sessionId);
    if (queue == null) return;

    AtomicBoolean runningFlag = running.get(sessionId);

    try {

//      int currentQueueSize = queue.size();
//      if (currentQueueSize > 50) {
//          log.warn("[Performance] Session {} hat hohen Backpressure! Queue-Größe: {}", sessionId, currentQueueSize);
//          // Optional: Wenn die Queue extrem voll ist (z.B. > 100), könnte man hier alte Audio-Pakete 
//          // am Kopf der Queue entfernen, um Platz für Kommandos zu machen.
//      }

      Object data = queue.poll();
      if (data == null) {
        if (runningFlag != null) runningFlag.set(false);
        return;
      }

      if (!current.isOpen()) {
        if (runningFlag != null) runningFlag.set(false);
        return;
      }

      // Gemeinsamer SendHandler für Text und Audio
      SendHandler handler = result -> {
        try {
          if (!result.isOK()) {
            int errors = errorCounter.computeIfAbsent(sessionId, id -> new AtomicInteger(0))
                .incrementAndGet();

            if (errors >= 45) {
              log.warn(
                  "[Dispatcher] Fehler beim Senden an Session {} (Fehler #{}/{})", sessionId, errors,
                  MAX_ERRORS_BEFORE_CLOSE
              );
            }

            if (errors >= MAX_ERRORS_BEFORE_CLOSE) {
              log.error("[Dispatcher] Session {} entfernt nach {} Fehlern", sessionId, errors);
              handleError(sessionId);
              errorCounter.remove(sessionId);
              if (current.isOpen()) {
                try {
                  current.close();
                }
                catch (IOException e) {
                  log.info(e.getMessage());
                }
              }
            }
          }
          else {
            errorCounter.remove(sessionId);
          }
        }
        finally {
          // Den nächsten Sendevorgang erst anstoßen, wenn dieser komplett fertig ist
          if (current.isOpen() && !queue.isEmpty()) {
            senderFIFO.execute(() -> processQueue(current));
          }
          else {
            if (runningFlag != null) runningFlag.set(false);
          }
        }
      };

      // Typ-Unterscheidung für den Versand
      if (data instanceof byte[]) {
        current.getAsyncRemote().sendBinary(ByteBuffer.wrap((byte[]) data), handler);
      }
      else if (data instanceof MESSAGE) {
        current.getAsyncRemote().sendObject(data, handler);
      }
      else {
        log.warn("[Dispatcher] Unbekannter Typ {} in Queue", data.getClass().getSimpleName());
        senderFIFO.execute(() -> processQueue(current));
      }

    }
    catch (Exception e) {
      log.error("[Dispatcher] Exception beim Senden an Session {}: {}", sessionId, e.getMessage());
      if (current.isOpen()) {
        handleError(sessionId);
        errorCounter.remove(sessionId);
        try {
          current.close();
        }
        catch (IOException e1) {
          log.info(e1.getMessage());
        }
      }
      if (runningFlag != null) runningFlag.set(false);
    }
  }



  /**
   * Sende eine Botschaft an viele Anwender aber nicht an alle Anwender.
   * 
   * @param message
   *                         diese Botschaft
   * @param receiverSessions
   *                         an diese Empfänger wird gesendet
   */
  public static void sendMessage(MESSAGE message, List<String> receiverSessions) {
    for (String receiver : receiverSessions) {
      sendMessage(message, receiver);
    }
  }



  /**
   * Der Server sendet eine einzelne Botschaft an den Client. Diese Methode ist
   * threadsafe.
   * 
   * 
   * @param message
   *                diese Botschaft
   * @param client
   *                dieser Client ist der Empfänger
   */
  public static void sendMessage(MESSAGE message, Session client) {
//    alte Session-Objekte können noch im Speicher existieren, auch wenn die Online-Tabelle bereits nur die aktuelle Session zeigt
//
//    Dein sendMessage() benutzt das Session-Objekt, nicht die Session-ID
//
//    messageQueues und processQueue laufen über das Session-Objekt
//
//    Das ist die einzige Stelle, wo ein „stale reference“-Problem auftreten kann, trotz Einmal-Anmeldung und funktionierender Online-Tabelle.

    if (client == null || !client.isOpen()) {
      log.warn("[Dispatcher] Session ist null oder geschlossen, Nachricht verworfen");
      return;
    }

    // Queue für die Session anlegen
    messageQueues.computeIfAbsent(client.getId(), id -> new ConcurrentLinkedQueue<>()).add(message);

    // Läuft schon ein Worker? Falls nein: starten
    running.computeIfAbsent(client.getId(), id -> new AtomicBoolean(false));
    if (running.get(client.getId()).compareAndSet(false, true)) {
      senderFIFO.execute(() -> processQueue(client));
    }
  }



  /**
   * Der Server sendet eine einzelne Botschaft an den Client. Diese Methode ist
   * threadsafe.
   * 
   * @param message
   *                  diese Botschaft
   * @param sessionid
   *                  an diesen Client wird gesendet
   */
  private static void sendMessage(MESSAGE message, String sessionid) {
    Session client = sessionmap.get(sessionid);
    if (client == null) {
      sessionmap.remove(sessionid);
      return;
    }
    sendMessage(message, client);
  }



  /**
   * Der Server wird heruntergefahren und alle offenen Threads beendet.
   */
  public static void shutdown() {
    executorKeepAlive.shutdown();
    try {
      boolean done = executorKeepAlive.awaitTermination(5, TimeUnit.SECONDS);
      if (!done) {
        executorKeepAlive.shutdownNow();
      }
    }
    catch (InterruptedException e) {
      log.error(e.getMessage(), e.getCause());
    }
    log.info("KEEPALIVE is down");

    senderFIFO.shutdown();
    try {
      boolean done = senderFIFO.awaitTermination(2, TimeUnit.SECONDS);
      if (!done) {
        senderFIFO.shutdownNow();
      }
    }
    catch (InterruptedException e) {
      log.error(e.getMessage(), e.getCause());
    }
    log.info("senderFIFO is down");

  }



  /**
   * Die Application wird gerade gestartet und als erstes werden die
   * Hintergrundthreads gestartet.
   */
  public static void startup() {
    executorKeepAlive = Executors.newScheduledThreadPool(
        2, runnable -> new Thread(Thread.currentThread().getThreadGroup(), runnable, "keepalive")
    );
    executorKeepAlive.scheduleWithFixedDelay(keepalvieRunnable, 20, 60, TimeUnit.SECONDS);
    executorKeepAlive.scheduleWithFixedDelay(activeStreamsCleaner, 30, 120, TimeUnit.SECONDS);
    senderFIFO = Executors.newCachedThreadPool();
  }



  /**
   * Jeder Login startet eine neue Instance.
   */
  public Webserver() {
    log.info("new client instance has started...");
  }



  /**
   * Chatuserliste wird gesendet;CHATUSERLIST fetchRoomlist wird gesende;ROOMLIST
   * Onlineliste wird gesendet;ONLINELIST
   * 
   * @param chatRaume
   *                  diese Räume werden aktualisiert
   */
  private void broadcast(List<String> chatRaume) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    final Logger log = LogManager.getLogger(Webserver.class);

    // an alle Anwender wird gesendet und nicht nur die Anwender im Raum
    // die Benutzerlsiten(Nicknames) werden aktualisert
    // die Aktualisierung bezieht sich nur auf bestimmte Räume

    chatRaume.forEach(room -> {
      CHATUSERLIST chatuserlist = databaseservice.fetchChatuserlist(room);
      // muss noch optimiert werden, weil er an alle Anwender sendet
      // er soll nur an die Anwender in einem Raum senden
      // TODO wenn die user/Sessions ermittelt wurden, ist alles vorhanden
      broadcastAll(chatuserlist);
    });

    // Roomliste übergeben
    ROOMLIST roomlist;
    try {
      roomlist = databaseservice.fetchRoomlist(); // Alle vorhandenen Chaträume werden ermittelt
    }
    catch (DatabaseException e2) {
      log.warn(e2.getMessage(), e2.getCause());
      return;
    }
    // sende eine vollständige Raumliste an alle Anwender
    // ROMMLIST beschreibt die Räume(Pausenraum, Forum etc.) und nicht die Anzahl
    // der User und deren Attribute JLauncher Alle Räume anzeigen
    broadcastAll(roomlist);
    // Onlinelist übergeben Onlinelist hat einen Request
    // Ermittle die Anzehl aller User in einem Raum
    // Zurückgegeben werden der Raum und die Anzahl der Anwender in dem Raum
    CHATONLINELIST onlinelist;
    try {
      onlinelist = databaseservice.fetchOnlinelist();
    }
    catch (DatabaseException e1) {
      log.warn(e1.getMessage(), e1.getCause());
      return;
    }
    // Sende an alle Anwender
    broadcastAll(onlinelist);
  }



  /**
   * 
   * Bekommt eine positiv response, wenn die Gegenstelle nicht bereit ist.
   * Rufnummerunterdrückung Bekommt eine error response, wenn die gegenstelle
   * offline ist
   * 
   * @param request
   * @return
   */
  private CALLPRIVATECHAT callPrivatechat(CALLREMOTEUSER remoteuser) {
    if (CONFIRM == remoteuser.getHeader()) {
      CALLPRIVATECHAT call = new CALLPRIVATECHAT();
      call.setCommand(Command.CALLPRIVATECHAT);
      call.setHeader(CONFIRM);
      call.setDataset(Protocol.DATASET);
      call.setRemoteNickname(remoteuser.getRemoteNickname());
      call.setRemotePseudoid(remoteuser.getRemotePseudoid());
      return call;
    }
    else if (RESPONSE == remoteuser.getHeader()) {
      CALLPRIVATECHAT call = new CALLPRIVATECHAT();
      call.setCommand(Command.CALLPRIVATECHAT);
      call.setHeader(RESPONSE);
      call.setDataset(Protocol.DATASET);
      call.setLocalNickname(remoteuser.getLocalNickname());
      call.setPseudoid(remoteuser.getPseudoid());
      call.setRemoteNickname(remoteuser.getRemoteNickname());
      call.setRemotePseudoid(remoteuser.getRemotePseudoid());
      call.setMultilingualkey(KEY.SERVER_LEHNT_CHATANFRAGEN_AB);
      return call;
    }
    CALLPRIVATECHAT call = new CALLPRIVATECHAT();
    return call;

  }



  private CANDIDATETOPICMEMBER candidateMember(CANDIDATETOPICMEMBER request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    CANDIDATETOPICMEMBER response = databaseservice.fetchCandidatemember(request);
    return response;
  }



  private CHANGETOPICMEMBER changeTopicmember(CHANGETOPICMEMBER request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    CHANGETOPICMEMBER response = databaseservice.changeMitglieder(
        request.getUserid(), request.getRoom(), request.getRoomtype(), request.getChatUser()
    );
    return response;
  }



  private CHATMESSAGE chatmessage(CHATMESSAGE request) {
    CHATMESSAGE response = new CHATMESSAGE();
    if (REQUEST == request.getHeader()) {
      DatabaseService databaseservice = WebdatabaseImpl.getInstance();
      log.info("<----- {}", request.toString());
      try {
        Record entity = databaseservice.insertChatmessage(
            request.getRoom(), request.getChatUser().getUserid(), request.getAttachment(),
            request.getMessage()
        );

        response.setCommand(Command.CHATMESSAGE);
        response.setHeader(RESPONSE);
        response.setDataset(Protocol.DATASET);
        response.setFilename(request.getFilename()); // Protocollaänderunge nicht übertragen
        response.setFilesize(request.getFilesize()); // Protocollaänderunge nicht übertragen

        ChatUser user = new ChatUser();
        user.setBackgroundColor(request.getChatUser().getBackgroundColor());
        user.setForegroundColor(request.getChatUser().getForegroundColor());
        user.setNickname(request.getChatUser().getNickname());

        response.setChatUser(user);
        response.setRoom(entity.getChatid());
        response.setAttachment(entity.getAnlage());
        response.setMessage(entity.getMessage());

        // Zeit ist nicht aus der Datenbank
        ZonedDateTime tz = ZonedDateTime.now(ZoneId.of(DatabaseService.TIMEZONE));
        long value = tz.toInstant().toEpochMilli();
        response.setDatetime(value);

        if (response.getAttachment() != null) databaseservice.updateDeletable(true, response.getAttachment());
        return response;
      }
      catch (DatabaseException e) {
        log.info(e.getMessage());
        response = new CHATMESSAGE();
        response.setCommand(Command.CHATMESSAGE);
        response.setHeader(ERROR);
        response.setDataset(Protocol.DATASET);
        response.setErrorMessage(e.getMessage());
      }
    }
    else {
      response.setCommand(Command.CHATMESSAGE);
      response.setHeader(ERROR);
      response.setDataset(Protocol.DATASET);
      response.setErrorMessage("CHATMESSAGE ist kein REQUEST");
    }
    return response;
  }



  CREATETEMPROOM createTemproom(CREATETEMPROOM request) {
    if (REQUEST == request.getHeader()) {
      DatabaseService databaseservice = WebdatabaseImpl.getInstance();
      CREATETEMPROOM response = databaseservice.createRoom(request);
      return response;
    }
    return new CREATETEMPROOM();
  }



  private DELETEROOM deleteRoom(DELETEROOM request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    boolean done = databaseservice.deleteRoom(request.getRoom(), request.getUserid());
    DELETEROOM response = new DELETEROOM();
    if (done) {
      response.setCommand(Command.DELETEROOM);
      response.setHeader(RESPONSE);
      response.setDataset(Protocol.DATASET);
      response.setRoom(request.getRoom());
      response.setUserid(request.getUserid());
      StringBuilder buffer = new StringBuilder();
      buffer.append("Der Besprechungsraum <b><span style=\"color:red\">");
      buffer.append(request.getRoom());
      buffer.append("</b></span> wurde gelöscht.");
      response.setText(buffer.toString());
    }
    else {
      response.setCommand(Command.DELETEROOM);
      response.setHeader(ERROR);
      response.setDataset(Protocol.DATASET);
      response.setRoom(request.getRoom());
      response.setUserid(request.getUserid());
      StringBuilder buffer = new StringBuilder();
      buffer.append("Diesen Besprechungsraum <b><span style=\"color:red\">");
      buffer.append(request.getRoom());
      buffer.append("</b></span> gibt es nicht.");
      response.setText(buffer.toString());
    }
    return response;
  }



  private DELETEUPLOADFILES deleteuploadfiles(DELETEUPLOADFILES request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    try {
      databaseservice.deleteUploadfiles(request.getUserid(), request.getIp());
      DELETEUPLOADFILES deletefiles = new DELETEUPLOADFILES();
      deletefiles.setCommand(Command.DELETEUPLOADFILES);
      deletefiles.setHeader(CONFIRM);
      return deletefiles;
    }
    catch (SQLException e) {
      log.warn(e.getMessage(), e.getCause());
      DELETEUPLOADFILES deletefiles = new DELETEUPLOADFILES();
      deletefiles.setCommand(Command.DELETEUPLOADFILES);
      deletefiles.setHeader(ERROR);
      deletefiles.setDataset(Protocol.DATASET);
      deletefiles.setErrorCode(e.getErrorCode());
      deletefiles.setErrorMessage(e.getMessage());
      deletefiles.setText("A database error has occured.");
      return deletefiles;
    }
  }



  /**
   * Der Anwender mit der {@code sessionid} ist gegangen.
   * 
   * @param sessionid
   *                  ein eindeutiger Onlineschlüssel
   */
  private void disconnect(String sessionid) {
    // Session markieren
    Session session = sessionmap.remove(sessionid);
    if (session != null) {
      session.getUserProperties().put("closed", true);
    }

    // Nachrichtenwarteschlange wegräumen
    messageQueues.remove(sessionid);

    log.info("Der Anwender {} ist gegangen", sessionid);

    // Räume/DB aktualisieren
    DatabaseService db = WebdatabaseImpl.getInstance();
    List<String> roomlist = db.fetchRoomlist(sessionid);
    removeUser(sessionid);
    broadcast(roomlist);

    // activeStreams bleibt unangetastet → wird "lazy" bereinigt
  }



  private HISTORYMESSAGE historymessage(HISTORYMESSAGE request) {
    if (REQUEST == request.getHeader()) {
      DatabaseService databaseservice = WebdatabaseImpl.getInstance();
      HISTORYMESSAGE response = databaseservice.fetchHistorymessage(request.getRoom());
      return response;
    }
    HISTORYMESSAGE history = new HISTORYMESSAGE();
    return history;
  }



  private ONCALL onCall(ONCALL request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    databaseservice.userOnCall(request.getUserid(), request.isOnCall());

    ONCALL response = new ONCALL();
    response.setCommand(request.getCommand());
    response.setDataset(request.getDataset());
    response.setHeader(RESPONSE);
    response.setOnCall(request.isOnCall());
    response.setUserid(request.getUserid());
    return response;
  }



  /**
   * Die Methode wird aufgerufen, wenn die Server-Client-Verbindung gerade
   * geschlossen wird.
   * 
   * @param session
   *                diese Session wird geschlossen.
   * @param reason
   *                der Grund für den Verbindungsabbruch
   */
  @Override
  public void onClose(Session session, CloseReason reason) {
    // Markiere Session als geschlossen, bevor Properties entfernt werden
    session.getUserProperties().put("closed", true);

    log.warn(
        "onClose: sessionId={} code={} reason={}", session == null ? "<null>" : session.getId(),
        reason == null ? -1 : reason.getCloseCode().getCode(),
        reason == null ? "<null>" : reason.getReasonPhrase()
    );

    String sessionid = session.getId();
    disconnect(sessionid);

    // Cleanup verzögert – nach dem Disconnect ist kein Handler mehr aktiv
    session.getUserProperties().remove("streamId");
    session.getUserProperties().remove("userid");
    session.getUserProperties().remove("userid|flac");
  }



  @Override
  public void onError(Session session, Throwable throwable) {
    // Markiere Session als geschlossen
    session.getUserProperties().put("closed", true);

    log.error(
        "onError: sessionId={} err={}", session == null ? "<null>" : session.getId(),
        throwable == null ? "<null>" : throwable.getMessage(), throwable
    );

    if (throwable != null) {
      log.fatal("Fatal error in WebSocket: {}", throwable.getMessage());
    }

    disconnect(session.getId());

    // Danach Properties wegräumen
    session.getUserProperties().remove("streamId");
    session.getUserProperties().remove("userid");
    session.getUserProperties().remove("userid|flac");
  }



  /**
   * Eine Server-Client-Verbindung wird in diesem Moment hergestellt.
   * 
   * @param session
   *                die gerade geöffnete Session
   * @param config
   *                Websocket Endpoint
   */
  @Override
  public void onOpen(Session session, EndpointConfig config) {
    session.getUserProperties().put("streamId", new ByteArrayOutputStream());
    session.getUserProperties().put("userid|flac", new ByteArrayOutputStream());
    session.getUserProperties().put("authorized", false);

    log.info("onOpen: sessionId={}", session.getId());
    sessionmap.put(session.getId(), session);
    messageQueues.computeIfAbsent(session.getId(), id -> new ConcurrentLinkedQueue<>());

    /**
     * MessageHandler für empfangene binary Nachrichten (Screenshots / Audio)
     */
    // --- PATCH START ---
    // Datei: BinarySender / Dispatcher

    // In sendBinary (session.addMessageHandler)
    session.addMessageHandler(byte[].class, data -> {

      if (!(Boolean) session.getUserProperties().get("authorized")) {
        try {
          sessionmap.remove(session.getId());
          // Nachrichtenwarteschlange wegräumen
          messageQueues.remove(session.getId());
          log.info("Verbindung schließen für " + session.getId());
          session.close();
          log.info("Verbindung wurde erfolgreich geschlossen für " + session.getId());
        }
        catch (IOException e) {
          log.warn(e.getMessage(), e);
        }
        return;
      }

      Boolean closed = (Boolean) session.getUserProperties().get("closed");
      if (Boolean.TRUE.equals(closed)) return;

      ByteArrayOutputStream streamId = (ByteArrayOutputStream) session.getUserProperties().get("streamId");
      ByteArrayOutputStream userid_flac = (ByteArrayOutputStream) session.getUserProperties()
          .get("userid|flac");

      if (streamId == null) return;

      try {
        streamId.reset();
        streamId.write(data, 0, 36);
      }
      catch (Exception e) {
        return;
      }

      CONFERENCEAUDIO meta = activeStreams.get(streamId.toString());
      if (meta == null) return;

      String userid = meta.getUserid();
      byte[] audioPacket;
      synchronized (userid_flac) {
        userid_flac.reset();
        userid_flac.writeBytes(userid.getBytes());
        userid_flac.write(data, 36, data.length - 36);
        audioPacket = userid_flac.toByteArray();
      }

      for (String targetId : meta.getReceiverSessions()) {
        Session targetSession = sessionmap.get(targetId);
        if (targetSession == null || !targetSession.isOpen()) continue;

        // Hier nutzen wir deine Logik: In Queue packen und Worker starten
        ConcurrentLinkedQueue<Object> queue = messageQueues.get(targetId);
        if (queue != null) {
          // Optional: Audio-Backpressure. Wenn schon mehr als 10 Pakete warten,
          // verwerfen wir das neue, um Kommandos nicht zu verzögern.
          if (queue.size() < 20) {
            queue.add(audioPacket);
          }

          // Deine bewährte Logik zum Starten des Workers
          running.computeIfAbsent(targetId, id -> new AtomicBoolean(false));
          if (running.get(targetId).compareAndSet(false, true)) {
            senderFIFO.execute(() -> processQueue(targetSession));
          }
        }
      }
    });

    // --- processQueue bleibt unverändert, nur sicherstellen:
    // running Flag + pendingSends verhindern Command-Loss
    // Keine Änderung nötig, da Commands bereits seriell durch processQueue laufen

    // --- PATCH END ---

    session.addMessageHandler(new MessageHandler.Whole<MESSAGE>() {
      @Override
      public void onMessage(MESSAGE message) {

        if (!(message instanceof USRLOGIN)) {
          // TODO session prüfen
          // ENTERROOM etc.

          if (!(Boolean) session.getUserProperties().get("authorized")) {

            try {
              sessionmap.remove(session.getId());
              // Nachrichtenwarteschlange wegräumen
              messageQueues.remove(session.getId());
              log.info("Verbindung schließen für " + session.getId());
              session.close();
              log.info("Verbindung wurde erfolgreich geschlossen für " + session.getId());
            }
            catch (IOException e) {
              log.warn(e.getMessage(), e);
            }
            return;
          }
        }

        if (message instanceof CHATMESSAGE) {
          CHATMESSAGE chatmessage = (CHATMESSAGE) message;
          // Die Nachricht wird noch nicht dauerhaft in der Datenbank abgelegt */

          CHATMESSAGE response = chatmessage(chatmessage);
          if (ERROR == response.getHeader()) {
            log.info("Antwort wird nicht gesendet.");
            return;
          }
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> sessionlist = databaseservice.fetchSessionlist(chatmessage.getRoom());
          log.info("-----> {}", response.toString());
          sendMessage(response, sessionlist);
          return;
        }
        else if (message instanceof CONFERENCEAUDIO) {
          CONFERENCEAUDIO conferenceaudio = (CONFERENCEAUDIO) message;
          // der Anwender sendet seinen Stream an alle

          activeStreams.put(conferenceaudio.getStreamId(), conferenceaudio);
          return;
        }
        else if (message instanceof CONFERENCEVIDEO) {
          CONFERENCEVIDEO conferenceVideo = (CONFERENCEVIDEO) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          // Bevor die Übertragung beginnen kann, muss geprüft werden, ob die anderen
          // Teilnehmer 1:1 Übertragungen am Laufen haben

          log.info("<----- " + conferenceVideo.toString());

          String userid = databaseservice.getUserBySession(session.getId());

          // filter alle receiverSessions heraus, die keine eigenen Übertragungen haben

          List<String> receiverSessions = List.of(conferenceVideo.getReceiverSessions());
          if (conferenceVideo.isShared()) {
            // Übertragungsaufbau

            try {
              if (databaseservice.isVideoRunning(userid) > 0) {
                // ERROR
                CONFERENCEVIDEO error = new CONFERENCEVIDEO();
                error.setCommand(Command.CONFERENCEVIDEO);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setMultilingualkey(KEY.SERVER_DRITTE_PERSON);
                error.setShared(false);
                log.info("-----> " + error.toString());
                sendMessage(error, session);
                return;
              }
            }
            catch (DatabaseException e) {
              return;
            }

            databaseservice.insertVideoSender(userid);
            // die Empfänger müssen noch eingetragen werden!!!!!!!!!!
            // finde zu allen receiverSessions die userid
            for (String receiverSession : receiverSessions) {
              String receiverUid = databaseservice.getUserBySession(receiverSession);
              if (receiverUid != null) databaseservice.insertVideo(userid, receiverUid);
            }
          }
          else {
            // SHARING = false
            // Übertragungsabbau ist immer erlaubt
            // entfernen Videoeinträge/ auch in KEEPALIVE
//            String usersession = databaseservice.getSessionidByUserid(conferenceVideo.getUserid());
            // Ist dieser userssion in ein sender in der TB_SCREENCAST?

            ViewSenderSession viewSenderSession = databaseservice.fetchVideosenderByReceiver(userid);

            if (viewSenderSession == null) {
              databaseservice.deleteVideoSenderUser(userid); // Lösche TB_VIDEO_SENDER
              databaseservice.deleteVideoUser(userid);
            }
            else {
              Screencast screencast = databaseservice
                  .getScreencastByCallerSession(viewSenderSession.getSession());
              // kommt die Session in der TB_SCREENCAST vor?
              if (screencast.getSenderSession() == null) {
                log.info("lösche Screenshot");
                databaseservice.deleteVideoSenderUser(userid);
                databaseservice.deleteVideoUser(userid);
              }
              else {
                log.info("lösche Screencaster");
                databaseservice.deleteVideoSenderUser(userid); // Lösche TB_VIDEO_SENDER
                databaseservice.deleteVideoScreencaster(userid); // Lösche TB_VIDEO
              }
            }

          }
          log.info("-----> " + conferenceVideo.toString());
          // übertrage an alle Empfänger
          sendMessage(conferenceVideo, receiverSessions);

          return;
        }
        else if (message instanceof CONFERENCE) {
          CONFERENCE conference = (CONFERENCE) message;
          log.info("<----- " + conference.toString());
          if (conference.getHeader() == REQUEST) {

            // OFFHOOK=false, dann ONPROJEKTOR = aus FENSTER Schließen senden,
            // IMAGE_RECEIVER stoppen

            // Lies den Status der Konferenzteilnehmer aus.
            // Wer ist online und wer ist offline

            String konferenzname = conference.getKonferenzname();

//            String userid = conference.getUserid(); // er hat die Anfrage gestellt
            boolean offhook = conference.isOffhook();
            DatabaseService databaseservice = WebdatabaseImpl.getInstance();

            // TODO As of vesrion 260501-PRE3
            String userid = databaseservice.getUserBySession(session.getId());

            String organisatorUid = databaseservice
                .fetchOrganisatorByConferencekey(conference.getConferenceKey());

            databaseservice.updateKonferenzraumOnlinestatus(konferenzname, organisatorUid, userid, offhook);
            // in CONFERENCE sind aller Telkomitglieder mit ihrem Status anwesend/abwesend
            // enthalten.
            if (!offhook) {
              // Hörer aufgelegt
              // Hole den Sender vom Receiver
              // Wenn der Sender nicht

              try {
                String senderFromReceiver = databaseservice.senderFromReceiver(userid);
                // senderFromReceiver kann null sein
                boolean genau1x = databaseservice.countVideosender(senderFromReceiver);
                log.info("genau1x=" + genau1x);
                if (genau1x) {

                  // Sende STOPSENDINGVIDEO
                  STOPSENDINGVIDEO stopSendingVideo = new STOPSENDINGVIDEO();
                  stopSendingVideo.setCommand(Command.STOPSENDINGVIDEO);
                  stopSendingVideo.setHeader(REQUEST);
                  stopSendingVideo.setDataset(Protocol.DATASET);
                  stopSendingVideo.setUserid(senderFromReceiver);

                  log.info("-----> " + stopSendingVideo.toString());

                  // Hole Sessionid vom Bildersender
                  String bildersender = databaseservice.getSessionidByUserid(senderFromReceiver);
                  sendMessage(stopSendingVideo, bildersender);

                }
              }
              catch (DatabaseException e) {
                log.info("Kein Sender für den Empfänger '" + userid + "' gefunden, ok");
              }

              databaseservice.deleteVideoReceiver(userid);
              // Lösche alle verwaisten Sender aus TB_VIDEO_SENDER
              // Wenn der Sender verwaist zurückbleibt, dann lösche ihn und alle anderen, die
              // du findest
              databaseservice.deleteOrphainedSender();

              // hier kann der Projektor gelöscht werden, wenn offhook=false ist. Nur
              // ein Receiver kann einen eingeschalteten Projektor haben
              databaseservice.switchProjektor(false, userid);
            }
            else {
              // Der Anwender hat den Telefonhörer in einem Konfernzraum abgenommen.
              // Läuft in diesem gerade eine Videokonferenz?
              // Wenn ja, dann muss der Projektor beim Anwender eingeschaltet werden.
              // Der Anwender muss in die TB_VIDEO eingetragen werden.
              // Der Client muss den Projektor einschalten

              String videosender = databaseservice
                  .fetchKonferenzraumActiveUser(konferenzname, organisatorUid);
              if (videosender != null) {
                databaseservice.insertVideo(videosender, userid);
                CONFERENCEVIDEO conferencevideo = new CONFERENCEVIDEO();
                conferencevideo.setCommand(Command.CONFERENCEVIDEO);
                conferencevideo.setHeader(REQUEST);
                conferencevideo.setDataset(Protocol.DATASET);
                conferencevideo.setKonferenzraum(konferenzname);
                conferencevideo.setReceiverSessions(new String[] {conference.getSession()}); // nur eine
                                                                                             // Receiversession
                                                                                             // vom
                                                                                             // Empfänger
                conferencevideo.setShared(true);
                // TODO As of version 260501-PRE3
//                conferencevideo.setUserid(videosender); // Datenquelle
                conferencevideo.setNickname(databaseservice.fetchNickname(videosender));
                log.info("-----> " + conferencevideo.toString());
                sendMessage(conferencevideo, conference.getSession());
              }
              // Konferenzraum Active_USER
            }
            CONFERENCE conferencE = databaseservice.readKonferenzuser(konferenzname, organisatorUid);
            conferencE.setOffhook(offhook);
            conferencE.setUserid(userid);
            // an alle Sessions eine Antwort senden
            // response ---> CONFERENCE sendet an alle Teilnehmer in einem Konferenzraum
            // in offhook steht derjenige, der aufgelgt oder abgelegt hat
            List<String> sessions = databaseservice.fetchKonferenzraumSessions(konferenzname, organisatorUid);
            log.info("-----> " + conferencE.toString());
            sendMessage(conferencE, sessions);
          }
          return;
        }
        else if (message instanceof CONFERENCEMUTE) {
          CONFERENCEMUTE mute = (CONFERENCEMUTE) message;
          log.info("<----- {}", mute.toString());
          switch(mute.getHeader()) {
            case CONFIRM:
              break;
            case ERROR:
              break;
            case REQUEST:
              try {
                DatabaseService databaseservice = WebdatabaseImpl.getInstance();
                databaseservice.updateMute(mute.isMute(), mute.getUserid());
                // TODO As of version 260501-PRE3
                mute.setUserid(null);
                log.info("------> {}", mute.toString());
                sendMessage(mute, List.of(mute.getReceiverSessions()));
              }
              catch (SQLException e) {
                log.error(e.getMessage(), e);
              }
              break;
            case RESPONSE:
              break;
            default:
              break;
          }
          return;
        }
        else if (message instanceof PROYECTORCLOSING) {
          PROYECTORCLOSING projector = (PROYECTORCLOSING) message;
          log.info("<----- " + projector.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          switch(projector.getHeader()) {
            case RESPONSE:
              databaseservice.switchProjektor(false, projector.getUserid());
              break;
            case REQUEST:
              // Teilenframe geschlossen
              String callerSession = databaseservice.getSessionidByUserid(projector.getUserid());
              Screencast screencast = databaseservice.getScreencastByCallerSession(callerSession);
              log.info("Die Projektorverbindung wurde gekappt.");
              log.info("Der Anwender(Sender) ist gerade offline gegangen= " + projector.getUserid());
              String receiverSession = screencast.getReceiverSession();
              if (receiverSession == null) {
                log.info("-----> Der Empfänger ist nicht mehr vorhanden");
                return;
              }
              databaseservice.deleteScreencastUser(receiverSession);
              databaseservice.deleteVideoSenderUser(projector.getUserid());
              databaseservice.deleteVideoUser(projector.getUserid());
              log.info("-----> " + projector.toString());
              sendMessage(projector, receiverSession);
              return;
            default:
              return;
          }
          return;
        }
        else if (message instanceof BEAMEROFF) {
          BEAMEROFF beamer = (BEAMEROFF) message;
          log.info("<----- " + beamer.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          switch(beamer.getHeader()) {
            case REQUEST:

              String sender = databaseservice.senderFromReceiver(beamer.getUserid());
              // Achtung die Löschreihenfolge spielt eine Rolle
              // muss vor deleteVideoUser stehen
              if (sender != null) databaseservice.deleteVideoSenderUser(sender);
              databaseservice.deleteVideoUser(beamer.getUserid());

              // aus der TB_SCREENCAST löschen
              String sessionid = session.getId();
              Screencast screencast = databaseservice.getScreencastByReceiverSession(sessionid);
              // die screencast.getSenderSession kann null sein, weil zuvor ein RESPONSE
              // eingegenagen ist.
              // der Sender hatte den Abbrechen-Knopf betätigt.
              // Später hat dann der Empfänger den Projektorframe geschlossen.
              // die sessionid ist in der Tabelle nicht mehr enthalten.
              if (screencast.getSenderSession() == null) return;
              databaseservice.deleteScreencastUser(screencast.getReceiverSession());

              // weiterleiten an den Sender
              log.info("-----> " + beamer.toString());
              sendMessage(beamer, screencast.getSenderSession());
              break;
            case RESPONSE:
              databaseservice.deleteVideoSenderUser(beamer.getUserid()); // Lösche aus TB_VIDEO_SENDER
              databaseservice.switchProjektor(false, beamer.getUserid()); // Projektor ist ausgeschaltet
              break;
            case CONFIRM:
              break;
            case ERROR:
              break;
            default:
              break;
          }
          return;
        }
        else if (message instanceof IMAGE) {
          IMAGE image = (IMAGE) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          Session item;
          switch(image.getHeader()) {
            case REQUEST:
              // an den Empfänger weiterleiten

              log.info("<----- " + image.toString());

              String receiverSessionId = databaseservice.getSessionidByNickname(image.getReceiverNickname());

              if (receiverSessionId == null) {
                // Der Empfänger 'Nickname' ist verschwunden.
                IMAGE error = new IMAGE();
                error.setCommand(Command.IMAGE);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setSenderUid(image.getSenderUid());
                error.setSenderNickname(image.getSenderNickname());
                error.setReceiverNickname(image.getReceiverNickname());
                error.setMultilingualkey(KEY.SERVER_DER_EMPFAENGER_IST_VERSCHWUNDEN);
                log.info("-----> " + error.toString());

                sendMessage(error, session);
                return;
              }

              String receiverUserid = databaseservice.getUserBySession(receiverSessionId);
              log.info("receiverUserid=" + receiverUserid);

              if (databaseservice.blockConferenceCall(receiverUserid)) {
                IMAGE error = new IMAGE();
                error.setCommand(Command.IMAGE);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setSenderUid(image.getSenderUid());
                error.setSenderNickname(image.getSenderNickname());
                error.setReceiverNickname(image.getReceiverNickname());
                error.setMultilingualkey(KEY.STRING_IST_IN_EINER_KONFERENZ);

                log.info("-----> " + error.toString());

                sendMessage(error, session);
                return;
              }

              try {
                if (databaseservice.isReceiverSender(receiverUserid)) {
                  // Er ist bereits Sender
                  IMAGE error = new IMAGE();
                  error.setCommand(Command.IMAGE);
                  error.setHeader(ERROR);
                  error.setDataset(Protocol.DATASET);
                  error.setSenderUid(image.getSenderUid());
                  error.setSenderNickname(image.getSenderNickname());
                  error.setReceiverNickname(image.getReceiverNickname());
                  error.setMultilingualkey(KEY.SERVER_VIDEO_RUNNING);
                  sendMessage(error, session);
                  log.info("-----> " + error.toString());

                  return;
                }
              }
              catch (DatabaseException e) {
                log.fatal(e.getMessage(), e);
                return;
              }

              item = sessionmap.get(receiverSessionId);
              if (item == null) {
                IMAGE error = new IMAGE();
                error.setCommand(Command.IMAGE);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setSenderUid(image.getSenderUid());
                error.setSenderNickname(image.getSenderNickname());
                error.setReceiverNickname(image.getReceiverNickname());
                error.setMultilingualkey(KEY.SERVER_DER_EMPFAENGER_IST_VERSCHWUNDEN);
                log.info("-----> " + error.toString());

                sendMessage(error, session);
                return;
              }
              // 2. Nimmt der Empfänger Übertragungen an?
              if (!databaseservice.isOnrekord(receiverSessionId)) {
                IMAGE error = new IMAGE();
                error.setCommand(Command.IMAGE);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setSenderUid(image.getSenderUid());
                error.setSenderNickname(image.getSenderNickname());
                error.setReceiverNickname(image.getReceiverNickname());
                error.setMultilingualkey(KEY.SERVER_LEHNT_UEBERTRAGUNGEN_AB);
                log.info("-----> " + error.toString());

                sendMessage(error, session);
                return;
              }
              // 3. Empfänger muss noch seinen Projektor aussschalten, weil er in einer
              // Sitzung ist.
              if (databaseservice.isOnProjektor(receiverSessionId)) {
                IMAGE error = new IMAGE();
                error.setCommand(Command.IMAGE);
                error.setHeader(ERROR);
                error.setDataset(Protocol.DATASET);
                error.setSenderUid(image.getSenderUid());
                error.setSenderNickname(image.getSenderNickname());
                error.setReceiverNickname(image.getReceiverNickname());
                error.setMultilingualkey(KEY.SERVER_MUSS_NOCH_SEINEN_PROJEKTOR_AUSSCHALTEN);
                log.info("-----> " + error.toString());

                sendMessage(error, session);
                return;
              }
              // 4. Der Sender darf nicht Empfänger sein
              try {
                if (databaseservice.isSenderReceiver(image.getSenderUid())) {
                  // Er ist bereits Empfänger
                  IMAGE error = new IMAGE();
                  error.setCommand(Command.IMAGE);
                  error.setHeader(ERROR);
                  error.setDataset(Protocol.DATASET);
                  error.setSenderUid(image.getSenderUid());
                  error.setSenderNickname(image.getSenderNickname());
                  error.setReceiverNickname(image.getReceiverNickname());
                  error.setMultilingualkey(KEY.SERVER_FORWARDING_IMAGE);
                  log.info("-----> " + error.toString());

                  sendMessage(error, session);
                  return;
                }
              }
              catch (DatabaseException e) {
                log.fatal(e.getMessage(), e);
                return;
              }
              // 5. Anfrage wird an den Empfänger weitergeleitet
              // Bevor der Sender eingetragen wird, prüfen ob er schon in der Tabelle steht
              log.info("als Sender eingetragen=" + image.getSenderUid());
              databaseservice.insertVideoSender(image.getSenderUid());

              log.info("-----> " + image.toString());

              sendMessage(image, receiverSessionId);
              break;
            case RESPONSE:
              String senderSessionId = databaseservice.getSessionidByUserid(image.getSenderUid());
              databaseservice.setProjektorSession(session.getId(), senderSessionId);
              databaseservice.insertVideo(image.getSenderUid(), image.getReceiverUid());
              log.info(
                  "als Sender x Empfänger eingetragen=(" + image.getSenderUid()
                      + "/"
                      + image.getReceiverUid()
                      + ")"
              );
              image.setReceiverSession(session.getId());
              log.info("-----> " + image.toString());
              sendMessage(image, senderSessionId);
              break;
            case ERROR:
              break;
            case CONFIRM:
              break;
          }
          return;
        }
        else if (message instanceof SCREENCAST) {
          SCREENCAST screencast = (SCREENCAST) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          switch(screencast.getHeader()) {
            case CONFIRM:
              break;
            case ERROR:
              break;
            case REQUEST:
              databaseservice.updateRekorderport(screencast.getCallerUserid(), screencast.getCallerPort());
              String receiverSessionid = databaseservice
                  .getSessionidByNickname(screencast.getReceiverNickname());
              // 1. Ist der Empfänger noch online?
              if (receiverSessionid == null) {
                // Der Empfänger ist nicht mehr online
                // Antwort mit Error senden
                SCREENCAST response = new SCREENCAST();
                response.setCommand(Command.SCREENCAST);
                response.setHeader(ERROR);
                response.setDataset(Protocol.DATASET);
                response.setReceiverNickname(screencast.getReceiverNickname());
                response.setErrorMessage(
                    "<html><b>" + screencast.getReceiverNickname() + "</b> ist nicht mehr online.</html>"
                );
                sendMessage(response, session);
                return;
              }

              // ich muss die
              // ic habe nur den ReceiverNickname
              if (databaseservice.isOnProjektor(receiverSessionid)) {
                log.info("Projektor ist eingeschaltet, Error zurücksenden");
                SCREENCAST response = new SCREENCAST();
                response.setCommand(Command.SCREENCAST);
                response.setHeader(ERROR);
                response.setDataset(Protocol.DATASET);
                response.setReceiverNickname(screencast.getReceiverNickname());
                response.setErrorMessage("<html>Der Projektor ist in Betrieb.</html>");
                sendMessage(response, session);
                return;
              }

              // 2. Nimmt der Empfänger Übertragungen an?

              if (!databaseservice.isOnrekord(receiverSessionid)) {
                SCREENCAST response = new SCREENCAST();
                response.setCommand(Command.SCREENCAST);
                response.setHeader(ERROR);
                response.setDataset(Protocol.DATASET);
                response.setReceiverNickname(screencast.getReceiverNickname());
                response.setErrorMessage(
                    "<html><b>" + screencast.getReceiverNickname()
                        + "</b> nimmt keine Bildschirmübertragungen entgegen.</html>"
                );
                sendMessage(response, session);
                return;
              }
              // weiterleiten an den Empfänger, damit er ein Fenster öffnet
              sendMessage(screencast, receiverSessionid);
              break;
            case RESPONSE:
              // Antwort weiterleiten an den Sender
              String callerSession = databaseservice.getSessionidByUserid(screencast.getCallerUserid());
              // eine Projektorverbindung wird eingetragen
              databaseservice.setProjektorSession(session.getId(), callerSession);
              sendMessage(screencast, callerSession);
              break;
            default:
              break;
          }
          return;
        }
        else if (message instanceof BREAKROOMS) {
          BREAKROOMS breakrooms = (BREAKROOMS) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> rooms = databaseservice.fetchBreakrooms(breakrooms.getUserid());
          BREAKROOMS response = new BREAKROOMS();
          response.setCommand(Command.BREAKROOMS);
          response.setHeader(RESPONSE);
          response.setDataset(Protocol.DATASET);
          response.setUserid(breakrooms.getUserid());
          response.setRooms(rooms.toArray(new String[rooms.size()]));
          sendMessage(response, session);
          return;
        }
        else if (message instanceof READROOMS) {
          READROOMS readrooms = (READROOMS) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> rooms = databaseservice.fetchRooms(readrooms.getUserid(), readrooms.getRoomtype());
          READROOMS response = new READROOMS();
          response.setCommand(Command.READROOMS);
          response.setHeader(RESPONSE);
          response.setDataset(Protocol.DATASET);
          response.setUserid(readrooms.getUserid());
          response.setRoomtype(readrooms.getRoomtype());
          response.setRooms(rooms.toArray(new String[rooms.size()]));
          sendMessage(response, session);
          return;
        }
        else if (message instanceof UPDATEFILETYPES) {
          UPDATEFILETYPES types = (UPDATEFILETYPES) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          databaseservice.updateUserBlacklist(types.getUserid(), types.getFiletypes());
          // Confirm senden
          UPDATEFILETYPES response = new UPDATEFILETYPES();
          response.setCommand(Command.UPDATEFILETYPES);
          response.setHeader(CONFIRM);
          response.setDataset(Protocol.DATASET);
          response.setUserid(types.getUserid());
          sendMessage(response, session);
          return;
        }
        else if (message instanceof FILETYPES) {
          FILETYPES filetypes = (FILETYPES) message;
          if (REQUEST != filetypes.getHeader()) return;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          databaseservice.createUserBlacklist(filetypes.getUserid());
          List<BlacklistTypes> sperrliste = databaseservice.fetchFiletypes(filetypes.getUserid());
          FILETYPES response = new FILETYPES();
          response.setCommand(Command.FILETYPES);
          response.setHeader(RESPONSE);
          response.setDataset(Protocol.DATASET);
          response.setUserid(filetypes.getUserid());
          response.setFiletypes(sperrliste.toArray(new BlacklistTypes[sperrliste.size()]));
          sendMessage(response, session);
          return;
        }
        else if (message instanceof SEARCHFILES) {
          // jetzt überträgt er alles
          SEARCHFILES searchfiles = (SEARCHFILES) message;
          List<Searchfile> foundFiles = searchfiles(searchfiles);

          final int blocksize = 64;
          int maxRound = foundFiles.size() / blocksize;
          ArrayList<Searchfile> part = new ArrayList<>();
          int round = 0;
          while (round <= maxRound) {
            part.clear();
            int m = 0;
            while (!foundFiles.isEmpty()) {
              if (m > blocksize) break;
              part.add(foundFiles.remove(0));
              m++;
            }
            // part senden // bestimmte Dateien filtern
            // part enthält bis 64 Dateien
            SEARCHFILES response = new SEARCHFILES();
            response.setCommand(Command.SEARCHFILES);
            response.setHeader(RESPONSE);
            response.setDataset(Protocol.DATASET);
            response.setSearchfiles(part.toArray(new Searchfile[part.size()]));
            response.setStarted(round == 0 ? true : false);
            response.setUserid(searchfiles.getUserid());
            sendMessage(response, session);
            round++;
          }
          return;
        }
        else if (message instanceof DOWNLOAD) {
          DOWNLOAD download = (DOWNLOAD) message;
          DOWNLOAD response = selectDOWNLOAD(download);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof UPLOADFILES) {
          UPLOADFILES uploadfiles = (UPLOADFILES) message;
          UPLOADFILES response = uploadfiles(uploadfiles);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof DELETEUPLOADFILES) {
          DELETEUPLOADFILES deletefiles = (DELETEUPLOADFILES) message;
          DELETEUPLOADFILES response = deleteuploadfiles(deletefiles);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof PRIVATECHATFILE) {
          PRIVATECHATFILE privatechatfile = (PRIVATECHATFILE) message;
          log.info("<----- {}", privatechatfile.toString());
          if (REQUEST == privatechatfile.getHeader()) {
            DatabaseService databaseservice = WebdatabaseImpl.getInstance();
            String receiver_uid = databaseservice.fetchSessionByPseudoid(privatechatfile.getRemotePseudoid());
            String uid = databaseservice.fetchSessionByPseudoid(privatechatfile.getPseudoid());
            try {
              PrivateChatfile entity = databaseservice.uploadPrivateChatfileAttributes(
                  uid, receiver_uid, privatechatfile.getAbsoluteFilename(), privatechatfile.getFilename(),
                  privatechatfile.getFilesize(), privatechatfile.getMessage(),
                  privatechatfile.getRemoteNickname()
              );
              PRIVATECHATFILE response = new PRIVATECHATFILE();
              response.setCommand(Command.PRIVATECHATFILE);
              response.setHeader(RESPONSE);
              response.setDataset(Protocol.DATASET);
              response.setFilename(entity.getFilename());
              response.setAbsoluteFilename(privatechatfile.getAbsoluteFilename());
              response.setFilesize(entity.getFilesize());
              response.setMessage(entity.getMessage());
              response.setNumber(entity.getAnlage());
              response.setRemoteNickname(entity.getRemoteNickname());
              response.setRemotePseudoid(privatechatfile.getRemotePseudoid());
              response.setPseudoid(privatechatfile.getPseudoid());
              log.info("-----> {}", response.toString());
              sendMessage(response, session);
            }
            catch (DatabaseException e) {
              log.info(e.getMessage(), e);
            }
          }
          return;
        }
        else if (message instanceof PRIVATEMESSAGE) {
          PRIVATEMESSAGE privatemessage = (PRIVATEMESSAGE) message;
          log.info("<----- {}", privatemessage.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          if (REQUEST == privatemessage.getHeader()) {
            String uid = databaseservice.fetchUserByPseudoid(privatemessage.getPseudoid());
            EntityUser entity = databaseservice.fetchUser(uid);
            ChatUser chatuser = new ChatUser();
            chatuser.setBackgroundColor(entity.getBackground());
            chatuser.setForegroundColor(entity.getForeground());
            chatuser.setNickname(entity.getNickname());
            // keine UserID in chatuser senden
            // TODO As of version 260501-PRE3 outdated
//            chatuser.setUserid(privatemessage.getSenderUID());
            privatemessage.setChatUser(chatuser);
            privatemessage.setDatetime(System.currentTimeMillis());
            log.info("-----> {}", privatemessage.toString());
            sendMessage(
                privatemessage, databaseservice.fetchSessionByPseudoid(privatemessage.getRemotePseudoid())
            );
          }
          else if (RESPONSE == privatemessage.getHeader()) {
            log.info("-----> {}", privatemessage.toString());
            sendMessage(privatemessage, databaseservice.fetchSessionByPseudoid(privatemessage.getPseudoid()));
          }
          return;
        }

        else if (message instanceof CHATUSERLIST) {
          // CHATUSERLIST response = chatuserlist((CHATUSERLIST)message);
          // Kann noch verbessert werden. Nur an die User senden, die
          // mit iherer SessionID im Raum sind
          // Aktuell sende ich an alle
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> userInRooms = databaseservice.fetchRoomlist(session.getId());
          // alle infos werden für den User aktualisiert
          broadcast(userInRooms);
          return;
        }
        else if (message instanceof HISTORYMESSAGE) {
          HISTORYMESSAGE response = historymessage((HISTORYMESSAGE) message);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof ROOMLIST) {
          if (message instanceof READTOPICROOMOWNER) return; // READTOPICROOMOWNER erweitert ROOMLIST
          // Eine Liste aller Chaträume wird angefordert
          // Der Server broadcastet eine Raumbelegung
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> roomlist = databaseservice.fetchRoomlist(session.getId());
          broadcast(roomlist);
          return;
        }
        else if (message instanceof READTOPICROOMOWNER) {
          READTOPICROOMOWNER response;
          response = readTopicroom((READTOPICROOMOWNER) message);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof LEAVEROOM) {
          LEAVEROOM leaveroom = (LEAVEROOM) message;
          log.info("<----- " + leaveroom.toString());
          // Nach LEAVEROMM ist der User mit der SESSION_ID nicht mehr vorhanden */
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          // Der User mit der session.getID ist schon nicht mehr in dem Raum
          // Auch nicht so effizient, weil alle Räume ermittelt werden
          List<String> roomlist = databaseservice.fetchRoomlist(session.getId());

          // Die Raumliste muss vielleicht gekürzt werden, wenn der Raum
          // ein Tempraum war und leer ist

          // Ist der Chat ein Pausenraum oder ein Gruppenraum?

          try {
            switch(databaseservice.getLifetime(leaveroom.getRoom())) {
              case PERMANENT:
                databaseservice.remove(leaveroom.getUserid(), leaveroom.getRoom());
                break;
              case TEMPORARY:
                databaseservice.remove(leaveroom.getUserid(), leaveroom.getRoom());
                int count = databaseservice.countTemproomUser(leaveroom.getRoom());
                if (count == 0) {
                  // Temproom löschen
                  // und raumliste kürzen
                  // log.info(response.getRoom());
                  roomlist.remove(leaveroom.getRoom());
                  // Beim Löschen eines Temprooms, spielt die Userid keine Rolle.
                  // Der Raum muss gelöscht werden, wenn niemand anwesend ist
                  databaseservice.deleteTemproom(leaveroom.getRoom());
                }
                break;
              default:
                break;
            }

            // log.info("muss wieder angepasst werden !!!!!!!!!!!!!!!!!!!!!!");
            // Kann noch verbessert werden. Nur an die Senden, die
            // mit iherer SessionID im Raum sind
            // Aktuell sende ich an alle
            broadcast(roomlist);
          }
          catch (DatabaseException e) {
            log.fatal(e.getMessage());
          }
          return;
        }
        else if (message instanceof ENTERROOM) {
          ENTERROOM request = (ENTERROOM) message;
          log.info("<----- " + request.toString());
          if (request.getHeader() != REQUEST) return;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          try {
            boolean exists = databaseservice.isRoomExists(request.getRoom());
            ENTERROOM response;
            if (exists) {
              response = databaseservice.enterRoom(request);
            }
            else {
              // der Raum ist nicht vorhanden
              // ERROR
              String language = databaseservice.fetchLanguage(request.getUserid());
              MultilingualString chatraumNichtVorhanden = new MultilingualString(
                  KEY.SERVER_DER_CHATRAUM_IST_NICHT_VORHANDEN, ISO639.fromValue(language)
              );
              String errortext = chatraumNichtVorhanden.toString().replace("XXX", request.getRoom());
              response = new ENTERROOM();
              response.setCommand(Command.ENTERROOM);
              response.setHeader(ERROR);
              response.setDataset(Protocol.DATASET);
              response.setMultilingualkey(KEY.SERVER_DER_CHATRAUM_IST_NICHT_VORHANDEN);
              response.setText(errortext);
            }
            log.info("-----> " + response.toString());
            sendMessage(response, session);
          }
          catch (DatabaseException e) {
            log.fatal(e.getMessage());
            CloseReason reason = new CloseReason(CloseCodes.NORMAL_CLOSURE, e.getMessage());
            try {
              session.close(reason);
            }
            catch (IOException e1) {
              log.info(e1.getMessage(), e1);
            }
          }
          return;
        }
        else if (message instanceof CREATETEMPROOM) {
          CREATETEMPROOM temproom = (CREATETEMPROOM) message;
          CREATETEMPROOM response = createTemproom(temproom);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof LEAVEPRIVATECHAT) {
          // REQUEST kommt rein
          LEAVEPRIVATECHAT leavechat = (LEAVEPRIVATECHAT) message;
          log.info("<----- " + leavechat.toString());

          // sendTo muss ermittelt werden aus der Datenbank
          String participant = leavechat.getRemotePseudoid();
          log.info("sendTo muss ermittelt werden {}", participant);

//          LEAVEPRIVATECHAT response = leaveprivatechat(session, participant);

          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          String absender = databaseservice.fetchPseudoidBySessionid(session.getId());
          String sessionPseudoid = databaseservice.fetchSessionByPseudoid(participant);
          databaseservice.deletePrivateChatSingle(absender, participant);

          LEAVEPRIVATECHAT leaveprivatechat = new LEAVEPRIVATECHAT();
          leaveprivatechat.setCommand(Command.LEAVEPRIVATECHAT);
          leaveprivatechat.setHeader(RESPONSE);
          leaveprivatechat.setDataset(Protocol.DATASET);
          leaveprivatechat.setRemotePseudoid(absender);
          leaveprivatechat.setParticipant(participant);

          log.info("Aufgelegt sessionid=" + session.getId());
          log.info("-----> " + leaveprivatechat.toString());
          sendMessage(leaveprivatechat, sessionPseudoid);
          return;
        }
        else if (message instanceof CALLREMOTEUSER) {
          CALLREMOTEUSER remoteuser = (CALLREMOTEUSER) message;
          log.info("<----- " + remoteuser.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();

          // Ist der Absender ein Browser und hat keinen weiteren Chat offen?

          if (CONFIRM == remoteuser.getHeader()) {
            String uid = databaseservice.fetchUserByNickname(remoteuser.getLocalNickname());
//            if (true) {
            if (databaseservice.checkExhausted(uid)) {
              String isocode = databaseservice.fetchLanguage(uid);
              MultilingualString limit = new MultilingualString(
                  KEY.SERVER_CHAT_GEGENSEITE_LIMIT, ISO639.fromValue(isocode)
              );
              // ERROR senden
              CALLREMOTEUSER response = new CALLREMOTEUSER();
              response.setCommand(Command.CALLREMOTEUSER);
              response.setHeader(ERROR);
              response.setDataset(Protocol.DATASET);
              response.setMultilingualkey(KEY.SERVER_CHAT_GEGENSEITE_LIMIT);
              response.setErrorMessage(limit.toString());
              log.info("-----> {}", response.toString());
              sendMessage(response, session);
              return;
            }

            databaseservice.insertPrivateChat(remoteuser.getRemotePseudoid(), remoteuser.getPseudoid());
          }
          else if (RESPONSE == remoteuser.getHeader()) {
            databaseservice.deletePrivateChatSingle(remoteuser.getPseudoid(), remoteuser.getRemotePseudoid());
          }
          else {
            log.error("unbekannter HEADER, " + remoteuser.getHeader());
          }
          // hier ist bereits der OK Fall
          CALLPRIVATECHAT callprivatechat = callPrivatechat(remoteuser);
          String localsession = databaseservice.getSessionidByNickname(remoteuser.getLocalNickname());
          log.info("-----> " + callprivatechat);
          sendMessage(callprivatechat, localsession);
          return;
        }
        else if (message instanceof CALLPRIVATECHAT) {
          CALLPRIVATECHAT callprivate = (CALLPRIVATECHAT) message;
          log.info("<----- " + callprivate.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();

          String isPresent = databaseservice.getSessionidByNickname(callprivate.getRemoteNickname());
          String uid = databaseservice.fetchUserByPseudoid(callprivate.getPseudoid());
          if (isPresent == null) {
            // nicht online error

            String isocode = databaseservice.fetchLanguage(uid);
            CALLPRIVATECHAT call = new CALLPRIVATECHAT();
            call.setCommand(Command.CALLPRIVATECHAT);
            call.setHeader(ERROR);
            call.setDataset(Protocol.DATASET);
            call.setMultilingualkey(KEY.SERVER_USER_IST_OFFLINE);
            MultilingualString offline = new MultilingualString(
                KEY.SERVER_USER_IST_OFFLINE, ISO639.fromValue(isocode)
            );
            String text = offline.toString().replace("XXX", callprivate.getRemoteNickname());
            call.setErrorMessage(text);
            log.info("-----> " + call.toString());
            sendMessage(call, session);
            return;
          }

          try {
            CALLPRIVATECHAT check = databaseservice.checkExhausted(callprivate.getRemoteNickname(), uid);
            if (ERROR == check.getHeader()) {
              log.info("----->" + check.toString());
              sendMessage(check, session);
              return;
            }
          }
          catch (DatabaseException e) {
            log.error(e.getMessage(), e);
            CALLPRIVATECHAT call = new CALLPRIVATECHAT();
            call.setCommand(Command.CALLPRIVATECHAT);
            call.setHeader(ERROR);
            call.setDataset(Protocol.DATASET);
            call.setErrorMessage("Server Error - generischer Fehler");
            log.info("-----> " + call.toString());
            sendMessage(call, session);
            return;
          }

          // Wenn der CALLER selber bereits chattet und ein ein Browser ist,
          // Verbindungsversuch abbrechen
          if (databaseservice.checkExhausted(uid)) {
            // erschöpft
            String isocode = databaseservice.fetchLanguage(uid);
            MultilingualString exhausted = new MultilingualString(
                KEY.SERVER_CHAT_EXHAUSTED_LIMIT, ISO639.fromValue(isocode)
            );
            CALLPRIVATECHAT call = new CALLPRIVATECHAT();
            call.setCommand(Command.CALLPRIVATECHAT);
            call.setHeader(ERROR);
            call.setDataset(Protocol.DATASET);
            call.setMultilingualkey(KEY.SERVER_CHAT_EXHAUSTED_LIMIT);
            call.setErrorMessage(exhausted.toString());
            log.info("-----> " + call.toString());
            sendMessage(call, session);
            return;
          }

          // ist der REmoteUser ein Desktop?
          // kein Desktop, wie viele Chats hat er offen?
          // CallPrivateChat kennt den RemoteNickname
          // um die chats zu zählen, brauche ich die remote userid

          // TODO statt sessionId vom Anfrager wird die Pseudoid des Anfrgers übergeben

          String pseudoid = databaseservice.fetchPseudoidBySessionid(session.getId());

          CALLREMOTEUSER remoteuser = databaseservice.callRemoteuser(callprivate, pseudoid);
          if (ERROR == remoteuser.getHeader()) {
            log.info("-----> " + remoteuser.toString());
            sendMessage(remoteuser, session);
            return;
          }
          // der 1:1 Chat oder PrivateChat wird durch CALLPRIVATECHAT eingeleitet
          // localSessionID und remoteSessionid müssen verschieden sein
          if (remoteuser.getPseudoid().equals(remoteuser.getRemotePseudoid())) {
            // Sender und Empfänger sind identisch
            CALLPRIVATECHAT call = new CALLPRIVATECHAT();
            call.setCommand(Command.CALLPRIVATECHAT);
            call.setHeader(ERROR);
            call.setDataset(Protocol.DATASET);
            call.setLocalNickname(remoteuser.getRemoteNickname());
            call.setPseudoid(remoteuser.getRemotePseudoid());
            call.setRemoteNickname(remoteuser.getLocalNickname());
            call.setMultilingualkey(KEY.STRING_EINSAME_SEELEN);
            log.info("-----> " + call.toString());
            sendMessage(call, remoteuser.getPseudoid());
            return;
          }

          // private chat eintragen
          // remote SessionID und LocalSessionID

          // remote und local sind vertauscht!
          // der Anrufer trägt sich ein

          try {
            databaseservice.insertPrivateChat(remoteuser.getRemotePseudoid(), remoteuser.getPseudoid());
          }
          catch (DatabaseException e) {
            // Verbindung steht schon
            CALLPRIVATECHAT call = new CALLPRIVATECHAT();
            call.setCommand(Command.CALLPRIVATECHAT);
            call.setHeader(ERROR);
            call.setDataset(Protocol.DATASET);
            call.setLocalNickname(remoteuser.getRemoteNickname());
            call.setPseudoid(remoteuser.getRemotePseudoid());
            call.setRemoteNickname(remoteuser.getLocalNickname());
            call.setErrorMessage("Der Chat läuft bereits.");
            call.setMultilingualkey(KEY.SERVER_DER_CHAT_LAEUFT_BEREITS);
            log.info("-----> " + call.toString());
            sendMessage(call, databaseservice.fetchSessionByPseudoid(call.getPseudoid()));
            return;
          }
          // sende an local, anaconda -> freigeist session id freigeist
          // database sessionByPSEUDOID
          log.info("-----> " + remoteuser.toString());
          sendMessage(remoteuser, databaseservice.fetchSessionByPseudoid(remoteuser.getPseudoid()));
          return;
        }
        else if (message instanceof DIAL) {
          DIAL request = (DIAL) message;
          log.info("<----- {}", request.toString());
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          DIAL dialResponse = databaseservice.dial(request);
          // War der RESPONSE positiv?
          // Falls ja
          // andere Seite benachrichtigen
          if (ERROR == dialResponse.getHeader()) {
            sendMessage(dialResponse, session);
            return;
          }
          else if (RESPONSE == dialResponse.getHeader()) {
            // Nachricht an dem Empfänger schicken, dass gleich eine Nachricht kommt
            // ich benötige die SessionID, damit die Nachricht einen Adressaten findet
            String sessionid = databaseservice.getSessionidByUserid(dialResponse.getReceiverUserid());
            if (sessionid == null) {
              // der Anrufempfänger ist nicht mehr online
              DIAL dial = new DIAL();
              dial.setHeader(ERROR);
              dial.setCommand(Command.DIAL);
              dial.setDataset(Protocol.DATASET);
              dial.setReceiverNickname(dialResponse.getReceiverNickname());
              dial.setMultilingualkey(KEY.STRING_IST_NICHT_MEHR_ONLINE);
              sendMessage(dial, session);
              return;
            }
            // RESPONSE ist eine positive Antwort
            // SESSIONID wird für TCP-Übertragung optional hinzugepackt.
            dialResponse.setReceiverSession(sessionid); // die Sessionid vom Empfänger
            dialResponse.setCallerSession(session.getId()); // die Sessionid vom Caller
            log.info("-----> {}", dialResponse.toString());
            sendMessage(dialResponse, session);
          }
          return;
        }
        else if (message instanceof INCOMINGCALL) {
          INCOMINGCALL incoming = (INCOMINGCALL) message;
          log.info("<----- " + incoming.toString());
          if (RESPONSE == incoming.getHeader()) {
            DatabaseService databaseservice = WebdatabaseImpl.getInstance();
            final String callerSessionId = databaseservice.getSessionidByUserid(incoming.getCallerUserid());
            if (callerSessionId == null) {
              // exakte Fehlermeldung zurück an den Absender, Caller ist nicht mehr online
              // Protocoll anpassen error auch für CALLER_NICKNAME? oder RECEIVER_NICKNAME
              // komplett streichen
              // Fehler simulieren
              // Emepfänger hat den Hörer noch nicht abgenommen
              // und der Anrufer legt auf
              INCOMINGCALL error = new INCOMINGCALL();
              error.setCommand(Command.INCOMINGCALL);
              error.setHeader(ERROR);
              error.setDataset(Protocol.DATASET);
              error.setReceiverNickname(incoming.getCallerNickname()); // receiver und caller vertauschen
                                                                       // wegen response oder receiverNickname
                                                                       // optional
              error.setMultilingualkey(KEY.STRING_IST_NICHT_MEHR_ONLINE);
              // error senden
              log.info("-----> " + error.toString());
              sendMessage(error, session);
              return;
            }
            // response ist der CALLER telefonisch noch erreichbar?

            if (!databaseservice.isOnBusyByUid(incoming.getCallerUserid())) {
              // Gegenseite hat aufgelegt
              INCOMINGCALL error = new INCOMINGCALL();
              error.setCommand(Command.INCOMINGCALL);
              error.setHeader(ERROR);
              error.setDataset(Protocol.DATASET);
              error.setReceiverNickname(incoming.getCallerNickname()); // receiver und caller vertauschen
                                                                       // wegen response oder receiverNickname
                                                                       // optional
              error.setMultilingualkey(KEY.STRING_HAT_AUFGELEGT);
              // error senden
              log.info("-----> " + error.toString());
              sendMessage(error, session);
              return;
            }
            // Empfänger ist am telefonieren
            databaseservice.setOnBusy(incoming.getReceiverUserid(), true);
            // Beide Seiten als telefonierend eintragen
            databaseservice.insertToPhone(incoming.getCallerUserid(), incoming.getReceiverUserid());

            // an den Receiver CONFIRM senden, damit er den Lautsprecher starten kann
            INCOMINGCALL confirm = new INCOMINGCALL();
            confirm.setCommand(Command.INCOMINGCALL);
            confirm.setHeader(CONFIRM);
            confirm.setDataset(Protocol.DATASET);
            confirm.setCallerNickname(null);
            confirm.setCallerUserid(null);
            confirm.setCallerVoice(incoming.getCallerVoice());
            confirm.setCallerSession(callerSessionId);
            confirm.setReceiverNickname(null);
            confirm.setReceiverUserid(null);
            confirm.setReceiverSession(null);
            log.info("-----> " + confirm.toString());
            // CONFIRM wird an den Empfänger gesendet
            // Der Empfänger sendete RESPONSE und erhält ein CONFIRM zurück
            sendMessage(confirm, session);
            // an den CALLER wird die RESPONSE vom Empfänger weitergereicht
            log.info("-----> " + incoming.toString());
            // incoming ist hier eine RESPONSE an CALLER
            incoming.setCallerSession(null);
            incoming.setCallerNickname(null);
            incoming.setCallerUserid(null);
            incoming.setCallerVoice(null);
            incoming.setReceiverNickname(null);
            incoming.setReceiverUserid(null);
            sendMessage(incoming, callerSessionId);
            return;
          }
          else if (REQUEST == incoming.getHeader()) {
            // RECEIVER_SESSIONID habe ich aus incoming
            DatabaseService databaseservice = WebdatabaseImpl.getInstance();
            final String sessionid = databaseservice.getSessionidByUserid(incoming.getReceiverUserid());
            if (sessionid == null) {
              INCOMINGCALL error = new INCOMINGCALL();
              error.setCommand(Command.INCOMINGCALL);
              error.setHeader(ERROR);
              error.setDataset(Protocol.DATASET);
              error.setReceiverNickname(incoming.getReceiverNickname());
              error.setMultilingualkey(KEY.STRING_IST_NICHT_MEHR_ONLINE);
              log.info("-----> " + error.toString());
              sendMessage(error, session);
              return;
            }
            // Der CALLER telefoniert
            // TODO später zusammen mit dem Empfänger eintragen oder wieder löschen bei
            // error
            databaseservice.setOnBusy(incoming.getReceiverUserid(), true);
            // über den HEADER_RESPONSE sagt er, ich telefoniere
            log.info("-----> " + incoming.toString());
            sendMessage(incoming, sessionid);
            return;
          }
          return;
        }
        else if (message instanceof MICROERROR) {
          MICROERROR microerror = (MICROERROR) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          final String sessionid = databaseservice.getSessionidByUserid(microerror.getToUserid());
          if (sessionid == null) return;
          sendMessage(microerror, sessionid);
          return;
        }
        else if (message instanceof ONCALL) {
          ONCALL response = onCall((ONCALL) message);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof ONHOOK) {
          ONHOOK onhook = (ONHOOK) message;
          log.info("<----- " + onhook);
          final String sessionid;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          switch(onhook.getHeader()) {
            case REQUEST:
              if (databaseservice.findPartnerUserid(onhook.getCallerUserid()) == null) {
//                Clientseite
//                1. Anrufen
//                2. Abnehmen
//                3. Wählen
//                4. Auflegen

                // An den Receiver trotzdem durchstellen, aber nicht klingeln
                onhook.setSignal(false);
                if (onhook.isTo()) {
                  // Caller hat aufgelegt an Receiver senden
                  sessionid = databaseservice.getSessionidByUserid(onhook.getReceiverUserid());
                }
                else {
                  // Receiver hat aufgeleg an Caller senden
                  sessionid = databaseservice.getSessionidByUserid(onhook.getCallerUserid());
                }
                if (sessionid == null) return;
                sendMessage(onhook, sessionid);
                return;
              }
              if (onhook.isTo()) {
                // Caller hat aufgelegt
//                log.info("der Caller hat aufgelegt");
                // Pärchen aus der Telefonierentabelle löschen
                databaseservice.deletePartner(onhook.getCallerUserid()); // TB_TOPHONE

                databaseservice.setOnBusy(onhook.getCallerUserid(), false); // TB_PHONE
                sessionid = databaseservice.getSessionidByUserid(onhook.getReceiverUserid());

                // Telefoniert der Receiver?
                if (!databaseservice.isOnBusyByUid(onhook.getReceiverUserid())) { // TB_PHONE
                  // RECEIVER ist nicht am telefonieren
                  // ONHOOK muss trotzdem durchgestellt werden
                  // SIGNAL auf 1
                  log.info(onhook.toString());
                  return;
                }

              }
              else {

                // Receiver hat aufgelegt
//                log.info("der receiver hat aufgelegt");

                // Pärchen aus der Telefonierentabelle löschen
                databaseservice.deletePartner(onhook.getReceiverUserid());

                databaseservice.setOnBusy(onhook.getReceiverUserid(), false);
                sessionid = databaseservice.getSessionidByUserid(onhook.getCallerUserid());

                // Telefoniert der Caller?
                if (!databaseservice.isOnBusyByUid(onhook.getCallerUserid())) {
                  log.info("444444444444444444444"); // OK wird aufgerufen
                  return;
                }

              }
              if (sessionid == null) return;
              log.info("-----> " + onhook.toString());
              sendMessage(onhook, sessionid);
              break;
            default:
              break;
          }
          return;
        }
        else if (message instanceof UPDATEPHONE) {
          UPDATEPHONE response = updatePhone((UPDATEPHONE) message); // TB_PHONE
          sendMessage(response, session);
          return;
        }
        else if (message instanceof UPDATEPORT) {
          UPDATEPORT response = updateport((UPDATEPORT) message);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof USRLOGIN) {
          try {

            USRLOGIN login = (USRLOGIN) message;
            // IDENTITY ist RSA gecrypted
            // in IDENTIYT ist der AES Schlüssel, den wir für die Antwort brauchen
            // zurück wird IDENTITY AES geschickt

            log.info("<----- " + login.toString());

            String RSA = login.getIdentity();
            DatabaseService databaseservice = WebdatabaseImpl.getInstance();

            String privatekeyRSA64 = databaseservice.fetchRsaPrivateKey();
            PrivateKey privateKey = Crypto.loadPrivateRSAKey(privatekeyRSA64);
            String klartext = Crypto.decryptRSA(RSA, privateKey);
            Token token = Token.toToken(klartext);

            String aes = token.getAES();
            String mail = token.getEMail();
            String password = token.getPassword();
            String userid = token.getUserid();
            String onetime = token.getOnetime();
            Agent agent = login.getAgent();

            USRLOGIN response = databaseservice
                .loginUser(session.getId(), aes, mail, password, userid, agent, onetime);

            if (response.getHeader() == ERROR) {
              log.info("-----> " + response.toString());
              sendMessage(response, session);

              return;
            }
            if (response.getHeader() == CONFIRM) {
              session.getUserProperties().put("authorized", true);
              log.info("-----> " + response.toString());
              sendMessage(response, session);

              SecretKey secretKey = Crypto.getAESFromBase64(aes);
              String result = Crypto.decryptAES(response.getIdentity(), secretKey);
              userid = Token.toToken(result).getUserid();
              session.getUserProperties().put("userid", userid);
              databaseservice.updateConfirmationFlag(userid, mail, 0);
              USERONLINELIST userOnlinelist = databaseservice.fetchUseronlinelist(userid);
              Thread.sleep(50);
              log.info("-----> " + userOnlinelist.toString());
              broadcastAll(userOnlinelist);
              return;
            }

          }
          catch (Exception e) {
            log.error(e.fillInStackTrace());
            CloseReason reason = new CloseReason(CloseCodes.NORMAL_CLOSURE, e.getMessage());
            try {
              session.close(reason);
            }
            catch (IOException e1) {
              log.info(e1.getMessage(), e1);
            }
          }
          return;
        }
        else if (message instanceof CHANGETOPICMEMBER) {
          CHANGETOPICMEMBER member = (CHANGETOPICMEMBER) message;
          CHANGETOPICMEMBER response = changeTopicmember(member);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof CANDIDATETOPICMEMBER) {
          CANDIDATETOPICMEMBER member = (CANDIDATETOPICMEMBER) message;
          CANDIDATETOPICMEMBER response = candidateMember(member);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof TOPICMEMBER) {
          TOPICMEMBER member = (TOPICMEMBER) message;
          TOPICMEMBER response = topicMember(member);
          sendMessage(response, session);
          return;
        }
        else if (message instanceof DELETEROOM) {
          DELETEROOM member = (DELETEROOM) message;
          DELETEROOM response = deleteRoom(member);
          sendMessage(response, session);
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          List<String> roomlist = databaseservice.fetchRoomlist(session.getId());
          broadcast(roomlist);
          return;
        }
        else if (message instanceof UPDATEUSER) {
          UPDATEUSER updateuser = (UPDATEUSER) message;
          DatabaseService databaseservice = WebdatabaseImpl.getInstance();
          String sessionid = session.getId();
          String aes = databaseservice.fetchAES(sessionid); // kann Exception auftreten
          SecretKey secretAES = Crypto.getAESFromBase64(aes);
          String identity = updateuser.getIdentity();
          String back = Crypto.decryptAES(identity, secretAES);
          Token backToken = Token.toToken(back);

          // alten Nickname finden, falls der neue Nickname nicht erlaubt ist
          String alterNickname = databaseservice.fetchNickname(backToken.getUserid());
          String nickname = updateuser.getNickname();
          Integer fgColor = updateuser.getForegroundColor();
          Integer bgColor = updateuser.getBackgroundColor();

          // ONCALL und VOLUME fehlen
          UPDATEUSER response = databaseservice.updateUser(
              identity, backToken.getUserid(), backToken.getEMail(), backToken.getPassword(), nickname,
              fgColor, bgColor
          );

          // true CONFIRM
          // false ERROR

          sendMessage(response, session);
          // nickname chaträume im Nachgang anpassen
          if (CONFIRM == response.getHeader()) {

            try {
              alterNickname = databaseservice.fetchNickname(backToken.getUserid());
              if (alterNickname == null) {
                log.warn(backToken.getUserid() + " - Nickname nicht gefunden");
                return;
              }

              databaseservice.replacePrivateRoomname(response.getNickname(), alterNickname);
              boolean done = databaseservice.updateOWNER(backToken.getUserid(), response.getNickname());
              if (done) {
                // broadcast roomlist
                List<String> roomlist = databaseservice.fetchRoomlist(session.getId());
                broadcast(roomlist);
              }
//             an alle User USERONLINELIST senden 
              USERONLINELIST userOnlinelist = databaseservice.fetchUseronlinelist(backToken.getPassword());

              List<String> ids = sessionmap.values().stream().map(Session::getId)
                  .collect(Collectors.toList());
              List<String> sessionAsString = List.copyOf(ids);
              sendMessage(userOnlinelist, sessionAsString);
            }
            catch (DatabaseException e) {
              log.error(e.getMessage());
            }

          }

          return;
        }

        log.error("unbekannter Befehl, {}", message.toString());
      }
    });

  }



  private READTOPICROOMOWNER readTopicroom(READTOPICROOMOWNER request) throws DatabaseException {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    READTOPICROOMOWNER response = databaseservice.fetchTopicroom(request);
    return response;
  }



  /**
   * Löscht den User aus allen Onlinelisten.
   * 
   * @param sessionID
   *                  der Onlineschlüssel identifiziert einen Anwender
   */
  private void removeUser(String sessionID) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();

    String uid = databaseservice.getUserBySession(sessionID);
    if (uid == null) return;

    try {
      databaseservice.unmute(uid);
      cleanUser(uid, sessionID);
      USERONLINELIST userOnlinelist = databaseservice.fetchUseronlinelist(uid);
      log.info("-----> " + userOnlinelist.toString());
      broadcastAll(userOnlinelist);
    }
    catch (DatabaseException e) {
      log.error(e.getMessage());
    }

  }



  // TODO aktuell werden maximal 256 zurückgegeben.
  private List<Searchfile> searchfiles(SEARCHFILES request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    final int max_files = 256;
    List<Searchfile> foundFiles = databaseservice.selectSearchfiles(request.getPatterns(), max_files);
    return foundFiles;
  }



  private DOWNLOAD selectDOWNLOAD(DOWNLOAD request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    DOWNLOAD response = databaseservice.selectDOWNLOAD(request);
    return response;
  }



  private TOPICMEMBER topicMember(TOPICMEMBER request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    TOPICMEMBER response = databaseservice.fetchTopicmember(request);
    return response;
  }



  private UPDATEPHONE updatePhone(UPDATEPHONE request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    databaseservice.setOnBusy(request.getUserid(), request.isOnBusy());
    UPDATEPHONE response = new UPDATEPHONE();
    response.setCommand(Command.UPDATEPHONE);
    response.setHeader(RESPONSE);
    response.setDataset(Protocol.DATASET);
    response.setUserid(request.getUserid());
    response.setOnBusy(request.isOnBusy());
    return response;
  }



  private UPDATEPORT updateport(UPDATEPORT request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    databaseservice.updateUpdateport(request.getIp(), request.getPort());
    UPDATEPORT response = new UPDATEPORT();
    response.setCommand(Command.UPDATEPORT);
    response.setHeader(RESPONSE);
    response.setDataset(Protocol.DATASET);
    response.setUserid(request.getUserid());
    response.setIp(request.getIp());
    response.setPort(request.getPort());
    return response;
  }



  private UPLOADFILES uploadfiles(UPLOADFILES request) {
    DatabaseService databaseservice = WebdatabaseImpl.getInstance();
    try {
      if (request.hasStarted()) databaseservice.deleteUploadfiles(request.getUserid(), request.getIp());

      databaseservice.insertUploadfiles(
          request.getUserid(), request.getIp(), request.getPort(), request.getUploadfile()
      );

      UPLOADFILES uploadfiles = new UPLOADFILES();
      uploadfiles.setCommand(Command.UPLOADFILES);
      uploadfiles.setHeader(RESPONSE);
      uploadfiles.setDataset(Protocol.DATASET);
      uploadfiles.setUserid(request.getUserid());
      uploadfiles.setIp(request.getIp());
      uploadfiles.setStarted(request.hasStarted());
      return uploadfiles;
    }
    catch (SQLException e) {
      log.error(e.getMessage(), e.getCause());
      UPLOADFILES uploadfiles = new UPLOADFILES();
      uploadfiles.setCommand(Command.UPLOADFILES);
      uploadfiles.setHeader(ERROR);
      uploadfiles.setDataset(Protocol.DATASET);
      uploadfiles.setErrorCode(e.getErrorCode());
      uploadfiles.setErrorMessage(e.getMessage());
      return uploadfiles;
    }
  }

}
