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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
private boolean namedQueryStartupCheckingEnabled;
private final boolean preferJavaTimeJdbcTypes;
private final boolean preferNativeEnumTypes;
private final boolean preferLocaleLanguageTagEnabled;
private final int preferredSqlTypeCodeForBoolean;
private final int preferredSqlTypeCodeForDuration;
private final int preferredSqlTypeCodeForUuid;
Expand Down Expand Up @@ -394,6 +395,7 @@ public SessionFactoryOptionsBuilder(StandardServiceRegistry serviceRegistry, Boo

preferJavaTimeJdbcTypes = MetadataBuildingContext.isPreferJavaTimeJdbcTypesEnabled( configurationService );
preferNativeEnumTypes = MetadataBuildingContext.isPreferNativeEnumTypesEnabled( configurationService );
preferLocaleLanguageTagEnabled = MetadataBuildingContext.isPreferNativeEnumTypesEnabled( configurationService );
preferredSqlTypeCodeForBoolean = ConfigurationHelper.getPreferredSqlTypeCodeForBoolean( serviceRegistry );
preferredSqlTypeCodeForDuration = ConfigurationHelper.getPreferredSqlTypeCodeForDuration( serviceRegistry );
preferredSqlTypeCodeForUuid = ConfigurationHelper.getPreferredSqlTypeCodeForUuid( serviceRegistry );
Expand Down Expand Up @@ -1358,6 +1360,11 @@ public boolean isPreferNativeEnumTypesEnabled() {
return preferNativeEnumTypes;
}

@Override
public boolean isPreferLocaleLanguageTagEnabled() {
return preferLocaleLanguageTagEnabled;
}

@Override
public FormatMapper getJsonFormatMapper() {
if ( jsonFormatMapper == null ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,11 @@ public boolean isPreferNativeEnumTypesEnabled() {
return delegate.isPreferNativeEnumTypesEnabled();
}

@Override
public boolean isPreferLocaleLanguageTagEnabled() {
return delegate.isPreferLocaleLanguageTagEnabled();
}

@Override
public FormatMapper getJsonFormatMapper() {
return delegate.getJsonFormatMapper();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ default boolean isPreferNativeEnumTypesEnabled() {
return isPreferNativeEnumTypesEnabled( getBootstrapContext().getServiceRegistry() );
}

@Incubating
default boolean isPreferLocaleLanguageTagEnabled() {
return isPreferLocaleLanguageTagEnabled( getBootstrapContext().getServiceRegistry() );
}

static boolean isPreferJavaTimeJdbcTypesEnabled(ServiceRegistry serviceRegistry) {
return isPreferJavaTimeJdbcTypesEnabled( serviceRegistry.requireService( ConfigurationService.class ) );
}
Expand All @@ -96,6 +101,10 @@ static boolean isPreferNativeEnumTypesEnabled(ServiceRegistry serviceRegistry) {
return isPreferNativeEnumTypesEnabled( serviceRegistry.requireService( ConfigurationService.class ) );
}

static boolean isPreferLocaleLanguageTagEnabled(ServiceRegistry serviceRegistry) {
return isPreferLocaleLanguageTagEnabled( serviceRegistry.requireService( ConfigurationService.class ) );
}

static boolean isPreferJavaTimeJdbcTypesEnabled(ConfigurationService configurationService) {
return ConfigurationHelper.getBoolean(
MappingSettings.JAVA_TIME_USE_DIRECT_JDBC,
Expand All @@ -114,6 +123,14 @@ static boolean isPreferNativeEnumTypesEnabled(ConfigurationService configuration
);
}

static boolean isPreferLocaleLanguageTagEnabled(ConfigurationService configurationService) {
return ConfigurationHelper.getBoolean(
MappingSettings.PREFER_LOCALE_LANGUAGE_TAG,
configurationService.getSettings(),
false
);
}

TypeDefinitionRegistry getTypeDefinitionRegistry();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,11 @@ default boolean isCollectionsInDefaultFetchGroupEnabled() {
*/
boolean isPreferNativeEnumTypesEnabled();

/**
* @see org.hibernate.cfg.MappingSettings#PREFER_LOCALE_LANGUAGE_TAG
*/
boolean isPreferLocaleLanguageTagEnabled();

/**
* The format mapper to use for serializing/deserializing JSON data.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OrderColumn;

import java.util.Locale;

/**
* @author Steve Ebersole
*/
Expand Down Expand Up @@ -275,6 +277,21 @@ public interface MappingSettings {
@Incubating
String PREFER_NATIVE_ENUM_TYPES = "hibernate.type.prefer_native_enum_types";

/**
* Indicates whether {@link Locale#toLanguageTag()} should be preferred over {@link Locale#toString()}
* when converting a value to a {@linkplain String}.
* <p/>
* This configuration property is used to specify a global preference,
* but Hibernate ORM can always read both formats, so no data needs to be migrated.
* The setting simply configures how {@link Locale} data is to be stored.
*
* @settingDefault false
*
* @since 7.2
*/
@Incubating
String PREFER_LOCALE_LANGUAGE_TAG = "hibernate.type.prefer_locale_language_tag";

/**
* Specifies the preferred JDBC type for storing plural i.e. array/collection values.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ default int getPreferredSqlTypeCodeForBoolean() {
return getSessionFactory().getSessionFactoryOptions().getPreferredSqlTypeCodeForBoolean();
}

/**
* Determines whether streams should be used for binding LOB values.
*
* @return {@code true}/{@code false}
*/
default boolean useLanguageTagForLocale() {
return getSessionFactory().getSessionFactoryOptions().isPreferLocaleLanguageTagEnabled();
}

/**
* Obtain access to the {@link LobCreator}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,53 +44,137 @@ public String toString(Locale value) {
}

public Locale fromString(CharSequence sequence) {
// TODO : Ultimately switch to Locale.Builder for this. However, Locale.Builder is Java 7

if ( sequence == null ) {
return null;
}

String string = sequence.toString();
if( string.isEmpty() ) {
if ( string.isEmpty() ) {
return Locale.ROOT;
}

boolean separatorFound = false;
final char[] chars = string.toCharArray();
final Locale.Builder builder = new Locale.Builder();
State state = State.LANGUAGE;
int position = 0;
char[] chars = string.toCharArray();

for ( int i = 0; i < chars.length; i++ ) {
// We just look for separators
if ( chars[i] == '_' ) {
if ( !separatorFound ) {
// On the first separator we know that we have at least a language
string = new String( chars, position, i - position );
position = i + 1;
state = state.parseNext( chars, position, i - position, builder );
position = i + 1;
}
// If we find a dash instead of an underscore, assume this is a BCP 47 language tag
else if ( state == State.LANGUAGE && chars[i] == '-' ) {
builder.setLanguageTag( string );
position = chars.length;
break;
}
}

if ( position != chars.length ) {
if ( state == State.LANGUAGE ) {
// This is special, we have no separators and at least 1 character, so assume it is a language tag.
// To retain backwards compatibility, convert old locale languages to the proper language tags.
// All the other locale languages are equivalent to the language tag languages.
builder.setLanguageTag( switch ( string ) {
case "iw", "Iw", "iW", "IW" -> "he";
case "ji", "Ji", "jI", "JI" -> "yi";
case "in", "In", "iN", "IN" -> "id";
default -> string;
} );
}
else {
state.parseNext( chars, position, chars.length - position, builder );
}
}
return builder.build();
}

enum State {
LANGUAGE,
REGION,
VARIANT,
SCRIPT,
EXTENSION,
END;

State parseNext(char[] chars, int start, int length, Locale.Builder builder) {
return switch ( this ) {
case LANGUAGE -> {
builder.setLanguage( new String( chars, start, length ) );
yield REGION;
}
else {
// On the second separator we have to check whether there are more chars available for variant
if ( chars.length > i + 1 ) {
// There is a variant so add it to the constructor
return new Locale( string, new String( chars, position, i - position ), new String( chars,
i + 1, chars.length - i - 1 ) );
case REGION -> {
builder.setRegion( new String( chars, start, length ) );
yield VARIANT;
}
case VARIANT -> {
if ( chars[start] == '#' ) {
if ( isScript( chars, start + 1, length - 1 ) ) {
builder.setScript( new String( chars, start + 1, length - 1 ) );
yield EXTENSION;
}
else {
handleExtension( chars, start + 1, length - 1, builder );
yield END;
}
}
else {
// No variant given, we just have language and country
return new Locale( string, new String( chars, position, i - position ), "" );
builder.setVariant( new String( chars, start, length ) );
yield SCRIPT;
}
}

separatorFound = true;
}
case SCRIPT -> {
if ( length < 5 || chars[start] != '#' ) {
throw new IllegalArgumentException( "Invalid script: " + new String( chars, start, length ) );
}
if ( isScript( chars, start + 1, length - 1 ) ) {
builder.setScript( new String( chars, start + 1, length - 1 ) );
yield EXTENSION;
}
else {
handleExtension( chars, start + 1, length - 1, builder );
yield END;
}
}
case EXTENSION -> {
handleExtension( chars, start, length, builder );
yield END;
}
case END -> throw new IllegalStateException( "Unexpected continuation of locale value after extension: " + new String( chars, start, length ) );
};
}

if ( !separatorFound ) {
// No separator found, there is only a language
return new Locale( string );
private boolean isScript(char[] chars, int start, int length) {
return length == 4
&& Character.isLetter( chars[start] )
&& Character.isLetter( chars[start + 1] )
&& Character.isLetter( chars[start + 2] )
&& Character.isLetter( chars[start + 3] );
}
else {
// Only one separator found, there is a language and a country
return new Locale( string, new String( chars, position, chars.length - position ) );

private void handleExtension(char[] chars, int start, int length, Locale.Builder builder) {
if ( length < 3 || chars[start + 1] != '-' ) {
throw new IllegalArgumentException( "Invalid extension: " + new String( chars, start, length ) );
}
if ( Character.toLowerCase( chars[start] ) == 'u' ) {
// After a Unicode extension, there could come a private use extension which we need to detect
int unicodeStart = start + 2;
int unicodeLength = length - 2;
final int end = start + length;
for ( int i = start + 2; i < end; i++ ) {
if ( chars[i] == '-' && i + 3 < end && chars[i + 1] == 'x' && chars[i + 2] == '-' ) {
builder.setExtension( 'x', new String( chars, i + 3, end - i - 3 ) );
unicodeLength = i - unicodeStart;
break;
}
}
builder.setExtension( chars[start], new String( chars, unicodeStart, unicodeLength ) );
}
else {
builder.setExtension( chars[start], new String( chars, start + 2, length - 2 ) );
}
}
}

Expand All @@ -103,7 +187,7 @@ public <X> X unwrap(Locale value, Class<X> type, WrapperOptions options) {
return (X) value;
}
if ( String.class.isAssignableFrom( type ) ) {
return (X) value.toString();
return (X) (options.useLanguageTagForLocale() ? value.toLanguageTag() : value.toString());
}
throw unknownUnwrap( type );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package org.hibernate.orm.test.mapping.type.java;

import static org.hibernate.internal.util.StringHelper.isEmpty;
import static org.junit.Assert.assertEquals;

import java.util.Locale;
Expand All @@ -20,9 +21,9 @@
* @author Steve Ebersole
*/
public class LocaleJavaTypeDescriptorTest extends AbstractDescriptorTest<Locale> {
final Locale original = toLocale( "de", "DE", null );
final Locale copy = toLocale( "de", "DE", null );
final Locale different = toLocale( "de", null, null );
final Locale original = toLocale( "de", "DE", null, null );
final Locale copy = toLocale( "de", "DE", null, null );
final Locale different = toLocale( "de", null, null, null );

public LocaleJavaTypeDescriptorTest() {
super( LocaleJavaType.INSTANCE );
Expand All @@ -45,18 +46,37 @@ protected boolean isIdentityDifferentFromEquality() {

@Test
public void testConversionFromString() {
assertEquals( toLocale( "de", null, null ), LocaleJavaType.INSTANCE.fromString( "de" ) );
assertEquals( toLocale( "de", "DE", null ), LocaleJavaType.INSTANCE.fromString( "de_DE" ) );
assertEquals( toLocale( null, "DE", null ), LocaleJavaType.INSTANCE.fromString( "_DE" ) );
assertEquals( toLocale( null, null, "ch123" ), LocaleJavaType.INSTANCE.fromString( "__ch123" ) );
assertEquals( toLocale( null, "DE", "ch123" ), LocaleJavaType.INSTANCE.fromString( "_DE_ch123" ) );
assertEquals( toLocale( "de", null, "ch123" ), LocaleJavaType.INSTANCE.fromString( "de__ch123" ) );
assertEquals( toLocale( "de", "DE", "ch123" ), LocaleJavaType.INSTANCE.fromString( "de_DE_ch123" ) );
assertEquals( toLocale( "", "", "" ), LocaleJavaType.INSTANCE.fromString( "" ) );
assertEquals( Locale.ROOT, LocaleJavaType.INSTANCE.fromString( "" ) );
assertLocaleString( toLocale( "de", null, null, null ), "de", "de" );
assertLocaleString( toLocale( "de", "DE", null, null ), "de_DE", "de-DE" );
assertLocaleString( toLocale( null, "DE", null, null ), "_DE", "und-DE" );
assertLocaleString( toLocale( null, "DE", "ch123", null ), "_DE_ch123", "und-DE-ch123" );
assertLocaleString( toLocale( "de", null, "ch123", null ), "de__ch123", "de-ch123" );
assertLocaleString( toLocale( "de", "DE", "ch123", null ), "de_DE_ch123", "de-DE-ch123" );
assertLocaleString( toLocale( "zh", "HK", null, "Hant" ), "zh_HK_#Hant", "zh-Hant-HK" );
assertLocaleString( toLocale( "ja", null, null, null, "u-nu-japanese" ), "ja__#u-nu-japanese", "ja-u-nu-japanese" );
assertLocaleString( toLocale( "ja", null, null, null, "u-nu-japanese", "x-linux" ), "ja__#u-nu-japanese-x-linux", "ja-u-nu-japanese-x-linux" );
assertLocaleString( toLocale( "ja", "JP", null, null, "u-nu-japanese" ), "ja_JP_#u-nu-japanese", "ja-JP-u-nu-japanese" );
assertLocaleString( toLocale( "ja", "JP", null, null, "u-nu-japanese", "x-linux" ), "ja_JP_#u-nu-japanese-x-linux", "ja-JP-u-nu-japanese-x-linux" );
assertLocaleString( toLocale( "ja", "JP", null, "Jpan", "u-nu-japanese" ), "ja_JP_#Jpan_u-nu-japanese", "ja-Jpan-JP-u-nu-japanese" );
assertLocaleString( toLocale( "ja", "JP", null, "Jpan", "u-nu-japanese", "x-linux" ), "ja_JP_#Jpan_u-nu-japanese-x-linux", "ja-Jpan-JP-u-nu-japanese-x-linux" );
// Note that these Locale objects make no sense, since Locale#toString requires at least a language or region
// to produce a non-empty string, but we test parsing that anyway, especially since the language tag now produces a value
assertLocaleString( toLocale( null, null, "ch123", null ), "__ch123", "und-ch123" );
assertLocaleString( toLocale( "", "", "", null ), "", "und" );
assertLocaleString( toLocale( null, null, null, "Hant" ), "___#Hant", "und-Hant" );
assertLocaleString( Locale.ROOT, "", "und" );
}

private static Locale toLocale(String lang, String region, String variant) {
private void assertLocaleString(Locale expectedLocale, String string, String languageTag) {
assertEquals( expectedLocale, LocaleJavaType.INSTANCE.fromString( string ) );
assertEquals( expectedLocale, LocaleJavaType.INSTANCE.fromString( languageTag ) );
assertEquals( expectedLocale.toLanguageTag(), languageTag );
if ( !isEmpty( expectedLocale.getLanguage() ) || !isEmpty( expectedLocale.getCountry() ) ) {
assertEquals( expectedLocale.toString(), string );
}
}

private static Locale toLocale(String lang, String region, String variant, String script, String... extensions) {
final Locale.Builder builder = new Locale.Builder();
if ( StringHelper.isNotEmpty( lang ) ) {
builder.setLanguage( lang );
Expand All @@ -67,6 +87,13 @@ private static Locale toLocale(String lang, String region, String variant) {
if ( StringHelper.isNotEmpty( variant ) ) {
builder.setVariant( variant );
}
if ( StringHelper.isNotEmpty( script ) ) {
builder.setScript( script );
}
for ( String extension : extensions ) {
assert extension.charAt( 1 ) == '-';
builder.setExtension( extension.charAt( 0 ), extension.substring( 2 ) );
}
return builder.build();
}
}
Loading
Loading