feat: add mysql vehicle identity store
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -40,6 +40,9 @@ public final class FileVehicleIdentityService implements VehicleIdentityResolver
|
||||
|
||||
@Override
|
||||
public void bind(VehicleIdentityBinding binding) {
|
||||
if (binding == null || !binding.hasResolvedVin()) {
|
||||
return;
|
||||
}
|
||||
delegate.bind(binding);
|
||||
synchronized (writeLock) {
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,9 @@ public final class InMemoryVehicleIdentityService implements VehicleIdentityReso
|
||||
|
||||
@Override
|
||||
public void bind(VehicleIdentityBinding binding) {
|
||||
if (binding == null || !binding.hasResolvedVin()) {
|
||||
return;
|
||||
}
|
||||
if (!binding.phone().isBlank()) {
|
||||
phoneToVin.put(key(binding.protocol(), binding.phone()), binding.vin());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.lingniu.ingest.identity;
|
||||
|
||||
import com.lingniu.ingest.api.ProtocolId;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class MySqlVehicleIdentityService implements VehicleIdentityResolver, VehicleIdentityRegistry {
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final String table;
|
||||
|
||||
public MySqlVehicleIdentityService(DataSource dataSource, String table) {
|
||||
if (dataSource == null) {
|
||||
throw new IllegalArgumentException("dataSource must not be null");
|
||||
}
|
||||
this.dataSource = dataSource;
|
||||
this.table = sanitizeTable(table);
|
||||
initializeSchema();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(VehicleIdentityBinding binding) {
|
||||
if (binding == null || !binding.hasResolvedVin()) {
|
||||
return;
|
||||
}
|
||||
upsert(binding.protocol(), "PHONE", binding.phone(), binding.vin());
|
||||
upsert(binding.protocol(), "DEVICE_ID", binding.deviceId(), binding.vin());
|
||||
upsert(binding.protocol(), "PLATE", binding.plate(), binding.vin());
|
||||
}
|
||||
|
||||
@Override
|
||||
public VehicleIdentity resolve(VehicleIdentityLookup lookup) {
|
||||
if (lookup == null) {
|
||||
return new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN);
|
||||
}
|
||||
if (!lookup.vin().isBlank()) {
|
||||
return new VehicleIdentity(lookup.vin(), true, VehicleIdentitySource.EXPLICIT_VIN);
|
||||
}
|
||||
VehicleIdentity phone = resolveBound(lookup.protocol(), "PHONE", lookup.phone(), VehicleIdentitySource.BOUND_PHONE);
|
||||
if (phone != null) return phone;
|
||||
VehicleIdentity device = resolveBound(lookup.protocol(), "DEVICE_ID", lookup.deviceId(), VehicleIdentitySource.BOUND_DEVICE_ID);
|
||||
if (device != null) return device;
|
||||
VehicleIdentity plate = resolveBound(lookup.protocol(), "PLATE", lookup.plate(), VehicleIdentitySource.BOUND_PLATE);
|
||||
if (plate != null) return plate;
|
||||
if (!lookup.deviceId().isBlank()) {
|
||||
return new VehicleIdentity(lookup.deviceId(), false, VehicleIdentitySource.FALLBACK_DEVICE_ID);
|
||||
}
|
||||
if (!lookup.phone().isBlank()) {
|
||||
return new VehicleIdentity(lookup.phone(), false, VehicleIdentitySource.FALLBACK_PHONE);
|
||||
}
|
||||
if (!lookup.plate().isBlank()) {
|
||||
return new VehicleIdentity(lookup.plate(), false, VehicleIdentitySource.FALLBACK_PLATE);
|
||||
}
|
||||
return new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN);
|
||||
}
|
||||
|
||||
private void initializeSchema() {
|
||||
String sql = """
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
protocol VARCHAR(32) NOT NULL,
|
||||
identifier_type VARCHAR(32) NOT NULL,
|
||||
identifier_value VARCHAR(128) NOT NULL,
|
||||
vin VARCHAR(64) NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (protocol, identifier_type, identifier_value),
|
||||
KEY idx_vehicle_identity_vin (vin)
|
||||
)
|
||||
""".formatted(table);
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute(sql);
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("vehicle identity mysql schema initialize failed: " + table, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void upsert(ProtocolId protocol, String identifierType, String identifierValue, String vin) {
|
||||
String normalizedIdentifier = normalize(identifierValue);
|
||||
if (normalizedIdentifier.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String sql = "INSERT INTO " + table + " (protocol, identifier_type, identifier_value, vin) "
|
||||
+ "VALUES (?, ?, ?, ?) "
|
||||
+ "ON DUPLICATE KEY UPDATE vin = VALUES(vin), updated_at = CURRENT_TIMESTAMP";
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, protocolName(protocol));
|
||||
statement.setString(2, identifierType);
|
||||
statement.setString(3, normalizedIdentifier);
|
||||
statement.setString(4, vin.trim());
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("vehicle identity mysql bind failed: " + table, e);
|
||||
}
|
||||
}
|
||||
|
||||
private VehicleIdentity resolveBound(ProtocolId protocol,
|
||||
String identifierType,
|
||||
String identifierValue,
|
||||
VehicleIdentitySource source) {
|
||||
String normalizedIdentifier = normalize(identifierValue);
|
||||
if (normalizedIdentifier.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String sql = "SELECT vin FROM " + table
|
||||
+ " WHERE protocol = ? AND identifier_type = ? AND identifier_value = ? LIMIT 1";
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, protocolName(protocol));
|
||||
statement.setString(2, identifierType);
|
||||
statement.setString(3, normalizedIdentifier);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
if (resultSet.next()) {
|
||||
return new VehicleIdentity(resultSet.getString(1), true, source);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("vehicle identity mysql resolve failed: " + table, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String sanitizeTable(String table) {
|
||||
String value = table == null || table.isBlank() ? "vehicle_identity_binding" : table.trim();
|
||||
if (!value.matches("[A-Za-z0-9_]+")) {
|
||||
throw new IllegalArgumentException("vehicle identity mysql table must contain only letters, digits, and underscore");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String protocolName(ProtocolId protocol) {
|
||||
return protocol == null ? "UNKNOWN" : protocol.name();
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,10 @@ public record VehicleIdentityBinding(
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasResolvedVin() {
|
||||
return !"unknown".equalsIgnoreCase(vin);
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.lingniu.ingest.identity.config;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.lingniu.ingest.identity.FileVehicleIdentityService;
|
||||
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
|
||||
import com.lingniu.ingest.identity.MySqlVehicleIdentityService;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityRegistry;
|
||||
import com.lingniu.ingest.identity.VehicleIdentityResolver;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
@@ -11,7 +12,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.SQLFeatureNotSupportedException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(VehicleIdentityProperties.class)
|
||||
@@ -32,6 +40,23 @@ public class VehicleIdentityAutoConfiguration {
|
||||
return new FileVehicleIdentityService(Path.of(properties.getFile().getPath()), objectMapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(DataSource.class)
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "mysql")
|
||||
public DataSource vehicleIdentityDataSource(VehicleIdentityProperties properties) {
|
||||
VehicleIdentityProperties.Mysql mysql = properties.getMysql();
|
||||
return new DriverManagerVehicleIdentityDataSource(
|
||||
mysql.getDriverClassName(), mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean({VehicleIdentityResolver.class, VehicleIdentityRegistry.class})
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "mysql")
|
||||
public MySqlVehicleIdentityService mySqlVehicleIdentityService(VehicleIdentityProperties properties,
|
||||
DataSource dataSource) {
|
||||
return new MySqlVehicleIdentityService(dataSource, properties.getMysql().getTable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean({VehicleIdentityResolver.class, VehicleIdentityRegistry.class})
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "memory",
|
||||
@@ -39,4 +64,61 @@ public class VehicleIdentityAutoConfiguration {
|
||||
public InMemoryVehicleIdentityService vehicleIdentityService() {
|
||||
return new InMemoryVehicleIdentityService();
|
||||
}
|
||||
|
||||
private static final class DriverManagerVehicleIdentityDataSource implements DataSource {
|
||||
private final String jdbcUrl;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private PrintWriter logWriter;
|
||||
private int loginTimeout;
|
||||
|
||||
private DriverManagerVehicleIdentityDataSource(String driverClassName,
|
||||
String jdbcUrl,
|
||||
String username,
|
||||
String password) {
|
||||
if (jdbcUrl == null || jdbcUrl.isBlank()) {
|
||||
throw new IllegalArgumentException("lingniu.ingest.identity.mysql.jdbc-url is required");
|
||||
}
|
||||
if (driverClassName != null && !driverClassName.isBlank()) {
|
||||
try {
|
||||
Class.forName(driverClassName.trim());
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IllegalArgumentException("vehicle identity mysql driver class not found: "
|
||||
+ driverClassName, e);
|
||||
}
|
||||
}
|
||||
this.jdbcUrl = jdbcUrl;
|
||||
this.username = username == null ? "" : username;
|
||||
this.password = password == null ? "" : password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
DriverManager.setLoginTimeout(loginTimeout);
|
||||
return DriverManager.getConnection(jdbcUrl, username, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection(String username, String password) throws SQLException {
|
||||
DriverManager.setLoginTimeout(loginTimeout);
|
||||
return DriverManager.getConnection(jdbcUrl, username, password);
|
||||
}
|
||||
|
||||
@Override public PrintWriter getLogWriter() { return logWriter; }
|
||||
@Override public void setLogWriter(PrintWriter out) { this.logWriter = out; }
|
||||
@Override public void setLoginTimeout(int seconds) { this.loginTimeout = seconds; }
|
||||
@Override public int getLoginTimeout() { return loginTimeout; }
|
||||
@Override public Logger getParentLogger() throws SQLFeatureNotSupportedException {
|
||||
throw new SQLFeatureNotSupportedException();
|
||||
}
|
||||
@Override public <T> T unwrap(Class<T> iface) throws SQLException {
|
||||
if (iface.isInstance(this)) {
|
||||
return iface.cast(this);
|
||||
}
|
||||
throw new SQLException("not a wrapper for " + iface);
|
||||
}
|
||||
@Override public boolean isWrapperFor(Class<?> iface) {
|
||||
return iface.isInstance(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ public class VehicleIdentityProperties {
|
||||
|
||||
private String store = "memory";
|
||||
private File file = new File();
|
||||
private Mysql mysql = new Mysql();
|
||||
|
||||
public String getStore() { return store; }
|
||||
public void setStore(String store) { this.store = store; }
|
||||
public File getFile() { return file; }
|
||||
public void setFile(File file) { this.file = file; }
|
||||
public Mysql getMysql() { return mysql; }
|
||||
public void setMysql(Mysql mysql) { this.mysql = mysql; }
|
||||
|
||||
public static class File {
|
||||
private String path = "./data/vehicle-identity.jsonl";
|
||||
@@ -19,4 +22,23 @@ public class VehicleIdentityProperties {
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
}
|
||||
|
||||
public static class Mysql {
|
||||
private String table = "vehicle_identity_binding";
|
||||
private String jdbcUrl = "";
|
||||
private String username = "";
|
||||
private String password = "";
|
||||
private String driverClassName = "com.mysql.cj.jdbc.Driver";
|
||||
|
||||
public String getTable() { return table; }
|
||||
public void setTable(String table) { this.table = table; }
|
||||
public String getJdbcUrl() { return jdbcUrl; }
|
||||
public void setJdbcUrl(String jdbcUrl) { this.jdbcUrl = jdbcUrl; }
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
public String getDriverClassName() { return driverClassName; }
|
||||
public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user