LoggedCollection.java

/*
** Module   : LoggedCollection.java
** Abstract : Helper class which logs changes to a collection, for each AST location.
**
** Copyright (c) 2020, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 CA  20200412 Created initial version, for incremental conversion support.
** 002 CA  20200416 Reduce the memory footprint for incremental conversion.
*/
/*
** 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.convert.db;

import java.sql.*;
import java.util.*;
import java.util.function.*;

/**
 * Log all changes to a specified collection, by keeping track of the source AST which made this
 * change.
 * <p>
 * On incremental conversion, this allows restoration of the collection's state, without the
 * currently parsed and converted ASTs.
 * <p>
 * All changes are stored in a work table, which has the same name as the main collection's table,
 * followed by a <code>__work</code> suffix.
 */
public class LoggedCollection
{
   /** 
    * The changes made to this collection.  The value may be null in case the collection is not
    * a map.
    */
   private final Map<AstKey, Object> changes = new LinkedHashMap<>();

   /**
    * Force certain values to be logged for a source AST.  This allows to identify cases where
    * the map already has a key and a value, but the source AST must keep a different value for
    * this key, even if the values are (almost) identical.
    */
   private final Map<AstKey, Object> forced = new LinkedHashMap<>();

   /**
    * Log this change being made to the collection.
    * 
    * @param    fileId
    *           The AST file ID.
    * @param    astId
    *           The source AST ID.
    * @param    key
    *           The collection key.
    * @param    value
    *           The value, which is non-null only for maps.
    */
   public void log(long fileId, long astId, Object key, Object value)
   {
      AstKey changeKey = new AstKey(fileId, astId, key);
      /*
      if (changes.containsKey(changeKey))
      {
         throw new IllegalStateException("Key already exists!");
      }
      */
      
      changes.put(changeKey, value);
   }

   /**
    * Log this change being made to the collection.
    * 
    * @param    fileId
    *           The AST file ID.
    * @param    astId
    *           The source AST ID.
    * @param    key
    *           The collection key.
    */
   public void log(long fileId, long astId, Object key)
   {
      log(fileId, astId, key, null);
   }

   /**
    * Force a change to be logged for the specified source AST.
    * 
    * @param    fileId
    *           The AST file ID.
    * @param    astId
    *           The source AST ID.
    * @param    key
    *           The collection key.
    * @param    value
    *           The value, which is non-null only for maps.
    */
   public void force(long fileId, long astId, Object key, Object value)
   {
      AstKey changeKey = new AstKey(fileId, astId, key);
      
      forced.put(changeKey, value);
   }
   
   /**
    * Remove all changes having this key.
    * 
    * @param    key
    *           The key to be removed.
    */
   public void remove(Object key)
   {
      if (key == null)
      {
         throw new NullPointerException("Invalid key!");
      }

      // delete all logged changes for this key (as we want to remove it);
      Iterator<AstKey> iter = changes.keySet().iterator();
      while (iter.hasNext())
      {
         AstKey changeKey = iter.next();
         if (changeKey.key.equals(key))
         {
            iter.remove();
         }
      }
      iter = forced.keySet().iterator();
      while (iter.hasNext())
      {
         AstKey changeKey = iter.next();
         if (changeKey.key.equals(key))
         {
            iter.remove();
         }
      }
   }

   /**
    * Given a set of keys, retain from the changes only those for which the key exists in this set.
    * <p>
    * This allows the changes to be 'in sync' with the main collection, as certain remove 
    * operations on the main collection may not have been logged to the collection.
    * 
    * @param    keys
    *           The keys which need to be retained.
    */
   public void retainAll(Set<?> keys)
   {
      Iterator<AstKey> iter = this.changes.keySet().iterator();
      while (iter.hasNext())
      {
         AstKey key = iter.next();
         if (!keys.contains(key.key))
         {
            iter.remove();
         }
      }
   }

   /**
    * Clear all changes.
    */
   public void clear()
   {
      changes.clear();
      forced.clear();
   }

   /**
    * Persist all changes to the database.
    * 
    * @param    insertSql
    *           The SQL used to insert into the table.
    * @param    helper
    *           The helper to perform SQL operations.
    * @param    c
    *           A consumer to resolve the arguments for the insert statement.
    */
   public void persist(String insertSql, DBHelper helper, BiFunction<AstKey, Object, Object[]> c)
   {
      changes.putAll(forced);
      
      helper.persistCollection(changes.keySet(), insertSql, (PreparedStatement ps, Object key) -> 
      {
         try
         {
            AstKey astKey = (AstKey) key;
            Object val = changes.get(astKey);
            
            Object[] args = c.apply(astKey, val);
            for (int i = 0; i < args.length; i++)
            {
               ps.setObject(i + 1, args[i]);
            }
            ps.addBatch();
         }
         catch (SQLException e)
         {
            throw new RuntimeException(e);
         }
      });
   }

   /**
    * Persist all changes to the database, using the specified statement.
    * 
    * @param    ps
    *           The prepared insert statement.
    * @param    c
    *           A consumer to resolve the arguments for the insert statement.
    * 
    * @throws   SQLException
    *           For SQL-related errors when setting the statement arguments.
    */
   public void persist(PreparedStatement ps, BiFunction<AstKey, Object, Object[]> c)
   throws SQLException
   {
      for (AstKey astKey : changes.keySet())
      {
         Object val = changes.get(astKey);
         
         Object[] args = c.apply(astKey, val);
         for (int i = 0; i < args.length; i++)
         {
            ps.setObject(i + 1, args[i]);
         }
         ps.addBatch();
      }
   }

   /**
    * Compact the elements of this collection, to reduce the memory footprint.
    * 
    * @param    fileId
    *           The AST file ID which has finished processing.
    * @param    c
    *           A consumer to process the values.
    */
   public void compact(long fileId, Consumer<CustomExternalizable> c)
   {
      for (AstKey key : changes.keySet())
      {
         if (key.fileId != fileId)
         {
            continue;
         }
         
         Object value = changes.get(key);
         c.accept((CustomExternalizable) value);
      }
   }

   /**
    * A key for a logged change, in a collection.
    */
   static class AstKey
   {
      /** The file AST ID. */
      final long fileId;
      
      /** The source AST ID. */
      final long astId;
      
      /** The change key. */
      final Object key;
      
      /**
       * Create a new instance.
       * 
       * @param    fileId
       *           The file AST ID.
       * @param    astId
       *           The source AST ID.
       * @param    key
       *           The change key.
       */
      public AstKey(long fileId, long astId, Object key)
      {
         this.fileId = fileId;
         this.astId = astId;
         this.key = key;
      }
      
      /**
       * Calculate the hashcode for this key.
       * 
       * @return   The hashcode.
       */
      @Override
      public int hashCode()
      {
         int result = 17;
         
         result = result * 37 + Long.hashCode(fileId);
         result = result * 37 + Long.hashCode(astId);
         result = result * 37 + key.hashCode();
         
         return result;
      }
      
      /**
       * Check if this instance is the same as the specified one.
       * 
       * @param    obj
       *           The object to compare against.
       * 
       * @return   <code>true</code> if the specified object has the same key, file and AST ID
       *           as this one.
       */
      @Override
      public boolean equals(Object obj)
      {
         if (!(obj instanceof AstKey))
         {
            return false;
         }
         
         AstKey other = (AstKey) obj;
         
         return this.fileId == other.fileId && 
                this.astId == other.astId && 
                this.key.equals(other.key);
      }
   }
}