From 27b78109185c4d6a0afee3fa638bdfceb50f3e63 Mon Sep 17 00:00:00 2001 From: abin <57697211+ABin-Huang@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:52:25 +0800 Subject: [PATCH 1/7] Add i18n support for description and display-name elements in web.xml Add a LocaleElement class to capture the content of locale-aware deployment descriptor elements together with their optional xml:lang attribute and use it to support multiple description and display-name elements with language variants throughout the descriptor model. --- .../descriptor/web/LocalStrings.properties | 1 + .../util/descriptor/web/LocaleElement.java | 122 ++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 java/org/apache/tomcat/util/descriptor/web/LocaleElement.java diff --git a/java/org/apache/tomcat/util/descriptor/web/LocalStrings.properties b/java/org/apache/tomcat/util/descriptor/web/LocalStrings.properties index f9f3b0366f98..40f9e57d09cd 100644 --- a/java/org/apache/tomcat/util/descriptor/web/LocalStrings.properties +++ b/java/org/apache/tomcat/util/descriptor/web/LocalStrings.properties @@ -40,6 +40,7 @@ webXml.duplicateResourceEnvRef=Duplicate resource-env-ref name [{0}] webXml.duplicateResourceRef=Duplicate resource-ref name [{0}] webXml.duplicateServletMapping=The servlets named [{0}] and [{1}] are both mapped to the url-pattern [{2}] which is not permitted webXml.duplicateTaglibUri=Duplicate tag library URI [{0}] +webXml.mergeConflictDescription=The description was defined in multiple fragments with different values including fragment with name [{0}] located at [{1}] webXml.mergeConflictDisplayName=The display name was defined in multiple fragments with different values including fragment with name [{0}] located at [{1}] webXml.mergeConflictFilter=The Filter [{0}] was defined inconsistently in multiple fragments including fragment with name [{1}] located at [{2}] webXml.mergeConflictLoginConfig=A LoginConfig was defined inconsistently in multiple fragments including fragment with name [{0}] located at [{1}] diff --git a/java/org/apache/tomcat/util/descriptor/web/LocaleElement.java b/java/org/apache/tomcat/util/descriptor/web/LocaleElement.java new file mode 100644 index 000000000000..d128bc0eea99 --- /dev/null +++ b/java/org/apache/tomcat/util/descriptor/web/LocaleElement.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.util.descriptor.web; + +import java.io.Serial; +import java.io.Serializable; + +/** + * Represents an element of a deployment descriptor that supports internationalization (i18n) via the optional + * {@code xml:lang} attribute, for example {@code } and {@code }. + *

+ * The {@code content} holds the element body text. The {@code lang} holds the value of the optional {@code xml:lang} + * attribute, or {@code null} if the attribute is absent. An element without a {@code lang} is the default element + * that applies when no language specific element matches. + *

+ */ +public class LocaleElement implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * The content of the element. + */ + private final String content; + + /** + * The value of the {@code xml:lang} attribute, or {@code null} if not specified. + */ + private final String lang; + + /** + * Creates a new LocaleElement. + * + * @param content The element content + * @param lang The value of the {@code xml:lang} attribute, or {@code null} if not specified + */ + public LocaleElement(String content, String lang) { + this.content = content; + this.lang = lang; + } + + /** + * Returns the content of the element. + * + * @return The element content + */ + public String getContent() { + return content; + } + + /** + * Returns the value of the {@code xml:lang} attribute, or {@code null} if not specified. + * + * @return The {@code xml:lang} attribute value + */ + public String getLang() { + return lang; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((content == null) ? 0 : content.hashCode()); + result = prime * result + ((lang == null) ? 0 : lang.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + LocaleElement other = (LocaleElement) obj; + if (content == null) { + if (other.content != null) { + return false; + } + } else if (!content.equals(other.content)) { + return false; + } + if (lang == null) { + return other.lang == null; + } else { + return lang.equals(other.lang); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("LocaleElement["); + sb.append("content="); + sb.append(content); + if (lang != null) { + sb.append(", lang="); + sb.append(lang); + } + sb.append(']'); + return sb.toString(); + } +} From 6e038895188a333bd74c7807b7554dda1581692c Mon Sep 17 00:00:00 2001 From: abin <57697211+ABin-Huang@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:53:36 +0800 Subject: [PATCH 2/7] Add i18n support for description and display-name elements in web.xml Add a LocaleElement class to capture the content of locale-aware deployment descriptor elements together with their optional xml:lang attribute and use it to support multiple description and display-name elements with language variants throughout the descriptor model. This commit updates the value classes: SecurityRoleRef (new description support), SecurityCollection, MessageDestination and ContextService. --- .../util/descriptor/web/ContextService.java | 55 ++++++++++++----- .../descriptor/web/MessageDestination.java | 53 ++++++++++++----- .../descriptor/web/SecurityCollection.java | 49 ++++++++++++--- .../util/descriptor/web/SecurityRoleRef.java | 59 +++++++++++++++++++ 4 files changed, 180 insertions(+), 36 deletions(-) diff --git a/java/org/apache/tomcat/util/descriptor/web/ContextService.java b/java/org/apache/tomcat/util/descriptor/web/ContextService.java index 80c0852494be..6ccfab68948c 100644 --- a/java/org/apache/tomcat/util/descriptor/web/ContextService.java +++ b/java/org/apache/tomcat/util/descriptor/web/ContextService.java @@ -17,9 +17,11 @@ package org.apache.tomcat.util.descriptor.web; import java.io.Serial; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; @@ -42,26 +44,55 @@ public ContextService() { /** - * The WebService reference name. + * The WebService reference display names. Multiple display names, each with an optional language, are supported as + * per the deployment descriptor specification. */ - private String displayname = null; + private final List displaynames = new ArrayList<>(); /** - * Returns the WebService reference display name. + * Returns the WebService reference display names. + * + * @return the display names + */ + public List getDisplaynames() { + return displaynames; + } + + /** + * Adds a WebService reference display name. + * + * @param displayname the display name to add + */ + public void addDisplayname(LocaleElement displayname) { + displaynames.add(displayname); + } + + /** + * Returns the WebService reference display name. The default display name (the one without a language) is + * returned if present, otherwise the first display name is returned. * * @return the display name */ public String getDisplayname() { - return this.displayname; + for (LocaleElement element : displaynames) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return displaynames.isEmpty() ? null : displaynames.get(0).getContent(); } /** - * Sets the WebService reference display name. + * Sets the WebService reference display name. Any existing display names, including language specific ones, are + * replaced by a single default display name. * * @param displayname the display name */ public void setDisplayname(String displayname) { - this.displayname = displayname; + displaynames.clear(); + if (displayname != null) { + displaynames.add(new LocaleElement(displayname, null)); + } } /** @@ -352,9 +383,9 @@ public String toString() { sb.append(", type="); sb.append(getType()); } - if (displayname != null) { + if (getDisplayname() != null) { sb.append(", displayname="); - sb.append(displayname); + sb.append(getDisplayname()); } if (largeIcon != null) { sb.append(", largeIcon="); @@ -403,7 +434,7 @@ public String toString() { public int hashCode() { final int prime = 31; int result = super.hashCode(); - result = prime * result + ((displayname == null) ? 0 : displayname.hashCode()); + result = prime * result + displaynames.hashCode(); result = prime * result + handlers.hashCode(); result = prime * result + ((jaxrpcmappingfile == null) ? 0 : jaxrpcmappingfile.hashCode()); result = prime * result + ((largeIcon == null) ? 0 : largeIcon.hashCode()); @@ -427,11 +458,7 @@ public boolean equals(Object obj) { return false; } ContextService other = (ContextService) obj; - if (displayname == null) { - if (other.displayname != null) { - return false; - } - } else if (!displayname.equals(other.displayname)) { + if (!displaynames.equals(other.displaynames)) { return false; } if (!handlers.equals(other.handlers)) { diff --git a/java/org/apache/tomcat/util/descriptor/web/MessageDestination.java b/java/org/apache/tomcat/util/descriptor/web/MessageDestination.java index 277ab76ef305..85ce954caada 100644 --- a/java/org/apache/tomcat/util/descriptor/web/MessageDestination.java +++ b/java/org/apache/tomcat/util/descriptor/web/MessageDestination.java @@ -18,6 +18,8 @@ import java.io.Serial; +import java.util.ArrayList; +import java.util.List; /** *

@@ -42,24 +44,51 @@ public MessageDestination() { /** - * The display name of this destination. + * The display names of this destination. Multiple display names, each with an optional language, are supported as + * per the deployment descriptor specification. */ - private String displayName = null; + private final List displayNames = new ArrayList<>(); /** - * Get the display name. + * Get the display names. + * @return the display names + */ + public List getDisplayNames() { + return displayNames; + } + + /** + * Add a display name to this destination. + * @param displayName the display name to add + */ + public void addDisplayName(LocaleElement displayName) { + displayNames.add(displayName); + } + + /** + * Get the display name. The default display name (the one without a language) is returned if present, otherwise + * the first display name is returned. * @return the display name */ public String getDisplayName() { - return this.displayName; + for (LocaleElement element : displayNames) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return displayNames.isEmpty() ? null : displayNames.get(0).getContent(); } /** - * Set the display name. + * Set the display name. Any existing display names, including language specific ones, are replaced by a single + * default display name. * @param displayName the display name */ public void setDisplayName(String displayName) { - this.displayName = displayName; + displayNames.clear(); + if (displayName != null) { + displayNames.add(new LocaleElement(displayName, null)); + } } @@ -118,9 +147,9 @@ public String toString() { StringBuilder sb = new StringBuilder("MessageDestination["); sb.append("name="); sb.append(getName()); - if (displayName != null) { + if (getDisplayName() != null) { sb.append(", displayName="); - sb.append(displayName); + sb.append(getDisplayName()); } if (largeIcon != null) { sb.append(", largeIcon="); @@ -143,7 +172,7 @@ public String toString() { public int hashCode() { final int prime = 31; int result = super.hashCode(); - result = prime * result + ((displayName == null) ? 0 : displayName.hashCode()); + result = prime * result + displayNames.hashCode(); result = prime * result + ((largeIcon == null) ? 0 : largeIcon.hashCode()); result = prime * result + ((smallIcon == null) ? 0 : smallIcon.hashCode()); return result; @@ -162,11 +191,7 @@ public boolean equals(Object obj) { return false; } MessageDestination other = (MessageDestination) obj; - if (displayName == null) { - if (other.displayName != null) { - return false; - } - } else if (!displayName.equals(other.displayName)) { + if (!displayNames.equals(other.displayNames)) { return false; } if (largeIcon == null) { diff --git a/java/org/apache/tomcat/util/descriptor/web/SecurityCollection.java b/java/org/apache/tomcat/util/descriptor/web/SecurityCollection.java index 7fdadc92f135..9ccaa38a222a 100644 --- a/java/org/apache/tomcat/util/descriptor/web/SecurityCollection.java +++ b/java/org/apache/tomcat/util/descriptor/web/SecurityCollection.java @@ -18,7 +18,9 @@ import java.io.Serial; import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; /** @@ -66,9 +68,10 @@ public SecurityCollection(String name, String description) { /** - * Description of this web resource collection. + * The descriptions of this web resource collection. Multiple descriptions, each with an optional language, are + * supported as per the deployment descriptor specification. */ - private String description = null; + private final List descriptions = new ArrayList<>(); /** @@ -103,22 +106,52 @@ public SecurityCollection(String name, String description) { /** - * Get the description of this web resource collection. + * Get the descriptions of this web resource collection. + * + * @return the descriptions of this web resource collection + */ + public List getDescriptions() { + return descriptions; + } + + + /** + * Add a description to this web resource collection. + * + * @param description The description to add + */ + public void addDescription(LocaleElement description) { + descriptions.add(description); + } + + + /** + * Get the description of this web resource collection. The default description (the one without a language) is + * returned if present, otherwise the first description is returned. * * @return the description of this web resource collection */ public String getDescription() { - return this.description; + for (LocaleElement element : descriptions) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return descriptions.isEmpty() ? null : descriptions.get(0).getContent(); } /** - * Set the description of this web resource collection. + * Set the description of this web resource collection. Any existing descriptions, including language specific + * ones, are replaced by a single default description. * * @param description The new description */ public void setDescription(String description) { - this.description = description; + descriptions.clear(); + if (description != null) { + descriptions.add(new LocaleElement(description, null)); + } } @@ -389,9 +422,9 @@ public void removePattern(String pattern) { public String toString() { StringBuilder sb = new StringBuilder("SecurityCollection["); sb.append(name); - if (description != null) { + if (getDescription() != null) { sb.append(", "); - sb.append(description); + sb.append(getDescription()); } sb.append(']'); return sb.toString(); diff --git a/java/org/apache/tomcat/util/descriptor/web/SecurityRoleRef.java b/java/org/apache/tomcat/util/descriptor/web/SecurityRoleRef.java index c0ca4b9e17b1..61e0e556709c 100644 --- a/java/org/apache/tomcat/util/descriptor/web/SecurityRoleRef.java +++ b/java/org/apache/tomcat/util/descriptor/web/SecurityRoleRef.java @@ -18,6 +18,8 @@ import java.io.Serial; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; @@ -91,6 +93,59 @@ public void setLink(String link) { } + /** + * The descriptions of this security role reference. Multiple descriptions, each with an optional language, are + * supported as per the deployment descriptor specification. + */ + private final List descriptions = new ArrayList<>(); + + /** + * Returns the descriptions of this security role reference. + * + * @return The descriptions + */ + public List getDescriptions() { + return descriptions; + } + + /** + * Adds a description to this security role reference. + * + * @param description The description to add + */ + public void addDescription(LocaleElement description) { + descriptions.add(description); + } + + /** + * Returns the description of this security role reference. The default description (the one without a language) is + * returned if present, otherwise the first description is returned. + * + * @return The description + */ + public String getDescription() { + for (LocaleElement element : descriptions) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return descriptions.isEmpty() ? null : descriptions.get(0).getContent(); + } + + /** + * Sets the description of this security role reference. Any existing descriptions, including language specific + * ones, are replaced by a single default description. + * + * @param description The description + */ + public void setDescription(String description) { + descriptions.clear(); + if (description != null) { + descriptions.add(new LocaleElement(description, null)); + } + } + + // --------------------------------------------------------- Public Methods @@ -115,6 +170,7 @@ public String toString() { public int hashCode() { final int prime = 31; int result = 1; + result = prime * result + descriptions.hashCode(); result = prime * result + ((link == null) ? 0 : link.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; @@ -130,6 +186,9 @@ public boolean equals(Object obj) { return false; } SecurityRoleRef other = (SecurityRoleRef) obj; + if (!descriptions.equals(other.descriptions)) { + return false; + } if (!Objects.equals(link, other.link)) { return false; } From 95708b22b0773ce05e2ad9bcab1f8fec0e7de850 Mon Sep 17 00:00:00 2001 From: abin <57697211+ABin-Huang@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:54:43 +0800 Subject: [PATCH 3/7] Add i18n support for description and display-name elements in web.xml Update ResourceBase (descriptions for env-entry, ejb-ref, ejb-local-ref, service-ref, resource-ref, resource-env-ref, message-destination-ref and message-destination), FilterDef and ServletDef to store locale-aware LocaleElement lists while preserving the String based compatibility getters and setters. --- .../tomcat/util/descriptor/web/FilterDef.java | 84 ++++++++++++++++--- .../util/descriptor/web/ResourceBase.java | 49 ++++++++--- .../util/descriptor/web/ServletDef.java | 84 ++++++++++++++++--- 3 files changed, 181 insertions(+), 36 deletions(-) diff --git a/java/org/apache/tomcat/util/descriptor/web/FilterDef.java b/java/org/apache/tomcat/util/descriptor/web/FilterDef.java index 01e6af030a40..ce112fd6fb45 100644 --- a/java/org/apache/tomcat/util/descriptor/web/FilterDef.java +++ b/java/org/apache/tomcat/util/descriptor/web/FilterDef.java @@ -18,7 +18,9 @@ import java.io.Serial; import java.io.Serializable; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import jakarta.servlet.Filter; @@ -48,50 +50,108 @@ public FilterDef() { /** - * The description of this filter. + * The descriptions of this filter. Multiple descriptions, each with an optional language, are supported as per the + * deployment descriptor specification. */ - private String description = null; + private final List descriptions = new ArrayList<>(); /** - * Returns the description of this filter. + * Returns the descriptions of this filter. + * + * @return The descriptions + */ + public List getDescriptions() { + return descriptions; + } + + /** + * Adds a description to this filter. + * + * @param description The description to add + */ + public void addDescription(LocaleElement description) { + descriptions.add(description); + } + + /** + * Returns the description of this filter. The default description (the one without a language) is returned if + * present, otherwise the first description is returned. * * @return The description */ public String getDescription() { - return this.description; + for (LocaleElement element : descriptions) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return descriptions.isEmpty() ? null : descriptions.get(0).getContent(); } /** - * Sets the description of this filter. + * Sets the description of this filter. Any existing descriptions, including language specific ones, are replaced + * by a single default description. * * @param description The new description */ public void setDescription(String description) { - this.description = description; + descriptions.clear(); + if (description != null) { + descriptions.add(new LocaleElement(description, null)); + } } /** - * The display name of this filter. + * The display names of this filter. Multiple display names, each with an optional language, are supported as per + * the deployment descriptor specification. */ - private String displayName = null; + private final List displayNames = new ArrayList<>(); /** - * Returns the display name of this filter. + * Returns the display names of this filter. + * + * @return The display names + */ + public List getDisplayNames() { + return displayNames; + } + + /** + * Adds a display name to this filter. + * + * @param displayName The display name to add + */ + public void addDisplayName(LocaleElement displayName) { + displayNames.add(displayName); + } + + /** + * Returns the display name of this filter. The default display name (the one without a language) is returned if + * present, otherwise the first display name is returned. * * @return The display name */ public String getDisplayName() { - return this.displayName; + for (LocaleElement element : displayNames) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return displayNames.isEmpty() ? null : displayNames.get(0).getContent(); } /** - * Sets the display name of this filter. + * Sets the display name of this filter. Any existing display names, including language specific ones, are replaced + * by a single default display name. * * @param displayName The new display name */ public void setDisplayName(String displayName) { - this.displayName = displayName; + displayNames.clear(); + if (displayName != null) { + displayNames.add(new LocaleElement(displayName, null)); + } } diff --git a/java/org/apache/tomcat/util/descriptor/web/ResourceBase.java b/java/org/apache/tomcat/util/descriptor/web/ResourceBase.java index daca36ff01f1..b3b8862efce8 100644 --- a/java/org/apache/tomcat/util/descriptor/web/ResourceBase.java +++ b/java/org/apache/tomcat/util/descriptor/web/ResourceBase.java @@ -42,26 +42,55 @@ public ResourceBase() { // ------------------------------------------------------------- Properties /** - * The description of this resource. + * The descriptions of this resource. Multiple descriptions, each with an optional language, are supported as per + * the deployment descriptor specification. */ - private String description = null; + private final List descriptions = new ArrayList<>(); /** - * Return the description of this resource. + * Returns the descriptions of this resource. + * + * @return The descriptions of this resource + */ + public List getDescriptions() { + return descriptions; + } + + /** + * Adds a description to this resource. + * + * @param description The description to add + */ + public void addDescription(LocaleElement description) { + descriptions.add(description); + } + + /** + * Return the description of this resource. The default description (the one without a language) is returned if + * present, otherwise the first description is returned. * * @return The description of this resource */ public String getDescription() { - return this.description; + for (LocaleElement element : descriptions) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return descriptions.isEmpty() ? null : descriptions.get(0).getContent(); } /** - * Set the description of this resource. + * Set the description of this resource. Any existing descriptions, including language specific ones, are replaced + * by a single default description. * * @param description The description of this resource */ public void setDescription(String description) { - this.description = description; + descriptions.clear(); + if (description != null) { + descriptions.add(new LocaleElement(description, null)); + } } @@ -201,7 +230,7 @@ public List getInjectionTargets() { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + descriptions.hashCode(); result = prime * result + injectionTargets.hashCode(); result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + properties.hashCode(); @@ -223,11 +252,7 @@ public boolean equals(Object obj) { return false; } ResourceBase other = (ResourceBase) obj; - if (description == null) { - if (other.description != null) { - return false; - } - } else if (!description.equals(other.description)) { + if (!descriptions.equals(other.descriptions)) { return false; } if (!injectionTargets.equals(other.injectionTargets)) { diff --git a/java/org/apache/tomcat/util/descriptor/web/ServletDef.java b/java/org/apache/tomcat/util/descriptor/web/ServletDef.java index 6b7daddac6dd..ad3f96773d32 100644 --- a/java/org/apache/tomcat/util/descriptor/web/ServletDef.java +++ b/java/org/apache/tomcat/util/descriptor/web/ServletDef.java @@ -18,8 +18,10 @@ import java.io.Serial; import java.io.Serializable; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -48,50 +50,108 @@ public ServletDef() { /** - * The description of this servlet. + * The descriptions of this servlet. Multiple descriptions, each with an optional language, are supported as per + * the deployment descriptor specification. */ - private String description = null; + private final List descriptions = new ArrayList<>(); /** - * Returns the description of this servlet. + * Returns the descriptions of this servlet. + * + * @return The descriptions + */ + public List getDescriptions() { + return descriptions; + } + + /** + * Adds a description to this servlet. + * + * @param description The description to add + */ + public void addDescription(LocaleElement description) { + descriptions.add(description); + } + + /** + * Returns the description of this servlet. The default description (the one without a language) is returned if + * present, otherwise the first description is returned. * * @return the description */ public String getDescription() { - return this.description; + for (LocaleElement element : descriptions) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return descriptions.isEmpty() ? null : descriptions.get(0).getContent(); } /** - * Sets the description of this servlet. + * Sets the description of this servlet. Any existing descriptions, including language specific ones, are replaced + * by a single default description. * * @param description the description */ public void setDescription(String description) { - this.description = description; + descriptions.clear(); + if (description != null) { + descriptions.add(new LocaleElement(description, null)); + } } /** - * The display name of this servlet. + * The display names of this servlet. Multiple display names, each with an optional language, are supported as per + * the deployment descriptor specification. */ - private String displayName = null; + private final List displayNames = new ArrayList<>(); /** - * Returns the display name of this servlet. + * Returns the display names of this servlet. + * + * @return The display names + */ + public List getDisplayNames() { + return displayNames; + } + + /** + * Adds a display name to this servlet. + * + * @param displayName The display name to add + */ + public void addDisplayName(LocaleElement displayName) { + displayNames.add(displayName); + } + + /** + * Returns the display name of this servlet. The default display name (the one without a language) is returned if + * present, otherwise the first display name is returned. * * @return the display name */ public String getDisplayName() { - return this.displayName; + for (LocaleElement element : displayNames) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return displayNames.isEmpty() ? null : displayNames.get(0).getContent(); } /** - * Sets the display name of this servlet. + * Sets the display name of this servlet. Any existing display names, including language specific ones, are + * replaced by a single default display name. * * @param displayName the display name */ public void setDisplayName(String displayName) { - this.displayName = displayName; + displayNames.clear(); + if (displayName != null) { + displayNames.add(new LocaleElement(displayName, null)); + } } From 9ef047ac4aa13f6c02316a19e1d0687e04ccddf8 Mon Sep 17 00:00:00 2001 From: abin <57697211+ABin-Huang@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:55:36 +0800 Subject: [PATCH 4/7] Add i18n support for description and display-name elements in web.xml Update SecurityConstraint to store locale-aware descriptions and display names with full language support while preserving the String based compatibility getters and setters. --- .../descriptor/web/SecurityConstraint.java | 110 +++++++++++++++++- 1 file changed, 104 insertions(+), 6 deletions(-) diff --git a/java/org/apache/tomcat/util/descriptor/web/SecurityConstraint.java b/java/org/apache/tomcat/util/descriptor/web/SecurityConstraint.java index 0e734dad476e..68ba15864c9e 100644 --- a/java/org/apache/tomcat/util/descriptor/web/SecurityConstraint.java +++ b/java/org/apache/tomcat/util/descriptor/web/SecurityConstraint.java @@ -111,10 +111,16 @@ public SecurityConstraint() { /** - * The display name of this security constraint. + * The display names of this security constraint. Multiple display names, each with an optional language, are + * supported as per the deployment descriptor specification. */ - private String displayName = null; + private final List displayNames = new ArrayList<>(); + /** + * The descriptions of this security constraint. Multiple descriptions, each with an optional language, are + * supported as per the deployment descriptor specification. + */ + private final List descriptions = new ArrayList<>(); /** * The user data constraint for this security constraint. Must be NONE, INTEGRAL, or CONFIDENTIAL. @@ -172,25 +178,117 @@ public void setAuthConstraint(boolean authConstraint) { /** - * Get the display name of this security constraint. + * Get the display names of this security constraint. + * + * @return the display names of this security constraint + */ + public List getDisplayNames() { + + return displayNames; + + } + + + /** + * Add a display name to this security constraint. + * + * @param displayName The display name to add + */ + public void addDisplayName(LocaleElement displayName) { + + displayNames.add(displayName); + + } + + + /** + * Get the display name of this security constraint. The default display name (the one without a language) is + * returned if present, otherwise the first display name is returned. * * @return the display name of this security constraint */ public String getDisplayName() { - return this.displayName; + for (LocaleElement element : displayNames) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return displayNames.isEmpty() ? null : displayNames.get(0).getContent(); } /** - * Set the display name of this security constraint. + * Set the display name of this security constraint. Any existing display names, including language specific ones, + * are replaced by a single default display name. * * @param displayName The new value */ public void setDisplayName(String displayName) { - this.displayName = displayName; + displayNames.clear(); + if (displayName != null) { + displayNames.add(new LocaleElement(displayName, null)); + } + + } + + + /** + * Get the descriptions of this security constraint. + * + * @return the descriptions of this security constraint + */ + public List getDescriptions() { + + return descriptions; + + } + + + /** + * Add a description to this security constraint. + * + * @param description The description to add + */ + public void addDescription(LocaleElement description) { + + descriptions.add(description); + + } + + + /** + * Get the description of this security constraint. The default description (the one without a language) is + * returned if present, otherwise the first description is returned. + * + * @return the description of this security constraint + */ + public String getDescription() { + + for (LocaleElement element : descriptions) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return descriptions.isEmpty() ? null : descriptions.get(0).getContent(); + + } + + + /** + * Set the description of this security constraint. Any existing descriptions, including language specific ones, + * are replaced by a single default description. + * + * @param description The new description + */ + public void setDescription(String description) { + + descriptions.clear(); + if (description != null) { + descriptions.add(new LocaleElement(description, null)); + } } From 7a5eae9fab7bdb258777c2caf36a6e5293210c9f Mon Sep 17 00:00:00 2001 From: abin <57697211+ABin-Huang@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:02:09 +0800 Subject: [PATCH 5/7] Add i18n support for description and display-name elements in web.xml Replace the string based addCallMethod rules for description and display-name elements with a new LocaleElementRule that captures the element content together with the optional xml:lang attribute as a LocaleElement and adds it via the corresponding addDescription / addDisplayName method. --- .../util/descriptor/web/WebRuleSet.java | 94 ++++++++++++++++--- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/java/org/apache/tomcat/util/descriptor/web/WebRuleSet.java b/java/org/apache/tomcat/util/descriptor/web/WebRuleSet.java index 3fcce66220df..c478c99aa800 100644 --- a/java/org/apache/tomcat/util/descriptor/web/WebRuleSet.java +++ b/java/org/apache/tomcat/util/descriptor/web/WebRuleSet.java @@ -182,7 +182,8 @@ public void addRuleInstances(Digester digester) { digester.addCallParam(fullPrefix + "/context-param/param-name", 0); digester.addCallParam(fullPrefix + "/context-param/param-value", 1); - digester.addCallMethod(fullPrefix + "/display-name", "setDisplayName", 0); + digester.addRule(fullPrefix + "/description", new LocaleElementRule("addDescription")); + digester.addRule(fullPrefix + "/display-name", new LocaleElementRule("addDisplayName")); digester.addRule(fullPrefix + "/distributable", new SetDistributableRule()); @@ -199,8 +200,8 @@ public void addRuleInstances(Digester digester) { digester.addObjectCreate(fullPrefix + "/filter", "org.apache.tomcat.util.descriptor.web.FilterDef"); digester.addSetNext(fullPrefix + "/filter", "addFilter", "org.apache.tomcat.util.descriptor.web.FilterDef"); - digester.addCallMethod(fullPrefix + "/filter/description", "setDescription", 0); - digester.addCallMethod(fullPrefix + "/filter/display-name", "setDisplayName", 0); + digester.addRule(fullPrefix + "/filter/description", new LocaleElementRule("addDescription")); + digester.addRule(fullPrefix + "/filter/display-name", new LocaleElementRule("addDisplayName")); digester.addCallMethod(fullPrefix + "/filter/filter-class", "setFilterClass", 0); digester.addCallMethod(fullPrefix + "/filter/filter-name", "setFilterName", 0); digester.addCallMethod(fullPrefix + "/filter/icon/large-icon", "setLargeIcon", 0); @@ -273,7 +274,8 @@ public void addRuleInstances(Digester digester) { digester.addRule(fullPrefix + "/security-constraint/auth-constraint", new SetAuthConstraintRule()); digester.addCallMethod(fullPrefix + "/security-constraint/auth-constraint/role-name", "addAuthRole", 0); - digester.addCallMethod(fullPrefix + "/security-constraint/display-name", "setDisplayName", 0); + digester.addRule(fullPrefix + "/security-constraint/description", new LocaleElementRule("addDescription")); + digester.addRule(fullPrefix + "/security-constraint/display-name", new LocaleElementRule("addDisplayName")); digester.addCallMethod(fullPrefix + "/security-constraint/user-data-constraint/transport-guarantee", "setUserConstraint", 0); @@ -281,6 +283,8 @@ public void addRuleInstances(Digester digester) { "org.apache.tomcat.util.descriptor.web.SecurityCollection"); digester.addSetNext(fullPrefix + "/security-constraint/web-resource-collection", "addCollection", "org.apache.tomcat.util.descriptor.web.SecurityCollection"); + digester.addRule(fullPrefix + "/security-constraint/web-resource-collection/description", + new LocaleElementRule("addDescription")); digester.addCallMethod(fullPrefix + "/security-constraint/web-resource-collection/http-method", "addMethod", 0); digester.addCallMethod(fullPrefix + "/security-constraint/web-resource-collection/http-method-omission", "addOmittedMethod", 0); @@ -294,6 +298,8 @@ public void addRuleInstances(Digester digester) { digester.addRule(fullPrefix + "/servlet", new ServletDefCreateRule()); digester.addSetNext(fullPrefix + "/servlet", "addServlet", "org.apache.tomcat.util.descriptor.web.ServletDef"); + digester.addRule(fullPrefix + "/servlet/description", new LocaleElementRule("addDescription")); + digester.addRule(fullPrefix + "/servlet/display-name", new LocaleElementRule("addDisplayName")); digester.addCallMethod(fullPrefix + "/servlet/init-param", "addInitParameter", 2); digester.addCallParam(fullPrefix + "/servlet/init-param/param-name", 0); digester.addCallParam(fullPrefix + "/servlet/init-param/param-value", 1); @@ -306,6 +312,8 @@ public void addRuleInstances(Digester digester) { "org.apache.tomcat.util.descriptor.web.SecurityRoleRef"); digester.addSetNext(fullPrefix + "/servlet/security-role-ref", "addSecurityRoleRef", "org.apache.tomcat.util.descriptor.web.SecurityRoleRef"); + digester.addRule(fullPrefix + "/servlet/security-role-ref/description", + new LocaleElementRule("addDescription")); digester.addCallMethod(fullPrefix + "/servlet/security-role-ref/role-link", "setLink", 0); digester.addCallMethod(fullPrefix + "/servlet/security-role-ref/role-name", "setName", 0); @@ -385,7 +393,7 @@ protected void configureNamingRules(Digester digester) { "org.apache.tomcat.util.descriptor.web.ContextLocalEjb"); digester.addSetNext(fullPrefix + "/ejb-local-ref", "addEjbLocalRef", "org.apache.tomcat.util.descriptor.web.ContextLocalEjb"); - digester.addCallMethod(fullPrefix + "/ejb-local-ref/description", "setDescription", 0); + digester.addRule(fullPrefix + "/ejb-local-ref/description", new LocaleElementRule("addDescription")); digester.addCallMethod(fullPrefix + "/ejb-local-ref/ejb-link", "setLink", 0); digester.addCallMethod(fullPrefix + "/ejb-local-ref/ejb-ref-name", "setName", 0); digester.addCallMethod(fullPrefix + "/ejb-local-ref/ejb-ref-type", "setType", 0); @@ -398,7 +406,7 @@ protected void configureNamingRules(Digester digester) { // ejb-ref digester.addObjectCreate(fullPrefix + "/ejb-ref", "org.apache.tomcat.util.descriptor.web.ContextEjb"); digester.addSetNext(fullPrefix + "/ejb-ref", "addEjbRef", "org.apache.tomcat.util.descriptor.web.ContextEjb"); - digester.addCallMethod(fullPrefix + "/ejb-ref/description", "setDescription", 0); + digester.addRule(fullPrefix + "/ejb-ref/description", new LocaleElementRule("addDescription")); digester.addCallMethod(fullPrefix + "/ejb-ref/ejb-link", "setLink", 0); digester.addCallMethod(fullPrefix + "/ejb-ref/ejb-ref-name", "setName", 0); digester.addCallMethod(fullPrefix + "/ejb-ref/ejb-ref-type", "setType", 0); @@ -413,7 +421,7 @@ protected void configureNamingRules(Digester digester) { digester.addSetNext(fullPrefix + "/env-entry", "addEnvEntry", "org.apache.tomcat.util.descriptor.web.ContextEnvironment"); digester.addRule(fullPrefix + "/env-entry", new SetOverrideRule()); - digester.addCallMethod(fullPrefix + "/env-entry/description", "setDescription", 0); + digester.addRule(fullPrefix + "/env-entry/description", new LocaleElementRule("addDescription")); digester.addCallMethod(fullPrefix + "/env-entry/env-entry-name", "setName", 0); digester.addCallMethod(fullPrefix + "/env-entry/env-entry-type", "setType", 0); digester.addCallMethod(fullPrefix + "/env-entry/env-entry-value", "setValue", 0); @@ -426,6 +434,7 @@ protected void configureNamingRules(Digester digester) { "org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef"); digester.addSetNext(fullPrefix + "/resource-env-ref", "addResourceEnvRef", "org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef"); + digester.addRule(fullPrefix + "/resource-env-ref/description", new LocaleElementRule("addDescription")); digester.addCallMethod(fullPrefix + "/resource-env-ref/resource-env-ref-name", "setName", 0); digester.addCallMethod(fullPrefix + "/resource-env-ref/resource-env-ref-type", "setType", 0); digester.addRule(fullPrefix + "/resource-env-ref/mapped-name", new MappedNameRule()); @@ -437,8 +446,8 @@ protected void configureNamingRules(Digester digester) { "org.apache.tomcat.util.descriptor.web.MessageDestination"); digester.addSetNext(fullPrefix + "/message-destination", "addMessageDestination", "org.apache.tomcat.util.descriptor.web.MessageDestination"); - digester.addCallMethod(fullPrefix + "/message-destination/description", "setDescription", 0); - digester.addCallMethod(fullPrefix + "/message-destination/display-name", "setDisplayName", 0); + digester.addRule(fullPrefix + "/message-destination/description", new LocaleElementRule("addDescription")); + digester.addRule(fullPrefix + "/message-destination/display-name", new LocaleElementRule("addDisplayName")); digester.addCallMethod(fullPrefix + "/message-destination/icon/large-icon", "setLargeIcon", 0); digester.addCallMethod(fullPrefix + "/message-destination/icon/small-icon", "setSmallIcon", 0); digester.addCallMethod(fullPrefix + "/message-destination/message-destination-name", "setName", 0); @@ -450,7 +459,7 @@ protected void configureNamingRules(Digester digester) { "org.apache.tomcat.util.descriptor.web.MessageDestinationRef"); digester.addSetNext(fullPrefix + "/message-destination-ref", "addMessageDestinationRef", "org.apache.tomcat.util.descriptor.web.MessageDestinationRef"); - digester.addCallMethod(fullPrefix + "/message-destination-ref/description", "setDescription", 0); + digester.addRule(fullPrefix + "/message-destination-ref/description", new LocaleElementRule("addDescription")); digester.addCallMethod(fullPrefix + "/message-destination-ref/message-destination-link", "setLink", 0); digester.addCallMethod(fullPrefix + "/message-destination-ref/message-destination-ref-name", "setName", 0); digester.addCallMethod(fullPrefix + "/message-destination-ref/message-destination-type", "setType", 0); @@ -463,7 +472,7 @@ protected void configureNamingRules(Digester digester) { digester.addObjectCreate(fullPrefix + "/resource-ref", "org.apache.tomcat.util.descriptor.web.ContextResource"); digester.addSetNext(fullPrefix + "/resource-ref", "addResourceRef", "org.apache.tomcat.util.descriptor.web.ContextResource"); - digester.addCallMethod(fullPrefix + "/resource-ref/description", "setDescription", 0); + digester.addRule(fullPrefix + "/resource-ref/description", new LocaleElementRule("addDescription")); digester.addCallMethod(fullPrefix + "/resource-ref/res-auth", "setAuth", 0); digester.addCallMethod(fullPrefix + "/resource-ref/res-ref-name", "setName", 0); digester.addCallMethod(fullPrefix + "/resource-ref/res-sharing-scope", "setScope", 0); @@ -476,8 +485,8 @@ protected void configureNamingRules(Digester digester) { digester.addObjectCreate(fullPrefix + "/service-ref", "org.apache.tomcat.util.descriptor.web.ContextService"); digester.addSetNext(fullPrefix + "/service-ref", "addServiceRef", "org.apache.tomcat.util.descriptor.web.ContextService"); - digester.addCallMethod(fullPrefix + "/service-ref/description", "setDescription", 0); - digester.addCallMethod(fullPrefix + "/service-ref/display-name", "setDisplayname", 0); + digester.addRule(fullPrefix + "/service-ref/description", new LocaleElementRule("addDescription")); + digester.addRule(fullPrefix + "/service-ref/display-name", new LocaleElementRule("addDisplayname")); digester.addCallMethod(fullPrefix + "/service-ref/icon/large-icon", "setLargeIcon", 0); digester.addCallMethod(fullPrefix + "/service-ref/icon/small-icon", "setSmallIcon", 0); digester.addCallMethod(fullPrefix + "/service-ref/service-ref-name", "setName", 0); @@ -1273,3 +1282,62 @@ public void begin(String namespace, String name, Attributes attributes) throws E } } } + + +/** + * A rule that captures the body text of an element that supports internationalization (for example + * {@code } and {@code }) together with the optional {@code xml:lang} attribute as a + * {@link LocaleElement} and adds it to the current object via the configured method. + */ +final class LocaleElementRule extends Rule { + + private final String methodName; + private String lang = null; + + LocaleElementRule(String methodName) { + this.methodName = methodName; + } + + @Override + public void begin(String namespace, String name, Attributes attributes) throws Exception { + lang = attributes.getValue("xml:lang"); + if (lang == null) { + lang = attributes.getValue("http://www.w3.org/XML/1998/namespace", "lang"); + } + } + + @Override + public void body(String namespace, String name, String text) throws Exception { + if (text == null || text.trim().isEmpty()) { + return; + } + String content = text.trim(); + LocaleElement element = new LocaleElement(content, lang); + Object target = digester.peek(); + IntrospectionUtils.callMethodN(target, methodName, new Object[] { element }, + new Class[] { LocaleElement.class }); + if (digester.getLogger().isTraceEnabled()) { + digester.getLogger().trace(target.getClass().getName() + "." + methodName + "(LocaleElement)"); + } + + StringBuilder code = digester.getGeneratedCode(); + if (code != null) { + code.append(System.lineSeparator()); + code.append(digester.toVariableName(target)).append('.').append(methodName).append("(new LocaleElement(\""); + code.append(content); + code.append("\", "); + if (lang == null) { + code.append("null"); + } else { + code.append('"').append(lang).append('"'); + } + code.append("));"); + code.append(System.lineSeparator()); + } + } + + @Override + public void end(String namespace, String name) throws Exception { + lang = null; + } +} \ No newline at end of file From 177735d2190d9b34b2a3e6a1ad644bd1fbee088f Mon Sep 17 00:00:00 2001 From: abin <57697211+ABin-Huang@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:04:14 +0800 Subject: [PATCH 6/7] Add i18n tests for description and display-name elements in web.xml Add TestWebXmlI18n covering parsing of multi-language description and display-name elements (web-app, filter, servlet, security-role-ref, security-constraint, web-resource-collection, env-entry, resource-ref, message-destination), toXml round-tripping, setter replacement semantics and merge behaviour for both distinct languages and same language conflicts. --- .../util/descriptor/web/TestWebXmlI18n.java | 378 ++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 test/org/apache/tomcat/util/descriptor/web/TestWebXmlI18n.java diff --git a/test/org/apache/tomcat/util/descriptor/web/TestWebXmlI18n.java b/test/org/apache/tomcat/util/descriptor/web/TestWebXmlI18n.java new file mode 100644 index 000000000000..7a354e149238 --- /dev/null +++ b/test/org/apache/tomcat/util/descriptor/web/TestWebXmlI18n.java @@ -0,0 +1,378 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.util.descriptor.web; + +import java.io.StringReader; +import java.net.URL; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.xml.sax.InputSource; + +public class TestWebXmlI18n { + + private static final String WEB_XML = "\n" + + "\n" + + " App default description\n" + + " 应用描述\n" + + " Default name\n" + + " 默认名称\n" + + " \n" + + " Filter default description\n" + + " Filter-Beschreibung\n" + + " Filter default name\n" + + " Filter-Anzeigename\n" + + " f1\n" + + " org.apache.catalina.filters.SetCharacterEncodingFilter\n" + + " \n" + + " \n" + + " f1\n" + + " /*\n" + + " \n" + + " \n" + + " Servlet default description\n" + + " Description du servlet\n" + + " Servlet default name\n" + + " Nom du servlet\n" + + " s1\n" + + " org.apache.catalina.servlets.DefaultServlet\n" + + " \n" + + " Role ref default description\n" + + " Descrizione del ruolo\n" + + " admin\n" + + " manager\n" + + " \n" + + " \n" + + " \n" + + " s1\n" + + " /\n" + + " \n" + + " \n" + + " Constraint default description\n" + + " Descripción de la restricción\n" + + " Constraint default name\n" + + " Nombre de la restricción\n" + + " \n" + + " Collection default description\n" + + " コレクションの説明\n" + + " wrc1\n" + + " /protected/*\n" + + " \n" + + " \n" + + " admin\n" + + " \n" + + " \n" + + " \n" + + " Env entry default description\n" + + " Descrição do ambiente\n" + + " env1\n" + + " java.lang.String\n" + + " value1\n" + + " \n" + + " \n" + + " Resource ref default description\n" + + " Описание ресурса\n" + + " jdbc/TestDB\n" + + " javax.sql.DataSource\n" + + " \n" + + " \n" + + " Message destination default description\n" + + " 메시지 대상 설명\n" + + " Message destination default name\n" + + " 메시지 대상 이름\n" + + " md1\n" + + " \n" + + "\n"; + + @Test + public void testParseWebAppDescriptionAndDisplayName() throws Exception { + WebXml webXml = parse(WEB_XML); + + List descriptions = webXml.getDescriptions(); + Assert.assertEquals(2, descriptions.size()); + Assert.assertEquals("App default description", descriptions.get(0).getContent()); + Assert.assertNull(descriptions.get(0).getLang()); + Assert.assertEquals("应用描述", descriptions.get(1).getContent()); + Assert.assertEquals("zh", descriptions.get(1).getLang()); + + List displayNames = webXml.getDisplayNames(); + Assert.assertEquals(2, displayNames.size()); + Assert.assertEquals("Default name", displayNames.get(0).getContent()); + Assert.assertNull(displayNames.get(0).getLang()); + Assert.assertEquals("默认名称", displayNames.get(1).getContent()); + Assert.assertEquals("zh", displayNames.get(1).getLang()); + + // The default (language-less) values must be returned by the compatibility getters + Assert.assertEquals("App default description", webXml.getDescription()); + Assert.assertEquals("Default name", webXml.getDisplayName()); + } + + @Test + public void testParseFilterDescriptionAndDisplayName() throws Exception { + WebXml webXml = parse(WEB_XML); + + FilterDef filter = webXml.getFilters().get("f1"); + Assert.assertNotNull(filter); + + List descriptions = filter.getDescriptions(); + Assert.assertEquals(2, descriptions.size()); + Assert.assertEquals("Filter default description", descriptions.get(0).getContent()); + Assert.assertEquals("Filter-Beschreibung", descriptions.get(1).getContent()); + Assert.assertEquals("de", descriptions.get(1).getLang()); + + List displayNames = filter.getDisplayNames(); + Assert.assertEquals(2, displayNames.size()); + Assert.assertEquals("Filter default name", displayNames.get(0).getContent()); + Assert.assertEquals("Filter-Anzeigename", displayNames.get(1).getContent()); + Assert.assertEquals("de", displayNames.get(1).getLang()); + + Assert.assertEquals("Filter default description", filter.getDescription()); + Assert.assertEquals("Filter default name", filter.getDisplayName()); + } + + @Test + public void testParseServletDescriptionDisplayNameAndRoleRef() throws Exception { + WebXml webXml = parse(WEB_XML); + + ServletDef servlet = webXml.getServlets().get("s1"); + Assert.assertNotNull(servlet); + + List descriptions = servlet.getDescriptions(); + Assert.assertEquals(2, descriptions.size()); + Assert.assertEquals("Servlet default description", descriptions.get(0).getContent()); + Assert.assertEquals("Description du servlet", descriptions.get(1).getContent()); + Assert.assertEquals("fr", descriptions.get(1).getLang()); + + List displayNames = servlet.getDisplayNames(); + Assert.assertEquals(2, displayNames.size()); + Assert.assertEquals("Servlet default name", displayNames.get(0).getContent()); + Assert.assertEquals("Nom du servlet", displayNames.get(1).getContent()); + Assert.assertEquals("fr", displayNames.get(1).getLang()); + + Assert.assertEquals(1, servlet.getSecurityRoleRefs().size()); + SecurityRoleRef roleRef = servlet.getSecurityRoleRefs().iterator().next(); + List roleRefDescriptions = roleRef.getDescriptions(); + Assert.assertEquals(2, roleRefDescriptions.size()); + Assert.assertEquals("Role ref default description", roleRefDescriptions.get(0).getContent()); + Assert.assertEquals("Descrizione del ruolo", roleRefDescriptions.get(1).getContent()); + Assert.assertEquals("it", roleRefDescriptions.get(1).getLang()); + } + + @Test + public void testParseSecurityConstraintAndCollection() throws Exception { + WebXml webXml = parse(WEB_XML); + + Assert.assertEquals(1, webXml.getSecurityConstraints().size()); + SecurityConstraint constraint = webXml.getSecurityConstraints().iterator().next(); + + List constraintDescriptions = constraint.getDescriptions(); + Assert.assertEquals(2, constraintDescriptions.size()); + Assert.assertEquals("Constraint default description", constraintDescriptions.get(0).getContent()); + Assert.assertEquals("Descripción de la restricción", constraintDescriptions.get(1).getContent()); + Assert.assertEquals("es", constraintDescriptions.get(1).getLang()); + + List constraintDisplayNames = constraint.getDisplayNames(); + Assert.assertEquals(2, constraintDisplayNames.size()); + Assert.assertEquals("Constraint default name", constraintDisplayNames.get(0).getContent()); + Assert.assertEquals("Nombre de la restricción", constraintDisplayNames.get(1).getContent()); + Assert.assertEquals("es", constraintDisplayNames.get(1).getLang()); + + Assert.assertEquals(1, constraint.findCollections().length); + SecurityCollection collection = constraint.findCollections()[0]; + List collectionDescriptions = collection.getDescriptions(); + Assert.assertEquals(2, collectionDescriptions.size()); + Assert.assertEquals("Collection default description", collectionDescriptions.get(0).getContent()); + Assert.assertEquals("コレクションの説明", collectionDescriptions.get(1).getContent()); + Assert.assertEquals("ja", collectionDescriptions.get(1).getLang()); + } + + @Test + public void testParseResourceBaseDescriptions() throws Exception { + WebXml webXml = parse(WEB_XML); + + ContextEnvironment envEntry = webXml.getEnvEntries().get("env1"); + Assert.assertNotNull(envEntry); + Assert.assertEquals(2, envEntry.getDescriptions().size()); + Assert.assertEquals("Env entry default description", envEntry.getDescriptions().get(0).getContent()); + Assert.assertEquals("Descrição do ambiente", envEntry.getDescriptions().get(1).getContent()); + Assert.assertEquals("pt", envEntry.getDescriptions().get(1).getLang()); + + ContextResource resource = webXml.getResourceRefs().get("jdbc/TestDB"); + Assert.assertNotNull(resource); + Assert.assertEquals(2, resource.getDescriptions().size()); + Assert.assertEquals("Resource ref default description", resource.getDescriptions().get(0).getContent()); + Assert.assertEquals("Описание ресурса", resource.getDescriptions().get(1).getContent()); + Assert.assertEquals("ru", resource.getDescriptions().get(1).getLang()); + + MessageDestination messageDestination = webXml.getMessageDestinations().get("md1"); + Assert.assertNotNull(messageDestination); + Assert.assertEquals(2, messageDestination.getDescriptions().size()); + Assert.assertEquals("Message destination default description", + messageDestination.getDescriptions().get(0).getContent()); + Assert.assertEquals("메시지 대상 설명", messageDestination.getDescriptions().get(1).getContent()); + Assert.assertEquals("ko", messageDestination.getDescriptions().get(1).getLang()); + Assert.assertEquals(2, messageDestination.getDisplayNames().size()); + Assert.assertEquals("Message destination default name", + messageDestination.getDisplayNames().get(0).getContent()); + Assert.assertEquals("메시지 대상 이름", messageDestination.getDisplayNames().get(1).getContent()); + Assert.assertEquals("ko", messageDestination.getDisplayNames().get(1).getLang()); + } + + @Test + public void testToXmlRoundTrip() throws Exception { + WebXml webXml = parse(WEB_XML); + + // Serialize and parse again + WebXml parsed = parse(webXml.toXml()); + + Assert.assertEquals(webXml.getDescriptions(), parsed.getDescriptions()); + Assert.assertEquals(webXml.getDisplayNames(), parsed.getDisplayNames()); + + FilterDef filter = parsed.getFilters().get("f1"); + Assert.assertNotNull(filter); + Assert.assertEquals(webXml.getFilters().get("f1").getDescriptions(), filter.getDescriptions()); + Assert.assertEquals(webXml.getFilters().get("f1").getDisplayNames(), filter.getDisplayNames()); + + ServletDef servlet = parsed.getServlets().get("s1"); + Assert.assertNotNull(servlet); + Assert.assertEquals(webXml.getServlets().get("s1").getDescriptions(), servlet.getDescriptions()); + Assert.assertEquals(webXml.getServlets().get("s1").getDisplayNames(), servlet.getDisplayNames()); + } + + @Test + public void testSetDescriptionAndSetDisplayNameReplaceAll() { + WebXml webXml = new WebXml(); + webXml.addDescription(new LocaleElement("App description", null)); + webXml.addDescription(new LocaleElement("应用描述", "zh")); + + // The compatibility setter must replace all existing entries + webXml.setDescription("New description"); + Assert.assertEquals(1, webXml.getDescriptions().size()); + Assert.assertEquals("New description", webXml.getDescription()); + + webXml.addDisplayName(new LocaleElement("App name", null)); + webXml.addDisplayName(new LocaleElement("默认名称", "zh")); + webXml.setDisplayName("New name"); + Assert.assertEquals(1, webXml.getDisplayNames().size()); + Assert.assertEquals("New name", webXml.getDisplayName()); + } + + @Test + public void testMergeDisplayNameDifferentLanguages() throws Exception { + WebXml main = new WebXml(); + + WebXml fragment1 = new WebXml(); + fragment1.setName("fragment1"); + fragment1.setURL(url("file:///fragment1")); + fragment1.addDisplayName(new LocaleElement("Name in German", "de")); + + WebXml fragment2 = new WebXml(); + fragment2.setName("fragment2"); + fragment2.setURL(url("file:///fragment2")); + fragment2.addDisplayName(new LocaleElement("Name in French", "fr")); + + Assert.assertTrue(main.merge(new HashSet<>(Arrays.asList(fragment1, fragment2)))); + + // Both language specific display names must be merged + Assert.assertEquals(2, main.getDisplayNames().size()); + Assert.assertEquals("Name in German", findContent(main.getDisplayNames(), "de")); + Assert.assertEquals("Name in French", findContent(main.getDisplayNames(), "fr")); + } + + @Test + public void testMergeDisplayNameConflict() throws Exception { + WebXml main = new WebXml(); + + WebXml fragment1 = new WebXml(); + fragment1.setName("fragment1"); + fragment1.setURL(url("file:///fragment1")); + fragment1.addDisplayName(new LocaleElement("Name one", "en")); + + WebXml fragment2 = new WebXml(); + fragment2.setName("fragment2"); + fragment2.setURL(url("file:///fragment2")); + fragment2.addDisplayName(new LocaleElement("Name two", "en")); + + Assert.assertFalse(main.merge(new HashSet<>(Arrays.asList(fragment1, fragment2)))); + } + + @Test + public void testMergeDescriptionDifferentLanguages() throws Exception { + WebXml main = new WebXml(); + + WebXml fragment1 = new WebXml(); + fragment1.setName("fragment1"); + fragment1.setURL(url("file:///fragment1")); + fragment1.addDescription(new LocaleElement("Beschreibung auf Deutsch", "de")); + + WebXml fragment2 = new WebXml(); + fragment2.setName("fragment2"); + fragment2.setURL(url("file:///fragment2")); + fragment2.addDescription(new LocaleElement("Description en français", "fr")); + + Assert.assertTrue(main.merge(new HashSet<>(Arrays.asList(fragment1, fragment2)))); + + Assert.assertEquals(2, main.getDescriptions().size()); + Assert.assertEquals("Beschreibung auf Deutsch", findContent(main.getDescriptions(), "de")); + Assert.assertEquals("Description en français", findContent(main.getDescriptions(), "fr")); + } + + @Test + public void testMergeDescriptionConflict() throws Exception { + WebXml main = new WebXml(); + + WebXml fragment1 = new WebXml(); + fragment1.setName("fragment1"); + fragment1.setURL(url("file:///fragment1")); + fragment1.addDescription(new LocaleElement("Description one", "en")); + + WebXml fragment2 = new WebXml(); + fragment2.setName("fragment2"); + fragment2.setURL(url("file:///fragment2")); + fragment2.addDescription(new LocaleElement("Description two", "en")); + + Assert.assertFalse(main.merge(new HashSet<>(Arrays.asList(fragment1, fragment2)))); + } + + private static WebXml parse(String xml) throws Exception { + WebXmlParser parser = new WebXmlParser(true, false, false); + WebXml webXml = new WebXml(); + Assert.assertTrue(parser.parseWebXml(new InputSource(new StringReader(xml)), webXml, false)); + return webXml; + } + + private static URL url(String spec) throws Exception { + return new URL(spec); + } + + private static String findContent(List elements, String lang) { + for (LocaleElement element : elements) { + if (lang.equals(element.getLang())) { + return element.getContent(); + } + } + return null; + } +} \ No newline at end of file From 80603ed87e6987e7063549bec45d00c734713f26 Mon Sep 17 00:00:00 2001 From: abin <57697211+ABin-Huang@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:25:00 +0800 Subject: [PATCH 7/7] Add i18n support for description and display-name elements in web.xml Store web-app level description and display-name elements as locale aware LocaleElement lists, output all locale aware elements with their optional xml:lang attribute in toXml() and merge descriptions and display names by language, detecting conflicts when two fragments define the same language. --- .../tomcat/util/descriptor/web/WebXml.java | 230 ++++++++++++++---- 1 file changed, 177 insertions(+), 53 deletions(-) diff --git a/java/org/apache/tomcat/util/descriptor/web/WebXml.java b/java/org/apache/tomcat/util/descriptor/web/WebXml.java index 95b3cb521587..61ef8e592a75 100644 --- a/java/org/apache/tomcat/util/descriptor/web/WebXml.java +++ b/java/org/apache/tomcat/util/descriptor/web/WebXml.java @@ -413,28 +413,105 @@ public int getMinorVersion() { // web-app elements // TODO: Ignored elements: - // - description // - icon + private final List descriptions = new ArrayList<>(); - // display-name - TODO should support multiple with language - private String displayName = null; + /** + * Returns the descriptions of the web application. + * + * @return the descriptions + */ + public List getDescriptions() { + return descriptions; + } + + /** + * Adds a description of the web application. + * + * @param description The description to add + */ + public void addDescription(LocaleElement description) { + descriptions.add(description); + } + + /** + * Returns the description of the web application. The default description (the one without a language) is + * returned if present, otherwise the first description is returned. + * + * @return the description + */ + public String getDescription() { + for (LocaleElement element : descriptions) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return descriptions.isEmpty() ? null : descriptions.get(0).getContent(); + } + + /** + * Sets the description. Any existing descriptions, including language specific ones, are replaced by a single + * default description. + * + * @param description The description + */ + public void setDescription(String description) { + descriptions.clear(); + if (description != null) { + descriptions.add(new LocaleElement(description, null)); + } + } + + /** + * The display names of the web application. Multiple display names, each with an optional language, are supported + * as per the deployment descriptor specification. + */ + private final List displayNames = new ArrayList<>(); + + /** + * Returns the display names of the web application. + * + * @return the display names + */ + public List getDisplayNames() { + return displayNames; + } + + /** + * Adds a display name of the web application. + * + * @param displayName The display name to add + */ + public void addDisplayName(LocaleElement displayName) { + displayNames.add(displayName); + } /** - * Returns the display name of the web application. + * Returns the display name of the web application. The default display name (the one without a language) is + * returned if present, otherwise the first display name is returned. * * @return the display name */ public String getDisplayName() { - return displayName; + for (LocaleElement element : displayNames) { + if (element.getLang() == null) { + return element.getContent(); + } + } + return displayNames.isEmpty() ? null : displayNames.get(0).getContent(); } /** - * Sets the display name. + * Sets the display name. Any existing display names, including language specific ones, are replaced by a single + * default display name. * * @param displayName The display name */ public void setDisplayName(String displayName) { - this.displayName = displayName; + displayNames.clear(); + if (displayName != null) { + displayNames.add(new LocaleElement(displayName, null)); + } } // distributable @@ -503,8 +580,6 @@ public Map getContextParams() { } // filter - // TODO: Should support multiple description elements with language - // TODO: Should support multiple display-name elements with language // TODO: Should support multiple icon elements // TODO: Description for init-param is ignored private final Map filters = new LinkedHashMap<>(); @@ -581,11 +656,8 @@ public Set getListeners() { } // servlet - // TODO: description (multiple with language) is ignored - // TODO: display-name (multiple with language) is ignored // TODO: icon (multiple) is ignored // TODO: init-param/description (multiple with language) is ignored - // TODO: security-role-ref/description (multiple with language) is ignored private final Map servlets = new HashMap<>(); /** @@ -806,8 +878,6 @@ public Set getJspPropertyGroups() { } // security-constraint - // TODO: Should support multiple display-name elements with language - // TODO: Should support multiple description elements with language private final Set securityConstraints = new HashSet<>(); /** @@ -873,7 +943,6 @@ public Set getSecurityRoles() { } // env-entry - // TODO: Should support multiple description elements with language private final Map envEntries = new HashMap<>(); /** @@ -901,7 +970,6 @@ public Map getEnvEntries() { } // ejb-ref - // TODO: Should support multiple description elements with language private final Map ejbRefs = new HashMap<>(); /** @@ -923,7 +991,6 @@ public Map getEjbRefs() { } // ejb-local-ref - // TODO: Should support multiple description elements with language private final Map ejbLocalRefs = new HashMap<>(); /** @@ -945,8 +1012,6 @@ public Map getEjbLocalRefs() { } // service-ref - // TODO: Should support multiple description elements with language - // TODO: Should support multiple display-names elements with language // TODO: Should support multiple icon elements ??? private final Map serviceRefs = new HashMap<>(); @@ -969,7 +1034,6 @@ public Map getServiceRefs() { } // resource-ref - // TODO: Should support multiple description elements with language private final Map resourceRefs = new HashMap<>(); /** @@ -997,7 +1061,6 @@ public Map getResourceRefs() { } // resource-env-ref - // TODO: Should support multiple description elements with language private final Map resourceEnvRefs = new HashMap<>(); /** @@ -1026,7 +1089,6 @@ public Map getResourceEnvRefs() { } // message-destination-ref - // TODO: Should support multiple description elements with language private final Map messageDestinationRefs = new HashMap<>(); /** @@ -1056,8 +1118,6 @@ public Map getMessageDestinationRefs() { } // message-destination - // TODO: Should support multiple description elements with language - // TODO: Should support multiple display-names elements with language // TODO: Should support multiple icon elements ??? private final Map messageDestinations = new HashMap<>(); @@ -1345,8 +1405,8 @@ public String toString() { */ public String toXml() { StringBuilder sb = new StringBuilder(2048); - // TODO - Various, icon, description etc elements are skipped - mainly - // because they are ignored when web.xml is parsed - see above + // TODO - icon elements are skipped - mainly because they are ignored + // when web.xml is parsed - see above // NOTE - Elements need to be written in the order defined in the 2.3 // DTD else validation of the merged web.xml will fail @@ -1425,7 +1485,8 @@ public String toXml() { } } - appendElement(sb, INDENT2, "display-name", displayName); + appendLocaleElements(sb, INDENT2, "description", descriptions); + appendLocaleElements(sb, INDENT2, "display-name", displayNames); if (isDistributable()) { sb.append(" \n\n"); @@ -1446,8 +1507,8 @@ public String toXml() { for (Map.Entry entry : filters.entrySet()) { FilterDef filterDef = entry.getValue(); sb.append(" \n"); - appendElement(sb, INDENT4, "description", filterDef.getDescription()); - appendElement(sb, INDENT4, "display-name", filterDef.getDisplayName()); + appendLocaleElements(sb, INDENT4, "description", filterDef.getDescriptions()); + appendLocaleElements(sb, INDENT4, "display-name", filterDef.getDisplayNames()); appendElement(sb, INDENT4, "filter-name", filterDef.getFilterName()); appendElement(sb, INDENT4, "filter-class", filterDef.getFilterClass()); // Async support was introduced for Servlet 3.0 onwards @@ -1514,8 +1575,8 @@ public String toXml() { for (Map.Entry entry : servlets.entrySet()) { ServletDef servletDef = entry.getValue(); sb.append(" \n"); - appendElement(sb, INDENT4, "description", servletDef.getDescription()); - appendElement(sb, INDENT4, "display-name", servletDef.getDisplayName()); + appendLocaleElements(sb, INDENT4, "description", servletDef.getDescriptions()); + appendLocaleElements(sb, INDENT4, "display-name", servletDef.getDisplayNames()); appendElement(sb, INDENT4, "servlet-name", entry.getKey()); appendElement(sb, INDENT4, "servlet-class", servletDef.getServletClass()); appendElement(sb, INDENT4, "jsp-file", servletDef.getJspFile()); @@ -1541,6 +1602,7 @@ public String toXml() { } for (SecurityRoleRef roleRef : servletDef.getSecurityRoleRefs()) { sb.append(" \n"); + appendLocaleElements(sb, INDENT6, "description", roleRef.getDescriptions()); appendElement(sb, INDENT6, "role-name", roleRef.getName()); appendElement(sb, INDENT6, "role-link", roleRef.getLink()); sb.append(" \n"); @@ -1696,7 +1758,7 @@ public String toXml() { if (getMajorVersion() > 2 || getMinorVersion() > 2) { for (ContextResourceEnvRef resourceEnvRef : resourceEnvRefs.values()) { sb.append(" \n"); - appendElement(sb, INDENT4, "description", resourceEnvRef.getDescription()); + appendLocaleElements(sb, INDENT4, "description", resourceEnvRef.getDescriptions()); appendElement(sb, INDENT4, "resource-env-ref-name", resourceEnvRef.getName()); appendElement(sb, INDENT4, "resource-env-ref-type", resourceEnvRef.getType()); appendElement(sb, INDENT4, "mapped-name", resourceEnvRef.getProperty("mappedName")); @@ -1716,7 +1778,7 @@ public String toXml() { for (ContextResource resourceRef : resourceRefs.values()) { sb.append(" \n"); - appendElement(sb, INDENT4, "description", resourceRef.getDescription()); + appendLocaleElements(sb, INDENT4, "description", resourceRef.getDescriptions()); appendElement(sb, INDENT4, "res-ref-name", resourceRef.getName()); appendElement(sb, INDENT4, "res-type", resourceRef.getType()); appendElement(sb, INDENT4, "res-auth", resourceRef.getAuth()); @@ -1742,12 +1804,13 @@ public String toXml() { sb.append(" \n"); // security-constraint/display-name was introduced in Servlet 2.3 if (getMajorVersion() > 2 || getMinorVersion() > 2) { - appendElement(sb, INDENT4, "display-name", constraint.getDisplayName()); + appendLocaleElements(sb, INDENT4, "display-name", constraint.getDisplayNames()); + appendLocaleElements(sb, INDENT4, "description", constraint.getDescriptions()); } for (SecurityCollection collection : constraint.findCollections()) { sb.append(" \n"); appendElement(sb, INDENT6, "web-resource-name", collection.getName()); - appendElement(sb, INDENT6, "description", collection.getDescription()); + appendLocaleElements(sb, INDENT6, "description", collection.getDescriptions()); for (String urlPattern : collection.findPatterns()) { appendElement(sb, INDENT6, "url-pattern", urlPattern); } @@ -1807,7 +1870,7 @@ public String toXml() { for (ContextEnvironment envEntry : envEntries.values()) { sb.append(" \n"); - appendElement(sb, INDENT4, "description", envEntry.getDescription()); + appendLocaleElements(sb, INDENT4, "description", envEntry.getDescriptions()); appendElement(sb, INDENT4, "env-entry-name", envEntry.getName()); appendElement(sb, INDENT4, "env-entry-type", envEntry.getType()); appendElement(sb, INDENT4, "env-entry-value", envEntry.getValue()); @@ -1827,7 +1890,7 @@ public String toXml() { for (ContextEjb ejbRef : ejbRefs.values()) { sb.append(" \n"); - appendElement(sb, INDENT4, "description", ejbRef.getDescription()); + appendLocaleElements(sb, INDENT4, "description", ejbRef.getDescriptions()); appendElement(sb, INDENT4, "ejb-ref-name", ejbRef.getName()); appendElement(sb, INDENT4, "ejb-ref-type", ejbRef.getType()); appendElement(sb, INDENT4, "home", ejbRef.getHome()); @@ -1851,7 +1914,7 @@ public String toXml() { if (getMajorVersion() > 2 || getMinorVersion() > 2) { for (ContextLocalEjb ejbLocalRef : ejbLocalRefs.values()) { sb.append(" \n"); - appendElement(sb, INDENT4, "description", ejbLocalRef.getDescription()); + appendLocaleElements(sb, INDENT4, "description", ejbLocalRef.getDescriptions()); appendElement(sb, INDENT4, "ejb-ref-name", ejbLocalRef.getName()); appendElement(sb, INDENT4, "ejb-ref-type", ejbLocalRef.getType()); appendElement(sb, INDENT4, "local-home", ejbLocalRef.getHome()); @@ -1876,8 +1939,8 @@ public String toXml() { if (getMajorVersion() > 2 || getMinorVersion() > 3) { for (ContextService serviceRef : serviceRefs.values()) { sb.append(" \n"); - appendElement(sb, INDENT4, "description", serviceRef.getDescription()); - appendElement(sb, INDENT4, "display-name", serviceRef.getDisplayname()); + appendLocaleElements(sb, INDENT4, "description", serviceRef.getDescriptions()); + appendLocaleElements(sb, INDENT4, "display-name", serviceRef.getDisplaynames()); appendElement(sb, INDENT4, "service-ref-name", serviceRef.getName()); appendElement(sb, INDENT4, "service-interface", serviceRef.getInterface()); appendElement(sb, INDENT4, "service-ref-type", serviceRef.getType()); @@ -1947,7 +2010,7 @@ public String toXml() { if (getMajorVersion() > 2 || getMinorVersion() > 3) { for (MessageDestinationRef mdr : messageDestinationRefs.values()) { sb.append(" \n"); - appendElement(sb, INDENT4, "description", mdr.getDescription()); + appendLocaleElements(sb, INDENT4, "description", mdr.getDescriptions()); appendElement(sb, INDENT4, "message-destination-ref-name", mdr.getName()); appendElement(sb, INDENT4, "message-destination-type", mdr.getType()); appendElement(sb, INDENT4, "message-destination-usage", mdr.getUsage()); @@ -1968,8 +2031,8 @@ public String toXml() { for (MessageDestination md : messageDestinations.values()) { sb.append(" \n"); - appendElement(sb, INDENT4, "description", md.getDescription()); - appendElement(sb, INDENT4, "display-name", md.getDisplayName()); + appendLocaleElements(sb, INDENT4, "description", md.getDescriptions()); + appendLocaleElements(sb, INDENT4, "display-name", md.getDisplayNames()); appendElement(sb, INDENT4, "message-destination-name", md.getName()); appendElement(sb, INDENT4, "mapped-name", md.getProperty("mappedName")); appendElement(sb, INDENT4, "lookup-name", md.getLookupName()); @@ -2041,6 +2104,35 @@ private void appendElement(StringBuilder sb, String indent, String elementName, appendElement(sb, indent, elementName, value.toString()); } + /** + * Appends a list of locale-aware elements (e.g. description, display-name) to the output. Elements with a language + * are written with the {@code xml:lang} attribute, elements without a language are written as plain elements. + * + * @param sb The output buffer + * @param indent The indentation to use + * @param elementName The element name + * @param elements The elements to append + */ + private void appendLocaleElements(StringBuilder sb, String indent, String elementName, + List elements) { + for (LocaleElement element : elements) { + if (element.getLang() == null || element.getLang().isEmpty()) { + appendElement(sb, indent, elementName, element.getContent()); + } else { + sb.append(indent); + sb.append('<'); + sb.append(elementName); + sb.append(" xml:lang=\""); + sb.append(Escape.xml(element.getLang())); + sb.append("\">"); + sb.append(Escape.xml(element.getContent())); + sb.append("\n"); + } + } + } + /** * Merge the supplied web fragments into this main web.xml. @@ -2065,20 +2157,36 @@ public boolean merge(Set fragments) { } contextParams.putAll(temp.getContextParams()); - if (displayName == null) { + if (descriptions.isEmpty()) { for (WebXml fragment : fragments) { - String value = fragment.getDisplayName(); - if (value != null) { - if (temp.getDisplayName() == null) { - temp.setDisplayName(value); - } else { + for (LocaleElement element : fragment.getDescriptions()) { + LocaleElement conflict = findLocaleElement(temp.getDescriptions(), element.getLang()); + if (conflict == null) { + temp.addDescription(element); + } else if (!conflict.equals(element)) { + log.error( + sm.getString("webXml.mergeConflictDescription", fragment.getName(), fragment.getURL())); + return false; + } + } + } + descriptions.addAll(temp.getDescriptions()); + } + + if (displayNames.isEmpty()) { + for (WebXml fragment : fragments) { + for (LocaleElement element : fragment.getDisplayNames()) { + LocaleElement conflict = findLocaleElement(temp.getDisplayNames(), element.getLang()); + if (conflict == null) { + temp.addDisplayName(element); + } else if (!conflict.equals(element)) { log.error( sm.getString("webXml.mergeConflictDisplayName", fragment.getName(), fragment.getURL())); return false; } } } - displayName = temp.getDisplayName(); + displayNames.addAll(temp.getDisplayNames()); } // Note: Not permitted in fragments, but we also use fragments for @@ -2435,9 +2543,25 @@ public boolean merge(Set fragments) { return true; } + /** + * Finds a locale-aware element in the supplied list that has the same language as the requested language. + * + * @param elements The elements to search + * @param lang The language to look for, or {@code null} for the default element + * + * @return The matching element, or {@code null} if no match is found + */ + private static LocaleElement findLocaleElement(List elements, String lang) { + for (LocaleElement element : elements) { + if (element.getLang() == null ? lang == null : element.getLang().equals(lang)) { + return element; + } + } + return null; + } + private boolean mergeResourceMap(Map fragmentResources, - Map mainResources, Map tempResources, WebXml fragment) { - for (T resource : fragmentResources.values()) { + Map mainResources, Map tempResources, WebXml fragment) { for (T resource : fragmentResources.values()) { String resourceName = resource.getName(); if (mainResources.containsKey(resourceName)) { mainResources.get(resourceName).getInjectionTargets().addAll(resource.getInjectionTargets()); @@ -2876,4 +3000,4 @@ private static void makeAfterOthersExplicit(Set afterOrdering, Map