/*
   This server takes
      port  root_directory  directory_listing
   as optional command-line arguments. The port number,
   root directory, and directory listing parameters can
   also be configured in the httpserver.properties file
   in this directory.
*/

import java.util.Properties;
import java.time.format.DateTimeFormatter;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.LocalDateTime;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;

/**
   This HTTP server only implements the GET method.

   This server does implement persistent connections
   (keep-alive). For this server there are three ways
   to terminate a persistent socket connection with
   a client.
      1) The client closes its socket.
      2) The client sends a "Connection: close" request header.
      3) This server's client socket times out while this server
         is trying to read a request line from the client.

   This server is multi-threaded, so it can communicate on
   several (persistent) tcp connections simultaneously.
*/
class HttpServer_v3 implements Runnable
{
   public  static final String SERVER_NAME = "HttpServer_v3.java";
   private static final int SERVER_PORT = 8080; // Should be above 1023.
   private static final String SERVER_ROOT = "public_html";
   private static final boolean DIRECTORY = true;
   private static final int SOCKET_TIMEOUT = 30_000; // 30 seconds.

   public  final int portNumber;
   private final String serverRoot;
   private final boolean doDirectory;
   private final String localHostName;
   private final int socketTimeout;

   /**
      The main() method configures an instance of the server
      using a httpserver.properties file and command-line
      arguments.

      Then the server instance is started running.
   */
   public static void main(String[] args)
   {
      String serverRoot = SERVER_ROOT;    // default value
      int portNumber = SERVER_PORT;       // default value
      boolean doDirectory = DIRECTORY;    // default value
      int socketTimeout = SOCKET_TIMEOUT; // default value

      // Use the properties file (if it exists) to set
      // the server's port number, root directory,
      // directory listing behavior, and socket timeout.
      try (final var br = new BufferedReader(
                             new FileReader(
                                new File("httpserver.properties"))))
      {
         final Properties properties = new Properties();
         properties.load(br);
         final String root = properties.getProperty("root");
         final String port = properties.getProperty("port");
         final String directory = properties.getProperty("directory");
         final String timeout = properties.getProperty("timeout");
         if (root != null) serverRoot = root;
         if (port != null) portNumber = Integer.parseInt(port);
         if (directory != null) doDirectory = Boolean.parseBoolean(directory);
         if (timeout != null) socketTimeout = Integer.parseInt(timeout);
      }
      catch (FileNotFoundException e)
      {
         /* Ignore FileNotFoundException. */
      }
      catch (IOException e)
      {
         e.printStackTrace(System.out);
         System.exit(-1);
      }

      // Use command-line arguments (if they exist) to
      // set the server's port number, root directory,
      // directory listing behavior, and socket timeout.
      if (0 < args.length)
      {
         portNumber = Integer.parseInt(args[0]);
      }
      if (1 < args.length)
      {
         serverRoot = args[1];
      }
      if (2 < args.length)
      {
         doDirectory = Boolean.parseBoolean(args[2]);
      }
      if (3 < args.length)
      {
         socketTimeout = Integer.parseInt(args[3]);
      }

      // Instantiate an instance of the http server.
      final var http_server = new HttpServer_v3(portNumber,
                                                serverRoot,
                                                doDirectory,
                                                socketTimeout);

      // Start up the instance of the http server.
      http_server.run();
   }


   /**
      This constructor initializes the server's
      port number, root folder, directory
      listing option, and socket timeout value.
   */
   public HttpServer_v3(int portNumber,
                        String serverRoot,
                        Boolean doDirectory,
                        int socketTimeout)
   {
      this.portNumber    = portNumber;
      this.serverRoot    = serverRoot;
      this.doDirectory   = doDirectory;
      this.socketTimeout = socketTimeout;

      String name = null;
      try
      {
         name = InetAddress.getLocalHost().getHostName();
      }
      catch (UnknownHostException e)
      {
         logMessage("Unable to determine this host's name.");
      }
      this.localHostName = name;
   }


