Skip to content
Closed
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
42 changes: 42 additions & 0 deletions src/main/java/org/apache/xmlbeans/XmlOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public enum XmlOptionsKeys {
SAVE_CDATA_LENGTH_THRESHOLD,
SAVE_CDATA_ENTITY_COUNT_THRESHOLD,
SAVE_SAX_NO_NSDECLS_IN_ATTRIBUTES,
SAVE_NO_ATTRIBUTE_WHITESPACE_ESCAPE,
LOAD_REPLACE_DOCUMENT_ELEMENT,
LOAD_STRIP_WHITESPACE,
LOAD_STRIP_COMMENTS,
Expand Down Expand Up @@ -558,6 +559,47 @@ public boolean isSaveNoXmlDecl() {
return hasOption(XmlOptionsKeys.SAVE_NO_XML_DECL);
}

/**
* By default the saver now escapes a tab ({@code #x9}), newline ({@code #xA})
* and carriage return ({@code #xD}) inside an attribute value as a character
* reference ({@code 	}, {@code 
}, {@code 
}). Without that, the
* literal characters are normalised to spaces when the document is read back
* in, so a save followed by a load silently rewrites the value. Set this
* option to restore the long-standing behaviour of writing those characters
* literally.
*
* @return this
* @since 5.4.0
*/
public XmlOptions setSaveNoAttributeWhitespaceEscape() {
return setSaveNoAttributeWhitespaceEscape(true);
}

/**
* Sets whether tab, newline and carriage return are written literally in
* attribute values instead of being escaped as character references. See
* {@link #setSaveNoAttributeWhitespaceEscape()}.
*
* @param b {@code true} to write those characters literally (pre-5.4.0 behaviour)
* @return this
* @since 5.4.0
*/
public XmlOptions setSaveNoAttributeWhitespaceEscape(boolean b) {
return set(XmlOptionsKeys.SAVE_NO_ATTRIBUTE_WHITESPACE_ESCAPE, b);
}

/**
* Returns whether tab, newline and carriage return are written literally in
* attribute values instead of being escaped. See
* {@link #setSaveNoAttributeWhitespaceEscape()}.
*
* @return {@code true} if those characters are written literally
* @since 5.4.0
*/
public boolean isSaveNoAttributeWhitespaceEscape() {
return hasOption(XmlOptionsKeys.SAVE_NO_ATTRIBUTE_WHITESPACE_ESCAPE);
}


/**
* This option controls when saving will use CDATA blocks.
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/org/apache/xmlbeans/impl/store/Cursor.java
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ private void insertNode(Cur that, String text) {
assert isValid(that);
assert isValid();

if (text != null && text.length() > 0) {
if (text != null && !text.isEmpty()) {
that.next();
that.insertString(text);
that.toParent();
Expand Down Expand Up @@ -247,11 +247,11 @@ public void _setName(QName name) {
case PROCINST: {
validatePrefix(name.getLocalPart());

if (name.getNamespaceURI().length() > 0) {
if (!name.getNamespaceURI().isEmpty()) {
throw new IllegalArgumentException("Procinst name must have no URI");
}

if (name.getPrefix().length() > 0) {
if (!name.getPrefix().isEmpty()) {
throw new IllegalArgumentException("Procinst name must have no prefix");
}

Expand Down Expand Up @@ -585,7 +585,7 @@ public void _save(Writer w, XmlOptions options) throws IOException {
}

if (options != null && options.isSaveOptimizeForSpeed()) {
Saver.OptimizedForSpeedSaver.save(_cur, w); //ignore all other options
Saver.OptimizedForSpeedSaver.save(_cur, w, options); //ignore all other options bar attribute whitespace escaping
return;
}

Expand Down
31 changes: 25 additions & 6 deletions src/main/java/org/apache/xmlbeans/impl/store/Saver.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ abstract class Saver {
private final boolean _useDefaultNamespace;
private Map<String, String> _preComputedNamespaces;
private final boolean _saveNamespacesFirst;
private final boolean _escapeAttrWhitespace;

private final ArrayList<QName> _attrNames = new ArrayList<>();
private final ArrayList<String> _attrValues = new ArrayList<>();
Expand Down Expand Up @@ -131,6 +132,8 @@ protected void syntheticNamespace(String prefix, String uri, boolean considerDef

_saveNamespacesFirst = options.isSaveNamespacesFirst();

_escapeAttrWhitespace = !options.isSaveNoAttributeWhitespaceEscape();


_suggestedPrefixes = options.getSaveSuggestedPrefixes();

Expand Down Expand Up @@ -273,6 +276,10 @@ protected boolean saveNamespacesFirst() {
return _saveNamespacesFirst;
}

protected boolean escapeAttrWhitespace() {
return _escapeAttrWhitespace;
}

protected final boolean process() {
assert _locale.entered();

Expand Down Expand Up @@ -1370,6 +1377,12 @@ private void entitizeAttrValue(boolean replaceEscapedChar) {
i = replace(i, "&amp;");
} else if (ch == '"') {
i = replace(i, "&quot;");
} else if (ch == '\t' && escapeAttrWhitespace()) {
i = replace(i, "&#9;");
} else if (ch == '\n' && escapeAttrWhitespace()) {
i = replace(i, "&#10;");
} else if (ch == '\r' && escapeAttrWhitespace()) {
i = replace(i, "&#13;");
} else if (isEscapedChar(ch)) {
if (replaceEscapedChar) {
i = replace(i, _replaceChar.getEscapedString(ch));
Expand Down Expand Up @@ -1576,7 +1589,7 @@ private int resize(int cch, int i) {
(_out == _in && _free == 0) // buffer full
: "_buf.length:" + _cbuf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

long newLen = _cbuf == null ? _initialBufSize : _cbuf.length * 2;
long newLen = _cbuf == null ? _initialBufSize : _cbuf.length * 2L;
int used = getAvailable();

while (newLen - used < cch) {
Expand Down Expand Up @@ -1795,15 +1808,15 @@ static private class SaverIOException
}


OptimizedForSpeedSaver(Cur cur, Writer writer) {
super(cur, XmlOptions.maskNull(null));
OptimizedForSpeedSaver(Cur cur, Writer writer, XmlOptions options) {
super(cur, XmlOptions.maskNull(options));
_w = writer;
}

static void save(Cur cur, Writer writer)
static void save(Cur cur, Writer writer, XmlOptions options)
throws IOException {
try {
Saver saver = new OptimizedForSpeedSaver(cur, writer);
Saver saver = new OptimizedForSpeedSaver(cur, writer, options);
//noinspection StatementWithEmptyBody
while (saver.process()) {
}
Expand Down Expand Up @@ -2027,6 +2040,12 @@ private void emitAttrValue(CharSequence attVal) {
emit("&amp;");
} else if (ch == '"') {
emit("&quot;");
} else if (ch == '\t' && escapeAttrWhitespace()) {
emit("&#9;");
} else if (ch == '\n' && escapeAttrWhitespace()) {
emit("&#10;");
} else if (ch == '\r' && escapeAttrWhitespace()) {
emit("&#13;");
} else {
emit(ch);
}
Expand Down Expand Up @@ -2481,7 +2500,7 @@ public void write(byte[] buf, int off, int cbyte) {
void resize(int cbyte) {
assert cbyte > _free : cbyte + " !> " + _free;

long newLen = _buf == null ? _initialBufSize : _buf.length * 2;
long newLen = _buf == null ? _initialBufSize : _buf.length * 2L;
int used = getAvailable();

while (newLen - used < cbyte) {
Expand Down
10 changes: 10 additions & 0 deletions src/test/java/misc/checkin/XmlOptionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,14 @@ void testLoadStrictFloatingPointFlag() {
xmlOptions.setLoadStrictFloatingPoint(false);
assertFalse(xmlOptions.isLoadStrictFloatingPoint());
}

@Test
void testSaveNoAttributeWhitespaceEscapeFlag() {
XmlOptions xmlOptions = new XmlOptions();
assertFalse(xmlOptions.isSaveNoAttributeWhitespaceEscape());
xmlOptions.setSaveNoAttributeWhitespaceEscape();
assertTrue(xmlOptions.isSaveNoAttributeWhitespaceEscape());
xmlOptions.setSaveNoAttributeWhitespaceEscape(false);
assertFalse(xmlOptions.isSaveNoAttributeWhitespaceEscape());
}
}
43 changes: 43 additions & 0 deletions src/test/java/misc/detailed/CharEscapeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
import jira.xmlbeans177.TestListDocument;
import jira.xmlbeans177A.TestListADocument;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptionCharEscapeMap;
import org.apache.xmlbeans.XmlOptions;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -236,4 +238,45 @@ void testEscapeAttribute() throws Exception {
end2;
assertEquals(exp3, doc.xmlText(opts).replaceFirst("(?s)<!--.*-->", ""));
}

@Test
void testEscapeAttributeWhitespace() throws Exception {
// tab, newline and carriage return survive attribute-value normalisation
// only when written as character references; a literal char would be
// turned into a space when the document is read back in
XmlObject doc = XmlObject.Factory.parse("<r a=\"x&#9;y&#10;z&#13;w\"/>");

String expected = "<r a=\"x&#9;y&#10;z&#13;w\"/>";
assertEquals(expected, doc.xmlText());

StringWriter sw = new StringWriter();
doc.save(sw);
assertEquals(expected, sw.toString());

// round-trips with the value intact
org.apache.xmlbeans.XmlCursor c = XmlObject.Factory.parse(doc.xmlText()).newCursor();
c.toFirstChild();
assertEquals("x\ty\nz\rw", c.getAttributeText(new javax.xml.namespace.QName("a")));
c.dispose();
}

@Test
void testEscapeAttributeWhitespaceOptOut() throws Exception {
// setSaveNoAttributeWhitespaceEscape restores the pre-5.4.0 behaviour of
// writing tab, newline and carriage return literally
XmlObject doc = XmlObject.Factory.parse("<r a=\"x&#9;y&#10;z&#13;w\"/>");

XmlOptions opts = new XmlOptions().setSaveNoAttributeWhitespaceEscape();
String expected = "<r a=\"x\ty\nz\rw\"/>";
assertEquals(expected, doc.xmlText(opts));

StringWriter sw = new StringWriter();
doc.save(sw, opts);
assertEquals(expected, sw.toString());

// the optimize-for-speed writer honours the option too
StringWriter sw2 = new StringWriter();
doc.save(sw2, new XmlOptions(opts).setSaveOptimizeForSpeed(true));
assertEquals(expected, sw2.toString());
}
}
Loading