ReportApi.java
/*
** Module : ReportApi.java
** Abstract : Server API for base reporting services of source code analytics web application.
**
** Copyright (c) 2017-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------------Description---------------------------------------
** 001 ECF 20170428 Created first version.
** 002 CA 20181003 Added flow chart support (for any top-level block).
** 003 ECF 20181117 Improved performance of standard summary report query.
** 004 ECF 20190523 Fixed typo.
** 005 ECF 20200929 Replaced report_cell column name ("row" apparently became a reserved work in the latest
** version of H2).
** OM 20220405 XmlFilePlugin can load either a file or a stream from application's jar.
** DDF 20220823 Replaced .nextvalue with next value for.
** RAA 20230109 Changed inline statement(s) to prepared statement(s).
** 006 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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.report.server;
import java.security.*;
import java.security.spec.*;
import java.sql.*;
import java.sql.Date;
import java.util.*;
import java.util.logging.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.pattern.*;
import com.goldencode.p2j.report.*;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;
/**
* This class provides the server-side API for all base reporting services which support the FWD
* Analytics web application. Methods in this class marked with the {@link WebApi} annotation
* represent API entry points, using a remote procedure call model. These methods can return
* simple Java objects with a bean interface, or lists, maps, or arrays of such objects. Each API
* must be annotated with a unique message type via its {@code WebApi} annotation; this type must
* match the message type used by the client application when invoking the API call.
* <p>
* This class must be registered with the server before an API call is first made by the client.
* This is done with a static initializer in the {@link ReportWebServer} class.
*/
public class ReportApi
implements ReportConstants
{
/** Base message type for all report-specific APIs */
private static final int MSG_BASE = ReportProtocol.MSG_BASE + 1000;
/** Base message type for all filter-specific APIs */
private static final int MSG_FILT_BASE = ReportProtocol.MSG_BASE + 4000;
/** API to retrieve master code report information */
private static final int MSG_MST_CODE_RPT = MSG_BASE + 1;
/** API to retrieve master schema report information */
private static final int MSG_MST_SCHEMA_RPT = MSG_BASE + 2;
/** API to retrieve a particular summary report */
private static final int MSG_SUMMARY_RPT = MSG_BASE + 3;
/** API to retrieve a particular detail report */
private static final int MSG_DETAIL_RPT = MSG_BASE + 4;
/** API to retrieve source code and AST data for a specific source file */
private static final int MSG_SOURCE_DATA = MSG_BASE + 5;
/** API to define a new or update an existing report */
private static final int MSG_DEF_RPT = MSG_BASE + 6;
/** API to delete an existing report */
private static final int MSG_DEL_RPT = MSG_BASE + 7;
/** API to run a single report incrementally */
private static final int MSG_RUN_RPT = MSG_BASE + 8;
/** API to run search */
private static final int MSG_SEARCH = MSG_BASE + 9;
/** API to retrieve search history for current user */
private static final int MSG_SEARCH_HISTORY = MSG_BASE + 10;
/** Message ID for reading the control flow for a top-level block. */
private static final int MSG_FLOW_CHART = MSG_BASE + 11;
/** API to retrieve all file filter profile data and the ID of the active filter */
private static final int MSG_FILTER_INFO = MSG_FILT_BASE + 1;
/** API to set the active file filter profile */
private static final int MSG_SET_FILTER = MSG_FILT_BASE + 2;
/** API to retrieve all files specifications for the target filter */
private static final int MSG_FILTER_SPEC = MSG_FILT_BASE + 3;
/** API to retrieve the qualified names of all source files */
private static final int MSG_ALL_SOURCE_FILES = MSG_FILT_BASE + 4;
/** API to define a new file filter profile */
private static final int MSG_DEF_FILTER = MSG_FILT_BASE + 5;
/** API to delete a file filter profile */
private static final int MSG_DEL_FILTER = MSG_FILT_BASE + 6;
/** SQL query to retrieve a user by name */
private static final String sqlQryUser =
"select id, salt, pass, admin, pass_changed from user where name = ?";
/** SQL query to get ID of default file filter */
private static final String sqlQryDefFileFilter =
"select id from filter_profile where default = true";
/** Delete a filter profile */
private static final String sqlDelFileFilter =
"delete from filter_profile where id = ? and not default";
/** SQL query to gather user reports of a certain type with their associated tags */
private static final String sqlQryUserRptDefs =
"select " +
"(select group_concat(t.name order by t.id separator ',') " +
"from tag t join report_tag rt on rt.tid = t.id where rt.rid = r.id) as tags, " +
"r.id, r.title, r.condition, r.multiplex, " +
"r.dumptype, r.dumpexpr, r.dumplevel, r.support " +
"from report r where r.user = true and r.type = ? order by r.id";
private static final String sqlQryAllUsers =
"select name, admin, salt, pass, pass_changed from user order by id";
private static final String sqlQryNonDefFlt =
"select p.id, p.name, f.spec, f.wild from filter_profile p " +
"join file_filter f on f.pid = p.id where not p.default order by p.id, f.spec";
private static final String sqlUsrSrchHist =
"select s.id, s.criteria, u.name from search_history s " +
"join user_search us on us.sid = s.id " +
"join user u on u.id = us.uid " +
"order by s.id, u.id";
/** SQL query for master code report using aggregate roll-ups */
private static final String sqlQryMasterAgg =
"select rid, title, matches, files, tag, support, conddesc from " +
"((select t.id as tid, r.id as rid, r.support as support, " +
"t.name as tag, r.title as title, r.conddesc as conddesc, " +
"count(a.fid) as matches, count(distinct(a.fid)) as files " +
"from report r " +
"join report_tag rt on rt.rid = r.id " +
"join tag t on t.id = rt.tid " +
"left outer join category c on r.id = c.rid " +
"left outer join match m on m.cid = c.id " +
"left outer join active_file a on a.fid = m.fid and a.sid = ? " +
"where r.type = ? and not r.custom " +
"group by r.title, t.name, r.id, r.conddesc) " +
"union " +
"(select t.id as tid, r.id as rid, r.support as support, " +
"t.name as tag, r.title as title, r.conddesc as conddesc, " +
"s.matches as matches, s.files as files " +
"from report r " +
"join report_stats s on s.rid = r.id " +
"join report_tag rt on rt.rid = r.id " +
"join tag t on t.id = rt.tid " +
"where r.type = ? and r.custom)) " +
"order by tid, rid";
/** SQL query for master code report using cached statistics, including all source files */
private static final String sqlQryMasterAll =
"select r.id, r.title, s.matches, s.files, t.name, r.support, r.conddesc " +
"from report r " +
"join report_stats s on s.rid = r.id " +
"join report_tag rt on rt.rid = s.rid " +
"join tag t on t.id = rt.tid " +
"where r.type = ? " +
"order by t.id, r.id";
/** SQL query for a particular custom report's column (meta) data */
private static final String sqlQryCustCols =
"select heading, format, sorting from report_column where rid = ? order by idx";
/** SQL query for a particular custom report's cell data */
private static final String sqlQryCustCells =
"select col.idx, cell.text, cell.num from report_column col " +
"join report_cell cell on cell.rcid = col.id " +
"where col.rid = ? " +
"order by cell.rowpos, col.idx";
/** SQL query for a standard summary report */
private static final String sqlQrySummaryStd =
"select c.id as cid, c.name as condition, r.multiplex, " +
"bitand(c.lvl, 0x001f), bitand(c.lvl, 0x7f00), " +
"count(m.fid) as matches, " +
"count(m.fid) * 100 / cast(? as double), " +
"count(distinct(m.fid)) as files, " +
"count(distinct(m.fid)) * 100 / cast(? as double) " +
"from category c " +
"join report r on r.id = c.rid " +
"left outer join match m on m.cid = c.id " +
"where c.rid = ? and exists(select 1 from active_file where fid = m.fid and sid = ?)" +
"group by c.id " +
"order by matches desc";
/** SQL query for a detail report */
private static final String sqlQryDetail =
"select m.id, f.id, am.astid, f.name, m.line, m.col, m.text " +
"from active_file a " +
"join ast_map am on am.fid = a.fid and am.type = ? " +
"join file f on f.id = a.fid " +
"join match m on m.fid = f.id " +
"where m.cid = ? and a.sid = ? " +
"order by f.name, m.line, m.col";
/** SQL query for all source lines (except marker line 0) for a particular source file */
private static final String sqlQrySourceLines =
"select line, data from source_line where fid = ? and line > 0 and bitand(?, type) <> 0 " +
"order by line";
/** SQL query to get AST ID associated with a file id and source type */
private static final String sqlQryAstId =
"select astid from ast_map where fid = ? and type = ?";
/** Query the ID of a file record with the given name. */
private static final String sqlQryFileID = "select id from file where name = ?";
/** Query all filter profiles */
private static final String sqlQryFilters =
"select id, default, name from filter_profile";
/** Query list of files specifications for the target filter*/
private static final String sqlQryFilterSpec =
"select wild, spec from file_filter where pid = ?";
/** Query list of all source file names */
private static final String sqlQryAllSrcFiles = "select name from file order by id";
/** Query a report definition */
private static final String sqlQryRptDef =
"select type, title, condition, conddesc, multiplex, " +
"dumptype, dumpexpr, dumplevel, support " +
"from report where id = ?";
/** Query all source file names of a certain type */
private static final String sqlQryFiles =
"select f.name from file f join ast_map a on a.fid = f.id where type = ?";
/** Query all source file names in the specified filter with WILD mark. */
private static final String sqlQryFltWildFiles =
"select f.name from file f where name like ?";
/** Query all source file names in the specified filter with non-WILD mark. */
private static final String sqlQryFltExactFiles =
"select f.name from file f where name = ?";
/** Query all source file names of a certain type, applying active filter */
private static final String sqlQryFltFiles =
"select f.name from file f " +
"join ast_map a on a.fid = f.id " +
"join active_file af on af.fid = f.id and af.sid = ? " +
"where type = ?";
/** Query all source file ids and names of a certain type */
private static final String sqlQryFidsNames =
"select f.id, f.name from file f join ast_map a on a.fid = f.id where type = ?";
/** Delete a single report */
private static final String sqlDelUsrRpt = "delete from report where id = ?";
/** Delete results for a single report */
private static final String sqlDelRptResults = "delete from category where rid = ?";
/** Update the user table with a new salt, password, and password changed date */
private static final String sqlUsrPassChg =
"update user set salt = ?, pass = ?, pass_changed = today where id = ?";
/** Query search history for the current user */
private static final String sqlQrySrchHist =
"select s.criteria from user_search u join search_history s on s.id = u.sid " +
"where u.uid = ? order by s.id desc";
/** Object used to load ASTs from the file system */
private static final AstManager astManager;
/** Database services helper object */
private static final DatabaseService dbs = DatabaseService.get();
/** Default file filter ID to include all files */
private static final long defaultFilterId;
/** Logger */
private static final ConversionStatus LOG = ConversionStatus.get(ReportApi.class);
static
{
try
{
// initialize AST manager to read AST files
String name = Configuration.getParameter("registry");
AstManager.initialize(new XmlFilePlugin(Configuration.forceHome(name), false));
astManager = AstManager.get();
// query default file filter profile ID
try (Connection conn = dbs.openConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sqlQryDefFileFilter);)
{
if (rs.next())
{
defaultFilterId = rs.getLong(1);
}
else
{
throw new RuntimeException("Unable to query default file filter profile!");
}
}
}
catch (AstException | SQLException exc)
{
LOG.log(Level.WARNING,"", exc);
throw new RuntimeException(exc);
}
}
/** Cache of master report results by report/file type, using the current filter profile */
private final Map<Integer, OverviewRow[]> cachedMasterResults = new HashMap<>();
/** Current session ID */
private long sessionId = -1;
/** Authenticated user */
private User user = null;
/** Active file filter ID */
private long filterId = defaultFilterId;
/**
* Default constructor.
*/
public ReportApi()
{
}
/**
* List all files in this filter.
*
* @param id
* The ID of a filter profile..
*
* @return All the files represented by this profile.
*/
static List<String> listFilesFromProfile(long id)
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQryFilterSpec))
{
List<String> list = new ArrayList<>();
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
while (rs.next())
{
FileFilter fileSpec = new FileFilter();
fileSpec.setWild(rs.getBoolean(1));
fileSpec.setSpec(rs.getString(2));
String fname = fileSpec.getSpec();
String sql = fileSpec.isWild() ? sqlQryFltWildFiles : sqlQryFltExactFiles;
if (fileSpec.isWild() && fname.endsWith("*"))
{
fname = fname.substring(0, fname.length() - 1) + "%";
}
try (PreparedStatement ps2 = conn.prepareStatement(sql))
{
ps2.setString(1, fname);
ResultSet rs2 = ps2.executeQuery();
while (rs2.next())
{
fname = rs2.getString(1);
list.add(fname);
}
}
}
conn.commit();
return list;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Get the record ID for the given file in the report database.
*
* @param filename
* The filename (as in the FWD registry.xml).
*
* @return the file record ID.
*/
static int getFileId(String filename)
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQryFileID))
{
ps.setString(1, filename);
ResultSet rs = ps.executeQuery();
if (rs.next())
{
return rs.getInt(1);
}
throw new RuntimeException("Can't locate fid for " + filename);
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Get a list of all user-defined reports of the given type.
*
* @param srcType
* Type of report (schema or code).
*
* @return List of {@link ReportDefinition} objects of the given type; may be empty, but will
* not be {@code null}.
*/
static List<ReportDefinition> getUserReports(int srcType)
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQryUserRptDefs))
{
List<ReportDefinition> reports = new ArrayList<>();
ps.setInt(1, srcType);
ResultSet rs = ps.executeQuery();
while (rs.next())
{
ReportDefinition rpt = new ReportDefinition();
rpt.user = true;
rpt.setTagList(rs.getString(1));
rpt.id = rs.getLong(2);
rpt.title = rs.getString(3);
rpt.condition = rs.getString(4);
rpt.multiplexExpr = rs.getString(5);
rpt.dumpType = rs.getString(6);
rpt.dumpExpr = rs.getString(7);
rpt.dumpLevel = rs.getInt(8);
rpt.supportLvlExpr = rs.getString(9);
reports.add(rpt);
}
conn.commit();
return reports;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Retrieve a list of {@link UserRecord} records, each of which holds the details of a user.
*
* @return List of user records.
*/
static List<UserRecord> getUserRecords()
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQryAllUsers))
{
List<UserRecord> list = new ArrayList<>();
ResultSet rs = ps.executeQuery();
while (rs.next())
{
String name = rs.getString(1);
boolean admin = rs.getBoolean(2);
byte[] salt = rs.getBytes(3);
byte[] pass = rs.getBytes(4);
Date passChanged = rs.getDate(5);
UserRecord user = new UserRecord(name, admin, salt, pass, passChanged);
list.add(user);
}
conn.commit();
return list;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Retrieve a list of all non-default {@link FilterProfile} records, including their file
* filter specifications.
*
* @return List of non-default filter profiles.
*/
static List<FilterProfile> getNonDefaultFilters()
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQryNonDefFlt))
{
List<FilterProfile> list = new ArrayList<>();
FilterProfile profile = null;
ResultSet rs = ps.executeQuery();
while (rs.next())
{
long pid = rs.getLong(1);
// add new FilterProfile object to list when profile ID changes; this works
// because the query returns records first in profile ID order
if (profile == null || pid != profile.getId())
{
String name = rs.getString(2);
profile = new FilterProfile();
profile.setId(pid);
profile.setName(name);
profile.setDefault(false);
list.add(profile);
}
// add a file filter to the profile object
String spec = rs.getString(3);
boolean wild = rs.getBoolean(4);
FileFilter filter = new FileFilter();
filter.setSpec(spec);
filter.setWild(wild);
profile.addFilter(filter);
}
conn.commit();
return list;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Retrieve a list of {@link SearchHistory} records, each of which holds a search history
* record's ID and criteria expression, and one or more user names.
*
* @return List of search history records.
*/
static List<SearchHistory> getUserSearchHistory()
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlUsrSrchHist))
{
List<SearchHistory> list = new ArrayList<>();
SearchHistory hist = null;
ResultSet rs = ps.executeQuery();
while (rs.next())
{
long sid = rs.getLong(1);
// add new SearchHistory object to list when search ID changes; this works
// because the query returns records first in search ID order
if (hist == null || sid != hist.getSearchId())
{
String crit = rs.getString(2);
hist = new SearchHistory(sid, crit);
list.add(hist);
}
// add a user to the history object
String name = rs.getString(3);
hist.addUser(name);
}
conn.commit();
return list;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Explicitly shut down the database. This is necessary because we use the {@code
* DB_CLOSE_ON_EXIT=FALSE} database option to enable our auto-export shutdown hook to run.
*
* @throws RuntimeException
* if there is an SQL error during shutdown.
*/
static void shutdownDatabase()
{
try
{
// no need to close connection as usual; shutdown will close all resources
dbs.openConnection().createStatement().execute("shutdown");
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Apply a file filter to the active session. No other sessions are affected.
* <p>
* The filter works by storing in a global temporary table ({@code active_file}) the primary
* keys of records from the {@code file} table which match the selected filter profile. All
* regular reports then join their results on the {@code active_file} table to select only
* those results which match the set of filtered files for the current session.
*
* @param conn
* Database connection.
* @param profileId
* ID of the filter profile to apply, or {@code null} to apply the default filter.
*/
private static void applyFileFilter(Connection conn, Long profileId, long sessionId)
{
PreparedStatement stmt = null;
ResultSet rs = null;
try
{
String sql;
if (profileId != null)
{
// if profileId is null, the active_file was just created and is already empty;
// otherwise, delete all its records in preparation for re-population with the new
// filter profile's data
stmt = conn.prepareStatement("delete from active_file where sid = ?");
stmt.setObject(1, sessionId);
stmt.executeUpdate();
}
// Query file specs and types for the provided file filter profile; we will compose a
// compound (union'd) query from this to populate the active_file table.
// If provided profile id was null, get the default profile.
int i;
int type = 0;
// wildcard path specs first
if (profileId == null)
{
sql = "select f.spec from file_filter f " +
"join filter_profile p on p.id = f.pid " +
"where p.default = true and f.wild = true";
}
else
{
sql = "select f.spec from file_filter f " +
"join filter_profile p on p.id = f.pid " +
"where p.id = ?" + " and f.wild = true";
type = 1;
}
if (stmt != null && !stmt.isClosed())
{
stmt.close();
}
stmt = conn.prepareStatement(sql);
if (type == 1)
{
stmt.setObject(1, profileId);
}
type = 0;
rs = stmt.executeQuery();
List<Object> args = new ArrayList<>();
StringBuilder buf = new StringBuilder("insert into active_file (fid, sid) ");
for (i = 0; rs.next(); i++)
{
String spec = rs.getString(1);
if (i > 0)
{
buf.append(" union ");
}
buf.append("select distinct(f.id), ");
buf.append(sessionId);
buf.append(" from file f join ast_map a on a.fid = f.id where f.name like ?");
String arg1 = dbs.escapeLikeRValue(spec);
arg1 = arg1.substring(0, arg1.length() - 1) + '%';
args.add(arg1);
}
if (profileId == null)
{
sql = "select f.spec from file_filter f " +
"join filter_profile p on p.id = f.pid " +
"where p.default = true and f.wild = false";
}
else
{
sql = "select f.spec from file_filter f " +
"join filter_profile p on p.id = f.pid " +
"where p.id = ?" + " and f.wild = false";
type = 1;
}
if (rs != null && !rs.isClosed())
{
rs.close();
}
if (stmt != null && !stmt.isClosed())
{
stmt.close();
}
stmt = conn.prepareStatement(sql);
if (type == 1)
{
stmt.setObject(1, profileId);
}
type = 0;
rs = stmt.executeQuery();
int j = 0;
for (; rs.next(); i++)
{
String spec = rs.getString(1);
if (i > 0 && j == 0)
{
buf.append(" union ");
}
if (j == 0)
{
buf.append("select distinct(id), ");
buf.append(sessionId);
buf.append(" from file where name in (");
}
else
{
buf.append(", ");
}
args.add(spec);
if (++j == 100)
{
j = 0;
buf.append(')');
}
}
if (j > 0)
{
buf.append(')');
}
sql = buf.toString();
if (rs != null && !rs.isClosed())
{
rs.close();
}
if (stmt != null && !stmt.isClosed())
{
stmt.close();
}
stmt = conn.prepareStatement(sql);
for (int k = 0; k < args.size();k++)
{
stmt.setObject(k + 1, args.get(k));
}
stmt.execute();
int uc = stmt.getUpdateCount();
System.out.println("applyActiveFile update count: " + uc);
conn.commit();
}
catch (SQLException exc)
{
if (conn != null)
{
try
{
conn.rollback();
}
catch (SQLException sqle)
{
LOG.log(Level.WARNING,"", exc);
}
}
throw new RuntimeException(exc);
}
finally
{
try
{
if (rs != null && !rs.isClosed())
{
rs.close();
}
if (stmt != null && !stmt.isClosed())
{
stmt.close();
}
}
catch (SQLException e)
{
LOG.log(Level.SEVERE, "", e);
}
}
}
/**
* Query and return information necessary to produce the master report of overview results
* for all code summary reports.
*
* @return Array of {@link OverviewRow} objects, each providing the high level match
* statistics for its associated summary report.
*/
@WebApi(type = MSG_MST_CODE_RPT)
public OverviewRow[] masterCodeReport()
{
return masterReport(SRC_CODE_CACHE);
}
/**
* Query and return information necessary to produce the master report of overview results
* for all schema summary reports.
*
* @return Array of {@link OverviewRow} objects, each providing the high level match
* statistics for its associated summary report.
*/
@WebApi(type = MSG_MST_SCHEMA_RPT)
public OverviewRow[] masterSchemaReport()
{
return masterReport(SRC_SCHEMA);
}
/**
* Retrieve information necessary to display a particular summary report.
*
* @param reportId
* ID of the summary report for which information is requested.
* @param totalMatches
* Total number of matches for this report. Used to calculate match percentages for
* individual match categories.
* @param totalFiles
* Total number of files for this report. Used to calculate file percentages for
* individual match categories.
*
* @return An array of two objects; the first is a boolean which is {@code true} if the
* report represents a custom report, else {@code false}; the second represents the
* report results, either in the form of a {@link CustomReport} object (for a custom
* report) or an array of {@link SummaryRow} objects (for a standard report).
*/
@WebApi(type = MSG_SUMMARY_RPT)
public Object[] summaryReport(long reportId, int totalMatches, int totalFiles)
{
Object[] result = new Object[2];
try (Connection conn = dbs.openConnection())
{
try
{
CustomReport customReport = null;
// try to query custom column information for this report
try (PreparedStatement ps = conn.prepareStatement(sqlQryCustCols))
{
ps.setLong(1, reportId);
ResultSet rs = ps.executeQuery();
List<CustomColumn> list = new ArrayList<>();
while (rs.next())
{
if (customReport == null)
{
customReport = new CustomReport();
customReport.setColDefs(list);
}
CustomColumn col = new CustomColumn();
col.setHeading(rs.getString(1));
col.setFormat(rs.getString(2));
col.setSorting(rs.getInt(3));
list.add(col);
}
}
if (customReport != null)
{
try (PreparedStatement ps = conn.prepareStatement(sqlQryCustCells))
{
ps.setLong(1, reportId);
ResultSet rs = ps.executeQuery();
List<Map<String, Object>> rows = new ArrayList<>();
Map<String, Object> row = null;
while (rs.next())
{
int idx = rs.getInt(1);
if (idx == 0)
{
row = new HashMap<>();
rows.add(row);
}
String text = rs.getString(2);
double num = rs.getDouble(3);
row.put(String.valueOf(idx), rs.wasNull() ? text : num);
}
conn.commit();
customReport.setRows(rows);
result[1] = customReport;
}
}
else
{
List<SummaryRow> list = new ArrayList<>();
try (PreparedStatement ps = conn.prepareStatement(sqlQrySummaryStd))
{
ps.setDouble(1, totalMatches);
ps.setDouble(2, totalFiles);
ps.setLong(3, reportId);
ps.setLong(4, sessionId);
ResultSet rs = ps.executeQuery();
while (rs.next())
{
SummaryRow row = new SummaryRow();
row.setCid(rs.getInt(1));
row.setCat(rs.getString(2));
row.setMplex(rs.getString(3));
row.setCLvl(rs.getInt(4));
row.setRLvl(rs.getInt(5));
row.setMCnt(rs.getInt(6));
row.setMPct(rs.getDouble(7));
row.setFCnt(rs.getInt(8));
row.setFPct(rs.getDouble(9));
list.add(row);
}
conn.commit();
}
SummaryRow[] rows = list.toArray(new SummaryRow[0]);
result[1] = rows;
}
result[0] = (customReport != null);
return result;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Get a list of {@link DetailRow} objects which represent the rows of a detail report for
* the specified match category of a report of the given type.
*
* @param categoryId
* Unique identifier of a category of detailed report results.
* @param astType
* Type of ASTs to retrieve (schema or code).
*
* @return List of detail report row objects.
*/
@WebApi(type = MSG_DETAIL_RPT)
public List<DetailRow> detailReport(long categoryId, int astType)
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQryDetail))
{
List<DetailRow> list = new ArrayList<>();
ps.setInt(1, astType);
ps.setLong(2, categoryId);
ps.setLong(3, sessionId);
ResultSet rs = ps.executeQuery();
while (rs.next())
{
DetailRow row = new DetailRow();
row.setId(rs.getLong(1));
row.setFid(rs.getLong(2));
row.setAstid(rs.getLong(3));
row.setFile(rs.getString(4));
row.setLine(rs.getInt(5));
row.setCol(rs.getInt(6));
row.setText(rs.getString(7));
list.add(row);
}
conn.commit();
return list;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Get the source code lines and AST nodes for a particular source file.
*
* @param fileId
* File ID for the source file.
* @param astId
* ID of the root node of the source file's AST. If {@code astId == 0}, an additional
* query is executed to determine the AST ID for the given file id and source type.
* If {@code astId < 0}, AST node data is not returned.
* @param sourceType
* Type of source file ({@code SRC_CODE_CACHE}, {@code SRC_CODE_BASE}, or {@code
* SRC_SCHEMA}).
*
* @return An array of two objects. The first is an array list of source code lines; the
* second is an array list of AST nodes, or {@code null}.
*/
@WebApi(type = MSG_SOURCE_DATA)
public Object[] getSourceData(long fileId, long astId, int sourceType)
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQrySourceLines))
{
List<SourceLine> sourceLines = new ArrayList<>();
ps.setLong(1, fileId);
ps.setInt(2, sourceType);
ResultSet rs = ps.executeQuery();
while (rs.next())
{
SourceLine src = new SourceLine();
src.setLine(rs.getInt(1));
src.setText(rs.getString(2));
sourceLines.add(src);
}
// if provided AST ID was 0, query the real ID using the other info we have
if (astId == 0L)
{
try (PreparedStatement ps2 = conn.prepareStatement(sqlQryAstId))
{
ps2.setLong(1, fileId);
ps2.setInt(2, sourceType);
rs = ps2.executeQuery();
if (rs.next())
{
astId = rs.getLong(1);
}
}
}
conn.commit();
List<AstNode> nodes = null;
// get the AST if we have a (presumably) valid ID
if (astId > 0)
{
String fileName = astManager.getTreeName(astId);
if (fileName != null)
{
if (sourceType == SRC_CODE_CACHE)
{
// code file names are mapped in the registry without an .ast extension
fileName += ".ast";
}
Aast ast = astManager.loadTree(DatabaseService.p2jHomePath + fileName);
nodes = prepareAstNodes(ast);
}
}
return new Object[] { sourceLines, nodes };
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Get all file filter profile records and the ID of the currently active profile.
*
* @return An array of two objects. The first is a list of {@link FilterProfile} objects
* representing all available filter profiles; the second is the ID of the currently
* active profile.
*/
@WebApi(type = MSG_FILTER_INFO)
public Object[] filterProfileInfo()
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQryFilters))
{
List<FilterProfile> list = new ArrayList<>();
ResultSet rs = ps.executeQuery();
while (rs.next())
{
FilterProfile profile = new FilterProfile();
profile.setId(rs.getLong(1));
profile.setDefault(rs.getBoolean(2));
profile.setName(rs.getString(3));
list.add(profile);
}
conn.commit();
return new Object[] { list, filterId };
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Set the active file filter profile.
*
* @param pid
* The identifier of the {@code filter_profile} to set.
* @param force
* {@code True} to force the filter update, even if the filter ID hasn't changed
* (presumably the filter definition has been updated).
*
* @throws RuntimeException
* if {@code pid} is not a valid profile identifier, or if there is any error setting
* the active profile.
*/
@WebApi(type = MSG_SET_FILTER)
public void setFilter(long pid, boolean force)
{
// TODO: check that pid is a valid profile ID
if (pid != this.filterId || force)
{
try (Connection conn = dbs.openConnection())
{
applyFileFilter(conn, pid, sessionId);
this.filterId = pid;
cachedMasterResults.clear();
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
}
/**
* Get all files specifications for the target filter.
*
* @param id
* The id of the target filter
*
* @return A list of {@link FileFilter} objects representing all files specifications
* for the target filter.
*/
@WebApi(type = MSG_FILTER_SPEC)
public List<FileFilter> filterSpecInfo(long id)
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQryFilterSpec))
{
List<FileFilter> list = new ArrayList<>();
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
while (rs.next())
{
FileFilter fileSpec = new FileFilter();
fileSpec.setWild(rs.getBoolean(1));
fileSpec.setSpec(rs.getString(2));
list.add(fileSpec);
}
conn.commit();
return list;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Get a list of the names of all source files.
*
* @return List of source file names.
*/
@WebApi(type = MSG_ALL_SOURCE_FILES)
public List<String> allSourceFiles()
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQryAllSrcFiles))
{
List<String> list = new ArrayList<>();
ResultSet rs = ps.executeQuery();
while (rs.next())
{
String name = rs.getString(1);
list.add(name);
}
conn.commit();
return list;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Define a new report, or update an existing definition.
* <p>
* TODO: add security check.
*
* @param definition
* Report definition.
* @param srcType
* Type of report (schema or code).
*
* @return Report ID assigned to the report.
*/
@WebApi(type = MSG_DEF_RPT)
public long defineReport(ReportDefinition definition, int srcType)
{
boolean databaseMode;
switch (srcType)
{
case SRC_SCHEMA:
databaseMode = true;
break;
case SRC_CODE_CACHE:
databaseMode = false;
break;
default:
throw new RuntimeException("Unknown source type: " + srcType);
}
try (Connection conn = dbs.openConnection())
{
try
{
ReportWorker rw = new ReportWorker(conn);
definition.user = true;
long reportId = rw.saveReport(definition, databaseMode, true);
conn.commit();
return reportId;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Define a new file filter profile, or update an existing profile.
* <p>
* TODO: add security check.
*
* @param profile
* Report definition.
* @param filters
* The filters associated with the profile.
*
* @return Report ID assigned to the profile.
*/
@WebApi(type = MSG_DEF_FILTER)
public long defineFilterProfile(FilterProfile profile, FileFilter[] filters)
{
try (Connection conn = dbs.openConnection())
{
try
{
ReportWorker rw = new ReportWorker(conn);
long profileId = rw.saveFilterProfile(profile, filters);
conn.commit();
return profileId;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Delete a file filter profile, but only if it is not the default profile.
* <p>
* TODO: synchronize; this method makes changes to the database.
* <p>
* TODO: add security check.
*
* @param profileId
* ID of the profile to delete.
*
* @return {@code True} if the profile was deleted, else {@code false}.
*/
@WebApi(type = MSG_DEL_FILTER)
public boolean deleteFilterProfile(long profileId)
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlDelFileFilter))
{
ps.setLong(1, profileId);
ps.execute();
boolean success = ps.getUpdateCount() > 0;
conn.commit();
return success;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Delete a single report definition.
* <p>
* TODO: synchronize; this method makes changes to the database.
* <p>
* TODO: add security check.
*
* @param reportId
* ID of the report to delete.
*/
@WebApi(type = MSG_DEL_RPT)
public void deleteReportDefinition(long reportId)
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlDelUsrRpt))
{
ps.setLong(1, reportId);
ps.execute();
conn.commit();
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Run a single report in incremental mode.
* <p>
* TODO: synchronize; this method makes changes to the database.
*
* @param reportId
* ID of the report to be run.
*/
@WebApi(type = MSG_RUN_RPT)
public void runReport(long reportId)
{
List<ReportDefinition> reports = new ArrayList<>();
FileList[] files = new FileList[1];
int srcType = 0;
String title;
boolean databaseMode;
ReportDefinition rpt;
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps1 = conn.prepareStatement(sqlQryRptDef);
PreparedStatement ps2 = conn.prepareStatement(sqlDelRptResults);)
{
ps1.setLong(1, reportId);
ResultSet rs = ps1.executeQuery();
if (rs.next())
{
rpt = new ReportDefinition();
srcType = rs.getInt(1);
rpt.id = reportId;
rpt.title = title = rs.getString(2);
rpt.condition = rs.getString(3);
rpt.conditionDescr = rs.getString(4);
rpt.multiplexExpr = rs.getString(5);
rpt.dumpType = rs.getString(6);
rpt.dumpExpr = rs.getString(7);
rpt.dumpLevel = rs.getInt(8);
rpt.supportLvlExpr = rs.getString(9);
reports.add(rpt);
}
else
{
throw new RuntimeException("Report not found [id: " + reportId + "]");
}
switch (srcType)
{
case SRC_SCHEMA:
databaseMode = true;
break;
case SRC_CODE_CACHE:
databaseMode = false;
break;
default:
throw new RuntimeException("Unexpected report type: " + srcType);
}
files[0] = gatherFiles(conn, false, srcType);
// clear the current results for this report
ps2.setLong(1, reportId);
ps2.execute();
ps2.clearParameters();
conn.commit();
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
finally
{
cachedMasterResults.remove(srcType);
}
Map<String, Object> vars = new HashMap<>();
vars.put("databaseMode", databaseMode);
vars.put("reports", reports);
vars.put("incremental", true);
try
{
ReportDriver.patternEngine(files, DebugLevels.MSG_STATUS, vars);
}
catch (Exception exc)
{
String msg = "Error running report '" + title + "'.";
LOG.log(Level.SEVERE, msg, exc);
throw new RuntimeException(msg, exc);
}
try (Connection conn = dbs.openConnection())
{
try
{
persistSearchHistory(conn, rpt.getCondition());
conn.commit();
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw exc;
}
}
catch (SQLException exc)
{
// this is not a fatal error, just log it
LOG.log(Level.WARNING,"", exc);
}
}
/**
* Run an ad-hoc search of the specified type using the given expression.
*
* @param criteria
* TRPL expression which must evaluate to true or false.
* @param applyFilter
* {@code True} to apply the current file filter when gathering the list of files to
* search; {@code false} to apply the search to all files.
* @param srcType
* Type of report (schema or code).
*
* @return An array of objects: the first is a list of detail rows; the second is the number
* of matches found by the search; the third is the number of files containing those
* matches.
*/
@WebApi(type = MSG_SEARCH)
public Object[] runSearch(String criteria, boolean applyFilter, int srcType)
{
FileList files = null;
int matchCount = 0;
int fileCount = 0;
try (Connection conn = dbs.openConnection())
{
try
{
files = gatherFiles(conn, applyFilter, srcType);
// create a new pattern engine, configure it and run simple_search
Map<String, Object> vars = new HashMap<>();
vars.put("criteria", "'" + criteria + "'");
vars.put("cmdline", false);
PatternEngine.setDebugLevel(DebugLevels.MSG_STATUS);
PatternEngine engine = new PatternEngine();
engine.addAstSpec(files);
engine.setReadOnly(true);
engine.setVariableInitializers(vars);
engine.run("reports/simple_search");
// results will be stored by simple_search under the id "results"
@SuppressWarnings("unchecked")
List<DetailRow> rows = (List<DetailRow>) engine.getStoredObject("results");
if (!rows.isEmpty())
{
// iterate results to extract file information we need for detail rows
Set<String> filenames = new HashSet<>();
for (DetailRow row : rows)
{
String file = row.getFile();
if (srcType == SRC_SCHEMA)
{
file = stripSchemaName(file);
}
filenames.add(file);
}
// create query to look up all file ids by name
StringBuilder buf = new StringBuilder(sqlQryFidsNames);
buf.append(" and name in('");
int i = 0;
for (String file : filenames)
{
if (i > 0)
{
buf.append("', '");
}
buf.append(file);
i++;
}
buf.append("')");
String sql = buf.toString();
Map<String, Long> fids = new HashMap<>();
// map file names to fids from the database
try (PreparedStatement ps = conn.prepareStatement(sql))
{
ps.setLong(1, srcType);
ResultSet rs = ps.executeQuery();
while (rs.next())
{
long fid = rs.getLong(1);
String name = rs.getString(2);
fids.put(name, fid);
}
}
matchCount = rows.size();
fileCount = fids.size();
// final pass to compose detail rows from collected information
for (DetailRow row : rows)
{
String file = row.getFile();
if (srcType == SRC_SCHEMA)
{
file = stripSchemaName(file);
}
row.setFid(fids.get(file));
}
}
try
{
persistSearchHistory(conn, criteria);
}
catch (SQLException exc)
{
// not fatal, just log it
LOG.log(Level.WARNING,"", exc);
}
conn.commit();
return new Object[] { rows, matchCount, fileCount };
}
catch (SQLException | AstException | ConfigurationException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Gather the search history, if any, for the current user.
*
* @return List of TRPL search expressions, or {@code null} if not tracking search history.
* An empty list may be returned.
*/
@WebApi(type = MSG_SEARCH_HISTORY)
public List<String> getSearchHistory()
{
try (Connection conn = dbs.openConnection())
{
try (PreparedStatement ps = conn.prepareStatement(sqlQrySrchHist))
{
List<String> list = new ArrayList<>();
ps.setLong(1, user.getId());
ResultSet rs = ps.executeQuery();
while (rs.next())
{
list.add(rs.getString(1));
}
conn.commit();
return list;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Compute the flow chart for the specified AST ID (referencing an entry point), using the
* specified direction (relevant for properly ordering the then/else nodes).
*
* @param astId
* The AST ID.
* @param flowDir
* The flow direction, <code>true</code> for vertical.
*
* @return The chart snippet.
*/
@WebApi(type = MSG_FLOW_CHART)
public FlowChartSnippet getFlowChart(long astId, boolean flowDir)
{
String fileName = astManager.getTreeName(astId);
if (fileName == null)
{
return null;
}
// code file names are mapped in the registry without an .ast extension
fileName += ".ast";
Aast root = astManager.loadTree(DatabaseService.p2jHomePath + fileName);
if (root == null)
{
return null;
}
// find our AST
Aast ast = null;
Iterator<Aast> iter = root.iterator();
while (iter.hasNext())
{
ast = iter.next();
Long id = ast.getId();
if (id != null && astId == id.longValue())
{
break;
}
else
{
ast = null;
}
}
if (ast == null)
{
// ast not found
return null;
}
return new FlowChart(ast, flowDir).build();
}
/**
* Authenticate the given user name and password.
*
* @param conn
* Database connection.
* @param name
* User name to authenticate.
* @param pass
* Clear text password candidate to authenticate.
*
* @return Authentication object containing the {@link User} instance which represents the
* authenticated user, if authentication was successful; {@code null} if it was not.
*
* @throws NoSuchAlgorithmException
* if the requested encryption algorithm is not available in the current environment.
* @throws InvalidKeySpecException
* if the specifications for the cryptographic key are invalid.
*/
Authentication authenticate(Connection conn, String name, String pass)
throws NoSuchAlgorithmException,
InvalidKeySpecException
{
try
{
Authentication auth = null;
try (PreparedStatement ps = conn.prepareStatement(sqlQryUser))
{
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
if (rs.next())
{
byte[] salt = rs.getBytes(2);
byte[] encPass = rs.getBytes(3);
if (new PasswordHelper().authenticate(pass, encPass, salt))
{
long id = rs.getLong(1);
boolean admin = rs.getBoolean(4);
Date pwChanged = rs.getDate(5);
User user = new User(id, name, admin);
auth = new Authentication(user, pwChanged);
}
}
}
return auth;
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Authenticate the given user name and password and log the user in if authentication
* succeeds.
*
* @param name
* User name to authenticate.
* @param pass
* Clear text password candidate to authenticate.
*
* @return Authentication object containing the {@link User} instance that will be associated
* with the current session, if authentication was successful; {@code null} if it was
* not.
*
* @throws NoSuchAlgorithmException
* if the requested encryption algorithm is not available in the current environment.
* @throws InvalidKeySpecException
* if the specifications for the cryptographic key are invalid.
*/
Authentication logIn(String name, String pass, long sessionId)
throws NoSuchAlgorithmException,
InvalidKeySpecException
{
try (Connection conn = dbs.openConnection())
{
try
{
Authentication auth = authenticate(conn, name, pass);
if (auth != null)
{
user = auth.getUser();
// remember the session ID for later
this.sessionId = sessionId;
// piggyback default filter setup on authentication
applyFileFilter(conn, null, sessionId);
}
conn.commit();
return auth;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Change the given user's password, following some basic conventions for a valid new
* password:
* <ul>
* <li>it must not be the same as the old password;</li>
* <li>it must be at least 6 characters in length;</li>
* <li>it must contain at least one letter and one digit.</li>
* </ul>
* The user is authenticated with the old password before the password change is allowed.
*
* @param name
* User name.
* @param oldPass
* Existing password.
* @param newPass1
* New password.
* @param newPass2
* Repeat of new password; must be identical to {@code newPass1}.
*
* @throws NoSuchAlgorithmException
* if the requested encryption algorithm is not available in the current environment.
* @throws InvalidKeySpecException
* if the specifications for the cryptographic key are invalid.
*/
void changePassword(String name, String oldPass, String newPass1, String newPass2)
throws NoSuchAlgorithmException,
InvalidKeySpecException
{
try (Connection conn = dbs.openConnection())
{
try
{
if (authenticate(conn, name, oldPass) == null)
{
throw new RuntimeException("Old password was incorrect.");
}
if (newPass1 == null || newPass2 == null || !newPass1.equals(newPass2))
{
throw new RuntimeException("New password entries do not match each other!");
}
if (newPass1.equals(oldPass))
{
throw new RuntimeException("New password must differ from old password!");
}
// basic password policy
boolean valid = true;
int len = newPass1.length();
if (len < 6)
{
valid = false;
}
if (valid)
{
boolean letter = false;
boolean digit = false;
for (int i = 0; i < len && !valid; i++)
{
char c = newPass1.charAt(i);
letter = letter || Character.isLetter(c);
digit = digit || Character.isDigit(c);
valid = letter && digit;
}
}
if (!valid)
{
throw new RuntimeException(
"A valid password is at minimum 6 characters long and contains at least " +
"one letter and one digit");
}
// update user record to change salt, password, and password changed date
try (PreparedStatement ps = conn.prepareStatement(sqlUsrPassChg);)
{
PasswordHelper hlp = new PasswordHelper();
byte[] salt = hlp.salt();
byte[] encPass = hlp.encryptPassword(newPass1, salt);
ps.setBytes(1, salt);
ps.setBytes(2, encPass);
ps.setLong(3, user.getId());
ps.execute();
ps.clearParameters();
}
conn.commit();
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Given the name of a schema export file AST, strip out the {@code namespace} portion of the
* path, and append a {@code .df} extension.
*
* @param name
* Original name.
*
* @return Adjusted name.
*/
private String stripSchemaName(String name)
{
name = name.substring(0, name.lastIndexOf('.'));
String[] parts = name.split("/");
int len = parts.length;
if (parts[len - 2].equalsIgnoreCase("namespace"))
{
StringBuilder buf = new StringBuilder();
for (int i = 0; i < len; i++)
{
if (i != len - 2)
{
buf.append(parts[i]);
if (i < len - 1)
{
buf.append("/");
}
}
}
buf.append(".df");
name = buf.toString();
}
return name;
}
/**
* Gather the full list of filenames of the specified type from the database file map table.
*
* @param conn
* Database connection.
* @param applyFilter
* {@code True} to apply the current file filter when gathering the list of files;
* {@code false} to gather all files.
* @param srcType
* Type of files (schema or code).
*
* @return A file list containing the filenames.
*
* @throws SQLException
* if there was an error with the database query.
*/
private FileList gatherFiles(Connection conn, boolean applyFilter, int srcType)
throws SQLException
{
String suffix;
switch (srcType)
{
case SRC_SCHEMA:
suffix = ".dict";
break;
case SRC_CODE_CACHE:
suffix = ".ast";
break;
default:
throw new RuntimeException("Unexpected source file type: " + srcType);
}
String sql = applyFilter ? sqlQryFltFiles : sqlQryFiles;
try (PreparedStatement ps = conn.prepareStatement(sql))
{
List<String> names = new ArrayList<>();
if (applyFilter)
{
ps.setLong(1, sessionId);
ps.setInt(2, srcType);
}
else
{
ps.setInt(1, srcType);
}
ResultSet rs = ps.executeQuery();
while (rs.next())
{
String name = rs.getString(1);
// special handling for DF files (I know, ugly)
if (srcType == SRC_SCHEMA && name.toLowerCase().endsWith(".df"))
{
name = name.substring(0, name.lastIndexOf('.'));
String[] parts = name.split("/");
StringBuilder buf = new StringBuilder();
int len = parts.length;
for (int i = 0; i < len - 1; i++)
{
buf.append(parts[i]);
buf.append("/");
}
buf.append("namespace/");
buf.append(parts[len - 1]);
name = buf.toString();
}
name += suffix;
name = Configuration.forceHome(name);
names.add(name);
}
return new ExplicitFileList(names.toArray(new String[names.size()]));
}
}
/**
* Flatten an AST into a list of {@link AstNode} objects suitable for marshalling and
* transport to the web client. A large, hierarchical AST structure does not marshall well
* over Jackson (results in {@code StackOverflowException} due to recursion), so we create
* a flat list of simplified beans instead.
* <p>
* Also compute additional information during this process needed by the web client, such as
* effective starting and ending line and column numbers.
*
* @param ast
* AST to be marshalled.
*
* @return List of {@link AstNode} objects, one per node in the original tree.
*/
private List<AstNode> prepareAstNodes(Aast ast)
{
List<AstNode> list = new ArrayList<>();
new AstWalkListener()
{
private Deque<AstNode> parents = new ArrayDeque<>();
private Deque<Coord[]> coords = new ArrayDeque<>();
private AstNode parent = null;
private AstNode node = null;
private Coord[] range = new Coord[2];
private Iterator<Aast> iter = ast.iterator(0, this);
{
while (iter.hasNext())
{
Aast next = iter.next();
node = new AstNode();
list.add(node);
String text = next.getText();
// TODO: account for multi-line text
int l0 = next.getLine();
int l1 = l0;
int c0 = next.getColumn();
int c1 = c0 + (c0 > 0 ? Math.max(text.length() - 1, 0) : 0);
if (l0 > 0)
{
Coord start = new Coord(l0, c0);
if (range[0] == null || start.compareTo(range[0]) < 0)
{
range[0] = start;
}
Coord end = new Coord(l1, c1);
if (range[1] == null || end.compareTo(range[1]) > 0)
{
range[1] = end;
}
}
node.setId(next.getId());
node.setType(next.getType());
node.setText(text);
node.setActualStartLine(l0);
node.setActualStartColumn(c0);
node.setActualEndLine(l1);
node.setActualEndColumn(c1);
node.setEffectiveStartLine(l0);
node.setEffectiveStartColumn(c0);
node.setEffectiveEndLine(l1);
node.setEffectiveEndColumn(c1);
Aast parent = next.getParent();
if (parent != null)
{
node.setParentId(parent.getId());
}
Map<String, Object> notes = null;
Iterator<String> keyIter = next.annotationKeys();
while (keyIter.hasNext())
{
if (notes == null)
{
notes = new LinkedHashMap<>();
node.setNotes(notes);
}
String key = keyIter.next();
Object val = next.getAnnotation(key);
notes.put(key, val);
}
}
}
@Override
public void descent(Aast ast)
{
if (parent == null)
{
// parent of root is null; will not be used, so push an empty one to avoid NPE
parents.push(new AstNode());
}
else
{
parents.push(parent);
}
parent = node;
coords.push(range);
range = new Coord[2];
}
@Override
public void ascent(Aast ast)
{
Coord start = range[0];
Coord end = range[1];
range = coords.pop();
if (start != null && end != null)
{
Coord pStart = new Coord(parent.getEffectiveStartLine(),
parent.getEffectiveStartColumn());
if (pStart.line == 0 || start.compareTo(pStart) < 0)
{
parent.setEffectiveStartLine(start.line);
parent.setEffectiveStartColumn(start.column);
}
Coord pEnd = new Coord(parent.getEffectiveEndLine(),
parent.getEffectiveEndColumn());
if (pEnd.line == 0 || end.compareTo(pEnd) > 0)
{
parent.setEffectiveEndLine(end.line);
parent.setEffectiveEndColumn(end.column);
}
Coord startParent = range[0];
Coord endParent = range[1];
if (startParent == null || start.compareTo(startParent) < 0)
{
range[0] = start;
}
if (endParent == null || end.compareTo(endParent) > 0)
{
range[1] = end;
}
}
parent = parents.pop();
}
@Override
public void nextChild(Aast ast, int index)
{
}
};
/*
System.out.println(ast.dumpTree());
for (AstNode n : list)
{
System.out.println(n.getText() +
" [" + n.getEffectiveStartLine() + ":" + n.getEffectiveStartColumn() + "-" +
n.getEffectiveEndLine() + ":" + n.getEffectiveEndColumn() + "] (" +
n.getId() + "/" + n.getParentId() + ")");
}
*/
return list;
}
/**
* Query and return information necessary to produce the master report of overview results
* for all summary reports of the given type.
*
* @param type
* Type of reports ({@code SRC_CODE_CACHE} or {@code SRC_SCHEMA}).
*
* @return Array of {@link OverviewRow} objects, each providing the high level match
* statistics for its associated summary report.
*/
private OverviewRow[] masterReport(int type)
{
OverviewRow[] results = cachedMasterResults.get(type);
if (results != null)
{
return results;
}
try (Connection conn = dbs.openConnection())
{
List<OverviewRow> list = new ArrayList<>();
boolean includeAll = filterId == defaultFilterId;
String sql = (includeAll ? sqlQryMasterAll : sqlQryMasterAgg);
try (PreparedStatement ps = conn.prepareStatement(sql))
{
// the two prepared statements for this report take different substitution
// parameters
if (includeAll)
{
ps.setInt(1, type);
}
else
{
ps.setLong(1, sessionId);
ps.setInt(2, type);
ps.setInt(3, type);
}
ResultSet rs = ps.executeQuery();
while (rs.next())
{
OverviewRow row = new OverviewRow();
row.setId(rs.getInt(1));
row.setTitle(rs.getString(2));
row.setMatches(rs.getInt(3));
row.setFiles(rs.getInt(4));
row.setTag(rs.getString(5));
rs.getString(6);
row.setSupport(!rs.wasNull());
row.setCondition(rs.getString(7));
list.add(row);
}
conn.commit();
results = list.toArray(new OverviewRow[0]);
cachedMasterResults.put(type, results);
return results;
}
catch (SQLException exc)
{
if (conn != null)
{
conn.rollback();
}
throw new RuntimeException(exc);
}
}
catch (SQLException exc)
{
throw new RuntimeException(exc);
}
}
/**
* Persist a (presumably successful) search/report TRPL expression.
*
* @param conn
* Database connection.
* @param criteria
* TRPL expression to be persisted.
* @throws SQLException
* if a database error occurs.
*/
private void persistSearchHistory(Connection conn, String criteria)
throws SQLException
{
// compose sql to insert into search_history table, if not already present
StringBuilder buf = new StringBuilder();
buf.append("insert into search_history ");
buf.append("select * from (select next value for seq_search_history_pk, '");
buf.append(criteria);
buf.append("' ) x where not exists (select 1 from search_history where criteria = ?)");
Object histArg = criteria;
String sqlInsSrchHist = buf.toString();
// compose sql to insert into user_search table, if not already present
buf.setLength(0);
buf.append("insert into user_search select * from (select ");
buf.append(user.getId());
buf.append(" as u, select id from search_history where criteria = ?");
buf.append(" as s) x where exists (select 1 from search_history where criteria = ?");
buf.append(") and not exists (select 1 from user_search where uid = x.u and sid = x.s)");
Object usrArg = criteria;
String sqlInsUsrSrch = buf.toString();
//tested with H2 1.4.200 and prepared statements now work with subselects
try (PreparedStatement stmt = conn.prepareStatement(sqlInsSrchHist))
{
stmt.setObject(1, histArg);
stmt.execute();
}
try (PreparedStatement stmt = conn.prepareStatement(sqlInsUsrSrch))
{
stmt.setObject(1, usrArg);
stmt.setObject(2, usrArg);
stmt.execute();
}
}
/**
* This class represents a specific location within a source file, as represented by the line
* and column number of a particular character.
*/
private static class Coord
implements Comparable<Coord>
{
/** Line number */
private final int line;
/** Column number */
private final int column;
/**
* Constructor.
*
* @param line
* Line number.
* @param column
* Column number.
*/
Coord(int line, int column)
{
this.line = line;
this.column = column;
}
/**
* Compare this line and column location with another. A coordinate is considered greater
* than another if it is located at a higher line number, or, if within the same line, at
* a higher column number.
*
* @param o
* Coordinate instance to compare with this one.
*
* @return A negative number if this coordinate is less than the specified one;
* 0 if both coordinates represent the same location in a source file;
* a positive number if this coordinate is greater than the specified one.
*/
@Override
public int compareTo(Coord o)
{
if (this.line < o.line)
{
return -1;
}
if (this.line > o.line)
{
return 1;
}
if (this.column < o.column)
{
return -1;
}
if (this.column > o.column)
{
return 1;
}
return 0;
}
}
}