   /**
      Implement the Runnable interface.

      This method creates the server socket and begins listening on it.

      When a client connection is established, the connection is
      dispatched, in its own thread, to the handleConnection() method.
   */
   @Override
   public void run()
   {
      // Identify the server in the console log.
      logMessage(SERVER_NAME + " (parallelized)");

      // Get this server's process id number (PID). This helps
      // to identify the server in TaskManager or TCPView.
      final ProcessHandle handle = ProcessHandle.current();
      final long pid = handle.pid();
      logMessage("Process ID number (PID): " + pid );

      // Get the name and IP address of the local host and
      // print them on the console for information purposes.
      try
      {
         final InetAddress address = InetAddress.getLocalHost();
         logMessage("Hostname: " + address.getCanonicalHostName() );
         logMessage("IP address: " +address.getHostAddress() );
         logMessage("Using port no. " + portNumber);
      }
      catch (UnknownHostException e)
      {
         logMessage("Unable to determine this host's address.");
         System.out.println( e );
      }

      // Check and log the server's root folder.
      try
      {
         final File dir = new File(serverRoot).getCanonicalFile();
         if ( dir.exists()
           && dir.canRead()
           && dir.isDirectory() )
         {
            logMessage("Serving root folder = " + dir);
         }
         else
         {
            logMessage("Error: Unable to access the server's root folder.");
            logMessage("root folder: " + dir);
            new Error("root folder not found").printStackTrace(System.out);
            System.exit(-1);
         }
      }
      catch (IOException e)
      {
         logMessage("Error: Unable to open the server's root folder.");
         logMessage("root folder: " + serverRoot);
         e.printStackTrace(System.out);
         System.exit(-1);
      }

      logMessage("Server allows dynamic directory listings = " + doDirectory);

      int clientCounter = 0;

      // Create the server's listening socket.
      try (final var serverSocket = new ServerSocket(portNumber))
      {
         logMessage("Server online...");
         while (true) // Run forever, accepting and servicing clients.
         {
            // Wait for an incoming client request.
            logMessage(""); // blank line
            logMessage("Waiting for client " + (1+clientCounter) + " to connect:");
            try
            {
               final Socket clientSock = serverSocket.accept();
               ++clientCounter;
               // Get the client's host name, IP address, and port and log them to the console.
               final InetAddress clientIP = clientSock.getInetAddress();
               final String clientHostAddress = clientIP.getHostAddress();
               final String clientHostName = clientIP.getCanonicalHostName();
               final int clientPortNumber = clientSock.getPort();
               logMessage(""); // blank line
               logMessage(clientCounter, clientHostName
                                       + " at " + clientHostAddress
                                       + ":" + clientPortNumber);

               // Handle this connection in its own thread.
               final int clientNumber = clientCounter; // Thread requires final variables.
               new Thread()
               {
                  @Override
                  public void run()
                  {
                     try
                     {
                        handleConnection(clientNumber, clientSock);
                     }
                     catch (UnsupportedEncodingException e)
                     {
                        logMessage(clientNumber, "Error communicating with client");
                        System.out.println( e );
                     }
                     catch(IOException e)
                     {
                        logMessage(clientNumber, "Error communicating with client");
                        System.out.println( e );
                     }
                  }
               }.start();
            }
            catch (IOException e)
            {
               logMessage(clientCounter, "Error communicating with client");
               System.out.println( e );
            }
            finally
            {
               logMessage(clientCounter, "Closed socket.");
            }
         }
      }
      catch (IOException | IllegalArgumentException e)
      {
         logMessage("Error creating server socket listening on port no. " + portNumber);
         e.printStackTrace(System.out);
         System.exit(-1);
      }
   }


   /**
      Handle multiple requests from one client on a single connection.

      This method sets up the persistent connection logic.
   */
   public void handleConnection(int clientNumber,
                                Socket clientSocket)
   throws UnsupportedEncodingException, IOException
   {
      // Make the low-level input and output streams easier to use.
      try (final var inFromClient = new DataInputStream(
                                       new BufferedInputStream(
                                          clientSocket.getInputStream()));
           final var outToClient = new DataOutputStream(
                                      new BufferedOutputStream(
                                         clientSocket.getOutputStream())))
      {
         boolean keepAlive = true;
         while (keepAlive)
         {
            keepAlive = handleRequest(clientNumber, clientSocket, inFromClient, outToClient);
         }
      }

      clientSocket.close();
      return;  // Finished with this client.
   }


