/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.eas.client;
import com.eas.client.cache.PlatypusIndexer;
import com.eas.client.metadata.Field;
import com.eas.client.metadata.Fields;
import com.eas.client.metadata.JdbcField;
import com.eas.client.metadata.Parameter;
import com.eas.client.model.QueryDocument;
import com.eas.client.model.QueryDocument.StoredFieldMetadata;
import com.eas.client.model.Relation;
import com.eas.client.model.query.QueryEntity;
import com.eas.client.model.query.QueryModel;
import com.eas.client.model.query.QueryParametersEntity;
import com.eas.client.queries.QueriesProxy;
import com.eas.client.sqldrivers.resolvers.TypesResolver;
import com.eas.script.JsDoc;
import java.io.File;
import java.io.StringReader;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.ResultsFinder;
import net.sf.jsqlparser.SourcesFinder;
import net.sf.jsqlparser.parser.*;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
/**
*
* @author mg.
*/
public class StoredQueryFactory {
public static final String _Q = "\\" + ClientConstants.STORED_QUERY_REF_PREFIX + "?";
private Fields processSubQuery(SqlQuery aQuery, SubSelect aSubSelect) throws Exception {
SqlQuery subQuery = new SqlQuery(aQuery.getBasesProxy(), aQuery.getDatasourceName(), "");
subQuery.setEntityName(aSubSelect.getAliasName());
resolveOutputFieldsFromTables(subQuery, aSubSelect.getSelectBody());
Fields subFields = subQuery.getFields();
return subFields;
}
public static final String INNER_JOIN_CONSTRUCTION = "select %s from %s %s inner join %s on (%s.%s = %s.%s)";
public static final String ABSENT_QUERY_MSG = "Query %s is not found";
public static final String CANT_LOAD_NULL_MSG = "Query name is null.";
public static final String COLON = ":";
public static final String CONTENT_EMPTY_MSG = "Content of %s is empty";
public static final String DUMMY_FIELD_NAME = "dummy";
public static final String INEER_JOIN_CONSTRUCTING_MSG = "Constructing query with left Query %s and right table %s";
public static final String LOADING_QUERY_MSG = "Loading stored query %s";
protected QueriesProxy<SqlQuery> subQueriesProxy;
private final DatabasesClient basesProxy;
private final PlatypusIndexer indexer;
private boolean aliasesToTableNames;
public boolean isAliasesToTableNames() {
return aliasesToTableNames;
}
public void setAliasesToTableNames(boolean aValue) {
aliasesToTableNames = aValue;
}
protected void addTableFieldsToSelectResults(SqlQuery aQuery, Table aFieldsSource) throws Exception {
FieldsResult fieldsRes = getTablyFields(aQuery.getDatasourceName(), aFieldsSource.getWholeTableName());
if (fieldsRes != null && fieldsRes.fields != null) {
MetadataCache mdCache = basesProxy.getMetadataCache(aQuery.getDatasourceName());
if (mdCache != null) {
TypesResolver resolver = mdCache.getDatasourceSqlDriver().getTypesResolver();
fieldsRes.fields.toCollection().stream().forEach((Field field) -> {
Field copied = new Field();
copied.assignFrom(field);
if (fieldsRes.fromRealTable) {
JdbcField jField = (JdbcField) field;
copied.setType(resolver.toApplicationType(jField.getJdbcType(), jField.getType()));
if (jField.getSchemaName() != null && !jField.getSchemaName().isEmpty()) {
copied.setTableName(jField.getSchemaName() + "." + copied.getTableName());
}
}
/**
* Заменять имя оригинальной таблицы нельзя, особенно если
* это поле ключевое т.к. при установлении связи по этим
* полям будут проблемы. ORM-у придется "разматывать"
* источник поля до таблицы чтобы восстановиит связи по
* ключам. Здесь это делается исключительно ради очень
* специального использования фабрики в дизайнере запросов.
*/
if (aliasesToTableNames
&& aFieldsSource.getAlias() != null && !aFieldsSource.getAlias().getName().isEmpty()) {
copied.setTableName(aFieldsSource.getAlias().getName());
}
aQuery.getFields().add(copied);
});
}
}
}
public static Map<String, FromItem> prepareUniqueTables(Map<String, FromItem> tables) {
Map<String, FromItem> uniqueTables = new HashMap<>();
tables.values().stream().forEach((fromItem) -> {
if (fromItem.getAlias() != null && !fromItem.getAlias().getName().isEmpty()) {
uniqueTables.put(fromItem.getAlias().getName().toLowerCase(), fromItem);
} else if (fromItem instanceof Table) {
uniqueTables.put(((Table) fromItem).getWholeTableName().toLowerCase(), fromItem);
}
});
return uniqueTables;
}
public SqlQuery loadQuery(String aAppElementName) throws Exception {
if (aAppElementName == null) {
throw new NullPointerException(CANT_LOAD_NULL_MSG);
}
Logger.getLogger(this.getClass().getName()).finer(String.format(LOADING_QUERY_MSG, aAppElementName));
File mainQueryFile = indexer.nameToFile(aAppElementName);
return mainQueryFile != null ? fileToSqlQuery(aAppElementName, mainQueryFile) : null;
}
protected SqlQuery fileToSqlQuery(String aName, File aFile) throws Exception {
QueryDocument queryDoc = QueryDocument.parse(aName, aFile, basesProxy, subQueriesProxy);
QueryModel model = queryDoc.getModel();
SqlQuery query = queryDoc.getQuery();
putRolesMutatables(query);
List<StoredFieldMetadata> additionalFieldsMetadata = queryDoc.getAdditionalFieldsMetadata();
String sqlText = query.getSqlText();
if (sqlText != null && !sqlText.isEmpty()) {
if (query.getFullSqlText() != null && !query.getFullSqlText().isEmpty() && !query.getFullSqlText().replaceAll("\\s", "").isEmpty()) {
sqlText = query.getFullSqlText();
}
try {
String compiledSqlText = compileSubqueries(sqlText, model);
try {
putParametersMetadata(query, model);
if (putTableFieldsMetadata(query)) {
putStoredTableFieldsMetadata(query, additionalFieldsMetadata);
} else {
query.setCommand(true);
}
} finally {
query.setSqlText(compiledSqlText);
}
} finally {
query.setTitle(aName);
query.getFields().setTableDescription(query.getTitle());
}
}
return query;
}
/**
* Constructs factory for stored in appliction database queries;
*
* @param aBasesProxy ClientIntf instance, responsible for interacting with
* appliction database.
* @param aSubQueriesProxy
* @param aIndexer
* @throws java.lang.Exception
*/
public StoredQueryFactory(DatabasesClient aBasesProxy, QueriesProxy<SqlQuery> aSubQueriesProxy, PlatypusIndexer aIndexer) throws Exception {
super();
basesProxy = aBasesProxy;
subQueriesProxy = aSubQueriesProxy;
indexer = aIndexer;
}
/**
* Заменяет в запросе ссылки на подзапросы на их содержимое. Подставляет
* параметры запроса в соответствии со связями в параметры подзапросов.
*
* @param aSqlText
* @param aModel
* @return Запрос без ссылок на подзапросы.
* @throws java.lang.Exception
*/
public String compileSubqueries(String aSqlText, QueryModel aModel) throws Exception {
/**
* Старая реализация заменяла текст всех подзапросов с одним и тем же
* идентификатором, не обращая внимания на алиасы. Поэтому запросы
* содержащие в себе один и тот же подзапрос несколько раз,
* компилировались неправильно. Неправильно подставлялись и параметры.
*/
assert aModel != null;
if (aModel.getEntities() != null) {
String processedSql = aSqlText;
for (QueryEntity entity : aModel.getEntities().values()) {
assert entity != null;
if (entity.getQueryName() != null) {
String queryTablyName = entity.getQueryName();
Pattern subQueryPattern = Pattern.compile(_Q + queryTablyName, Pattern.CASE_INSENSITIVE);
String tAlias = entity.getAlias();
if (tAlias != null && !tAlias.isEmpty()) {
subQueryPattern = Pattern.compile(_Q + queryTablyName + "[\\s]+" + tAlias, Pattern.CASE_INSENSITIVE);
if (tAlias.equalsIgnoreCase(queryTablyName)
&& !subQueryPattern.matcher(processedSql).find()) {
/**
* Эта проверка с финтом ушами нужна, т.к. даже в
* отсутствии алиаса, он все равно есть и равен
* queryTablyName. А так как алиас может в SQL
* совпадать с именем таблицы, то эти ситуации никак
* не различить, кроме как явной проверкой на
* нахождение такого алиаса и имени таблицы
* (подзапроса).
*/
subQueryPattern = Pattern.compile(_Q + queryTablyName, Pattern.CASE_INSENSITIVE);
}
}
Matcher subQueryMatcher = subQueryPattern.matcher(processedSql);
if (subQueryMatcher.find()) {
SqlQuery subQuery = subQueriesProxy.getQuery(entity.getQueryName(), null, null, null);
if (subQuery != null && subQuery.getSqlText() != null) {
String subQueryText = subQuery.getSqlText();
subQueryText = replaceLinkedParameters(subQueryText, entity.getInRelations());
String sqlBegin = processedSql.substring(0, subQueryMatcher.start());
String sqlToInsert = " (" + subQueryText + ") ";
String sqlTail = processedSql.substring(subQueryMatcher.end());
if (tAlias != null && !tAlias.isEmpty()) {
processedSql = sqlBegin + sqlToInsert + " " + tAlias + " " + sqlTail;
} else {
processedSql = sqlBegin + sqlToInsert + " " + queryTablyName + " " + sqlTail;
}
}
}
}
}
return processedSql;
}
return aSqlText;
}
private String replaceLinkedParameters(String aSqlText, Set<Relation<QueryEntity>> parametersRelations) {
for (Relation<QueryEntity> rel : parametersRelations) {
if (rel.getLeftEntity() instanceof QueryParametersEntity && rel.getLeftField() != null && rel.getRightParameter() != null) {
aSqlText = Pattern.compile(COLON + rel.getRightParameter().getName() + "\\b", Pattern.CASE_INSENSITIVE).matcher(aSqlText).replaceAll(COLON + rel.getLeftField().getName());
}
}
return aSqlText;
}
private void putParametersMetadata(SqlQuery aQuery, QueryModel aModel) {
for (int i = 1; i <= aModel.getParameters().getParametersCount(); i++) {
Parameter p = aModel.getParameters().get(i);
aQuery.getParameters().add(p);
}
}
private void putStoredTableFieldsMetadata(SqlQuery aQuery, List<StoredFieldMetadata> storedMetadata) {
Fields fields = aQuery.getFields();
if (fields != null) {
storedMetadata.stream().forEach((addition) -> {
Field queryField = fields.get(addition.getBindedColumn());
if (queryField != null) {
if (addition.description != null && !addition.description.isEmpty()) {
queryField.setDescription(addition.description);
}
if (addition.getType() != null && !addition.getType().equals(queryField.getType())) {
queryField.setType(addition.getType());
}
}
});
}
}
public static void putRolesMutatables(SqlQuery aQuery) throws Exception {
// Let's extract all comments
Set<String> comments = new HashSet<>();
CCJSqlParserTokenManager tokenManager = new CCJSqlParserTokenManager(new SimpleCharStream(new StringReader(aQuery.getSqlText())));
Token token = tokenManager.getNextToken();
while (token != null && token.kind != CCJSqlParserConstants.EOF) {
if (token.specialToken != null) {
comments.add(token.specialToken.toString());
}
token = tokenManager.getNextToken();
}
if (token != null && token.specialToken != null) {
comments.add(token.specialToken.toString());
}
boolean readonly = false;
for (String comment : comments) {
JsDoc jsDoc = new JsDoc(comment);
jsDoc.parseAnnotations();
for (JsDoc.Tag tag : jsDoc.getAnnotations()) {
switch (tag.getName().toLowerCase()) {
case JsDoc.Tag.ROLES_ALLOWED_TAG:
aQuery.getReadRoles().addAll(tag.getParams());
if (aQuery.getWriteRoles().isEmpty()) {
aQuery.getWriteRoles().addAll(tag.getParams());
}
break;
case JsDoc.Tag.ROLES_ALLOWED_READ_TAG:
aQuery.getReadRoles().addAll(tag.getParams());
break;
case JsDoc.Tag.ROLES_ALLOWED_WRITE_TAG:
if (!aQuery.getWriteRoles().isEmpty()) {
aQuery.getWriteRoles().clear();
}
aQuery.getWriteRoles().addAll(tag.getParams());
break;
case JsDoc.Tag.READONLY_TAG:
readonly = true;
break;
case JsDoc.Tag.WRITABLE_TAG:
Set<String> writables = new HashSet<>();
if (tag.getParams() != null) {
tag.getParams().stream().forEach((writable) -> {
if (writable != null) {
writables.add(writable.toLowerCase());
}
});
}
aQuery.setWritable(writables);
break;
case JsDoc.Tag.PROCEDURE_TAG:
aQuery.setProcedure(true);
break;
case JsDoc.Tag.PUBLIC_TAG:
aQuery.setPublicAccess(true);
break;
}
}
}
if (readonly) {
aQuery.setWritable(Collections.<String>emptySet());
}
}
/**
* @param aQuery
* @return True if query is select query.
* @throws Exception
*/
public boolean putTableFieldsMetadata(SqlQuery aQuery) throws Exception {
CCJSqlParserManager parserManager = new CCJSqlParserManager();
try {
Statement parsedQuery = parserManager.parse(new StringReader(aQuery.getSqlText()));
if (parsedQuery instanceof Select) {
Select select = (Select) parsedQuery;
resolveOutputFieldsFromTables(aQuery, select.getSelectBody());
return true;
}
} catch (JSQLParserException ex) {
if (aQuery.isProcedure()) {
Logger.getLogger(StoredQueryFactory.class.getName()).log(Level.WARNING, ex.getMessage());
} else {
throw ex;
}
}
return false;
}
private void resolveOutputFieldsFromTables(SqlQuery aQuery, SelectBody aSelectBody) throws Exception {
Map<String, FromItem> sources = SourcesFinder.getSourcesMap(SourcesFinder.TO_CASE.LOWER, aSelectBody);
for (SelectItem sItem : ResultsFinder.getResults(aSelectBody)) {
if (sItem instanceof AllColumns) {// *
Map<String, FromItem> uniqueTables = prepareUniqueTables(sources);
for (FromItem source : uniqueTables.values()) {
if (source instanceof Table) {
addTableFieldsToSelectResults(aQuery, (Table) source);
} else if (source instanceof SubSelect) {
Fields subFields = processSubQuery(aQuery, (SubSelect) source);
Fields destFields = aQuery.getFields();
subFields.toCollection().stream().forEach((field) -> {
destFields.add(field);
});
}
}
} else if (sItem instanceof AllTableColumns) {// t.*
AllTableColumns cols = (AllTableColumns) sItem;
assert cols.getTable() != null : "<table>.* syntax must lead to .getTable() != null";
FromItem source = sources.get(cols.getTable().getWholeTableName().toLowerCase());
if (source instanceof Table) {
addTableFieldsToSelectResults(aQuery, (Table) source);
} else if (source instanceof SubSelect) {
Fields subFields = processSubQuery(aQuery, (SubSelect) source);
Fields destFields = aQuery.getFields();
subFields.toCollection().stream().forEach((field) -> {
destFields.add(field);
});
}
} else {
assert sItem instanceof SelectExpressionItem;
SelectExpressionItem col = (SelectExpressionItem) sItem;
Field field = null;
/* Если пользоваться этим приемом, то будет введение разработчика в заблуждение
* т.к. в дизайнере и автозавершении кода, поле результата будет поименовано
* так же как и поле-агрумент функции, а из скрипта оно будет недоступно.
if (col.getExpression() instanceof Function) {
Function func = (Function) col.getExpression();
if (func.getParameters() != null && func.getParameters().getExpressions() != null
&& func.getParameters().getExpressions().size() == 1) {
Expression firstArg = (Expression) func.getParameters().getExpressions().get(0);
if (firstArg instanceof Column) {
field = resolveFieldByColumn(aQuery, (Column) firstArg, col, tables);
}
}
} else */
if (col.getExpression() instanceof Column) {
field = resolveFieldByColumn(aQuery, (Column) col.getExpression(), col, sources);
} else // free expression like a ...,'text' as txt,...
{
field = null;
/*
* // Absent alias generation is parser's work. field = new
* Field(col.getAlias()); // Such field is absent in
* database tables and so, field's table is the processed
* query. field.setTableName(ClientConstants.QUERY_ID_PREFIX
* + aQuery.getEntityId().toString()); /** There is an
* unsolved problem about type of the expression. This might
* be solved using manually setted up field's type and
* description information during
* "putStoredTableFieldsMetadata(...)" call.
*/
//field.getTypeInfo().setSqlType(Types.OTHER);
}
if (field == null) {
// Absent alias generation is parser's work.
// Безымянные поля, получающиеся когда нет алиаса, будут
// замещены полями полученными из базы во время исполнения запроса.
field = new Field(col.getAlias() != null ? col.getAlias().getName()
: (col.getExpression() instanceof Column ? ((Column) col.getExpression()).getColumnName() : ""));
field.setTableName(aQuery.getEntityName());
/**
* There is an unsolved problem about type of the
* expression. This might be solved using manually setted up
* field's type and description information during
* "putStoredTableFieldsMetadata(...)" call.
*/
field.setType(null);
}
aQuery.getFields().add(field);
}
}
}
protected class FieldsResult {
public Fields fields;
public boolean fromRealTable;
public FieldsResult(Fields aResult, boolean aFromRealTable) {
super();
fields = aResult;
fromRealTable = aFromRealTable;
}
}
/**
* Returns cached table fields if <code>aTablyName</code> is a table name or
* query fields if <code>aTablyName</code> is query tably name in format:
* #<id>.
*
* @param aDatasourceName Database identifier, the query belongs to. That
* database is query-inner table metadata source, but query is stored in
* application.
* @param aTablyName Table or query tably name.
* @return Fields instance.
* @throws Exception
*/
protected FieldsResult getTablyFields(String aDatasourceName, String aTablyName) throws Exception {
Fields tableFields;
if (aTablyName.startsWith(ClientConstants.STORED_QUERY_REF_PREFIX)) {// strong reference to stored subquery
tableFields = null;
aTablyName = aTablyName.substring(ClientConstants.STORED_QUERY_REF_PREFIX.length());
} else {// soft reference to table or a stored subquery.
try {
tableFields = basesProxy.getMetadataCache(aDatasourceName).getTableMetadata(aTablyName);
} catch (Exception ex) {
tableFields = null;
}
}
if (tableFields != null) {// Tables have a higher priority in soft reference case
return new FieldsResult(tableFields, true);
} else {
SqlQuery query = subQueriesProxy.getQuery(aTablyName, null, null, null);
if (query != null) {
return new FieldsResult(query.getFields(), false);
} else {
return null;
}
}
}
private Field resolveFieldByColumn(SqlQuery aQuery, Column column, SelectExpressionItem selectItem, Map<String, FromItem> aSources) throws Exception {
Field field = null;
FromItem fieldSource = null;// Это таблица парсера - источник данных в составе запроса.
boolean fieldFromRealTable = false;
if (column.getTable() != null && column.getTable().getWholeTableName() != null) {
FromItem namedSource = aSources.get(column.getTable().getWholeTableName().toLowerCase());
if (namedSource != null) {
if (namedSource instanceof Table) {
/**
* Таблица поля, предоставляемая парсером никак не связана с
* таблицей из списка from. Поэтому мы должны связать их
* самостоятельно. Такая вот особенность парсера.
*/
FieldsResult tableFieldsResult = getTablyFields(aQuery.getDatasourceName(), ((Table) namedSource).getWholeTableName());
if (tableFieldsResult != null && tableFieldsResult.fields != null && tableFieldsResult.fields.contains(column.getColumnName())) {
field = tableFieldsResult.fields.get(column.getColumnName());
fieldSource = namedSource;
fieldFromRealTable = tableFieldsResult.fromRealTable;
}
} else if (namedSource instanceof SubSelect) {
Fields subFields = processSubQuery(aQuery, (SubSelect) namedSource);
if (subFields.contains(column.getColumnName())) {
field = subFields.get(column.getColumnName());
fieldSource = namedSource;
fieldFromRealTable = false;
}
}
}
}
if (field == null) {
/**
* Часто бывает, что алиас/имя таблицы из которой берется поле не
* указаны. Поэтому парсер не предоставляет таблицу. В этом случае
* как и в первой версии поищем первую таблицу, содержащую поле с
* таким именем.
*/
for (FromItem anySource : aSources.values()) {
if (anySource instanceof Table) {
FieldsResult fieldsResult = getTablyFields(aQuery.getDatasourceName(), ((Table) anySource).getWholeTableName());
if (fieldsResult != null && fieldsResult.fields != null && fieldsResult.fields.contains(column.getColumnName())) {
field = fieldsResult.fields.get(column.getColumnName());
fieldSource = anySource;
fieldFromRealTable = fieldsResult.fromRealTable;
break;
}
} else if (anySource instanceof SubSelect) {
Fields fields = processSubQuery(aQuery, (SubSelect) anySource);
if (fields != null && fields.contains(column.getColumnName())) {
field = fields.get(column.getColumnName());
fieldSource = anySource;
fieldFromRealTable = false;
break;
}
}
}
}
if (field != null) {
/**
* Скопируем поле, чтобы избежать пересечения информации о полях
* таблицы из-за её участия в разных запросах.
*/
Field copied = new Field();
copied.assignFrom(field);
if (fieldFromRealTable) {
TypesResolver resolver = basesProxy.getMetadataCache(aQuery.getDatasourceName()).getDatasourceSqlDriver().getTypesResolver();
JdbcField jField = (JdbcField) field;
copied.setType(resolver.toApplicationType(jField.getJdbcType(), jField.getType()));
if (jField.getSchemaName() != null && !jField.getSchemaName().isEmpty()) {
copied.setTableName(jField.getSchemaName() + "." + copied.getTableName());
}
}
/**
* Заменим отметку о первичном ключе из оригинальной таблицы на
* отметку о внешнем ключе, указывающем на ту же таблицу. Замена
* производится с учетом "главной" таблицы. Теперь эта обработка не
* нужна, т.к. все таблицы "главные", т.е. изменения могут попасть в
* несколько таблиц одновременно, с учетом их ключей, конечно.
*/
//checkPrimaryKey(aQuery, copied);
/**
* Заменим имя поля из оригинальной таблицы на алиас. Если его нет,
* то его надо сгенерировать. Генерация алиаса, - это работа
* парсера. По возможности, парсер должен генерировать алиасы
* совпадающие с именем поля.
*/
copied.setName(selectItem.getAlias() != null ? selectItem.getAlias().getName() : column.getColumnName());
copied.setOriginalName(column.getColumnName() != null ? column.getColumnName() : copied.getName());
/**
* Заменять имя оригинальной таблицы нельзя, особенно если это поле
* ключевое т.к. при установлении связи по этим полям будут
* проблемы. ORM-у и дизайнеру придется "разматывать" источник поля
* сквозь все запросы до таблицы чтобы восстановить связи по ключам.
* Здесь это делается исключительно ради очень специального
* использования фабрики в дизайнере запросов.
*/
if (aliasesToTableNames
&& fieldSource != null && fieldSource.getAlias() != null && !fieldSource.getAlias().getName().isEmpty()) {
copied.setTableName(fieldSource.getAlias().getName());
}
return copied;
} else {
return null;
}
}
}