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