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
9 changes: 9 additions & 0 deletions exist-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,7 @@ The BaseX Team. The original license statement is also included below.]]></pream
<log4j.configurationFile>${project.build.testOutputDirectory}/log4j2.xml</log4j.configurationFile>
</systemPropertyVariables>

<forkedProcessTimeoutInSeconds>180</forkedProcessTimeoutInSeconds>
<excludes>

<!-- NOTE: these can still exhibit deadlocks
Expand All @@ -1218,6 +1219,14 @@ The BaseX Team. The original license statement is also included below.]]></pream
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<forkedProcessTimeoutInSeconds>180</forkedProcessTimeoutInSeconds>
<excludes>
<!-- Pre-existing deadlocks during BrokerPool initialization -->
<!-- see https://github.com/eXist-db/exist/issues/4140 -->
<!-- see https://github.com/eXist-db/exist/issues/3685 -->
<exclude>org.exist.storage.lock.DeadlockIT</exclude>
<exclude>org.exist.xmldb.RemoveCollectionIT</exclude>
</excludes>
<argLine>@{jacocoArgLine} --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dfile.encoding=${project.build.sourceEncoding} -Dexist.recovery.progressbar.hide=true</argLine>
<systemPropertyVariables>
<jetty.home>${project.basedir}/../exist-jetty-config/target/classes/org/exist/jetty</jetty.home>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public class EXistOutputKeys {
*/
public static final String ITEM_SEPARATOR = "item-separator";

// --- QT4 Serialization 4.0 parameters ---
public static final String CANONICAL = "canonical";
public static final String ESCAPE_SOLIDUS = "escape-solidus";
public static final String JSON_LINES = "json-lines";

public static final String OMIT_ORIGINAL_XML_DECLARATION = "omit-original-xml-declaration";

public static final String OUTPUT_DOCTYPE = "output-doctype";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,27 @@ protected SerializerWriter getDefaultWriter() {
public void setOutput(Writer writer, Properties properties) {
outputProperties = Objects.requireNonNullElseGet(properties, () -> new Properties(defaultProperties));
final String method = outputProperties.getProperty(OutputKeys.METHOD, "xml");
final String htmlVersionProp = outputProperties.getProperty(EXistOutputKeys.HTML_VERSION, "1.0");

// For html/xhtml methods, determine HTML version:
// 1. Use html-version if explicitly set
// 2. Otherwise use version (W3C spec: version controls HTML version for html method)
// 3. Default to 5.0
double htmlVersion;
try {
htmlVersion = Double.parseDouble(htmlVersionProp);
} catch (NumberFormatException e) {
htmlVersion = 1.0;
final String explicitHtmlVersion = outputProperties.getProperty(EXistOutputKeys.HTML_VERSION);
if (explicitHtmlVersion != null) {
try {
htmlVersion = Double.parseDouble(explicitHtmlVersion);
} catch (NumberFormatException e) {
htmlVersion = 5.0;
}
} else if (("html".equalsIgnoreCase(method) || "xhtml".equalsIgnoreCase(method))
&& outputProperties.getProperty(OutputKeys.VERSION) != null) {
try {
htmlVersion = Double.parseDouble(outputProperties.getProperty(OutputKeys.VERSION));
} catch (NumberFormatException e) {
htmlVersion = 5.0;
}
} else {
htmlVersion = 5.0;
}

final SerializerWriter baseSerializerWriter = getBaseSerializerWriter(method, htmlVersion);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,15 @@ private void writeAtomic(AtomicValue value) throws IOException, SAXException, XP
}

private void writeDouble(final DoubleValue item) throws SAXException {
final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.US);
symbols.setExponentSeparator("e");
final DecimalFormat df = new DecimalFormat("0.0##########################E0", symbols);
writeText(df.format(item.getDouble()));
final double d = item.getDouble();
if (Double.isInfinite(d) || Double.isNaN(d)) {
writeText(item.getStringValue());
} else {
final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.US);
symbols.setExponentSeparator("e");
final DecimalFormat df = new DecimalFormat("0.0##########################E0", symbols);
writeText(df.format(d));
}
}

private void writeArray(final ArrayType array) throws XPathException, SAXException, TransformerException {
Expand All @@ -215,9 +220,7 @@ private void writeArray(final ArrayType array) throws XPathException, SAXExcepti

private void writeMap(final AbstractMapType map) throws SAXException, XPathException, TransformerException {
try {
writer.write("map");
addSpaceIfIndent();
writer.write('{');
writer.write("map{");
addIndent();
indent();
for (final Iterator<IEntry<AtomicValue, Sequence>> i = map.iterator(); i.hasNext(); ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,44 @@ protected void closeStartTag(boolean isEmpty) throws TransformerException {
}
}

@Override
public void processingInstruction(String target, String data) throws TransformerException {
try {
closeStartTag(false);
final Writer writer = getWriter();
writer.write("<?");
writer.write(target);
if (data != null && !data.isEmpty()) {
writer.write(' ');
writer.write(data);
}
writer.write('>');
} catch (IOException e) {
throw new TransformerException(e.getMessage(), e);
}
}

@Override
protected boolean needsEscape(char ch) {
if (RAW_TEXT_ELEMENTS.contains(currentTag)) {
return false;
}
return super.needsEscape(ch);
}

@Override
protected boolean needsEscape(final char ch, final boolean inAttribute) {
// In raw text elements (script, style), suppress escaping for TEXT content only.
// Attribute values must always be escaped, even on raw text elements.
if (!inAttribute && RAW_TEXT_ELEMENTS.contains(currentTag)) {
return false;
}
// For attributes, always return true (bypass the 1-arg override
// which returns false for all script/style content)
if (inAttribute) {
return true;
}
return super.needsEscape(ch, inAttribute);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
import java.io.Writer;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerException;
Expand All @@ -48,6 +50,8 @@ public class IndentingXMLWriter extends XMLWriter {
private boolean sameline = false;
private boolean whitespacePreserve = false;
private final Deque<Integer> whitespacePreserveStack = new ArrayDeque<>();
private Set<String> suppressIndentation = null;
private int suppressIndentDepth = 0;

public IndentingXMLWriter() {
super();
Expand Down Expand Up @@ -75,6 +79,9 @@ public void startElement(final String namespaceURI, final String localName, fina
indent();
}
super.startElement(namespaceURI, localName, qname);
if (isSuppressIndentation(localName)) {
suppressIndentDepth++;
}
addIndent();
afterTag = true;
sameline = true;
Expand All @@ -86,6 +93,9 @@ public void startElement(final QName qname) throws TransformerException {
indent();
}
super.startElement(qname);
if (isSuppressIndentation(qname.getLocalPart())) {
suppressIndentDepth++;
}
addIndent();
afterTag = true;
sameline = true;
Expand All @@ -95,6 +105,9 @@ public void startElement(final QName qname) throws TransformerException {
public void endElement(final String namespaceURI, final String localName, final String qname) throws TransformerException {
endIndent(namespaceURI, localName);
super.endElement(namespaceURI, localName, qname);
if (isSuppressIndentation(localName) && suppressIndentDepth > 0) {
suppressIndentDepth--;
}
popWhitespacePreserve(); // apply ancestor's xml:space value _after_ end element
sameline = isInlineTag(namespaceURI, localName);
afterTag = true;
Expand All @@ -104,6 +117,9 @@ public void endElement(final String namespaceURI, final String localName, final
public void endElement(final QName qname) throws TransformerException {
endIndent(qname.getNamespaceURI(), qname.getLocalPart());
super.endElement(qname);
if (isSuppressIndentation(qname.getLocalPart()) && suppressIndentDepth > 0) {
suppressIndentDepth--;
}
popWhitespacePreserve(); // apply ancestor's xml:space value _after_ end element
sameline = isInlineTag(qname.getNamespaceURI(), qname.getLocalPart());
afterTag = true;
Expand Down Expand Up @@ -164,7 +180,29 @@ public void setOutputProperties(final Properties properties) {
} catch (final NumberFormatException e) {
LOG.warn("Invalid indentation value: '{}'", option);
}
indent = "yes".equals(outputProperties.getProperty(OutputKeys.INDENT, "no"));
final String indentValue = outputProperties.getProperty(OutputKeys.INDENT, "no").trim();
indent = "yes".equals(indentValue) || "true".equals(indentValue) || "1".equals(indentValue);
final String suppressProp = outputProperties.getProperty("suppress-indentation");
if (suppressProp != null && !suppressProp.isEmpty()) {
suppressIndentation = new HashSet<>();
for (final String name : suppressProp.split("\\s+")) {
if (!name.isEmpty()) {
// Handle URI-qualified names: Q{ns}local or {ns}local → extract local part
if (name.startsWith("Q{") || name.startsWith("{")) {
final int closeBrace = name.indexOf('}');
if (closeBrace > 0 && closeBrace < name.length() - 1) {
suppressIndentation.add(name.substring(closeBrace + 1));
} else {
suppressIndentation.add(name);
}
} else {
suppressIndentation.add(name);
}
}
}
} else {
suppressIndentation = null;
}
}

@Override
Expand Down Expand Up @@ -220,8 +258,12 @@ protected void addSpaceIfIndent() throws IOException {
writer.write(' ');
}

private boolean isSuppressIndentation(final String localName) {
return suppressIndentation != null && suppressIndentation.contains(localName);
}

protected void indent() throws TransformerException {
if (!indent || whitespacePreserve) {
if (!indent || whitespacePreserve || suppressIndentDepth > 0) {
return;
}
final int spaces = indentAmount * level;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.Writer;
import javax.xml.transform.TransformerException;

import org.exist.storage.serializers.EXistOutputKeys;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectSet;

Expand Down Expand Up @@ -128,7 +129,45 @@ protected void writeDoctype(String rootElement) throws TransformerException {
return;
}

documentType("html", null, null);
// Canonical serialization: never output DOCTYPE
final String canonicalProp = outputProperties != null
? outputProperties.getProperty(EXistOutputKeys.CANONICAL) : null;
if ("yes".equals(canonicalProp) || "true".equals(canonicalProp) || "1".equals(canonicalProp)) {
doctypeWritten = true;
return;
}

// Only output DOCTYPE when the root element is <html> (case-insensitive)
// Per W3C Serialization: DOCTYPE is for the html element only, not fragments
final String localName = rootElement.contains(":") ? rootElement.substring(rootElement.indexOf(':') + 1) : rootElement;
if (!"html".equalsIgnoreCase(localName)) {
doctypeWritten = true; // suppress future attempts
return;
}

final String publicId = outputProperties != null
? outputProperties.getProperty(javax.xml.transform.OutputKeys.DOCTYPE_PUBLIC) : null;
final String systemId = outputProperties != null
? outputProperties.getProperty(javax.xml.transform.OutputKeys.DOCTYPE_SYSTEM) : null;
final String method = outputProperties != null
? outputProperties.getProperty(javax.xml.transform.OutputKeys.METHOD, "xhtml") : "xhtml";

if ("xhtml".equalsIgnoreCase(method)) {
// XHTML: per W3C spec section 5.2, only output doctype-public when
// doctype-system is also present
if (systemId != null) {
documentType("html", publicId, systemId);
} else if (publicId == null) {
// Neither set — simple DOCTYPE
documentType("html", null, null);
} else {
// doctype-public without doctype-system — suppress DOCTYPE for XHTML
doctypeWritten = true;
}
} else {
// HTML method: pass through doctype-public and doctype-system as set
documentType("html", publicId, systemId);
}
doctypeWritten = true;
}
}
Loading
Loading