diff --git a/build.sbt b/build.sbt index 7b86182d..eb8ab9b6 100644 --- a/build.sbt +++ b/build.sbt @@ -68,6 +68,15 @@ lazy val xml = crossProject(JSPlatform, JVMPlatform, NativePlatform) // we compare classes built on JDK 16 (which we only do on CI, not at release time) // to previous-version artifacts that were built on 8. see scala/scala-xml#501 exclude[DirectMissingMethodProblem]("scala.xml.include.sax.XIncluder.declaration"), + + // caused by the switch from DefaultHandler to DefaultHandler2: + exclude[MissingTypesProblem]("scala.xml.parsing.FactoryAdapter"), + exclude[MissingTypesProblem]("scala.xml.parsing.NoBindingFactoryAdapter"), + + exclude[DirectMissingMethodProblem]("scala.xml.parsing.FactoryAdapter.comment"), + exclude[ReversedMissingMethodProblem]("scala.xml.parsing.FactoryAdapter.createComment"), + exclude[DirectMissingMethodProblem]("scala.xml.parsing.FactoryAdapter.createComment"), + exclude[DirectMissingMethodProblem]("scala.xml.parsing.NoBindingFactoryAdapter.createComment") ) }, diff --git a/jvm/src/test/scala/scala/xml/XMLTest.scala b/jvm/src/test/scala/scala/xml/XMLTest.scala index e4321aa6..70b59a92 100644 --- a/jvm/src/test/scala/scala/xml/XMLTest.scala +++ b/jvm/src/test/scala/scala/xml/XMLTest.scala @@ -6,7 +6,6 @@ import org.junit.{Test => UnitTest} import org.junit.Assert.assertTrue import org.junit.Assert.assertFalse import org.junit.Assert.assertEquals -import scala.xml.parsing.ConstructingParser import java.io.StringWriter import java.io.ByteArrayOutputStream import java.io.StringReader @@ -581,6 +580,21 @@ class XMLTestJVM { XML.loadString(broken) } + @UnitTest + def issue508: Unit = { + def check(xml: String): Unit = assertEquals(xml, XML.loadString(xml).toString) + + check(" suffix") + check("prefix suffix") + check("prefix suffix") + + // TODO since XMLLoader retrieves FactoryAdapter.rootNode, + // capturing comments before and after the root element is not currently possible + // (by the way, the same applies to processing instructions). + //check("text") + //check("text") + } + @UnitTest def nodeSeqNs: Unit = { val x = { @@ -746,5 +760,4 @@ class XMLTestJVM { assertEquals("", x.xEntityValue()) } - } diff --git a/shared/src/main/scala/scala/xml/factory/NodeFactory.scala b/shared/src/main/scala/scala/xml/factory/NodeFactory.scala index ad70d888..7ae0fd7e 100644 --- a/shared/src/main/scala/scala/xml/factory/NodeFactory.scala +++ b/shared/src/main/scala/scala/xml/factory/NodeFactory.scala @@ -34,7 +34,7 @@ trait NodeFactory[A <: Node] { def eqElements(ch1: Seq[Node], ch2: Seq[Node]): Boolean = ch1.view.zipAll(ch2.view, null, null) forall { case (x, y) => x eq y } - def nodeEquals(n: Node, pre: String, name: String, attrSeq: MetaData, scope: NamespaceBinding, children: Seq[Node]) = + def nodeEquals(n: Node, pre: String, name: String, attrSeq: MetaData, scope: NamespaceBinding, children: Seq[Node]): Boolean = n.prefix == pre && n.label == name && n.attributes == attrSeq && @@ -55,7 +55,7 @@ trait NodeFactory[A <: Node] { } } - def makeText(s: String) = Text(s) + def makeText(s: String): Text = Text(s) def makeComment(s: String): Seq[Comment] = if (ignoreComments) Nil else List(Comment(s)) def makeProcInstr(t: String, s: String): Seq[ProcInstr] = diff --git a/shared/src/main/scala/scala/xml/factory/XMLLoader.scala b/shared/src/main/scala/scala/xml/factory/XMLLoader.scala index 00f43074..7562c0c1 100644 --- a/shared/src/main/scala/scala/xml/factory/XMLLoader.scala +++ b/shared/src/main/scala/scala/xml/factory/XMLLoader.scala @@ -14,9 +14,10 @@ package scala package xml package factory +import org.xml.sax.SAXNotRecognizedException import javax.xml.parsers.SAXParserFactory -import parsing.{ FactoryAdapter, NoBindingFactoryAdapter } -import java.io.{ InputStream, Reader, File, FileDescriptor } +import parsing.{FactoryAdapter, NoBindingFactoryAdapter} +import java.io.{File, FileDescriptor, InputStream, Reader} import java.net.URL /** @@ -28,7 +29,7 @@ trait XMLLoader[T <: Node] { def adapter: FactoryAdapter = new NoBindingFactoryAdapter() private lazy val parserInstance = new ThreadLocal[SAXParser] { - override def initialValue = { + override def initialValue: SAXParser = { val parser = SAXParserFactory.newInstance() parser.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true) parser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) @@ -52,6 +53,12 @@ trait XMLLoader[T <: Node] { def loadXML(source: InputSource, parser: SAXParser): T = { val newAdapter = adapter + try { + parser.setProperty("http://xml.org/sax/properties/lexical-handler", newAdapter) + } catch { + case _: SAXNotRecognizedException => + } + newAdapter.scopeStack = TopScope :: newAdapter.scopeStack parser.parse(source, newAdapter) newAdapter.scopeStack = newAdapter.scopeStack.tail diff --git a/shared/src/main/scala/scala/xml/parsing/FactoryAdapter.scala b/shared/src/main/scala/scala/xml/parsing/FactoryAdapter.scala index accc9a68..c6c8849d 100644 --- a/shared/src/main/scala/scala/xml/parsing/FactoryAdapter.scala +++ b/shared/src/main/scala/scala/xml/parsing/FactoryAdapter.scala @@ -16,10 +16,10 @@ package parsing import scala.collection.Seq import org.xml.sax.Attributes -import org.xml.sax.helpers.DefaultHandler +import org.xml.sax.ext.DefaultHandler2 // can be mixed into FactoryAdapter if desired -trait ConsoleErrorHandler extends DefaultHandler { +trait ConsoleErrorHandler extends DefaultHandler2 { // ignore warning, crimson warns even for entity resolution! override def warning(ex: SAXParseException): Unit = {} override def error(ex: SAXParseException): Unit = printError("Error", ex) @@ -39,8 +39,8 @@ trait ConsoleErrorHandler extends DefaultHandler { * namespace bindings, without relying on namespace handling of the * underlying SAX parser. */ -abstract class FactoryAdapter extends DefaultHandler with factory.XMLLoader[Node] { - var rootElem: Node = null +abstract class FactoryAdapter extends DefaultHandler2 with factory.XMLLoader[Node] { + var rootElem: Node = _ val buffer = new StringBuilder() /** List of attributes @@ -72,7 +72,7 @@ abstract class FactoryAdapter extends DefaultHandler with factory.XMLLoader[Node */ var scopeStack = List.empty[NamespaceBinding] - var curTag: String = null + var curTag: String = _ var capture: Boolean = false // abstract methods @@ -105,6 +105,11 @@ abstract class FactoryAdapter extends DefaultHandler with factory.XMLLoader[Node */ def createProcInstr(target: String, data: String): Seq[ProcInstr] + /** + * creates a new comment node. + */ + def createComment(characters: String): Seq[Comment] + // // ContentHandler methods // @@ -118,7 +123,7 @@ abstract class FactoryAdapter extends DefaultHandler with factory.XMLLoader[Node * @param length */ override def characters(ch: Array[Char], offset: Int, length: Int): Unit = { - if (!capture) return + if (!capture) () // compliant: report every character else if (!normalizeWhitespace) buffer.appendAll(ch, offset, length) // normalizing whitespace is not compliant, but useful @@ -170,7 +175,7 @@ abstract class FactoryAdapter extends DefaultHandler with factory.XMLLoader[Node if (pre == "xmlns" || (pre == null && qname == "xmlns")) { val arg = if (pre == null) null else key - scpe = new NamespaceBinding(arg, nullIfEmpty(value), scpe) + scpe = NamespaceBinding(arg, nullIfEmpty(value), scpe) } else m = Attribute(Option(pre), key, Text(value), m) } @@ -183,7 +188,7 @@ abstract class FactoryAdapter extends DefaultHandler with factory.XMLLoader[Node * captures text, possibly normalizing whitespace */ def captureText(): Unit = { - if (capture && buffer.length > 0) + if (capture && buffer.nonEmpty) hStack = createText(buffer.toString) :: hStack buffer.clear() @@ -226,4 +231,10 @@ abstract class FactoryAdapter extends DefaultHandler with factory.XMLLoader[Node captureText() hStack = hStack.reverse_:::(createProcInstr(target, data).toList) } + + override def comment(ch: Array[Char], start: Int, length: Int): Unit = { + captureText() + val commentText: String = String.valueOf(ch.slice(start, start + length)) + hStack = hStack.reverse_:::(createComment(commentText).toList) + } } diff --git a/shared/src/main/scala/scala/xml/parsing/NoBindingFactoryAdapter.scala b/shared/src/main/scala/scala/xml/parsing/NoBindingFactoryAdapter.scala index 819d8ca3..6f1384fc 100644 --- a/shared/src/main/scala/scala/xml/parsing/NoBindingFactoryAdapter.scala +++ b/shared/src/main/scala/scala/xml/parsing/NoBindingFactoryAdapter.scala @@ -23,20 +23,23 @@ import factory.NodeFactory */ class NoBindingFactoryAdapter extends FactoryAdapter with NodeFactory[Elem] { /** True. Every XML node may contain text that the application needs */ - def nodeContainsText(label: String) = true + override def nodeContainsText(label: String) = true /** From NodeFactory. Constructs an instance of scala.xml.Elem -- TODO: deprecate as in Elem */ - protected def create(pre: String, label: String, attrs: MetaData, scope: NamespaceBinding, children: Seq[Node]): Elem = + override protected def create(pre: String, label: String, attrs: MetaData, scope: NamespaceBinding, children: Seq[Node]): Elem = Elem(pre, label, attrs, scope, children.isEmpty, children: _*) /** From FactoryAdapter. Creates a node. never creates the same node twice, using hash-consing. TODO: deprecate as in Elem, or forward to create?? */ - def createNode(pre: String, label: String, attrs: MetaData, scope: NamespaceBinding, children: List[Node]): Elem = + override def createNode(pre: String, label: String, attrs: MetaData, scope: NamespaceBinding, children: List[Node]): Elem = Elem(pre, label, attrs, scope, children.isEmpty, children: _*) /** Creates a text node. */ - def createText(text: String) = Text(text) + override def createText(text: String): Text = makeText(text) /** Creates a processing instruction. */ - def createProcInstr(target: String, data: String) = makeProcInstr(target, data) + override def createProcInstr(target: String, data: String): Seq[ProcInstr] = makeProcInstr(target, data) + + /** Creates a comment. */ + override def createComment(characters: String): Seq[Comment] = makeComment(characters) }