   /**
      Dispatch one client request to the appropriate method handler.

      1.) Read the request line.
      2.) Read all the request headers and store
          important values in an instance of HeaderData.
      3.) Parse and validate the request line.
      4.) Dispatch the request method.

      The boolean return value determines if the persistent connection
      should be kept open (keep-alive) or be closed.
   */
   @SuppressWarnings("deprecation") // readLine() in DataInputStream
   public boolean handleRequest(int clientNumber,
                                Socket clientSocket,
                                DataInputStream inFromClient,
                                DataOutputStream outToClient)
   throws UnsupportedEncodingException, IOException
   {
      logMessage(clientNumber, "===> Reading request line & request headers from Client:");

      boolean keepAlive = true; // default value

      //**** 1.) Read the request line.
      clientSocket.setSoTimeout(socketTimeout); // Timeout after socketTimeout milliseconds.
      String requestLine;
      try
      {
         requestLine = inFromClient.readLine(); // deprecated
      }
      catch (SocketTimeoutException e) // the first way to end the persistent connection
      {
         logMessage(clientNumber, "===> Client's socket timed out (" + socketTimeout + " milliseconds)");
         keepAlive = false;
         return keepAlive;  // Finished with this request.
      }
      finally
      {
         clientSocket.setSoTimeout(0); // remove the timer from the socket
      }

      // Log the request line.
      logMessage(clientNumber, ">" + requestLine);

      if (null == requestLine) // eof, the second way to end the persistent connection
      {
         logMessage(clientNumber, "===> Read \"null\" request line (eof)");
         keepAlive = false;
         return keepAlive;  // Finished with this request.
      }
      requestLine = requestLine.trim();


      //**** 2.) Read all the request headers.
      //****     Store important values in an instance of HeaderData.
      final HeaderData headerData = new HeaderData();
      headerData.connectionClose = false; // Default for HTTP/1.1.

      String requestHeader;
      while ( (requestHeader = inFromClient.readLine()) != null ) // While not eof.
      {
         // Log the request header.
         logMessage(clientNumber, ">" + requestHeader);
         if ( requestHeader.startsWith("Connection") )
         {
            final int i = requestHeader.indexOf(':');
            final String value = requestHeader.substring(i+1).trim();
            if ( value.equals("close") )  // the third way to end the persistent connection
            {
               headerData.connectionClose = true;
               keepAlive = false;
            }
         }
         else if ( requestHeader.startsWith("Content-Length") )
         {
            final int i = requestHeader.indexOf(':');
            headerData.entityLength = Integer.parseInt(requestHeader.substring(i+2));
            // Log the value of Content-Length
            //logMessage(clientNumber, "===> Content-Length = " + headerData.entityLength);
         }
         else if ( requestHeader.isEmpty() )  // stop on a blank line
         {
            break;
         }
      }
      //**** Done reading the request headers.


      //**** 3.) Parse and validate the request line.
      // Break the request line into a request method, url, and http version.
      final int index1 = requestLine.indexOf(" ");
      final int index2 = requestLine.lastIndexOf(" ");
      final String requestMethod = requestLine.substring(0, index1).toUpperCase();
      final String requestURL = requestLine.substring(index1, index2).trim();
      final String requestHTTP = requestLine.substring(index2).trim().toUpperCase();

      // Check for a proper version of HTTP.
      if ( ! ( requestHTTP.equals("HTTP/1.1")
            || requestHTTP.equals("HTTP/1.0")
            || requestHTTP.equals("HTTP/0.9") ) )
      {
         sendErrorResponse(outToClient,
             "505", "HTTP Version Not Supported",
             "The HTTP version in this request<br>" + requestLine +
                  "<br>is unsupported.",
             headerData,
             clientNumber);

         return keepAlive;  // Finished with this request.
      }

      // Check that the request method is a defined method.
      if ( ! ( requestMethod.equals("GET")
            || requestMethod.equals("HEAD")
            || requestMethod.equals("OPTIONS")
            || requestMethod.equals("POST")
            || requestMethod.equals("PUT")
            || requestMethod.equals("PATCH")
            || requestMethod.equals("DELETE")
            || requestMethod.equals("TRACE")
            || requestMethod.equals("CONNECT") ) )
      {
         sendErrorResponse(outToClient,
             "501", "Not Implemented",
             "The HTTP method in this request<br>" + requestLine +
                  "<br>is not supported.",
             headerData,
             clientNumber);

         return keepAlive;  // Finished with this request.
      }

      // Important: We need to decode the URL from its urlencoded form.
      // See
      //   https://docs.oracle.com/en/java/javase/21/docs/api//java.base/java/net/URLDecoder.html
      // or
      //   http://www.w3schools.com/tags/ref_urlencode.asp
      // or
      //   http://en.wikipedia.org/wiki/URL_encoding
      //
      String decodedURL = "";
      try
      {
         decodedURL = URLDecoder.decode(requestURL, "UTF-8"); // throws UnsupportedEncodingException
      }
      catch (IllegalArgumentException e)
      {
         logMessage(clientNumber, "===> Illegal characters encountered in URL.");
         logMessage(clientNumber, "===> requestURL = " + requestURL);
         System.out.println( e );
         sendErrorResponse(outToClient,
             "400", "Bad Request",
             "This URL<br><b>" + requestURL +
                  "</b><br>cannot be decoded.",
             headerData,
             clientNumber);

         return keepAlive;  // Finished with this request.
      }

      // Log the results of decoding the URL.
      logMessage(clientNumber, "===> requestURL = " + requestURL);
      logMessage(clientNumber, "===> decodedURL = " + decodedURL);

      // Break the decoded URL into a resource and a query.
      // (We need to be careful about a ? with nothing after it.)
      final String resourceName;
      final String queryString;
      if ( 0 < decodedURL.indexOf('?') )
      {
         final String[] temp = decodedURL.split("\\Q?\\E");
         resourceName = (temp.length > 0) ? temp[0] : decodedURL;
         queryString  = (temp.length > 1) ? temp[1] : null;
      }
      else
      {
         resourceName = decodedURL;
         queryString = null;
      }

      // Important: Check that the resource is not a file
      // that "escapes" from the server's root directory.
      // This is a form of server attack that tries to
      // sneak in a URL like this.
      //    /../../../../Windows/System32/cmd.exe
      // See
      //   https://en.wikipedia.org/wiki/Directory_traversal_attack
      // or
      //   https://owasp.org/www-community/attacks/Path_Traversal
      //
      final File file1 = new File(serverRoot).getCanonicalFile();
      final File file2 = new File(serverRoot + resourceName).getCanonicalFile();
      if (! file2.getAbsolutePath().startsWith(file1.getAbsolutePath()))
      {
         sendErrorResponse(outToClient,
             "403", "Forbidden",
             "You can't pull this stunt<br>"+requestLine+"<br>on us.",
             headerData,
             clientNumber);

         keepAlive = false;
         return keepAlive;  // Finished with this request.
      }

      // Log the results of parsing the request line.
      logMessage(clientNumber, "===> Request method = " + requestMethod);
      logMessage(clientNumber, "===> Resource name = " + resourceName);
      logMessage(clientNumber, "===> Query string = " + queryString);
      logMessage(clientNumber, "===> HTTP version = " + requestHTTP);
      //**** Done parsing and validating the request line.


      //**** 4.) Dispatch the request method.
      if ( requestMethod.equals("GET") )
      {
         doGet(requestMethod,
               inFromClient,
               outToClient,
               resourceName,
               queryString,
               headerData,
               clientNumber);
      }
      else if ( requestMethod.equals("OPTIONS") )
      {
         doOptions(inFromClient,
                   outToClient,
                   resourceName,
                   queryString,
                   headerData,
                   clientNumber);
      }
      else if ( requestMethod.equals("HEAD")
             || requestMethod.equals("POST")
             || requestMethod.equals("PUT")
             || requestMethod.equals("PATCH")
             || requestMethod.equals("DELETE")
             || requestMethod.equals("TRACE")
             || requestMethod.equals("CONNECT") )
      {
         doNotSuported(outToClient,
                       requestMethod,
                       headerData,
                       clientNumber);
      }
      else // Undefined request method.
      {
         sendErrorResponse(outToClient,
             "501", "Not Implemented",
             "This server does not implement this method: " + requestMethod,
             headerData,
             clientNumber);
      }

      return keepAlive;  // Finished with this request.
   }


