package com.battery.device.neware;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import javax.sql.DataSource;
/**
* 厂商数据源的只读防线:在 JDBC 调用到达驱动之前按语句白名单放行。
*
*
只允许 {@code SELECT} / {@code SHOW} / {@code DESCRIBE}(含 {@code DESC});
* 其余语句(含存储过程调用)直接抛 {@link NonQueryStatementException},不转发给驱动,
* 因此在服务端不产生任何效果。被拒绝的语句记入 {@link ReadOnlyStatementAudit},
* 使"该数据源从未发出过非查询语句"可以被测试断言。
*
*
防线是结构性的,不是数学上的:任何绕过本数据源另开连接的代码仍能写。
* 服务端只读账号(见现场对接清单第七节)是第一道防线,本层是第二道。
*
*
{@link DataSource} 接口中与只读边界无关的 JDK 样板方法(日志写出器、登录超时、
* 父日志器、{@code unwrap} / {@code isWrapperFor} / {@code toString})直接转发或拒绝驱动对象,
* 不承载业务语义,因此不单独加方法注释。
*/
public final class ReadOnlyVendorDataSource implements DataSource
{
private static final List ALLOWED_PREFIXES = List.of("select", "show", "describe", "desc");
/**
* 加锁读会真的阻塞厂商写入,所以它虽然以 {@code select} 开头也要单独拒绝:
* 现场实测厂商库 {@code tx_isolation=REPEATABLE-READ},行锁会挡住上位机的写库。
*/
private static final List LOCKING_CLAUSES = List.of("for update", "for share",
"lock in share mode", "updlock", "holdlock", "xlock", "tablock");
private final DataSource delegate;
private final String sourceLabel;
private final ReadOnlyStatementAudit audit = new ReadOnlyStatementAudit();
public ReadOnlyVendorDataSource(DataSource delegate, String sourceLabel)
{
if (delegate == null) throw new IllegalArgumentException("delegate data source must not be null");
this.delegate = delegate;
this.sourceLabel = sourceLabel == null || sourceLabel.isBlank() ? "vendor" : sourceLabel.trim();
}
/** 返回本数据源上的语句审计,用于断言从未发出过非查询语句。 */
public ReadOnlyStatementAudit audit()
{
return audit;
}
/** 取一条受只读约束的连接;语句白名单在返回的连接代理上生效。 */
@Override
public Connection getConnection() throws SQLException
{
return guard(delegate.getConnection());
}
/** 同上,按给定账号取连接;账号仍受只读白名单约束。 */
@Override
public Connection getConnection(String username, String password) throws SQLException
{
return guard(delegate.getConnection(username, password));
}
private Connection guard(Connection connection)
{
return (Connection) Proxy.newProxyInstance(connection.getClass().getClassLoader(),
new Class>[] { Connection.class }, (proxy, method, args) -> {
switch (method.getName())
{
case "prepareStatement" ->
{
String sql = (String) args[0];
requireQuery(sql);
return guardStatement((Statement) invoke(connection, method, args), sql, true);
}
case "prepareCall" -> throw new NonQueryStatementException(
"厂商数据源禁止存储过程调用:" + abbreviate((String) args[0]));
case "createStatement" ->
{
return guardStatement((Statement) invoke(connection, method, args), null, false);
}
default -> { }
}
return invoke(connection, method, args);
});
}
private Statement guardStatement(Statement statement, String preparedSql, boolean prepared)
{
Class> statementType = prepared ? PreparedStatement.class : Statement.class;
return (Statement) Proxy.newProxyInstance(statement.getClass().getClassLoader(),
new Class>[] { statementType }, (proxy, method, args) -> {
String sql = sqlArgument(method, args);
if (sql != null) requireQuery(sql);
return invoke(statement, method, args);
});
}
/**
* 取出本次调用携带的 SQL 文本:语句级方法带 SQL 时按该 SQL 判定,
* 预编译语句的无参执行按 prepare 时的 SQL 判定(已在 prepareStatement 中放行)。
*/
private static String sqlArgument(Method method, Object[] args)
{
if (args == null || args.length == 0 || !(args[0] instanceof String sql)) return null;
return switch (method.getName())
{
case "execute", "executeUpdate", "executeLargeUpdate", "executeQuery", "addBatch", "executeBatch",
"executeLargeBatch" -> sql;
default -> null;
};
}
private void requireQuery(String sql)
{
boolean query = isQuery(sql);
boolean locking = query && containsLockingClause(sql);
if (query && !locking)
{
audit.recordForwarded(sql);
return;
}
audit.recordRejected(sql);
throw new NonQueryStatementException(locking
? "厂商数据源禁止加锁读(会阻塞厂商写入):" + abbreviate(sql)
: "厂商数据源只允许查询语句:" + abbreviate(sql));
}
/** 只按语句首关键字判定,避免用正则近似 SQL 语法。 */
static boolean isQuery(String sql)
{
if (sql == null) return false;
String normalized = stripLeadingComments(sql).toLowerCase(Locale.ROOT);
if (normalized.isEmpty()) return false;
for (String prefix : ALLOWED_PREFIXES)
{
if (!normalized.startsWith(prefix)) continue;
int boundary = prefix.length();
if (normalized.length() == boundary || Character.isWhitespace(normalized.charAt(boundary))
|| normalized.charAt(boundary) == '(' || normalized.charAt(boundary) == '*'
|| normalized.charAt(boundary) == '`')
return true;
}
return false;
}
/**
* 语句里是否带加锁读子句。这是**语法层的拦截**,不是 SQL 解析器:厂商取数的语句由本模块固定给出,
* 不会把加锁子句藏在字符串常量里;新增取数语句时由 {@code ReadOnlyVendorDataSourceTest} 一起覆盖。
*/
static boolean containsLockingClause(String sql)
{
if (sql == null) return false;
String normalized = stripLeadingComments(sql).toLowerCase(Locale.ROOT).replaceAll("\\s+", " ");
for (String clause : LOCKING_CLAUSES)
{
if (normalized.contains(clause)) return true;
}
return false;
}
private static String stripLeadingComments(String sql)
{
String remaining = sql.trim();
while (true)
{
if (remaining.startsWith("/*"))
{
int end = remaining.indexOf("*/");
if (end < 0) return remaining;
remaining = remaining.substring(end + 2).trim();
continue;
}
if (remaining.startsWith("--") || remaining.startsWith("#"))
{
int end = remaining.indexOf('\n');
if (end < 0) return "";
remaining = remaining.substring(end + 1).trim();
continue;
}
return remaining;
}
}
private static String abbreviate(String sql)
{
if (sql == null) return "";
String trimmed = sql.trim();
return trimmed.length() <= 200 ? trimmed : trimmed.substring(0, 200);
}
private static Object invoke(Object target, Method method, Object[] args) throws Throwable
{
try
{
return method.invoke(target, args);
}
catch (InvocationTargetException exception)
{
throw exception.getTargetException();
}
}
@Override
public PrintWriter getLogWriter() throws SQLException
{
return delegate.getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException
{
delegate.setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException
{
delegate.setLoginTimeout(seconds);
}
@Override
public int getLoginTimeout() throws SQLException
{
return delegate.getLoginTimeout();
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException
{
return delegate.getParentLogger();
}
@Override
public T unwrap(Class iface) throws SQLException
{
if (iface.isInstance(this)) return iface.cast(this);
throw new SQLFeatureNotSupportedException("厂商数据源不暴露未受约束的驱动对象");
}
@Override
public boolean isWrapperFor(Class> iface)
{
return iface.isInstance(this);
}
@Override
public String toString()
{
return "ReadOnlyVendorDataSource[" + sourceLabel + "]";
}
/** 便于测试与排查的来源说明。 */
public String sourceLabel()
{
return sourceLabel;
}
}