All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.dao.simple.AbstractSimpleDAO Maven / Gradle / Ivy

There is a newer version: 11.1.10
Show newest version
/*
 * Copyright (C) 2014-2024 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.dao.simple;

import java.io.File;
import java.io.OutputStream;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Locale;
import java.util.function.Supplier;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.ELockType;
import com.helger.commons.annotation.MustBeLocked;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.datetime.PDTFactory;
import com.helger.commons.datetime.PDTToString;
import com.helger.commons.io.file.FileHelper;
import com.helger.commons.io.file.FileIOError;
import com.helger.commons.io.file.FileOperationManager;
import com.helger.commons.io.relative.IFileRelativeIO;
import com.helger.commons.io.resource.FileSystemResource;
import com.helger.commons.io.resource.IReadableResource;
import com.helger.commons.state.EChange;
import com.helger.commons.state.ESuccess;
import com.helger.commons.statistics.IMutableStatisticsHandlerCounter;
import com.helger.commons.statistics.IMutableStatisticsHandlerTimer;
import com.helger.commons.statistics.StatisticsManager;
import com.helger.commons.string.ToStringGenerator;
import com.helger.commons.timing.StopWatch;
import com.helger.dao.AbstractDAO;
import com.helger.dao.DAOException;
import com.helger.xml.microdom.IMicroComment;
import com.helger.xml.microdom.IMicroDocument;
import com.helger.xml.microdom.IMicroElement;
import com.helger.xml.microdom.MicroComment;
import com.helger.xml.microdom.serialize.MicroReader;
import com.helger.xml.microdom.serialize.MicroWriter;
import com.helger.xml.serialize.write.IXMLWriterSettings;
import com.helger.xml.serialize.write.XMLWriterSettings;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * Base class for a simple DAO.
 *
 * @author Philip Helger
 */
@ThreadSafe
public abstract class AbstractSimpleDAO extends AbstractDAO
{
  private static final Logger LOGGER = LoggerFactory.getLogger (AbstractSimpleDAO.class);

  private final IMutableStatisticsHandlerCounter m_aStatsCounterInitTotal = StatisticsManager.getCounterHandler (getClass ().getName () +
                                                                                                                 "$init-total");
  private final IMutableStatisticsHandlerCounter m_aStatsCounterInitSuccess = StatisticsManager.getCounterHandler (getClass ().getName () +
                                                                                                                   "$init-success");
  private final IMutableStatisticsHandlerTimer m_aStatsCounterInitTimer = StatisticsManager.getTimerHandler (getClass ().getName () +
                                                                                                             "$init");
  private final IMutableStatisticsHandlerCounter m_aStatsCounterReadTotal = StatisticsManager.getCounterHandler (getClass ().getName () +
                                                                                                                 "$read-total");
  private final IMutableStatisticsHandlerCounter m_aStatsCounterReadSuccess = StatisticsManager.getCounterHandler (getClass ().getName () +
                                                                                                                   "$read-success");
  private final IMutableStatisticsHandlerTimer m_aStatsCounterReadTimer = StatisticsManager.getTimerHandler (getClass ().getName () +
                                                                                                             "$read");
  private final IMutableStatisticsHandlerCounter m_aStatsCounterWriteTotal = StatisticsManager.getCounterHandler (getClass ().getName () +
                                                                                                                  "$write-total");
  private final IMutableStatisticsHandlerCounter m_aStatsCounterWriteSuccess = StatisticsManager.getCounterHandler (getClass ().getName () +
                                                                                                                    "$write-success");
  private final IMutableStatisticsHandlerCounter m_aStatsCounterWriteExceptions = StatisticsManager.getCounterHandler (getClass ().getName () +
                                                                                                                       "$write-exceptions");
  private final IMutableStatisticsHandlerTimer m_aStatsCounterWriteTimer = StatisticsManager.getTimerHandler (getClass ().getName () +
                                                                                                              "$write");

  private final IFileRelativeIO m_aIO;
  private final Supplier  m_aFilenameProvider;
  private String m_sPreviousFilename;
  private int m_nInitCount = 0;
  private LocalDateTime m_aLastInitDT;
  private int m_nReadCount = 0;
  private LocalDateTime m_aLastReadDT;
  private int m_nWriteCount = 0;
  private LocalDateTime m_aLastWriteDT;

