Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping;
import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping;
import org.hibernate.type.BasicType;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.BasicPluralJavaType;
import org.hibernate.type.descriptor.java.JavaType;
Expand All @@ -27,6 +26,7 @@

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Collection;

import static org.hibernate.type.descriptor.jdbc.StructHelper.getSubPart;

Expand All @@ -37,7 +37,8 @@ public class JsonGeneratingVisitor {

public static final JsonGeneratingVisitor INSTANCE = new JsonGeneratingVisitor();

protected JsonGeneratingVisitor() {}
protected JsonGeneratingVisitor() {
}

/**
* Serializes an array of values into JSON object/array
Expand All @@ -56,40 +57,82 @@ public void visitArray(JavaType<?> elementJavaType, JdbcType elementJdbcType, Ob
}

if ( elementJdbcType instanceof JsonJdbcType jsonElementJdbcType ) {
final EmbeddableMappingType embeddableMappingType = jsonElementJdbcType.getEmbeddableMappingType();
for ( Object value : values ) {
visitPluralAggregateValues( jsonElementJdbcType, values, options, writer );
}
else {
assert !(elementJdbcType instanceof AggregateJdbcType);
visitBasicPluralValues( elementJavaType, elementJdbcType, values, options, writer );
}
writer.endArray();
}

private void visitPluralAggregateValues(
AggregateJdbcType elementJdbcType,
Object values,
WrapperOptions options,
JsonDocumentWriter writer) {
final EmbeddableMappingType embeddableMappingType = elementJdbcType.getEmbeddableMappingType();
if ( values.getClass().isArray() ) {
final var length = Array.getLength( values );
for ( int j = 0; j < length; j++ ) {
try {
visit( embeddableMappingType, Array.get( values, j ), options, writer );
}
catch (IOException e) {
throw new IllegalArgumentException( "Could not serialize array element", e );
}
}
}
else if ( values instanceof Collection<?> collection ) {
for ( final var item : collection ) {
try {
visit( embeddableMappingType, value, options, writer );
visit( embeddableMappingType, item, options, writer );
}
catch (IOException e) {
throw new IllegalArgumentException( "Could not serialize JSON array value", e );
throw new IllegalArgumentException( "Could not serialize array element", e );
}
}
}
else {
assert !(elementJdbcType instanceof AggregateJdbcType);
for ( Object value : values ) {
if ( value == null ) {
throw new IllegalArgumentException(
"Expected array or collection value, but got: " + values.getClass().getName()
);
}
}

private void visitBasicPluralValues(
JavaType<?> elementJavaType,
JdbcType elementJdbcType,
Object values,
WrapperOptions options,
JsonDocumentWriter writer) {
if ( values.getClass().isArray() ) {
final var length = Array.getLength( values );
for ( int j = 0; j < length; j++ ) {
final var item = Array.get( values, j );
if ( item == null ) {
writer.nullValue();
}
else {
writer.serializeJsonValue( value, (JavaType<?>) elementJavaType, elementJdbcType, options );
writer.serializeJsonValue( item, elementJavaType, elementJdbcType, options );
}
}
}

writer.endArray();
}

/**
* Checks that a <code>JDBCType</code> is assignable to an array
*
* @param type the jdbc type
* @return <code>true</code> if types is of array kind <code>false</code> otherwise.
*/
private static boolean isArrayType(JdbcType type) {
return (type.getDefaultSqlTypeCode() == SqlTypes.ARRAY ||
type.getDefaultSqlTypeCode() == SqlTypes.JSON_ARRAY);
else if ( values instanceof Collection<?> collection ) {
for ( final var item : collection ) {
if ( item == null ) {
writer.nullValue();
}
else {
writer.serializeJsonValue( item, elementJavaType, elementJdbcType, options );
}
}
}
else {
throw new IllegalArgumentException(
"Expected array or collection value, but got: " + values.getClass().getName()
);
}
}

public void visit(MappingType mappedType, Object value, WrapperOptions options, JsonDocumentWriter writer)
Expand All @@ -106,17 +149,26 @@ else if ( mappedType instanceof ManagedMappingType managedMappingType ) {
serializeObject( managedMappingType, value, options, writer );
}
else if ( mappedType instanceof BasicType<?> basicType ) {
if ( isArrayType( basicType.getJdbcType() ) ) {
final int length = Array.getLength( value );
if ( basicType.getJdbcType() instanceof ArrayJdbcType arrayJdbcType ) {
final var domainValue = basicType.convertToRelationalValue( value );
final var elementJdbcType = arrayJdbcType.getElementJdbcType();
writer.startArray();
if ( length != 0 ) {
//noinspection unchecked
final JavaType<Object> elementJavaType = ((BasicPluralJavaType<Object>) basicType.getJdbcJavaType()).getElementJavaType();
final var elementJdbcType = ((ArrayJdbcType) basicType.getJdbcType()).getElementJdbcType();
final Object domainArray = basicType.convertToRelationalValue( value );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can keep on relying on Object[] and hence keep all the code as it is. You just have to use basicType.getJavaType().unwrap( value, Object[].class, options ) here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I intuitively did initially as well, but then I realized that unwrap would cause additional array instantiations and/or iterations of the values in case the original type is not exactly Object[] so I went this route.

We could still use that as a fall-back, though I don't think there are cases where the value is not either an array or an instance of java.util.Collection.

for ( int j = 0; j < length; j++ ) {
writer.serializeJsonValue( Array.get( domainArray, j ), elementJavaType, elementJdbcType, options );
}
if ( elementJdbcType instanceof AggregateJdbcType aggregateJdbcType ) {
visitPluralAggregateValues(
aggregateJdbcType,
domainValue,
options,
writer
);
}
else {
visitBasicPluralValues(
((BasicPluralJavaType<?>) basicType.getJdbcJavaType()).getElementJavaType(),
elementJdbcType,
domainValue,
options,
writer
);
}
writer.endArray();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.RequiresDialectFeature;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.jdbc.spi.DescriptiveJsonGeneratingVisitor;
import org.hibernate.type.format.StringJsonDocumentWriter;
import org.junit.jupiter.api.AfterAll;
Expand All @@ -26,6 +30,8 @@

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
Expand All @@ -34,12 +40,14 @@
import static org.assertj.core.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

@DomainModel( annotatedClasses = {
@DomainModel(annotatedClasses = {
DescriptiveJsonGeneratingVisitorSmokeTest.Company.class,
DescriptiveJsonGeneratingVisitorSmokeTest.Address.class,
DescriptiveJsonGeneratingVisitorSmokeTest.Employee.class,
} )
DescriptiveJsonGeneratingVisitorSmokeTest.EntityOfArrays.class,
})
@SessionFactory
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArray.class)
public class DescriptiveJsonGeneratingVisitorSmokeTest {
private final DescriptiveJsonGeneratingVisitor visitor = new DescriptiveJsonGeneratingVisitor();

Expand All @@ -52,7 +60,7 @@ public void testCompany(SessionFactoryScope scope) {
.findEntityDescriptor( Company.class );

scope.inTransaction( session -> {
final Company company = session.createQuery(
final Company company = session.createSelectionQuery(
"from Company where id = 1",
Company.class
).getSingleResult();
Expand Down Expand Up @@ -84,7 +92,7 @@ public void testCompanyFetchEmployees(SessionFactoryScope scope) {
.findEntityDescriptor( Company.class );

scope.inTransaction( session -> {
final Company company = session.createQuery(
final Company company = session.createSelectionQuery(
"from Company c join fetch c.employees where c.id = 1",
Company.class
).getSingleResult();
Expand Down Expand Up @@ -115,13 +123,78 @@ public void testCompanyFetchEmployees(SessionFactoryScope scope) {
} );
}

@Test
public void testEntityOfArrays(SessionFactoryScope scope) {
final SessionFactoryImplementor sessionFactory = scope.getSessionFactory();
final EntityPersister entityDescriptor = sessionFactory.getMappingMetamodel()
.findEntityDescriptor( EntityOfArrays.class );

scope.inTransaction( session -> {
final var entity = session.createSelectionQuery(
"from EntityOfArrays e where id = 1",
EntityOfArrays.class
).getSingleResult();

try {
final StringJsonDocumentWriter writer = new StringJsonDocumentWriter();
visitor.visit( entityDescriptor.getEntityMappingType(), entity, sessionFactory.getWrapperOptions(), writer );
final String result = writer.toString();

final JsonNode jsonNode = mapper.readTree( result );
System.out.println("Result: "+jsonNode.toPrettyString());

assertJsonArray( jsonNode.get( "integerArray" ), Arrays.asList( 1, null, 3 ) );
assertJsonArray( jsonNode.get( "stringCollectionArray" ), List.of( "one", "two", "three" ) );
assertJsonArray( jsonNode.get( "longArray" ), List.of( 10L, 20L, 30L ) );
assertJsonArray( jsonNode.get( "booleanListArray" ), List.of( true, false, true ) );
}
catch (Exception e) {
fail( "Test failed with exception", e );
}
} );
}

private static void assertJsonArray(JsonNode jsonNode, List<?> expectedValues) {
assertThat( jsonNode.isArray() ).isTrue();
assertThat( jsonNode.size() ).isEqualTo( expectedValues.size() );
for (int i = 0; i < expectedValues.size(); i++) {
final Object expectedValue = expectedValues.get( i );
final JsonNode element = jsonNode.get( i );
if ( expectedValue == null ) {
assertThat( element.isNull() ).isTrue();
}
else if ( expectedValue instanceof Boolean ) {
assertThat( element.booleanValue() ).isEqualTo( expectedValue );
}
else if ( expectedValue instanceof Integer ) {
assertThat( element.intValue() ).isEqualTo( expectedValue );
}
else if ( expectedValue instanceof Long ) {
assertThat( element.longValue() ).isEqualTo( expectedValue );
}
else if ( expectedValue instanceof String ) {
assertThat( element.textValue() ).isEqualTo( expectedValue );
}
else {
fail( "Unsupported element type: " + expectedValue.getClass() );
}
}
}

@BeforeAll
public void beforeAll(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Company rh = new Company( 1L, "Red Hat", new Address( "Milan", "Via Gustavo Fara" ) );
session.persist( rh );
session.persist( new Employee( UUID.randomUUID(), "Marco", "Belladelli", 100_000, rh ) );
session.persist( new Employee( UUID.randomUUID(), "Matteo", "Cauzzi", 50_000, rh ) );
final var ofArrays = new EntityOfArrays();
ofArrays.id = 1L;
ofArrays.integerArray = new Integer[] { 1, null, 3 };
ofArrays.stringCollectionArray = List.of( "one", "two", "three" );
ofArrays.longArray = new long[] { 10, 20, 30 };
ofArrays.booleanListArray = List.of( true, false, true );
session.persist( ofArrays );
} );
}

Expand Down Expand Up @@ -276,4 +349,22 @@ public void setCompany(Company company) {
this.company = company;
}
}

@Entity(name = "EntityOfArrays")
static class EntityOfArrays {
@Id
private Long id;

@JdbcTypeCode(SqlTypes.ARRAY)
private Integer[] integerArray;

@JdbcTypeCode(SqlTypes.ARRAY)
private Collection<String> stringCollectionArray;

@JdbcTypeCode(SqlTypes.JSON_ARRAY)
private long[] longArray;

@JdbcTypeCode(SqlTypes.JSON_ARRAY)
private List<Boolean> booleanListArray;
}
}
Loading