   /**
      Dispatch static GET requests.

      See
         https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/GET
   */
   public void doGet(String requestMethod,
                     DataInputStream inFromClient,
                     DataOutputStream outToClient,
                     String resourceName,
                     String queryString,
                     HeaderData headerData,
                     int clientNumber)
   {
      logMessage(clientNumber, "===> Do GET method");

      // Make sure the resource exits and is readable.
      final File file = new File(serverRoot + resourceName);
      if ( ! file.exists() || ! file.canRead() )
      {
         if (! file.exists())
         {
            logMessage(clientNumber, "===> File not found error response");

            sendErrorResponse(outToClient,
                "404", "Not Found",
                "The requested URL <b>"+resourceName+"</b> was not found on this server.",
                headerData,
                clientNumber);
         }
         else
         {
            logMessage(clientNumber, "===> Forbidden file error response");

            sendErrorResponse(outToClient,
                "403", "Forbidden",
                "The requested URL "+resourceName+" is not readable.",
                headerData,
                clientNumber);
         }

         return;  // Finished with this request.
      }

      if ( file.isDirectory() )
      {
         // Check for existence of an index.html file.
         final File file2 = new File(serverRoot + resourceName + "index.html");
         if ( file2.exists() && file2.canRead() )
         {
            doStaticGet(requestMethod,
                        inFromClient,
                        outToClient,
                        resourceName + "index.html",
                        queryString,
                        headerData,
                        clientNumber,
                        file2);
         }
         else if (doDirectory)
         {
            doDynDirListing(requestMethod,
                            inFromClient,
                            outToClient,
                            resourceName,
                            queryString,
                            headerData,
                            clientNumber,
                            file);
         }
         else
         {
            logMessage(clientNumber, "===> Dynamic directory listing error response");

            sendErrorResponse(outToClient,
                "403", "Forbidden",
                "The requested URL "+resourceName+" is not readable.",
                headerData,
                clientNumber);
         }
      }
      else // The file exists, so send it to the client.
      {
         doStaticGet(requestMethod,
                     inFromClient,
                     outToClient,
                     resourceName,
                     queryString,
                     headerData,
                     clientNumber,
                     file);
      }
   }


