From 390fb6ca9d54f1ed86f5c7e9212050041c2b076f Mon Sep 17 00:00:00 2001 From: Marco Belladelli Date: Tue, 5 Aug 2025 16:33:55 +0200 Subject: [PATCH 1/2] HBX-3084 Take advantage of ORM's native JSON capabilities --- .../tool/language/internal/JsonHelper.java | 1818 ----------------- .../internal/MetamodelJsonSerializerImpl.java | 112 +- .../internal/ResultsJsonSerializerImpl.java | 116 +- .../tool/language/spi/ResultsSerializer.java | 3 +- .../language/MetamodelJsonSerializerTest.java | 14 +- .../language/ResultsJsonSerializerTest.java | 37 +- 6 files changed, 163 insertions(+), 1937 deletions(-) delete mode 100644 language/src/main/java/org/hibernate/tool/language/internal/JsonHelper.java diff --git a/language/src/main/java/org/hibernate/tool/language/internal/JsonHelper.java b/language/src/main/java/org/hibernate/tool/language/internal/JsonHelper.java deleted file mode 100644 index 06ea05ab30..0000000000 --- a/language/src/main/java/org/hibernate/tool/language/internal/JsonHelper.java +++ /dev/null @@ -1,1818 +0,0 @@ -/* - * Hibernate Tools, Tooling for your Hibernate Projects - * - * Copyright 2023-2025 Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.hibernate.tool.language.internal; - -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.CharSequenceHelper; -import org.hibernate.internal.util.collections.ArrayHelper; -import org.hibernate.internal.util.collections.IdentitySet; -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.sql.ast.spi.SqlAppender; -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.EnumJavaType; -import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.descriptor.java.JdbcDateJavaType; -import org.hibernate.type.descriptor.java.JdbcTimeJavaType; -import org.hibernate.type.descriptor.java.JdbcTimestampJavaType; -import org.hibernate.type.descriptor.java.OffsetDateTimeJavaType; -import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; -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.StructAttributeValues; -import org.hibernate.type.descriptor.jdbc.StructHelper; - -import java.io.OutputStream; -import java.lang.reflect.Array; -import java.sql.SQLException; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.util.AbstractCollection; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import static org.hibernate.Hibernate.isInitialized; -import static org.hibernate.type.descriptor.jdbc.StructHelper.instantiate; - -/** - * A Helper for serializing JSON, based on the {@link org.hibernate.metamodel.mapping mapping model}. - * - * @implNote This is a subset of the functionalities of {@link org.hibernate.type.descriptor.jdbc.JsonHelper}, - * extracted from ORM as that is being worked on at the moment. The goal is to align the implementations, - * and have a single place for the JSON serialization/deserialization logic within Hibernate core. - */ -@Internal -public class JsonHelper { - - private static void managedTypeToString( - Object object, - ManagedMappingType managedMappingType, - WrapperOptions options, - JsonAppender appender, - char separator) { - 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]; - separator = toString( value, subPart, options, appender, separator ); - } - } - - static ValuedModelPart getSubPart(ManagedMappingType type, int position) { - if ( position == type.getNumberOfAttributeMappings() ) { - assert type instanceof EmbeddableMappingType; - return ( (EmbeddableMappingType) type ).getDiscriminatorMapping(); - } - return type.getAttributeMapping( position ); - } - - public static Character toString( - Object value, - ValuedModelPart modelPart, - WrapperOptions options, - JsonAppender appender, - Character separator) { - if ( modelPart instanceof SelectableMapping selectable ) { - separateAndQuote( - () -> appender.expandProperties() ? modelPart.getPartName() : selectable.getSelectableName(), - separator, - appender - ); - toString( value, modelPart.getMappedType(), options, appender ); - return ','; - } - else if ( modelPart instanceof EmbeddedAttributeMapping embeddedAttribute ) { - if ( appender.expandProperties() ) { - separateAndQuote( embeddedAttribute::getAttributeName, separator, appender ); - toString( value, embeddedAttribute.getMappedType(), options, appender ); - } - else { - if ( value == null ) { - // Skipping the update of the separator is on purpose - return separator; - } - - final EmbeddableMappingType mappingType = embeddedAttribute.getMappedType(); - final SelectableMapping aggregateMapping = mappingType.getAggregateMapping(); - if ( aggregateMapping == null ) { - managedTypeToString( value, mappingType, options, appender, separator ); - } - else { - separateAndQuote( aggregateMapping::getSelectableName, separator, appender ); - toString( value, mappingType, options, appender ); - } - } - return ','; - } - else if ( appender.expandProperties() ) { - if ( modelPart instanceof EntityValuedModelPart entityPart ) { - separateAndQuote( entityPart::getPartName, separator, appender ); - toString( value, entityPart.getEntityMappingType(), options, appender ); - return ','; - } - else if ( modelPart instanceof PluralAttributeMapping plural ) { - separateAndQuote( plural::getPartName, separator, appender ); - pluralAttributeToString( value, plural, options, appender ); - return ','; - } - } - - // 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 separateAndQuote(Supplier nameSupplier, Character separator, JsonAppender appender) { - if ( separator != null ) { - final String name = nameSupplier.get(); - appender.append( separator ).append( '"' ).append( name ).append( "\":" ); - } - } - - private static void entityToString( - Object value, - EntityMappingType entityType, - WrapperOptions options, - JsonAppender appender) { - final EntityIdentifierMapping identifierMapping = entityType.getIdentifierMapping(); - appender.trackingEntity( value, entityType, shouldProcessEntity -> { - appender.append( "{\"" ).append( identifierMapping.getAttributeName() ).append( "\":" ); - entityIdentifierToString( value, identifierMapping, options, appender ); - if ( shouldProcessEntity ) { - // if it wasn't already encountered, append all properties - managedTypeToString( value, entityType, options, appender, ',' ); - } - appender.append( '}' ); - } ); - } - - private static void entityIdentifierToString( - Object value, - EntityIdentifierMapping identifierMapping, - WrapperOptions options, - JsonAppender appender) { - final Object identifier = identifierMapping.getIdentifier( value ); - if ( identifierMapping instanceof SingleAttributeIdentifierMapping singleAttribute ) { - //noinspection unchecked - convertedValueToString( - (JavaType) singleAttribute.getJavaType(), - singleAttribute.getSingleJdbcMapping().getJdbcType(), - identifier, - options, - appender - ); - } - else if ( identifier instanceof CompositeIdentifierMapping composite ) { - toString( identifier, composite.getMappedType(), options, appender ); - } - else { - throw new UnsupportedOperationException( "Unsupported identifier type: " + identifier.getClass().getName() ); - } - } - - private static void pluralAttributeToString( - Object value, - PluralAttributeMapping plural, - WrapperOptions options, - JsonAppender appender) { - if ( handleNullOrLazy( value, appender ) ) { - // 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: - final PersistentMap pm = (PersistentMap) value; - persistentMapToString( pm, plural.getIndexDescriptor(), element, options, appender ); - break; - default: - final PersistentCollection pc = (PersistentCollection) value; - final Iterator entries = pc.entries( plural.getCollectionDescriptor() ); - char separator = '['; - while ( entries.hasNext() ) { - appender.append( separator ); - collectionPartToString( entries.next(), element, options, appender ); - separator = ','; - } - appender.append( ']' ); - } - } - - private static void persistentMapToString( - PersistentMap map, - CollectionPart key, - CollectionPart value, - WrapperOptions options, - JsonAppender appender) { - char separator = '{'; - for ( final Map.Entry entry : map.entrySet() ) { - appender.append( separator ); - collectionPartToString( entry.getKey(), key, options, appender ); - appender.append( ':' ); - collectionPartToString( entry.getValue(), value, options, appender ); - separator = ','; - } - appender.append( '}' ); - } - - private static void collectionPartToString( - Object value, - CollectionPart collectionPart, - WrapperOptions options, - JsonAppender appender) { - if ( collectionPart instanceof BasicValuedCollectionPart basic ) { - // special case for basic values as they use lambdas as mapping type - //noinspection unchecked - convertedValueToString( - (JavaType) basic.getJavaType(), - basic.getJdbcMapping().getJdbcType(), - value, - options, - appender - ); - } - else { - toString( value, collectionPart.getMappedType(), options, appender ); - } - } - - public static void toString(Object value, MappingType mappedType, WrapperOptions options, JsonAppender appender) { - if ( handleNullOrLazy( value, appender ) ) { - // nothing left to do - return; - } - - if ( mappedType instanceof EntityMappingType entityType ) { - entityToString( value, entityType, options, appender ); - } - else if ( mappedType instanceof ManagedMappingType managedMappingType ) { - managedTypeToString( value, managedMappingType, options, appender, '{' ); - appender.append( '}' ); - } - else if ( mappedType instanceof BasicType type ) { - //noinspection unchecked - convertedBasicValueToString( - type.convertToRelationalValue( value ), - options, - appender, - (JavaType) type.getJdbcJavaType(), - type.getJdbcType() - ); - } - 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 appender the current {@link JsonAppender} - * - * @return {@code true} if it was, indicating no further processing of the value is needed, {@code false otherwise}. - */ - private static boolean handleNullOrLazy(Object value, JsonAppender appender) { - if ( value == null ) { - appender.append( "null" ); - return true; - } - else if ( appender.expandProperties() ) { - // avoid force-initialization when serializing all properties - if ( value == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { - appender.append( '"' ).append( value.toString() ).append( '"' ); - return true; - } - else if ( !isInitialized( value ) ) { - appender.append( '"' ).append( "" ).append( '"' ); - return true; - } - } - return false; - } - - private static void convertedValueToString( - JavaType javaType, - JdbcType jdbcType, - Object value, - WrapperOptions options, - JsonAppender appender) { - if ( value == null ) { - appender.append( "null" ); - } - else if ( jdbcType instanceof AggregateJdbcType aggregateJdbcType ) { - toString( value, aggregateJdbcType.getEmbeddableMappingType(), options, appender ); - } - else { - convertedBasicValueToString( value, options, appender, javaType, jdbcType ); - } - } - - private static void convertedBasicValueToString( - Object value, - WrapperOptions options, - JsonAppender appender, - JavaType javaType, - JdbcType jdbcType) { - switch ( jdbcType.getDefaultSqlTypeCode() ) { - case SqlTypes.TINYINT: - case SqlTypes.SMALLINT: - case SqlTypes.INTEGER: - if ( value instanceof Boolean booleanValue ) { - // BooleanJavaType has this as an implicit conversion - appender.append( booleanValue ? '1' : '0' ); - break; - } - if ( value instanceof Enum enumValue ) { - appender.appendSql( enumValue.ordinal() ); - break; - } - case SqlTypes.BOOLEAN: - case SqlTypes.BIT: - case SqlTypes.BIGINT: - case SqlTypes.FLOAT: - case SqlTypes.REAL: - case SqlTypes.DOUBLE: - // These types fit into the native representation of JSON, so let's use that - javaType.appendEncodedString( appender, value ); - break; - case SqlTypes.CHAR: - case SqlTypes.NCHAR: - case SqlTypes.VARCHAR: - case SqlTypes.NVARCHAR: - if ( value instanceof Boolean booleanValue ) { - // BooleanJavaType has this as an implicit conversion - appender.append( '"' ); - appender.append( booleanValue ? 'Y' : 'N' ); - appender.append( '"' ); - break; - } - case SqlTypes.LONGVARCHAR: - case SqlTypes.LONGNVARCHAR: - case SqlTypes.LONG32VARCHAR: - case SqlTypes.LONG32NVARCHAR: - case SqlTypes.CLOB: - case SqlTypes.MATERIALIZED_CLOB: - case SqlTypes.NCLOB: - case SqlTypes.MATERIALIZED_NCLOB: - case SqlTypes.ENUM: - case SqlTypes.NAMED_ENUM: - // These literals can contain the '"' character, so we need to escape it - appender.append( '"' ); - appender.startEscaping(); - javaType.appendEncodedString( appender, value ); - appender.endEscaping(); - appender.append( '"' ); - break; - case SqlTypes.DATE: - appender.append( '"' ); - JdbcDateJavaType.INSTANCE.appendEncodedString( - appender, - javaType.unwrap( value, java.sql.Date.class, options ) - ); - appender.append( '"' ); - break; - case SqlTypes.TIME: - case SqlTypes.TIME_WITH_TIMEZONE: - case SqlTypes.TIME_UTC: - appender.append( '"' ); - JdbcTimeJavaType.INSTANCE.appendEncodedString( - appender, - javaType.unwrap( value, java.sql.Time.class, options ) - ); - appender.append( '"' ); - break; - case SqlTypes.TIMESTAMP: - appender.append( '"' ); - JdbcTimestampJavaType.INSTANCE.appendEncodedString( - appender, - javaType.unwrap( value, java.sql.Timestamp.class, options ) - ); - appender.append( '"' ); - break; - case SqlTypes.TIMESTAMP_WITH_TIMEZONE: - case SqlTypes.TIMESTAMP_UTC: - appender.append( '"' ); - DateTimeFormatter.ISO_OFFSET_DATE_TIME.formatTo( - javaType.unwrap( value, OffsetDateTime.class, options ), - appender - ); - appender.append( '"' ); - break; - case SqlTypes.DECIMAL: - case SqlTypes.NUMERIC: - case SqlTypes.DURATION: - case SqlTypes.UUID: - // These types need to be serialized as JSON string, but don't have a need for escaping - appender.append( '"' ); - javaType.appendEncodedString( appender, value ); - appender.append( '"' ); - break; - case SqlTypes.BINARY: - case SqlTypes.VARBINARY: - case SqlTypes.LONGVARBINARY: - case SqlTypes.LONG32VARBINARY: - case SqlTypes.BLOB: - case SqlTypes.MATERIALIZED_BLOB: - // These types need to be serialized as JSON string, and for efficiency uses appendString directly - appender.append( '"' ); - appender.write( javaType.unwrap( value, byte[].class, options ) ); - appender.append( '"' ); - break; - case SqlTypes.ARRAY: - case SqlTypes.JSON_ARRAY: - final int length = Array.getLength( value ); - appender.append( '[' ); - if ( length != 0 ) { - //noinspection unchecked - final JavaType elementJavaType = ( (BasicPluralJavaType) javaType ).getElementJavaType(); - final JdbcType elementJdbcType = ( (ArrayJdbcType) jdbcType ).getElementJdbcType(); - Object arrayElement = Array.get( value, 0 ); - convertedValueToString( elementJavaType, elementJdbcType, arrayElement, options, appender ); - for ( int i = 1; i < length; i++ ) { - arrayElement = Array.get( value, i ); - appender.append( ',' ); - convertedValueToString( elementJavaType, elementJdbcType, arrayElement, options, appender ); - } - } - appender.append( ']' ); - break; - default: - throw new UnsupportedOperationException( "Unsupported JdbcType nested in JSON: " + jdbcType ); - } - } - - public static X fromString( - EmbeddableMappingType embeddableMappingType, - String string, - boolean returnEmbeddable, - WrapperOptions options) throws SQLException { - if ( string == null ) { - return null; - } - - final int jdbcValueCount = embeddableMappingType.getJdbcValueCount(); - final Object[] values = new Object[jdbcValueCount + ( embeddableMappingType.isPolymorphic() ? 1 : 0 )]; - final int end = fromString( embeddableMappingType, string, 0, string.length(), values, returnEmbeddable, options ); - assert string.substring( end ).isBlank(); - if ( returnEmbeddable ) { - final StructAttributeValues attributeValues = StructHelper.getAttributeValues( - embeddableMappingType, - values, - options - ); - //noinspection unchecked - return (X) instantiate( embeddableMappingType, attributeValues ); - } - //noinspection unchecked - return (X) values; - } - - // This is also used by Hibernate Reactive - public static X arrayFromString( - JavaType javaType, - JdbcType elementJdbcType, - String string, - WrapperOptions options) throws SQLException { - if ( string == null ) { - return null; - } - final JavaType elementJavaType = ((BasicPluralJavaType) javaType).getElementJavaType(); - final Class preferredJavaTypeClass = elementJdbcType.getPreferredJavaTypeClass( options ); - final JavaType jdbcJavaType; - if ( preferredJavaTypeClass == null || preferredJavaTypeClass == elementJavaType.getJavaTypeClass() ) { - jdbcJavaType = elementJavaType; - } - else { - jdbcJavaType = options.getTypeConfiguration().getJavaTypeRegistry().resolveDescriptor( preferredJavaTypeClass ); - } - final CustomArrayList arrayList = new CustomArrayList(); - final int i = fromArrayString( - string, - false, - options, - 0, - arrayList, - elementJavaType, - jdbcJavaType, - elementJdbcType - ); - assert string.charAt( i - 1 ) == ']'; - return javaType.wrap( arrayList, options ); - } - - private static int fromString( - EmbeddableMappingType embeddableMappingType, - String string, - int begin, - int end, - Object[] values, - boolean returnEmbeddable, - WrapperOptions options) throws SQLException { - boolean hasEscape = false; - assert string.charAt( begin ) == '{'; - int start = begin + 1; - State s = State.KEY_START; - int selectableIndex = -1; - // The following parsing logic assumes JSON is well-formed, - // but for the sake of the Java compiler's flow analysis - // and hopefully also for a better understanding, contains throws for some syntax errors - for ( int i = start; i < string.length(); i++ ) { - final char c = string.charAt( i ); - switch ( c ) { - case '\\': - assert s == State.KEY_QUOTE || s == State.VALUE_QUOTE; - hasEscape = true; - i++; - break; - case '"': - switch ( s ) { - case KEY_START: - s = State.KEY_QUOTE; - selectableIndex = -1; - start = i + 1; - hasEscape = false; - break; - case KEY_QUOTE: - s = State.KEY_END; - selectableIndex = getSelectableMapping( - embeddableMappingType, - string, - start, - i, - hasEscape - ); - start = -1; - hasEscape = false; - break; - case VALUE_START: - s = State.VALUE_QUOTE; - start = i + 1; - hasEscape = false; - break; - case VALUE_QUOTE: - s = State.VALUE_END; - values[selectableIndex] = fromString( - embeddableMappingType.getJdbcValueSelectable( selectableIndex ).getJdbcMapping(), - string, - start, - i, - hasEscape, - returnEmbeddable, - options - ); - selectableIndex = -1; - start = -1; - hasEscape = false; - break; - default: - throw syntaxError( string, s, i ); - } - break; - case ':': - switch ( s ) { - case KEY_QUOTE: - // I guess it's ok to have a ':' in the key.. - case VALUE_QUOTE: - // In the value it's fine - break; - case KEY_END: - s = State.VALUE_START; - break; - default: - throw syntaxError( string, s, i ); - } - break; - case ',': - switch ( s ) { - case KEY_QUOTE: - // I guess it's ok to have a ',' in the key.. - case VALUE_QUOTE: - // In the value it's fine - break; - case VALUE_END: - s = State.KEY_START; - break; - default: - throw syntaxError( string, s, i ); - } - break; - case '{': - switch ( s ) { - case KEY_QUOTE: - // I guess it's ok to have a '{' in the key.. - case VALUE_QUOTE: - // In the value it's fine - break; - case VALUE_START: - final SelectableMapping selectable = embeddableMappingType.getJdbcValueSelectable( - selectableIndex - ); - if ( !( selectable.getJdbcMapping().getJdbcType() - instanceof AggregateJdbcType aggregateJdbcType) ) { - throw new IllegalArgumentException( - String.format( - "JSON starts sub-object for a non-aggregate type at index %d. Selectable [%s] is of type [%s]", - i, - selectable.getSelectableName(), - selectable.getJdbcMapping().getJdbcType().getClass().getName() - ) - ); - } - final EmbeddableMappingType subMappingType = aggregateJdbcType.getEmbeddableMappingType(); - // This encoding is only possible if the JDBC type is JSON again - assert aggregateJdbcType.getJdbcTypeCode() == SqlTypes.JSON - || aggregateJdbcType.getDefaultSqlTypeCode() == SqlTypes.JSON; - final Object[] subValues = new Object[subMappingType.getJdbcValueCount()]; - i = fromString( subMappingType, string, i, end, subValues, returnEmbeddable, options ) - 1; - assert string.charAt( i ) == '}'; - if ( returnEmbeddable ) { - final StructAttributeValues attributeValues = StructHelper.getAttributeValues( - subMappingType, - subValues, - options - ); - values[selectableIndex] = instantiate( embeddableMappingType, attributeValues ); - } - else { - values[selectableIndex] = subValues; - } - s = State.VALUE_END; - selectableIndex = -1; - break; - default: - throw syntaxError( string, s, i ); - } - break; - case '[': - switch ( s ) { - case KEY_QUOTE: - // I guess it's ok to have a '[' in the key.. - case VALUE_QUOTE: - // In the value it's fine - break; - case VALUE_START: - final SelectableMapping selectable = embeddableMappingType.getJdbcValueSelectable( - selectableIndex - ); - final JdbcMapping jdbcMapping = selectable.getJdbcMapping(); - if ( !(jdbcMapping instanceof BasicPluralType pluralType) ) { - throw new IllegalArgumentException( - String.format( - "JSON starts array for a non-plural type at index %d. Selectable [%s] is of type [%s]", - i, - selectable.getSelectableName(), - jdbcMapping.getJdbcType().getClass().getName() - ) - ); - } - final BasicType elementType = pluralType.getElementType(); - final CustomArrayList arrayList = new CustomArrayList(); - i = fromArrayString( string, returnEmbeddable, options, i, arrayList, elementType ) - 1; - assert string.charAt( i ) == ']'; - values[selectableIndex] = pluralType.getJdbcJavaType().wrap( arrayList, options ); - s = State.VALUE_END; - selectableIndex = -1; - break; - default: - throw syntaxError( string, s, i ); - } - break; - case '}': - switch ( s ) { - case KEY_QUOTE: - // I guess it's ok to have a '}' in the key.. - case VALUE_QUOTE: - // In the value it's fine - break; - case VALUE_END: - // At this point, we are done - return i + 1; - default: - throw syntaxError( string, s, i ); - } - break; - default: - switch ( s ) { - case KEY_QUOTE: - case VALUE_QUOTE: - // In keys and values, all chars are fine - break; - case VALUE_START: - // Skip whitespace - if ( Character.isWhitespace( c ) ) { - break; - } - // Here we also allow certain literals - final int endIdx = consumeLiteral( - string, - i, - values, - embeddableMappingType.getJdbcValueSelectable( selectableIndex ).getJdbcMapping(), - selectableIndex, - returnEmbeddable, - options - ); - if ( endIdx != -1 ) { - i = endIdx; - s = State.VALUE_END; - selectableIndex = -1; - start = -1; - break; - } - throw syntaxError( string, s, i ); - case KEY_START: - case KEY_END: - case VALUE_END: - // Only whitespace is allowed here - if ( Character.isWhitespace( c ) ) { - break; - } - default: - throw syntaxError( string, s, i ); - } - break; - } - } - - throw new IllegalArgumentException( "JSON not properly formed: " + string.subSequence( start, end ) ); - } - - private static int fromArrayString( - String string, - boolean returnEmbeddable, - WrapperOptions options, - int begin, - CustomArrayList arrayList, - BasicType elementType) throws SQLException { - return fromArrayString( - string, - returnEmbeddable, - options, - begin, - arrayList, - elementType.getMappedJavaType(), - elementType.getJdbcJavaType(), - elementType.getJdbcType() - ); - } - - private static int fromArrayString( - String string, - boolean returnEmbeddable, - WrapperOptions options, - int begin, - CustomArrayList arrayList, - JavaType javaType, - JavaType jdbcJavaType, - JdbcType jdbcType) throws SQLException { - if ( string.length() == begin + 2 ) { - return begin + 2; - } - boolean hasEscape = false; - assert string.charAt( begin ) == '['; - int start = begin + 1; - State s = State.VALUE_START; - // The following parsing logic assumes JSON is well-formed, - // but for the sake of the Java compiler's flow analysis - // and hopefully also for a better understanding, contains throws for some syntax errors - for ( int i = start; i < string.length(); i++ ) { - final char c = string.charAt( i ); - switch ( c ) { - case '\\': - assert s == State.VALUE_QUOTE; - hasEscape = true; - i++; - break; - case '"': - switch ( s ) { - case VALUE_START: - s = State.VALUE_QUOTE; - start = i + 1; - hasEscape = false; - break; - case VALUE_QUOTE: - s = State.VALUE_END; - arrayList.add( - fromString( - javaType, - jdbcJavaType, - jdbcType, - string, - start, - i, - hasEscape, - returnEmbeddable, - options - ) - ); - start = -1; - hasEscape = false; - break; - default: - throw syntaxError( string, s, i ); - } - break; - case ',': - switch ( s ) { - case VALUE_QUOTE: - // In the value it's fine - break; - case VALUE_END: - s = State.VALUE_START; - break; - default: - throw syntaxError( string, s, i ); - } - break; - case '{': - switch ( s ) { - case VALUE_QUOTE: - // In the value it's fine - break; -// case VALUE_START: -// final SelectableMapping selectable = embeddableMappingType.getJdbcValueSelectable( -// selectableIndex -// ); -// if ( !( selectable.getJdbcMapping().getJdbcType() instanceof AggregateJdbcType ) ) { -// throw new IllegalArgumentException( -// String.format( -// "JSON starts sub-object for a non-aggregate type at index %d. Selectable [%s] is of type [%s]", -// i, -// selectable.getSelectableName(), -// selectable.getJdbcMapping().getJdbcType().getClass().getName() -// ) -// ); -// } -// final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) selectable.getJdbcMapping().getJdbcType(); -// final EmbeddableMappingType subMappingType = aggregateJdbcType.getEmbeddableMappingType(); -// // This encoding is only possible if the JDBC type is JSON again -// assert aggregateJdbcType.getJdbcTypeCode() == SqlTypes.JSON -// || aggregateJdbcType.getDefaultSqlTypeCode() == SqlTypes.JSON; -// final Object[] subValues = new Object[subMappingType.getJdbcValueCount()]; -// i = fromString( subMappingType, string, i, end, subValues, returnEmbeddable, options ) - 1; -// assert string.charAt( i ) == '}'; -// if ( returnEmbeddable ) { -// final Object[] attributeValues = StructHelper.getAttributeValues( -// subMappingType, -// subValues, -// options -// ); -// values[selectableIndex] = embeddableMappingType.getRepresentationStrategy() -// .getInstantiator() -// .instantiate( -// () -> attributeValues, -// options.getSessionFactory() -// ); -// } -// else { -// values[selectableIndex] = subValues; -// } -// s = State.VALUE_END; -// selectableIndex = -1; -// break; - default: - throw syntaxError( string, s, i ); - } - break; - case ']': - switch ( s ) { - case VALUE_QUOTE: - // In the value it's fine - break; - case VALUE_END: - // At this point, we are done - return i + 1; - default: - throw syntaxError( string, s, i ); - } - break; - default: - switch ( s ) { - case VALUE_QUOTE: - // In keys and values, all chars are fine - break; - case VALUE_START: - // Skip whitespace - if ( Character.isWhitespace( c ) ) { - break; - } - final int elementIndex = arrayList.size(); - arrayList.add( null ); - // Here we also allow certain literals - final int endIdx = consumeLiteral( - string, - i, - arrayList.getUnderlyingArray(), - javaType, - jdbcJavaType, - jdbcType, - elementIndex, - returnEmbeddable, - options - ); - if ( endIdx != -1 ) { - i = endIdx; - s = State.VALUE_END; - start = -1; - break; - } - throw syntaxError( string, s, i ); - case VALUE_END: - // Only whitespace is allowed here - if ( Character.isWhitespace( c ) ) { - break; - } - default: - throw syntaxError( string, s, i ); - } - break; - } - } - - throw new IllegalArgumentException( "JSON not properly formed: " + string.subSequence( start, string.length() ) ); - } - - private static int consumeLiteral( - String string, - int start, - Object[] values, - JdbcMapping jdbcMapping, - int selectableIndex, - boolean returnEmbeddable, - WrapperOptions options) throws SQLException { - return consumeLiteral( - string, - start, - values, - jdbcMapping.getMappedJavaType(), - jdbcMapping.getJdbcJavaType(), - jdbcMapping.getJdbcType(), - selectableIndex, - returnEmbeddable, - options - ); - } - - private static int consumeLiteral( - String string, - int start, - Object[] values, - JavaType javaType, - JavaType jdbcJavaType, - JdbcType jdbcType, - int selectableIndex, - boolean returnEmbeddable, - WrapperOptions options) throws SQLException { - final char c = string.charAt( start ); - switch ( c ) { - case 'n': - // only null is possible - values[selectableIndex] = null; - return consume(string, start, "null"); - case 'f': - // only false is possible - values[selectableIndex] = false; - return consume(string, start, "false"); - case 't': - // only false is possible - values[selectableIndex] = true; - return consume(string, start, "true"); - case '0': - switch ( string.charAt( start + 1 ) ) { - case '.': - return consumeFractional( - string, - start, - start + 1, - values, - javaType, - jdbcJavaType, - jdbcType, - selectableIndex, - returnEmbeddable, - options - ); - case 'E': - case 'e': - return consumeExponential( - string, - start, - start + 1, - values, - javaType, - jdbcJavaType, - jdbcType, - selectableIndex, - returnEmbeddable, - options - ); - } - values[selectableIndex] = fromString( - javaType, - jdbcJavaType, - jdbcType, - string, - start, - start + 1, - returnEmbeddable, - options - ); - return start; - case '-': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - // number = [ minus ] int [ frac ] [ exp ] - // decimal-point = %x2E ; . - // digit1-9 = %x31-39 ; 1-9 - // e = %x65 / %x45 ; e E - // exp = e [ minus / plus ] 1*DIGIT - // frac = decimal-point 1*DIGIT - // int = zero / ( digit1-9 *DIGIT ) - // minus = %x2D ; - - // plus = %x2B ; + - // zero = %x30 ; 0 - for (int i = start + 1; i < string.length(); i++) { - final char digit = string.charAt( i ); - switch ( digit ) { - case '.': - return consumeFractional( - string, - start, - i, - values, - javaType, - jdbcJavaType, - jdbcType, - selectableIndex, - returnEmbeddable, - options - ); - case 'E': - case 'e': - return consumeExponential( - string, - start, - i, - values, - javaType, - jdbcJavaType, - jdbcType, - selectableIndex, - returnEmbeddable, - options - ); - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - break; - default: - values[selectableIndex] = fromString( - javaType, - jdbcJavaType, - jdbcType, - string, - start, - i, - returnEmbeddable, - options - ); - return i - 1; - } - } - } - - return -1; - } - - private static int consumeFractional( - String string, - int start, - int dotIndex, - Object[] values, - JavaType javaType, - JavaType jdbcJavaType, - JdbcType jdbcType, - int selectableIndex, - boolean returnEmbeddable, - WrapperOptions options) throws SQLException { - for (int i = dotIndex + 1; i < string.length(); i++) { - final char digit = string.charAt( i ); - switch ( digit ) { - case 'E': - case 'e': - return consumeExponential( - string, - start, - i, - values, - javaType, - jdbcJavaType, - jdbcType, - selectableIndex, - returnEmbeddable, - options - ); - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - break; - default: - values[selectableIndex] = fromString( - javaType, - jdbcJavaType, - jdbcType, - string, - start, - i, - returnEmbeddable, - options - ); - return i - 1; - } - } - return start; - } - - private static int consumeExponential( - String string, - int start, - int eIndex, - Object[] values, - JavaType javaType, - JavaType jdbcJavaType, - JdbcType jdbcType, - int selectableIndex, - boolean returnEmbeddable, - WrapperOptions options) throws SQLException { - int i = eIndex + 1; - switch ( string.charAt( i ) ) { - case '-': - case '+': - i++; - break; - } - for (; i < string.length(); i++) { - final char digit = string.charAt( i ); - switch ( digit ) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - break; - default: - values[selectableIndex] = fromString( - javaType, - jdbcJavaType, - jdbcType, - string, - start, - i, - returnEmbeddable, - options - ); - return i - 1; - } - } - return start; - } - - private static int consume(String string, int start, String text) { - if ( !string.regionMatches( start + 1, text, 1, text.length() - 1 ) ) { - throw new IllegalArgumentException( - String.format( - "Syntax error at position %d. Unexpected char [%s]. Expecting [%s]", - start + 1, - string.charAt( start + 1 ), - text - ) - ); - } - return start + text.length() - 1; - } - - private static IllegalArgumentException syntaxError(String string, State s, int charIndex) { - return new IllegalArgumentException( - String.format( - "Syntax error at position %d. Unexpected char [%s]. Expecting one of [%s]", - charIndex, - string.charAt( charIndex ), - s.expectedChars() - ) - ); - } - - private static int getSelectableMapping( - EmbeddableMappingType embeddableMappingType, - String string, - int start, - int end, - boolean hasEscape) { - final String name = hasEscape - ? unescape( string, start, end ) - : string.substring( start, end ); - final int selectableIndex = embeddableMappingType.getSelectableIndex( name ); - if ( selectableIndex == -1 ) { - throw new IllegalArgumentException( - String.format( - "Could not find selectable [%s] in embeddable type [%s] for JSON processing.", - name, - embeddableMappingType.getMappedJavaType().getJavaTypeClass().getName() - ) - ); - } - return selectableIndex; - } - - private static Object fromString( - JdbcMapping jdbcMapping, - String string, - int start, - int end, - boolean hasEscape, - boolean returnEmbeddable, - WrapperOptions options) throws SQLException { - return fromString( - jdbcMapping.getMappedJavaType(), - jdbcMapping.getJdbcJavaType(), - jdbcMapping.getJdbcType(), - string, - start, - end, - hasEscape, - returnEmbeddable, - options - ); - } - - private static Object fromString( - JavaType javaType, - JavaType jdbcJavaType, - JdbcType jdbcType, - String string, - int start, - int end, - boolean hasEscape, - boolean returnEmbeddable, - WrapperOptions options) throws SQLException { - if ( hasEscape ) { - final String unescaped = unescape( string, start, end ); - return fromString( - javaType, - jdbcJavaType, - jdbcType, - unescaped, - 0, - unescaped.length(), - returnEmbeddable, - options - ); - } - return fromString( - javaType, - jdbcJavaType, - jdbcType, - string, - start, - end, - returnEmbeddable, - options - ); - } - - private static Object fromString( - JavaType javaType, - JavaType jdbcJavaType, - JdbcType jdbcType, - String string, - int start, - int end, - boolean returnEmbeddable, - WrapperOptions options) throws SQLException { - switch ( jdbcType.getDefaultSqlTypeCode() ) { - case SqlTypes.BINARY: - case SqlTypes.VARBINARY: - case SqlTypes.LONGVARBINARY: - case SqlTypes.LONG32VARBINARY: - return jdbcJavaType.wrap( - PrimitiveByteArrayJavaType.INSTANCE.fromEncodedString( - string, - start, - end - ), - options - ); - case SqlTypes.UUID: - return jdbcJavaType.wrap( - PrimitiveByteArrayJavaType.INSTANCE.fromString( - string.substring( start, end ).replace( "-", "" ) - ), - options - ); - case SqlTypes.DATE: - return jdbcJavaType.wrap( - JdbcDateJavaType.INSTANCE.fromEncodedString( - string, - start, - end - ), - options - ); - case SqlTypes.TIME: - case SqlTypes.TIME_WITH_TIMEZONE: - case SqlTypes.TIME_UTC: - return jdbcJavaType.wrap( - JdbcTimeJavaType.INSTANCE.fromEncodedString( - string, - start, - end - ), - options - ); - case SqlTypes.TIMESTAMP: - return jdbcJavaType.wrap( - JdbcTimestampJavaType.INSTANCE.fromEncodedString( - string, - start, - end - ), - options - ); - case SqlTypes.TIMESTAMP_WITH_TIMEZONE: - case SqlTypes.TIMESTAMP_UTC: - return jdbcJavaType.wrap( - OffsetDateTimeJavaType.INSTANCE.fromEncodedString( - string, - start, - end - ), - options - ); - case SqlTypes.TINYINT: - case SqlTypes.SMALLINT: - case SqlTypes.INTEGER: - if ( jdbcJavaType.getJavaTypeClass() == Boolean.class ) { - return jdbcJavaType.wrap( Integer.parseInt( string, start, end, 10 ), options ); - } - else if ( jdbcJavaType instanceof EnumJavaType ) { - return jdbcJavaType.wrap( Integer.parseInt( string, start, end, 10 ), options ); - } - case SqlTypes.CHAR: - case SqlTypes.NCHAR: - case SqlTypes.VARCHAR: - case SqlTypes.NVARCHAR: - if ( jdbcJavaType.getJavaTypeClass() == Boolean.class && end == start + 1 ) { - return jdbcJavaType.wrap( string.charAt( start ), options ); - } - default: - if ( jdbcType instanceof AggregateJdbcType aggregateJdbcType ) { - final Object[] subValues = aggregateJdbcType.extractJdbcValues( - CharSequenceHelper.subSequence( - string, - start, - end - ), - options - ); - if ( returnEmbeddable ) { - final StructAttributeValues subAttributeValues = StructHelper.getAttributeValues( - aggregateJdbcType.getEmbeddableMappingType(), - subValues, - options - ); - return instantiate( aggregateJdbcType.getEmbeddableMappingType(), subAttributeValues ) ; - } - return subValues; - } - - return jdbcJavaType.fromEncodedString( string, start, end ); - } - } - - private static String unescape(String string, int start, int end) { - final StringBuilder sb = new StringBuilder( end - start ); - for ( int i = start; i < end; i++ ) { - final char c = string.charAt( i ); - if ( c == '\\' ) { - i++; - final char cNext = string.charAt( i ); - switch ( cNext ) { - case '\\': - case '"': - case '/': - sb.append( cNext ); - break; - case 'b': - sb.append( '\b' ); - break; - case 'f': - sb.append( '\f' ); - break; - case 'n': - sb.append( '\n' ); - break; - case 'r': - sb.append( '\r' ); - break; - case 't': - sb.append( '\t' ); - break; - case 'u': - sb.append( (char) Integer.parseInt( string, i + 1, i + 5, 16 ) ); - i += 4; - break; - } - continue; - } - sb.append( c ); - } - return sb.toString(); - } - - enum State { - KEY_START( "\"\\s" ), - KEY_QUOTE( "" ), - KEY_END( ":\\s" ), - VALUE_START( "\"\\s" ), - VALUE_QUOTE( "" ), - VALUE_END( ",}\\s" ); - - final String expectedChars; - - State(String expectedChars) { - this.expectedChars = expectedChars; - } - - String expectedChars() { - return expectedChars; - } - } - - public static class JsonAppender extends OutputStream implements SqlAppender { - - private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - - private final StringBuilder sb; - private final boolean expandProperties; - - private boolean escape; - private Map> circularityTracker; - - public JsonAppender(StringBuilder sb, boolean expandProperties) { - this.sb = sb; - this.expandProperties = expandProperties; - } - - public boolean expandProperties() { - return expandProperties; - } - - @Override - public void appendSql(String fragment) { - append( fragment ); - } - - @Override - public void appendSql(char fragment) { - append( fragment ); - } - - @Override - public void appendSql(int value) { - sb.append( value ); - } - - @Override - public void appendSql(long value) { - sb.append( value ); - } - - @Override - public void appendSql(boolean value) { - sb.append( value ); - } - - @Override - public String toString() { - return sb.toString(); - } - - public void startEscaping() { - assert !escape; - escape = true; - } - - public void endEscaping() { - assert escape; - escape = false; - } - - @Override - public JsonAppender append(char fragment) { - if ( escape ) { - appendEscaped( fragment ); - } - else { - sb.append( fragment ); - } - return this; - } - - @Override - public JsonAppender append(CharSequence csq) { - return append( csq, 0, csq.length() ); - } - - @Override - public JsonAppender append(CharSequence csq, int start, int end) { - if ( escape ) { - int len = end - start; - sb.ensureCapacity( sb.length() + len ); - for ( int i = start; i < end; i++ ) { - appendEscaped( csq.charAt( i ) ); - } - } - else { - sb.append( csq, start, end ); - } - return this; - } - - @Override - public void write(int v) { - final String hex = Integer.toHexString( v ); - sb.ensureCapacity( sb.length() + hex.length() + 1 ); - if ( ( hex.length() & 1 ) == 1 ) { - sb.append( '0' ); - } - sb.append( hex ); - } - - @Override - public void write(byte[] bytes) { - write(bytes, 0, bytes.length); - } - - @Override - public void write(byte[] bytes, int off, int len) { - 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] ); - sb.append( HEX_ARRAY[v & 0x0F] ); - } - } - - /** - * 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 - */ - 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 void appendEscaped(char fragment) { - switch ( fragment ) { - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - // 8 is '\b' - // 9 is '\t' - // 10 is '\n' - case 11: - // 12 is '\f' - // 13 is '\r' - case 14: - case 15: - case 16: - case 17: - case 18: - case 19: - case 20: - case 21: - case 22: - case 23: - case 24: - case 25: - case 26: - case 27: - case 28: - case 29: - case 30: - case 31: - sb.append( "\\u" ).append( Integer.toHexString( fragment ) ); - break; - case '\b': - sb.append("\\b"); - break; - case '\t': - sb.append("\\t"); - break; - case '\n': - sb.append("\\n"); - break; - case '\f': - sb.append("\\f"); - break; - case '\r': - sb.append("\\r"); - break; - case '"': - sb.append( "\\\"" ); - break; - case '\\': - sb.append( "\\\\" ); - break; - default: - sb.append( fragment ); - break; - } - } - } - - private static class CustomArrayList extends AbstractCollection implements Collection { - Object[] array = ArrayHelper.EMPTY_OBJECT_ARRAY; - int size; - - public void ensureCapacity(int minCapacity) { - int oldCapacity = array.length; - if ( minCapacity > oldCapacity ) { - int newCapacity = oldCapacity + ( oldCapacity >> 1 ); - newCapacity = Math.max( Math.max( newCapacity, minCapacity ), 10 ); - array = Arrays.copyOf( array, newCapacity ); - } - } - - public Object[] getUnderlyingArray() { - return array; - } - - @Override - public int size() { - return size; - } - - @Override - public boolean add(Object o) { - if ( size == array.length ) { - ensureCapacity( size + 1 ); - } - array[size++] = o; - return true; - } - - @Override - public boolean isEmpty() { - return size == 0; - } - - @Override - public boolean contains(Object o) { - for ( int i = 0; i < size; i++ ) { - if ( Objects.equals(o, array[i] ) ) { - return true; - } - } - return false; - } - - @Override - public Iterator iterator() { - return new Iterator<>() { - int index; - @Override - public boolean hasNext() { - return index != size; - } - - @Override - public Object next() { - if ( index == size ) { - throw new NoSuchElementException(); - } - return array[index++]; - } - }; - } - - @Override - public Object[] toArray() { - return Arrays.copyOf( array, size ); - } - - @Override - @AllowReflection // We need the ability to create arrays of requested types dynamically. - public T[] toArray(T[] a) { - //noinspection unchecked - final T[] r = a.length >= size - ? a - : (T[]) Array.newInstance( a.getClass().getComponentType(), size ); - for (int i = 0; i < size; i++) { - //noinspection unchecked - r[i] = (T) array[i]; - } - return null; - } - } - -} diff --git a/language/src/main/java/org/hibernate/tool/language/internal/MetamodelJsonSerializerImpl.java b/language/src/main/java/org/hibernate/tool/language/internal/MetamodelJsonSerializerImpl.java index bedee63ddf..0dec6021c1 100644 --- a/language/src/main/java/org/hibernate/tool/language/internal/MetamodelJsonSerializerImpl.java +++ b/language/src/main/java/org/hibernate/tool/language/internal/MetamodelJsonSerializerImpl.java @@ -19,6 +19,7 @@ import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.tool.language.spi.MetamodelSerializer; +import org.hibernate.type.format.StringJsonDocumentWriter; import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.EmbeddableType; @@ -29,7 +30,6 @@ import jakarta.persistence.metamodel.MappedSuperclassType; import jakarta.persistence.metamodel.Metamodel; import jakarta.persistence.metamodel.PluralAttribute; -import jakarta.persistence.metamodel.SingularAttribute; import jakarta.persistence.metamodel.Type; import java.util.ArrayList; import java.util.Collection; @@ -37,7 +37,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import org.hibernate.tool.language.internal.JsonHelper.JsonAppender; /** * Implementation of {@link MetamodelSerializer} that represents the {@link Metamodel} as a JSON array of mapped objects. @@ -57,9 +56,9 @@ public class MetamodelJsonSerializerImpl implements MetamodelSerializer { */ @Override public String toString(Metamodel metamodel) { - final List entities = new ArrayList<>(); - final List embeddables = new ArrayList<>(); - final List mappedSupers = new ArrayList<>(); + final List> entities = new ArrayList<>(); + final List> embeddables = new ArrayList<>(); + final List> mappedSupers = new ArrayList<>(); for ( ManagedType managedType : metamodel.getManagedTypes() ) { switch ( managedType.getPersistenceType() ) { case ENTITY -> entities.add( getEntityTypeDescription( (EntityType) managedType ) ); @@ -76,42 +75,46 @@ public String toString(Metamodel metamodel) { ) ); } - private static String toJson(Collection strings) { - return strings.isEmpty() ? "[]" : "[" + String.join( ",", strings ) + "]"; - } - private static String toJson(Map map) { if ( map.isEmpty() ) { return "{}"; } - final StringBuilder sb = new StringBuilder( "{" ); - final JsonAppender appender = new JsonAppender( sb, false ); - for ( final var entry : map.entrySet() ) { - appender.append( "\"" ).append( entry.getKey() ).append( "\":" ); - final Object value = entry.getValue(); - if ( value instanceof String strValue ) { - appender.append( "\"" ); - appender.startEscaping(); - appender.append( strValue ); - appender.endEscaping(); - appender.append( "\"" ); - } - else if ( value instanceof Collection collection ) { - //noinspection unchecked - appender.append( toJson( (Collection) collection ) ); - } - else if ( value instanceof Number || value instanceof Boolean ) { - appender.append( value.toString() ); - } - else if ( value == null ) { - appender.append( "null" ); + + final StringJsonDocumentWriter writer = new StringJsonDocumentWriter( new StringBuilder() ); + toJson( map, writer ); + return writer.toString(); + } + + private static void toJson(Object value, StringJsonDocumentWriter writer) { + if ( value instanceof String strValue ) { + writer.stringValue( strValue ); + } + else if ( value instanceof Boolean boolValue ) { + writer.booleanValue( boolValue ); + } else if ( value instanceof Number numValue ) { + writer.numericValue( numValue ); + } + else if ( value instanceof Map map ) { + writer.startObject(); + for ( final var entry : map.entrySet() ) { + writer.objectKey( entry.getKey().toString() ); + toJson( entry.getValue(), writer ); } - else { - throw new IllegalArgumentException( "Unsupported value type: " + value.getClass().getName() ); + writer.endObject(); + } + else if ( value instanceof Collection collection ) { + writer.startArray(); + for ( final var item : collection ) { + toJson( item, writer ); } - appender.append( "," ); + writer.endArray(); + } + else if ( value == null ) { + writer.nullValue(); + } + else { + throw new IllegalArgumentException( "Unsupported value type: " + value.getClass().getName() ); } - return sb.deleteCharAt( sb.length() - 1 ).append( '}' ).toString(); } private static void putIfNotNull(Map map, String key, Object value) { @@ -120,22 +123,22 @@ private static void putIfNotNull(Map map, String key, Object val } } - private static String getEntityTypeDescription(EntityType entityType) { + private static Map getEntityTypeDescription(EntityType entityType) { final Map map = new HashMap<>( 5 ); map.put( "name", entityType.getName() ); map.put( "class", entityType.getJavaType().getTypeName() ); putIfNotNull( map, "superType", superTypeDescriptor( (ManagedDomainType) entityType ) ); putIfNotNull( map, "identifierAttribute", identifierDescriptor( entityType ) ); map.put( "attributes", attributeArray( entityType.getAttributes() ) ); - return toJson( map ); + return map; } private static String superTypeDescriptor(ManagedDomainType managedType) { - final ManagedDomainType superType = managedType.getSuperType(); + final var superType = managedType.getSuperType(); return superType != null ? superType.getJavaType().getTypeName() : null; } - private static String getMappedSuperclassTypeDescription(MappedSuperclassType mappedSuperclass) { + private static Map getMappedSuperclassTypeDescription(MappedSuperclassType mappedSuperclass) { final Class javaType = mappedSuperclass.getJavaType(); final Map map = new HashMap<>( 5 ); map.put( "name", javaType.getSimpleName() ); @@ -143,13 +146,13 @@ private static String getMappedSuperclassTypeDescription(MappedSuperclassTyp putIfNotNull( map, "superType", superTypeDescriptor( (ManagedDomainType) mappedSuperclass ) ); putIfNotNull( map, "identifierAttribute", identifierDescriptor( mappedSuperclass ) ); map.put( "attributes", attributeArray( mappedSuperclass.getAttributes() ) ); - return toJson( map ); + return map; } private static String identifierDescriptor(IdentifiableType identifiableType) { final Type idType = identifiableType.getIdType(); if ( idType != null ) { - final SingularAttribute id = identifiableType.getId( idType.getJavaType() ); + final var id = identifiableType.getId( idType.getJavaType() ); return id.getName(); } else { @@ -157,36 +160,37 @@ private static String identifierDescriptor(IdentifiableType identifiableT } } - private static String getEmbeddableTypeDescription(EmbeddableType embeddableType) { + private static Map getEmbeddableTypeDescription(EmbeddableType embeddableType) { final Class javaType = embeddableType.getJavaType(); final Map map = new HashMap<>( 4 ); map.put( "name", javaType.getSimpleName() ); map.put( "class", javaType.getTypeName() ); putIfNotNull( map, "superType", superTypeDescriptor( (ManagedDomainType) embeddableType ) ); map.put( "attributes", attributeArray( embeddableType.getAttributes() ) ); - return toJson( map ); + return map; } - private static List attributeArray(Set> attributes) { + private static List> attributeArray(Set> attributes) { if ( attributes.isEmpty() ) { return List.of(); } - final ArrayList result = new ArrayList<>( attributes.size() ); - for ( final Attribute attribute : attributes ) { - String attributeDescription = "{\"name\":\"" + attribute.getName() + - "\",\"type\":\"" + attribute.getJavaType().getTypeName(); + return attributes.stream().map( attribute -> { + final String name = attribute.getName(); + String type = attribute.getJavaType().getTypeName(); // add key and element types for plural attributes if ( attribute instanceof PluralAttribute pluralAttribute ) { - attributeDescription += "<"; - final PluralAttribute.CollectionType collectionType = pluralAttribute.getCollectionType(); + type += "<"; + final var collectionType = pluralAttribute.getCollectionType(); if ( collectionType == PluralAttribute.CollectionType.MAP ) { - attributeDescription += ( (MapAttribute) pluralAttribute ).getKeyJavaType().getTypeName() + ","; + type += ( (MapAttribute) pluralAttribute ).getKeyJavaType().getTypeName() + ","; } - attributeDescription += pluralAttribute.getElementType().getJavaType().getTypeName() + ">"; + type += pluralAttribute.getElementType().getJavaType().getTypeName() + ">"; } - result.add( attributeDescription + "\"}" ); - } - return result; + return Map.of( + "type", type, + "name", name + ); + } ).toList(); } } diff --git a/language/src/main/java/org/hibernate/tool/language/internal/ResultsJsonSerializerImpl.java b/language/src/main/java/org/hibernate/tool/language/internal/ResultsJsonSerializerImpl.java index bb44a4992c..031aa311e5 100644 --- a/language/src/main/java/org/hibernate/tool/language/internal/ResultsJsonSerializerImpl.java +++ b/language/src/main/java/org/hibernate/tool/language/internal/ResultsJsonSerializerImpl.java @@ -33,12 +33,15 @@ import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.select.SqmJpaCompoundSelection; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.query.sqm.tree.select.SqmSelection; -import org.hibernate.tool.language.internal.JsonHelper.JsonAppender; import org.hibernate.tool.language.spi.ResultsSerializer; +import org.hibernate.type.descriptor.jdbc.spi.DescriptiveJsonGeneratingVisitor; +import org.hibernate.type.format.StringJsonDocumentWriter; import jakarta.persistence.Tuple; import jakarta.persistence.criteria.Selection; +import java.io.IOException; import java.util.List; import static org.hibernate.internal.util.NullnessUtil.castNonNull; @@ -47,6 +50,9 @@ * Utility class to serialize query results into a JSON string format. */ public class ResultsJsonSerializerImpl implements ResultsSerializer { + + private static final DescriptiveJsonGeneratingVisitor JSON_VISITOR = new DescriptiveJsonGeneratingVisitor(); + private final SessionFactoryImplementor factory; public ResultsJsonSerializerImpl(SessionFactoryImplementor factory) { @@ -54,24 +60,26 @@ public ResultsJsonSerializerImpl(SessionFactoryImplementor factory) { } @Override - public String toString(List values, SelectionQuery query) { + public String toString(List values, SelectionQuery query) throws IOException { if ( values.isEmpty() ) { return "[]"; } final StringBuilder sb = new StringBuilder(); - final JsonAppender jsonAppender = new JsonAppender( sb, true ); + final StringJsonDocumentWriter writer = new StringJsonDocumentWriter( sb ); char separator = '['; for ( final T value : values ) { sb.append( separator ); - renderValue( value, (SqmQuery) query, jsonAppender ); + //noinspection unchecked + renderValue( value, (SqmQuery) query, writer ); separator = ','; } sb.append( ']' ); return sb.toString(); } - private void renderValue(T value, SqmQuery query, JsonAppender jsonAppender) { + private void renderValue(T value, SqmQuery query, StringJsonDocumentWriter writer) + throws IOException { final SqmStatement sqm = query.getSqmStatement(); if ( !( sqm instanceof SqmSelectStatement sqmSelect ) ) { throw new IllegalArgumentException( "Query is not a select statement." ); @@ -79,78 +87,82 @@ private void renderValue(T value, SqmQuery query, JsonAppender jsonAppender) final List> selections = sqmSelect.getQuerySpec().getSelectClause().getSelections(); assert !selections.isEmpty(); if ( selections.size() == 1 ) { - renderValue( value, selections.get( 0 ).getSelectableNode(), jsonAppender ); + renderValue( value, selections.get( 0 ).getSelectableNode(), writer ); } else { // wrap each result tuple in square brackets - char separator = '['; + writer.startArray(); for ( int i = 0; i < selections.size(); i++ ) { - jsonAppender.append( separator ); final SqmSelection selection = selections.get( i ); if ( value instanceof Object[] array ) { - renderValue( array[i], selection.getSelectableNode(), jsonAppender ); + renderValue( array[i], selection.getSelectableNode(), writer ); } else if ( value instanceof Tuple tuple ) { - renderValue( tuple.get( i ), selection.getSelectableNode(), jsonAppender ); + renderValue( tuple.get( i ), selection.getSelectableNode(), writer ); } else { - // todo : might it be a compound selection ? - renderValue( value, selection.getSelectableNode(), jsonAppender ); + renderValue( value, selection.getSelectableNode(), writer ); } - separator = ','; } - jsonAppender.append( ']' ); + writer.endArray(); } } - private void renderValue(Object value, Selection selection, JsonAppender jsonAppender) { - if ( selection instanceof SqmRoot root ) { - final EntityPersister persister = factory.getMappingMetamodel() - .getEntityDescriptor( root.getEntityName() ); - JsonHelper.toString( - value, - persister.getEntityMappingType(), - factory.getWrapperOptions(), - jsonAppender - ); - } - else if ( selection instanceof SqmPath path ) { - // extract the attribute from the path - final ValuedModelPart subPart = getSubPart( path.getLhs(), path.getNavigablePath().getLocalName() ); - if ( subPart != null ) { - JsonHelper.toString( value, subPart, factory.getWrapperOptions(), jsonAppender, null ); - } - else { - jsonAppender.append( expressibleToString( path, value ) ); - } - } - else if ( selection instanceof SqmJpaCompoundSelection compoundSelection ) { - final List> compoundSelectionItems = compoundSelection.getCompoundSelectionItems(); - assert compoundSelectionItems.size() > 1; - char separator = '['; - for ( int j = 0; j < compoundSelectionItems.size(); j++ ) { - jsonAppender.append( separator ); - renderValue( getValue( value, j ), compoundSelectionItems.get( j ), jsonAppender ); - separator = ','; - } - jsonAppender.append( ']' ); - } - else if ( selection instanceof SqmExpressibleAccessor node ) { - jsonAppender.append( expressibleToString( node, value ) ); + private void renderValue(Object value, Selection selection, StringJsonDocumentWriter writer) throws IOException { + if ( selection instanceof SqmRoot root ) { + final EntityPersister persister = factory.getMappingMetamodel() + .getEntityDescriptor( root.getEntityName() ); + JSON_VISITOR.visit( persister.getEntityMappingType(), value, factory.getWrapperOptions(), writer ); + } + else if ( selection instanceof SqmPath path ) { + // extract the attribute from the path + final ValuedModelPart subPart = getSubPart( path.getLhs(), path.getNavigablePath().getLocalName() ); + if ( subPart != null ) { + JSON_VISITOR.visit( subPart.getMappedType(), value, factory.getWrapperOptions(), writer ); } else { - jsonAppender.append( "\"" ).append( value.toString() ).append( "\"" ); // best effort + expressibleToString( path, value, writer ); } + } + else if ( selection instanceof SqmJpaCompoundSelection compoundSelection ) { + final List> compoundSelectionItems = compoundSelection.getCompoundSelectionItems(); + assert compoundSelectionItems.size() > 1; + writer.startArray(); + for ( int j = 0; j < compoundSelectionItems.size(); j++ ) { + renderValue( getValue( value, j ), compoundSelectionItems.get( j ), writer ); + } + writer.endArray(); + } + else if ( selection instanceof SqmExpressibleAccessor node ) { + expressibleToString( node, value, writer ); + } + else { + writer.stringValue( String.valueOf( value ) ); + } } - private static String expressibleToString(SqmExpressibleAccessor node, Object value) { + private static void expressibleToString( + SqmExpressibleAccessor node, + Object value, + StringJsonDocumentWriter writer) { //noinspection unchecked final SqmExpressible expressible = (SqmExpressible) node.getExpressible(); final String result = expressible != null ? expressible.getExpressibleJavaType().toString( value ) : value.toString(); // best effort - // avoid quoting numbers as they can be represented in JSON - return value instanceof Number ? result : "\"" + result + "\""; + // avoid quoting numeric and boolean values as they can be represented in JSON + if ( value instanceof Boolean boolValue ) { + writer.booleanValue( boolValue ); + } + else if ( value instanceof Number numValue ) { + writer.numericValue( numValue ); + } + else if ( result == null ) { + writer.nullValue(); + } + else { + writer.stringValue( result ); + } } private static Object getValue(Object value, int index) { diff --git a/language/src/main/java/org/hibernate/tool/language/spi/ResultsSerializer.java b/language/src/main/java/org/hibernate/tool/language/spi/ResultsSerializer.java index 28663cfa1b..bc030e3386 100644 --- a/language/src/main/java/org/hibernate/tool/language/spi/ResultsSerializer.java +++ b/language/src/main/java/org/hibernate/tool/language/spi/ResultsSerializer.java @@ -19,6 +19,7 @@ import org.hibernate.query.SelectionQuery; +import java.io.IOException; import java.util.List; /** @@ -36,5 +37,5 @@ public interface ResultsSerializer { * * @return JSON string representation of the values */ - String toString(List values, SelectionQuery query); + String toString(List values, SelectionQuery query) throws IOException; } diff --git a/language/src/test/java/org/hibernate/tool/language/MetamodelJsonSerializerTest.java b/language/src/test/java/org/hibernate/tool/language/MetamodelJsonSerializerTest.java index 081e39ec21..2ff5ffb023 100644 --- a/language/src/test/java/org/hibernate/tool/language/MetamodelJsonSerializerTest.java +++ b/language/src/test/java/org/hibernate/tool/language/MetamodelJsonSerializerTest.java @@ -95,7 +95,6 @@ public void testSimpleDomainModel() { assertThat( addressNode ).isNotNull(); assertAttributes( Address.class, addressNode.get( "attributes" ), AccessType.FIELD ); - // Mapped superclasses final JsonNode superclasses = root.get( "mappedSuperclasses" ); @@ -118,7 +117,6 @@ public void testMappedSuperclasses() { .buildMetadata(); try (final SessionFactory sf = metadata.buildSessionFactory()) { try { - System.out.printf( "JSON: " + toJson( sf.getMetamodel() ) ); final JsonNode root = toJson( sf.getMetamodel() ); final JsonNode superclasses = root.get( "mappedSuperclasses" ); @@ -195,9 +193,6 @@ public void testStandardDomainModelInheritance() { private static JsonNode toJson(Metamodel metamodel) throws JsonProcessingException { final String result = MetamodelJsonSerializerImpl.INSTANCE.toString( metamodel ); - - System.out.println( "JSON: " + result ); - final JsonNode jsonNode; try { jsonNode = mapper.readTree( result ); @@ -266,10 +261,11 @@ static MemberInfo[] getPersistentMembers(Class clazz, AccessType accessType) .filter( method -> method.getName().startsWith( "get" ) || method.getName().startsWith( "is" ) ) .map( method -> { final String name = method.getName(); - final String fieldName = getJavaBeansFieldName( name.startsWith( "get" ) ? - name.substring( 3 ) : - name.substring( 2 ) ); - return new MemberInfo( fieldName, method.getReturnType() ); + // Convert "getFoo" or "isFoo" to "foo" + final String fieldName = name.startsWith( "get" ) ? + name.substring( 3 ) : + name.substring( 2 ); + return new MemberInfo( getJavaBeansFieldName( fieldName ), method.getReturnType() ); } ) .toArray( MemberInfo[]::new ); } diff --git a/language/src/test/java/org/hibernate/tool/language/ResultsJsonSerializerTest.java b/language/src/test/java/org/hibernate/tool/language/ResultsJsonSerializerTest.java index 097d7140ef..53c923d458 100644 --- a/language/src/test/java/org/hibernate/tool/language/ResultsJsonSerializerTest.java +++ b/language/src/test/java/org/hibernate/tool/language/ResultsJsonSerializerTest.java @@ -42,6 +42,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.Tuple; +import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Date; import java.util.List; import java.util.Map; @@ -143,6 +145,27 @@ public void testStringyFunction(SessionFactoryScope scope) { } ); } + @Test + public void testNullFunction(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final SelectionQuery q = query( + "select lower(address.street) from Company where id = 4", + String.class, + session + ); + + try { + final String result = toString( q.getResultList(), q, scope.getSessionFactory() ); + + final JsonNode jsonNode = getSingleValue( mapper.readTree( result ) ); + assertThat( jsonNode.isNull() ).isTrue(); + } + catch (JsonProcessingException e) { + fail( "Serialization failed with exception", e ); + } + } ); + } + @Test public void testCompany(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -341,8 +364,11 @@ public void testComplexInheritance(SessionFactoryScope scope) { assertThat( jsonNode.get( "id" ).intValue() ).isEqualTo( 1 ); final JsonNode family = jsonNode.get( "family" ); - assertThat( family.isObject() ).isTrue(); - assertThat( family.get( "sister" ).get( "description" ).textValue() ).isEqualTo( "Marco's sister" ); + assertThat( family.isArray() ).isTrue(); + final JsonNode mapNode = getSingleValue( family ); + assertThat( mapNode.isObject() ).isTrue(); + assertThat( mapNode.get( "key" ).textValue() ).isEqualTo( "sister" ); + assertThat( mapNode.get( "value" ).get( "description" ).textValue() ).isEqualTo( "Marco's sister" ); final JsonNode pets = jsonNode.get( "pets" ); assertThat( pets.isArray() ).isTrue(); @@ -402,7 +428,12 @@ static String toString( List values, SelectionQuery query, SessionFactoryImplementor sessionFactory) { - return new ResultsJsonSerializerImpl( sessionFactory ).toString( values, query ); + try { + return new ResultsJsonSerializerImpl( sessionFactory ).toString( values, query ); + } + catch (IOException e) { + throw new UncheckedIOException( "Error during result serialization", e ); + } } static JsonNode getSingleValue(JsonNode jsonNode) { From ba97af8624ec1b207f2d2dedc97f12b66ffe6689 Mon Sep 17 00:00:00 2001 From: Marco Belladelli Date: Tue, 5 Aug 2025 16:34:10 +0200 Subject: [PATCH 2/2] HBX-3075 Restore language module --- pom.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pom.xml b/pom.xml index c0b9de159d..46dca119eb 100644 --- a/pom.xml +++ b/pom.xml @@ -82,9 +82,7 @@ ant test utils -