diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleOsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleOsonArrayJdbcType.java index ee241225562d..a30e1ae7f7ff 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleOsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleOsonArrayJdbcType.java @@ -12,19 +12,17 @@ import org.hibernate.dialect.type.OracleJsonArrayJdbcType; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; -import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; import org.hibernate.type.descriptor.jdbc.BasicBinder; import org.hibernate.type.descriptor.jdbc.BasicExtractor; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JsonHelper; -import org.hibernate.type.descriptor.jdbc.JsonJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JsonGeneratingVisitor; import org.hibernate.type.format.OsonDocumentReader; import org.hibernate.type.format.OsonDocumentWriter; @@ -78,20 +76,7 @@ private byte[] toOsonStream(T value, JavaType javaType, WrapperOptions op } else { final OsonDocumentWriter writer = new OsonDocumentWriter( generator ); - if ( getElementJdbcType() instanceof JsonJdbcType jsonElementJdbcType ) { - final EmbeddableMappingType embeddableMappingType = jsonElementJdbcType.getEmbeddableMappingType(); - JsonHelper.serializeArray( embeddableMappingType, domainObjects, options, writer ); - } - else { - assert !(getElementJdbcType() instanceof AggregateJdbcType); - JsonHelper.serializeArray( - elementJavaType, - getElementJdbcType(), - domainObjects, - options, - writer - ); - } + JsonGeneratingVisitor.INSTANCE.visitArray( elementJavaType, getElementJdbcType(), domainObjects, options, writer ); } } return out.toByteArray(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleOsonJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleOsonJdbcType.java index 93e2ebe9aa4a..01b37382297f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleOsonJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleOsonJdbcType.java @@ -22,6 +22,7 @@ import org.hibernate.type.descriptor.jdbc.BasicBinder; import org.hibernate.type.descriptor.jdbc.BasicExtractor; import org.hibernate.type.descriptor.jdbc.JsonHelper; +import org.hibernate.type.descriptor.jdbc.spi.JsonGeneratingVisitor; import org.hibernate.type.format.OsonDocumentReader; import org.hibernate.type.format.OsonDocumentWriter; @@ -79,7 +80,7 @@ private byte[] toOson(T value, JavaType javaType, WrapperOptions options) if ( getEmbeddableMappingType() != null ) { // OracleJsonFactory is used and not OracleOsonFactory as Jackson is not involved here try (OracleJsonGenerator generator = OSON_JSON_FACTORY.createJsonBinaryGenerator( out )) { - JsonHelper.serialize( + JsonGeneratingVisitor.INSTANCE.visit( getEmbeddableMappingType(), value, options, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java index bf0d680227ad..1a5162d76dc2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/type/AbstractPostgreSQLStructJdbcType.java @@ -43,7 +43,7 @@ import org.hibernate.type.descriptor.jdbc.StructuredJdbcType; import org.hibernate.type.spi.TypeConfiguration; -import static org.hibernate.type.descriptor.jdbc.StructHelper.getEmbeddedPart; +import static org.hibernate.type.descriptor.jdbc.StructHelper.getSubPart; import static org.hibernate.type.descriptor.jdbc.StructHelper.instantiate; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; @@ -999,7 +999,7 @@ private SelectableMapping getJdbcValueSelectable(int jdbcValueSelectableIndex) { final int size = numberOfAttributeMappings + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); int count = 0; for ( int i = 0; i < size; i++ ) { - final ValuedModelPart modelPart = getEmbeddedPart( embeddableMappingType, orderMapping[i] ); + final ValuedModelPart modelPart = getSubPart( embeddableMappingType, orderMapping[i] ); if ( modelPart.getMappedType() instanceof EmbeddableMappingType embeddableMappingType ) { final SelectableMapping aggregateMapping = embeddableMappingType.getAggregateMapping(); if ( aggregateMapping == null ) { @@ -1378,7 +1378,7 @@ private StructAttributeValues getAttributeValues( attributeIndex = orderMapping[i]; } jdbcIndex += injectAttributeValue( - getEmbeddedPart( embeddableMappingType, attributeIndex ), + getSubPart( embeddableMappingType, attributeIndex ), attributeValues, attributeIndex, rawJdbcValues, diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java index 5784a5efd18d..ab967578cb11 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java @@ -9,7 +9,6 @@ import java.sql.ResultSet; import java.sql.SQLException; -import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.ValueExtractor; @@ -17,6 +16,7 @@ import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; +import org.hibernate.type.descriptor.jdbc.spi.JsonGeneratingVisitor; import org.hibernate.type.format.StringJsonDocumentWriter; /** @@ -74,17 +74,9 @@ protected String toString(X value, JavaType javaType, WrapperOptions opti return options.getJsonFormatMapper().toString( value, javaType, options); } else { - final JdbcType elementJdbcType = getElementJdbcType(); final Object[] domainObjects = javaType.unwrap( value, Object[].class, options ); final StringJsonDocumentWriter writer = new StringJsonDocumentWriter(); - if ( elementJdbcType instanceof JsonJdbcType jsonElementJdbcType ) { - final EmbeddableMappingType embeddableMappingType = jsonElementJdbcType.getEmbeddableMappingType(); - JsonHelper.serializeArray( embeddableMappingType, domainObjects, options, writer ); - } - else { - assert !(elementJdbcType instanceof AggregateJdbcType); - JsonHelper.serializeArray( elementJavaType, elementJdbcType, domainObjects, options, writer ); - } + JsonGeneratingVisitor.INSTANCE.visitArray( elementJavaType, getElementJdbcType(), domainObjects, options, writer ); return writer.getJson(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonHelper.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonHelper.java index 5021a6e6c1f5..8e5fbb1ff73d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonHelper.java @@ -5,8 +5,6 @@ package org.hibernate.type.descriptor.jdbc; -import java.io.IOException; -import java.lang.reflect.Array; import java.sql.SQLException; import java.util.AbstractCollection; import java.util.ArrayList; @@ -24,20 +22,14 @@ import org.hibernate.internal.util.collections.StandardStack; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; -import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.SelectableMapping; -import org.hibernate.metamodel.mapping.ValuedModelPart; -import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; import org.hibernate.type.BasicPluralType; -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; import org.hibernate.type.format.JsonDocumentItemType; import org.hibernate.type.format.JsonDocumentReader; -import org.hibernate.type.format.JsonDocumentWriter; -import static org.hibernate.type.descriptor.jdbc.StructHelper.getEmbeddedPart; + import static org.hibernate.type.descriptor.jdbc.StructHelper.instantiate; import org.hibernate.type.format.JsonValueJDBCTypeAdapter; import org.hibernate.type.format.JsonValueJDBCTypeAdapterFactory; @@ -52,174 +44,6 @@ @Internal public class JsonHelper { - /** - * Serializes an array of values into JSON object/array - * @param elementMappingType the type definitions - * @param values the values to be serialized - * @param options wrapping options - * @param writer the document writer used for serialization - */ - public static void serializeArray(MappingType elementMappingType, Object[] values, WrapperOptions options, JsonDocumentWriter writer) { - writer.startArray(); - if ( values.length == 0 ) { - writer.endArray(); - return; - } - for ( Object value : values ) { - try { - serialize(elementMappingType, value, options, writer); - } - catch (IOException e) { - throw new IllegalArgumentException( "Could not serialize JSON array value" , e ); - } - } - writer.endArray(); - } - - /** - * Serializes an array of values into JSON object/array - * @param elementJavaType the array element type - * @param elementJdbcType the JDBC type - * @param values values to be serialized - * @param options wrapping options - * @param writer the document writer used for serialization - */ - public static void serializeArray(JavaType elementJavaType, JdbcType elementJdbcType, Object[] values, WrapperOptions options, JsonDocumentWriter writer) { - writer.startArray(); - if ( values.length == 0 ) { - writer.endArray(); - return; - } - for ( Object value : values ) { - if (value == null) { - writer.nullValue(); - } - else { - writer.serializeJsonValue( value ,(JavaType) elementJavaType,elementJdbcType,options); - } - } - writer.endArray(); - } - - /** - * Checks that a JDBCType is assignable to an array - * @param type the jdbc type - * @return true if types is of array kind false otherwise. - */ - private static boolean isArrayType(JdbcType type) { - return (type.getDefaultSqlTypeCode() == SqlTypes.ARRAY || - type.getDefaultSqlTypeCode() == SqlTypes.JSON_ARRAY); - } - - /** - * Serialized an Object value to JSON object using a document writer. - * - * @param embeddableMappingType the embeddable mapping definition of the given value. - * @param domainValue the value to be serialized. - * @param options wrapping options - * @param writer the document writer - * @throws IOException if the underlying writer failed to serialize a mpped value or failed to perform need I/O. - */ - public static void serialize(EmbeddableMappingType embeddableMappingType, - Object domainValue, WrapperOptions options, JsonDocumentWriter writer) throws IOException { - writer.startObject(); - serializeMapping(embeddableMappingType, domainValue, options, writer); - writer.endObject(); - } - - private static void serialize(MappingType mappedType, Object value, WrapperOptions options, JsonDocumentWriter writer) - throws IOException { - if ( value == null ) { - writer.nullValue(); - } - else if ( mappedType instanceof EmbeddableMappingType ) { - serialize( (EmbeddableMappingType) mappedType, value, options, writer ); - } - else if ( mappedType instanceof BasicType basicType) { - if ( isArrayType(basicType.getJdbcType())) { - final int length = Array.getLength( value ); - writer.startArray(); - if ( length != 0 ) { - final JavaType elementJavaType = ( (BasicPluralJavaType) basicType.getJdbcJavaType() ).getElementJavaType(); - final JdbcType elementJdbcType = ( (ArrayJdbcType) basicType.getJdbcType() ).getElementJdbcType(); - final Object domainArray = basicType.convertToRelationalValue( value ); - for ( int j = 0; j < length; j++ ) { - writer.serializeJsonValue(Array.get(domainArray,j), elementJavaType, elementJdbcType, options); - } - } - writer.endArray(); - } - else { - writer.serializeJsonValue(basicType.convertToRelationalValue( value), - (JavaType)basicType.getJdbcJavaType(),basicType.getJdbcType(), options); - } - } - else { - throw new UnsupportedOperationException( "Support for mapping type not yet implemented: " + mappedType.getClass().getName() ); - } - } - - /** - * JSON object attirbute serialization - * @see #serialize(EmbeddableMappingType, Object, WrapperOptions, JsonDocumentWriter) - * @param embeddableMappingType the embeddable mapping definition of the given value. - * @param domainValue the value to be serialized. - * @param options wrapping options - * @param writer the document writer - * @throws IOException if an error occurred while writing to an underlying writer - */ - private static void serializeMapping(EmbeddableMappingType embeddableMappingType, - Object domainValue, WrapperOptions options, JsonDocumentWriter writer) throws IOException { - final Object[] values = embeddableMappingType.getValues( domainValue ); - for ( int i = 0; i < values.length; i++ ) { - final ValuedModelPart attributeMapping = getEmbeddedPart( embeddableMappingType, i ); - if ( attributeMapping instanceof SelectableMapping ) { - final String name = ( (SelectableMapping) attributeMapping ).getSelectableName(); - writer.objectKey( name ); - - if ( attributeMapping.getMappedType() instanceof EmbeddableMappingType ) { - writer.startObject(); - serializeMapping( (EmbeddableMappingType)attributeMapping.getMappedType(), values[i], options,writer); - writer.endObject(); - } - else { - serialize(attributeMapping.getMappedType(), values[i], options, writer); - } - - } - else if ( attributeMapping instanceof EmbeddedAttributeMapping ) { - if ( values[i] == null ) { - continue; - } - final EmbeddableMappingType mappingType = (EmbeddableMappingType) attributeMapping.getMappedType(); - final SelectableMapping aggregateMapping = mappingType.getAggregateMapping(); - if (aggregateMapping == null) { - serializeMapping( - mappingType, - values[i], - options, - writer ); - } - else { - final String name = aggregateMapping.getSelectableName(); - writer.objectKey( name ); - writer.startObject(); - serializeMapping( - mappingType, - values[i], - options, - writer); - writer.endObject(); - - } - } - else { - throw new UnsupportedOperationException( "Support for attribute mapping type not yet implemented: " + attributeMapping.getClass().getName() ); - } - - } - } - /** * Consumes Json document items from a document reader and return the serialized Objects * @param reader the document reader @@ -227,7 +51,7 @@ else if ( attributeMapping instanceof EmbeddedAttributeMapping ) { * @param returnEmbeddable do we return an Embeddable object or array of Objects ? * @param options wrapping options * @return serialized values - * @param + * @param the type of the returned value * @throws SQLException if error occured during mapping of types */ private static X consumeJsonDocumentItems(JsonDocumentReader reader, EmbeddableMappingType embeddableMappingType, boolean returnEmbeddable, WrapperOptions options) diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonJdbcType.java index 59b5b70d5935..3126fec438d0 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonJdbcType.java @@ -17,6 +17,7 @@ import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.spi.JsonGeneratingVisitor; import org.hibernate.type.format.StringJsonDocumentReader; import org.hibernate.type.format.StringJsonDocumentWriter; @@ -92,7 +93,7 @@ public Object createJdbcValue(Object domainValue, WrapperOptions options) throws assert embeddableMappingType != null; final StringJsonDocumentWriter writer = new StringJsonDocumentWriter(); try { - JsonHelper.serialize( embeddableMappingType, domainValue, options, writer ); + JsonGeneratingVisitor.INSTANCE.visit( embeddableMappingType, domainValue, options, writer ); return writer.getJson(); } catch (IOException e) { @@ -110,7 +111,7 @@ protected String toString(X value, JavaType javaType, WrapperOptions opti if ( embeddableMappingType != null ) { try { final StringJsonDocumentWriter writer = new StringJsonDocumentWriter(); - JsonHelper.serialize( embeddableMappingType, value, options, writer ); + JsonGeneratingVisitor.INSTANCE.visit( embeddableMappingType, value, options, writer ); return writer.getJson(); } catch (IOException e) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/StructHelper.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/StructHelper.java index ec323d067547..2e9d7f6e3f9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/StructHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/StructHelper.java @@ -17,6 +17,7 @@ import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.ValuedModelPart; @@ -44,7 +45,7 @@ public static StructAttributeValues getAttributeValues( int jdbcIndex = 0; for ( int i = 0; i < size; i++ ) { jdbcIndex += injectAttributeValue( - getEmbeddedPart( embeddableMappingType, i ), + getSubPart( embeddableMappingType, i ), attributeValues, i, rawJdbcValues, @@ -171,7 +172,7 @@ private static int injectJdbcValues( int offset = 0; for ( int i = 0; i < values.length; i++ ) { offset += injectJdbcValue( - getEmbeddedPart( embeddableMappingType, i ), + getSubPart( embeddableMappingType, i ), values, i, jdbcValues, @@ -203,10 +204,12 @@ private static EmbeddableInstantiator embeddableInstantiator( } } - public static ValuedModelPart getEmbeddedPart(EmbeddableMappingType embeddableMappingType, int position) { - return position == embeddableMappingType.getNumberOfAttributeMappings() - ? embeddableMappingType.getDiscriminatorMapping() - : embeddableMappingType.getAttributeMapping( position ); + public static ValuedModelPart getSubPart(ManagedMappingType type, int position) { + if ( position == type.getNumberOfAttributeMappings() ) { + assert type instanceof EmbeddableMappingType : "Unexpected position for non-embeddable type: " + type; + return ( (EmbeddableMappingType) type ).getDiscriminatorMapping(); + } + return type.getAttributeMapping( position ); } private static int injectJdbcValue( diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/StructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/StructJdbcType.java index c73a7755818f..32966a536221 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/StructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/StructJdbcType.java @@ -32,7 +32,7 @@ import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; import org.hibernate.type.spi.TypeConfiguration; -import static org.hibernate.type.descriptor.jdbc.StructHelper.getEmbeddedPart; +import static org.hibernate.type.descriptor.jdbc.StructHelper.getSubPart; import static org.hibernate.type.descriptor.jdbc.StructHelper.instantiate; /** @@ -241,7 +241,7 @@ private StructAttributeValues getAttributeValues( for ( int i = 0; i < size; i++ ) { final int attributeIndex = orderMapping == null ? i : orderMapping[i]; jdbcIndex += injectAttributeValue( - getEmbeddedPart( embeddableMappingType, attributeIndex ), + getSubPart( embeddableMappingType, attributeIndex ), attributeValues, attributeIndex, rawJdbcValues, @@ -383,7 +383,7 @@ private int wrapRawJdbcValues( WrapperOptions options) throws SQLException { final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); for ( int i = 0; i < numberOfAttributeMappings + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); i++ ) { - final ValuedModelPart attributeMapping = getEmbeddedPart( embeddableMappingType, i ); + final ValuedModelPart attributeMapping = getSubPart( embeddableMappingType, i ); if ( attributeMapping instanceof ToOneAttributeMapping toOneAttributeMapping ) { if ( toOneAttributeMapping.getSideNature() == ForeignKeyDescriptor.Nature.TARGET ) { continue; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlHelper.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlHelper.java index 36e494fb83b2..0ab60c3579a3 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlHelper.java @@ -39,7 +39,7 @@ import static java.lang.Character.isLetter; import static java.lang.Character.isLetterOrDigit; -import static org.hibernate.type.descriptor.jdbc.StructHelper.getEmbeddedPart; +import static org.hibernate.type.descriptor.jdbc.StructHelper.getSubPart; import static org.hibernate.type.descriptor.jdbc.StructHelper.instantiate; /** @@ -758,7 +758,7 @@ private static void toString( final int attributeCount = embeddableMappingType.getNumberOfAttributeMappings(); for ( int i = 0; i < attributeCount; i++ ) { final Object attributeValue = attributeValues == null ? null : attributeValues[i]; - final ValuedModelPart attributeMapping = getEmbeddedPart( embeddableMappingType, i ); + final ValuedModelPart attributeMapping = getSubPart( embeddableMappingType, i ); if ( attributeMapping instanceof SelectableMapping selectable ) { final String tagName = selectable.getSelectableName(); sb.append( '<' ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/DescriptiveJsonGeneratingVisitor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/DescriptiveJsonGeneratingVisitor.java new file mode 100644 index 000000000000..e9479f640994 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/DescriptiveJsonGeneratingVisitor.java @@ -0,0 +1,216 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.type.descriptor.jdbc.spi; + +import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.collection.spi.CollectionSemantics; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.collection.spi.PersistentMap; +import org.hibernate.internal.util.collections.IdentitySet; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.EntityValuedModelPart; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.ValuedModelPart; +import org.hibernate.metamodel.mapping.internal.BasicValuedCollectionPart; +import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.jdbc.JsonHelper; +import org.hibernate.type.format.JsonDocumentWriter; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.function.Consumer; + +import static org.hibernate.Hibernate.isInitialized; + +/** + * Extension of {@link JsonHelper} that can be used to generate + * descriptive JSON output, that is more appropriate for representing + * complex data structures for displaying purposes. + */ +public class DescriptiveJsonGeneratingVisitor extends JsonGeneratingVisitor { + + private Map> circularityTracker; + + @Override + protected void serializeEntity(Object value, EntityMappingType entityType, WrapperOptions options, JsonDocumentWriter writer) { + final EntityIdentifierMapping identifierMapping = entityType.getIdentifierMapping(); + trackingEntity( value, entityType, shouldProcessEntity -> { + try { + writer.startObject(); + writer.objectKey( identifierMapping.getAttributeName() ); + serializeEntityIdentifier( value, identifierMapping, options, writer ); + if ( shouldProcessEntity ) { + // if it wasn't already encountered, append all properties + serializeObjectValues( entityType, value, options, writer ); + } + writer.endObject(); + } + catch (IOException e) { + throw new UncheckedIOException( "Error serializing entity", e ); + } + } ); + } + + private void trackingEntity(Object entity, EntityMappingType entityType, Consumer action) { + if ( circularityTracker == null ) { + circularityTracker = new HashMap<>(); + } + final IdentitySet entities = circularityTracker.computeIfAbsent( + entityType.getEntityName(), + k -> new IdentitySet<>() + ); + final boolean added = entities.add( entity ); + action.accept( added ); + if ( added ) { + entities.remove( entity ); + } + } + + @Override + protected boolean handleNullOrLazy(Object value, JsonDocumentWriter writer) { + if ( value == null ) { + writer.nullValue(); + return true; + } + else if ( value == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { + writer.stringValue( value.toString() ); + return true; + } + else if ( !isInitialized( value ) ) { + writer.stringValue( "" ); + return true; + } + else { + return false; + } + } + + @Override + protected void serializeModelPart( + ValuedModelPart modelPart, + Object value, + WrapperOptions options, + JsonDocumentWriter writer) throws IOException { + // Extended version of default method that always expands embeddable + // objects and can handle entities and plural attributes + if ( modelPart instanceof SelectableMapping ) { + writer.objectKey( modelPart.getPartName() ); + visit( modelPart.getMappedType(), value, options, writer ); + } + else if ( modelPart instanceof EmbeddedAttributeMapping embeddedAttribute ) { + writer.objectKey( embeddedAttribute.getAttributeName() ); + visit( embeddedAttribute.getMappedType(), value, options, writer ); + } + else if ( modelPart instanceof EntityValuedModelPart entityPart ) { + writer.objectKey( entityPart.getPartName() ); + visit( entityPart.getEntityMappingType(), value, options, writer ); + } + else if ( modelPart instanceof PluralAttributeMapping plural ) { + writer.objectKey( plural.getPartName() ); + serializePluralAttribute( value, plural, options, writer ); + } + else { + // could not handle model part, throw exception + throw new UnsupportedOperationException( + "Support for model part type not yet implemented: " + + (modelPart != null ? modelPart.getClass().getName() : "null") + ); + } + } + + private void serializePluralAttribute( + Object value, + PluralAttributeMapping plural, + WrapperOptions options, + JsonDocumentWriter writer) throws IOException { + if ( handleNullOrLazy( value, writer ) ) { + // nothing left to do + return; + } + + final CollectionPart element = plural.getElementDescriptor(); + final CollectionSemantics collectionSemantics = plural.getMappedType().getCollectionSemantics(); + switch ( collectionSemantics.getCollectionClassification() ) { + case MAP: + case SORTED_MAP: + case ORDERED_MAP: + serializePersistentMap( + (PersistentMap) value, + plural.getIndexDescriptor(), + element, + options, + writer + ); + break; + default: + serializePersistentCollection( + (PersistentCollection) value, + plural.getCollectionDescriptor(), + element, + options, + writer + ); + } + } + + /** + * Serializes a persistent map to JSON [{key: ..., value: ...}, ...] + */ + private void serializePersistentMap( + PersistentMap map, + CollectionPart key, + CollectionPart value, + WrapperOptions options, + JsonDocumentWriter writer) throws IOException { + writer.startArray(); + for ( final Map.Entry entry : map.entrySet() ) { + writer.startObject(); + writer.objectKey( "key" ); + serializeCollectionPart( entry.getKey(), key, options, writer ); + writer.objectKey( "value" ); + serializeCollectionPart( entry.getValue(), value, options, writer ); + writer.endObject(); + } + writer.endArray(); + } + + /** + * Serializes a persistent collection to a JSON array + */ + private void serializePersistentCollection( + PersistentCollection collection, + CollectionPersister persister, + CollectionPart element, + WrapperOptions options, + JsonDocumentWriter appender) throws IOException { + appender.startArray(); + final Iterator entries = collection.entries( persister ); + while ( entries.hasNext() ) { + serializeCollectionPart( entries.next(), element, options, appender ); + } + appender.endArray(); + } + + private void serializeCollectionPart( + Object value, + CollectionPart collectionPart, + WrapperOptions options, + JsonDocumentWriter appender) throws IOException { + if ( collectionPart instanceof BasicValuedCollectionPart basic ) { + appender.serializeJsonValue( value, basic.getJavaType(), basic.getJdbcMapping().getJdbcType(), options ); + } + else { + visit( collectionPart.getMappedType(), value, options, appender ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JsonGeneratingVisitor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JsonGeneratingVisitor.java new file mode 100644 index 000000000000..c237c4f9a63d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JsonGeneratingVisitor.java @@ -0,0 +1,256 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.type.descriptor.jdbc.spi; + +import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ManagedMappingType; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.ValuedModelPart; +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; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JsonJdbcType; +import org.hibernate.type.format.JsonDocumentWriter; + +import java.io.IOException; +import java.lang.reflect.Array; + +import static org.hibernate.type.descriptor.jdbc.StructHelper.getSubPart; + +/** + * Stateless helper class to serialize managed type values to JSON. + */ +public class JsonGeneratingVisitor { + + public static final JsonGeneratingVisitor INSTANCE = new JsonGeneratingVisitor(); + + protected JsonGeneratingVisitor() {} + + /** + * Serializes an array of values into JSON object/array + * + * @param elementJavaType the array element type + * @param elementJdbcType the JDBC type + * @param values values to be serialized + * @param options wrapping options + * @param writer the document writer used for serialization + */ + public void visitArray(JavaType elementJavaType, JdbcType elementJdbcType, Object[] values, WrapperOptions options, JsonDocumentWriter writer) { + writer.startArray(); + if ( values.length == 0 ) { + writer.endArray(); + return; + } + + if ( elementJdbcType instanceof JsonJdbcType jsonElementJdbcType ) { + final EmbeddableMappingType embeddableMappingType = jsonElementJdbcType.getEmbeddableMappingType(); + for ( Object value : values ) { + try { + visit( embeddableMappingType, value, options, writer ); + } + catch (IOException e) { + throw new IllegalArgumentException( "Could not serialize JSON array value", e ); + } + } + } + else { + assert !(elementJdbcType instanceof AggregateJdbcType); + for ( Object value : values ) { + if ( value == null ) { + writer.nullValue(); + } + else { + writer.serializeJsonValue( value, (JavaType) elementJavaType, elementJdbcType, options ); + } + } + } + + writer.endArray(); + } + + /** + * Checks that a JDBCType is assignable to an array + * + * @param type the jdbc type + * @return true if types is of array kind false otherwise. + */ + private static boolean isArrayType(JdbcType type) { + return (type.getDefaultSqlTypeCode() == SqlTypes.ARRAY || + type.getDefaultSqlTypeCode() == SqlTypes.JSON_ARRAY); + } + + public void visit(MappingType mappedType, Object value, WrapperOptions options, JsonDocumentWriter writer) + throws IOException { + if ( handleNullOrLazy( value, writer ) ) { + // nothing left to do + return; + } + + if ( mappedType instanceof EntityMappingType entityType ) { + serializeEntity( value, entityType, options, writer ); + } + 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 ); + writer.startArray(); + if ( length != 0 ) { + //noinspection unchecked + final JavaType elementJavaType = ((BasicPluralJavaType) basicType.getJdbcJavaType()).getElementJavaType(); + final JdbcType elementJdbcType = ((ArrayJdbcType) basicType.getJdbcType()).getElementJdbcType(); + final Object domainArray = basicType.convertToRelationalValue( value ); + for ( int j = 0; j < length; j++ ) { + writer.serializeJsonValue( Array.get( domainArray, j ), elementJavaType, elementJdbcType, options ); + } + } + writer.endArray(); + } + else { + writer.serializeJsonValue( + basicType.convertToRelationalValue( value ), + basicType.getJdbcJavaType(), + basicType.getJdbcType(), + options + ); + } + } + else { + throw new UnsupportedOperationException( + "Support for mapping type not yet implemented: " + mappedType.getClass().getName() + ); + } + } + + /** + * Checks the provided {@code value} is either null or a lazy property. + * + * @param value the value to check + * @param writer the current {@link JsonDocumentWriter} + * @return {@code true} if it was, indicating no further processing of the value is needed, {@code false otherwise}. + */ + protected boolean handleNullOrLazy(Object value, JsonDocumentWriter writer) { + if ( value == null ) { + writer.nullValue(); + return true; + } + else { + return false; + } + } + + /** + * Serialized an Object value to JSON object using a document writer. + * + * @param managedMappingType the managed mapping type of the given value + * @param value the value to be serialized + * @param options wrapping options + * @param writer the document writer + * @throws IOException if the underlying writer failed to serialize a mpped value or failed to perform need I/O. + */ + private void serializeObject(ManagedMappingType managedMappingType, Object value, WrapperOptions options, JsonDocumentWriter writer) + throws IOException { + writer.startObject(); + serializeObjectValues( managedMappingType, value, options, writer ); + writer.endObject(); + } + + /** + * JSON object managed type serialization. + * + * @param managedMappingType the managed mapping type of the given object + * @param object the object to be serialized + * @param options wrapping options + * @param writer the document writer + * @throws IOException if an error occurred while writing to an underlying writer + * @see #serializeObject(ManagedMappingType, Object, WrapperOptions, JsonDocumentWriter) + */ + protected void serializeObjectValues(ManagedMappingType managedMappingType, Object object, WrapperOptions options, JsonDocumentWriter writer) + throws IOException { + final Object[] values = managedMappingType.getValues( object ); + for ( int i = 0; i < values.length; i++ ) { + final ValuedModelPart subPart = getSubPart( managedMappingType, i ); + final Object value = values[i]; + serializeModelPart( subPart, value, options, writer ); + } + } + + protected void serializeModelPart( + ValuedModelPart modelPart, + Object value, + WrapperOptions options, + JsonDocumentWriter writer) throws IOException { + if ( modelPart instanceof SelectableMapping selectableMapping ) { + writer.objectKey( selectableMapping.getSelectableName() ); + visit( modelPart.getMappedType(), value, options, writer ); + } + else if ( modelPart instanceof EmbeddedAttributeMapping embeddedAttribute ) { + if ( value != null ) { + final EmbeddableMappingType mappingType = embeddedAttribute.getMappedType(); + final SelectableMapping aggregateMapping = mappingType.getAggregateMapping(); + if ( aggregateMapping == null ) { + serializeObjectValues( mappingType, value, options, writer ); + } + else { + final String name = aggregateMapping.getSelectableName(); + writer.objectKey( name ); + visit( mappingType, value, options, writer ); + } + } + } + else { + // could not handle model part, throw exception + throw new UnsupportedOperationException( + "Support for model part type not yet implemented: " + + (modelPart != null ? modelPart.getClass().getName() : "null") + ); + } + } + + protected void serializeEntity( + Object value, + EntityMappingType entityType, + WrapperOptions options, + JsonDocumentWriter writer) throws IOException { + // We only need the identifier here + final EntityIdentifierMapping identifierMapping = entityType.getIdentifierMapping(); + serializeEntityIdentifier( value, identifierMapping, options, writer ); + } + + protected void serializeEntityIdentifier( + Object value, + EntityIdentifierMapping identifierMapping, + WrapperOptions options, + JsonDocumentWriter writer) throws IOException { + final Object identifier = identifierMapping.getIdentifier( value ); + if ( identifierMapping instanceof SingleAttributeIdentifierMapping singleAttribute ) { + writer.serializeJsonValue( + identifier, + singleAttribute.getJavaType(), + singleAttribute.getSingleJdbcMapping().getJdbcType(), + options + ); + } + else if ( identifier instanceof CompositeIdentifierMapping composite ) { + visit( composite.getMappedType(), identifier, options, writer ); + } + else { + throw new UnsupportedOperationException( + "Unsupported identifier type: " + identifier.getClass().getName() ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/JsonDocumentWriter.java b/hibernate-core/src/main/java/org/hibernate/type/format/JsonDocumentWriter.java index 03bac2085eb7..99339fbbfb57 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/JsonDocumentWriter.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/JsonDocumentWriter.java @@ -61,6 +61,13 @@ public interface JsonDocumentWriter { */ JsonDocumentWriter nullValue(); + /** + * Adds a new JSON element numeric value. + * @return this instance + * @param value the element numeric name. + */ + JsonDocumentWriter numericValue(Number value); + /** * Adds a new JSON element boolean value. * @return this instance diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/OsonDocumentWriter.java b/hibernate-core/src/main/java/org/hibernate/type/format/OsonDocumentWriter.java index a88eca7a908a..8f5bb1473625 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/OsonDocumentWriter.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/OsonDocumentWriter.java @@ -92,6 +92,31 @@ public JsonDocumentWriter booleanValue(boolean value) { return this; } + @Override + public JsonDocumentWriter numericValue(Number number) { + if ( number instanceof Integer || number instanceof Short || number instanceof Byte ) { + this.generator.write( number.intValue() ); + } + else if ( number instanceof Long ) { + this.generator.write( number.longValue() ); + } + else if ( number instanceof Float ) { + this.generator.write( number.floatValue() ); + } + else if ( number instanceof Double ) { + this.generator.write( number.doubleValue() ); + } + else if ( number instanceof BigInteger ) { + this.generator.write( (BigInteger) number ); + } + else if ( number instanceof BigDecimal ) { + this.generator.write( (BigDecimal) number ); + } + else { + throw new IllegalArgumentException( "Unsupported numeric type: " + number.getClass().getName() ); + } + return this; + } @Override public JsonDocumentWriter stringValue(String value) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/StringJsonDocumentWriter.java b/hibernate-core/src/main/java/org/hibernate/type/format/StringJsonDocumentWriter.java index c08aa01485fc..b092d8a95d15 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/StringJsonDocumentWriter.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/StringJsonDocumentWriter.java @@ -22,6 +22,7 @@ /** * Implementation of JsonDocumentWriter for String-based OSON document. * This implementation will receive a {@link JsonAppender } to a serialze JSON object to it + * * @author Emmanuel Jannetti */ public class StringJsonDocumentWriter extends StringJsonDocument implements JsonDocumentWriter { @@ -32,11 +33,12 @@ public class StringJsonDocumentWriter extends StringJsonDocument implements Json * Creates a new StringJsonDocumentWriter. */ public StringJsonDocumentWriter() { - this(new StringBuilder()); + this( new StringBuilder() ); } /** * Creates a new StringJsonDocumentWriter. + * * @param sb the StringBuilder to receive the serialized JSON */ public StringJsonDocumentWriter(StringBuilder sb) { @@ -50,19 +52,19 @@ public StringJsonDocumentWriter(StringBuilder sb) { @Override public JsonDocumentWriter startObject() { // Note: startArray and startObject must not call moveProcessingStateMachine() - if ( this.processingStates.getCurrent() == JsonProcessingState.STARTING_ARRAY) { + if ( this.processingStates.getCurrent() == JsonProcessingState.STARTING_ARRAY ) { // are we building an array of objects? // i.e, [{},...] // move to JsonProcessingState.ARRAY first - this.processingStates.push( JsonProcessingState.ARRAY); + this.processingStates.push( JsonProcessingState.ARRAY ); } - else if ( this.processingStates.getCurrent() == JsonProcessingState.ARRAY) { + else if ( this.processingStates.getCurrent() == JsonProcessingState.ARRAY ) { // That means that we ae building an array of object ([{},...]) // JSON object hee are treat as array item. // -> add the marker first - this.appender.append(StringJsonDocumentMarker.SEPARATOR.getMarkerCharacter()); + this.appender.append( StringJsonDocumentMarker.SEPARATOR.getMarkerCharacter() ); } - this.appender.append( StringJsonDocumentMarker.OBJECT_START.getMarkerCharacter()); + this.appender.append( StringJsonDocumentMarker.OBJECT_START.getMarkerCharacter() ); this.processingStates.push( JsonProcessingState.STARTING_OBJECT ); return this; } @@ -73,7 +75,7 @@ else if ( this.processingStates.getCurrent() == JsonProcessingState.ARRAY) { @Override public JsonDocumentWriter endObject() { this.appender.append( StringJsonDocumentMarker.OBJECT_END.getMarkerCharacter() ); - this.processingStates.push( JsonProcessingState.ENDING_OBJECT); + this.processingStates.push( JsonProcessingState.ENDING_OBJECT ); moveProcessingStateMachine(); return this; } @@ -95,20 +97,18 @@ public JsonDocumentWriter startArray() { @Override public JsonDocumentWriter endArray() { this.appender.append( StringJsonDocumentMarker.ARRAY_END.getMarkerCharacter() ); - this.processingStates.push( JsonProcessingState.ENDING_ARRAY); + this.processingStates.push( JsonProcessingState.ENDING_ARRAY ); moveProcessingStateMachine(); return this; } - @Override public JsonDocumentWriter objectKey(String key) { - - if (key == null || key.length() == 0) { + if ( key == null || key.isEmpty() ) { throw new IllegalArgumentException( "key cannot be null or empty" ); } - if (JsonProcessingState.OBJECT.equals(this.processingStates.getCurrent())) { + if ( JsonProcessingState.OBJECT.equals( this.processingStates.getCurrent() ) ) { // we have started an object, and we are adding an item key: we do add a separator. this.appender.append( StringJsonDocumentMarker.SEPARATOR.getMarkerCharacter() ); } @@ -126,7 +126,7 @@ public JsonDocumentWriter objectKey(String key) { * Separator is to separate array items or key/value pairs in an object. */ private void addItemsSeparator() { - if (this.processingStates.getCurrent().equals( JsonProcessingState.ARRAY )) { + if ( this.processingStates.getCurrent().equals( JsonProcessingState.ARRAY ) ) { // We started to serialize an array and already added item to it:add a separator anytime. this.appender.append( StringJsonDocumentMarker.SEPARATOR.getMarkerCharacter() ); } @@ -152,10 +152,9 @@ private void addItemsSeparator() { * -> EO -> NONE * * - * */ private void moveProcessingStateMachine() { - switch (this.processingStates.getCurrent()) { + switch ( this.processingStates.getCurrent() ) { case STARTING_OBJECT: //after starting an object, we start adding key/value pairs this.processingStates.push( JsonProcessingState.OBJECT ); @@ -171,8 +170,9 @@ private void moveProcessingStateMachine() { // first pop ENDING_ARRAY this.processingStates.pop(); // if we have ARRAY, so that's not an empty array. pop that state - if (this.processingStates.getCurrent().equals( JsonProcessingState.ARRAY )) + if ( this.processingStates.getCurrent().equals( JsonProcessingState.ARRAY ) ) { this.processingStates.pop(); + } assert this.processingStates.pop().equals( JsonProcessingState.STARTING_ARRAY ); break; case ENDING_OBJECT: @@ -182,8 +182,9 @@ private void moveProcessingStateMachine() { // first pop ENDING_OBJECT this.processingStates.pop(); // if we have OBJECT, so that's not an empty object. pop that state - if (this.processingStates.getCurrent().equals( JsonProcessingState.OBJECT )) + if ( this.processingStates.getCurrent().equals( JsonProcessingState.OBJECT ) ) { this.processingStates.pop(); + } assert this.processingStates.pop().equals( JsonProcessingState.STARTING_OBJECT ); break; default: @@ -199,10 +200,18 @@ public JsonDocumentWriter nullValue() { return this; } + @Override + public JsonDocumentWriter numericValue(Number value) { + addItemsSeparator(); + appender.append( value.toString() ); + moveProcessingStateMachine(); + return this; + } + @Override public JsonDocumentWriter booleanValue(boolean value) { addItemsSeparator(); - BooleanJavaType.INSTANCE.appendEncodedString( this.appender, value); + BooleanJavaType.INSTANCE.appendEncodedString( this.appender, value ); moveProcessingStateMachine(); return this; } @@ -211,11 +220,11 @@ public JsonDocumentWriter booleanValue(boolean value) { public JsonDocumentWriter stringValue(String value) { addItemsSeparator(); - appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter()); + appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); appender.startEscaping(); appender.append( value ); appender.endEscaping(); - appender.append(StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); + appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); moveProcessingStateMachine(); return this; @@ -224,22 +233,17 @@ public JsonDocumentWriter stringValue(String value) { @Override public JsonDocumentWriter serializeJsonValue(Object value, JavaType javaType, JdbcType jdbcType, WrapperOptions options) { addItemsSeparator(); - convertedBasicValueToString(value, options,this.appender,javaType,jdbcType); + convertedBasicValueToString( value, options, this.appender, javaType, jdbcType ); moveProcessingStateMachine(); return this; } - private void convertedCastBasicValueToString(Object value, - WrapperOptions options, - JsonAppender appender, - JavaType javaType, - JdbcType jdbcType) { + private void convertedCastBasicValueToString(Object value, WrapperOptions options, JsonAppender appender, JavaType javaType, JdbcType jdbcType) { assert javaType.isInstance( value ); //noinspection unchecked convertedBasicValueToString( (T) value, options, appender, javaType, jdbcType ); } - /** * Converts a value to String according to its mapping type. * This method serializes the value and writes it into the underlying appender @@ -255,7 +259,6 @@ private void convertedBasicValueToString( JsonAppender appender, JavaType javaType, JdbcType jdbcType) { - assert javaType.isInstance( value ); switch ( jdbcType.getDefaultSqlTypeCode() ) { @@ -268,7 +271,7 @@ private void convertedBasicValueToString( break; } if ( value instanceof Enum ) { - appender.appendSql( ((Enum) value ).ordinal() ); + appender.appendSql( ((Enum) value).ordinal() ); break; } case SqlTypes.BOOLEAN: @@ -278,7 +281,7 @@ private void convertedBasicValueToString( case SqlTypes.REAL: case SqlTypes.DOUBLE: // These types fit into the native representation of JSON, so let's use that - javaType.appendEncodedString( appender, (T)value ); + javaType.appendEncodedString( appender, (T) value ); break; case SqlTypes.CHAR: case SqlTypes.NCHAR: @@ -288,7 +291,7 @@ private void convertedBasicValueToString( // BooleanJavaType has this as an implicit conversion appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); appender.append( (Boolean) value ? 'Y' : 'N' ); - appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter()); + appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); break; } case SqlTypes.LONGVARCHAR: @@ -304,7 +307,7 @@ private void convertedBasicValueToString( // These literals can contain the '"' character, so we need to escape it appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); appender.startEscaping(); - javaType.appendEncodedString( appender, (T)value ); + javaType.appendEncodedString( appender, (T) value ); appender.endEscaping(); appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); break; @@ -312,7 +315,7 @@ private void convertedBasicValueToString( appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); JdbcDateJavaType.INSTANCE.appendEncodedString( appender, - javaType.unwrap( (T)value, java.sql.Date.class, options ) + javaType.unwrap( (T) value, java.sql.Date.class, options ) ); appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); break; @@ -322,7 +325,7 @@ private void convertedBasicValueToString( appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); JdbcTimeJavaType.INSTANCE.appendEncodedString( appender, - javaType.unwrap( (T)value, java.sql.Time.class, options ) + javaType.unwrap( (T) value, java.sql.Time.class, options ) ); appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); break; @@ -330,7 +333,7 @@ private void convertedBasicValueToString( appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); JdbcTimestampJavaType.INSTANCE.appendEncodedString( appender, - javaType.unwrap( (T)value, java.sql.Timestamp.class, options ) + javaType.unwrap( (T) value, java.sql.Timestamp.class, options ) ); appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); break; @@ -338,7 +341,7 @@ private void convertedBasicValueToString( case SqlTypes.TIMESTAMP_UTC: appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); DateTimeFormatter.ISO_OFFSET_DATE_TIME.formatTo( - javaType.unwrap( (T)value, OffsetDateTime.class, options ), + javaType.unwrap( (T) value, OffsetDateTime.class, options ), appender ); appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); @@ -349,7 +352,7 @@ private void convertedBasicValueToString( case SqlTypes.UUID: // These types need to be serialized as JSON string, but don't have a need for escaping appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); - javaType.appendEncodedString( appender, (T)value ); + javaType.appendEncodedString( appender, (T) value ); appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); break; case SqlTypes.BINARY: @@ -360,13 +363,13 @@ private void convertedBasicValueToString( case SqlTypes.MATERIALIZED_BLOB: // These types need to be serialized as JSON string, and for efficiency uses appendString directly appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); - appender.write( javaType.unwrap( (T)value, byte[].class, options ) ); + appender.write( javaType.unwrap( (T) value, byte[].class, options ) ); appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); break; case SqlTypes.ARRAY: case SqlTypes.JSON_ARRAY: // Caller handles this. We should never end up here actually. - throw new IllegalStateException("unexpected JSON array type"); + throw new IllegalStateException( "unexpected JSON array type" ); default: throw new UnsupportedOperationException( "Unsupported JdbcType nested in JSON: " + jdbcType ); } @@ -467,7 +470,7 @@ public JsonAppender append(CharSequence csq, int start, int end) { public void write(int v) { final String hex = Integer.toHexString( v ); sb.ensureCapacity( sb.length() + hex.length() + 1 ); - if ( ( hex.length() & 1 ) == 1 ) { + if ( (hex.length() & 1) == 1 ) { sb.append( '0' ); } sb.append( hex ); @@ -475,12 +478,12 @@ public void write(int v) { @Override public void write(byte[] bytes) { - write(bytes, 0, bytes.length); + write( bytes, 0, bytes.length ); } @Override public void write(byte[] bytes, int off, int len) { - sb.ensureCapacity( sb.length() + ( len << 1 ) ); + sb.ensureCapacity( sb.length() + (len << 1) ); for ( int i = 0; i < len; i++ ) { final int v = bytes[off + i] & 0xFF; sb.append( HEX_ARRAY[v >>> 4] ); @@ -498,12 +501,12 @@ private void appendEscaped(char fragment) { case 5: case 6: case 7: - // 8 is '\b' - // 9 is '\t' - // 10 is '\n' + // 8 is '\b' + // 9 is '\t' + // 10 is '\n' case 11: - // 12 is '\f' - // 13 is '\r' + // 12 is '\f' + // 13 is '\r' case 14: case 15: case 16: @@ -525,19 +528,19 @@ private void appendEscaped(char fragment) { sb.append( "\\u" ).append( Integer.toHexString( fragment ) ); break; case '\b': - sb.append("\\b"); + sb.append( "\\b" ); break; case '\t': - sb.append("\\t"); + sb.append( "\\t" ); break; case '\n': - sb.append("\\n"); + sb.append( "\\n" ); break; case '\f': - sb.append("\\f"); + sb.append( "\\f" ); break; case '\r': - sb.append("\\r"); + sb.append( "\\r" ); break; case '"': sb.append( "\\\"" ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/util/DescriptiveJsonGeneratingVisitorSmokeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/util/DescriptiveJsonGeneratingVisitorSmokeTest.java new file mode 100644 index 000000000000..f7411909a40a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/util/DescriptiveJsonGeneratingVisitorSmokeTest.java @@ -0,0 +1,279 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.type.descriptor.jdbc.spi.DescriptiveJsonGeneratingVisitor; +import org.hibernate.type.format.StringJsonDocumentWriter; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@DomainModel( annotatedClasses = { + DescriptiveJsonGeneratingVisitorSmokeTest.Company.class, + DescriptiveJsonGeneratingVisitorSmokeTest.Address.class, + DescriptiveJsonGeneratingVisitorSmokeTest.Employee.class, +} ) +@SessionFactory +public class DescriptiveJsonGeneratingVisitorSmokeTest { + private final DescriptiveJsonGeneratingVisitor visitor = new DescriptiveJsonGeneratingVisitor(); + + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void testCompany(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final EntityPersister entityDescriptor = sessionFactory.getMappingMetamodel() + .findEntityDescriptor( Company.class ); + + scope.inTransaction( session -> { + final Company company = session.createQuery( + "from Company where id = 1", + Company.class + ).getSingleResult(); + + try { + final StringJsonDocumentWriter writer = new StringJsonDocumentWriter(); + visitor.visit( entityDescriptor.getEntityMappingType(), company, sessionFactory.getWrapperOptions(), writer ); + final String result = writer.toString(); + + final JsonNode jsonNode = mapper.readTree( result ); + assertThat( jsonNode.get( "id" ).intValue() ).isEqualTo( 1 ); + assertThat( jsonNode.get( "name" ).textValue() ).isEqualTo( "Red Hat" ); + assertThat( jsonNode.get( "employees" ).textValue() ).isEqualTo( "" ); + + final JsonNode address = jsonNode.get( "address" ); + assertThat( address.get( "city" ).textValue() ).isEqualTo( "Milan" ); + assertThat( address.get( "street" ).textValue() ).isEqualTo( "Via Gustavo Fara" ); + } + catch (Exception e) { + fail( "Test failed with exception", e ); + } + } ); + } + + @Test + public void testCompanyFetchEmployees(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final EntityPersister entityDescriptor = sessionFactory.getMappingMetamodel() + .findEntityDescriptor( Company.class ); + + scope.inTransaction( session -> { + final Company company = session.createQuery( + "from Company c join fetch c.employees where c.id = 1", + Company.class + ).getSingleResult(); + + try { + final StringJsonDocumentWriter writer = new StringJsonDocumentWriter(); + visitor.visit( entityDescriptor.getEntityMappingType(), company, sessionFactory.getWrapperOptions(), writer ); + final String result = writer.toString(); + + final JsonNode jsonNode = mapper.readTree( result ); + assertThat( jsonNode.get( "id" ).intValue() ).isEqualTo( 1 ); + assertThat( jsonNode.get( "name" ).textValue() ).isEqualTo( "Red Hat" ); + + final JsonNode employees = jsonNode.get( "employees" ); + assertThat( employees.isArray() ).isTrue(); + employees.forEach( employee -> { + assertDoesNotThrow( () -> UUID.fromString( employee.get( "uniqueIdentifier" ).asText() ) ); + assertThat( employee.get( "firstName" ).textValue() ).startsWith( "Ma" ); + final JsonNode c = employee.get( "company" ); + assertThat( c.get( "id" ).intValue() ).isEqualTo( 1 ); + assertThat( c.properties().stream().map( Map.Entry::getKey ) ) + .containsOnly( "id" ); // circular relationship + } ); + } + catch (Exception e) { + fail( "Test failed with exception", e ); + } + } ); + } + + @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 ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Company") + static class Company implements Serializable { + @Id + private long id; + + @Column(nullable = false) + private String name; + + @Embedded + private Address address; + + @OneToMany(mappedBy = "company") + private List employees; + + public Company() { + } + + public Company(long id, String name, Address address) { + this.id = id; + this.name = name; + this.address = address; + this.employees = new ArrayList<>(); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + } + + @Embeddable + static class Address implements Serializable { + private String city; + + private String street; + + public Address() { + } + + public Address(String city, String street) { + this.city = city; + this.street = street; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + } + + + @Entity(name = "Employee") + static class Employee { + private UUID uniqueIdentifier; + + private String firstName; + + private String lastName; + + private float salary; + + private Company company; + + public Employee() { + } + + public Employee(UUID uniqueIdentifier, String firstName, String lastName, float salary, Company company) { + this.uniqueIdentifier = uniqueIdentifier; + this.firstName = firstName; + this.lastName = lastName; + this.salary = salary; + this.company = company; + } + + @Id + public UUID getUniqueIdentifier() { + return uniqueIdentifier; + } + + public void setUniqueIdentifier(UUID uniqueIdentifier) { + this.uniqueIdentifier = uniqueIdentifier; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public float getSalary() { + return salary; + } + + public void setSalary(float salary) { + this.salary = salary; + } + + @ManyToOne + public Company getCompany() { + return company; + } + + public void setCompany(Company company) { + this.company = company; + } + } +}