
com.hazelcast.jet.sql.impl.connector.keyvalue.KvMetadataJavaResolver Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2024 Hazelcast Inc.
*
* Licensed under the Hazelcast Community License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://hazelcast.com/hazelcast-community-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hazelcast.jet.sql.impl.connector.keyvalue;
import com.hazelcast.internal.serialization.InternalSerializationService;
import com.hazelcast.jet.impl.util.ReflectionUtils;
import com.hazelcast.jet.sql.impl.inject.HazelcastObjectUpsertTargetDescriptor;
import com.hazelcast.jet.sql.impl.inject.PojoUpsertTargetDescriptor;
import com.hazelcast.jet.sql.impl.inject.PrimitiveUpsertTargetDescriptor;
import com.hazelcast.sql.impl.FieldUtils;
import com.hazelcast.sql.impl.QueryException;
import com.hazelcast.sql.impl.extract.GenericQueryTargetDescriptor;
import com.hazelcast.sql.impl.extract.QueryPath;
import com.hazelcast.sql.impl.schema.MappingField;
import com.hazelcast.sql.impl.schema.TableField;
import com.hazelcast.sql.impl.schema.map.MapTableField;
import com.hazelcast.sql.impl.type.QueryDataType;
import com.hazelcast.sql.impl.type.QueryDataTypeFamily;
import com.hazelcast.sql.impl.type.QueryDataTypeUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Stream;
import static com.hazelcast.jet.sql.impl.connector.SqlConnector.JAVA_FORMAT;
import static com.hazelcast.jet.sql.impl.connector.SqlConnector.OPTION_KEY_CLASS;
import static com.hazelcast.jet.sql.impl.connector.SqlConnector.OPTION_KEY_FORMAT;
import static com.hazelcast.jet.sql.impl.connector.SqlConnector.OPTION_VALUE_CLASS;
import static com.hazelcast.jet.sql.impl.connector.SqlConnector.OPTION_VALUE_FORMAT;
import static com.hazelcast.jet.sql.impl.connector.keyvalue.KvMetadataResolver.extractFields;
import static com.hazelcast.jet.sql.impl.connector.keyvalue.KvMetadataResolver.getMetadata;
import static com.hazelcast.jet.sql.impl.connector.keyvalue.KvMetadataResolver.getTopLevelType;
import static com.hazelcast.jet.sql.impl.connector.keyvalue.KvMetadataResolver.maybeAddDefaultField;
import static com.hazelcast.sql.impl.extract.QueryPath.KEY;
import static com.hazelcast.sql.impl.extract.QueryPath.VALUE;
import static java.util.Map.entry;
/**
* A utility for key-value connectors that use Java serialization
* ({@link java.io.Serializable}) to resolve fields.
*/
public final class KvMetadataJavaResolver implements KvMetadataResolver {
public static final KvMetadataJavaResolver INSTANCE = new KvMetadataJavaResolver();
private KvMetadataJavaResolver() { }
@Override
public Stream supportedFormats() {
return Stream.concat(Stream.of(JAVA_FORMAT), JavaClassNameResolver.formats());
}
@Override
public Stream resolveAndValidateFields(
boolean isKey,
List userFields,
Map options,
InternalSerializationService serializationService
) {
Map fieldsByPath = extractFields(userFields, isKey);
Class> typeClass = getMetadata(fieldsByPath)
.>map(KvMetadataJavaResolver::loadClass)
.orElseGet(() -> loadClass(options, isKey));
QueryDataType type = QueryDataTypeUtils.resolveTypeForClass(typeClass);
if (type.getTypeFamily() != QueryDataTypeFamily.OBJECT || type.isCustomType()) {
return userFields.isEmpty()
? resolvePrimitiveField(isKey, type)
: resolveAndValidatePrimitiveField(isKey, fieldsByPath, type);
} else {
return userFields.isEmpty()
? resolveObjectFields(isKey, typeClass)
: resolveAndValidateObjectFields(isKey, fieldsByPath, typeClass);
}
}
private Stream resolvePrimitiveField(boolean isKey, QueryDataType type) {
QueryPath path = isKey ? QueryPath.KEY_PATH : QueryPath.VALUE_PATH;
String name = isKey ? KEY : VALUE;
String externalName = path.toString();
return Stream.of(new MappingField(name, type, externalName));
}
private Stream resolveAndValidatePrimitiveField(
boolean isKey,
Map fieldsByPath,
QueryDataType type
) {
QueryPath path = isKey ? QueryPath.KEY_PATH : QueryPath.VALUE_PATH;
String name = isKey ? KEY : VALUE;
String externalName = path.toString();
MappingField userField = fieldsByPath.get(path);
if (userField != null && !userField.name().equals(name)) {
throw QueryException.error("Cannot rename field: '" + name + '\'');
}
if (userField != null && type.getTypeFamily() != userField.type().getTypeFamily()) {
throw QueryException.error("Mismatch between declared and resolved type for field '" + userField.name() + "'");
}
for (MappingField field : fieldsByPath.values()) {
if (!externalName.equals(field.externalName())) {
throw QueryException.error("The field '" + externalName + "' is of type " + type.getTypeFamily()
+ ", you can't map '" + field.externalName() + "' too");
}
}
return fieldsByPath.values().stream();
}
private Stream resolveObjectFields(boolean isKey, Class> typeClass) {
Map> classFields = FieldUtils.resolveClass(typeClass);
if (classFields.isEmpty()) {
// we didn't find any non-object fields in the class, map the whole value (e.g. in java.lang.Object)
String name = isKey ? KEY : VALUE;
return Stream.of(new MappingField(name, QueryDataType.OBJECT, name));
}
return classFields.entrySet().stream().map(classField -> {
QueryPath path = new QueryPath(classField.getKey(), isKey);
QueryDataType type = QueryDataTypeUtils.resolveTypeForClass(classField.getValue());
String name = classField.getKey();
return new MappingField(name, type, path.toString());
});
}
private Stream resolveAndValidateObjectFields(
boolean isKey,
Map fieldsByPath,
Class> typeClass
) {
for (Entry> classField : FieldUtils.resolveClass(typeClass).entrySet()) {
QueryPath path = new QueryPath(classField.getKey(), isKey);
QueryDataType type = QueryDataTypeUtils.resolveTypeForClass(classField.getValue());
MappingField userField = fieldsByPath.get(path);
if (userField != null && type.getTypeFamily() != userField.type().getTypeFamily()) {
throw QueryException.error("Mismatch between declared and resolved type for field '"
+ userField.name() + "'. Declared: " + userField.type().getTypeFamily()
+ ", resolved: " + type.getTypeFamily());
}
}
return fieldsByPath.values().stream();
}
@Override
public KvMetadata resolveMetadata(
boolean isKey,
List resolvedFields,
Map options,
InternalSerializationService serializationService
) {
Map fieldsByPath = extractFields(resolvedFields, isKey);
Entry> entry = getTopLevelType(fieldsByPath)
.>>map(type -> entry(type, loadClass(type.getObjectTypeMetadata())))
.orElseGet(() -> {
Class> typeClass = loadClass(options, isKey);
return entry(QueryDataTypeUtils.resolveTypeForClass(typeClass), typeClass);
});
QueryDataType type = entry.getKey();
Class> typeClass = entry.getValue();
if (type.getTypeFamily() != QueryDataTypeFamily.OBJECT || type.isCustomType()) {
return resolvePrimitiveMetadata(isKey, resolvedFields, fieldsByPath, type);
} else {
return resolveObjectMetadata(isKey, resolvedFields, fieldsByPath, typeClass);
}
}
private KvMetadata resolvePrimitiveMetadata(
boolean isKey,
List resolvedFields,
Map fieldsByPath,
QueryDataType type
) {
List fields = new ArrayList<>();
QueryPath path = isKey ? QueryPath.KEY_PATH : QueryPath.VALUE_PATH;
MappingField field = fieldsByPath.get(path);
if (field != null) {
fields.add(new MapTableField(field.name(), field.type(), false, path));
}
maybeAddDefaultField(isKey, resolvedFields, fields, type);
return new KvMetadata(
fields,
GenericQueryTargetDescriptor.DEFAULT,
type.isCustomType()
? HazelcastObjectUpsertTargetDescriptor.INSTANCE
: PrimitiveUpsertTargetDescriptor.INSTANCE
);
}
private KvMetadata resolveObjectMetadata(
boolean isKey,
List resolvedFields,
Map fieldsByPath,
Class> typeClass
) {
Map> classFields = FieldUtils.resolveClass(typeClass);
List fields = new ArrayList<>();
Map typeNamesByPaths = new HashMap<>();
for (Entry entry : fieldsByPath.entrySet()) {
QueryPath path = entry.getKey();
QueryDataType type = entry.getValue().type();
String name = entry.getValue().name();
fields.add(new MapTableField(name, type, false, path));
if (path.getPath() != null && classFields.get(path.getPath()) != null) {
typeNamesByPaths.put(path.getPath(), classFields.get(path.getPath()).getName());
}
}
maybeAddDefaultField(isKey, resolvedFields, fields, QueryDataType.OBJECT);
return new KvMetadata(
fields,
GenericQueryTargetDescriptor.DEFAULT,
new PojoUpsertTargetDescriptor(typeClass.getName(), typeNamesByPaths)
);
}
public static Class> loadClass(Map options, boolean isKey) {
String formatProperty = options.get(isKey ? OPTION_KEY_FORMAT : OPTION_VALUE_FORMAT);
String classNameProperty = isKey ? OPTION_KEY_CLASS : OPTION_VALUE_CLASS;
String className = JAVA_FORMAT.equals(formatProperty)
? options.get(classNameProperty)
: JavaClassNameResolver.resolveClassName(formatProperty);
if (className == null) {
throw QueryException.error(classNameProperty + " is required to create Java-based mapping");
}
return loadClass(className);
}
public static Class> loadClass(String className) {
try {
return ReflectionUtils.loadClass(className);
} catch (Exception e) {
throw QueryException.error("Unable to load class '" + className + "'", e);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy