HHH-16160 Fix some XML related issues that came up

This commit is contained in:
Christian Beikov 2024-11-07 16:52:25 +01:00
parent c5f5e10df4
commit c02eae1d89
22 changed files with 1290 additions and 241 deletions

View File

@ -12,6 +12,7 @@
<option name="WHILE_ON_NEW_LINE" value="true" />
<option name="CATCH_ON_NEW_LINE" value="true" />
<option name="FINALLY_ON_NEW_LINE" value="true" />
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="true" />
<option name="SPACE_WITHIN_METHOD_CALL_PARENTHESES" value="true" />
<option name="SPACE_WITHIN_IF_PARENTHESES" value="true" />
<option name="SPACE_WITHIN_WHILE_PARENTHESES" value="true" />

View File

@ -659,8 +659,6 @@ disable_userland_proxy() {
sudo service docker stop
echo "Updating /etc/docker/daemon.json..."
sudo bash -c "export docker_daemon_json='$docker_daemon_json'; echo \"\${docker_daemon_json/\}/,}\\\"userland-proxy\\\": false}\" > /etc/docker/daemon.json"
echo "New docker daemon config:"
cat /etc/docker/daemon.json
echo "Starting docker..."
sudo service docker start
echo "Service status:"

View File

@ -93,6 +93,7 @@
import static org.hibernate.cfg.AvailableSettings.JPA_COMPLIANCE;
import static org.hibernate.cfg.AvailableSettings.WRAPPER_ARRAY_HANDLING;
import static org.hibernate.cfg.MappingSettings.XML_FORMAT_MAPPER_LEGACY_FORMAT;
import static org.hibernate.engine.config.spi.StandardConverters.BOOLEAN;
import static org.hibernate.internal.util.StringHelper.nullIfEmpty;
@ -652,6 +653,7 @@ public static class MetadataBuildingOptionsImpl
private final String schemaCharset;
private final boolean xmlMappingEnabled;
private final boolean allowExtensionsInCdi;
private final boolean xmlFormatMapperLegacyFormat;
public MetadataBuildingOptionsImpl(StandardServiceRegistry serviceRegistry) {
this.serviceRegistry = serviceRegistry;
@ -670,6 +672,7 @@ public MetadataBuildingOptionsImpl(StandardServiceRegistry serviceRegistry) {
BOOLEAN,
true
);
xmlFormatMapperLegacyFormat = configService.getSetting( XML_FORMAT_MAPPER_LEGACY_FORMAT, BOOLEAN, false );
implicitDiscriminatorsForJoinedInheritanceSupported = configService.getSetting(
AvailableSettings.IMPLICIT_DISCRIMINATOR_COLUMNS_FOR_JOINED_SUBCLASS,
@ -954,6 +957,11 @@ public boolean isAllowExtensionsInCdi() {
return allowExtensionsInCdi;
}
@Override
public boolean isXmlFormatMapperLegacyFormatEnabled() {
return xmlFormatMapperLegacyFormat;
}
/**
* Yuck. This is needed because JPA lets users define "global building options"
* in {@code orm.xml} mappings. Forget that there are generally multiple

View File

@ -166,6 +166,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
private Object validatorFactoryReference;
private FormatMapper jsonFormatMapper;
private FormatMapper xmlFormatMapper;
private final boolean xmlFormatMapperLegacyFormatEnabled;
// SessionFactory behavior
private final boolean jpaBootstrap;
@ -323,7 +324,8 @@ public SessionFactoryOptionsBuilder(StandardServiceRegistry serviceRegistry, Boo
);
this.xmlFormatMapper = determineXmlFormatMapper(
configurationSettings.get( AvailableSettings.XML_FORMAT_MAPPER ),
strategySelector
strategySelector,
this.xmlFormatMapperLegacyFormatEnabled = context.getMetadataBuildingOptions().isXmlFormatMapperLegacyFormatEnabled()
);
this.sessionFactoryName = (String) configurationSettings.get( SESSION_FACTORY_NAME );
@ -866,13 +868,13 @@ private static FormatMapper determineJsonFormatMapper(Object setting, StrategySe
);
}
private static FormatMapper determineXmlFormatMapper(Object setting, StrategySelector strategySelector) {
private static FormatMapper determineXmlFormatMapper(Object setting, StrategySelector strategySelector, boolean legacyFormat) {
return strategySelector.resolveDefaultableStrategy(
FormatMapper.class,
setting,
(Callable<FormatMapper>) () -> {
final FormatMapper jacksonFormatMapper = getXMLJacksonFormatMapperOrNull();
return jacksonFormatMapper != null ? jacksonFormatMapper : new JaxbXmlFormatMapper();
final FormatMapper jacksonFormatMapper = getXMLJacksonFormatMapperOrNull( legacyFormat );
return jacksonFormatMapper != null ? jacksonFormatMapper : new JaxbXmlFormatMapper( legacyFormat );
}
);
}
@ -1332,6 +1334,11 @@ public FormatMapper getXmlFormatMapper() {
return xmlFormatMapper;
}
@Override
public boolean isXmlFormatMapperLegacyFormatEnabled() {
return xmlFormatMapperLegacyFormatEnabled;
}
@Override
public boolean isPassProcedureParameterNames() {
return passProcedureParameterNames;

View File

@ -182,4 +182,9 @@ public boolean isXmlMappingEnabled() {
public boolean isAllowExtensionsInCdi() {
return delegate.isAllowExtensionsInCdi();
}
@Override
public boolean isXmlFormatMapperLegacyFormatEnabled() {
return delegate.isXmlFormatMapperLegacyFormatEnabled();
}
}

View File

@ -523,6 +523,11 @@ public FormatMapper getXmlFormatMapper() {
return delegate.getXmlFormatMapper();
}
@Override
public boolean isXmlFormatMapperLegacyFormatEnabled() {
return delegate.isXmlFormatMapperLegacyFormatEnabled();
}
@Override
public boolean isPassProcedureParameterNames() {
return delegate.isPassProcedureParameterNames();

View File

@ -6,6 +6,7 @@
import java.util.List;
import org.hibernate.Incubating;
import org.hibernate.TimeZoneStorageStrategy;
import org.hibernate.boot.model.naming.ImplicitNamingStrategy;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
@ -144,6 +145,15 @@ default CollectionSemanticsResolver getPersistentCollectionRepresentationResolve
*/
boolean isMultiTenancyEnabled();
/**
* Whether to use the legacy format for serializing/deserializing XML data.
*
* @since 7.0
* @see org.hibernate.cfg.MappingSettings#XML_FORMAT_MAPPER_LEGACY_FORMAT
*/
@Incubating
boolean isXmlFormatMapperLegacyFormatEnabled();
/**
* @return the {@link TypeConfiguration} belonging to the {@link BootstrapContext}
*/

View File

@ -571,6 +571,15 @@ default boolean isCollectionsInDefaultFetchGroupEnabled() {
@Incubating
FormatMapper getXmlFormatMapper();
/**
* Whether to use the legacy format for serializing/deserializing XML data.
*
* @since 7.0
* @see org.hibernate.cfg.MappingSettings#XML_FORMAT_MAPPER_LEGACY_FORMAT
*/
@Incubating
boolean isXmlFormatMapperLegacyFormatEnabled();
/**
* The default tenant identifier java type to use, in case no explicit tenant identifier property is defined.
*

View File

@ -325,6 +325,17 @@ public interface MappingSettings {
@Incubating
String XML_FORMAT_MAPPER = "hibernate.type.xml_format_mapper";
/**
* Specifies whether to use the legacy provider specific and non-portable XML format for collections and byte arrays
* for XML serialization/deserialization.
* <p>
* {@code false} by default. This property only exists for backwards compatibility.
*
* @since 7.0
*/
@Incubating
String XML_FORMAT_MAPPER_LEGACY_FORMAT = "hibernate.type.xml_format_mapper.legacy_format";
/**
* Configurable control over how to handle {@code Byte[]} and {@code Character[]} types
* encountered in the application domain model. Allowable semantics are defined by

View File

@ -5,15 +5,16 @@
package org.hibernate.dialect;
import java.io.IOException;
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.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.Internal;
import org.hibernate.engine.spi.LazySessionWrapperOptions;
import org.hibernate.engine.spi.SessionFactoryImplementor;
@ -24,16 +25,22 @@
import org.hibernate.metamodel.mapping.ValuedModelPart;
import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping;
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.IntegerJavaType;
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.XmlArrayJdbcType;
import static java.lang.Character.isLetter;
import static java.lang.Character.isLetterOrDigit;
@ -52,37 +59,10 @@ public class XmlHelper {
public static final String ROOT_TAG = "e";
private static final String START_TAG = "<" + ROOT_TAG + ">";
private static final String END_TAG = "</" + ROOT_TAG + ">";
private static Object fromEscapedString(
JdbcMapping jdbcMapping,
String string,
int start,
int end) {
final String unescaped = unescape( string, start, end );
return fromString( jdbcMapping, unescaped, 0, unescaped.length() );
}
private static Object fromString(
JdbcMapping jdbcMapping,
String string,
int start,
int end) {
return jdbcMapping.getJdbcJavaType().fromEncodedString(
string,
start,
end
);
}
private static Object fromRawObject(
JdbcMapping jdbcMapping,
Object raw,
WrapperOptions options) {
return jdbcMapping.getJdbcJavaType().wrap(
raw,
options
);
}
private static final String NULL_TAG = "<" + ROOT_TAG + "/>";
private static final String COLLECTION_START_TAG = "<Collection>";
private static final String COLLECTION_END_TAG = "</Collection>";
private static final String EMPTY_COLLECTION_TAG = "<Collection/>";
private static String unescape(String string, int start, int end) {
final StringBuilder sb = new StringBuilder( end - start );
@ -118,6 +98,16 @@ private static String unescape(String string, int start, int end) {
i += 3;
}
break OUTER;
case 'q':
if ( i + 5 < end
&& string.charAt( i + 2 ) == 'u'
&& string.charAt( i + 3 ) == 'o'
&& string.charAt( i + 4 ) == 't'
&& string.charAt( i + 5 ) == ';' ) {
sb.append( '"' );
i += 5;
}
break OUTER;
}
}
throw new IllegalArgumentException( "Illegal XML content: " + string.substring( start, end ) );
@ -132,37 +122,60 @@ private static String unescape(String string, int start, int end) {
private static Object fromString(
EmbeddableMappingType embeddableMappingType,
String string,
boolean returnEmbeddable,
WrapperOptions options,
int selectableIndex,
int start,
int end) {
final JdbcMapping jdbcMapping = embeddableMappingType.getJdbcValueSelectable( selectableIndex ).getJdbcMapping();
switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) {
case SqlTypes.BOOLEAN:
case SqlTypes.BIT:
int end) throws SQLException {
final JdbcMapping jdbcMapping = embeddableMappingType.getJdbcValueSelectable( selectableIndex )
.getJdbcMapping();
return fromString(
jdbcMapping.getMappedJavaType(),
jdbcMapping.getJdbcJavaType(),
jdbcMapping.getJdbcType(),
string,
returnEmbeddable,
options,
start,
end
);
}
private static Object fromString(
JavaType<?> javaType,
JavaType<?> jdbcJavaType,
JdbcType jdbcType,
String string,
boolean returnEmbeddable,
WrapperOptions options,
int start,
int end) throws SQLException {
switch ( jdbcType.getDefaultSqlTypeCode() ) {
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.BOOLEAN:
case SqlTypes.BIT:
case SqlTypes.BIGINT:
case SqlTypes.FLOAT:
case SqlTypes.REAL:
case SqlTypes.DOUBLE:
case SqlTypes.DECIMAL:
case SqlTypes.NUMERIC:
Class<?> javaTypeClass = jdbcMapping.getMappedJavaType().getJavaTypeClass();
if ( javaTypeClass.isEnum() ) {
return javaTypeClass.getEnumConstants()
[IntegerJavaType.INSTANCE.fromEncodedString( string, start, end )];
}
return fromString(
jdbcMapping,
case SqlTypes.UUID:
return jdbcJavaType.fromEncodedString(
string,
start,
end
);
case SqlTypes.DATE:
return fromRawObject(
jdbcMapping,
return jdbcJavaType.wrap(
JdbcDateJavaType.INSTANCE.fromEncodedString(
string,
start,
@ -173,8 +186,7 @@ private static Object fromString(
case SqlTypes.TIME:
case SqlTypes.TIME_WITH_TIMEZONE:
case SqlTypes.TIME_UTC:
return fromRawObject(
jdbcMapping,
return jdbcJavaType.wrap(
JdbcTimeJavaType.INSTANCE.fromEncodedString(
string,
start,
@ -183,8 +195,7 @@ private static Object fromString(
options
);
case SqlTypes.TIMESTAMP:
return fromRawObject(
jdbcMapping,
return jdbcJavaType.wrap(
JdbcTimestampJavaType.INSTANCE.fromEncodedString(
string,
start,
@ -194,8 +205,7 @@ private static Object fromString(
);
case SqlTypes.TIMESTAMP_WITH_TIMEZONE:
case SqlTypes.TIMESTAMP_UTC:
return fromRawObject(
jdbcMapping,
return jdbcJavaType.wrap(
OffsetDateTimeJavaType.INSTANCE.fromEncodedString(
string,
start,
@ -207,19 +217,46 @@ private static Object fromString(
case SqlTypes.VARBINARY:
case SqlTypes.LONGVARBINARY:
case SqlTypes.LONG32VARBINARY:
case SqlTypes.UUID:
return fromRawObject(
jdbcMapping,
Base64.getDecoder().decode( string.substring( start, end ) ),
case SqlTypes.BLOB:
case SqlTypes.MATERIALIZED_BLOB:
return jdbcJavaType.wrap(
PrimitiveByteArrayJavaType.INSTANCE.fromEncodedString(
string,
start,
end
),
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:
return fromEscapedString(
jdbcMapping,
string,
start,
end
);
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
);
final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType();
return instantiate( embeddableMappingType, subAttributeValues, options.getSessionFactory() ) ;
}
return subValues;
}
final String unescaped = unescape( string, start, end );
return jdbcJavaType.fromEncodedString( unescaped, 0, unescaped.length() );
}
}
@ -254,6 +291,45 @@ public static <X> X fromString(
return (X) array;
}
public static <X> X arrayFromString(
JavaType<X> javaType,
XmlArrayJdbcType xmlArrayJdbcType,
String string,
WrapperOptions options) throws SQLException {
if ( string == null ) {
return null;
}
else if ( EMPTY_COLLECTION_TAG.equals( string ) ) {
return javaType.wrap( Collections.emptyList(), options );
}
else if ( !string.startsWith( COLLECTION_START_TAG ) || !string.endsWith( COLLECTION_END_TAG ) ) {
throw new IllegalArgumentException( "Illegal XML for array: " + string );
}
final JavaType<?> elementJavaType = ((BasicPluralJavaType<?>) javaType).getElementJavaType();
final Class<?> preferredJavaTypeClass = xmlArrayJdbcType.getElementJdbcType().getPreferredJavaTypeClass( options );
final JavaType<?> jdbcJavaType;
if ( preferredJavaTypeClass == null || preferredJavaTypeClass == elementJavaType.getJavaTypeClass() ) {
jdbcJavaType = elementJavaType;
}
else {
jdbcJavaType = options.getSessionFactory().getTypeConfiguration().getJavaTypeRegistry()
.resolveDescriptor( preferredJavaTypeClass );
}
final ArrayList<Object> arrayList = new ArrayList<>();
final int end = fromArrayString(
string,
false,
options,
COLLECTION_START_TAG.length(),
arrayList,
elementJavaType,
jdbcJavaType,
xmlArrayJdbcType.getElementJdbcType()
);
assert end + COLLECTION_END_TAG.length() == string.length();
return javaType.wrap( arrayList, options );
}
private static int fromString(
String string,
List<Object> values,
@ -365,6 +441,7 @@ private static int fromString(
values[selectableMapping] = fromString(
embeddableMappingType,
string,
returnEmbeddable,
options,
selectableMapping,
contentStart,
@ -377,63 +454,86 @@ private static int fromString(
final SelectableMapping selectable = embeddableMappingType.getJdbcValueSelectable(
selectableIndex
);
if ( !( selectable.getJdbcMapping().getJdbcType() instanceof AggregateJdbcType ) ) {
final JdbcType jdbcType = selectable.getJdbcMapping().getJdbcType();
if ( jdbcType instanceof AggregateJdbcType aggregateJdbcType ) {
final EmbeddableMappingType subMappingType = aggregateJdbcType.getEmbeddableMappingType();
final Object[] subValues;
final int end;
if ( aggregateJdbcType.getJdbcTypeCode() == SqlTypes.SQLXML || aggregateJdbcType.getDefaultSqlTypeCode() == SqlTypes.SQLXML ) {
// If we stay in XML land, we can recurse instead
subValues = new Object[subMappingType.getJdbcValueCount()];
end = fromString(
subMappingType,
string,
returnEmbeddable,
options,
subValues,
i
);
}
else {
// Determine the end of the XML element
while ( string.charAt( i ) != '<' ) {
i++;
}
end = i;
subValues = aggregateJdbcType.extractJdbcValues(
CharSequenceHelper.subSequence(
string,
start,
end
),
options
);
}
if ( returnEmbeddable ) {
final StructAttributeValues attributeValues = StructHelper.getAttributeValues(
subMappingType,
subValues,
options
);
values[selectableIndex] = instantiate( subMappingType, attributeValues,
options.getSessionFactory() );
}
else {
values[selectableIndex] = subValues;
}
// The end is the start angle bracket for the end tag
assert string.charAt( end ) == '<';
assert string.charAt( end + 1 ) == '/';
assert string.regionMatches( end + 2, tagName, 0, tagName.length() );
i = end;
}
else if ( selectable.getJdbcMapping() instanceof BasicPluralType<?,?> pluralType ) {
final BasicType<?> elementType = pluralType.getElementType();
final ArrayList<Object> arrayList = new ArrayList<>();
final int end = fromArrayString(
string,
returnEmbeddable,
options,
i,
arrayList,
elementType.getMappedJavaType(),
elementType.getJdbcJavaType(),
elementType.getJdbcType()
);
values[selectableIndex] = selectable.getJdbcMapping().getJdbcJavaType().wrap( arrayList, options );
// The end is the start angle bracket for the end tag
assert string.charAt( end ) == '<';
assert string.charAt( end + 1 ) == '/';
assert string.regionMatches( end + 2, tagName, 0, tagName.length() );
i = end;
}
else {
throw new IllegalArgumentException(
String.format(
"XML 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()
jdbcType.getClass().getName()
)
);
}
final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) selectable.getJdbcMapping().getJdbcType();
final EmbeddableMappingType subMappingType = aggregateJdbcType.getEmbeddableMappingType();
final Object[] subValues;
final int end;
if ( aggregateJdbcType.getJdbcTypeCode() == SqlTypes.SQLXML || aggregateJdbcType.getDefaultSqlTypeCode() == SqlTypes.SQLXML ) {
// If we stay in XML land, we can recurse instead
subValues = new Object[subMappingType.getJdbcValueCount()];
end = fromString(
subMappingType,
string,
returnEmbeddable,
options,
subValues,
i
);
}
else {
// Determine the end of the XML element
while ( string.charAt( i ) != '<' ) {
i++;
}
end = i;
subValues = aggregateJdbcType.extractJdbcValues(
CharSequenceHelper.subSequence(
string,
start,
end
),
options
);
}
if ( returnEmbeddable ) {
final StructAttributeValues attributeValues = StructHelper.getAttributeValues(
subMappingType,
subValues,
options
);
values[selectableIndex] = instantiate( subMappingType, attributeValues, options.getSessionFactory() );
}
else {
values[selectableIndex] = subValues;
}
// The end is the start angle bracket for the end tag
assert string.charAt( end ) == '<';
assert string.charAt( end + 1 ) == '/';
assert string.regionMatches( end + 2, tagName, 0, tagName.length() );
i = end;
}
// consume the whole closing tag
i += tagName.length() + 2;
@ -474,86 +574,240 @@ private static int fromString(
throw new IllegalArgumentException( "XML not properly formed: " + string.substring( start ) );
}
private static int fromArrayString(
String string,
boolean returnEmbeddable,
WrapperOptions options,
int start,
ArrayList<Object> arrayList,
JavaType<?> javaType,
JavaType<?> jdbcJavaType,
JdbcType jdbcType) throws SQLException {
int tagNameStart = -1;
int contentStart = -1;
for ( int i = start; i < string.length(); i++ ) {
final char c = string.charAt( i );
switch ( c ) {
case '<':
if ( tagNameStart == -1 ) {
if ( string.charAt( i + 1 ) == '/' ) {
// This is the parent closing tag, so we stop here
assert contentStart == -1;
return i;
}
// A start tag
tagNameStart = i + 1;
}
else {
if ( string.charAt( i + 1 ) == '/' ) {
// This is a closing tag
if ( !string.regionMatches( i + 2, ROOT_TAG + ">", 0, ROOT_TAG.length() + 1 ) ) {
throw new IllegalArgumentException( "XML not properly formed: " + string.substring( start ) );
}
arrayList.add( fromString(
javaType,
jdbcJavaType,
jdbcType,
string,
returnEmbeddable,
options,
contentStart,
i
) );
}
else {
// Nested tag
if ( jdbcType instanceof AggregateJdbcType aggregateJdbcType ) {
final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType();
final Object[] array = new Object[embeddableMappingType.getJdbcValueCount() + ( embeddableMappingType.isPolymorphic() ? 1 : 0 )];
final int end = fromString( embeddableMappingType, string, returnEmbeddable, options, array, contentStart );
if ( returnEmbeddable ) {
final StructAttributeValues attributeValues = StructHelper.getAttributeValues( embeddableMappingType, array, options );
arrayList.add( instantiate( embeddableMappingType, attributeValues, options.getSessionFactory() ) );
}
else {
arrayList.add( array );
}
i = end + 1;
}
else {
throw new IllegalArgumentException( "XML not properly formed: " + string.substring( start ) );
}
}
// consume the whole closing tag
i += ROOT_TAG.length() + 2;
tagNameStart = -1;
contentStart = -1;
}
break;
case '>':
if ( contentStart == -1 ) {
// The closing angle bracket of the start tag
assert tagNameStart != -1;
if ( !ROOT_TAG.equals( string.substring( tagNameStart, i ) ) ) {
throw new IllegalArgumentException( "XML not properly formed: " + string.substring( start ) );
}
contentStart = i + 1;
}
else {
// This must be a char in the content
}
break;
case '/':
if ( contentStart == -1 ) {
// A shorthand tag encodes null
// Also, skip the closing angle bracket
arrayList.add( null );
i++;
tagNameStart = -1;
assert string.charAt( i ) == '>';
}
else {
// This must be a char in the content
}
break;
}
}
throw new IllegalArgumentException( "XML not properly formed: " + string.substring( start ) );
}
public static String toString(
EmbeddableMappingType embeddableMappingType,
Object value,
WrapperOptions options) {
WrapperOptions options) throws SQLException {
final StringBuilder sb = new StringBuilder();
sb.append( START_TAG );
toString( embeddableMappingType, value, options, new XMLAppender( sb ) );
toString( embeddableMappingType, embeddableMappingType.getValues( value ), options, new XMLAppender( sb ) );
sb.append( END_TAG );
return sb.toString();
}
public static String arrayToString(EmbeddableMappingType elementMappingType, Object[] values, WrapperOptions options) {
if ( values.length == 0 ) {
return EMPTY_COLLECTION_TAG;
}
final StringBuilder sb = new StringBuilder();
final XMLAppender xmlAppender = new XMLAppender( sb );
sb.append( COLLECTION_START_TAG );
for ( Object value : values ) {
if ( value == null ) {
sb.append( NULL_TAG );
}
else {
sb.append( START_TAG );
toString( elementMappingType, elementMappingType.getValues( value ), options, xmlAppender );
sb.append( END_TAG );
}
}
sb.append( COLLECTION_END_TAG );
return sb.toString();
}
public static String arrayToString(
JavaType<?> elementJavaType,
JdbcType elementJdbcType,
Object[] values,
WrapperOptions options) {
if ( values.length == 0 ) {
return EMPTY_COLLECTION_TAG;
}
final StringBuilder sb = new StringBuilder();
final XMLAppender xmlAppender = new XMLAppender( sb );
sb.append( COLLECTION_START_TAG );
for ( Object value : values ) {
if ( value == null ) {
sb.append( NULL_TAG );
}
else {
sb.append( START_TAG );
//noinspection unchecked
convertedBasicValueToString( xmlAppender, value, options, (JavaType<Object>) elementJavaType, elementJdbcType );
sb.append( END_TAG );
}
}
sb.append( COLLECTION_END_TAG );
return sb.toString();
}
private static void toString(
EmbeddableMappingType embeddableMappingType,
Object value,
@Nullable Object[] attributeValues,
WrapperOptions options,
XMLAppender sb) {
final Object[] array = embeddableMappingType.getValues( value );
for ( int i = 0; i < array.length; i++ ) {
if ( array[i] == null ) {
continue;
}
// Always append all the nodes, even if the value is null.
// This is done in order to allow using xmlextract/xmlextractvalue
// which fail if a XPath expression does not resolve to a result
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 );
if ( attributeMapping instanceof SelectableMapping ) {
final SelectableMapping selectable = (SelectableMapping) attributeMapping;
if ( attributeMapping instanceof SelectableMapping selectable ) {
final String tagName = selectable.getSelectableName();
sb.append( '<' );
sb.append( tagName );
sb.append( '>' );
serializeValueTo( sb, selectable, array[i], options );
sb.append( '<' );
sb.append( '/' );
sb.append( tagName );
sb.append( '>' );
if ( attributeValue == null ) {
sb.append( "/>" );
}
else {
sb.append( '>' );
//noinspection unchecked
convertedBasicValueToString(
sb,
selectable.getJdbcMapping().convertToRelationalValue( attributeValue ),
options,
(JavaType<Object>) selectable.getJdbcMapping().getJdbcJavaType(),
selectable.getJdbcMapping().getJdbcType()
);
sb.append( "</" );
sb.append( tagName );
sb.append( '>' );
}
}
else if ( attributeMapping instanceof EmbeddedAttributeMapping ) {
final EmbeddableMappingType mappingType = (EmbeddableMappingType) attributeMapping.getMappedType();
final SelectableMapping aggregateMapping = mappingType.getAggregateMapping();
if ( aggregateMapping == null ) {
toString(
mappingType,
array[i],
options,
sb
);
}
else {
final String tagName = aggregateMapping.getSelectableName();
final String tagName = aggregateMapping == null ? null : aggregateMapping.getSelectableName();
if ( tagName != null ) {
sb.append( '<' );
sb.append( tagName );
sb.append( '>' );
toString(
mappingType,
array[i],
options,
sb
);
sb.append( '<' );
sb.append( '/' );
}
toString(
mappingType,
attributeValue == null ? null : mappingType.getValues( attributeValue ),
options,
sb
);
if ( tagName != null ) {
sb.append( "</" );
sb.append( tagName );
sb.append( '>' );
}
}
else {
throw new UnsupportedOperationException( "Unsupported attribute mapping: " + attributeMapping );
throw new UnsupportedOperationException( "Support for attribute mapping type not yet implemented: " + attributeMapping.getClass().getName() );
}
}
}
private static void serializeValueTo(XMLAppender appender, SelectableMapping selectable, Object value, WrapperOptions options) {
final JdbcMapping jdbcMapping = selectable.getJdbcMapping();
//noinspection unchecked
final JavaType<Object> jdbcJavaType = (JavaType<Object>) jdbcMapping.getJdbcJavaType();
final Object relationalValue = jdbcMapping.convertToRelationalValue( value );
switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) {
private static void convertedBasicValueToString(
XMLAppender appender,
Object value,
WrapperOptions options,
JavaType<Object> jdbcJavaType,
JdbcType jdbcType) {
switch ( jdbcType.getDefaultSqlTypeCode() ) {
case SqlTypes.TINYINT:
case SqlTypes.SMALLINT:
case SqlTypes.INTEGER:
if ( relationalValue instanceof Boolean ) {
if ( value instanceof Boolean ) {
// BooleanJavaType has this as an implicit conversion
appender.append( (Boolean) relationalValue ? '1' : '0' );
appender.append( (Boolean) value ? '1' : '0' );
break;
}
if ( value instanceof Enum ) {
appender.appendSql( ((Enum<?>) value ).ordinal() );
break;
}
case SqlTypes.BOOLEAN:
@ -565,10 +819,11 @@ private static void serializeValueTo(XMLAppender appender, SelectableMapping sel
case SqlTypes.DECIMAL:
case SqlTypes.NUMERIC:
case SqlTypes.DURATION:
case SqlTypes.UUID:
jdbcJavaType.appendEncodedString(
appender,
jdbcJavaType.unwrap(
relationalValue,
value,
jdbcJavaType.getJavaTypeClass(),
options
)
@ -578,20 +833,26 @@ private static void serializeValueTo(XMLAppender appender, SelectableMapping sel
case SqlTypes.NCHAR:
case SqlTypes.VARCHAR:
case SqlTypes.NVARCHAR:
if ( relationalValue instanceof Boolean ) {
if ( value instanceof Boolean ) {
// BooleanJavaType has this as an implicit conversion
appender.append( (Boolean) relationalValue ? 'Y' : 'N' );
appender.append( (Boolean) value ? 'Y' : 'N' );
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:
appender.startEscaping();
jdbcJavaType.appendEncodedString(
appender,
jdbcJavaType.unwrap(
relationalValue,
value,
jdbcJavaType.getJavaTypeClass(),
options
)
@ -626,11 +887,49 @@ private static void serializeValueTo(XMLAppender appender, SelectableMapping sel
case SqlTypes.VARBINARY:
case SqlTypes.LONGVARBINARY:
case SqlTypes.LONG32VARBINARY:
case SqlTypes.UUID:
appender.writeBase64( jdbcJavaType.unwrap( relationalValue, byte[].class, options ) );
case SqlTypes.BLOB:
case SqlTypes.MATERIALIZED_BLOB:
appender.write( jdbcJavaType.unwrap( value, byte[].class, options ) );
break;
case SqlTypes.ARRAY:
case SqlTypes.XML_ARRAY:
final int length = Array.getLength( value );
if ( length != 0 ) {
//noinspection unchecked
final JavaType<Object> elementJavaType = ( (BasicPluralJavaType<Object>) jdbcJavaType ).getElementJavaType();
final JdbcType elementJdbcType = ( (ArrayJdbcType) jdbcType ).getElementJdbcType();
if ( elementJdbcType instanceof AggregateJdbcType aggregateJdbcType ) {
final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType();
for ( int i = 0; i < length; i++ ) {
final Object arrayElement = Array.get( value, i );
final Object[] arrayElementValues = arrayElement == null
? null
: embeddableMappingType.getValues( arrayElement );
appender.append( START_TAG );
toString( embeddableMappingType, arrayElementValues, options, appender );
appender.append( END_TAG );
}
}
else {
for ( int i = 0; i < length; i++ ) {
final Object arrayElement = Array.get( value, i );
if ( arrayElement == null ) {
appender.append( '<' );
appender.append( ROOT_TAG );
appender.append( "/>" );
}
else {
appender.append( START_TAG );
convertedBasicValueToString( appender, arrayElement, options, elementJavaType, elementJdbcType );
appender.append( END_TAG );
}
}
}
}
break;
default:
throw new UnsupportedOperationException( "Unsupported JdbcType nested in struct: " + jdbcMapping.getJdbcType() );
throw new UnsupportedOperationException( "Unsupported JdbcType nested in struct: " + jdbcType );
}
}
@ -641,7 +940,7 @@ private static int getSelectableMapping(
if ( selectableIndex == -1 ) {
throw new IllegalArgumentException(
String.format(
"Could not find selectable [%s] in embeddable type [%s] for JSON processing.",
"Could not find selectable [%s] in embeddable type [%s] for XML processing.",
name,
embeddableMappingType.getMappedJavaType().getJavaTypeClass().getName()
)
@ -677,7 +976,6 @@ private static class XMLAppender extends OutputStream implements SqlAppender {
private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
private final StringBuilder sb;
private boolean escape;
private OutputStream base64OutputStream;
public XMLAppender(StringBuilder sb) {
this.sb = sb;
@ -792,22 +1090,14 @@ public void write(byte[] bytes, int off, int len) {
sb.append( HEX_ARRAY[v & 0x0F] );
}
}
public void writeBase64(byte[] bytes) {
if ( base64OutputStream == null ) {
base64OutputStream = Base64.getEncoder().wrap( this );
}
try {
base64OutputStream.write( bytes );
}
catch (IOException e) {
// Should never happen
throw new RuntimeException( e );
}
}
}
private static final CollectionTags DEFAULT = new CollectionTags( "Collection", ROOT_TAG );
public static CollectionTags determineCollectionTags(BasicPluralJavaType<?> pluralJavaType, SessionFactoryImplementor sessionFactory) {
if ( !sessionFactory.getSessionFactoryOptions().isXmlFormatMapperLegacyFormatEnabled() ) {
return DEFAULT;
}
//noinspection unchecked
final JavaType<Object> javaType = (JavaType<Object>) pluralJavaType;
final LazySessionWrapperOptions lazySessionWrapperOptions = new LazySessionWrapperOptions( sessionFactory );

View File

@ -48,10 +48,21 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators context) {
return new DelayedStructJdbcType( this, structName );
}
}
// prefer json by default for now
final JdbcType descriptor = context.getJdbcType( SqlTypes.JSON );
if ( descriptor != null ) {
return descriptor;
// When the column is mapped as XML array, the component type must be SQLXML
if ( context.getExplicitJdbcTypeCode() != null && context.getExplicitJdbcTypeCode() == SqlTypes.XML_ARRAY
// Also prefer XML is the Dialect prefers XML arrays
|| context.getDialect().getPreferredSqlTypeCodeForArray() == SqlTypes.XML_ARRAY ) {
final JdbcType descriptor = context.getJdbcType( SqlTypes.SQLXML );
if ( descriptor != null ) {
return descriptor;
}
}
else {
// Otherwise use json by default for now
final JdbcType descriptor = context.getJdbcType( SqlTypes.JSON );
if ( descriptor != null ) {
return descriptor;
}
}
throw new JdbcTypeRecommendationException(
"Could not determine recommended JdbcType for `" + getTypeName() + "`"

View File

@ -111,6 +111,22 @@ public int resolveJdbcTypeCode(int jdbcTypeCode) {
return delegate.resolveJdbcTypeCode( jdbcTypeCode );
}
@Override
@Incubating
public boolean isXmlFormatMapperLegacyFormatEnabled() {
return delegate.isXmlFormatMapperLegacyFormatEnabled();
}
@Override
public boolean preferJdbcDatetimeTypes() {
return delegate.preferJdbcDatetimeTypes();
}
@Override
public int getPreferredSqlTypeCodeForArray(int elementSqlTypeCode) {
return delegate.getPreferredSqlTypeCodeForArray( elementSqlTypeCode );
}
@Override
public TypeConfiguration getTypeConfiguration() {
return delegate.getTypeConfiguration();

View File

@ -239,6 +239,17 @@ default boolean preferJdbcDatetimeTypes() {
return false;
}
/**
* Whether to use the legacy format for serializing/deserializing XML data.
*
* @since 7.0
* @see org.hibernate.cfg.MappingSettings#XML_FORMAT_MAPPER_LEGACY_FORMAT
*/
@Incubating
default boolean isXmlFormatMapperLegacyFormatEnabled() {
return getCurrentBaseSqlTypeIndicators().isXmlFormatMapperLegacyFormatEnabled();
}
/**
* Provides access to the {@link TypeConfiguration} for access to various type system related registries.
*/

View File

@ -10,10 +10,13 @@
import java.sql.SQLException;
import java.sql.SQLXML;
import org.hibernate.dialect.XmlHelper;
import org.hibernate.metamodel.mapping.EmbeddableMappingType;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.BasicPluralJavaType;
import org.hibernate.type.descriptor.java.JavaType;
/**
@ -65,19 +68,21 @@ protected <X> X fromString(String string, JavaType<X> javaType, WrapperOptions o
//noinspection unchecked
return (X) sqlxml;
}
return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().fromString(
string,
javaType,
options
);
return XmlHelper.arrayFromString( javaType, this, string, options );
}
protected <X> String toString(X value, JavaType<X> javaType, WrapperOptions options) {
return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().toString(
value,
javaType,
options
);
final JdbcType elementJdbcType = getElementJdbcType();
final Object[] domainObjects = javaType.unwrap( value, Object[].class, options );
if ( elementJdbcType instanceof XmlJdbcType xmlElementJdbcType ) {
final EmbeddableMappingType embeddableMappingType = xmlElementJdbcType.getEmbeddableMappingType();
return XmlHelper.arrayToString( embeddableMappingType, domainObjects, options );
}
else {
assert !( elementJdbcType instanceof AggregateJdbcType );
final JavaType<?> elementJavaType = ( (BasicPluralJavaType<?>) javaType ).getElementJavaType();
return XmlHelper.arrayToString( elementJavaType, elementJdbcType, domainObjects, options );
}
}
@Override

View File

@ -77,7 +77,7 @@ public Object[] extractJdbcValues(Object rawJdbcValue, WrapperOptions options) t
return XmlHelper.fromString( embeddableMappingType, (String) rawJdbcValue, false, options );
}
protected <X> String toString(X value, JavaType<X> javaType, WrapperOptions options) {
protected <X> String toString(X value, JavaType<X> javaType, WrapperOptions options) throws SQLException {
if ( embeddableMappingType != null ) {
return XmlHelper.toString( embeddableMappingType, value, options );
}

View File

@ -13,6 +13,7 @@ public final class JacksonIntegration {
private static final boolean JACKSON_XML_AVAILABLE = ableToLoadJacksonXMLMapper();
private static final boolean JACKSON_JSON_AVAILABLE = ableToLoadJacksonJSONMapper();
private static final JacksonXmlFormatMapper XML_FORMAT_MAPPER = JACKSON_XML_AVAILABLE ? new JacksonXmlFormatMapper() : null;
private static final JacksonXmlFormatMapper XML_FORMAT_MAPPER_PORTABLE = JACKSON_XML_AVAILABLE ? new JacksonXmlFormatMapper( false ) : null;
private static final JacksonJsonFormatMapper JSON_FORMAT_MAPPER = JACKSON_JSON_AVAILABLE ? new JacksonJsonFormatMapper() : null;
private JacksonIntegration() {
@ -31,6 +32,10 @@ public static FormatMapper getXMLJacksonFormatMapperOrNull() {
return XML_FORMAT_MAPPER;
}
public static FormatMapper getXMLJacksonFormatMapperOrNull(boolean legacyFormat) {
return legacyFormat ? XML_FORMAT_MAPPER : XML_FORMAT_MAPPER_PORTABLE;
}
public static FormatMapper getJsonJacksonFormatMapperOrNull() {
return JSON_FORMAT_MAPPER;
}

View File

@ -5,11 +5,25 @@
package org.hibernate.type.format.jackson;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;
import org.hibernate.type.format.FormatMapper;
import com.fasterxml.jackson.core.JsonParser;
@ -22,6 +36,7 @@
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
import org.hibernate.type.internal.ParameterizedTypeImpl;
/**
* @author Christian Beikov
@ -29,18 +44,24 @@
public final class JacksonXmlFormatMapper implements FormatMapper {
public static final String SHORT_NAME = "jackson-xml";
private boolean legacyFormat;
private final ObjectMapper objectMapper;
public JacksonXmlFormatMapper() {
this( createXmlMapper() );
this( true );
}
public JacksonXmlFormatMapper(boolean legacyFormat) {
this( createXmlMapper( legacyFormat ) );
this.legacyFormat = legacyFormat;
}
public JacksonXmlFormatMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
private static XmlMapper createXmlMapper() {
private static XmlMapper createXmlMapper(boolean legacyFormat) {
final XmlMapper xmlMapper = new XmlMapper();
// needed to automatically find and register Jackson's jsr310 module for java.time support
xmlMapper.findAndRegisterModules();
@ -50,6 +71,10 @@ private static XmlMapper createXmlMapper() {
// see: https://github.com/FasterXML/jackson-dataformat-xml/issues/344
final SimpleModule module = new SimpleModule();
module.addDeserializer( String[].class, new StringArrayDeserializer() );
if ( !legacyFormat ) {
module.addDeserializer( byte[].class, new ByteArrayDeserializer() );
module.addSerializer( byte[].class, new ByteArraySerializer() );
}
xmlMapper.registerModule( module );
return xmlMapper;
}
@ -60,6 +85,50 @@ public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, Wrapper
return (T) charSequence.toString();
}
try {
if ( !legacyFormat ) {
if ( Map.class.isAssignableFrom( javaType.getJavaTypeClass() ) ) {
final Type keyType;
final Type elementType;
if ( javaType.getJavaType() instanceof ParameterizedType parameterizedType ) {
keyType = parameterizedType.getActualTypeArguments()[0];
elementType = parameterizedType.getActualTypeArguments()[1];
}
else {
keyType = Object.class;
elementType = Object.class;
}
final MapWrapper<?, ?> collectionWrapper = objectMapper.readValue(
charSequence.toString(),
objectMapper.constructType( new ParameterizedTypeImpl( MapWrapper.class,
new Type[] {keyType, elementType}, null ) )
);
final Map<Object, Object> map = new LinkedHashMap<>( collectionWrapper.entry.size() );
for ( EntryWrapper<?, ?> entry : collectionWrapper.entry ) {
map.put( entry.key, entry.value );
}
return javaType.wrap( map, wrapperOptions );
}
else if ( Collection.class.isAssignableFrom( javaType.getJavaTypeClass() ) ) {
final Type elementType = javaType.getJavaType() instanceof ParameterizedType parameterizedType
? parameterizedType.getActualTypeArguments()[0]
: Object.class;
final CollectionWrapper<?> collectionWrapper = objectMapper.readValue(
charSequence.toString(),
objectMapper.constructType(
new ParameterizedTypeImpl( CollectionWrapper.class, new Type[] {elementType},
null ) )
);
return javaType.wrap( collectionWrapper.value, wrapperOptions );
}
else if ( javaType.getJavaTypeClass().isArray() ) {
final CollectionWrapper<?> collectionWrapper = objectMapper.readValue(
charSequence.toString(),
objectMapper.constructType( new ParameterizedTypeImpl( CollectionWrapper.class,
new Type[] {javaType.getJavaTypeClass().getComponentType()}, null ) )
);
return javaType.wrap( collectionWrapper.value, wrapperOptions );
}
}
return objectMapper.readValue(
charSequence.toString(),
objectMapper.constructType( javaType.getJavaType() )
@ -75,6 +144,60 @@ public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapper
if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) {
return (String) value;
}
if ( !legacyFormat ) {
if ( Map.class.isAssignableFrom( javaType.getJavaTypeClass() ) ) {
final Type keyType;
final Type elementType;
if ( javaType.getJavaType() instanceof ParameterizedType parameterizedType ) {
keyType = parameterizedType.getActualTypeArguments()[0];
elementType = parameterizedType.getActualTypeArguments()[1];
}
else {
keyType = Object.class;
elementType = Object.class;
}
final MapWrapper<Object, Object> mapWrapper = new MapWrapper<>();
for ( Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet() ) {
mapWrapper.entry.add( new EntryWrapper<>( entry.getKey(), entry.getValue() ) );
}
return writeValueAsString(
mapWrapper,
javaType,
new ParameterizedTypeImpl( MapWrapper.class, new Type[] {keyType, elementType}, null )
);
}
else if ( Collection.class.isAssignableFrom( javaType.getJavaTypeClass() ) ) {
final Type elementType = javaType.getJavaType() instanceof ParameterizedType parameterizedType
? parameterizedType.getActualTypeArguments()[0]
: Object.class;
return writeValueAsString(
new CollectionWrapper<>( (Collection<?>) value ),
javaType,
new ParameterizedTypeImpl( CollectionWrapper.class, new Type[] {elementType}, null )
);
}
else if ( javaType.getJavaTypeClass().isArray() ) {
final CollectionWrapper<Object> collectionWrapper;
if ( Object[].class.isAssignableFrom( javaType.getJavaTypeClass() ) ) {
collectionWrapper = new CollectionWrapper<>( Arrays.asList( (Object[]) value ) );
}
else {
// Primitive arrays get a special treatment
final int length = Array.getLength( value );
final List<Object> list = new ArrayList<>( length );
for ( int i = 0; i < length; i++ ) {
list.add( Array.get( value, i ) );
}
collectionWrapper = new CollectionWrapper<>( list );
}
return writeValueAsString(
collectionWrapper,
javaType,
new ParameterizedTypeImpl( CollectionWrapper.class,
new Type[] {javaType.getJavaTypeClass().getComponentType()}, null )
);
}
}
return writeValueAsString( value, javaType, javaType.getJavaType() );
}
@ -87,6 +210,51 @@ private <T> String writeValueAsString(Object value, JavaType<T> javaType, Type t
}
}
@JacksonXmlRootElement(localName = "Collection")
public static class CollectionWrapper<E> {
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "e")
Collection<E> value;
public CollectionWrapper() {
this.value = new ArrayList<>();
}
public CollectionWrapper(Collection<E> value) {
this.value = value;
}
}
@JacksonXmlRootElement(localName = "Map")
public static class MapWrapper<K, V> {
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "e")
Collection<EntryWrapper<K, V>> entry;
public MapWrapper() {
this.entry = new ArrayList<>();
}
public MapWrapper(Collection<EntryWrapper<K, V>> entry) {
this.entry = entry;
}
}
public static class EntryWrapper<K, V> {
@JacksonXmlProperty(localName = "k")
K key;
@JacksonXmlProperty(localName = "v")
V value;
public EntryWrapper() {
}
public EntryWrapper(K key, V value) {
this.key = key;
this.value = value;
}
}
private static class StringArrayDeserializer extends JsonDeserializer<String[]> {
@Override
public String[] deserialize(JsonParser jp, DeserializationContext deserializationContext) throws IOException {
@ -100,4 +268,28 @@ public String[] deserialize(JsonParser jp, DeserializationContext deserializatio
return result.toArray( String[]::new );
}
}
private static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser jp, DeserializationContext deserializationContext) throws IOException {
return PrimitiveByteArrayJavaType.INSTANCE.fromString( jp.getValueAsString() );
}
}
public static class ByteArraySerializer extends StdSerializer<byte[]> {
public ByteArraySerializer() {
super( byte[].class );
}
@Override
public boolean isEmpty(SerializerProvider prov, byte[] value) {
return value.length == 0;
}
@Override
public void serialize(byte[] value, JsonGenerator g, SerializerProvider provider) throws IOException {
g.writeString( PrimitiveByteArrayJavaType.INSTANCE.toString( value ) );
}
}
}

View File

@ -18,6 +18,8 @@
import javax.xml.namespace.QName;
import jakarta.xml.bind.annotation.XmlElement;
import org.hibernate.dialect.XmlHelper;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.sql.ast.spi.StringBuilderSqlAppender;
@ -44,8 +46,27 @@
public final class JaxbXmlFormatMapper implements FormatMapper {
public static final String SHORT_NAME = "jaxb";
private final boolean legacyFormat;
private final String collectionElementTagName;
private final String mapKeyTagName;
private final String mapValueTagName;
public JaxbXmlFormatMapper() {
this( true );
}
public JaxbXmlFormatMapper(boolean legacyFormat) {
this.legacyFormat = legacyFormat;
if ( legacyFormat ) {
collectionElementTagName = "value";
mapKeyTagName = "key";
mapValueTagName = "value";
}
else {
collectionElementTagName = XmlHelper.ROOT_TAG;
mapKeyTagName = "k";
mapValueTagName = "v";
}
}
@Override
@ -64,18 +85,27 @@ public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, Wrapper
final Type[] typeArguments = ( (ParameterizedType) javaType.getJavaType() ).getActualTypeArguments();
keyClass = ReflectHelper.getClass( typeArguments[0] );
valueClass = ReflectHelper.getClass( typeArguments[1] );
context = JAXBContext.newInstance( MapWrapper.class, keyClass, valueClass );
if ( legacyFormat ) {
context = JAXBContext.newInstance( LegacyMapWrapper.class, keyClass, valueClass );
}
else {
context = JAXBContext.newInstance( MapWrapper.class, EntryWrapper.class, keyClass, valueClass );
}
}
else {
keyClass = Object.class;
valueClass = Object.class;
context = JAXBContext.newInstance( MapWrapper.class );
if ( legacyFormat ) {
context = JAXBContext.newInstance( LegacyMapWrapper.class );
}
else {
context = JAXBContext.newInstance( MapWrapper.class, EntryWrapper.class );
}
}
final Unmarshaller unmarshaller = context.createUnmarshaller();
final MapWrapper mapWrapper = (MapWrapper) unmarshaller
final ManagedMapWrapper mapWrapper = (ManagedMapWrapper) unmarshaller
.unmarshal( new StringReader( charSequence.toString() ) );
final Collection<Object> elements = mapWrapper.elements;
final Map<Object, Object> map = CollectionHelper.linkedMapOfSize( elements.size() >> 1 );
final Map<Object, Object> map = CollectionHelper.linkedMapOfSize( mapWrapper.size() >> 1 );
final JAXBIntrospector jaxbIntrospector = context.createJAXBIntrospector();
final JAXBElementTransformer keyTransformer;
final JAXBElementTransformer valueTransformer;
@ -83,7 +113,7 @@ public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, Wrapper
keyTransformer = createTransformer(
appender,
keyClass,
"key",
mapKeyTagName,
null,
jaxbIntrospector,
wrapperOptions
@ -91,7 +121,7 @@ public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, Wrapper
valueTransformer = createTransformer(
appender,
( (BasicPluralJavaType<?>) javaType ).getElementJavaType(),
"value",
mapValueTagName,
null,
jaxbIntrospector,
wrapperOptions
@ -101,7 +131,7 @@ public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, Wrapper
keyTransformer = createTransformer(
appender,
keyClass,
"key",
mapKeyTagName,
null,
jaxbIntrospector,
wrapperOptions
@ -109,16 +139,26 @@ public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, Wrapper
valueTransformer = createTransformer(
appender,
valueClass,
"value",
mapValueTagName,
null,
jaxbIntrospector,
wrapperOptions
);
}
for ( final Iterator<Object> iterator = elements.iterator(); iterator.hasNext(); ) {
final Object key = keyTransformer.fromJAXBElement( iterator.next(), unmarshaller );
final Object value = valueTransformer.fromJAXBElement( iterator.next(), unmarshaller );
map.put( key, value );
if ( legacyFormat ) {
final Collection<Object> elements = ( (LegacyMapWrapper) mapWrapper).elements;
for ( final Iterator<Object> iterator = elements.iterator(); iterator.hasNext(); ) {
final Object key = keyTransformer.fromJAXBElement( iterator.next(), unmarshaller );
final Object value = valueTransformer.fromJAXBElement( iterator.next(), unmarshaller );
map.put( key, value );
}
}
else {
for ( EntryWrapper entry : ((MapWrapper) mapWrapper).entries ) {
final Object key = keyTransformer.fromXmlContent( entry.key );
final Object value = valueTransformer.fromXmlContent( entry.value );
map.put( key, value );
}
}
return javaType.wrap( map, wrapperOptions );
}
@ -145,7 +185,7 @@ else if ( Collection.class.isAssignableFrom( javaType.getJavaTypeClass() ) ) {
valueTransformer = createTransformer(
appender,
( (BasicPluralJavaType<?>) javaType ).getElementJavaType(),
"value",
collectionElementTagName,
null,
jaxbIntrospector,
wrapperOptions
@ -155,7 +195,7 @@ else if ( Collection.class.isAssignableFrom( javaType.getJavaTypeClass() ) ) {
valueTransformer = createTransformer(
appender,
valueClass,
"value",
collectionElementTagName,
null,
jaxbIntrospector,
wrapperOptions
@ -180,7 +220,7 @@ else if ( javaType.getJavaTypeClass().isArray() ) {
valueTransformer = createTransformer(
appender,
( (BasicPluralJavaType<?>) javaType ).getElementJavaType(),
"value",
collectionElementTagName,
null,
jaxbIntrospector,
wrapperOptions
@ -190,7 +230,7 @@ else if ( javaType.getJavaTypeClass().isArray() ) {
valueTransformer = createTransformer(
appender,
valueClass,
"value",
collectionElementTagName,
null,
jaxbIntrospector,
wrapperOptions
@ -243,19 +283,28 @@ public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapper
final JAXBContext context;
final Class<Object> keyClass;
final Class<Object> valueClass;
final MapWrapper mapWrapper = new MapWrapper();
final Map<?, ?> map = (Map<?, ?>) value;
if ( javaType.getJavaType() instanceof ParameterizedType ) {
final Type[] typeArguments = ( (ParameterizedType) javaType.getJavaType() ).getActualTypeArguments();
keyClass = ReflectHelper.getClass( typeArguments[0] );
valueClass = ReflectHelper.getClass( typeArguments[1] );
context = JAXBContext.newInstance( MapWrapper.class, keyClass, valueClass );
if ( legacyFormat ) {
context = JAXBContext.newInstance( LegacyMapWrapper.class, keyClass, valueClass );
}
else {
context = JAXBContext.newInstance( MapWrapper.class, EntryWrapper.class, keyClass, valueClass );
}
}
else {
if ( map.isEmpty() ) {
keyClass = Object.class;
valueClass = Object.class;
context = JAXBContext.newInstance( MapWrapper.class );
if ( legacyFormat ) {
context = JAXBContext.newInstance( LegacyMapWrapper.class );
}
else {
context = JAXBContext.newInstance( MapWrapper.class, EntryWrapper.class );
}
}
else {
final Map.Entry<?, ?> firstEntry = map.entrySet().iterator().next();
@ -263,9 +312,15 @@ public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapper
keyClass = (Class<Object>) firstEntry.getKey().getClass();
//noinspection unchecked
valueClass = (Class<Object>) firstEntry.getValue().getClass();
context = JAXBContext.newInstance( MapWrapper.class, keyClass, valueClass );
if ( legacyFormat ) {
context = JAXBContext.newInstance( LegacyMapWrapper.class, keyClass, valueClass );
}
else {
context = JAXBContext.newInstance( MapWrapper.class, EntryWrapper.class, keyClass, valueClass );
}
}
}
final ManagedMapWrapper managedMapWrapper = legacyFormat ? new LegacyMapWrapper() : new MapWrapper();
if ( !map.isEmpty() ) {
Object exampleKey = null;
Object exampleValue = null;
@ -289,7 +344,7 @@ public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapper
final JAXBElementTransformer keyTransformer = createTransformer(
appender,
keyClass,
"key",
mapKeyTagName,
exampleKey,
jaxbIntrospector,
wrapperOptions
@ -297,17 +352,29 @@ public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapper
final JAXBElementTransformer valueTransformer = createTransformer(
appender,
valueClass,
"value",
mapValueTagName,
exampleValue,
jaxbIntrospector,
wrapperOptions
);
for ( Map.Entry<?, ?> entry : map.entrySet() ) {
mapWrapper.elements.add( keyTransformer.toJAXBElement( entry.getKey() ) );
mapWrapper.elements.add( valueTransformer.toJAXBElement( entry.getValue() ) );
if ( legacyFormat ) {
final LegacyMapWrapper legacyMapWrapper = (LegacyMapWrapper) managedMapWrapper;
for ( Map.Entry<?, ?> entry : map.entrySet() ) {
legacyMapWrapper.elements.add( keyTransformer.toJAXBElement( entry.getKey() ) );
legacyMapWrapper.elements.add( valueTransformer.toJAXBElement( entry.getValue() ) );
}
}
else {
final MapWrapper mapWrapper = (MapWrapper) managedMapWrapper;
for ( Map.Entry<?, ?> entry : map.entrySet() ) {
mapWrapper.entries.add( new EntryWrapper(
(String) keyTransformer.toJAXBElement( entry.getKey() ).getValue(),
(String) valueTransformer.toJAXBElement( entry.getValue() ).getValue()
) );
}
}
}
createMarshaller( context ).marshal( mapWrapper, stringWriter );
createMarshaller( context ).marshal( managedMapWrapper, stringWriter );
}
else if ( Collection.class.isAssignableFrom( javaType.getJavaTypeClass() ) ) {
final JAXBContext context;
@ -347,7 +414,7 @@ else if ( Collection.class.isAssignableFrom( javaType.getJavaTypeClass() ) ) {
valueTransformer = createTransformer(
appender,
( (BasicPluralJavaType<?>) javaType ).getElementJavaType(),
"value",
collectionElementTagName,
exampleValue,
context.createJAXBIntrospector(),
wrapperOptions
@ -357,7 +424,7 @@ else if ( Collection.class.isAssignableFrom( javaType.getJavaTypeClass() ) ) {
valueTransformer = createTransformer(
appender,
valueClass,
"value",
collectionElementTagName,
exampleValue,
context.createJAXBIntrospector(),
wrapperOptions
@ -389,7 +456,7 @@ else if ( javaType.getJavaTypeClass().isArray() ) {
transformer = createTransformer(
appender,
( (BasicPluralJavaType<?>) javaType ).getElementJavaType(),
"value",
collectionElementTagName,
exampleElement,
context.createJAXBIntrospector(),
wrapperOptions
@ -399,7 +466,7 @@ else if ( javaType.getJavaTypeClass().isArray() ) {
transformer = createTransformer(
appender,
valueClass,
"value",
collectionElementTagName,
exampleElement,
context.createJAXBIntrospector(),
wrapperOptions
@ -417,7 +484,7 @@ else if ( javaType.getJavaTypeClass().isArray() ) {
final JavaTypeJAXBElementTransformer transformer = new JavaTypeJAXBElementTransformer(
appender,
( (BasicPluralJavaType<?>) javaType ).getElementJavaType(),
"value"
collectionElementTagName
);
for ( int i = 0; i < length; i++ ) {
list.add( transformer.toJAXBElement( Array.get( value, i ) ) );
@ -500,18 +567,61 @@ private Marshaller createMarshaller(JAXBContext context) throws JAXBException {
return marshaller;
}
public static interface ManagedMapWrapper {
int size();
}
@XmlRootElement(name = "Map")
public static class MapWrapper {
public static class LegacyMapWrapper implements ManagedMapWrapper {
@XmlAnyElement
Collection<Object> elements;
public MapWrapper() {
public LegacyMapWrapper() {
this.elements = new ArrayList<>();
}
public MapWrapper(Collection<Object> elements) {
public LegacyMapWrapper(Collection<Object> elements) {
this.elements = elements;
}
@Override
public int size() {
return elements.size();
}
}
@XmlRootElement(name = "Map")
public static class MapWrapper implements ManagedMapWrapper {
@XmlElement(name = "e")
Collection<EntryWrapper> entries;
public MapWrapper() {
this.entries = new ArrayList<>();
}
public MapWrapper(Collection<EntryWrapper> elements) {
this.entries = elements;
}
@Override
public int size() {
return entries.size();
}
}
public static class EntryWrapper {
@XmlElement(name = "k", nillable = true)
String key;
@XmlElement(name = "v", nillable = true)
String value;
public EntryWrapper() {
}
public EntryWrapper(String key, String value) {
this.key = key;
this.value = value;
}
}
@XmlRootElement(name = "Collection")
@ -531,6 +641,7 @@ public CollectionWrapper(Collection<Object> elements) {
private static interface JAXBElementTransformer {
JAXBElement<?> toJAXBElement(Object o);
Object fromJAXBElement(Object element, Unmarshaller unmarshaller) throws JAXBException;
Object fromXmlContent(String content);
}
private static class SimpleJAXBElementTransformer implements JAXBElementTransformer {
@ -560,6 +671,11 @@ public Object fromJAXBElement(Object element, Unmarshaller unmarshaller) throws
}
return value;
}
@Override
public Object fromXmlContent(String content) {
return content;
}
}
private static class JavaTypeJAXBElementTransformer implements JAXBElementTransformer {
@ -593,8 +709,13 @@ public JAXBElement<?> toJAXBElement(Object o) {
@Override
public Object fromJAXBElement(Object element, Unmarshaller unmarshaller) throws JAXBException {
final String value = unmarshaller.unmarshal( (Node) element, String.class ).getValue();
final String value = element == null ? null : unmarshaller.unmarshal( (Node) element, String.class ).getValue();
return value == null ? null : elementJavaType.fromEncodedString( value, 0, value.length() );
}
@Override
public Object fromXmlContent(String content) {
return content == null ? null : elementJavaType.fromEncodedString( content, 0, content.length() );
}
}
}

View File

@ -488,6 +488,19 @@ public boolean preferJdbcDatetimeTypes() {
&& sessionFactory.getSessionFactoryOptions().isPreferJdbcDatetimeTypesInNativeQueriesEnabled();
}
@Override
public boolean isXmlFormatMapperLegacyFormatEnabled() {
if ( metadataBuildingContext != null ) {
return metadataBuildingContext.getBuildingOptions().isXmlFormatMapperLegacyFormatEnabled();
}
else if ( sessionFactory != null ) {
return sessionFactory.getSessionFactoryOptions().isXmlFormatMapperLegacyFormatEnabled();
}
else {
return false;
}
}
private Scope(TypeConfiguration typeConfiguration) {
this.typeConfiguration = typeConfiguration;
}

View File

@ -193,6 +193,7 @@ public void testNodeBuilderXmlTableObject(SessionFactoryScope scope) {
}
@Test
@SkipForDialect(dialectClass = SybaseASEDialect.class, reason = "Sybase doesn't support such xpath expressions directly in xmltable. We could emulate that through generating xmlextract calls though")
public void testCorrelateXmlTable(SessionFactoryScope scope) {
scope.inSession( em -> {
final String query = """
@ -202,10 +203,10 @@ public void testCorrelateXmlTable(SessionFactoryScope scope) {
t.theString,
t.theBoolean
from XmlHolder e join lateral xmltable('/Map' passing e.xml columns
theInt Integer,
theFloat Float,
theString String,
theBoolean Boolean
theInt Integer path 'e[k/text()="theInt"]/v',
theFloat Float path 'e[k/text()="theFloat"]/v',
theString String path 'e[k/text()="theString"]/v',
theBoolean Boolean path 'e[k/text()="theBoolean"]/v'
) t
""";
List<Tuple> resultList = em.createQuery( query, Tuple.class ).getResultList();

View File

@ -0,0 +1,325 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.mapping.type.format;
import org.hibernate.testing.orm.domain.StandardDomainModel;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.SessionFactoryScopeAware;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;
import org.hibernate.type.format.FormatMapper;
import org.hibernate.type.format.jackson.JacksonXmlFormatMapper;
import org.hibernate.type.format.jaxb.JaxbXmlFormatMapper;
import org.hibernate.type.internal.ParameterizedTypeImpl;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
@DomainModel(standardModels = StandardDomainModel.LIBRARY)
@SessionFactory
public class XmlFormatterTest implements SessionFactoryScopeAware {
private SessionFactoryScope scope;
@Override
public void injectSessionFactoryScope(SessionFactoryScope scope) {
this.scope = scope;
}
private static Stream<Arguments> formatMappers() {
return Stream.of( new JaxbXmlFormatMapper( false ), new JacksonXmlFormatMapper( false ) )
.map( Arguments::of );
}
@ParameterizedTest
@MethodSource("formatMappers")
public void testCollection(FormatMapper formatMapper) {
assertCollection( List.of(), Integer.class, formatMapper );
assertCollection( Arrays.asList( new Integer[]{ null } ), Integer.class, formatMapper );
assertCollection( List.of( "Abc" ), String.class, formatMapper );
assertCollection( List.of( 123 ), Integer.class, formatMapper );
}
@ParameterizedTest
@MethodSource("formatMappers")
public void testArray(FormatMapper formatMapper) {
assertArray( new int[0], formatMapper );
assertArray( new String[]{ null }, formatMapper );
assertArray( new String[]{ "Abc" }, formatMapper );
assertArray( new int[]{ 123 }, formatMapper );
assertArray( new Integer[]{ 123 }, formatMapper );
}
@ParameterizedTest
@MethodSource("formatMappers")
public void testByteArray(FormatMapper formatMapper) {
assertArray( new byte[0][0], formatMapper );
assertArray( new byte[][]{ new byte[]{ 1 } }, formatMapper );
}
@ParameterizedTest
@MethodSource("formatMappers")
public void testMap(FormatMapper formatMapper) {
assertMap( Map.of(), Integer.class, Integer.class, formatMapper );
assertMap( new HashMap<>(){{ put(null, "Abc"); }}, Integer.class, String.class, formatMapper );
assertMap( new HashMap<>(){{ put(123, null); }}, Integer.class, String.class, formatMapper );
assertMap( Map.of( 123, "Abc" ), Integer.class, String.class, formatMapper );
}
private void assertCollection(List<Object> values, Type elementType, FormatMapper formatMapper) {
assertXmlEquals( expectedCollectionString( values ), collectionToString( values, elementType, formatMapper ) );
}
private void assertArray(Object values, FormatMapper formatMapper) {
assertXmlEquals( expectedArrayString( values ), arrayToString( values, formatMapper ) );
}
private void assertMap(Map<?, ?> values, Type keyType, Type elementType, FormatMapper formatMapper) {
assertXmlEquals( expectedMapString( values ), mapToString( values, keyType, elementType, formatMapper ) );
}
private String expectedArrayString(Object values) {
if ( values instanceof Object[] array ) {
return expectedCollectionString( Arrays.asList( array ) );
}
else {
final int length = Array.getLength( values );
final ArrayList<Object> list = new ArrayList<>( length );
for ( int i = 0; i < length; i++ ) {
list.add( Array.get( values, i ) );
}
return expectedCollectionString( list );
}
}
private String expectedCollectionString(Collection<?> values) {
final StringBuilder sb = new StringBuilder();
sb.append( "<Collection" );
if ( values.isEmpty() ) {
sb.append( "/>" );
}
else {
sb.append( ">" );
for ( Object value : values ) {
if ( value == null ) {
sb.append( "<e xsi:nil=\"true\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"/>" );
}
else {
sb.append( "<e>" );
if ( value instanceof byte[] bytes ) {
sb.append( PrimitiveByteArrayJavaType.INSTANCE.toString( bytes ) );
}
else {
sb.append( value );
}
sb.append( "</e>" );
}
}
sb.append( "</Collection>" );
}
return sb.toString();
}
private String expectedMapString(Map<?, ?> values) {
final StringBuilder sb = new StringBuilder();
sb.append( "<Map" );
if ( values.isEmpty() ) {
sb.append( "/>" );
}
else {
sb.append( ">" );
for ( Map.Entry<?, ?> entry : values.entrySet() ) {
final Object key = entry.getKey();
final Object value = entry.getValue();
sb.append( "<e>" );
if ( key == null ) {
sb.append( "<k xsi:nil=\"true\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"/>" );
}
else {
sb.append( "<k>" );
if ( key instanceof byte[] bytes ) {
sb.append( PrimitiveByteArrayJavaType.INSTANCE.toString( bytes ) );
}
else {
sb.append( key );
}
sb.append( "</k>" );
}
if ( value == null ) {
sb.append( "<v xsi:nil=\"true\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"/>" );
}
else {
sb.append( "<v>" );
if ( value instanceof byte[] bytes ) {
sb.append( PrimitiveByteArrayJavaType.INSTANCE.toString( bytes ) );
}
else {
sb.append( value );
}
sb.append( "</v>" );
}
sb.append( "</e>" );
}
sb.append( "</Map>" );
}
return sb.toString();
}
private String collectionToString(Collection<?> value, Type elementType, FormatMapper formatMapper) {
final JavaType<Object> javaType = scope.getSessionFactory().getTypeConfiguration().getJavaTypeRegistry()
.resolveDescriptor( new ParameterizedTypeImpl( Collection.class, new Type[] {elementType}, null ) );
final WrapperOptions wrapperOptions = scope.getSessionFactory().getWrapperOptions();
final String actualValue = formatMapper.toString(
value,
javaType,
wrapperOptions
);
assertXmlEquals(
actualValue,
formatMapper.toString(
formatMapper.fromString( actualValue, javaType, wrapperOptions ),
javaType,
wrapperOptions
)
);
return actualValue;
}
private String arrayToString(Object value, FormatMapper formatMapper) {
final JavaType<Object> javaType = scope.getSessionFactory().getTypeConfiguration().getJavaTypeRegistry()
.resolveDescriptor( value.getClass() );
final WrapperOptions wrapperOptions = scope.getSessionFactory().getWrapperOptions();
final String actualValue = formatMapper.toString(
value,
javaType,
wrapperOptions
);
assertXmlEquals(
actualValue,
formatMapper.toString(
formatMapper.fromString( actualValue, javaType, wrapperOptions ),
javaType,
wrapperOptions
)
);
return actualValue;
}
private String mapToString(Map<?, ?> value, Type keyType, Type elementType, FormatMapper formatMapper) {
final JavaType<Object> javaType = scope.getSessionFactory().getTypeConfiguration().getJavaTypeRegistry()
.resolveDescriptor( new ParameterizedTypeImpl( Map.class, new Type[] {keyType, elementType}, null ) );
final WrapperOptions wrapperOptions = scope.getSessionFactory().getWrapperOptions();
final String actualValue = formatMapper.toString(
value,
javaType,
wrapperOptions
);
assertXmlEquals(
actualValue,
formatMapper.toString(
formatMapper.fromString( actualValue, javaType, wrapperOptions ),
javaType,
wrapperOptions
)
);
return actualValue;
}
private void assertXmlEquals(String expected, String actual) {
final Document expectedDoc = parseXml( xmlNormalize( expected ) );
final Document actualDoc = parseXml( xmlNormalize( actual ) );
normalize( expectedDoc );
normalize( actualDoc );
assertEquals( toXml( expectedDoc ).trim(), toXml( actualDoc ).trim() );
}
private void normalize(Document document) {
normalize( document.getChildNodes() );
}
private void normalize(NodeList childNodes) {
for ( int i = 0; i < childNodes.getLength(); i++ ) {
final Node childNode = childNodes.item( i );
if ( childNode.getNodeType() == Node.ELEMENT_NODE ) {
normalize( childNode.getChildNodes() );
}
else if ( childNode.getNodeType() == Node.TEXT_NODE ) {
if ( childNode.getNodeValue().isBlank() ) {
childNode.getParentNode().removeChild( childNode );
}
else {
childNode.setNodeValue( childNode.getNodeValue().trim() );
}
}
else if ( childNode.getNodeType() == Node.COMMENT_NODE ) {
childNode.setNodeValue( childNode.getNodeValue().trim() );
}
}
}
private String xmlNormalize(String doc) {
final String prefix = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
return doc.startsWith( "<?xml" ) ? doc : prefix + doc;
}
private static Document parseXml(String document) {
final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
final DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse( new InputSource( new StringReader( document ) ) );
}
catch (ParserConfigurationException | IOException | SAXException e) {
throw new RuntimeException( e );
}
}
private static String toXml(Document document) {
final TransformerFactory tf = TransformerFactory.newInstance();
try {
final Transformer transformer = tf.newTransformer();
transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty( OutputKeys.INDENT, "yes" );
final StringWriter writer = new StringWriter();
transformer.transform( new DOMSource( document ), new StreamResult( writer ) );
return writer.toString();
}
catch (TransformerException e) {
throw new RuntimeException( e );
}
}
}

View File

@ -436,6 +436,11 @@ public boolean isPreferNativeEnumTypesEnabled() {
return MetadataBuildingContext.super.isPreferNativeEnumTypesEnabled();
}
@Override
public boolean isXmlFormatMapperLegacyFormatEnabled() {
return false;
}
@Override
public FastSessionServices getFastSessionServices() {
throw new UnsupportedOperationException("operation not supported");