diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index 086062475..b14efa6e9 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -29,6 +29,8 @@ import com.clickhouse.client.api.internal.SettingsConverter; import com.clickhouse.client.api.internal.TableSchemaParser; import com.clickhouse.client.api.internal.ValidationUtils; +import com.clickhouse.client.api.metadata.ColumnToMethodMatchingStrategy; +import com.clickhouse.client.api.metadata.DefaultColumnToMethodMatchingStrategy; import com.clickhouse.client.api.metadata.TableSchema; import com.clickhouse.client.api.metrics.ClientMetrics; import com.clickhouse.client.api.metrics.OperationMetrics; @@ -146,8 +148,10 @@ public class Client implements AutoCloseable { private Map tableSchemaCache = new ConcurrentHashMap<>(); private Map tableSchemaHasDefaults = new ConcurrentHashMap<>(); + private final ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy; + private Client(Set endpoints, Map configuration, boolean useNewImplementation, - ExecutorService sharedOperationExecutor) { + ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy) { this.endpoints = endpoints; this.configuration = configuration; this.endpoints.forEach(endpoint -> { @@ -170,6 +174,7 @@ private Client(Set endpoints, Map configuration, boolean this.oldClient = ClientV1AdaptorHelper.createClient(configuration); LOG.info("Using old http client implementation"); } + this.columnToMethodMatchingStrategy = columnToMethodMatchingStrategy; } /** @@ -211,6 +216,7 @@ public static class Builder { private boolean useNewImplementation = true; private ExecutorService sharedOperationExecutor = null; + private ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy; public Builder() { this.endpoints = new HashSet<>(); @@ -857,6 +863,18 @@ public Builder serverSetting(String name, Collection values) { return this; } + /** + * Sets column to method matching strategy. It is used while registering POJO serializers and deserializers. + * Default is {@link DefaultColumnToMethodMatchingStrategy}. + * + * @param strategy - matching strategy + * @return same instance of the builder + */ + public Builder columnToMethodMatchingStrategy(ColumnToMethodMatchingStrategy strategy) { + this.columnToMethodMatchingStrategy = strategy; + return this; + } + public Client build() { setDefaults(); @@ -914,7 +932,7 @@ public Client build() { throw new IllegalArgumentException("Nor server timezone nor specific timezone is set"); } - return new Client(this.endpoints, this.configuration, this.useNewImplementation, this.sharedOperationExecutor); + return new Client(this.endpoints, this.configuration, this.useNewImplementation, this.sharedOperationExecutor, this.columnToMethodMatchingStrategy); } private static final int DEFAULT_NETWORK_BUFFER_SIZE = 300_000; @@ -986,6 +1004,10 @@ private void setDefaults() { if (!configuration.containsKey("client_allow_binary_reader_to_reuse_buffers")) { allowBinaryReaderToReuseBuffers(false); } + + if (columnToMethodMatchingStrategy == null) { + columnToMethodMatchingStrategy = DefaultColumnToMethodMatchingStrategy.INSTANCE; + } } } @@ -1041,19 +1063,17 @@ public synchronized void register(Class clazz, TableSchema schema) { } tableSchemaCache.put(schemaKey, schema); + ColumnToMethodMatchingStrategy matchingStrategy = columnToMethodMatchingStrategy; + //Create a new POJOSerializer with static .serialize(object, columns) methods Map classGetters = new HashMap<>(); Map classSetters = new HashMap<>(); for (Method method : clazz.getMethods()) {//Clean up the method names - String methodName = method.getName(); - if (methodName.startsWith("get") || methodName.startsWith("has")) { - methodName = methodName.substring(3).toLowerCase(); - classGetters.put(methodName, method); - } else if (methodName.startsWith("is")) { - methodName = methodName.substring(2).toLowerCase(); + if (matchingStrategy.isGetter(method.getName())) { + String methodName = matchingStrategy.normalizeMethodName(method.getName()); classGetters.put(methodName, method); - } else if (methodName.startsWith("set")) { - methodName = methodName.substring(3).toLowerCase(); + } else if (matchingStrategy.isSetter(method.getName())) { + String methodName = matchingStrategy.normalizeMethodName(method.getName()); classSetters.put(methodName, method); } } @@ -1063,7 +1083,7 @@ public synchronized void register(Class clazz, TableSchema schema) { boolean defaultsSupport = schema.hasDefaults(); tableSchemaHasDefaults.put(schemaKey, defaultsSupport); for (ClickHouseColumn column : schema.getColumns()) { - String propertyName = column.getColumnName().toLowerCase().replace("_", "").replace(".", ""); + String propertyName = columnToMethodMatchingStrategy.normalizeColumnName(column.getColumnName()); Method getterMethod = classGetters.get(propertyName); if (getterMethod != null) { schemaSerializers.put(column.getColumnName(), (obj, stream) -> { diff --git a/client-v2/src/main/java/com/clickhouse/client/api/metadata/ColumnToMethodMatchingStrategy.java b/client-v2/src/main/java/com/clickhouse/client/api/metadata/ColumnToMethodMatchingStrategy.java new file mode 100644 index 000000000..096344de1 --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/metadata/ColumnToMethodMatchingStrategy.java @@ -0,0 +1,36 @@ +package com.clickhouse.client.api.metadata; + + +/** + * Strategy to match column names to method names. + */ +public interface ColumnToMethodMatchingStrategy { + + /** + * Normalizes method name to match column name. + * @param methodName original method name + * @return normalized method name + */ + String normalizeMethodName(String methodName); + + /** + * Checks if the method is a setter. + * @param methodName original (not normalized) method name + * @return true if the method is a setter + */ + boolean isSetter(String methodName); + + /** + * Checks if the method is a getter. + * @param methodName original (not normalized) method name + * @return true if the method is a getter + */ + boolean isGetter(String methodName); + + /** + * Normalizes column name to match method name. + * @param columnName original column name + * @return normalized column name + */ + String normalizeColumnName(String columnName); +} diff --git a/client-v2/src/main/java/com/clickhouse/client/api/metadata/DefaultColumnToMethodMatchingStrategy.java b/client-v2/src/main/java/com/clickhouse/client/api/metadata/DefaultColumnToMethodMatchingStrategy.java new file mode 100644 index 000000000..b2610cf44 --- /dev/null +++ b/client-v2/src/main/java/com/clickhouse/client/api/metadata/DefaultColumnToMethodMatchingStrategy.java @@ -0,0 +1,58 @@ +package com.clickhouse.client.api.metadata; + + +import java.util.regex.Pattern; + +/** + * Default implementation of {@link ColumnToMethodMatchingStrategy} takes the following rules: + *
    + *
  • Method name is normalized by removing prefixes like "get", "set", "is", "has".
  • + *
  • Column name is normalized by removing special characters like "-", "_", ".".
  • + *
  • Normalized method name and column name are compared case-insensitively.
  • + *
+ * + * + */ +public class DefaultColumnToMethodMatchingStrategy implements ColumnToMethodMatchingStrategy { + + public static final DefaultColumnToMethodMatchingStrategy INSTANCE = new DefaultColumnToMethodMatchingStrategy(); + + private final Pattern getterPattern; + private final Pattern setterPattern; + + private final Pattern methodReplacePattern; + + private final Pattern columnReplacePattern; + + + public DefaultColumnToMethodMatchingStrategy() { + this("^(get|is|has).+", "^(set).+", "^(get|set|is|has)|_", "[-_.]"); + } + + public DefaultColumnToMethodMatchingStrategy(String getterPatternRegEx, String setterPaternRegEx, String methodReplacePatternRegEx, String columnReplacePatternRegEx) { + this.getterPattern = Pattern.compile(getterPatternRegEx); + this.setterPattern = Pattern.compile(setterPaternRegEx); + this.methodReplacePattern = Pattern.compile(methodReplacePatternRegEx); + this.columnReplacePattern = Pattern.compile(columnReplacePatternRegEx); + } + + @Override + public String normalizeMethodName(String methodName) { + return methodReplacePattern.matcher(methodName).replaceAll("").toLowerCase(); + } + + @Override + public boolean isSetter(String methodName) { + return setterPattern.matcher(methodName).matches(); + } + + @Override + public boolean isGetter(String methodName) { + return getterPattern.matcher(methodName).matches(); + } + + @Override + public String normalizeColumnName(String columnName) { + return columnReplacePattern.matcher(columnName).replaceAll("").toLowerCase(); + } +} diff --git a/client-v2/src/test/java/com/clickhouse/client/metadata/MetadataTests.java b/client-v2/src/test/java/com/clickhouse/client/metadata/MetadataTests.java index 98077586a..ce314d9ae 100644 --- a/client-v2/src/test/java/com/clickhouse/client/metadata/MetadataTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/metadata/MetadataTests.java @@ -9,11 +9,13 @@ import com.clickhouse.client.ClickHouseRequest; import com.clickhouse.client.ClickHouseResponse; import com.clickhouse.client.api.Client; +import com.clickhouse.client.api.metadata.DefaultColumnToMethodMatchingStrategy; import com.clickhouse.client.api.metadata.TableSchema; import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseRecord; import org.testng.Assert; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.util.Iterator; @@ -82,4 +84,28 @@ private void prepareDataSet(String tableName) { Assert.fail("Failed to prepare data set", e); } } + + @Test(groups = {"integration"}, dataProvider = "testMatchingNormalizationData") + public void testDefaultColumnToMethodMatchingStrategy(String methodName, String columnName) { + methodName = DefaultColumnToMethodMatchingStrategy.INSTANCE.normalizeMethodName(methodName); + columnName = DefaultColumnToMethodMatchingStrategy.INSTANCE.normalizeColumnName(columnName); + Assert.assertEquals(methodName, columnName, "Method name: " + methodName + " Column name: " + columnName); + } + + @DataProvider(name = "testMatchingNormalizationData") + public Object[][] testMatchingNormalizationData() { + return new Object[][]{ + {"getLastName", "LastName"}, + {"getLastName", "last_name"}, + {"getLastName", "last.name"}, + {"setLastName", "last.name"}, + {"isLastUpdate", "last_update"}, + {"hasMore", "more"}, + {"getFIRST_NAME", "first_name"}, + {"setUPDATED_ON", "updated.ON"}, + {"getNUM_OF_TRIES", "num_of_tries"}, + {"gethas_more", "has_more"}, + + }; + } }