  protected AbstractSimpleDAO (@Nonnull final IFileRelativeIO aIO, @Nonnull final Supplier  aFilenameProvider)
  {
    m_aIO = ValueEnforcer.notNull (aIO, "IO");
    m_aFilenameProvider = ValueEnforcer.notNull (aFilenameProvider, "FilenameProvider");
  }

  /**
   * @return The file-relative IO as passed in the constructor. Never
   *         null.
   */
  @Nonnull
  protected final IFileRelativeIO getIO ()
  {
    return m_aIO;
  }

  /**
   * @return The filename provider used internally to build filenames. Never
   *         null.
   */
  @Nonnull
  public final Supplier  getFilenameProvider ()
  {
    return m_aFilenameProvider;
  }

  /**
   * Custom initialization routine. Called only if the underlying file does not
   * exist yet. This method is only called within a write lock!
   *
   * @return {@link EChange#CHANGED} if something was modified inside this
   *         method
   */
  @Nonnull
  @OverrideOnDemand
  protected EChange onInit ()
  {
    return EChange.UNCHANGED;
  }

  /**
   * Fill the internal structures with from the passed XML document. This method
   * is only called within a write lock!
   *
   * @param aDoc
   *        The XML document to read from. Never null.
   * @return {@link EChange#CHANGED} if reading the data changed something in
   *         the internal structures that requires a writing.
   */
  @Nonnull
  @MustBeLocked (ELockType.WRITE)
  protected abstract EChange onRead (@Nonnull IMicroDocument aDoc);

  @Nonnull
  protected final File getSafeFile (@Nonnull final String sFilename, @Nonnull final EMode eMode) throws DAOException
  {
    ValueEnforcer.notNull (sFilename, "Filename");
    ValueEnforcer.notNull (eMode, "Mode");

    final File aFile = m_aIO.getFile (sFilename);
    if (aFile.exists ())
    {
      // file exist -> must be a file!
      if (!aFile.isFile ())
        throw new DAOException ("The passed filename '" +
                                sFilename +
                                "' is not a file - maybe a directory? Path is '" +
                                aFile.getAbsolutePath () +
                                "'");

      switch (eMode)
      {
        case READ:
          // Check for read-rights
          if (!aFile.canRead ())
            throw new DAOException ("The DAO of class " +
                                    getClass ().getName () +
                                    " has no access rights to read from '" +
                                    aFile.getAbsolutePath () +
                                    "'");
          break;
        case WRITE:
          // Check for write-rights
          if (!aFile.canWrite ())
            throw new DAOException ("The DAO of class " +
                                    getClass ().getName () +
                                    " has no access rights to write to '" +
                                    aFile.getAbsolutePath () +
                                    "'");
          break;
      }
    }
    else
    {
      // Ensure the parent directory is present
      final File aParentDir = aFile.getParentFile ();
      if (aParentDir != null)
      {
        final FileIOError aError = FileOperationManager.INSTANCE.createDirRecursiveIfNotExisting (aParentDir);
        if (aError.isFailure ())
          throw new DAOException ("The DAO of class " +
                                  getClass ().getName () +
                                  " failed to create parent directory '" +
                                  aParentDir +
                                  "': " +
                                  aError);
      }
    }
    return aFile;
  }

  /**
   * Trigger the registered custom exception handlers for read errors.
   *
   * @param t
   *        Thrown exception. Never null.
   * @param bIsInitialization
   *        true if this happened during initialization of a new
   *        file, false if it happened during regular reading.
   * @param aFile
   *        The file that was read. May be null for in-memory DAOs.
   */
  protected static void triggerExceptionHandlersRead (@Nonnull final Throwable t,
                                                      final boolean bIsInitialization,
                                                      @Nullable final File aFile)
  {
    // Custom exception handler for reading
    if (exceptionHandlersRead ().isNotEmpty ())
    {
      final IReadableResource aRes = aFile == null ? null : new FileSystemResource (aFile);
      exceptionHandlersRead ().forEach (aCB -> aCB.onDAOReadException (t, bIsInitialization, aRes));
    }
  }

