ru.curs.celesta.dbutils.BasicCursor Maven / Gradle / Ivy
The newest version!
/*
Copyright 2013 COURSE-IT Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ru.curs.celesta.dbutils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.curs.celesta.CallContext;
import ru.curs.celesta.CelestaException;
import ru.curs.celesta.PermissionDeniedException;
import ru.curs.celesta.dbutils.filter.AbstractFilter;
import ru.curs.celesta.dbutils.filter.Filter;
import ru.curs.celesta.dbutils.filter.In;
import ru.curs.celesta.dbutils.filter.Range;
import ru.curs.celesta.dbutils.filter.SingleValue;
import ru.curs.celesta.dbutils.query.FromClause;
import ru.curs.celesta.dbutils.stmt.MaskedStatementHolder;
import ru.curs.celesta.dbutils.stmt.ParameterSetter;
import ru.curs.celesta.dbutils.stmt.PreparedStatementHolderFactory;
import ru.curs.celesta.dbutils.stmt.PreparedStmtHolder;
import ru.curs.celesta.dbutils.term.FromTerm;
import ru.curs.celesta.dbutils.term.WhereMakerParamsProvider;
import ru.curs.celesta.dbutils.term.WhereTerm;
import ru.curs.celesta.dbutils.term.WhereTermsMaker;
import ru.curs.celesta.score.CelestaParser;
import ru.curs.celesta.score.ColumnMeta;
import ru.curs.celesta.score.DataGrainElement;
import ru.curs.celesta.score.Expr;
import ru.curs.celesta.score.ParseException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Base cursor class for reading data from views.
*/
public abstract class BasicCursor extends BasicDataAccessor {
private static final Logger LOGGER = LoggerFactory.getLogger(BasicCursor.class);
private static final String DATABASE_CLOSING_ERROR =
"Database error when closing recordset for table '%s': %s";
private static final String NAVIGATING_ERROR = "Error while navigating cursor: %s";
private static final Pattern COLUMN_NAME =
Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*)( +([Aa]|[Dd][Ee])[Ss][Cc])?");
private static final Pattern NAVIGATION = Pattern.compile("[+-<>=]+");
private static final Pattern NAVIGATION_WITH_OFFSET = Pattern.compile("[<>]");
protected Set fields = Collections.emptySet();
protected Set fieldsForStatement = Collections.emptySet();
protected ResultSet cursor;
protected FromTerm fromTerm;
final PreparedStmtHolder set = PreparedStatementHolderFactory.createFindSetHolder(
BasicCursor.this.db(),
BasicCursor.this.conn(),
//NB: do not replace with method reference, this will cause NPE in initializer
() -> BasicCursor.this.getFrom(),
() -> {
if (BasicCursor.this.fromTerm == null) {
BasicCursor.this.fromTerm = new FromTerm(BasicCursor.this.getFrom().getParameters());
return BasicCursor.this.fromTerm;
}
return BasicCursor.this.fromTerm;
},
() -> BasicCursor.this.qmaker.getWhereTerm(),
() -> BasicCursor.this.getOrderBy(),
() -> BasicCursor.this.offset,
() -> BasicCursor.this.rowCount,
() -> BasicCursor.this.fieldsForStatement
);
final PreparedStmtHolder count = new PreparedStmtHolder() {
@Override
protected PreparedStatement initStatement(List program) {
FromClause from = getFrom();
if (fromTerm == null) {
fromTerm = new FromTerm(from.getParameters());
}
WhereTerm where = qmaker.getWhereTerm();
fromTerm.programParams(program, db());
where.programParams(program, db());
return db().getSetCountStatement(conn(), from, where.getWhere());
}
};
/**
* Base holder class for a number of queries that depend on null mask on
* sort fields.
*/
abstract class OrderFieldsMaskedStatementHolder extends MaskedStatementHolder {
@Override
protected final int[] getNullsMaskIndices() {
if (orderByNames == null) {
orderBy();
}
return orderByIndices;
}
}
final PreparedStmtHolder position = new OrderFieldsMaskedStatementHolder() {
@Override
protected PreparedStatement initStatement(List program) {
FromClause from = getFrom();
if (fromTerm == null) {
fromTerm = new FromTerm(from.getParameters());
}
WhereTerm where = qmaker.getWhereTerm('<');
fromTerm.programParams(program, db());
where.programParams(program, db());
return db().getSetCountStatement(conn(), getFrom(), where.getWhere());
}
};
final PreparedStmtHolder forwards = new OrderFieldsMaskedStatementHolder() {
@Override
protected PreparedStatement initStatement(List program) {
FromClause from = getFrom();
if (fromTerm == null) {
fromTerm = new FromTerm(from.getParameters());
}
WhereTerm where = qmaker.getWhereTerm('>');
fromTerm.programParams(program, db());
where.programParams(program, db());
return db().getNavigationStatement(
conn(), getFrom(), getOrderBy(), where.getWhere(), fieldsForStatement, navigationOffset
);
}
};
final PreparedStmtHolder backwards = new OrderFieldsMaskedStatementHolder() {
@Override
protected PreparedStatement initStatement(List program) {
FromClause from = getFrom();
if (fromTerm == null) {
fromTerm = new FromTerm(from.getParameters());
}
WhereTerm where = qmaker.getWhereTerm('<');
fromTerm.programParams(program, db());
where.programParams(program, db());
return db().getNavigationStatement(
conn(), getFrom(), getReversedOrderBy(), where.getWhere(), fieldsForStatement, navigationOffset
);
}
};
final PreparedStmtHolder here = getHereHolder();
final PreparedStmtHolder first = new PreparedStmtHolder() {
@Override
protected PreparedStatement initStatement(List program) {
FromClause from = getFrom();
if (fromTerm == null) {
fromTerm = new FromTerm(from.getParameters());
}
WhereTerm where = qmaker.getWhereTerm();
fromTerm.programParams(program, db());
where.programParams(program, db());
return db().getNavigationStatement(
conn(), getFrom(), getOrderBy(), where.getWhere(), fieldsForStatement, 0
);
}
};
final PreparedStmtHolder last = new PreparedStmtHolder() {
@Override
protected PreparedStatement initStatement(List program) {
FromClause from = getFrom();
if (fromTerm == null) {
fromTerm = new FromTerm(from.getParameters());
}
WhereTerm where = qmaker.getWhereTerm();
fromTerm.programParams(program, db());
where.programParams(program, db());
return db().getNavigationStatement(
conn(), getFrom(), getReversedOrderBy(), where.getWhere(), fieldsForStatement, 0
);
}
};
// Filter and sort fields
private final Map filters = new HashMap<>();
private String[] orderByNames;
private int[] orderByIndices;
private boolean[] descOrders;
private long offset = 0;
private long navigationOffset = 0;
private long rowCount = 0;
private Expr complexFilter;
private final WhereTermsMaker qmaker = new WhereTermsMaker(new WhereMakerParamsProvider() {
@Override
public void initOrderBy() {
if (orderByNames == null) {
orderBy();
}
}
@Override
public QueryBuildingHelper dba() {
return db();
}
@Override
public String[] sortFields() {
return orderByNames;
}
@Override
public boolean[] descOrders() {
return descOrders;
}
@Override
public Map filters() {
return filters;
}
@Override
public Expr complexFilter() {
return complexFilter;
}
@Override
public In inFilter() {
return getIn();
}
@Override
public int[] sortFieldsIndices() {
return orderByIndices;
}
@Override
public Object[] values() {
return _currentValues();
}
@Override
public boolean isNullable(String columnName) {
return meta().getColumns().get(columnName).isNullable();
}
});
public BasicCursor(CallContext context) {
super(context);
}
public BasicCursor(CallContext context, Set fields) {
this(context);
if (!meta().getColumns().keySet().containsAll(fields)) {
throw new CelestaException("Not all of specified columns exist");
}
this.fields = fields;
prepareOrderBy();
fillFieldsForStatement();
}
static BasicCursor create(DataGrainElement element, CallContext callContext) {
try {
return getCursorClass(element).getConstructor(CallContext.class).newInstance(callContext);
} catch (ReflectiveOperationException ex) {
throw new CelestaException("Cursor creation failed for grain element: " + element.getName(), ex);
}
}
static BasicCursor create(DataGrainElement element, CallContext callContext, Set fields) {
try {
return getCursorClass(element)
.getConstructor(CallContext.class, Set.class).newInstance(callContext, fields);
} catch (ReflectiveOperationException ex) {
throw new CelestaException("Cursor creation failed for grain element: " + element.getName(), ex);
}
}
@SuppressWarnings("unchecked")
static Class extends BasicCursor> getCursorClass(DataGrainElement element) throws ClassNotFoundException {
final String namespace = element.getGrain().getNamespace().getValue();
String cursorClassName =
element.getName().substring(0, 1).toUpperCase() + element.getName().substring(1) + "Cursor";
cursorClassName = (namespace.isEmpty() ? "" : namespace + ".") + cursorClassName;
return (Class extends BasicCursor>) Class.forName(
cursorClassName, true, Thread.currentThread().getContextClassLoader());
}
/**
* Returns prepared statement holder which retrieves the current cursor position.
*/
PreparedStmtHolder getHereHolder() {
// To be overriden in Cursor class
return new OrderFieldsMaskedStatementHolder() {
@Override
protected PreparedStatement initStatement(List program) {
WhereTerm where = qmaker.getWhereTerm('=');
where.programParams(program, db());
return db().getNavigationStatement(
conn(), getFrom(), "", where.getWhere(), fieldsForStatement, 0
);
}
};
}
final void closeStatements(PreparedStmtHolder... stmts) {
for (PreparedStmtHolder stmt : stmts) {
stmt.close();
}
}
/**
* Releases all PreparedStatements of the cursor.
*/
@Override
protected void closeInternal() {
super.closeInternal();
closeStatements(set, forwards, backwards, here, first, last, count, position);
}
final Map getFilters() {
return filters;
}
@Override
public abstract DataGrainElement meta();
/**
* Whether the session has rights to insert data into current table.
*
* @return true, if current record can be inserted
*/
public boolean canInsert() {
if (isClosed()) {
throw new CelestaException(DATA_ACCESSOR_IS_CLOSED);
}
IPermissionManager permissionManager = callContext().getPermissionManager();
return permissionManager.isActionAllowed(callContext(), meta(), Action.INSERT);
}
/**
* Whether the session has rights to modify data of current table.
*
* @return true, if current record can be updated
*/
public boolean canModify() {
if (isClosed()) {
throw new CelestaException(DATA_ACCESSOR_IS_CLOSED);
}
IPermissionManager permissionManager = callContext().getPermissionManager();
return permissionManager.isActionAllowed(callContext(), meta(), Action.MODIFY);
}
/**
* Whether the session has rights to delete data from current table.
*
* @return true, if current record can be deleted
*/
public boolean canDelete() {
if (isClosed()) {
throw new CelestaException(DATA_ACCESSOR_IS_CLOSED);
}
IPermissionManager permissionManager = callContext().getPermissionManager();
return permissionManager.isActionAllowed(callContext(), meta(), Action.DELETE);
}
private void closeStmt(PreparedStatement stmt) {
try {
stmt.close();
} catch (SQLException e) {
throw new CelestaException(DATABASE_CLOSING_ERROR, _objectName(), e.getMessage());
}
}
protected final void closeSet() {
cursor = null;
set.close();
forwards.close();
backwards.close();
first.close();
last.close();
count.close();
position.close();
}
private String getOrderBy(boolean reverse) {
if (orderByNames == null) {
orderBy();
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < orderByNames.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(orderByNames[i]);
if (reverse ^ descOrders[i]) {
sb.append(" desc");
}
}
return sb.toString();
}
/**
* Returns "order by" clause for the cursor.
*
* @return
*/
public final String getOrderBy() {
return getOrderBy(false);
}
final List getOrderByFields() {
if (orderByNames == null) {
orderBy();
}
return Arrays.asList(orderByNames);
}
final String getReversedOrderBy() {
return getOrderBy(true);
}
/**
* Returns column names that are in sorting.
*
* @return
*/
public String[] orderByColumnNames() {
if (orderByNames == null) {
orderBy();
}
return orderByNames;
}
/**
* Returns mask of DESC orders.
*
* @return
*/
public boolean[] descOrders() {
if (orderByNames == null) {
orderBy();
}
return descOrders;
}
/**
* Moves to the first record in the filtered data set and returns information
* about the success of transition.
*
* @return {@code true} if the transition was successful,
* {@code false} if there are no records in the data set.
*/
public final boolean tryFindSet() {
if (!canRead()) {
throw new PermissionDeniedException(callContext(), meta(), Action.READ);
}
PreparedStatement ps = set.getStatement(_currentValues(), 0);
boolean result;
try {
if (cursor != null) {
cursor.close();
}
cursor = ps.executeQuery();
result = cursor.next();
if (result) {
_parseResult(cursor);
}
} catch (SQLException e) {
throw new CelestaException(e.getMessage(), e);
}
return result;
}
/**
* The same as navigate("-").
*
* @return
*/
public final boolean tryFirst() {
return navigate("-");
}
/**
* The same as tryFirst() but causes an error if no record is found.
*
* @return
*/
public final void first() {
if (!navigate("-")) {
raiseNotFound();
}
}
/**
* The same as navigate("+").
*
* @return
*/
public final boolean tryLast() {
return navigate("+");
}
/**
* The same as tryLast() but causes an error if no record is found.
*
* @return
*/
public final void last() {
if (!navigate("+")) {
raiseNotFound();
}
}
/**
* The same as navigate(">").
*
* @return
*/
public final boolean next() {
return navigate(">");
}
/**
* The same as navigate("<").
*
* @return
*/
public final boolean previous() {
return navigate("<");
}
private void raiseNotFound() {
StringBuilder sb = new StringBuilder();
for (Entry e : filters.entrySet()) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(String.format("%s=%s", e.getKey(), e.getValue().toString()));
}
throw new CelestaException("There is no %s (%s).", _objectName(), sb.toString());
}
/**
* Moves to the first record in the filtered data set causing an error in the case
* if the transition was not successful.
*/
public final void findSet() {
if (!tryFindSet()) {
raiseNotFound();
}
}
/**
* Returns current state of the cursor in form of CSV string with comma delimiters.
*
* @return
*/
public final String asCSVLine() {
Object[] values = _currentValues();
StringBuilder sb = new StringBuilder();
for (Object value : values) {
if (sb.length() > 0) {
sb.append(",");
}
if (value == null) {
sb.append("NULL");
} else {
quoteFieldForCSV(value.toString(), sb);
}
}
return sb.toString();
}
private static void quoteFieldForCSV(String fieldValue, StringBuilder sb) {
boolean needQuotes = false;
for (int i = 0; !needQuotes && i < fieldValue.length(); i++) {
char c = fieldValue.charAt(i);
needQuotes = c == '"' || c == ',';
}
if (needQuotes) {
sb.append('"');
for (int i = 0; i < fieldValue.length(); i++) {
char c = fieldValue.charAt(i);
sb.append(c);
if (c == '"') {
sb.append('"');
}
}
sb.append('"');
} else {
sb.append(fieldValue);
}
}
/**
* Moves to the next record in the sorted data set. Returns {@code false} if
* the end of the set is reached.
*
* @return
*/
public final boolean nextInSet() {
boolean result;
try {
if (cursor == null) {
result = tryFindSet();
} else {
result = cursor.next();
}
if (result) {
_parseResult(cursor);
} else {
cursor.close();
cursor = null;
}
} catch (SQLException e) {
result = false;
}
return result;
}
/**
* Navigation method (step-by-step transition in the filtered and sorted data set).
*
* @param command Command consisting of a sequence of symbols:
*
* - = update current record (if it exists in the filtered data set)
*
- > move to the next record in the filtered data set
* - < move to the previous record in the filtered data set
* - - move to the first record in the filtered data set
* - + move to the last record in the filtered data set
*
* @return {@code true} if the record was found and the transition completed
* {@code false} - otherwise.
*/
public boolean navigate(String command) {
if (!canRead()) {
throw new PermissionDeniedException(callContext(), meta(), Action.READ);
}
Matcher m = NAVIGATION.matcher(command);
if (!m.matches()) {
throw new CelestaException(
"Invalid navigation command: '%s', should consist of '+', '-', '>', '<' and '=' only!",
command);
}
if (navigationOffset != 0) {
closeStatements(backwards, forwards);
}
navigationOffset = 0;
for (int i = 0; i < command.length(); i++) {
char c = command.charAt(i);
PreparedStatement navigator = chooseNavigator(c);
if (executeNavigator(navigator)) {
return true;
}
}
return false;
}
/** Navigate forwards or backwards for the given offset.
*
* @param command Can be either '>' or '<' for forwards or backwards navigation.
* @param offset Offset.
*/
@SuppressWarnings("HiddenField")
public boolean navigate(String command, long offset) {
if (!canRead()) {
throw new PermissionDeniedException(callContext(), meta(), Action.READ);
}
Matcher m = NAVIGATION_WITH_OFFSET.matcher(command);
if (!m.matches()) {
throw new CelestaException(
"Invalid navigation command: '%s', should consist only one of '>' or '<'!",
command);
}
if (offset < 0) {
throw new CelestaException("Invalid navigation offset: offset should not be less than 0");
}
if (navigationOffset != offset) {
navigationOffset = offset;
closeStatements(backwards, forwards);
}
PreparedStatement navigator = chooseNavigator(command.charAt(0));
LOGGER.trace("{}", navigator);
return executeNavigator(navigator);
}
private boolean executeNavigator(PreparedStatement navigator) {
try {
LOGGER.trace("{}", navigator);
try (ResultSet rs = navigator.executeQuery()) {
if (rs.next()) {
_parseResult(rs);
return true;
}
}
} catch (SQLException e) {
throw new CelestaException(
String.format(NAVIGATING_ERROR, e.getMessage()), e);
}
return false;
}
private PreparedStatement chooseNavigator(char c) {
Object[] rec = _currentValues();
switch (c) {
case '<':
return backwards.getStatement(rec, 0);
case '>':
return forwards.getStatement(rec, 0);
case '=':
return here.getStatement(rec, 0);
case '-':
return first.getStatement(rec, 0);
case '+':
return last.getStatement(rec, 0);
default:
// THIS WILL NEVER EVER HAPPEN, WE'VE ALREADY CHECKED
return null;
}
}
final WhereTermsMaker getQmaker() {
return qmaker;
}
final ColumnMeta> validateColumnName(String name) {
ColumnMeta> column = meta().getColumns().get(name);
if (column == null) {
throw new CelestaException("No column %s exists in table %s.", name, _objectName());
}
return column;
}
private Object validateColumnValue(ColumnMeta> column, Object value) {
if (value == null) {
return null;
}
if (!column.getJavaClass().isAssignableFrom(value.getClass())) {
throw new CelestaException("Value %s is not of type %s.", value, column.getJavaClass());
}
return value;
}
/**
* Resets any filter on a field.
*
* @param name field name
*/
@Deprecated
public final void setRange(String name) {
setRange(validateColumnName(name));
}
/**
* Resets any filter on a field.
*
* @param column field column
*/
public final void setRange(ColumnMeta> column) {
validateColumnName(column.getName());
if (isClosed()) {
return;
}
// If filter was present on the field - reset the data set. If not - do nothing.
if (filters.remove(column.getName()) != null) {
closeSet();
}
}
/**
* Sets range from a single value on the field.
*
* @param name field name
* @param value value along which filtering is performed
*/
@Deprecated
public final void setRange(String name, Object value) {
@SuppressWarnings("unchecked")
ColumnMeta
© 2015 - 2025 Weber Informatics LLC | Privacy Policy