/*
   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.util.List;
import java.util.ArrayList;
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.InputStreamReader;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
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 implements the GET, HEAD, POST, PUT, and DELETE methods.

   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_v3x implements Runnable
{
   public  static final String SERVER_NAME = "HttpServer_v3x.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_v3x(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_v3x(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")
        || requestMethod.equals("HEAD") )
      {
         doGet(requestMethod,
               inFromClient,
               outToClient,
               resourceName,
               queryString,
               headerData,
               clientNumber);
      }
      else if ( requestMethod.equals("OPTIONS") )
      {
         doOptions(inFromClient,
                   outToClient,
                   resourceName,
                   queryString,
                   headerData,
                   clientNumber);
      }
      else if ( requestMethod.equals("POST") )
      {
         doPost(inFromClient,
                outToClient,
                resourceName,
                queryString,
                headerData,
                clientNumber);
      }
      else if ( requestMethod.equals("PUT") )
      {
         doPut(inFromClient,
               outToClient,
               resourceName,
               queryString,
               headerData,
               clientNumber);
      }
      else if ( requestMethod.equals("DELETE") )
      {
         doDelete(inFromClient,
                  outToClient,
                  resourceName,
                  queryString,
                  headerData,
                  clientNumber);
      }
      else if ( requestMethod.equals("PATCH")
             || 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 and dynamic GET requests and also
      implement the HEAD method.

      See
         https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/GET
      and
         https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/HEAD
   */
   public void doGet(String requestMethod,
                     DataInputStream inFromClient,
                     DataOutputStream outToClient,
                     String resourceName,
                     String queryString,
                     HeaderData headerData,
                     int clientNumber)
   {
      logMessage(clientNumber, "===> Do " + requestMethod + " 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 if ( resourceName.endsWith(".class") )
      {
         doJavaCGI(requestMethod, // do the GET (not POST) version of CGI
                   inFromClient,
                   outToClient,
                   resourceName,
                   queryString,
                   headerData,
                   clientNumber,
                   file,
                   null); // no entity body
      }
      else if ( resourceName.endsWith(".php") )
      {
         doPhpCGI(requestMethod, // do the GET (not POST) version of CGI
                  inFromClient,
                  outToClient,
                  resourceName,
                  queryString,
                  headerData,
                  clientNumber,
                  file,
                  null); // no entity body
      }
      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 " + requestMethod + " 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.
         if ( requestMethod.toUpperCase().equals("GET") ) // not HEAD
         {
            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.
         if ( requestMethod.equals("GET") ) // not HEAD
         {
            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 );
      }
   }


   /**
      Dispatch the different kinds of POST requests.

      See
         https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/POST
   */
   public void doPost(DataInputStream inFromClient,
                      DataOutputStream outToClient,
                      String resourceName,
                      String queryString,
                      HeaderData headerData,
                      int clientNumber)
   {
      logMessage(clientNumber, "===> Do POST 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() )
      {
         logMessage(clientNumber, "===> Cannot execute a directory error response");

         sendErrorResponse(outToClient,
             "405", "Method Not Allowed",
             "The POST method applied to a directory<br>" + resourceName +
                   "<br>is not allowed.",
             headerData,
             clientNumber);

         return;  // Finished with this request.
      }

      // Read the entity body.
      final int entityLength = headerData.entityLength;
      final byte[] entityBodyBytes = new byte[entityLength];
      int totalBytesRcvd = 0;  // Total bytes received so far.
      int bytesRcvd;           // bytes received in last read.
      logMessage(clientNumber, "===> doPost: Reading entity body");
      while ( totalBytesRcvd < entityLength )
      {
         try
         {
            bytesRcvd = inFromClient.read(entityBodyBytes,
                                          totalBytesRcvd,
                                          entityLength - totalBytesRcvd);
            if (-1 == bytesRcvd)
            {
               break; // reached end-of-file
            }
            totalBytesRcvd += bytesRcvd;
            logMessage(clientNumber, "===> doPost: bytes received so far = " + totalBytesRcvd);
         }
         catch (IOException e)
         {
            logMessage(clientNumber, "===> doPost: Unable to read entity body.");
            System.out.println( e );
         }
      }
      logMessage(clientNumber, "===> doPost: Total bytes received = " + totalBytesRcvd);

      if ( resourceName.endsWith(".class") )
      {
         doJavaCGI("POST", // Do the POST (not GET) version of CGI.
                   inFromClient,
                   outToClient,
                   resourceName,
                   queryString,
                   headerData,
                   clientNumber,
                   file,
                   entityBodyBytes);
      }
      else if ( resourceName.endsWith(".php") )
      {
         doPhpCGI("POST", // Do the POST (not GET) version of CGI.
                  inFromClient,
                  outToClient,
                  resourceName,
                  queryString,
                  headerData,
                  clientNumber,
                  file,
                  entityBodyBytes);
      }
      else if ( resourceName.equals("/post_data") )
      {
         doBuiltInPost(inFromClient,
                       outToClient,
                       resourceName,
                       queryString,
                       headerData,
                       clientNumber,
                       file,
                       entityBodyBytes);
      }
      else
      {
         logMessage(clientNumber, "===> Unsupported POST error response");

         sendErrorResponse(outToClient,
             "501", "Not Implemented",
             "This server does not understand this kind of POST method: " + resourceName,
             headerData,
             clientNumber);
      }
   }


   /**
      Implement the CGI protocol for inter-process
      communication between the server process, its
      child process, and the client.

      The CGI data can come from either a POST method (and its
      entity body) or a GET method (and its query string).

      This method only works with Java class files
      (not with .exe or .php executable files).
   */
   public void doJavaCGI(String requestMethod,
                         DataInputStream inFromClient,
                         DataOutputStream outToClient,
                         String resourceName,
                         String queryString,
                         HeaderData headerData,
                         int clientNumber,
                         File file,
                         byte[] entityBodyBytes)
   {
      // Log the CGI data to stdout.
      if (requestMethod.toUpperCase().equals("POST"))
      {
         logMessage(clientNumber, "===> Do Java CGI POST");
         final String entityBody = new String(entityBodyBytes);
         logMessage(clientNumber, "===> Entity body = " + entityBody);
      }
      else // do GET or HEAD
      {
         logMessage(clientNumber, "===> Do Java CGI " + requestMethod);
         logMessage(clientNumber, "===> Query string = " + queryString);
      }

      // We need to launch the Java program called resourceName.
      // Get the path of the java.exe program that started this server's JVM.
      final String javaPath = ProcessHandle.current()
                                           .info()
                                           .command()
                                           .orElse(null);
      if(javaPath == null)
      {
         logMessage(clientNumber, "Could not get the path to java.exe.");
         return;
      }
      logMessage(clientNumber, "===> Using this JVM for CGI: " + javaPath);

      // Configure the ProcessBuilder.
      try
      {
         final String resourceClass = resourceName
                                     .substring(0, resourceName.lastIndexOf("."));
         final String classFile = new File(serverRoot + resourceClass)
                                      .getCanonicalFile()
                                      .getAbsolutePath();
         logMessage(clientNumber, "===> Running this class file: " + classFile);

         final ProcessBuilder pb = new ProcessBuilder(
                                         javaPath,
                                         resourceClass.substring(1));
         logMessage(clientNumber, "===>" + pb.command() );

         // Start a new java process in the serverRoot directory.
         // We are assuming that the Java class files are written as
         // packages with the package folder stored in serverRoot.
         pb.directory(new File(serverRoot));
         final Process p = pb.start();
         logMessage(clientNumber, "===> Launched this child process: " + p);
         // Write the client's data to the input stream of the java child process.
         try (final var out = new BufferedOutputStream(p.getOutputStream()))
         {
            if (requestMethod.toUpperCase().equals("POST"))
            {
               out.write(entityBodyBytes);
            }
            else // do GET or HEAD
            {
               if (null == queryString) queryString = "";
               out.write(queryString.getBytes("UTF-8"));
            }
            out.flush();
         }
         logMessage(clientNumber, "===> Server wrote data to java child process");

         // Read the standard output stream of the java child process
         // and forward the results to the client.
         logMessage(clientNumber, "===> Server reading java child process's response for client");
         byte[] javaBytes = {};
         try (final var in = new BufferedInputStream(p.getInputStream()))
         {
            final List<Byte> javaResponse = new ArrayList<>();
            int oneByte; // must be int for eof value
            while ( (oneByte = in.read()) != -1 ) // while not eof
            {
               javaResponse.add((byte)oneByte);
            }
            javaBytes = new byte[javaResponse.size()];
            for (int i = 0; i < javaBytes.length; ++i)
               javaBytes[i] = javaResponse.get(i);
            //System.out.println(java.util.Arrays.toString(javaBytes));
         }
         catch (IOException e)
         {
            logMessage(clientNumber, "===> do Java CGI: Error reading child java process response.");
            System.out.println( e );
         }
         logMessage(clientNumber, "===> Server done reading response from java child process");

         // Read the standard error stream of the java child process.
         logMessage(clientNumber, "===> Server reading error stream from java child process");
         try (final var err = new BufferedReader(
                                 new InputStreamReader(
                                    p.getErrorStream())))
         {
            String oneLine;
            while ((oneLine = err.readLine()) != null) // read up to end-of-stream
            {
               logMessage(clientNumber, "err: " + oneLine);
            }
         }
         logMessage(clientNumber, "===> Server done reading error stream from java child process");

         try
         {
            // Send the HTTP response line and headers to the client.
            // The response line.
            logMessage(clientNumber, "===> Server response to client");
            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, "Access-Control-Allow-Origin: *");
            doMessage(outToClient, clientNumber, "Content-Type: text/html");
            doMessage(outToClient, clientNumber, "Content-Length: " + javaBytes.length);
            // Add a blank line to denote the end of response headers.
            doMessage(outToClient, clientNumber, "");
            outToClient.flush();

            // Send the entity body created by the java child process.
            if (! requestMethod.toUpperCase().equals("HEAD"))
            {
               logMessage(clientNumber, "===> Server sending java child process's response to client");
               outToClient.write(javaBytes);
               outToClient.flush();
               //System.out.print(new String(javaBytes)); // assumes the response is text
               logMessage(clientNumber, "====> Server done sending java child process's response to client");
            }
         }
         catch (IOException e)
         {
            logMessage(clientNumber, "===> do Java CGI: Unable to write to client.");
            System.out.println( e );
         }
      }
      catch (IOException e)
      {
         logMessage(clientNumber, "Error while running " + resourceName);
         System.out.println( e );
      }
   }


   /**
      Implement the CGI protocol for inter-process
      communication between the server process, its
      child process, and the client.

      The CGI data can come from either a POST method (and its
      entity body) or a GET method (and its query string).

      This method only works with PHP files.
   */
   public void doPhpCGI(String requestMethod,
                        DataInputStream inFromClient,
                        DataOutputStream outToClient,
                        String resourceName,
                        String queryString,
                        HeaderData headerData,
                        int clientNumber,
                        File file,
                        byte[] entityBodyBytes)
   {
      // Log the CGI data to stdout.
      if (requestMethod.toUpperCase().equals("POST"))
      {
         logMessage(clientNumber, "===> Do PHP CGI POST");
         final String entityBody = new String(entityBodyBytes);
         logMessage(clientNumber, "===> Entity body = " + entityBody);
      }
      else // do GET or HEAD
      {
         logMessage(clientNumber, "===> Do PHP CGI " + requestMethod);
         logMessage(clientNumber, "===> Query string = " + queryString);
      }

      // We need to launch the php.exe program called resourceName.
      final String phpPath = ".\\php.exe";
      final File phpExe = new File(phpPath);
      if ( ! phpExe.exists() || ! phpExe.canRead() )
      {
         logMessage(clientNumber, "Could not open the path to php.exe.");

         sendErrorResponse(outToClient,
             "404", "Not Found",
             "The PHP interpreter was not found on this server.",
             headerData,
             clientNumber);

         return;  // Finished with this request.
      }
      logMessage(clientNumber, "===> Using this php for CGI: " + phpPath);

      // Configure the ProcessBuilder.
      try
      {
         final String phpFile = new File(serverRoot + resourceName)
                                    .getCanonicalFile()
                                    .getAbsolutePath();
         logMessage(clientNumber, "===> Running this php file: " + phpFile);

         final ProcessBuilder pb = new ProcessBuilder(
                                         phpPath,
                                         "-f",
                                         phpFile);
         logMessage(clientNumber, "===>" + pb.command() );

         // Start a new PHP process.
         final Process p = pb.start();
         logMessage(clientNumber, "===> Launched this child process: " + p);
         // Write the client's data to the input stream of the php child process.
         try (final var out = new BufferedOutputStream(p.getOutputStream()))
         {
            if (requestMethod.toUpperCase().equals("POST"))
            {
               out.write(entityBodyBytes);
            }
            else // do GET or HEAD
            {
               if (null == queryString) queryString = "";
               out.write(queryString.getBytes("UTF-8"));
            }
            out.flush();
         }
         logMessage(clientNumber, "===> Server wrote data to php child process");

         // Read the standard output stream of the php child process
         // and forward the results to the client.
         logMessage(clientNumber, "===> Server reading php child process's response for client");
         byte[] phpBytes = {};
         try (final var in = new BufferedInputStream(p.getInputStream()))
         {
            final List<Byte> phpResponse = new ArrayList<>();
            int oneByte; // must be int for eof value
            while ( (oneByte = in.read()) != -1 ) // while not eof
            {
               phpResponse.add((byte)oneByte);
            }
            phpBytes = new byte[phpResponse.size()];
            for (int i = 0; i < phpBytes.length; ++i)
               phpBytes[i] = phpResponse.get(i);
            //System.out.println(java.util.Arrays.toString(phpBytes));
         }
         catch (IOException e)
         {
            logMessage(clientNumber, "===> do PHP CGI: Error reading child php process response.");
            System.out.println( e );
         }
         logMessage(clientNumber, "===> Server done reading response from php child process");

         // Read the standard error stream of the php child process.
         logMessage(clientNumber, "===> Server reading error stream from php child process");
         try (final var err = new BufferedReader(
                                 new InputStreamReader(
                                    p.getErrorStream())))
         {
            String oneLine;
            while ((oneLine = err.readLine()) != null) // read up to end-of-stream
            {
               logMessage(clientNumber, "err: " + oneLine);
            }
         }
         logMessage(clientNumber, "===> Server done reading error stream from php child process");

         try
         {
            // Send the HTTP response line and headers to the client.
            // The response line.
            logMessage(clientNumber, "===> Server response to client");
            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, "Access-Control-Allow-Origin: *");
            doMessage(outToClient, clientNumber, "Content-Type: text/html");
            doMessage(outToClient, clientNumber, "Content-Length: " + phpBytes.length);
            // Add a blank line to denote the end of response headers.
            doMessage(outToClient, clientNumber, "");
            outToClient.flush();

            // Send the entity body created by the php child process.
            if (! requestMethod.toUpperCase().equals("HEAD"))
            {
               logMessage(clientNumber, "===> Server sending php child process's response to client");
               outToClient.write(phpBytes);
               outToClient.flush();
               //System.out.print(new String(phpBytes)); // assumes the response is text
               logMessage(clientNumber, "====> Server done sending php child process's response to client");
            }
         }
         catch (IOException e)
         {
            logMessage(clientNumber, "===> do PHP CGI: Unable to write to client.");
            System.out.println( e );
         }
      }
      catch (IOException e)
      {
         logMessage(clientNumber, "Error while running " + resourceName);
         System.out.println( e );
      }
   }


   /**
      Implement a simple built-in POST protocol.

      This method logs the entity body to the server's stdout
      and then sends back to the client a dynamically generated
      "thank you" web page which includes a copy of the client's
      POST data.

      In general, a method like this can do any amount of
      computation using the client's POST data as the input
      to the calculations. The returned web page can then
      depend on those calculations.
   */
   public void doBuiltInPost(DataInputStream inFromClient,
                             DataOutputStream outToClient,
                             String resourceName,
                             String queryString,
                             HeaderData headerData,
                             int clientNumber,
                             File file,
                             byte[] entityBodyBytes)
   {
      logMessage(clientNumber, "===> Do server's internal POST method");

      // Log the entity body to stdout.
      final String entityBody = new String(entityBodyBytes);
      logMessage(clientNumber, "===> " + entityBody);

      // Build the HTTP response body.
      final String body = "<!doctype html>\r\n" +
                          "<html>\r\n" +
                          "<head>\r\n" +
                          "<title>Thank you!</title>\r\n" +
                          "</head>\r\n" +
                          "<body>\r\n" +
                          "<h1>Thank you!</h1>\r\n" +
                          "<p>Thank you for your data!</p>\r\n" +
                          "<p></p>\r\n" +
                          "<p style=\"" +
                                "display:inline-block;" +
                                "padding:5px;" +
                                "border-style:solid;" +
                                "border-color:black;" +
                                "border-width:1px;" +
                                "margin:10px 0px;" +
                                "background-color:#eee;" +
                          "\">\r\n" +
                          "\"" + entityBody + "\"\r\n" +
                          "</p>\r\n" +
                          "<p></p>\r\n" +
                          "<p>We will protect it forever.</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 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, "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, "===> do builtin Post: Unable to write to client.");
         System.out.println( e );
      }
   }


   /**
      Implement the HTTP PUT command.

      This implementation only puts files into the sub-folder
      called "files" in the server's root directory.

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

      // Read the entity body.
      final int entityLength = headerData.entityLength;
      final byte[] entityBodyBytes = new byte[entityLength];
      int totalBytesRcvd = 0;  // Total bytes received so far.
      int bytesRcvd;           // bytes received in last read.
      logMessage(clientNumber, "===> doPut: Reading entity body");
      while ( totalBytesRcvd < entityLength )
      {
         try
         {
            // We must handle "short reads".
            // https://www.club.cc.cmu.edu/~cmccabe/blog_short_io.html
            // https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/DataInputStream.html#read(byte%5B%5D,int,int)
            bytesRcvd = inFromClient.read(entityBodyBytes,
                                          totalBytesRcvd,
                                          entityLength - totalBytesRcvd);
            if (-1 == bytesRcvd)
            {
               break; // reached end-of-file
            }
            totalBytesRcvd += bytesRcvd;
            logMessage(clientNumber, "===> doPut: bytes received so far = " + totalBytesRcvd);
         }
         catch (IOException e)
         {
            logMessage(clientNumber, "===> doPut: Unable to read entity body.");
            System.out.println( e );
         }
      }
      logMessage(clientNumber, "===> doPut: Total bytes received = " + totalBytesRcvd);

      // Store the entity body in the server's file system.
      final File file = new File(serverRoot + "/files/" + resourceName);

      // NOTE: We should check if the file already exists and
      // if so, send a response code of 204 (instead of 201).

      logMessage(clientNumber, "===> doPut: about to write file " +  file);

      boolean uploadSuceeded = true;
      try (final var fileOut = new BufferedOutputStream(
                                  new FileOutputStream(file)))
      {
         // https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/BufferedOutputStream.html#write(byte%5B%5D,int,int)
         fileOut.write(entityBodyBytes);
         fileOut.flush();
         //logMessage(clientNumber, "\n" + new String(entityBodyBytes)); // assumes entity body is text
         logMessage(clientNumber, "===> doPut: done writing file " +  file);
      }
      catch (FileNotFoundException e)
      {
         logMessage(clientNumber, "===> doPut: Unable to write destination file.");
         System.out.println( e );
         uploadSuceeded = false;
      }
      catch (IOException e)
      {
         logMessage(clientNumber, "===> doPut: Unable to write destination file.");
         System.out.println( e );
         uploadSuceeded = false;
      }

      try
      {
         // Send the HTTP response line and headers.
         // The response line.
         if (uploadSuceeded)
            doMessage(outToClient, clientNumber, "HTTP/1.1 201 Created");
         else
            doMessage(outToClient, clientNumber, "HTTP/1.1 409 Conflict");
         // 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");
         if (uploadSuceeded)
            doMessage(outToClient, clientNumber, "Content-Location: " + resourceName);
         // Add a blank line to denote end of response headers.
         doMessage(outToClient, clientNumber, "");
         outToClient.flush();
      }
      catch (IOException e)
      {
         logMessage(clientNumber, "===> doPut: Unable to write to client.");
         System.out.println( e );
      }
   }


   /**
      Implement the HTTP DELETE command.

      This implementation only deletes files from the sub-folder
      called "files" in the server's root directory.

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

      // Delete the resource from the server's file system.
      final String fileName = serverRoot + "/files/" + resourceName;
      final File file = new File(fileName);

      logMessage(clientNumber, "===> doDelete: about to delete file " + fileName);

      final boolean deleted = file.delete();

      if (deleted)
      {
         logMessage(clientNumber, "===> doDelete: done deleting file " + fileName);
      }
      else
      {
         logMessage(clientNumber, "===> doDelete: could not delete " + fileName);
      }

      try
      {
         // Send the HTTP response line and headers.
         // The response line.
         if (deleted)
            doMessage(outToClient, clientNumber, "HTTP/1.1 204 No Content");
         else
            doMessage(outToClient, clientNumber, "HTTP/1.1 404 Not Found");
         // 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");
         // Add a blank line to denote end of response headers.
         doMessage(outToClient, clientNumber, "");
         outToClient.flush();
      }
      catch (IOException e)
      {
         logMessage(clientNumber, "===> doDelete: 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,HEAD,POST,PUT,DELETE,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";
      else if ( resourceName.endsWith(".php") )
         contentType = "application/x-httpd-php";

      // 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;
   }
}