   /**
      Send a static resource to the client.
   */
   public void doStaticGet(String requestMethod,
                           DataInputStream inFromClient,
                           DataOutputStream outToClient,
                           String resourceName,
                           String queryString,
                           HeaderData headerData,
                           int clientNumber,
                           File file)
   {
      logMessage(clientNumber, "===> Do static GET method");

      try (final var fileStream = new BufferedInputStream(
                                     new FileInputStream(file)))
      {
         // Read all the bytes from the file into a buffer.
         final int numberOfBytes = (int)file.length();
         final byte[] bytesFromFile = new byte[numberOfBytes];
         fileStream.read(bytesFromFile);

         // Send the HTTP response line and headers.
         // The response line.
         doMessage(outToClient, clientNumber, "HTTP/1.1 200 OK");
         // The response headers.
         final String date = OffsetDateTime.now(ZoneOffset.UTC)
                               .format(DateTimeFormatter.RFC_1123_DATE_TIME);
         doMessage(outToClient, clientNumber, "Date: " + date);
         doMessage(outToClient, clientNumber, "Server: " + SERVER_NAME);
         if (headerData.connectionClose)
         {
            doMessage(outToClient, clientNumber, "Connection: close");
         }
         // Determine the resource's content type.
         final String contentType = mimeType(resourceName);
         doMessage(outToClient, clientNumber, "Content-Type: " + contentType);
         doMessage(outToClient, clientNumber, "Content-Length: " + numberOfBytes);
         // Add a blank line to denote end of response headers.
         doMessage(outToClient, clientNumber, "");
         outToClient.flush();

         // Send the entity body.
         outToClient.write(bytesFromFile, 0, numberOfBytes);
         outToClient.flush();
         //System.out.print(new String(bytesFromFile));
         logMessage(clientNumber, "<=== Resource " + resourceName + " sent to Client");
      }
      catch (FileNotFoundException e)
      {
         logMessage(clientNumber, "===> doStaticGet: Unable to find file.");
         System.out.println( e );
      }
      catch (IOException e)
      {
         logMessage(clientNumber, "===> doStaticGet: Unable to read file.");
         System.out.println( e );
      }
   }


