# LegacyLogWriteExecutor.java

    /*
     ** Module   : LegacyLogWriteExecutor.java
     ** Abstract : Implementation for synchronization of LOG-MANAGER instances when writing to one log file.
     **
     ** Copyright (c) 2014-2023, Golden Code Development Corporation.
     **
     ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
     ** 001 GBB 20230110 Initial implementation of LegacyLogWriteExecutor to enable synchronized writing to the
     **                  same file.
     ** 002 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
     */
    /*
     ** This program is free software: you can redistribute it and/or modify
     ** it under the terms of the GNU Affero General Public License as
     ** published by the Free Software Foundation, either version 3 of the
     ** License, or (at your option) any later version.
     **
     ** This program is distributed in the hope that it will be useful,
     ** but WITHOUT ANY WARRANTY; without even the implied warranty of
     ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     ** GNU Affero General Public License for more details.
     **
     ** You may find a copy of the GNU Affero GPL version 3 at the following
     ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
     **
     ** Additional terms under GNU Affero GPL version 3 section 7:
     **
     **   Under Section 7 of the GNU Affero GPL version 3, the following additional
     **   terms apply to the works covered under the License.  These additional terms
     **   are non-permissive additional terms allowed under Section 7 of the GNU
     **   Affero GPL version 3 and may not be removed by you.
     **
     **   0. Attribution Requirement.
     **
     **     You must preserve all legal notices or author attributions in the covered
     **     work or Appropriate Legal Notices displayed by works containing the covered
     **     work.  You may not remove from the covered work any author or developer
     **     credit already included within the covered work.
     **
     **   1. No License To Use Trademarks.
     **
     **     This license does not grant any license or rights to use the trademarks
     **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
     **     of Golden Code Development Corporation. You are not authorized to use the
     **     name Golden Code, FWD, or the names of any author or contributor, for
     **     publicity purposes without written authorization.
     **
     **   2. No Misrepresentation of Affiliation.
     **
     **     You may not represent yourself as Golden Code Development Corporation or FWD.
     **
     **     You may not represent yourself for publicity purposes as associated with
     **     Golden Code Development Corporation, FWD, or any author or contributor to
     **     the covered work, without written authorization.
     **
     **   3. No Misrepresentation of Source or Origin.
     **
     **     You may not represent the covered work as solely your work.  All modified
     **     versions of the covered work must be marked in a reasonable way to make it
     **     clear that the modified work is not originating from Golden Code Development
     **     Corporation or FWD.  All modified versions must contain the notices of
     **     attribution required in this license.
     */

    package com.goldencode.p2j.util;

    import com.goldencode.p2j.util.logging.*;

    import java.io.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.*;
    import java.util.logging.*;

    /** A single point of executing legacy LOG-MANAGER file write actions. */
    public class LegacyLogWriteExecutor
    {
       /** A temporary logger to just write messages to server's log. */
       private static final CentralLogger LOG = CentralLogger.get(LegacyLogWriteExecutor.class);

       /** Initialization on demand holder for LegacyLogManagerQueue singleton initialization. */
       private static class LegacyLogManagerQueueHolder
       {
          /** Instance of LegacyLogManagerQueue. */
          private static final LegacyLogWriteExecutor INSTANCE = new LegacyLogWriteExecutor();
       }

       /** Thread factory for the thread executor providing threads for writing to log files with rotation. */
       private static final ThreadFactory THREAD_FACTORY_ROTATION =  new ExecutorsThreadFactory(true);

       /** Thread factory for the thread executor providing threads for writing to log files without rotation. */
       private static final ThreadFactory THREAD_FACTORY_NO_ROTATION = new ExecutorsThreadFactory(false);

       /** Mutex for rotation related actions. */
       private final Object rotationMutex = new Object();

       /** Map with absolute log file path : client pid pairs for clients that need file rotation. */
       private final Map<String, Long> rotationPathToPidMap = new HashMap<>();

       /** Map with absolute log file path : executor service pairs for clients that need file rotation. */
       private final Map<String, ExecutorService> rotationPathToExecutorServicedMap = new HashMap<>();

       /** Mutex for no rotation related actions. */
       private final Object noRotationMutex = new Object();

       /** Map with absolute log file path : client pids pairs for clients that don't need file rotation. */
       private final Map<String, Set<Long>> noRotationPathToPidMap = new HashMap<>();

       /** Map with absolute log file path : executor service pairs for clients that don't need file rotation. */
       private final Map<String, ExecutorService> noRotationPathToExecutorServicedMap = new HashMap<>();

       /**
        * Getter for LegacyLogManagerQueue singleton instance.
        *
        * @return   instance of LegacyLogManagerQueue.
        */
       public static LegacyLogWriteExecutor get()
       {
          return LegacyLogManagerQueueHolder.INSTANCE;
       }

       /** Private constructor for LegacyLogManagerQueue. */
       private LegacyLogWriteExecutor()
       {
       }

       /**
        * Closes the ExecutorService executing write tasks for the file path if no other pids are using it.
        *
        * @param    pid
        *           The process id of the client writing to the file.
        * @param    path
        *           Absolute path to the file to write to.
        * @param    isRotation
        *           Flag to indicate the file gets rotated.
        *
        * @return   Always returns <code>true</code>.
        */
       public synchronized boolean close(long pid, String path, boolean isRotation)
       {
          if (isRotation)
          {
             synchronized(rotationMutex)
             {
                rotationPathToPidMap.remove(path);
                ExecutorService es = rotationPathToExecutorServicedMap.remove(path);
                if (es != null)
                {
                   es.shutdown();
                }
             }
          }
          else
          {
             synchronized(noRotationMutex)
             {
                Set<Long> pids = noRotationPathToPidMap.get(path);
                if (pids == null) {
                   return true;
                }
                pids.remove(pid);
                if (pids.isEmpty())
                {
                   ExecutorService es = noRotationPathToExecutorServicedMap.remove(path);
                   if (es != null)
                   {
                      es.shutdown();
                   }
                }
             }
          }

          return true;
       }

       /**
        * Each unique path has a dedicated thread and a queue (newSingleThreadExecutor). In the case of
        * rotation, only one client / pid will be able to add to the queue. In the case of no rotation,
        * multiple clients / pids can write to the same path by adding tasks to the same queue.
        *
        * @param    pid
        *           The process id of the client writing to the file.
        * @param    path
        *           Absolute path to the file to write to.
        * @param    isRotation
        *           Flag to indicate the file gets rotated.
        * @param    writer
        *           The prepared FileWriter for writing to the file.
        * @param    msg
        *           The log message.
        *
        * @return   Empty Optional when the process is not allowed to write to the file (in case of rotation
        *           and another process already writing). Otherwise the Optional contains Future for the write
        *           task execution that returns a boolean.
        */
       public Optional<Future<Boolean>> write(long pid,
                                              String path,
                                              boolean isRotation,
                                              FileWriter writer,
                                              String msg)
       {
          if (isRotation)
          {
             synchronized(rotationMutex)
             {
                rotationPathToPidMap.putIfAbsent(path, pid);
                if (pid == rotationPathToPidMap.get(path))
                {
                   rotationPathToExecutorServicedMap.putIfAbsent(path,
                                                                  Executors.newSingleThreadExecutor(THREAD_FACTORY_ROTATION));
                   ExecutorService es = rotationPathToExecutorServicedMap.get(path);
                   return Optional.of(write(es, writer, msg));
                }
             }
             // only one process can write to the same path
             return Optional.empty();
          }

          synchronized(noRotationMutex)
          {
             noRotationPathToPidMap.putIfAbsent(path, new HashSet<>());
             noRotationPathToPidMap.get(path).add(pid);

             noRotationPathToExecutorServicedMap.putIfAbsent(path,
                                                              Executors.newSingleThreadExecutor(THREAD_FACTORY_NO_ROTATION));
             ExecutorService es = noRotationPathToExecutorServicedMap.get(path);
             return Optional.of(write(es, writer, msg));
          }
       }

       /**
        * Creates a new WriteCallable and submits it to the ExecutorService.
        *
        * @param    es
        *           The executor service to execute the WriteCallable.
        * @param    writer
        *           The prepared FileWriter for writing to the file.
        * @param    msg
        *           The log message.
        *
        * @return   Future for the write task execution that returns a boolean.
        */
       private Future<Boolean> write(ExecutorService es, FileWriter writer, String msg)
       {
          return es.submit(new WriteCallable(writer, msg));
       }

       /**
        * Private static class ExecutorsThreadFactory to provide meaningful names for threads used by the
        * executors.
        */
       private static class ExecutorsThreadFactory implements ThreadFactory
       {
          /** An identifier for the type of thread. */
          private final String rotationFix;

          /** Counter for threads created by the factory to provide unique identification. */
          private final AtomicInteger count = new AtomicInteger(1);

          /** Package private constructor for ExecutorsThreadFactory */
          ExecutorsThreadFactory(boolean hasRotation)
          {
             rotationFix = hasRotation ? "-Rotation-" : "-NoRotation-";
          }

          /**
           * Creates a new thread for the provided runnable and assigns a name to it that identifies it uniquely.
           *
           * @param    runnable
           *           The task to be executed by the thread.
           *
           * @return   The new thread.
           */
          @Override
          public Thread newThread(Runnable runnable)
          {
             Thread newThread = new Thread(runnable);
             newThread.setName(LegacyLogWriteExecutor.class.getSimpleName() + rotationFix + count.getAndIncrement());
             return newThread;
          }
       }

       /** Private static class WriteCallable that extends Callable<Boolean>. */
       private static class WriteCallable
          implements Callable<Boolean>
       {
          /** The prepared FileWriter for writing to the file. */
          private final FileWriter writer;

          /** The log message. */
          private final String msg;

          /** Package-private constructor for WriteCallable. */
          WriteCallable(FileWriter writer, String msg)
          {
             this.writer = writer;
             this.msg = msg;
          }

          /**
           * Writes the message using the FileWriter.
           *
           * @return   <code>true</code> if the write was successful, <code>false</code> otherwise.
           */
          public Boolean call()
          {
             try
             {
                writer.write(msg);
                writer.flush();
                return Boolean.TRUE;
             }
             catch (Exception e)
             {
                LOG.log(Level.FINE, "Writing to the LOG-MANAGER log file failed for msg: " + msg, e);
                return Boolean.FALSE;
             }
          }
       }
    }