  /**
   * Call this method inside the constructor to read the file contents directly.
   * This method is write locked!
   *
   * @throws DAOException
   *         in case initialization or reading failed!
   */
  @MustBeLocked (ELockType.WRITE)
  protected final void initialRead () throws DAOException
  {
    File aFile = null;
    final String sFilename = m_aFilenameProvider.get ();
    if (sFilename == null)
    {
      // this branch is required for testing
      CONDLOG.info ( () -> "This DAO of class " + getClass ().getName () + " will not be able to read from a file");

      // do not return - run initialization anyway
    }
    else
    {
      // Check consistency
      aFile = getSafeFile (sFilename, EMode.READ);
    }
    final File aFinalFile = aFile;
    m_aRWLock.writeLockedThrowing ( () -> {
      final boolean bIsInitialization = aFinalFile == null || !aFinalFile.exists ();
      try
      {
        ESuccess eWriteSuccess = ESuccess.SUCCESS;
        if (bIsInitialization)
        {
          // initial setup for non-existing file
          CONDLOG.info ( () -> "Trying to initialize DAO XML file '" + aFinalFile + "'");

          beginWithoutAutoSave ();
          try
          {
            m_aStatsCounterInitTotal.increment ();
            final StopWatch aSW = StopWatch.createdStarted ();

            if (onInit ().isChanged ())
              if (aFinalFile != null)
                eWriteSuccess = _writeToFile ();

            m_aStatsCounterInitTimer.addTime (aSW.stopAndGetMillis ());
            m_aStatsCounterInitSuccess.increment ();
            m_nInitCount++;
            m_aLastInitDT = PDTFactory.getCurrentLocalDateTime ();
          }
          finally
          {
            endWithoutAutoSave ();
            // reset any pending changes, because the initialization should
            // be read-only. If the implementing class changed something,
            // the return value of onInit() is what counts
            internalSetPendingChanges (false);
          }
        }
        else
        {
          // Read existing file
          CONDLOG.info ( () -> "Trying to read DAO XML file '" + aFinalFile + "'");

          m_aStatsCounterReadTotal.increment ();
          final IMicroDocument aDoc = MicroReader.readMicroXML (aFinalFile);
          if (aDoc == null)
          {
            LOGGER.error ("Failed to read XML document from file '" + aFinalFile + "'");
          }
          else
          {
            // Valid XML - start interpreting
            beginWithoutAutoSave ();
            try
            {
              final StopWatch aSW = StopWatch.createdStarted ();

              if (onRead (aDoc).isChanged ())
                eWriteSuccess = _writeToFile ();

              m_aStatsCounterReadTimer.addTime (aSW.stopAndGetMillis ());
              m_aStatsCounterReadSuccess.increment ();
              m_nReadCount++;
              m_aLastReadDT = PDTFactory.getCurrentLocalDateTime ();
            }
            finally
            {
              endWithoutAutoSave ();
              // reset any pending changes, because the initialization should
              // be read-only. If the implementing class changed something,
              // the return value of onRead() is what counts
              internalSetPendingChanges (false);
            }
          }
        }
        // Check if writing was successful on any of the 2 branches
        if (eWriteSuccess.isSuccess ())
          internalSetPendingChanges (false);
        else
        {
          LOGGER.error ("File '" + aFinalFile + "' has pending changes after initialRead!");
        }
      }
      catch (final Exception ex)
      {
        triggerExceptionHandlersRead (ex, bIsInitialization, aFinalFile);
        throw new DAOException ("Error " +
                                (bIsInitialization ? "initializing" : "reading") +
                                " the file '" +
                                aFinalFile +
                                "'",
                                ex);
      }
    });
  }

  /**
   * Called after a successful write of the file, if the filename is different
   * from the previous filename. This can e.g. be used to clear old data.
   *
   * @param sPreviousFilename
   *        The previous filename. May be null.
   * @param sNewFilename
   *        The new filename. Never null.
   */
  @OverrideOnDemand
  @MustBeLocked (ELockType.WRITE)
  protected void onFilenameChange (@Nullable final String sPreviousFilename, @Nonnull final String sNewFilename)
  {}