   /**
      Send the client a dynamically generated html page
      that is a listing of the given directory.
   */
   public void doDynDirListing(String requestMethod,
                               DataInputStream inFromClient,
                               DataOutputStream outToClient,
                               String resourceName,
                               String queryString,
                               HeaderData headerData,
                               int clientNumber,
                               File file)
   {
      logMessage(clientNumber, "===> Do dynamic directory listing");

      // Get an array of all the files (and directories) in this directory.
      File[] contents = file.listFiles();

      // Build the response body.
      String body = "";
      body += "<html>\r\n" +
              "<head>\r\n" +
              "<title>Index of " + resourceName + "</title>\r\n" +
              "</head>\r\n" +
              "<body>\r\n" +
              "<h1>Index of " + resourceName + "</h1>\r\n" +
              "<hr/>\r\n" +
              "<pre>\r\n" +
              "<table border=\"0\">\r\n";
      // Put in a link to the parent directory.
      body += "<tr><td><a href=\"" + resourceName + "..\">Parent directory</a></td></tr>\r\n";
      for (final File fileInDir : contents)
      {
         body += "<tr><td><a href=\""
              + resourceName
              + fileInDir.getName();
         if( fileInDir.isDirectory() )
         {
            body += "/";
         }
         body += "\">";
         body += fileInDir.getName();
         if( fileInDir.isDirectory() )
         {
            body += "/";
         }
         body += "</a></td></tr>\r\n";
      }
      body += "</table>\r\n" +
              "</pre>\r\n" +
              "<hr/>\r\n" +
              "<address>" + SERVER_NAME
                          + " at " + localHostName
                          + " Port " + portNumber + "</address>\r\n" +
              "</body>\r\n" +
              "</html>\r\n";
      try
      {
         // Send the HTTP response line and headers.
         // The response line.
         doMessage(outToClient, clientNumber, "HTTP/1.1 200 OK");
         // The response headers.
         final String date = OffsetDateTime.now(ZoneOffset.UTC)
                               .format(DateTimeFormatter.RFC_1123_DATE_TIME);
         doMessage(outToClient, clientNumber, "Date: " + date);
         doMessage(outToClient, clientNumber, "Server: " + SERVER_NAME);
         doMessage(outToClient, clientNumber, "Connection: close");
         doMessage(outToClient, clientNumber, "Content-Type: text/html");
         doMessage(outToClient, clientNumber, "Content-Length: " + body.length());
         // Add a blank line to denote end of response headers.
         doMessage(outToClient, clientNumber, "");
         outToClient.flush();

         // Send the entity body.
         outToClient.writeBytes(body);
         outToClient.flush();
         //System.out.print(body);
         logMessage(clientNumber, "<=== Entity body sent to Client");
      }
      catch (IOException e)
      {
         logMessage(clientNumber, "===> doDynDirListing: Unable to write to client.");
         System.out.println( e );
      }
   }


   /**
      Implement the HTTP OPTIONS command.

      Send the client a response with a list of the
      HTTP methods implemented by this server.

      See
         https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/OPTIONS
   */
   public void doOptions(DataInputStream inFromClient,
                         DataOutputStream outToClient,
                         String resourceName,
                         String queryString,
                         HeaderData headerData,
                         int clientNumber)
   {
      logMessage(clientNumber, "===> Do OPTIONS method");

      try
      {
         // Send the HTTP response line and headers.
         // The response line.
         doMessage(outToClient, clientNumber, "HTTP/1.1 200 OK");
         // The response headers.
         final String date = OffsetDateTime.now(ZoneOffset.UTC)
                               .format(DateTimeFormatter.RFC_1123_DATE_TIME);
         doMessage(outToClient, clientNumber, "Date: " + date);
         doMessage(outToClient, clientNumber, "Server: " + SERVER_NAME);
         if (headerData.connectionClose)
         {
            doMessage(outToClient, clientNumber, "Connection: close");
         }
         doMessage(outToClient, clientNumber, "Allow: GET,OPTIONS");
         // Add a blank line to denote end of response headers.
         doMessage(outToClient, clientNumber, "");
         outToClient.flush();
      }
      catch (IOException e)
      {
         logMessage(clientNumber, "===> doOptions: Unable to write to client.");
         System.out.println( e );
      }
   }


   /**
      Send the client an error message
      for unsupported HTTP methods.
   */
   public void doNotSuported(DataOutputStream outToClient,
                             String method,
                             HeaderData headerData,
                             int clientNumber)
   {
      logMessage(clientNumber, "===> Do unsupported method");

      sendErrorResponse(outToClient,
          "400", "Bad Request",
          "This server cannot yet handle " + method + " requests.",
          headerData,
          clientNumber);
   }


