gt:                         network.log(channel, Helpers.getTimestampEpochMillis() + text);
context: package xyz.guyaaltonen.pickleirc;
  
  import java.beans.PropertyVetoException;
  import java.io.BufferedReader;
  import java.io.BufferedWriter;
  import java.io.File;
  import java.io.FileNotFoundException;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.io.OutputStreamWriter;
  import java.net.Socket;
  import java.net.SocketAddress;
  import java.security.cert.Certificate;
  import java.security.cert.CertificateExpiredException;
  import java.security.cert.CertificateNotYetValidException;
  import java.security.cert.X509Certificate;
  import java.util.ArrayList;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  import javax.net.ssl.HandshakeCompletedListener;
  import javax.net.ssl.SSLException;
  import javax.net.ssl.SSLHandshakeException;
  import javax.net.ssl.SSLParameters;
  import javax.net.ssl.SSLSession;
  import javax.net.ssl.SSLSocket;
  import javax.net.ssl.SSLSocketFactory;
  import javax.swing.JTextPane;
  import javax.swing.SwingUtilities;
  import javax.swing.text.BadLocationException;
  import javax.swing.text.Document;
  import javax.swing.text.html.HTMLDocument;
  import xyz.guyaaltonen.pickleirc.ConnectionManager;
          
  
  public class Connection {
      private final int STATUS_MAX_LENGTH = 10000;
      private Socket socket;
      private SSLSocket socketSsl;
      private Network network;
      private JInternalFrameServerWindow serverWindow;
      private JInternalFrameChannelWindow[] channelWindows = new JInternalFrameChannelWindow[512]; // max 512 channel windows
      private JInternalFrameQueryWindow[] queryWindows = new JInternalFrameQueryWindow[512];
      private JInternalFrameListWindow listWindow;
      private Channel[] channelModels = new Channel[512]; // max 512 channels per connection
      private ChannelList channelList = new ChannelList(); // this is the list that appears when you type /list
      private ConnectionManager pcm;
      private InputStream inputRaw;
      private BufferedReader input;
      private OutputStream outputRaw;
      private BufferedWriter output;
      private boolean useSSL;
      private int port;
      private String host;
      private ClientLoop clientLoop;
      private boolean connected;
      private boolean stayconnected = true;
      private byte[] inBuffer = new byte[1000000];
      private byte[] outBuffer = new byte[1000000];
      private byte[] fileOut = new byte[1000000];
      private FileOutputStream outFileStream;
      private String nickname = "javaserver";
      private String ident = "pickle";
      private String realname = "javaserver";
      private JPopupMenuChannelList listMenu;
      private String realCurrentServer; // because one is often transferred upon connection, this is the actual server connected to
      private int timeout = 30 * 1000;
      private int timeSinceLastPing = 0;
      private int bytesSinceLastUpdate = 0;
      private static int speedUpdateInterval = 1000;
      private int reconnectTimeout = 10 * 1000;
      private boolean registered = false;
      private boolean terminate = false; // set this to true after closing status window intentionally
      private PingThread ppt;
      private SpeedThread pst;
      private int speed;
      private ArrayList<NickName> whoNicks = new ArrayList<NickName>();
      private Object nextWhoLock = new Object();
      private long nextWho = -1;
      private WhoNickConsumerThread whoThread;
      private ArrayList<String> whoNicksToSend = new ArrayList<String>();
      private boolean acceptInvalidCertificate = true;
      private boolean trustFailed = false;
      private String statusLog = "";
      private int MAX_LOG_SIZE = 20000;
      private final Document serverDocument;
      private final JTextPane statusPane;
      private Object connectionLock = new Object();
      private long DISCONNECTED_WAIT = 5000;
      
      
      public void addHostData(String nickname, String host, String ident) {
          int c = 0;
          NickName nick;
  
          while (channelModels[c] != null) {
              nick = channelModels[c].getNickName(nickname);
  
              if (nick != null) {
                  nick.addHostData(host, ident);
                  break;
              }
              c++;
          }
  
      }
  
      public void createQueryWindow(String querynick, Connection connection) {
          for (int i = 0; i < queryWindows.length; i++) {
              if (queryWindows[i] == null) {
                  queryWindows[i] = new JInternalFrameQueryWindow();
                  queryWindows[i].setQuery(querynick);
                  queryWindows[i].setConnection(this);
                  pcm.addQueryWindow(this, querynick, queryWindows[i]);
                  queryWindows[i].addKeyListener(Helpers.getMainFrame());
                  queryWindows[i].JTextFieldConsole.addKeyListener(Helpers.getMainFrame());
                  PCMHelper.getPCM().getButtonsPanel().revalidate();
                  queryWindows[i].setVisible(true);
                  break;
              }
          }
      }
  
      public JInternalFrameQueryWindow getQuery(String querynick) {
  //        JInternalFrameQueryWindow query = null;
          for (int i = 0; i < queryWindows.length; i++) {
              if (queryWindows[i] != null) {
                  if (queryWindows[i].getQuery().equalsIgnoreCase(querynick)) {
                      return queryWindows[i];
                  }
              } else {
                  return null;
              }
          }
  
          return null;
      }
  
      public void quit(String quitmsg) {
          try {
              sendMessage("QUIT :" + quitmsg);
              if (socket != null) {
                  this.socket.close();
              }
          } catch (IOException ex) {
              System.out.println("IOEXCEPTION at pickleConnection.quit()");
              //d.log(ex.toString());
          } finally {
              this.setStayConnected(false);
              this.setConnected(false,false);
              this.terminate();
  
          }
      }
  
      private void terminate() {
          this.terminate = true;
      }
  
      public void devoice(String nickname, Channel channel) {
          sendMessage("MODE " + channel.getChannelName() + " -v " + nickname);
      }
  
      public void voice(String nickname, Channel channel) {
          sendMessage("MODE " + channel.getChannelName() + " +v " + nickname);
      }
  
      public void deop(String nickname, Channel channel) {
          sendMessage("MODE " + channel.getChannelName() + " -o " + nickname);
      }
  
      public void op(String nickname, Channel channel) {
          sendMessage("MODE " + channel.getChannelName() + " +o " + nickname);
      }
  
      public void kickban(String nickname, Channel channel, String reason) {
  
      }
  
      public void silence(String nickname, Channel channel) {
  
      }
  
      public void kick(String nickname, Channel channel, String reason) {
          if (reason == null) {
              sendMessage("KICK " + channel.getChannelName() + " " + Helpers.sanitiseNick(nickname) + " :");
          } else {
              sendMessage("KICK " + channel.getChannelName() + " " + Helpers.sanitiseNick(nickname) + " :" + reason);
          }
      }
  
      public void whois(String nickname) {
          sendMessage("WHOIS :" + Helpers.sanitiseNick(nickname));
      }
  
      public void processCommand(String command, String channel) {
          String[] tokens = null;
          command = command.substring(1);
          command = command.replace("\r\n", "");
          command = command.replace("\n", "");
          tokens = command.split(" +");
          command = command.replaceFirst(tokens[0], tokens[0].toUpperCase());
          tokens[0] = tokens[0].toUpperCase();
  
          if (tokens[0].equals("JOIN")) {
              sendMessage(command);
          } else if (tokens[0].equals("DISCONNECT")) {
              setStayConnected(false);
              sendToWindow("", "*** Disconnected ***", 0, 0);
              quit("");
          } else if (tokens[0].equals("CONNECT")
                  || tokens[0].equals("RECONNECT")
                  || tokens[0].equals("SERVER")) {
              setStayConnected(true);
              sendToWindow("", "*** Connecting ***", 0, 0);
          } else if (tokens[0].equals("MODE")) {
              sendMessage(command);
          } else if (tokens[0].equals("PART")) {
              sendMessage(command);
          } else if (tokens[0].equals("LIST")) {
              /// NEEDS TO SEND "LIST *" BY DEFAULT, IF NO PARAMETERS GIVEN. BECAUSE FREENODE DOESN'T SEND CHANNELS BELOW 3 USERS OTHERWISE.
              sendMessage(command);
          } else if (tokens[0].equals("KICK")) {
              if (channel.equals("")) {
                  sendMessage("KICK " + allTokens(tokens, 1));
              } else {
                  String kicknick = tokens[1];
                  String kickmsg = allTokens(tokens, 2);
                  sendMessage("KICK " + channel + " " + kicknick + " :" + kickmsg);
              }
          } else if (tokens[0].equals("CLOSE")) {
              sendMessage("QUIT :" + command.substring(5));
              PCMHelper.getPCM().deleteConnection(this, this.network.getDefaultQuitMessage());
              setConnected(false,false);
              stayconnected = false;
              terminate = true;
          } else if (tokens[0].equals("QUIT")) {
              if (tokens.length > 1) {
                  sendMessage("QUIT :" + command.substring(5));
              } else {
                  sendMessage("QUIT :" + this.network.getDefaultQuitMessage());
              }
              setConnected(false,false);
              stayconnected = false;
          } // default
          else if(tokens[0].equals("LAF")) {
              Helpers.getMainFrame().refreshLAF();
          }
          else {
              sendMessage(command);
          }
      }
  
      public String allTokens(String[] tokens, int start) {
          String alltokens = "";
          int c = start;
          while (c < tokens.length) {
              alltokens = alltokens + tokens[c];
              if (c < tokens.length - 1) {
                  alltokens = alltokens + " ";
              }
              c++;
          }
  
          return alltokens;
      }
  
      public String allTokens(String[] tokens, int start, int end) {
          String alltokens = "";
          int c = start;
          while (c < tokens.length && c <= end) {
              alltokens += tokens[c] + " ";
              c++;
          }
          return alltokens;
      }
  
      public synchronized void sendMessage(String msg) {
          msg += "\r\n";
          byte[] outBuffer = msg.getBytes();
          try {
              if (outputRaw != null) {
                  outputRaw.write(outBuffer);
              }
          } catch (IOException ex) {
              //d.log("IOException in sendMessage() output.write()");
              System.out.println("IOException in sendMessage() output.write()");
              setConnected(false,true);
  /*
              try {
                  socket.close();
              } catch (IOException ex1) {
                  //d.log("IOException in sendMessage() socket.close()");
              }
  */
  
  // don't think we need this...
  /*
              try {
                  Thread.sleep(reconnectTimeout);
              } catch (InterruptedException ex1) {
                  //d.log(ex.toString());
              }
  */
          }
      }
  
      public Connection(String host, int port, boolean useSSL, ConnectionManager pcm, Network network) throws PropertyVetoException, FileNotFoundException {
          // this.outFileStream = new FileOutputStream(new File("picklelog.txt"));
          this.useSSL = useSSL;
          this.port = port;
          this.host = host;
          this.pcm = pcm;
          this.network = network;
          this.nickname = this.network.getDefaultNickname();
          this.serverWindow = new JInternalFrameServerWindow();
          this.serverWindow.setSize(100, 100);
          this.serverWindow.show();
          this.serverWindow.JTextFieldConsole.addKeyListener(Helpers.getMainFrame());
          this.serverDocument = serverWindow.JTextPaneStatus.getDocument();
          this.statusPane = serverWindow.JTextPaneStatus;
          this.serverWindow.setConnection(this);
          this.network.initLoggers(this);
      }
  
      public boolean connect() throws CertificateNotYetValidException {
          nextWho = -1;
          whoNicks.clear();
          whoNicksToSend.clear();
          
          if(whoThread == null) {
              whoThread = new WhoNickConsumerThread(1000, whoNicksToSend, this);
              whoThread.start();
          }
  
          registered = false;
          try {
              if (useSSL == false) {
                  if(socket == null || socket.isClosed() == true) {
                      socket = new Socket(host, port);
                  }
                  socket.setSoTimeout(timeout);
                  inputRaw = socket.getInputStream();
                  input = new BufferedReader(new InputStreamReader(inputRaw));
                  outputRaw = socket.getOutputStream();
                  output = new BufferedWriter(new OutputStreamWriter(outputRaw));
  
              } else {
                  SSLSocketFactory sslFactory = null;
  
                  if (acceptInvalidCertificate == true && trustFailed == true) {
                      sendToWindow(null, "Attempting to connect via SSL without chain of trust...", 0, ColorPalette.COLOR_WARNING);
                      sendToWindow(null, "WARNING: This is far less secure! Your connection may be spied upon.", 0, ColorPalette.COLOR_WARNING);
                      sendToWindow(null, "To mitigate this possibility:", 0, ColorPalette.COLOR_WARNING);
                      sendToWindow(null, "1. Disconnect now.", 0, ColorPalette.COLOR_WARNING);
                      sendToWindow(null, "2. Untick 'Accept invalid SSL certificates' in File > Edit networks.", 0, ColorPalette.COLOR_WARNING);
  
                      try {
                          sslFactory = (new InsecureSocketFactory()).getInsecureSslSocketFactory();
                      } catch (Exception ex) {
                          Logger.getLogger(Connection.class.getName()).log(Level.SEVERE, null, ex);
                      }
                  } 
                  else {
                      sendToWindow(null, "Connecting with SSL...", 0, ColorPalette.COLOR_GOOD);
                      sslFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
                  }
  
                  socket = new Socket(host, port);
  
                  try {
                      socketSsl = (SSLSocket) sslFactory.createSocket(socket, host, port, true);
                      socketSsl.setSoTimeout(timeout);
                      socketSsl.setUseClientMode(true);
                  } catch (SSLException e) {
                      System.out.println(e.toString());
                  }
  
                  socketSsl.startHandshake();
                  inputRaw = socketSsl.getInputStream();
                  input = new BufferedReader(new InputStreamReader(inputRaw));
                  outputRaw = socketSsl.getOutputStream();
                  output = new BufferedWriter(new OutputStreamWriter(outputRaw));
              }
              setConnected(true,false);
              return true;
          }
          catch (SSLHandshakeException e) {
              System.out.println("SSLHandshakeException in connect(): " + e.toString());
              sendToWindow(null, "Failed to establish SSL trust chain!", 0, ColorPalette.COLOR_WARNING);
              setConnected(false,true);
              trustFailed = true;
              return false;
           }
          catch (IOException e) {
              System.out.println("IOException in connect(): " + e.toString());
              setConnected(false,true);
              return false;
          }
      }
  
      public void setConnected(boolean connected,boolean pause) {
          synchronized(connectionLock) {
              if (pause == true && connected == false) {
                  try {
                      Thread.sleep(DISCONNECTED_WAIT);
                  } catch (InterruptedException ex) {
                      Logger.getLogger(Connection.class.getName()).log(Level.SEVERE, null, ex);
                  }
              }            
              this.connected = connected;
          }
      }
  
      public boolean isConnected() {
          synchronized(connectionLock) {
              return connected;
          }
      }
  
      public void runClientLoop() {
          clientLoop = new ClientLoop(this);
          clientLoop.start();
      }
  
      boolean isTerminated() {
          return terminate;
      }
  
      public boolean getStayConnected() {
          return stayconnected;
      }
  
      private void setStayConnected(boolean stayconnected) {
          this.stayconnected = stayconnected;
      }
  
      public void sendToWindow(String channel, String text, int type, int color) {    // type: 0: status, 1: channel, 2: query  
  //        Runnable sendToWindow = new Runnable() {
          //          public synchronized void run() {
          if (type == 0) {
              SwingUtilities.invokeLater(() -> {
                  if (serverDocument.getLength() != 0) {
                      appendStatusLog(Helpers.plainToHtml("\n" + Helpers.getTimestamp() + text), color);
                  } else {
                      appendStatusLog(Helpers.plainToHtml(Helpers.getTimestamp() + text), color);
                  }
  
                  statusPane.setText(serverWindow.getConnection().getStatusLog());
                  serverWindow.setScrollCausedByText(true);
                  if (serverWindow != PCMHelper.getPCM().getCurrentWindow()) {
                      serverWindow.getButton().setNotify(true);
                  }
              });
          } else if (type == 1) {
              int c = 0;
              while (channelWindows[c] != null) {
                  if (channelWindows[c].getChannel().equals(channel)) {
                      Channel channelM = channelWindows[c].getChannelModel();
                      JInternalFrameChannelWindow channelW = channelWindows[c];
  
                      channelM.appendLog(Helpers.plainToHtml("\n" + Helpers.getTimestamp() + text), color);
                      
                      channelM.appendLine(Helpers.getTimestamp() + text);
                      
                      // new, conditional upon window being active
                      if (PCMHelper.getPCM().getCurrentWindow() == channelW) {
                          channelM.getController().rebuildIRCDocument(channelW, channelM);
                      }
                      
                      if (PCMHelper.getPCM().getCurrentWindow() == channelW) {
                          channelM.getController().rebuildHTML(channelW);
                      }
                      
                      
                      network.log(channel, Helpers.getTimestampEpochMillis() + text);
  
                      SwingUtilities.invokeLater(() -> {
                          if (channelW != PCMHelper.getPCM().getCurrentWindow()) {
                              channelW.getButton().setNotify(true);
                          }
                      });
                      break;
                  }
                  c++;
              }
          } else if (type == 2) {
              SwingUtilities.invokeLater(() -> {
                  int c = 0;
                  while (queryWindows[c] != null) {
                      if (queryWindows[c].getQuery().equalsIgnoreCase(channel)) {
                          if (queryWindows[c].getTextPaneDocument().getLength() != 0) {
                              queryWindows[c].getQueryModel().appendLog(Helpers.plainToHtml("\n" + Helpers.getTimestamp() + text), color);
                          } else {
                              queryWindows[c].getQueryModel().appendLog(Helpers.plainToHtml(Helpers.getTimestamp() + text), color);
                          }
                          queryWindows[c].JTextPaneChat.setText(queryWindows[c].getQueryModel().getLog());
                          queryWindows[c].setScrollCausedByText(true);
  