  /**
   * Create the XML document that should be saved to the file. This method is
   * only called within a write lock!
   *
   * @return The non-null document to write to the file.
   */
  @Nonnull
  @MustBeLocked (ELockType.WRITE)
  protected abstract IMicroDocument createWriteData ();

  /**
   * Modify the created document by e.g. adding some comment or digital
   * signature or whatsoever.
   *
   * @param aDoc
   *        The created non-null document.
   */
  @OverrideOnDemand
  @MustBeLocked (ELockType.WRITE)
  protected void modifyWriteData (@Nonnull final IMicroDocument aDoc)
  {
    final IMicroComment aComment = new MicroComment ("This file was generated automatically - do NOT modify!\n" +
                                                     "Written at " +
                                                     PDTToString.getAsString (ZonedDateTime.now (Clock.systemUTC ()),
                                                                              Locale.US));
    final IMicroElement eRoot = aDoc.getDocumentElement ();
    // Add a small comment
    if (eRoot != null)
      aDoc.insertBefore (aComment, eRoot);
    else
      aDoc.appendChild (aComment);
  }

  /**
   * Optional callback method that is invoked before the file handle gets
   * opened. This method can e.g. be used to create backups.
   *
   * @param sFilename
   *        The filename provided by the internal filename provider. Never
   *        null.
   * @param aFile
   *        The resolved file. It is already consistency checked. Never
   *        null.
   */
  @OverrideOnDemand
  @MustBeLocked (ELockType.WRITE)
  protected void beforeWriteToFile (@Nonnull final String sFilename, @Nonnull final File aFile)
  {}

  /**
   * @return The {@link IXMLWriterSettings} to be used to serialize the data.
   */
  @Nonnull
  @OverrideOnDemand
  protected IXMLWriterSettings getXMLWriterSettings ()
  {
    return XMLWriterSettings.DEFAULT_XML_SETTINGS;
  }

  /**
   * @return The filename to which was written last. May be null if
   *         no wrote action was performed yet.
   */
  @Nullable
  public final String getLastFilename ()
  {
    return m_aRWLock.readLockedGet ( () -> m_sPreviousFilename);
  }

  /**
   * Trigger the registered custom exception handlers for read errors.
   *
   * @param t
   *        Thrown exception. Never null.
   * @param sErrorFilename
   *        The filename tried to write to. Never null.
   * @param aDoc
   *        The XML content that should be written. May be null if
   *        the error occurred in XML creation.
   */
  protected static void triggerExceptionHandlersWrite (@Nonnull final Throwable t,
                                                       @Nonnull final String sErrorFilename,
                                                       @Nullable final IMicroDocument aDoc)
  {
    // Check if a custom exception handler is present
    if (exceptionHandlersWrite ().isNotEmpty ())
    {
      final IReadableResource aRes = new FileSystemResource (sErrorFilename);
      final String sXMLContent = aDoc == null ? "no XML document created" : MicroWriter.getNodeAsString (aDoc);
      exceptionHandlersWrite ().forEach (aCB -> aCB.onDAOWriteException (t, aRes, sXMLContent));
    }
  }