   /**
      Send the client an HTTP error response code along
      with a short web page containing error information.
   */
   public void sendErrorResponse(DataOutputStream outToClient,
                                 String code,
                                 String shortMsg,
                                 String longMsg,
                                 HeaderData headerData,
                                 int clientNumber)
   {
      logMessage(clientNumber, "===> Send error response");

      // Build the response body.
      String body = "<!doctype html>\r\n" +
                    "<html>\r\n" +
                    "<head>\r\n" +
                    "<title>" + code + " " + shortMsg + "</title>\r\n" +
                    "</head>\r\n" +
                    "<body>\r\n" +
                    "<h1>" + shortMsg + "</h1>\r\n" +
                    "<p>" + code + ": " + shortMsg + "</p>\r\n" +
                    "<p>" + longMsg + "</p>\r\n" +
                    "<hr>\r\n" +
                    "<address>" + SERVER_NAME + " at " +
                        localHostName + " Port " + portNumber +
                    "</address>\r\n" +
                    "</body>\r\n" +
                    "</html>\r\n";
      try
      {
         // Send the HTTP response line and headers.
         // The response line.
         doMessage(outToClient, clientNumber, "HTTP/1.1 " + code + " " + shortMsg);
         // The response headers.
         final String date = OffsetDateTime.now(ZoneOffset.UTC)
                               .format(DateTimeFormatter.RFC_1123_DATE_TIME);
         doMessage(outToClient, clientNumber, "Date: " + date);
         doMessage(outToClient, clientNumber, "Server: " + SERVER_NAME);
         if (headerData.connectionClose)
         {
            doMessage(outToClient, clientNumber, "Connection: close");
         }
         doMessage(outToClient, clientNumber, "Content-Type: text/html");
         doMessage(outToClient, clientNumber, "Content-Length: " + body.length());
         // Add a blank line to denote end of response headers.
         doMessage(outToClient, clientNumber, "");
         outToClient.flush();

         // Send the entity body.
         outToClient.writeBytes(body);
         outToClient.flush();
         //System.out.print(body);
         logMessage(clientNumber, "<=== Entity body sent to Client");
      }
      catch (IOException e)
      {
         logMessage(clientNumber, "===> sendErrorResponse: Unable to write to client.");
         System.out.println( e );
      }
   }


   private void logMessage(String message)
   {
      // Output a time stamped log message.
      final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SS");
      final String formattedTime = LocalDateTime.now().format(formatter);
      System.out.println(formattedTime + ": SERVER: " + message);
   }


   private void logMessage(int clientNumber,
                           String message)
   {
      logMessage("Client " + clientNumber + ": " + message);
   }


   private void doMessage(DataOutputStream outToClient,
                          int clientNumber,
                          String message)
   throws IOException
   {
      logMessage(clientNumber, "<" + message);
      outToClient.writeBytes(message + "\r\n");
   }


   /**
      Use file name extensions to determine the
      content type (mime type) of a resource file.
      See
         https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types
   */
   private String mimeType(final String resourceName)
   {
      final String contentType;

      // Core web technology file types.
      if ( resourceName.endsWith(".html")
        || resourceName.endsWith(".htm") )
         contentType = "text/html";   // Try making this "text/plain".
      else if ( resourceName.endsWith(".css") )
         contentType = "text/css";
      else if ( resourceName.endsWith(".js") )
         contentType = "application/javascript";
      else if ( resourceName.endsWith(".json") )
         contentType = "application/json";

      // Important image file types.
      else if ( resourceName.endsWith(".png") )
         contentType = "image/png";
      else if ( resourceName.endsWith(".jpg")
             || resourceName.endsWith(".jpeg") )
         contentType = "image/jpeg";
      else if ( resourceName.endsWith(".svg") )
         contentType = "image/svg+xml";
      else if ( resourceName.endsWith(".wepb") )
         contentType = "image/wepb";
      else if ( resourceName.endsWith(".gif") )
         contentType = "image/gif";

      // Some document types.
      else if ( resourceName.endsWith(".md") )
         contentType = "text/markdown";
      else if ( resourceName.endsWith(".xml") )
         contentType = "text/xml";
      else if ( resourceName.endsWith(".txt") )
         contentType = "text/plain";
      else if ( resourceName.endsWith(".pdf") )
         contentType = "application/pdf";   // Try making this "text/plain".

      // Compressed file types.
      else if ( resourceName.endsWith(".zip") )
         contentType = "application/zip";

      // A default file type.
      else  // default content type
         contentType = "text/plain";  // or maybe "application/octet-stream"

      return contentType;
   }


   private static class HeaderData
   {
      public boolean connectionClose;
      public boolean connectionKeepAlive;
      public int keepAliveTimeout;
      public int keepAliveMax;
      public int entityLength;
   }
}
