diff --git a/src/java.desktop/share/classes/javax/swing/JEditorPane.java b/src/java.desktop/share/classes/javax/swing/JEditorPane.java index 3134b8bace2..34528f9426b 100644 --- a/src/java.desktop/share/classes/javax/swing/JEditorPane.java +++ b/src/java.desktop/share/classes/javax/swing/JEditorPane.java @@ -1721,7 +1721,16 @@ public class JEditorPane extends JTextComponent { * @return the accessible text */ public AccessibleText getAccessibleText() { - return new JEditorPaneAccessibleHypertextSupport(); + JEditorPaneAccessibleHypertextSupport axText = (JEditorPaneAccessibleHypertextSupport) getClientProperty("JEditorPaneAccessibleHypertextSupport"); + if (axText != null && axText.doc != getDocument()) { + axText.doc.removeDocumentListener(axText); + axText = null; + } + if (axText == null) { + axText = new JEditorPaneAccessibleHypertextSupport(); + putClientProperty("JEditorPaneAccessibleHypertextSupport", axText); + } + return axText; } /** @@ -1999,25 +2008,32 @@ public class JEditorPane extends JTextComponent { linksValid = true; } + private final Document doc; + /** * Constructs a {@code JEditorPaneAccessibleHypertextSupport}. */ public JEditorPaneAccessibleHypertextSupport() { hyperlinks = new LinkVector(); - Document d = JEditorPane.this.getDocument(); - if (d != null) { - d.addDocumentListener(new DocumentListener() { - public void changedUpdate(DocumentEvent theEvent) { - linksValid = false; - } - public void insertUpdate(DocumentEvent theEvent) { - linksValid = false; - } - public void removeUpdate(DocumentEvent theEvent) { - linksValid = false; - } - }); - } + doc = JEditorPane.this.getDocument(); + } + + @Override + public void changedUpdate(DocumentEvent theEvent) { + linksValid = false; + super.changedUpdate(theEvent); + } + + @Override + public void insertUpdate(DocumentEvent theEvent) { + linksValid = false; + super.insertUpdate(theEvent); + } + + @Override + public void removeUpdate(DocumentEvent theEvent) { + linksValid = false; + super.removeUpdate(theEvent); } /** diff --git a/test/jdk/javax/accessibility/8380790/GetAccessibleTextAddsDocumentListeners.java b/test/jdk/javax/accessibility/8380790/GetAccessibleTextAddsDocumentListeners.java new file mode 100644 index 00000000000..77d00384bfe --- /dev/null +++ b/test/jdk/javax/accessibility/8380790/GetAccessibleTextAddsDocumentListeners.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +import javax.accessibility.AccessibleHypertext; +import javax.swing.JTextPane; +import javax.swing.event.DocumentListener; +import javax.swing.text.DefaultStyledDocument; +import javax.swing.text.Document; +import javax.swing.text.html.HTMLDocument; + +/* + * @test + * @bug 8380790 + * @summary make sure getAccessibleText() doesn't add DocumentListeners + * @run main GetAccessibleTextAddsDocumentListeners testOriginalComplaint + * @run main GetAccessibleTextAddsDocumentListeners testSetNewHTMLDocument + * @run main GetAccessibleTextAddsDocumentListeners testSetExistingHTMLDocument + * @run main GetAccessibleTextAddsDocumentListeners testDocumentListeners + */ + +public class GetAccessibleTextAddsDocumentListeners { + public static void main(String[] args) throws Exception { + GetAccessibleTextAddsDocumentListeners.class. + getMethod(args[0]).invoke(null); + } + + public static void testOriginalComplaint() throws Exception { + JTextPane textPane = new JTextPane(); + textPane.setContentType("text/html"); + for (int a = 0; a < 10_000; a++) { + textPane.getAccessibleContext().getAccessibleText(); + } + HTMLDocument doc = (HTMLDocument) textPane.getDocument(); + if (doc.getDocumentListeners().length > 1000) { + throw new Exception("too many DocumentListeners"); + } + } + + /** + * This makes sure getAccessibleText().getLinkCount() is based on the + * current Document (instead of based on an old/stale Document). + * + * see https://github.com/openjdk/jdk/pull/30401#issuecomment-4144874584 + */ + public static void testSetNewHTMLDocument() throws Exception { + JTextPane textPane = new JTextPane(); + textPane.setContentType("text/html"); + + // test some baseline expectations: + testLinkCount(textPane); + // now change the document + textPane.setDocument(new HTMLDocument()); + + testLinkCount(textPane); + } + + /** + * Test hyperlink count after calling `p.setDocument(p.getDocument());` + */ + public static void testSetExistingHTMLDocument() throws Exception { + JTextPane textPane = new JTextPane(); + textPane.setContentType("text/html"); + testLinkCount(textPane); + + textPane.setDocument(textPane.getDocument()); + testLinkCount(textPane); + } + + /** + * This tests AccessibleHypertext.getLinkCount() when a text pane is given + * 0, 1, and 2 link tags. + * + * By calling `getAccessibleText().getLinkCount()` we also trigger code + * that installs listeners in the JTextPane. + */ + private static void testLinkCount(JTextPane textPane) throws Exception { + textPane.setText(""); + assertEquals(0, ((AccessibleHypertext) textPane. + getAccessibleContext().getAccessibleText()).getLinkCount()); + + textPane.setText("y"); + assertEquals(1, ((AccessibleHypertext) textPane. + getAccessibleContext().getAccessibleText()).getLinkCount()); + + textPane.setText("y y"); + assertEquals(2, ((AccessibleHypertext) textPane. + getAccessibleContext().getAccessibleText()).getLinkCount()); + } + + /** + * This switches between a DefaultStyledDocument and an HTMLDocument + * several times and tests whether we ended up with too many + * DocumentListeners + * + * see https://github.com/openjdk/jdk/pull/30401#discussion_r3025612299 + */ + public static void testDocumentListeners() throws Exception { + JTextPane textPane = new JTextPane(); + + // each call to setContentType replaces textPane.getDocument() + textPane.setContentType("text/html"); + + for (int a = 0; a < 100; a++) { + textPane.setContentType("text/plain"); + assertTrue(!(textPane.getAccessibleContext().getAccessibleText() + instanceof AccessibleHypertext)); + + textPane.setContentType("text/html"); + testLinkCount(textPane); + } + + int docListenerCount = log("testDocumentListeners_simpleCase", + textPane.getDocument()); + assertTrue(docListenerCount < 10); + } + + private static void assertEquals(int expected, int actual) + throws Exception { + if (expected != actual) { + throw new Exception("expected: " + expected + ", actual: " + actual); + } + } + + private static void assertTrue(boolean b) throws Exception { + if (!b) { + throw new Exception("expected: true, actual: false"); + } + } + + /** + * This returns the number of DocumentListeners, and it writes them + * to System.out. + */ + private static int log(String name, Document doc) { + DocumentListener[] docListeners; + if (doc instanceof HTMLDocument) { + HTMLDocument htmlDoc = (HTMLDocument) doc; + docListeners = htmlDoc.getDocumentListeners(); + } else { + DefaultStyledDocument styledDoc = (DefaultStyledDocument) doc; + docListeners = styledDoc.getDocumentListeners(); + } + + System.out.println(docListeners.length + " listeners at \"" + + name + "\""); + for (DocumentListener l : docListeners) { + System.out.println("\t" + l.getClass().getName() + " 0x" + + Long.toHexString(System.identityHashCode(l))); + } + return docListeners.length; + } +}