  /**
   * The main method for writing the new data to a file. This method may only be
   * called within a write lock!
   *
   * @return {@link ESuccess} and never null.
   */
  @Nonnull
  @SuppressFBWarnings ("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
  @MustBeLocked (ELockType.WRITE)
  private ESuccess _writeToFile ()
  {
    // Build the filename to write to
    final String sFilename = m_aFilenameProvider.get ();
    if (sFilename == null)
    {
      // We're not operating on a file! Required for testing
      CONDLOG.info ( () -> "The DAO of class " + getClass ().getName () + " cannot write to a file");
      return ESuccess.FAILURE;
    }
    // Check for a filename change before writing
    if (!sFilename.equals (m_sPreviousFilename))
    {
      onFilenameChange (m_sPreviousFilename, sFilename);
      m_sPreviousFilename = sFilename;
    }
    CONDLOG.info ( () -> "Trying to write DAO file '" + sFilename + "'");

    File aFile = null;
    IMicroDocument aDoc = null;
    try
    {
      // Get the file handle
      aFile = getSafeFile (sFilename, EMode.WRITE);

      m_aStatsCounterWriteTotal.increment ();
      final StopWatch aSW = StopWatch.createdStarted ();

      // Create XML document to write
      aDoc = createWriteData ();
      if (aDoc == null)
        throw new DAOException ("Failed to create data to write to file");

      // Generic modification
      modifyWriteData (aDoc);

      // Perform optional stuff like backup etc. Must be done BEFORE the output
      // stream is opened!
      beforeWriteToFile (sFilename, aFile);

      // Get the output stream
      final OutputStream aOS = FileHelper.getOutputStream (aFile);
      if (aOS == null)
      {
        // Happens, when another application has the file open!
        // Logger warning already emitted
        throw new DAOException ("Failed to open output stream");
      }
      // Write to file (closes the OS)
      final IXMLWriterSettings aXWS = getXMLWriterSettings ();
      if (MicroWriter.writeToStream (aDoc, aOS, aXWS).isFailure ())
        throw new DAOException ("Failed to write DAO XML data to file");

      m_aStatsCounterWriteTimer.addTime (aSW.stopAndGetMillis ());
      m_aStatsCounterWriteSuccess.increment ();
      m_nWriteCount++;
      m_aLastWriteDT = PDTFactory.getCurrentLocalDateTime ();
      return ESuccess.SUCCESS;
    }
    catch (final Exception ex)
    {
      final String sErrorFilename = aFile != null ? aFile.getAbsolutePath () : sFilename;

      LOGGER.error ("The DAO of class " +
                    getClass ().getName () +
                    " failed to write the DAO data to '" +
                    sErrorFilename +
                    "'",
                    ex);

      triggerExceptionHandlersWrite (ex, sErrorFilename, aDoc);
      m_aStatsCounterWriteExceptions.increment ();
      return ESuccess.FAILURE;
    }
  }

  /**
   * This method must be called every time something changed in the DAO. It
   * triggers the writing to a file if auto-save is active. This method must be
   * called within a write-lock as it is not locked!
   */
  @MustBeLocked (ELockType.WRITE)
  protected final void markAsChanged ()
  {
    // Just remember that something changed
    internalSetPendingChanges (true);
    if (internalIsAutoSaveEnabled ())
    {
      // Auto save
      if (_writeToFile ().isSuccess ())
        internalSetPendingChanges (false);
      else
      {
        LOGGER.error ("The DAO of class " + getClass ().getName () + " still has pending changes after markAsChanged!");
      }
    }
  }

  /**
   * In case there are pending changes write them to the file. This method is
   * write locked!
   */
  public final void writeToFileOnPendingChanges ()
  {
    if (hasPendingChanges ())
    {
      m_aRWLock.writeLocked ( () -> {
        // Write to file
        if (_writeToFile ().isSuccess ())
          internalSetPendingChanges (false);
        else
        {
          LOGGER.error ("The DAO of class " +
                        getClass ().getName () +
                        " still has pending changes after writeToFileOnPendingChanges!");
        }
      });
    }
  }

  @Nonnegative
  public int getInitCount ()
  {
    return m_nInitCount;
  }

  @Nullable
  public final LocalDateTime getLastInitDateTime ()
  {
    return m_aLastInitDT;
  }

  @Nonnegative
  public int getReadCount ()
  {
    return m_nReadCount;
  }

  @Nullable
  public final LocalDateTime getLastReadDateTime ()
  {
    return m_aLastReadDT;
  }

  @Nonnegative
  public int getWriteCount ()
  {
    return m_nWriteCount;
  }

  @Nullable
  public final LocalDateTime getLastWriteDateTime ()
  {
    return m_aLastWriteDT;
  }

  @Override
  public String toString ()
  {
    return ToStringGenerator.getDerived (super.toString ())
                            .append ("IO", m_aIO)
                            .append ("FilenameProvider", m_aFilenameProvider)
                            .append ("PreviousFilename", m_sPreviousFilename)
                            .append ("InitCount", m_nInitCount)
                            .appendIfNotNull ("LastInitDT", m_aLastInitDT)
                            .append ("ReadCount", m_nReadCount)
                            .appendIfNotNull ("LastReadDT", m_aLastReadDT)
                            .append ("WriteCount", m_nWriteCount)
                            .appendIfNotNull ("LastWriteDT", m_aLastWriteDT)
                            .getToString ();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy