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-2023 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.docstore.DocStoreUtils.targetsDocStore;
import static com.sap.cds.util.CdsModelUtils.element;
import static com.sap.cds.util.CdsModelUtils.entity;
import static com.sap.cds.util.CdsTypeUtils.sqlTimestamp;
import static com.sap.cds.util.CqnStatementUtils.$JSON;
import static com.sap.cds.util.CqnStatementUtils.containsRef;
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 static java.util.stream.Collectors.toList;
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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.Select;
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.CdsElement;
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);
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) {
CdsModel model = context.getCdsModel();
CdsStructuredType targetType = CqnStatementUtils.targetType(model, select);
CqnStructuredTypeRef ref = null;
if (!CqnStatementUtils.containsPathExpression(select.where())) {
ref = CqnStatementUtils.targetRef(select);
}
List expandProcessors = emptyList();
if (!targetsDocStore(targetType)) {
boolean addKeys = !select.isDistinct() && select.groupBy().isEmpty() && containsRef(select.items());
Map> items = select.items().stream()
.collect(Collectors.partitioningBy(CqnSelectListItem::isExpand));
((Select>) select).columns(items.getOrDefault(Boolean.FALSE, emptyList()));
if (addKeys) { // TODO always select the keys if not explicitly excluded? include assoc keys
Set keys = targetType.concreteNonAssociationElements()
.filter(element -> element.isKey() && !element.getType().isStructured())
.map(CdsElement::getName).collect(Collectors.toSet());
CqnStatementUtils.selectHidden(keys, select);
}
List expands = items.get(Boolean.TRUE);
if (expands != null) {
boolean optimizeToManyExpands = addKeys && ref != null;
expandProcessors = new ArrayList<>(expands.size());
for (CqnSelectListItem expand : expands) {
ExpandProcessor expandProcessor = ExpandProcessor.create(model, ref, targetType, (CqnExpand) expand,
optimizeToManyExpands, select.top());
expandProcessor.addMappingKeys(select);
expandProcessors.add(expandProcessor);
}
}
}
SQLStatementBuilder.SQLStatement stmt = adapter.get().process(select);
return PreparedCqnStmt.create(stmt.sql(), select.items(), expandProcessors, select.excluding(), stmt.params(),
ref, targetType);
}
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");
}
}
private void setContextVariables(Map contextVariables) {
DbContext dbCtx = context.getDbContext();
try (Connection conn = ds.get()) {
dbCtx.getSessionVariableSetter().set(conn, contextVariables);
oldSessionVars.putAll(contextVariables);
} catch (SQLException e) {
throw new CdsDataStoreException(String.format("Failed to set context variables %s", contextVariables), e);
}
}
@Override
public ResultBuilder executeQuery(PreparedCqnStatement preparedStmt, Map parameterValues,
CdsDataStore dataStore, boolean isTransactionRequired) {
if (isTransactionRequired) {
requireTransaction();
}
PreparedCqnStmt pcqn = (PreparedCqnStmt) preparedStmt;
String sql = pcqn.toNative();
CdsStructuredType targetType = pcqn.targetType();
List params = pcqn.parameters();
Setter[] setters = createSetters(params);
List> rows;
try (Connection conn = ds.get()) {
rows = timed.debugSql(() -> {
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
bindValues(pstmt, parameterValues, params, setters, targetType);
try (ResultSet rs = pstmt.executeQuery()) {
return result(pcqn, rs, dataStore);
}
}
}, sql, List::size);
} catch (SQLException ex) {
chainNextExceptions(ex);
if (exceptionAnalyzer.isLockTimeout(ex)) {
throw new CdsLockTimeoutException(targetType);
}
throw new CdsDataStoreException("Error executing the statement", ex);
} catch (UndeclaredThrowableException ex) { // NOSONAR
throw new CdsDataStoreException("Error executing the statement", ex);
}
if (!rows.isEmpty()) {
// optimized to-many EXPANDs using path
pcqn.expands().stream().filter(ExpandProcessor::isPathExpand).forEach(
processor -> processor.expand(rows, dataStore, parameterValues));
if (!pcqn.excluding().isEmpty()) {
rows.forEach(row -> row.keySet().removeAll(pcqn.excluding()));
}
}
return selectedRows(rows);
}
@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()) {
int[] rowCount = timed.debugSql(() -> {
rejectAutoCommit(conn);
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
List params = pcqn.parameters();
if (parameterValues.size() > 1) {
return executeBatch(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());
return rowCount;
} catch (CdsException e) {
throw e;
} catch (Exception e) {
ExceptionHandler exHandler = new ExceptionHandler(entity, exceptionAnalyzer);
throw exHandler.cdsException(firstEntry(parameterValues), e);
}
}
private int[] executeBatch(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);
}
}
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, CdsDataStore dataStore)
throws SQLException {
CdsStructuredType targetType = pcqn.targetType();
int columnCount = dbResult.getMetaData().getColumnCount();
List selectValues = pcqn.selectListItems().stream().flatMap(CqnSelectListItem::ofValue)
.collect(toList());
List parentKeyExpands = pcqn.expands().stream().filter(ExpandProcessor::isParentKeyExpand)
.collect(Collectors.toList());
ColumnHandler[] columnHandlers = columnHandlers(targetType, selectValues, columnCount);
try {
if (parentKeyExpands.isEmpty()) {
List> rows = new ArrayList<>();
while (dbResult.next()) {
rows.add(extractData(dbResult, selectValues, null, columnHandlers));
}
return rows;
}
// EXPAND using parent-keys (n+1 selects) TODO optimize (cds-java 590)
return expandByParentKeys(dbResult, dataStore, parentKeyExpands, targetType, selectValues, columnHandlers);
} catch (SQLException e) {
chainNextExceptions(e);
throw new CdsDataStoreException("Failed to process result set", e);
}
}
private List> expandByParentKeys(ResultSet dbResult, CdsDataStore dataStore,
List expands, CdsStructuredType targetType, List selectList,
ColumnHandler[] columnHandlers) throws SQLException {
List> rows = new ArrayList<>();
while (dbResult.next()) {
AssociationLoader assocLoader = new AssociationLoader(dataStore, targetType);
Map data = extractData(dbResult, selectList, assocLoader, columnHandlers);
for (ExpandProcessor expandProcessor : expands) {
CqnExpand expand = expandProcessor.getExpand();
if (logger.isDebugEnabled()) {
logger.debug("Expand {} using parent-keys", expand.ref());
}
assocLoader.expand(expand, data);
}
rows.add(data);
}
return rows;
}
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) throws SQLException {
// 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.isPresent()) {
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,
AssociationLoader assocLoader, 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 (assocLoader != null) {
assocLoader.addValueOfRootEntity(slv, value);
}
if (column.hidden) {
DataUtils.createPath(row, column.displayName, value != null);
} else if (column.structuringAlias) {
resolvePathAndAdd(row, column.displayName, value);
} else if (value != null) {
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);
Map contextVariables = new HashMap<>();
String locale = LocaleUtils.getLocaleString(session.getLocale());
if (!Objects.equals(oldSessionVars.getOrDefault("LOCALE", INITIAL), locale)) {
contextVariables.put("LOCALE", locale);
}
int timestampPrecision = capabilities.timestampPrecision();
String validFrom = sqlTimestamp(session.getValidFrom(), timestampPrecision);
if (!Objects.equals(oldSessionVars.getOrDefault("VALID-FROM", INITIAL), validFrom)) {
contextVariables.put("VALID-FROM", validFrom);
}
String validTo = sqlTimestamp(session.getValidTo(), timestampPrecision);
if (!Objects.equals(oldSessionVars.getOrDefault("VALID-TO", INITIAL), validTo)) {
contextVariables.put("VALID-TO", validTo);
}
String user = session.getUserContext().getId() != null ? session.getUserContext().getId() : null;
if (!Objects.equals(oldSessionVars.getOrDefault("APPLICATIONUSER", INITIAL), user)) {
contextVariables.put("APPLICATIONUSER", user);
}
if (!contextVariables.isEmpty()) {
setContextVariables(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();
}
}