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/JsonHelper.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonHelper.java index 5021a6e6c1f5..f8a323d5971d 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 @@ -6,6 +6,7 @@ import java.io.IOException; +import java.io.UncheckedIOException; import java.lang.reflect.Array; import java.sql.SQLException; import java.util.AbstractCollection; @@ -14,20 +15,35 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Internal; +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.build.AllowReflection; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.internal.util.collections.StandardStack; +import org.hibernate.metamodel.mapping.CollectionPart; +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.EntityValuedModelPart; 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.SelectableMapping; import org.hibernate.metamodel.mapping.ValuedModelPart; +import org.hibernate.metamodel.mapping.internal.BasicValuedCollectionPart; import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; +import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; +import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.type.BasicPluralType; import org.hibernate.type.BasicType; import org.hibernate.type.SqlTypes; @@ -37,7 +53,9 @@ 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.Hibernate.isInitialized; +import static org.hibernate.type.descriptor.jdbc.StructHelper.getSubPart; import static org.hibernate.type.descriptor.jdbc.StructHelper.instantiate; import org.hibernate.type.format.JsonValueJDBCTypeAdapter; import org.hibernate.type.format.JsonValueJDBCTypeAdapterFactory; @@ -67,7 +85,7 @@ public static void serializeArray(MappingType elementMappingType, Object[] value } for ( Object value : values ) { try { - serialize(elementMappingType, value, options, writer); + serializeValue(elementMappingType, value, options, writer); } catch (IOException e) { throw new IllegalArgumentException( "Could not serialize JSON array value" , e ); @@ -111,112 +129,292 @@ private static boolean isArrayType(JdbcType type) { 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) + public static void serializeValue(MappingType mappedType, Object value, WrapperOptions options, JsonDocumentWriter writer) throws IOException { - if ( value == null ) { - writer.nullValue(); + if ( handleNullOrLazy( value, writer ) ) { + // nothing left to do + return; } - else if ( mappedType instanceof EmbeddableMappingType ) { - serialize( (EmbeddableMappingType) mappedType, value, options, writer ); + + if ( mappedType instanceof EntityMappingType entityType ) { + serializeEntity( value, entityType, options, writer ); + } + else if ( mappedType instanceof ManagedMappingType managedMappingType ) { + serialize( managedMappingType, value, options, writer ); } - else if ( mappedType instanceof BasicType basicType) { - if ( isArrayType(basicType.getJdbcType())) { + 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(); + //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.serializeJsonValue( Array.get( domainArray, j ), elementJavaType, elementJdbcType, options ); } } writer.endArray(); } else { - writer.serializeJsonValue(basicType.convertToRelationalValue( value), - (JavaType)basicType.getJdbcJavaType(),basicType.getJdbcType(), options); + writer.serializeJsonValue( + basicType.convertToRelationalValue( value ), + basicType.getJdbcJavaType(), + basicType.getJdbcType(), + options + ); } } else { - throw new UnsupportedOperationException( "Support for mapping type not yet implemented: " + mappedType.getClass().getName() ); + 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}. + */ + private static boolean handleNullOrLazy(Object value, JsonDocumentWriter writer) { + if ( value == null ) { + writer.nullValue(); + return true; + } + else if ( writer.expandProperties() ) { + // avoid force-initialization when serializing all properties + if ( value == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { + writer.stringValue( value.toString() ); + return true; + } + else if ( !isInitialized( value ) ) { + writer.stringValue( "" ); + return true; + } } + 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. + */ + public static void serialize(ManagedMappingType managedMappingType, Object value, WrapperOptions options, JsonDocumentWriter writer) + throws IOException { + writer.startObject(); + serializeObject( managedMappingType, value, options, writer ); + writer.endObject(); } /** - * 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. + * 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 #serialize(ManagedMappingType, Object, WrapperOptions, JsonDocumentWriter) */ - private static void serializeMapping(EmbeddableMappingType embeddableMappingType, - Object domainValue, WrapperOptions options, JsonDocumentWriter writer) throws IOException { - final Object[] values = embeddableMappingType.getValues( domainValue ); + private static void serializeObject(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 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); - } + final ValuedModelPart subPart = getSubPart( managedMappingType, i ); + final Object value = values[i]; + serializeModelPart( subPart, value, options, writer ); + } + } + private static void serializeModelPart( + ValuedModelPart modelPart, + Object value, + WrapperOptions options, + JsonDocumentWriter writer) throws IOException { + if ( modelPart instanceof SelectableMapping selectableMapping ) { + writer.objectKey( writer.expandProperties() ? modelPart.getPartName() : selectableMapping.getSelectableName() ); + serializeValue( modelPart.getMappedType(), value, options, writer ); + } + else if ( modelPart instanceof EmbeddedAttributeMapping embeddedAttribute ) { + if ( writer.expandProperties() ) { + writer.objectKey( embeddedAttribute.getAttributeName() ); + serializeValue( embeddedAttribute.getMappedType(), value, options, writer ); } - else if ( attributeMapping instanceof EmbeddedAttributeMapping ) { - if ( values[i] == null ) { - continue; + else { + if ( value == null ) { + return; } - final EmbeddableMappingType mappingType = (EmbeddableMappingType) attributeMapping.getMappedType(); + + final EmbeddableMappingType mappingType = embeddedAttribute.getMappedType(); final SelectableMapping aggregateMapping = mappingType.getAggregateMapping(); - if (aggregateMapping == null) { - serializeMapping( - mappingType, - values[i], - options, - writer ); + if ( aggregateMapping == null ) { + serializeObject( mappingType, value, options, writer ); } else { final String name = aggregateMapping.getSelectableName(); writer.objectKey( name ); - writer.startObject(); - serializeMapping( - mappingType, - values[i], - options, - writer); - writer.endObject(); + serializeValue( mappingType, value, options, writer ); + } + } + } + else if ( writer.expandProperties() ) { + // Entity and plural attribute serialization is only supported for expanded properties + if ( modelPart instanceof EntityValuedModelPart entityPart ) { + writer.objectKey( entityPart.getPartName() ); + serializeValue( 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 static void serializeEntity( + Object value, + EntityMappingType entityType, + WrapperOptions options, + JsonDocumentWriter writer) throws IOException { + final EntityIdentifierMapping identifierMapping = entityType.getIdentifierMapping(); + writer.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 + serializeObject( entityType, value, options, writer ); } + writer.endObject(); } - else { - throw new UnsupportedOperationException( "Support for attribute mapping type not yet implemented: " + attributeMapping.getClass().getName() ); + catch (IOException e) { + throw new UncheckedIOException( "Error serializing entity", e ); } + } ); + } + private static 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 ) { + serializeValue( composite.getMappedType(), identifier, options, writer ); + } + else { + throw new UnsupportedOperationException( "Unsupported identifier type: " + identifier.getClass().getName() ); + } + } + + private static 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 static 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 static 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 static 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 { + serializeValue( collectionPart.getMappedType(), value, options, appender ); } } @@ -227,7 +425,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/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/format/JsonDocumentWriter.java b/hibernate-core/src/main/java/org/hibernate/type/format/JsonDocumentWriter.java index 03bac2085eb7..1ad14bb5294f 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 @@ -5,11 +5,14 @@ package org.hibernate.type.format; +import org.hibernate.Internal; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; import java.io.IOException; +import java.util.function.Consumer; /** * JSON document producer. @@ -87,4 +90,28 @@ JsonDocumentWriter serializeJsonValue(Object value, JavaType javaType, JdbcType jdbcType, WrapperOptions options); + + /** + * Returns {@code true} if this writer always expands properties to nested JSON objects, + * which is useful for obtaining a verbose representation Hibernate data in JSON format. + * + * @return {@code true} if properties should be expanded, {@code false} otherwise + */ + @Internal + default boolean expandProperties() { + return false; + } + + /** + * Tracks the provided {@code entity} instance and invokes the {@code action} with either + * {@code true} if the entity was not already encountered or {@code false} otherwise. + * + * @param entity the entity instance to track + * @param entityType the type of the entity instance + * @param action the action to invoke while tracking the entity + */ + @Internal + default void trackingEntity(Object entity, EntityMappingType entityType, Consumer action) throws IOException { + action.accept( true ); + } } 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..d8ff4792e931 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 @@ -4,6 +4,8 @@ */ package org.hibernate.type.format; +import org.hibernate.internal.util.collections.IdentitySet; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.WrapperOptions; @@ -17,31 +19,47 @@ import java.io.OutputStream; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; /** * 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 { private final JsonAppender appender; + private final boolean expandProperties; + + private Map> circularityTracker; + /** * Creates a new StringJsonDocumentWriter. */ public StringJsonDocumentWriter() { - this(new StringBuilder()); + this( new StringBuilder(), false ); } /** * Creates a new StringJsonDocumentWriter. + * * @param sb the StringBuilder to receive the serialized JSON + * @param expandProperties see {@link #expandProperties()} */ - public StringJsonDocumentWriter(StringBuilder sb) { + public StringJsonDocumentWriter(StringBuilder sb, boolean expandProperties) { this.processingStates.push( JsonProcessingState.NONE ); this.appender = new JsonAppender( sb ); + this.expandProperties = expandProperties; + } + + @Override + public boolean expandProperties() { + return expandProperties; } /** @@ -50,19 +68,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 +91,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 +113,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 +142,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 +168,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 +186,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 +198,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: @@ -202,20 +219,28 @@ public JsonDocumentWriter nullValue() { @Override public JsonDocumentWriter booleanValue(boolean value) { addItemsSeparator(); - BooleanJavaType.INSTANCE.appendEncodedString( this.appender, value); + BooleanJavaType.INSTANCE.appendEncodedString( this.appender, value ); moveProcessingStateMachine(); return this; } @Override public JsonDocumentWriter stringValue(String value) { + return stringValue( value, true ); + } + + public JsonDocumentWriter stringValue(String value, boolean quote) { addItemsSeparator(); - appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter()); - appender.startEscaping(); + if ( quote ) { + appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); + appender.startEscaping(); + } appender.append( value ); - appender.endEscaping(); - appender.append(StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); + if ( quote ) { + appender.endEscaping(); + appender.append( StringJsonDocumentMarker.QUOTE.getMarkerCharacter() ); + } moveProcessingStateMachine(); return this; @@ -224,22 +249,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 +275,6 @@ private void convertedBasicValueToString( JsonAppender appender, JavaType javaType, JdbcType jdbcType) { - assert javaType.isInstance( value ); switch ( jdbcType.getDefaultSqlTypeCode() ) { @@ -268,7 +287,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 +297,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 +307,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 +323,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 +331,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 +341,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 +349,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 +357,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 +368,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 +379,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 ); } @@ -381,6 +400,22 @@ public String toString() { return appender.toString(); } + @Override + public 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 ); + } + } + private static class JsonAppender extends OutputStream implements SqlAppender { private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); @@ -467,7 +502,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 +510,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 +533,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 +560,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( "\\\"" );