001/* 002 * Copyright 2023 the original author or authors. 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.tools.lang; 017 018import java.util.Locale; 019 020import de.cuioss.tools.string.MoreStrings; 021import lombok.experimental.UtilityClass; 022 023/** 024 * <p> 025 * Operations to assist when working with a {@link Locale}. 026 * </p> 027 * 028 * <p> 029 * This class tries to handle {@code null} input gracefully. An exception will 030 * not be thrown for a {@code null} input. Each method documents its behavior in 031 * more detail. 032 * </p> 033 * 034 * @author https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/LocaleUtils.java 035 * 036 */ 037@UtilityClass 038public class LocaleUtils { 039 040 private static final String INVALID_LOCALE_FORMAT = "Invalid locale format: "; 041 042 /** 043 * <p> 044 * Converts a String to a Locale. 045 * </p> 046 * 047 * <p> 048 * This method takes the string format of a locale and creates the locale object 049 * from it. 050 * </p> 051 * 052 * <pre> 053 * LocaleUtils.toLocale("") = new Locale("", "") 054 * LocaleUtils.toLocale("en") = new Locale("en", "") 055 * LocaleUtils.toLocale("en_GB") = new Locale("en", "GB") 056 * LocaleUtils.toLocale("en_001") = new Locale("en", "001") 057 * LocaleUtils.toLocale("en_GB_xxx") = new Locale("en", "GB", "xxx") (#) 058 * </pre> 059 * 060 * <p> 061 * This method validates the input strictly. The language code must be 062 * lowercase. The country code must be uppercase. The separator must be an 063 * underscore. The length must be correct. 064 * </p> 065 * 066 * @param str the locale String to convert, null returns null 067 * @return a Locale, null if null input 068 * @throws IllegalArgumentException if the string is an invalid format 069 * @see Locale#forLanguageTag(String) 070 */ 071 @SuppressWarnings("squid:S3776") // owolff: Original code 072 public static Locale toLocale(final String str) { 073 if (str == null) { 074 return null; 075 } 076 if (str.isEmpty()) { // LANG-941 - JDK 8 introduced an empty locale where all fields are 077 // blank 078 return new Locale(MoreStrings.EMPTY, MoreStrings.EMPTY); 079 } 080 if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions 081 throw new IllegalArgumentException(INVALID_LOCALE_FORMAT + str); 082 } 083 final var len = str.length(); 084 if (len < 2) { 085 throw new IllegalArgumentException(INVALID_LOCALE_FORMAT + str); 086 } 087 final var ch0 = str.charAt(0); 088 if (ch0 == '_') { 089 if (len < 3) { 090 throw new IllegalArgumentException(INVALID_LOCALE_FORMAT + str); 091 } 092 final var ch1 = str.charAt(1); 093 final var ch2 = str.charAt(2); 094 if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) { 095 throw new IllegalArgumentException(INVALID_LOCALE_FORMAT + str); 096 } 097 if (len == 3) { 098 return new Locale(MoreStrings.EMPTY, str.substring(1, 3)); 099 } 100 if (len < 5 || str.charAt(3) != '_') { 101 throw new IllegalArgumentException(INVALID_LOCALE_FORMAT + str); 102 } 103 return new Locale(MoreStrings.EMPTY, str.substring(1, 3), str.substring(4)); 104 } 105 106 return parseLocale(str); 107 } 108 109 /** 110 * Tries to parse a locale from the given String. 111 * 112 * @param str the String to parse a locale from. 113 * 114 * @return a Locale instance parsed from the given String. 115 * @throws IllegalArgumentException if the given String can not be parsed. 116 */ 117 private static Locale parseLocale(final String str) { 118 if (isISO639LanguageCode(str)) { 119 return new Locale(str); 120 } 121 122 final var segments = str.split("_", -1); 123 final var language = segments[0]; 124 if (segments.length == 2) { 125 final var country = segments[1]; 126 if (isISO639LanguageCode(language) && isISO3166CountryCode(country) || isNumericAreaCode(country)) { 127 return new Locale(language, country); 128 } 129 } else if (segments.length == 3) { 130 final var country = segments[1]; 131 final var variant = segments[2]; 132 if (isISO639LanguageCode(language) 133 && (country.isEmpty() || isISO3166CountryCode(country) || isNumericAreaCode(country)) 134 && !variant.isEmpty()) { 135 return new Locale(language, country, variant); 136 } 137 } 138 throw new IllegalArgumentException(INVALID_LOCALE_FORMAT + str); 139 } 140 141 /** 142 * Checks whether the given String is a ISO 639 compliant language code. 143 * 144 * @param str the String to check. 145 * 146 * @return true, if the given String is a ISO 639 compliant language code. 147 */ 148 private static boolean isISO639LanguageCode(final String str) { 149 return MoreStrings.isAllLowerCase(str) && (str.length() == 2 || str.length() == 3); 150 } 151 152 /** 153 * Checks whether the given String is a ISO 3166 alpha-2 country code. 154 * 155 * @param str the String to check 156 * 157 * @return true, is the given String is a ISO 3166 compliant country code. 158 */ 159 private static boolean isISO3166CountryCode(final String str) { 160 return MoreStrings.isAllUpperCase(str) && str.length() == 2; 161 } 162 163 /** 164 * Checks whether the given String is a UN M.49 numeric area code. 165 * 166 * @param str the String to check 167 * 168 * @return true, is the given String is a UN M.49 numeric area code. 169 */ 170 private static boolean isNumericAreaCode(final String str) { 171 return MoreStrings.isNumeric(str) && str.length() == 3; 172 } 173}