diff options
author | Jon Bratseth <bratseth@oath.com> | 2019-04-02 15:03:27 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-02 15:03:27 +0200 |
commit | 6ad65a6ce0b7e56cde45a61db3dfb6314d9e8caf (patch) | |
tree | 86741360c720367644dac01b70985267b5c3c8d8 | |
parent | 67f18277a9d68bb35bca00e25b38378495bcdd6a (diff) | |
parent | 79d41d1d168220c47d1056a7d1db6828602314a5 (diff) |
Merge pull request #8977 from vespa-engine/bratseth/allow-destructive-changes-in-dev
Allow destructive changes in manually deployed zones
13 files changed, 110 insertions, 39 deletions
diff --git a/config-model-api/abi-spec.json b/config-model-api/abi-spec.json index 1f4ecfd3793..1b494379413 100644 --- a/config-model-api/abi-spec.json +++ b/config-model-api/abi-spec.json @@ -209,8 +209,8 @@ "public com.yahoo.config.provision.Environment environment()", "public java.util.Optional region()", "public boolean active()", - "public java.util.Optional athenzService()", "public java.util.Optional testerFlavor()", + "public java.util.Optional athenzService()", "public java.util.List zones()", "public boolean deploysTo(com.yahoo.config.provision.Environment, java.util.Optional)", "public int hashCode()", @@ -487,16 +487,17 @@ ], "methods": [ "public void <init>(java.util.List)", - "public void invalid(com.yahoo.config.application.api.ValidationId, java.lang.String, java.time.Instant)", - "public boolean allows(java.lang.String, java.time.Instant)", + "public final void invalid(com.yahoo.config.application.api.ValidationId, java.lang.String, java.time.Instant)", + "public final boolean allows(java.lang.String, java.time.Instant)", "public boolean allows(com.yahoo.config.application.api.ValidationId, java.time.Instant)", - "public static java.lang.String toAllowMessage(com.yahoo.config.application.api.ValidationId)", "public java.lang.String xmlForm()", + "public static java.lang.String toAllowMessage(com.yahoo.config.application.api.ValidationId)", "public static com.yahoo.config.application.api.ValidationOverrides fromXml(java.io.Reader)", "public static com.yahoo.config.application.api.ValidationOverrides fromXml(java.lang.String)" ], "fields": [ - "public static final com.yahoo.config.application.api.ValidationOverrides empty" + "public static final com.yahoo.config.application.api.ValidationOverrides empty", + "public static final com.yahoo.config.application.api.ValidationOverrides all" ] } }
\ No newline at end of file diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java index 441ef273a6f..e076ffd0f10 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/ValidationOverrides.java @@ -16,6 +16,7 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.logging.Logger; /** * A set of allows which suppresses specific validations in limited time periods. @@ -27,7 +28,12 @@ import java.util.Optional; */ public class ValidationOverrides { - public static final ValidationOverrides empty = new ValidationOverrides(ImmutableList.of(), "<deployment version='1.0'/>"); + private static final Logger log = Logger.getLogger(ValidationOverrides.class.getName()); + + public static final ValidationOverrides empty = new ValidationOverrides(ImmutableList.of(), "<validation-overrides/>"); + + /** A special instance which behaves as if it contained a valid allow override for every (valid) validation id */ + public static final ValidationOverrides all = new AllowAllValidationOverrides(); private final List<Allow> overrides; @@ -44,12 +50,12 @@ public class ValidationOverrides { } /** Throws a ValidationException unless this validation is overridden at this time */ - public void invalid(ValidationId validationId, String message, Instant now) { + public final void invalid(ValidationId validationId, String message, Instant now) { if ( ! allows(validationId, now)) throw new ValidationException(validationId, message); } - public boolean allows(String validationIdString, Instant now) { + public final boolean allows(String validationIdString, Instant now) { Optional<ValidationId> validationId = ValidationId.from(validationIdString); if ( ! validationId.isPresent()) return false; // unknown id -> not allowed return allows(validationId.get(), now); @@ -66,14 +72,14 @@ public class ValidationOverrides { return false; } + /** Returns the XML form of this, or null if it was not created by fromXml, nor is empty */ + public String xmlForm() { return xmlForm; } + public static String toAllowMessage(ValidationId id) { return "To allow this add <allow until='yyyy-mm-dd'>" + id + "</allow> to validation-overrides.xml" + ", see https://docs.vespa.ai/documentation/reference/validation-overrides.html"; } - /** Returns the XML form of this, or null if it was not created by fromXml, nor is empty */ - public String xmlForm() { return xmlForm; } - /** * Returns a ValidationOverrides instance with the content of the given Reader. * @@ -166,4 +172,26 @@ public class ValidationOverrides { } + private static class AllowAllValidationOverrides extends ValidationOverrides { + + public AllowAllValidationOverrides() { + super(List.of()); + } + + /** Returns whether the given (assumed invalid) change is allowed by this at the moment */ + @Override + public boolean allows(ValidationId validationId, Instant now) { + log.warning("Possibly destructive change '" + validationId + "' allowed"); + return true; + } + + /** Returns the XML form of this, or null if it was not created by fromXml, nor is empty */ + @Override + public String xmlForm() { return null; } + + @Override + public String toString() { return "(A validation override which allows everything)"; } + + } + } diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/ValidationOverrideTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/ValidationOverrideTest.java index c17b9a6f220..2943b0bab34 100644 --- a/config-model-api/src/test/java/com/yahoo/config/application/api/ValidationOverrideTest.java +++ b/config-model-api/src/test/java/com/yahoo/config/application/api/ValidationOverrideTest.java @@ -8,10 +8,12 @@ import org.xml.sax.SAXException; import java.io.IOException; import java.io.StringReader; +import java.time.Clock; import java.time.Instant; import java.util.Optional; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * @author bratseth @@ -19,7 +21,7 @@ import static org.junit.Assert.assertEquals; public class ValidationOverrideTest { @Test - public void testValidationOverridesInIsolation() throws IOException, SAXException { + public void testValidationOverridesInIsolation() { String validationOverrides = "<validation-overrides>" + " <allow until='2000-01-01'>indexing-change</allow>" + @@ -55,7 +57,7 @@ public class ValidationOverrideTest { } @Test - public void testInvalidOverridePeriod() throws IOException, SAXException { + public void testInvalidOverridePeriod() { String validationOverrides = "<validation-overrides>" + " <allow until='2000-02-02'>indexing-change</allow>" + @@ -80,6 +82,15 @@ public class ValidationOverrideTest { assertEquals(empty.xmlForm(), emptyReserialized.xmlForm()); } + @Test + public void testAll() { + ValidationOverrides all = ValidationOverrides.all; + assertTrue(all.allows(ValidationId.deploymentRemoval, Clock.systemUTC().instant())); + assertTrue(all.allows(ValidationId.contentClusterRemoval, Clock.systemUTC().instant())); + assertTrue(all.allows(ValidationId.indexModeChange, Clock.systemUTC().instant())); + assertTrue(all.allows(ValidationId.fieldTypeChange, Clock.systemUTC().instant())); + } + private void assertOverridden(String validationId, ValidationOverrides overrides, Instant now) { overrides.invalid(ValidationId.from(validationId).get(), "message", now); // should not throw exception } diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java b/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java index 910eff065d1..7a981cd6a53 100644 --- a/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java +++ b/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java @@ -121,7 +121,12 @@ public class DeployState implements ConfigDefinitionStore { this.importedModels = new ImportedMlModels(applicationPackage.getFileReference(ApplicationPackage.MODELS_DIR), modelImporters); - this.validationOverrides = applicationPackage.getValidationOverrides().map(ValidationOverrides::fromXml).orElse(ValidationOverrides.empty); + this.validationOverrides = + zone.environment().isManuallyDeployed() + ? ValidationOverrides.all // Don't protect manually deployed zones + : applicationPackage.getValidationOverrides().map(ValidationOverrides::fromXml) + .orElse(ValidationOverrides.empty); + this.wantedNodeVespaVersion = wantedNodeVespaVersion; this.now = now; } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java index a95ae1d1706..3f6b5dac2dc 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java @@ -9,6 +9,11 @@ import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.deploy.TestProperties; import com.yahoo.config.model.provision.InMemoryProvisioner; import com.yahoo.config.model.test.MockApplicationPackage; +import com.yahoo.config.provision.CloudName; +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Zone; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg; @@ -43,10 +48,14 @@ public class ValidationTester { * * @param previousModel the previous model, or null if no previous * @param services the services file content + * @param environment the environment this deploys to * @param validationOverrides the validation overrides file content, or null if none * @return the new model and any change actions */ - public Pair<VespaModel, List<ConfigChangeAction>> deploy(VespaModel previousModel, String services, String validationOverrides) { + public Pair<VespaModel, List<ConfigChangeAction>> deploy(VespaModel previousModel, + String services, + Environment environment, + String validationOverrides) { Instant now = LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE).atStartOfDay().atZone(ZoneOffset.UTC).toInstant(); ApplicationPackage newApp = new MockApplicationPackage.Builder() .withServices(services) @@ -55,6 +64,10 @@ public class ValidationTester { .build(); VespaModelCreatorWithMockPkg newModelCreator = new VespaModelCreatorWithMockPkg(newApp); DeployState.Builder deployStateBuilder = new DeployState.Builder() + .zone(new Zone(CloudName.defaultName(), + SystemName.defaultSystem(), + environment, + RegionName.defaultName())) .applicationPackage(newApp) .properties(new TestProperties().setHostedVespa(true)) .modelHostProvisioner(new InMemoryProvisioner(nodeCount)) diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java index 4c3583ba0ae..bce28dd9236 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java @@ -5,6 +5,7 @@ import com.yahoo.config.application.api.ValidationId; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.model.api.ConfigChangeAction; import com.yahoo.config.model.api.ConfigChangeRefeedAction; +import com.yahoo.config.provision.Environment; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.application.validation.ValidationTester; import com.yahoo.vespa.model.search.AbstractSearchCluster; @@ -29,9 +30,9 @@ public class ClusterSizeReductionValidatorTest { public void testSizeReductionValidation() throws IOException, SAXException { ValidationTester tester = new ValidationTester(30); - VespaModel previous = tester.deploy(null, getServices(30), null).getFirst(); + VespaModel previous = tester.deploy(null, getServices(30), Environment.prod, null).getFirst(); try { - tester.deploy(previous, getServices(14), null); + tester.deploy(previous, getServices(14), Environment.prod, null); fail("Expected exception due to cluster size reduction"); } catch (IllegalArgumentException expected) { @@ -45,8 +46,8 @@ public class ClusterSizeReductionValidatorTest { public void testSizeReductionValidationMinimalDecreaseIsAllowed() throws IOException, SAXException { ValidationTester tester = new ValidationTester(30); - VespaModel previous = tester.deploy(null, getServices(3), null).getFirst(); - tester.deploy(previous, getServices(2), null); + VespaModel previous = tester.deploy(null, getServices(3), Environment.prod, null).getFirst(); + tester.deploy(previous, getServices(2), Environment.prod, null); } /* @@ -63,8 +64,8 @@ public class ClusterSizeReductionValidatorTest { public void testOverridingSizereductionValidation() throws IOException, SAXException { ValidationTester tester = new ValidationTester(30); - VespaModel previous = tester.deploy(null, getServices(30), null).getFirst(); - tester.deploy(previous, getServices(14), sizeReductionOverride); // Allowed due to override + VespaModel previous = tester.deploy(null, getServices(30), Environment.prod, null).getFirst(); + tester.deploy(previous, getServices(14), Environment.prod, sizeReductionOverride); // Allowed due to override } private static String getServices(int size) { diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java index ee58ca67b02..84b50b37146 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.model.application.validation.change; import com.yahoo.config.application.api.ValidationId; import com.yahoo.config.application.api.ValidationOverrides; +import com.yahoo.config.provision.Environment; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.application.validation.ValidationTester; import com.yahoo.yolean.Exceptions; @@ -20,9 +21,9 @@ public class ContentClusterRemovalValidatorTest { public void testContentRemovalValidation() { ValidationTester tester = new ValidationTester(); - VespaModel previous = tester.deploy(null, getServices("contentClusterId"), null).getFirst(); + VespaModel previous = tester.deploy(null, getServices("contentClusterId"), Environment.prod, null).getFirst(); try { - tester.deploy(previous, getServices("newContentClusterId"), null); + tester.deploy(previous, getServices("newContentClusterId"), Environment.prod, null); fail("Expected exception due to content cluster id change"); } catch (IllegalArgumentException expected) { @@ -36,8 +37,8 @@ public class ContentClusterRemovalValidatorTest { public void testOverridingContentRemovalValidation() { ValidationTester tester = new ValidationTester(); - VespaModel previous = tester.deploy(null, getServices("contentClusterId"), null).getFirst(); - tester.deploy(previous, getServices("newContentClusterId"), removalOverride); // Allowed due to override + VespaModel previous = tester.deploy(null, getServices("contentClusterId"), Environment.prod, null).getFirst(); + tester.deploy(previous, getServices("newContentClusterId"), Environment.prod, removalOverride); // Allowed due to override } private static String getServices(String contentClusterId) { diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentTypeRemovalValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentTypeRemovalValidatorTest.java index ca45520711e..c8fdb8348c3 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentTypeRemovalValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentTypeRemovalValidatorTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.model.application.validation.change; import com.yahoo.config.application.api.ValidationId; import com.yahoo.config.application.api.ValidationOverrides; +import com.yahoo.config.provision.Environment; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.application.validation.ValidationTester; import com.yahoo.yolean.Exceptions; @@ -23,9 +24,9 @@ public class ContentTypeRemovalValidatorTest { public void testContentTypeRemovalValidation() { ValidationTester tester = new ValidationTester(); - VespaModel previous = tester.deploy(null, getServices("music"), null).getFirst(); + VespaModel previous = tester.deploy(null, getServices("music"), Environment.prod, null).getFirst(); try { - tester.deploy(previous, getServices("book"), null); + tester.deploy(previous, getServices("book"), Environment.prod, null); fail("Expected exception due to removal of context type 'music"); } catch (IllegalArgumentException expected) { @@ -40,8 +41,16 @@ public class ContentTypeRemovalValidatorTest { public void testOverridingContentTypeRemovalValidation() { ValidationTester tester = new ValidationTester(); - VespaModel previous = tester.deploy(null, getServices("music"), null).getFirst(); - tester.deploy(previous, getServices("book"), removalOverride); // Allowed due to override + VespaModel previous = tester.deploy(null, getServices("music"), Environment.prod, null).getFirst(); + tester.deploy(previous, getServices("book"), Environment.prod, removalOverride); // Allowed due to override + } + + @Test + public void testNoOverrideNeededinDev() { + ValidationTester tester = new ValidationTester(); + + VespaModel previous = tester.deploy(null, getServices("music"), Environment.prod, null).getFirst(); + tester.deploy(previous, getServices("book"), Environment.dev, null); } private static String getServices(String documentType) { diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/GlobalDocumentChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/GlobalDocumentChangeValidatorTest.java index 65423ad1333..bc6863c9a7c 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/GlobalDocumentChangeValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/GlobalDocumentChangeValidatorTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.model.application.validation.change; import com.yahoo.config.model.api.ConfigChangeAction; import com.yahoo.config.model.api.ConfigChangeRefeedAction; +import com.yahoo.config.provision.Environment; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.application.validation.ValidationTester; import org.junit.Test; @@ -33,9 +34,9 @@ public class GlobalDocumentChangeValidatorTest { private void testChangeGlobalAttribute(boolean allowed, boolean oldGlobal, boolean newGlobal, String validationOverrides) { ValidationTester tester = new ValidationTester(); - VespaModel oldModel = tester.deploy(null, getServices(oldGlobal), validationOverrides).getFirst(); + VespaModel oldModel = tester.deploy(null, getServices(oldGlobal), Environment.prod, validationOverrides).getFirst(); try { - tester.deploy(oldModel, getServices(newGlobal), validationOverrides).getSecond(); + tester.deploy(oldModel, getServices(newGlobal), Environment.prod, validationOverrides).getSecond(); assertTrue(allowed); } catch (IllegalStateException e) { assertFalse(allowed); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidatorTest.java index e9979119d84..cca112f3bd2 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidatorTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.model.application.validation.change; import com.yahoo.config.model.api.ConfigChangeAction; import com.yahoo.config.model.api.ConfigChangeRefeedAction; +import com.yahoo.config.provision.Environment; import com.yahoo.vespa.model.VespaModel; import com.yahoo.vespa.model.application.validation.ValidationTester; import com.yahoo.vespa.model.search.AbstractSearchCluster; @@ -26,9 +27,9 @@ public class IndexingModeChangeValidatorTest { ValidationTester tester = new ValidationTester(); VespaModel oldModel = - tester.deploy(null, getServices(AbstractSearchCluster.IndexingMode.REALTIME), validationOverrides).getFirst(); + tester.deploy(null, getServices(AbstractSearchCluster.IndexingMode.REALTIME), Environment.prod, validationOverrides).getFirst(); List<ConfigChangeAction> changeActions = - tester.deploy(oldModel, getServices(AbstractSearchCluster.IndexingMode.STREAMING), validationOverrides).getSecond(); + tester.deploy(oldModel, getServices(AbstractSearchCluster.IndexingMode.STREAMING), Environment.prod, validationOverrides).getSecond(); assertRefeedChange(true, // allowed=true due to validation override "Cluster 'default' changed indexing mode from 'indexed' to 'streaming'", diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java index 172d9c025d5..61f5e4f1230 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java @@ -90,11 +90,11 @@ public class RemoteSession extends Session { void makeActive(ReloadHandler reloadHandler) { Curator.CompletionWaiter waiter = zooKeeperClient.getActiveWaiter(); - log.log(LogLevel.DEBUG, logPre() + "Getting session from repo: " + getSessionId()); + log.log(LogLevel.DEBUG, () -> logPre() + "Getting session from repo: " + getSessionId()); ApplicationSet app = ensureApplicationLoaded(); - log.log(LogLevel.DEBUG, logPre() + "Reloading config for " + getSessionId()); + log.log(LogLevel.DEBUG, () -> logPre() + "Reloading config for " + getSessionId()); reloadHandler.reloadConfig(app); - log.log(LogLevel.DEBUG, logPre() + "Notifying " + waiter); + log.log(LogLevel.DEBUG, () -> logPre() + "Notifying " + waiter); notifyCompletion(waiter); log.log(LogLevel.INFO, logPre() + "Session activated: " + getSessionId()); } diff --git a/container-search/src/main/java/com/yahoo/search/query/Model.java b/container-search/src/main/java/com/yahoo/search/query/Model.java index a874ed45e30..d2f59e0710e 100644 --- a/container-search/src/main/java/com/yahoo/search/query/Model.java +++ b/container-search/src/main/java/com/yahoo/search/query/Model.java @@ -170,8 +170,8 @@ public class Model implements Cloneable { } /** - * <p>Explicitly sets the locale to be used during parsing. This method also calls {@link #setLanguage(Language)} - * with the corresponding {@link Language} instance.</p> + * Explicitly sets the locale to be used during parsing. This method also calls {@link #setLanguage(Language)} + * with the corresponding {@link Language} instance. * * @param locale the locale to set * @see #getLocale() diff --git a/linguistics/src/main/java/com/yahoo/language/LocaleFactory.java b/linguistics/src/main/java/com/yahoo/language/LocaleFactory.java index 39345399ea5..2760f9e673e 100644 --- a/linguistics/src/main/java/com/yahoo/language/LocaleFactory.java +++ b/linguistics/src/main/java/com/yahoo/language/LocaleFactory.java @@ -18,7 +18,7 @@ public final class LocaleFactory { * Implements a simple parser for RFC5646 language tags. The language tag is parsed into a Locale. * * @param tag The language tag to parse. - * @return The corrseponding Locale. + * @return The corresponding Locale. */ @SuppressWarnings("ConstantConditions") public static Locale fromLanguageTag(String tag) { |