FrameAstKey.java
/*
** Module : FrameAstKey.java
** Abstract : a dedicated key for comparing (shared) frames definitions
**
** Copyright (c) 2016-2020, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description---------------------------------
** 001 OM 20161121 Initial release.
** 002 CA 20200412 Added incremental conversion support.
** 003 CA 20200416 Reduce the memory footprint for incremental conversion.
** 004 CA 20200423 Keep a copy of the AST in memory, to not pin the entire file.
** 005 IAS 20200922 Get rid of possible NPE on serialization.
*/
/*
** 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.uast;
import static com.goldencode.util.NativeTypeSerializer.*;
import java.io.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.pattern.*;
/**
* Objects of this class are used as keys for a hashtable that match frame definitions with
* compatible structure that can be used by shared frames partitions. If two frames are compatible
* then their {@code FrameAstKey} will be {@code equals}. If the keys are different, then the two
* frames belong to different partitions.
* <p>
* The matching algorithm takes into consideration:
* <ul>
* <li>type and name of all fields and variables used in FORM/FRAME definition;</li>
* <li>type of all literals from frame definition;</li>
* <li>the order of the above;</li>
* <li>the frame's name.</li>
* </ul>
* The matching algorithm does NOT take into consideration:
* <ul>
* <li>spacers (SKIP and SPACE);</li>
* <li>the value of the literals;</li>
* <li>any form item in the HEADER definition;</li>
* <li>any declaration from any frame format node.</li>
* </ul>
*/
public class FrameAstKey
implements Externalizable
{
/**
* The Progress AST contained. It must be a {@code KW_FORM} or {@code DEFINE_FRAME} node.
*/
private Aast fAst;
/**
* Cached value of hash. {@code FrameAstKey} is imutabil so once computed in constructor it
* remains unchanged during the lifetime of the container.
*/
private int hash;
/**
* The name of the FRAME. This attribute is not mandatory. More frames can share the same
* interface using different names, although this is rather strange. To have the code more
* readable and consistent we enforce this constraint.
*/
private String frameName;
/**
* Default c'tor, used for (de)serialization.
*/
public FrameAstKey()
{
// no-op
}
/**
* The single constructor. Saves a copy of the input data and computes the hash for quick match
* as a hashing key.
*
* @param ast
* The content node. Must be a {@code KW_FORM} or {@code DEFINE_FRAME}
* @param frameName
* The name of the frame. It cannot be always extracted directly form {@code ast}.
*
* @throws NullPointerException
* if any argument is {@code null}.
* @throws RuntimeException
* on parameter validation failure: the first argument is not a FRAME definition
* ({@code KW_FORM} or {@code DEFINE_FRAME}).
*/
public FrameAstKey(Aast ast, String frameName)
{
// ensure we detach the AST from the tree, to not keep the entire tree in the memory
ast = ast.duplicate();
initialize(ast, frameName);
}
/**
* Obtain the hashing value for this container. This method is fast as the value was already
* computed in constructor.
*
* @return the hash value for this object.
*/
@Override
public int hashCode()
{
return hash;
}
/**
* Check if another object {@code obj} and this are identical (represent FRAMES with compatible
* structure, which can be SHARED). The algorithm iterates all relevant nodes from both
* containers and try to match them. Like the hashing algorithm, only {@code FORM_ITEM} that
* contains accessible information are taken into consideration (fields and variables). The
* constants also add to hashing value but their actual value is not important. Spacers are
* completely ignored.
*
* @param obj
* A second object to test for item structure.
*
* @return {@code true} if the {@code obj} and this contain compatible frame definitions.
*/
@Override
public boolean equals(Object obj)
{
if (!(obj instanceof FrameAstKey))
{
return false;
}
FrameAstKey that = (FrameAstKey) obj;
if (!this.frameName.equals(that.frameName))
{
return false;
}
Aast ch = getNextSignificantNode((Aast) this.fAst.getFirstChild());
Aast ch_ = getNextSignificantNode((Aast) that.fAst.getFirstChild());
while (ch != null)
{
if (ch_ == null)
{
// [that] has more significant widgets
return false;
}
// at this point ch and ch_ should both be ProgressParserTokenTypes.EXPRESSION
Aast exprThis = (Aast) ch.getFirstChild();
Aast exprThat = (Aast) ch_.getFirstChild();
int thisType = exprThis.getType();
if (thisType != exprThat.getType())
{
// the form item thisType does not match
return false;
}
// in case of fields (TODO: and variables), check their names, too
if ((thisType > ProgressParserTokenTypes.BEGIN_FIELDTYPES &&
thisType < ProgressParserTokenTypes.END_FIELDTYPES ||
thisType > ProgressParserTokenTypes.BEGIN_VARTYPES &&
thisType < ProgressParserTokenTypes.END_VARTYPES) &&
!exprThis.getText().equals(exprThat.getText()))
{
return false;
}
ch = getNextSignificantNode((Aast) ch.getNextSibling());
ch_ = getNextSignificantNode((Aast) ch_.getNextSibling());
}
return ch_ == null;
}
/**
* Write this instance to stream.
*
* @param out
* The object output stream.
*/
@Override
public void writeExternal(ObjectOutput out)
throws IOException
{
writeLong(out, fAst == null ? null : fAst.getId());
writeString(out, frameName);
}
/**
* Restore this instance by re-loading the AST with the given ID and {@link #initialize} it.
*
* @param in
* The object input stream.
*/
@Override
public void readExternal(ObjectInput in)
throws IOException,
ClassNotFoundException
{
Long astId = readLong(in);
String frameName = readString(in);
// TODO: if AST doesn't exist, don't initialize... throw exception?
AstSymbolResolver resolver = AstSymbolResolver.getResolver();
Aast ast = resolver.getAst(astId);
if (ast == null)
{
AstManager mgr = AstManager.get();
long treeId = mgr.getTreeId(astId);
Aast tree = mgr.loadTree(mgr.getTreeName(treeId) + ".ast");
resolver.registerTree(tree);
ast = resolver.getAst(astId);
resolver.deregisterTree(tree);
ast = ast.duplicate();
// re-register the tree so that other references to the same AST will re-use it
resolver.registerTree(ast);
}
initialize(ast, frameName);
}
/**
* Utility method that searches next relevant item from a frame definition, starting from a
* current node, iterating its right siblings, until one is found or teh list ends.
*
* @param frameDefNode
* the starting point for relevant node search.
*
* @return a significant sibling if anyone is found or {@code null} otherwise.
*/
private static Aast getNextSignificantNode(Aast frameDefNode)
{
while (frameDefNode != null)
{
switch (frameDefNode.getType())
{
case ProgressParserTokenTypes.KW_HEADER:
// from this point forward the widgets are not significant for comparing frame
// forms
return null;
case ProgressParserTokenTypes.FORM_ITEM:
Aast ch2 = (Aast) frameDefNode.getFirstChild();
if (ch2.getType() == ProgressParserTokenTypes.EXPRESSION)
{
return frameDefNode;
}
// spacers are irrelevant, too: ProgressParserTokenTypes.KW_SPACE, KW_SKIP:
break;
}
// search next sibling node
frameDefNode = (Aast) frameDefNode.getNextSibling();
}
// there are no more sibling nodes
return null;
}
/**
* Compute the hashing value for the FRAME component.
* <p>
* Iterate all nodes and create a hashing value for all relevant sub-nodes (the ones that are
* needed for frame matching). Only {@code FORM_ITEM} that contains accessible information
* are taken into consideration (fields and variables). The constants also add to hashing value
* but their actual value is not important. Spacers are totally ignored.
* <p>
* The order is also important but it is not enforced directly. However, because of the way the
* items are added and shifted sequentially, the result value should be sufficient for a
* hashing key.
*
* @return hashing value of this key.
*
* @throws RuntimeException
* if some unexpected children are encountered.
*/
private int computeHash()
{
int hash = frameName.hashCode();
Aast ch = (Aast) fAst.getFirstChild();
while (ch != null)
{
switch (ch.getType())
{
case ProgressParserTokenTypes.KW_HEADER:
// next elements (from the header) are irrelevant for our class
return hash;
case ProgressParserTokenTypes.FORM_ITEM:
Aast ch2 = (Aast) ch.getFirstChild();
int ch2Type = ch2.getType();
if (ch2Type == ProgressParserTokenTypes.KW_SPACE ||
ch2Type == ProgressParserTokenTypes.KW_SKIP)
{
// spacers are irrelevant
break;
}
if (ch2Type == ProgressParserTokenTypes.EXPRESSION)
{
ch2 = (Aast) ch2.getFirstChild();
ch2Type = ch2.getType();
}
if (ch2Type > ProgressParserTokenTypes.BEGIN_FIELDTYPES &&
ch2Type < ProgressParserTokenTypes.END_FIELDTYPES ||
ch2Type > ProgressParserTokenTypes.BEGIN_VARTYPES &&
ch2Type < ProgressParserTokenTypes.END_VARTYPES)
{
hash = 37 * hash + ch2Type * 11 + ch2.getText().hashCode();
}
else
{
hash = 37 * hash + ch2Type;
}
break;
case ProgressParserTokenTypes.FRAME_PHRASE:
// the elements under WITH keyword does not matter for this analyse
break;
case ProgressParserTokenTypes.SYMBOL:
case ProgressParserTokenTypes.KW_NEW:
case ProgressParserTokenTypes.KW_SHARED:
if (ch.getParent() == fAst)
{
// in DEFINE [[NEW] SHARED] FRAME syntax this is normal, but we ignore the nodes
break;
}
throw new RuntimeException("Unexpected SYMBOL, NEW or SHARED token");
default:
throw new RuntimeException("Unknown frame element: " +
ch.getSymbolicTokenType() + "(" + ch.getLine() + ":" + ch.getColumn() +")\n" +
fAst.dumpTree());
}
ch = (Aast) ch.getNextSibling();
}
return hash;
}
/**
* Initialize this instance from the backing AST.
*
* @param ast
* The content node. Must be a {@code KW_FORM} or {@code DEFINE_FRAME}
* @param frameName
* The name of the frame. It cannot be always extracted directly form {@code ast}.
*
* @throws NullPointerException
* if any argument is {@code null}.
* @throws RuntimeException
* on parameter validation failure: the first argument is not a FRAME definition
* ({@code KW_FORM} or {@code DEFINE_FRAME}).
*/
private void initialize(Aast ast, String frameName)
{
if (ast == null || frameName == null)
{
throw new NullPointerException("FORM and frame name must not be null.");
}
int aType = ast.getType();
if (aType != ProgressParserTokenTypes.KW_FORM &&
aType != ProgressParserTokenTypes.DEFINE_FRAME)
{
throw new RuntimeException("Invalid parameter. Not a form.");
}
this.fAst = ast;
this.frameName = frameName;
hash = computeHash();
}
}