P2OAccessWorker.java
/*
** Module : P2OAccessWorker.java
** Abstract : Pattern worker which provides lookup access to information in P2O ASTs
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ---------------------------------- Description ----------------------------------
** 001 ECF 20050921 @22771 Created initial version. Lazily loads P2O
** ASTs and enables lookups of Java names for
** legacy Progress table and field names.
** This first implementation supports fixed
** tables only (not temp- or work-tables).
** 002 ECF 20051004 @22933 Added javaPackage user function. Retrieves
** java package name of DMO associated with a
** legacy table name.
** 003 ECF 20051005 @22985 Shorten P2O filename. Replace ".schema" with
** ".p2o" instead of appending ".p2o" extension.
** 004 ECF 20051013 @23035 Refactored to abstract out P2O AST loading
** and initialization. These tasks are now
** delegated to P2OLookup.
** 005 ECF 20051017 @23053 Added javaPropertyName user function. Returns
** name of DMO property associated with a legacy
** database field name.
** 006 ECF 20051024 @23096 Added boolean 'qualified' parameter to
** javaPropertyName user function. Optionally
** qualifies property name with a buffer name to
** enable use within HQL where and order by
** clauses, where qualification is necessary.
** 007 ECF 20051102 @23182 Added legacyFieldNames user function.
** Retrieves set of legacy field names for a
** Progress table, in display order.
** 008 ECF 20051121 @23422 Added isValidatable user function. Indicates
** whether a schema field must invoke validation
** processing before it may be saved to its DMO.
** Delegates work to P2OLookup.
** 009 ECF 20060202 @24253 Added support for temp/work-table schema
** data lookups. Implemented visitAst method to
** load current, temporary schema data into
** P2OLookup's state each time a new source file
** is visited during conversion.
** 010 ECF 20060408 @25430 Added isDMOInterface() method. Indicates
** whether a given class name collides with any
** known DMO interface name. Temp table DMOs not
** currently handled.
** 011 ECF 20061026 @30796 Added isForeignReference() method. Indicates
** whether a given table is referenced by the
** foreign key of any other table.
** 012 GES 20090424 @41952 Import change.
** 013 GES 20090518 @42397 Import change.
** 014 OM 20131030 Added support for checking if a schema contains a certain dmo iface.
** 015 VMN 20140328 Added support for denormalized properties.
** 016 VMN 20150128 Fixed denormalized field assignments.
** 017 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 018 ECF 20180305 Added isReadOnly API to check for a read-only table.
** 019 ECF 20180909 Added support for unused database resource reports.
** 020 CA 20190508 Buffers must be looked in the class hierarchy, not only in the
** current class (thus we need to recursively load .p2o files).
** 021 CA 20200122 Javadoc fixes.
** 022 CA 20200412 Added incremental conversion support.
** 023 CA 20201008 Added APIs to obtain an iterator over the indexes or properties of a table.
** OM 20201231 Avoid java name conflicts.
** OM 20210711 In case of buffer parameters, use the super .Buf interface name to allow calls
** with 'sibling' tables. Access that with the new javaInterfaceName() method.
** IAS 20211928 Added isNullable methods
** CA 20211214 The AST manager plugin must be context-local, for runtime conversion, as the
** InMemoryRegistryPlugin is not thread-safe.
** All worker's state must be context-local, for runtime conversion to work in
** multi-context mode, as the pattern workers are singletons.
** CA 20230110 Let 'getImmediateChild' work with a set, if an array is used for the types.
** 024 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 025 RAA 20240821 Added support for reporting indeterminate sorting queries.
** RAA 20240821 Modified query string initialization in writeIndeterminateSortQueryContent.
** RAA 20240826 Added isIndeterminateSortQuery and checkFieldMatching functions.
** RAA 20240904 Added risk factor to indeterminate sorting queries.
** RAA 20240905 Refactored QueryIndexInfo to not store the given AST.
** RAA 20240906 Added writeHeaderToIndeterminateSortFile() method and cleared the query list.
** RAA 20240910 checkFieldMatching now uses the legacy name criteria instead of the order by.
** 026 OM 20241128 Multi-tenant conversion support: added getTenantType() method.
** 027 CA 20250319 This worker can be used before the 'qualified_oo_name' is set in annotation
** (like in schema conversion phase), so ensure we can load the ClassDefinition.
*/
/*
** 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.schema;
import java.io.*;
import java.util.*;
import java.util.logging.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.util.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.pattern.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.uast.*;
/**
* A pattern worker that provides services to support the retrieval of
* information from previously created, persisted, P2O ASTs.
* <p>
* Currently, this worker supports lookups of converted Java schema names,
* based upon legacy, Progress schema names as input. Specifically, given a
* fully qualified, Progress database table name, the Java name for the
* corresponding Data Model Object (DMO) class can be retrieved. The Java
* name of the implicit (default) buffer for that table is also available.
* Given a fully qualified, Progress database field name, the Java name for
* the corresponding DMO getter and setter methods can be retrieved.
*/
public final class P2OAccessWorker
extends AbstractPatternWorker
implements DataModelTokenTypes
{
/** Logger. */
private static final ConversionStatus LOG = ConversionStatus.get(P2OAccessWorker.class);
/** Table token types */
private static final Collection<Integer> types = new HashSet<>();
static
{
types.add(ProgressParserTokenTypes.TABLE);
types.add(ProgressParserTokenTypes.TEMP_TABLE);
types.add(ProgressParserTokenTypes.WORK_TABLE);
types.add(ProgressParserTokenTypes.BUFFER);
}
/** Context local work area. */
private static final ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
{
protected WorkArea initialValue()
{
return new WorkArea();
}
};
/**
* Default constructor which initializes libraries.
*/
public P2OAccessWorker()
{
super();
setLibrary(new Library());
}
/**
* Extract the target table (or temp/work-table or buffer) name from the given AST, which is
* expected to be of type {@code RECORD_PHRASE} or {@code EMBEDDED_SQL}.
*
* @param ast
* Record phrase or embedded SQL AST.
*
* @return Target table name, qualified by database schema name.
*
* @throws SchemaException
* if an unexpected AST node is passed to this method.
*/
private static String getTableName(Aast ast)
throws SchemaException
{
Aast child = null;
switch (ast.getType())
{
case ProgressParserTokenTypes.RECORD_PHRASE:
child = ast.getImmediateChild(types, null);
break;
case ProgressParserTokenTypes.EMBEDDED_SQL:
child = ast.getChildAt(0); // kw_select or delete_from
if (child.getType() == ProgressParserTokenTypes.KW_SELECT)
{
// table node is one generation further down in this case, under an explicit
// kw_from node
child = child.getImmediateChild(ProgressParserTokenTypes.KW_FROM, null);
}
child = child.getImmediateChild(types, null);
break;
default:
throw new SchemaException("Unexpected token: " + ast.toString());
}
return (String) child.getAnnotation("schemaname");
}
/**
* This method is called each time a new AST is visited, just before the
* pattern engine processes that AST. We exploit this hook by loading the
* current temp/work-table schema (P2O) AST into the <code>P2OLookup</code>
* class' state. This enables us to resolve references to temp and work
* tables, which are local to the currently loaded source file AST.
*
* @param ast
* The root node of the source AST about to be processed by the
* pattern engine.
*/
public void visitAst(Aast ast)
{
WorkArea wa = context.get();
ClassDefinition clsDef = SymbolResolver.resolveClassDefinition(ast);
String fileName = ast.getFilename();
P2OLookup.loadTemporarySchema(fileName, clsDef);
wa.suspiciousQueries.clear();
wa.indeterminateSortQueries.clear();
}
/**
* Resolve <code>constant</code> to a literal which can be compiled into
* expressions.
*
* @param constant
* Constant indicating a data model token name.
*
* @return Token type associated with the token name.
*/
public Object resolveConstant(String constant)
{
return DataModelAst.resolveConstant(constant);
}
/**
* Helper to lookup Java names for tables and fields, given their
* historical, Progress names.
*/
public class Library
{
/**
* Obtains an iterator to the current record's list of indexes.
*
* @param schemaname
* The fully qualified schema name of the record being processed.
*
* @return The list of indexes.
*/
public Iterator<P2JIndex> indexes(String schemaname)
{
Iterator<P2JIndex> iter = null;
try
{
iter = P2OLookup.indexes(schemaname);
}
catch (SchemaException se)
{
LOG.log(Level.SEVERE, "", se);
}
return iter;
}
/**
* Obtains an iterator to the current record's list of indexes.
*
* @param schemaname
* The fully qualified schema name of the record being processed.
*
* @return The list of indexes.
*/
public Iterator<Aast> properties(String schemaname)
{
Iterator<Aast> iter = null;
try
{
iter = P2OLookup.properties(schemaname);
}
catch (SchemaException se)
{
LOG.log(Level.SEVERE, "", se);
}
return iter;
}
/**
* Look up the package of a Java DMO interface, given its legacy name.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a table.
*
* @return Package name of the DMO interface.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public String javaPackage(String historical)
throws SchemaException
{
return P2OLookup.javaPackage(historical);
}
/**
* Look up the name of a Java DMO interface, given its legacy name.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a table.
*
* @return Unqualified, Java name of the interface which defines the
* DMO API.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the table portion of <code>historical</code> cannot be
* resolved to a corresponding Java name.
*/
public String javaClassName(String historical)
throws SchemaException
{
return P2OLookup.javaName(historical, EntityName.TABLE);
}
/**
* Obtain the multi-tenant table type. The values are:
* 0 = single-tenant (not store here),
* 1 = multi-tenant,
* 2 = multi-tenant-with-default.
*
* @param historical
* The table name.
*
* @return The table multi-tenant type, as described above.
*
* @throws SchemaException
* In case of error accessing internal data.
*/
public int getTenantType(String historical)
throws SchemaException
{
return P2OLookup.getTenantType(historical);
}
/**
* In the case of temp-tables, look up the name of a Java DMO super interface, given its legacy name.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a temp-table.
*
* @return Unqualified, Java name of the interface which defines the DMO API.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the table portion of {@code historical} cannot be resolved to a corresponding Java name.
*/
public String javaInterfaceName(String historical)
throws SchemaException
{
return P2OLookup.javaInterfaceName(historical);
}
/**
* Look up the name of a Java DMO interface, given its legacy name,
* and return the name of the default, implicit buffer associated with
* this interface. This is simply the name of the appropriate DMO
* interface, with the first letter lowercased.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a table.
*
* @return Short DMO interface name with first letter decapitalized.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the table portion of <code>historical</code> cannot be
* resolved to a corresponding Java name.
*/
public String javaBufferName(String historical)
throws SchemaException
{
return StringHelper.changeCase(javaClassName(historical), false);
}
/**
* Look up the name of the name of the DMO property which represents
* the specified, legacy database field.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a database field.
* @param qualified
* <code>true</code> if property name should be qualified with
* a buffer name; <code>false</code> if it should remain
* unqualified.
*
* @return Converted Java property name.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the field portion of <code>historical</code> cannot be
* resolved to a corresponding Java name.
*/
public String javaPropertyName(String historical, boolean qualified)
throws SchemaException
{
return P2OLookup.javaPropertyName(historical, qualified, null);
}
/**
* Look up the name of the name of the DMO property which represents
* the specified, legacy database field.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a database field.
* @param qualified
* <code>true</code> if property name should be qualified with
* a buffer name; <code>false</code> if it should remain
* unqualified.
* @param index
* Zero-based index of custom property name for denormalized field with extent,
* otherwise null.
*
* @return Converted Java property name.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the field portion of <code>historical</code> cannot be
* resolved to a corresponding Java name.
*/
public String javaPropertyName(String historical, boolean qualified, Long index)
throws SchemaException
{
return P2OLookup.javaPropertyName(historical, qualified, index);
}
/**
* Retrieve the full set of fully qualified, legacy field names for a
* legacy Progress table. The set is guaranteed to iterate in the
* default display order defined for the original, Progress schema for
* that table.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a table.
* @param includeHidden
* If {@code true} the hidden fields are also included.
*
* @return Set of fully qualified, legacy field names.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if <code>historical</code> does not represent a known table.
*/
public Set<String> legacyFieldNames(String historical, boolean includeHidden)
throws SchemaException
{
return P2OLookup.legacyFieldNames(historical, includeHidden);
}
/**
* Look up the name of the accessor (getter) method associated with the
* DMO property which represents the specified, legacy database field.
* The method name is in the form of a typical Java-bean getter:
* <pre>
* get{PropertyName}
* </pre>
* or
* <pre>
* is{PropertyName}
* </pre>
* if <i>PropertyName</i> represents a <code>logical</code> data type.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a database field.
* @param isLogical
* <code>true</code> if <code>historical</code> represents a
* <code>logical</code> field, else <code>false</code>.
*
* @return Java-bean style getter method name.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the field portion of <code>historical</code> cannot be
* resolved to a corresponding Java name.
*/
public String javaGetterName(String historical, boolean isLogical)
throws SchemaException
{
String prefix = isLogical ? "is" : "get";
return P2OLookup.beanMethodName(prefix, historical);
}
/**
* Look up the name of the accessor (getter) method associated with the
* DMO property which represents the specified, legacy database field.
* The method name is in the form of a typical Java-bean getter:
* <pre>
* get{PropertyName}
* </pre>
* or
* <pre>
* is{PropertyName}
* </pre>
* if <i>PropertyName</i> represents a <code>logical</code> data type.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a database field.
* @param index
* Zero-based index of custom property name for denormalized field with extent,
* otherwise null.
* @param isLogical
* <code>true</code> if <code>historical</code> represents a
* <code>logical</code> field, else <code>false</code>.
*
* @return Java-bean style getter method name.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the field portion of <code>historical</code> cannot be
* resolved to a corresponding Java name.
*/
public String javaGetterName(String historical, Long index, boolean isLogical)
throws SchemaException
{
String prefix = isLogical ? "is" : "get";
if ( index != null)
{
index --;
}
return P2OLookup.beanMethodName(prefix, historical, index);
}
/**
* Look up the name of the mutator (setter) method associated with the
* DMO property which represents the specified, legacy database field.
* The method name is in the form of a typical Java-bean setter:
* <pre>
* set{PropertyName}
* </pre>
*
* @param historical
* Fully qualified, legacy, Progress schema name for a database field.
*
* @return Java-bean style setter method name.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the field portion of <code>historical</code> cannot be
* resolved to a corresponding Java name.
*/
public String javaSetterName(String historical)
throws SchemaException
{
return P2OLookup.beanMethodName("set", historical);
}
/**
* Look up the name of the mutator (setter) method associated with the
* DMO property which represents the specified, legacy database field.
* The method name is in the form of a typical Java-bean setter:
* <pre>
* set{PropertyName}
* </pre>
*
* @param historical
* Fully qualified, legacy, Progress schema name for a
* database field.
* @param index
* Zero-based index of custom property name for denormalized field with extent,
* otherwise null.
*
* @return Java-bean style setter method name.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the field portion of <code>historical</code> cannot be
* resolved to a corresponding Java name.
*/
public String javaSetterName(String historical, Long index)
throws SchemaException
{
if ( index != null)
{
index --;
}
return P2OLookup.beanMethodName("set", historical, index);
}
/**
* Check if given historical fully qualified property is denormalized.
*
* @param historical
* Historical fully qualified property name.
*
* @return <code>true</code> if given property is denormalized.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public boolean isDenormalizedProperty(String historical)
throws SchemaException
{
return P2OLookup.isDenormalizedProperty(historical);
}
/**
* Indicate whether a DMO property can be validated at runtime, given
* its legacy schema name. Possible forms of validation include:
* <ul>
* <li>mandatory (non-nullable) constraint;
* <li>participation in a unique constraint;
* <li>explicit validation method (converted from a Progress validation expression).
* </ul>
*
* @param historical
* Fully qualified, legacy Progress field name.
*
* @return <code>true</code> if the field should be validated at
* runtime, else <code>false</code>.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public boolean isValidatable(String historical)
throws SchemaException
{
return P2OLookup.isValidatable(historical);
}
/**
* Indicate whether a DMO property is nullable.
*
* @param historical
* Fully qualified, legacy Progress field name.
*
* @return <code>true</code> if the field is nullable, else <code>false</code>.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public boolean isNullable(String historical)
throws SchemaException
{
return P2OLookup.isNullable(historical);
}
/**
* Indicate whether a DMO class represents a read-only table, given its schema name.
*
* @param historical
* Fully qualified, legacy Progress table name.
*
* @return {@code true} if the table is read-only, else {@code false}.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public boolean isReadOnly(String historical)
throws SchemaException
{
return P2OLookup.isReadOnly(historical);
}
/**
* Report whether the given class name is the unqualified name of any
* DMO interface, in any schema tracked by the current P2J
* configuration.
*
* @param name
* An unqualified class name.
*
* @return <code>true</code> if <code>name</code> collides with any
* DMO interface in any schema, else <code>false</code>.
*
* @throws SchemaException
* if there is an error loading schema names from configuration.
*/
public boolean isDMOInterface(String name)
throws SchemaException
{
return P2OLookup.isDMOInterface(name);
}
/**
* Report whether the given class name is the unqualified name of a DMO interface, in the
* <code>schema</code> tracked by the current P2J configuration.
*
* @param name
* An unqualified class name.
* @param schema
* The name of the schema to look into.
*
* @return <code>true</code> if <code>name</code> is present as java name of a table from
* the specified <code>schema</code> and <code>false</code> if <code>schema</code>
* does not exist or it does not contain such interface.
*
* @throws SchemaException
* if there is an error loading schema names from configuration.
*/
public boolean isDMOInterface(String name, String schema)
throws SchemaException
{
return P2OLookup.isDMOInterface(name, schema);
}
/**
* Report whether the given, historical table will be mapped in the
* converted application to a database table which represents a foreign
* reference of another table's foreign key.
*
* @param historical
* Fully qualified, legacy Progress table name.
*
* @return <code>true</code> if the given table represents the foreign
* end of a one-to-one or a one-to-many relation between two
* tables.
*
* @throws SchemaException
* if there is an error loading schema names from configuration.
*/
public boolean isForeignReference(String historical)
throws SchemaException
{
return P2OLookup.isForeignReference(historical);
}
/**
* The given index in the given table has been referenced in business logic. Remove it from
* the mutable index resource map.
*
* @param ast
* Record phrase or embedded SQL AST node.
* @param index
* Legacy name of index which was referenced.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public void indexReferenced(Aast ast, String index)
throws SchemaException
{
if (Configuration.isRuntimeConfig())
{
return;
}
String table = getTableName(ast);
P2OLookup.indexReferenced(table, index);
}
/**
* The given field has been referenced in business logic. Remove it from the mutable field
* resource map.
*
* @param field
* Legacy name (normalized schemaname) of field which was referenced.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public void fieldReferenced(String field)
throws SchemaException
{
if (Configuration.isRuntimeConfig())
{
return;
}
P2OLookup.fieldReferenced(field);
}
/**
* Delete previous database resource report files, if any.
*/
public void deleteDatabaseResourceReports()
{
if (Configuration.isRuntimeConfig())
{
return;
}
P2OLookup.deleteDatabaseResourceReports();
}
/**
* Write the header for the indeterminate sorting queries report.
*/
public void writeHeaderToIndeterminateSortFile()
throws IOException
{
if (Configuration.isRuntimeConfig())
{
return;
}
P2OLookup.writeHeaderToIndeterminateSortFile();
}
/**
* Emit content (if any) for the current source file into the unused index report.
*
* @param temp
* {@code true} if content relates to temp-tables; {@code false} for permanent
* tables.
* @throws IOException
* if there is an error writing the report.
*/
public void unusedIndexReport(boolean temp)
throws IOException
{
if (Configuration.isRuntimeConfig())
{
return;
}
P2OLookup.unusedIndexReport(temp);
}
/**
* Emit content (if any) for the current source file into the unused field report.
*
* @param temp
* {@code true} if content relates to temp-tables; {@code false} for permanent
* tables.
* @throws IOException
* if there is an error writing the report.
*/
public void unusedFieldReport(boolean temp)
throws IOException
{
if (Configuration.isRuntimeConfig())
{
return;
}
P2OLookup.unusedFieldReport(temp);
}
/**
* Store information about queries with suspicious index selection.
*
* @param ast
* Record phrase or embedded SQL AST.
* @param comment
* The comment to store.
*/
public void unindexedQuery(Aast ast, String comment)
throws SchemaException
{
if (Configuration.isRuntimeConfig())
{
return;
}
context.get().suspiciousQueries.add(new QueryIndexInfo(ast, comment, null, 0));
}
/**
* Emit content (if any) for the current source file into the unindexed query report.
*
* @throws IOException
* if there is an error writing the report.
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public void unindexedQueryReport()
throws IOException,
SchemaException
{
WorkArea wa = context.get();
if (Configuration.isRuntimeConfig() || wa.suspiciousQueries.isEmpty())
{
return;
}
String sep = System.lineSeparator();
FileWriter fw = new FileWriter(P2OLookup.UNINDEXED_QUERIES_FILE, true);
try (BufferedWriter bw = new BufferedWriter(fw))
{
boolean writeFilename = true;
for (QueryIndexInfo qii : wa.suspiciousQueries)
{
qii.writeContent(bw, writeFilename);
writeFilename = false;
}
bw.write(sep);
}
}
/**
* Check if the chosen legacy name criteria leads to an indeterminate sorting situation.
* In order to do this, check if the criteria set contains the fields of at least one unique index.
*
* @param ast
* Record phrase or embedded SQL AST.
* @param legacyNames
* A set with the legacy name criteria.
*
* @return {@code 0} if the query has a deterministic sort,
* {@code 1} if the query is nearly deterministic (i.e. there is an unique index that has
* overlapping fields with the legacy name criteria, but the index fields are not all
* mandatory),
* {@code 2} if the query has an indeterminate sort (i.e. no unique index overlaps with the
* legacy name criteria).
*/
public int isIndeterminateSortQuery(Aast ast, Set<String> legacyNames)
throws SchemaException
{
if (Configuration.isRuntimeConfig())
{
return 0;
}
String tableName = getTableName(ast);
Iterator<P2JIndex> indexes = P2OLookup.indexes(tableName);
if (!indexes.hasNext())
{
return 0;
}
boolean foundNonMandatoryOverlappingUniqueIndex = false;
while (indexes.hasNext())
{
P2JIndex index = indexes.next();
if (index.isUnique() && checkFieldMatching(index, legacyNames))
{
if (areIndexFieldsMandatory(index))
{
return 0;
}
else
{
foundNonMandatoryOverlappingUniqueIndex = true;
}
}
}
return foundNonMandatoryOverlappingUniqueIndex ? 1 : 2;
}
/**
* Store information about queries with an indeterminate sorting order.
*
* @param ast
* Record phrase or embedded SQL AST.
* @param indexName
* The name of the chosen index.
* @param risk
* The risk factor associated with the query.
*/
public void indeterminateSortQuery(Aast ast, String indexName, int risk)
throws SchemaException
{
if (Configuration.isRuntimeConfig())
{
return;
}
context.get().indeterminateSortQueries.add(new QueryIndexInfo(ast, null, indexName, risk));
}
/**
* Generate a report in a CSV file about the queries with an indeterminate sorting order.
*/
public void indeterminateSortQueryReport()
throws IOException,
SchemaException
{
WorkArea wa = context.get();
if (Configuration.isRuntimeConfig() || wa.indeterminateSortQueries.isEmpty())
{
return;
}
FileWriter fw = new FileWriter(P2OLookup.INDETERMINATE_SORT_QUERIES_FILE, true);
try (BufferedWriter bw = new BufferedWriter(fw))
{
for (QueryIndexInfo qii : wa.indeterminateSortQueries)
{
qii.writeIndeterminateSortQueryContent(bw);
}
bw.close();
}
}
/**
* Check if the fields of the given index are part of the legacy name criteria.
*
* @param index
* The index for which to checks its fields.
* @param legacyNames
* A set with the legacy name criteria.
*
* @return {@code true} if the fields of the index are part of the legacy name criteria set,
* {@code false} otherwise.
*/
private boolean checkFieldMatching(P2JIndex index, Set<String> legacyNames)
{
Iterator<P2JIndexComponent> components = index.componentsIter();
while (components.hasNext())
{
String legacyName = components.next().getNormalizedName();
if (legacyName == null || !legacyNames.contains(legacyName))
{
return false;
}
}
return true;
}
/**
* Check if all the fields from an index are mandatory. A field is mandatory if it is marked as
* non-nullable.
*
* @param index
* The index for which to check the fields.
*
* @return {@code true} if all the fields are mandatory, {@code false} otherwise.
*/
private boolean areIndexFieldsMandatory(P2JIndex index)
throws SchemaException
{
Iterator<P2JIndexComponent> components = index.componentsIter();
while (components.hasNext())
{
String legacyName = components.next().getLegacyName();
if (isNullable(legacyName))
{
return false;
}
}
return true;
}
}
/**
* Helper class which keeps track of queries with suspicious index selection, and which
* writes content to the unindexed query report.
*/
private static class QueryIndexInfo
{
/** The source file in which the query was found. */
private final String sourceFile;
/** The table against which the query is executed. */
private final String tableName;
/** Suspicious index selection comment. */
private final String comment;
/** The name of the index. */
private final String indexName;
/** The risk factor of the query. */
private final int risk;
/** The start and end lines of the query from the source file. */
private final int bounds[];
/**
* Constructor.
*
* @param ast
* Record phrase or embedded SQL AST.
* @param comment
* Suspicious index selection comment.
* @param indexName
* The name of the index.
* @param risk
* The risk factor of the query.
*/
QueryIndexInfo(Aast ast, String comment, String indexName, int risk)
throws SchemaException
{
this.sourceFile = ast.getFilename();
this.tableName = getTableName(ast);
this.comment = comment;
this.indexName = indexName;
this.risk = risk;
this.bounds = computeStartAndEndLine(ast);
}
/**
* Write content about this query's index selection to the report, including a code
* snippet of the query.
* <p>
* TODO: make the source file reading more efficient; if there are multiple queries from
* the same file, we re-read a lot of lines unnecessarily.
*
* @param bw
* Report writer.
* @param writeFilename
* Flag that denotes if the filename should be written.
*
* @throws IOException
* if there is an error reading from the source file or writing to the report
* file.
* @throws SchemaException
* if there is an error inspecting the AST for information.
*/
void writeContent(Writer bw, boolean writeFilename)
throws IOException,
SchemaException
{
String cacheFile = sourceFile + ".cache";
String sep = System.lineSeparator();
String dsep = sep + sep;
String pad = " ";
String dpad = pad + pad;
if (writeFilename)
{
bw.write(cacheFile + ':' + dsep);
}
// emit section heading with table name and index selection comment
bw.write(pad + '(' + tableName + " - " + comment + ')' + dsep);
// read the relevant source lines from the cache file and emit them into the report
try (BufferedReader br = new BufferedReader(new FileReader(cacheFile)))
{
for (int i = 1; i <= bounds[1]; i++)
{
String line = br.readLine();
if (i >= bounds[0])
{
String fmtLine = String.format("%05d %s", i, line);
bw.write(dpad + fmtLine + sep);
}
}
}
bw.write(sep);
}
/**
* Write content about a query that has an indeterminate sorting order. This includes writing the
* source file name, the source file line number, the query, the table that is accessed and the index
* that has been chosen.
*
* @param bw
* Report writer.
*/
void writeIndeterminateSortQueryContent(Writer bw)
throws IOException,
SchemaException
{
String cacheFile = sourceFile + ".cache";
String sep = System.lineSeparator();
try (BufferedReader br = new BufferedReader(new FileReader(cacheFile)))
{
String query = new String();
int startingLine = -1;
for (int i = 1; i <= bounds[1]; i++)
{
String line = br.readLine();
if (line == null)
{
continue;
}
if (i >= bounds[0])
{
query += line.trim() + ' ';
if (startingLine == -1)
{
startingLine = i;
}
}
}
String fmtQuery = "\"" + query.replaceAll("\"", "\"\"") + "\"";
String fmtRisk;
switch (risk)
{
case 1:
fmtRisk = "Low";
break;
case 2:
fmtRisk = "Moderate";
break;
default:
fmtRisk = "None";
}
String fmtLine = String.format("%s, %05d, %s, %s, \"%s\", %s",
cacheFile,
startingLine,
tableName,
indexName,
fmtRisk,
fmtQuery);
bw.write(fmtLine + sep);
}
}
/**
* Compute the start and end line of the source file.
*
* @param ast
* The referenced record phrase or embedded SQL AST.
*
* @return An array where the first element is the start line, and the second element is the end line.
*/
private int[] computeStartAndEndLine(Aast ast)
{
int start;
Aast next = ast;
switch (ast.getType())
{
case ProgressParserTokenTypes.RECORD_PHRASE:
// record_phrase has no line number; walk up the tree to find nearest enclosing
// node with a line number
do
{
start = next.getLine();
next = next.getParent();
} while (start == 0 && next != null);
break;
case ProgressParserTokenTypes.EMBEDDED_SQL:
// embedded_sql has no line number, but its first child does
start = ast.getChildAt(0).getLine();
break;
default:
// we shouldn't encounter any other token types at this point
start = 0;
break;
}
if (start == 0)
{
// TODO: raise error?
return new int[] {0, 0};
}
// determine the appropriate source file end line
int end = start;
Iterator<Aast> iter = ast.iterator();
while (iter.hasNext())
{
next = iter.next();
end = Math.max(end, next.getLine());
}
return new int[] {start, end};
}
}
/**
* Context local work area.
*/
private static class WorkArea
{
/** List of objects describing potentially suspicious query index selection */
private final List<QueryIndexInfo> suspiciousQueries = new ArrayList<>();
/** List of queries that produce an indeterminate order of the records when executed. */
private final List<QueryIndexInfo> indeterminateSortQueries = new ArrayList<>();
}
}