Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sap.cds.impl.JDBCClient Maven / Gradle / Ivy
/************************************************************************
* © 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.impl;
import static com.sap.cds.DataStoreConfiguration.MAX_BATCH_SIZE;
import static com.sap.cds.DataStoreConfiguration.MAX_BATCH_SIZE_DEFAULT;
import static com.sap.cds.ResultBuilder.selectedRows;
import static com.sap.cds.impl.ContextImpl.context;
import static com.sap.cds.impl.ExceptionHandler.chainNextExceptions;
import static com.sap.cds.impl.ExceptionHandler.dataStoreException;
import static com.sap.cds.impl.docstore.DocStoreUtils.targetsDocStore;
import static com.sap.cds.jdbc.spi.SessionVariableSetter.APPLICATIONUSER;
import static com.sap.cds.jdbc.spi.SessionVariableSetter.LOCALE;
import static com.sap.cds.jdbc.spi.SessionVariableSetter.TENANT;
import static com.sap.cds.jdbc.spi.SessionVariableSetter.VALID_FROM;
import static com.sap.cds.jdbc.spi.SessionVariableSetter.VALID_TO;
import static com.sap.cds.util.CdsModelUtils.element;
import static com.sap.cds.util.CdsModelUtils.entity;
import static com.sap.cds.util.CqnStatementUtils.$JSON;
import static com.sap.cds.util.CqnStatementUtils.isMediaType;
import static com.sap.cds.util.DataUtils.resolvePathAndAdd;
import static java.lang.System.arraycopy;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import java.io.Reader;
import java.lang.reflect.UndeclaredThrowableException;
import java.sql.BatchUpdateException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.sap.cds.CdsDataStore;
import com.sap.cds.CdsDataStoreConnector.Capabilities;
import com.sap.cds.CdsDataStoreException;
import com.sap.cds.CdsException;
import com.sap.cds.CdsLockTimeoutException;
import com.sap.cds.ResultBuilder;
import com.sap.cds.SessionContext;
import com.sap.cds.impl.PreparedCqnStmt.Parameter;
import com.sap.cds.impl.localized.LocaleUtils;
import com.sap.cds.impl.parser.JsonParser;
import com.sap.cds.impl.parser.StructDataParser;
import com.sap.cds.impl.parser.token.Jsonizer;
import com.sap.cds.impl.sql.SQLStatementBuilder;
import com.sap.cds.impl.sql.SQLStatementBuilder.SQLStatement;
import com.sap.cds.jdbc.spi.DbContext;
import com.sap.cds.jdbc.spi.ExceptionAnalyzer;
import com.sap.cds.jdbc.spi.ValueBinder;
import com.sap.cds.jdbc.spi.ValueBinder.Getter;
import com.sap.cds.jdbc.spi.ValueBinder.Setter;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.CdsDataException;
import com.sap.cds.ql.ElementRef;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnExpand;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnSelectListValue;
import com.sap.cds.ql.cqn.CqnStatement;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.impl.ExpandProcessor;
import com.sap.cds.reflect.CdsArrayedType;
import com.sap.cds.reflect.CdsBaseType;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.CdsType;
import com.sap.cds.transaction.TransactionManager;
import com.sap.cds.transaction.TransactionRequiredException;
import com.sap.cds.util.CqnStatementUtils;
import com.sap.cds.util.DataUtils;
public class JDBCClient implements ConnectedClient {
private static final Logger logger = LoggerFactory.getLogger(JDBCClient.class);
private static final TimingLogger timed = new TimingLogger(logger, false);
private static final Object INITIAL = new Object();
private final Supplier adapter;
private final TransactionManager transactionManager;
private final Supplier ds;
private final ValueBinder binder;
private final ExceptionAnalyzer exceptionAnalyzer;
private final Capabilities capabilities;
private final Map oldSessionVars = new HashMap<>();
private Context context;
private int maxBatchSize;
public JDBCClient(Context context, Supplier ds, TransactionManager transactionManager) {
this.context = context;
this.ds = ds;
this.transactionManager = transactionManager;
DbContext dbContext = context.getDbContext();
this.binder = dbContext.getBinder(context.getSessionContext().getTimeZone());
this.adapter = () -> new JdbcDataSourceAdapter(this.context);
this.exceptionAnalyzer = context.getDbContext().getExceptionAnalyzer();
this.capabilities = context.getDbContext().getCapabilities();
this.maxBatchSize = getMaxBatchSize(context);
}
@Override
public PreparedCqnStatement prepare(CqnStatement statement) {
if (statement.isSelect()) {
return prepare(statement.asSelect());
}
SQLStatement stmt = adapter.get().process(statement);
CdsEntity root = entity(context.getCdsModel(), statement.ref());
CqnStructuredTypeRef ref = null;
try {
ref = statement.ref();
} catch (CdsException ignore) {
// ignore for now.
}
return PreparedCqnStmt.createUpdate(stmt.sql(), stmt.params(), ref, root);
}
public PreparedCqnStmt prepare(CqnSelect select) {
CdsStructuredType targetType = CqnStatementUtils.targetType(context.getCdsModel(), select);
CqnStructuredTypeRef ref = null;
if (!CqnStatementUtils.containsPathExpression(select.where())) {
ref = CqnStatementUtils.targetRef(select);
}
return prepare(select, ref, targetType);
}
private PreparedCqnStmt prepare(CqnSelect select, CqnStructuredTypeRef ref, CdsStructuredType targetType) {
List expandProcessors = prepareExpands(select, context.getCdsModel(), targetType, ref);
SQLStatementBuilder.SQLStatement stmt = adapter.get().process(select);
return PreparedCqnStmt.create(stmt.sql(), select.items(), expandProcessors, select.excluding(), stmt.params(),
ref, targetType);
}
private static List prepareExpands(CqnSelect select, CdsModel model, CdsStructuredType targetType,
CqnStructuredTypeRef ref) {
if (targetsDocStore(targetType)) {
return emptyList();
}
List expands = CqnStatementUtils.removeExpands(select);
List expandProcessors = new ArrayList<>(expands.size());
if (!expands.isEmpty()) {
boolean addMissingKeys = !select.isDistinct() && CqnStatementUtils.isNoAggregation(select);
boolean optimizeToManyExpands = addMissingKeys && ref != null;
Map keyAliases = CqnStatementUtils.selectedKeys(select, targetType, addMissingKeys);
for (CqnExpand expand : expands) {
ExpandProcessor expandProcessor = ExpandProcessor.create(select, model, ref, targetType, keyAliases,
expand, optimizeToManyExpands);
expandProcessor.addMappingKeys(select);
expandProcessors.add(expandProcessor);
}
}
return expandProcessors;
}
private static int append(int[] arr, int[] elements, int pos) {
arraycopy(elements, 0, arr, pos, elements.length);
return pos + elements.length;
}
private static void rejectAutoCommit(Connection conn) throws SQLException {
if (conn.getAutoCommit()) {
throw new TransactionRequiredException("Connection must not be in auto-commit mode");
}
}
@Override
public ResultBuilder executeQuery(PreparedCqnStatement preparedStmt, Map paramValues,
CdsDataStore dataStore, boolean isTransactionRequired) {
if (isTransactionRequired) {
requireTransaction();
}
PreparedCqnStmt stmt = (PreparedCqnStmt) preparedStmt;
while (stmt != null) {
try {
List> rows = executeQuery(stmt, paramValues);
if (!rows.isEmpty()) {
executeExpands(stmt.expands(), paramValues, dataStore, stmt.targetType(), rows);
List excluding = stmt.excluding();
if (!excluding.isEmpty()) {
rows.forEach(row -> row.keySet().removeAll(excluding));
}
}
return selectedRows(rows);
} catch (SQLException ex) {
chainNextExceptions(ex);
if (exceptionAnalyzer.isLockTimeout(ex)) {
throw new CdsLockTimeoutException(stmt.targetType());
}
throw dataStoreException(ex, stmt.toNative());
}
}
throw new IllegalStateException("PreparedCqnStatement must not be null");
}
private List> executeQuery(PreparedCqnStmt pcqn, Map paramValues) throws SQLException {
String sql = pcqn.toNative();
List params = pcqn.parameters();
Setter[] setters = createSetters(params);
try (Connection conn = ds.get()) {
return timed.sql(() -> {
establishSessionVariables(conn);
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
bindValues(pstmt, paramValues, params, setters, pcqn.targetType());
try (ResultSet rs = pstmt.executeQuery()) {
return result(pcqn, rs);
}
}
}, sql, List::size);
} catch (UndeclaredThrowableException ex) { // NOSONAR
Throwable rootCause = ExceptionAnalyzer.getRootCause(ex);
if (rootCause instanceof SQLException e) {
throw e;
}
throw dataStoreException(ex, sql);
}
}
private void executeExpands(List expands, Map paramValues, CdsDataStore dataStore,
CdsStructuredType targetType, List> rows) {
AssociationLoader assocLoader = new AssociationLoader(dataStore, context.getDbContext(), targetType);
for (ExpandProcessor expandProcessor : expands) {
if (expandProcessor.isPathExpand()) { // to-many EXPANDs using path
expandProcessor.expand(rows, dataStore, paramValues);
} else { // parent-keys expand
assocLoader.expand(expandProcessor, rows);
}
if (expandProcessor.hasCountAndLimit()) {
expandProcessor.inlineCount(rows, dataStore, paramValues);
}
}
}
@Override
public int[] executeUpdate(PreparedCqnStatement preparedStmt, List> parameterValues) {
PreparedCqnStmt pcqn = (PreparedCqnStmt) preparedStmt;
requireTransaction();
String sql = pcqn.toNative();
CdsEntity entity = pcqn.targetType();
try (Connection conn = ds.get()) {
return timed.sql(() -> {
rejectAutoCommit(conn);
establishSessionVariables(conn);
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
List params = pcqn.parameters();
if (parameterValues.size() > 1) {
return executeBatch(sql, pstmt, params, parameterValues, entity);
}
Map values = firstEntry(parameterValues);
Setter[] setters = createSetters(params);
bindValues(pstmt, values, params, setters, entity);
return new int[] { pstmt.executeUpdate() };
}
}, sql, rc -> Arrays.stream(rc).sum());
} catch (CdsException e) {
throw e;
} catch (Exception e) {
ExceptionHandler exHandler = new ExceptionHandler(entity, exceptionAnalyzer);
throw exHandler.cdsException(firstEntry(parameterValues), e, sql);
}
}
private int[] executeBatch(String sql, PreparedStatement pstmt, List params,
List> entries, CdsEntity entity) throws SQLException {
int row = 0;
int rcPosition = 0;
int[] result = new int[entries.size()];
try {
Setter[] setters = createSetters(params);
for (Map entry : entries) {
bindValues(pstmt, entry, params, setters, entity);
pstmt.addBatch();
row++;
if (row % maxBatchSize == 0) {
int[] rc = pstmt.executeBatch();
rcPosition = append(result, rc, rcPosition);
}
}
int[] rc = pstmt.executeBatch();
rcPosition = append(result, rc, rcPosition); // NOSONAR
return result;
} catch (BatchUpdateException ex) {
chainNextExceptions(ex);
throw new ExceptionHandler(entity, exceptionAnalyzer).cdsBatchException(entries, rcPosition, ex, sql);
}
}
private Setter[] createSetters(List params) {
int size = params.size();
Setter[] setters = new Setter[size + 1];
for (int col = 1; col <= size; col++) {
Parameter param = params.get(col - 1);
Setter setter = binder.setter(param.type());
setters[col] = setter::set;
}
return setters;
}
private void requireTransaction() {
if (!transactionManager.isActive()) {
throw new TransactionRequiredException();
}
}
private void bindValues(PreparedStatement pstmt, Map values, List params,
Setter[] binders, CdsStructuredType entity) throws SQLException {
for (int col = 1; col <= params.size(); col++) {
Parameter param = params.get(col - 1);
Object value = param.get(values);
if (value != null && Collection.class.isAssignableFrom(value.getClass())) {
// TODO: docstore - refactor arrayed elements parameters to JSON parameter
value = Jsonizer.json(value);
}
try {
binders[col].set(pstmt, col, value);
} catch (IllegalArgumentException | NullPointerException e) {
throw new CdsDataException(
"Invalid value for '" + entity + "." + param.name() + "' of type " + param.type(), e);
}
}
}
private List> result(PreparedCqnStmt pcqn, ResultSet dbResult) throws SQLException {
CdsStructuredType targetType = pcqn.targetType();
int columnCount = dbResult.getMetaData().getColumnCount();
List selectValues = pcqn.selectListItems().stream().flatMap(CqnSelectListItem::ofValue)
.toList();
ColumnHandler[] columnHandlers = columnHandlers(targetType, selectValues, columnCount);
try {
List> rows = new ArrayList<>(); // TODO use top as size?
while (dbResult.next()) {
rows.add(extractData(dbResult, selectValues, columnHandlers));
}
return rows;
} catch (SQLException e) {
chainNextExceptions(e);
throw new CdsDataStoreException("Failed to process result set", e);
}
}
private class ColumnHandler {
final String displayName;
final boolean hidden;
final boolean structuringAlias;
final Getter valueExtractor;
ColumnHandler(String displayName, Getter valueExtractor) {
this.displayName = displayName;
this.hidden = displayName.endsWith("?");
this.structuringAlias = displayName.contains(".");
this.valueExtractor = valueExtractor;
}
}
private ColumnHandler[] columnHandlers(CdsStructuredType targetType, List selectList,
int columnCount) {
// on docstore entities ($json) the column count can be lower than the length of
// selectList
ColumnHandler[] columnHandlers = new ColumnHandler[columnCount];
for (int i = 0; i < columnCount; i++) {
CqnSelectListValue slv = selectList.get(i);
String displayName = slv.displayName();
columnHandlers[i] = createHandler(targetType, slv, displayName);
}
return columnHandlers;
}
private ColumnHandler createHandler(CdsStructuredType targetType, CqnSelectListValue slv, String displayName) {
if (slv.value().isRef()) {
CqnElementRef ref = slv.asRef();
CdsType type = element(targetType, ref).getType();
if (type.isArrayed()) {
CdsType itemsType = type.as(CdsArrayedType.class).getItemsType();
Getter typeMapper = binder.getter(CdsBaseType.LARGE_STRING, false);
Getter valueExtractor = (result, col) -> {
String json = (String) typeMapper.get(result, col);
return StructDataParser.parseArrayOf(itemsType, json);
};
return new ColumnHandler(displayName, valueExtractor);
} else if (displayName.endsWith($JSON)) {
Getter typeMapper = binder.getter(CdsBaseType.LARGE_STRING, true);
Getter valueExtractor = (result, col) -> {
Reader reader = (Reader) typeMapper.get(result, col);
if (reader == null) {
return emptyMap();
}
return JsonParser.map(reader);
};
String prefix = displayName.substring(0, displayName.lastIndexOf($JSON));
return new ColumnHandler(prefix, valueExtractor);
}
}
Optional cdsType = CqnStatementUtils.getCdsType(targetType, slv.value());
if (cdsType.isEmpty()) {
logger.debug("Cannot determine CDS type of {}", slv.value());
}
boolean mediaType = isMediaType(targetType, slv);
Getter valueExtractor = binder.getter(cdsType.orElse(null), mediaType);
return new ColumnHandler(displayName, valueExtractor);
}
@SuppressWarnings("unchecked")
private Map extractData(ResultSet result, List selectList,
ColumnHandler[] columnHandlers) throws SQLException {
Map row = new HashMap<>(selectList.size());
for (int i = 1; i <= columnHandlers.length; i++) {
ColumnHandler column = columnHandlers[i - 1];
Object value = column.valueExtractor.get(result, i);
CqnSelectListValue slv = selectList.get(i - 1);
if (value instanceof Map) { // JSON element
mergeObject(row, slv, (Map) value, column.displayName);
} else {
if (column.hidden) {
DataUtils.createPath(row, column.displayName, value != null);
} else if (column.structuringAlias) {
resolvePathAndAdd(row, column.displayName, value);
} else {
row.put(column.displayName, value);
}
}
}
return row;
}
private void mergeObject(Map data, CqnSelectListValue slv, Map mapValue,
String displayName) {
CqnElementRef jsonRef = slv.asRef();
mapValue.forEach((k, v) -> {
ElementRef> innerRef = CQL.to(jsonRef.segments().subList(0, jsonRef.segments().size() - 1)).get(k);
CqnSelectListValue innerSlv = innerRef.as(displayName + k);
resolvePathAndAdd(data, innerSlv.displayName(), v);
});
}
private Map firstEntry(List> valueList) {
return valueList.isEmpty() ? emptyMap() : valueList.get(0);
}
@Override
public void setSessionContext(SessionContext session) {
this.context = context(context.getCdsModel(), context.getDbContext(), session,
context.getDataStoreConfiguration());
this.maxBatchSize = getMaxBatchSize(context);
// eagerly set session variables
if (transactionManager.isActive()) {
try (Connection conn = ds.get()) {
establishSessionVariables(conn);
} catch (SQLException e) {
throw new CdsDataStoreException("Failed to eagerly set context variables on transaction", e);
}
}
}
@VisibleForTesting
void establishSessionVariables(Connection conn) {
SessionContext session = context.getSessionContext();
boolean txIsActive = transactionManager.isActive();
ContextVars contextVars = new ContextVars(session, !txIsActive);
contextVars.putIfRequired(LOCALE, s -> LocaleUtils.getLocaleString(s.getLocale()));
contextVars.putIfRequired(VALID_FROM, s -> s.getValidFrom());
contextVars.putIfRequired(VALID_TO, s -> s.getValidTo());
contextVars.putIfRequired(APPLICATIONUSER, s -> s.getUserContext() != null ? s.getUserContext().getId() : null);
contextVars.putIfRequired(TENANT, s -> s.getUserContext() != null ? s.getUserContext().getTenant() : null);
if (!contextVars.isEmpty()) {
Map vars = contextVars.get();
try {
context.getDbContext().getSessionVariableSetter().set(conn, vars);
// only changed vars need to be updated in case of tx
// so let's keep track of them
if (txIsActive) {
oldSessionVars.putAll(vars);
}
} catch (SQLException e) {
throw new CdsDataStoreException("Failed to set context variables %s".formatted(vars), e);
}
}
}
private class ContextVars {
private final SessionContext session;
private final boolean enforce;
Map contextVariables = new HashMap<>();
public ContextVars(SessionContext session, boolean enforce) {
this.session = session;
this.enforce = enforce;
}
private void putIfRequired(String key, Function valueSupplier) {
Object value = valueSupplier.apply(session);
if (enforce || !Objects.equals(oldSessionVars.getOrDefault(key, INITIAL), value)) {
contextVariables.put(key, value);
}
}
private boolean isEmpty() {
return contextVariables.isEmpty();
}
private Map get() {
return contextVariables;
}
}
private static int getMaxBatchSize(Context context) {
return Math.max(1, context.getDataStoreConfiguration().getProperty(MAX_BATCH_SIZE, MAX_BATCH_SIZE_DEFAULT));
}
@Override
public Capabilities capabilities() {
return capabilities;
}
@Override
public void setRollbackOnly() {
transactionManager.setRollbackOnly();
}
@Override
public void deleteAll(Stream entities) {
DbContext db = context.getDbContext();
int i = 0;
try (Connection conn = ds.get()) {
establishSessionVariables(conn);
try (Statement stmt = conn.createStatement()) {
Iterator iter = entities.map(e -> deleteStatement(db, e)).iterator();
while (iter.hasNext()) {
stmt.addBatch(iter.next());
if (++i % maxBatchSize == 0)
stmt.executeBatch();
}
stmt.executeBatch();
}
} catch (SQLException e) {
throw new CdsDataStoreException("Failed to delete all entities", e);
}
}
private static String deleteStatement(DbContext db, CdsEntity entity) {
String tableName = db.getSqlMapping(entity).tableName();
return db.getStatementResolver().deleteAll(tableName);
}
}