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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ public static void replaceNamespacePrefixes(
final String oldPrefix =
currentResolver.getNamespaceURI2PrefixMap().get(currentUri);

if (StringUtils.isNotEmpty(oldPrefix)) {
if (StringUtils.isNotEmpty(oldPrefix) && !oldPrefix.equals(newPrefix)) {
// Can we perform the prefix substitution?
validatePrefixSubstitutionIsPossible(oldPrefix, newPrefix, currentResolver);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public boolean accept(final Node aNode) {

if (isNamespaceDefinition(attribute)
|| isElementReference(attribute)
|| isElementReferenceWithoutPrefix(attribute)
|| isTypeAttributeWithPrefix(attribute)
|| isExtension(attribute)) {
return true;
Expand All @@ -104,6 +105,20 @@ public boolean accept(final Node aNode) {
return false;
}

/**
* Discovers if the provided attribute is an unprefixed element reference, on the form
* <code>&lt;xs:element ref="aRequiredElementInTheTargetNamespace"/&gt;</code>.
* This is used as a workaround for schema generated with unprefixed {@code ref} values, so they can be
* treated as target-namespace references when rewriting the {@code tns} prefix.
*
* @param attribute the attribute to test.
* @return <code>true</code> if the provided attribute is named "ref" and its value has no namespace prefix.
*/
private boolean isElementReferenceWithoutPrefix(final Attr attribute) {
return REFERENCE_ATTRIBUTE_NAME.equals(attribute.getName())
&& !attribute.getValue().contains(":");
}

/**
* {@inheritDoc}
*/
Expand All @@ -114,6 +129,14 @@ public void process(final Node aNode) {
final Attr attribute = (Attr) aNode;
final Element parentElement = attribute.getOwnerElement();

if (isElementReferenceWithoutPrefix(attribute) && oldPrefix.equals("tns")) {
// For some reason schemagen does not generate the default namespace prefix for references to
// simpletypes, such as enum types => we want to add the default namespace prefix to those nodes in
// order to get xjc-compatible XSD:s
Comment thread
slachiewicz marked this conversation as resolved.
attribute.setValue(newPrefix + ":" + attribute.getValue());
return;
}

if (isNamespaceDefinition(attribute)) {

// Use the incredibly smooth DOM way to rename an attribute...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,20 +198,41 @@ public void process(final Node aNode) {
final String nodeValue = aNode.getNodeValue();

// Cache the namespace in both caches.
final String oldUriValue = prefix2Uri.put(cacheKey, nodeValue);
final String oldPrefixValue = uri2Prefix.put(nodeValue, cacheKey);
// The "tns" prefix is special: once "tns" is the canonical prefix for a URI,
// no other prefix should replace it. Conversely, "tns" is allowed to replace
// a previously seen non-"tns" prefix for the same URI.
final String oldPrefix = uri2Prefix.get(nodeValue);

if (oldPrefix != null && oldPrefix.equals("tns")) {
// "tns" already owns this URI — skip the new (non-tns) prefix entirely.
// Do not add it to prefix2Uri to keep both maps consistent.
return;
}

// Check sanity; we should not be overwriting values here.
if (oldUriValue != null) {
throw new IllegalStateException("Replaced URI [" + oldUriValue + "] with [" + aNode.getNodeValue()
+ "] for prefix [" + cacheKey + "]");
// Validate before mutating: ensure we are not overwriting an existing
// mapping for a different URI under the same prefix key.
final String existingUri = prefix2Uri.get(cacheKey);
if (existingUri != null && !existingUri.equals(nodeValue)) {
throw new IllegalStateException(
"Replaced URI [" + existingUri + "] with [" + nodeValue + "] for prefix [" + cacheKey + "]");
}
// If old prefix has changed, throw exception. The "tns" prefix may be overridden by a specific namespace in
// @XmlSchema(xmlns=...), and is therefore ignored here
if (oldPrefixValue != null && !oldPrefixValue.equals(cacheKey) && !cacheKey.equals("tns")) {
throw new IllegalStateException("Replaced prefix [" + oldPrefixValue + "] with [" + cacheKey
+ "] for URI [" + aNode.getNodeValue() + "]");

// Validate that we are not overwriting a non-tns prefix with another non-tns
// prefix for the same URI (genuine conflict).
if (oldPrefix != null && !oldPrefix.equals(cacheKey) && !cacheKey.equals("tns")) {
throw new IllegalStateException(
"Replaced prefix [" + oldPrefix + "] with [" + cacheKey + "] for URI [" + nodeValue + "]");
}

// If "tns" is overriding a previously seen prefix, remove the stale
// entry from prefix2Uri to keep both maps consistent.
if (oldPrefix != null && !oldPrefix.equals(cacheKey)) {
prefix2Uri.remove(oldPrefix);
}

// Now safe to mutate both maps.
prefix2Uri.put(cacheKey, nodeValue);
uri2Prefix.put(nodeValue, cacheKey);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.w3c.dom.Node;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* @author <a href="mailto:lj@jguru.se">Lennart J&ouml;relid</a>
Expand Down Expand Up @@ -51,6 +52,76 @@ void validateAcceptCriteria() {
assertEquals(newNamespacePrefix + ":aBaseType", extensionAttribute.getNodeValue());
}

@Test
void validateElementReferenceWithoutPrefixIsPrefixedForTns() {
// Assemble
final String oldNamespacePrefix = "tns";
final String newNamespacePrefix = "newTns";
final String xmlStream =
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + "<xs:schema version=\"1.0\"\n"
+ " targetNamespace=\"http://some/namespace\"\n"
+ " xmlns:tns=\"http://some/namespace\"\n"
+ " xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n"
+ " <xs:element name=\"enumElement\" type=\"xs:string\"/>\n"
+ " <xs:element ref=\"EnumType\" />\n"
+ // ref without prefix
"</xs:schema>\n";

final ChangeNamespacePrefixProcessor unitUnderTest =
new ChangeNamespacePrefixProcessor(oldNamespacePrefix, newNamespacePrefix);
final DebugNodeProcessor debugNodeProcessor = new DebugNodeProcessor(unitUnderTest);

// Act
final Document document = XsdGeneratorHelper.parseXmlStream(new StringReader(xmlStream));
XsdGeneratorHelper.process(document.getFirstChild(), true, debugNodeProcessor);

// Assert
// Find the ref attribute and check its value
boolean found = false;
for (Node node : debugNodeProcessor.getAcceptedNodes()) {
if ("ref".equals(node.getNodeName())) {
found = true;
assertEquals(newNamespacePrefix + ":EnumType", node.getNodeValue());
}
}
assertTrue(found, "Should have found a ref attribute without prefix");
}

@Test
void validateElementReferenceWithoutPrefixIsNotPrefixedForNonTns() {
// Assemble
final String oldNamespacePrefix = "foo";
final String newNamespacePrefix = "bar";
final String xmlStream =
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + "<xs:schema version=\"1.0\"\n"
+ " targetNamespace=\"http://some/namespace\"\n"
+ " xmlns:foo=\"http://some/namespace\"\n"
+ " xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n"
+ " <xs:element name=\"enumElement\" type=\"xs:string\"/>\n"
+ " <xs:element ref=\"EnumType\" />\n"
+ // ref without prefix
"</xs:schema>\n";

final ChangeNamespacePrefixProcessor unitUnderTest =
new ChangeNamespacePrefixProcessor(oldNamespacePrefix, newNamespacePrefix);
final DebugNodeProcessor debugNodeProcessor = new DebugNodeProcessor(unitUnderTest);

// Act
final Document document = XsdGeneratorHelper.parseXmlStream(new StringReader(xmlStream));
XsdGeneratorHelper.process(document.getFirstChild(), true, debugNodeProcessor);

// Assert
// Find the ref attribute and check its value remains unchanged
boolean found = false;
for (Node node : debugNodeProcessor.getAcceptedNodes()) {
if ("ref".equals(node.getNodeName())) {
found = true;
assertEquals("EnumType", node.getNodeValue());
}
}
assertTrue(found, "Should have found a ref attribute without prefix");
}

//
// Private helpers
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import javax.xml.XMLConstants;

import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -179,4 +180,130 @@ void validateCollectingSchemaInfoWithTnsPrefix() {
assertEquals("base", namespaceURI2PrefixMap.get("http://schemas.acme.com"));
assertEquals("tns", namespaceURI2PrefixMap.get("http://schemas.acme.com/student"));
}

@Test
void validateTnsPrefixIsNotOverwritten() {
// Assemble: XML with two prefixes for the same URI, one is 'tns'
final String xmlStream =
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + "<xs:schema version=\"1.0\"\n"
+ " targetNamespace=\"http://some/namespace\"\n"
+ " xmlns:tns=\"http://some/namespace\"\n"
+ " xmlns:other=\"http://some/namespace\"\n"
+ " xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n"
+ " <xs:element name=\"foo\" type=\"xs:string\"/>\n"
+ "</xs:schema>\n";
// Write to temp file
try {
File tempFile = File.createTempFile("test-schema", ".xsd");
tempFile.deleteOnExit();
Files.write(tempFile.toPath(), xmlStream.getBytes(java.nio.charset.StandardCharsets.UTF_8));

// Act
SimpleNamespaceResolver resolver = new SimpleNamespaceResolver(tempFile);
Map<String, String> uri2Prefix = resolver.getNamespaceURI2PrefixMap();

// Assert: 'tns' should be the prefix for the URI, not 'other'
assertEquals("tns", uri2Prefix.get("http://some/namespace"));
} catch (Exception e) {
fail("Exception during test: " + e.getMessage());
}
}

@Test
void validateExceptionThrownOnReplacedUri() throws Exception {
// Assemble: nested elements with the same prefix but different URIs
final String xmlStream = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
+ "<xs:schema xmlns:foo=\"http://uri1/\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n"
+ " <xs:element name=\"bar\" type=\"xs:string\"/>\n"
+ " <xs:element name=\"baz\">\n"
+ " <xs:complexType>\n"
+ " <xs:sequence>\n"
+ " <xs:element name=\"qux\" type=\"xs:string\" xmlns:foo=\"http://uri2/\"/>\n"
+ " </xs:sequence>\n"
+ " </xs:complexType>\n"
+ " </xs:element>\n"
+ "</xs:schema>\n";
File tempFile = File.createTempFile("test-schema-uri", ".xsd");
tempFile.deleteOnExit();
Files.write(tempFile.toPath(), xmlStream.getBytes(java.nio.charset.StandardCharsets.UTF_8));

// Act & Assert
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> {
new SimpleNamespaceResolver(tempFile);
});
assertTrue(ex.getMessage().contains("Replaced URI"));
}

@Test
void validateExceptionThrownOnReplacedPrefix() throws Exception {
// Assemble: two different prefixes for the same URI (not tns)
final String xmlStream =
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" + "<xs:schema version=\"1.0\"\n"
+ " xmlns:foo=\"http://uri/\"\n"
+ " xmlns:bar=\"http://uri/\"\n"
+ " xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n"
+ " <xs:element name=\"baz\" type=\"xs:string\"/>\n"
+ "</xs:schema>\n";
File tempFile = File.createTempFile("test-schema-prefix", ".xsd");
tempFile.deleteOnExit();
Files.write(tempFile.toPath(), xmlStream.getBytes(java.nio.charset.StandardCharsets.UTF_8));

// Act & Assert
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> {
new SimpleNamespaceResolver(tempFile);
});
assertTrue(ex.getMessage().contains("Replaced prefix"));
}

/**
* Verifies that prefix2Uri and uri2Prefix stay bidirectionally consistent when
* both a "tns" prefix and another prefix (e.g. "other") are declared for the
* same namespace URI. The "tns" prefix must win as the canonical mapping, and
* the non-"tns" prefix must not leave a stale entry in prefix2Uri.
*
* <p>This is independent of DOM attribute iteration order: regardless of whether
* "tns" or "other" is encountered first, the end state must have "tns" as the
* sole prefix for the URI in both maps.</p>
*/
@Test
void validateBidirectionalMapConsistencyWhenTnsAndOtherPrefixShareUri() throws Exception {
// Assemble: schema where "tns" and "other" both declare the same namespace URI
final String xmlStream = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
+ "<xs:schema version=\"1.0\"\n"
+ " targetNamespace=\"http://some/namespace\"\n"
+ " xmlns:tns=\"http://some/namespace\"\n"
+ " xmlns:other=\"http://some/namespace\"\n"
+ " xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">\n"
+ " <xs:element name=\"foo\" type=\"xs:string\"/>\n"
+ "</xs:schema>\n";

File tempFile = File.createTempFile("test-schema-bidir", ".xsd");
tempFile.deleteOnExit();
Files.write(tempFile.toPath(), xmlStream.getBytes(java.nio.charset.StandardCharsets.UTF_8));

// Act
SimpleNamespaceResolver resolver = new SimpleNamespaceResolver(tempFile);
Map<String, String> uri2PrefixMap = resolver.getNamespaceURI2PrefixMap();

// Assert 1: uri2Prefix → prefix2Uri round-trip is consistent.
// For every (uri → prefix) entry, getNamespaceURI(prefix) must return the same uri.
for (Map.Entry<String, String> entry : uri2PrefixMap.entrySet()) {
String uri = entry.getKey();
String prefix = entry.getValue();
assertEquals(
uri,
resolver.getNamespaceURI(prefix),
"Round-trip inconsistency: uri2Prefix maps [" + uri + "] to prefix [" + prefix
+ "], but getNamespaceURI(\"" + prefix + "\") returns a different URI");
}

// Assert 2: "other" must not be resolvable as a prefix.
// Since "tns" is the canonical prefix for the URI, the discarded "other" prefix
// must not remain in the internal prefix2Uri map.
assertNull(
resolver.getNamespaceURI("other"),
"The non-canonical prefix 'other' should not be resolvable via getNamespaceURI(). "
+ "When 'tns' is the canonical prefix for a URI, competing prefixes must be "
+ "removed from prefix2Uri to maintain bidirectional map consistency.");
}
}