Skip to content

Commit 0e57fb9

Browse files
Jeremy Woodprrace
authored andcommitted
8380790: Each invocation of getAccessibleText() adds DocumentListener
Reviewed-by: prr, serb, kizune
1 parent f66380e commit 0e57fb9

2 files changed

Lines changed: 204 additions & 15 deletions

File tree

src/java.desktop/share/classes/javax/swing/JEditorPane.java

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,7 +1721,16 @@ protected class AccessibleJEditorPaneHTML extends AccessibleJEditorPane {
17211721
* @return the accessible text
17221722
*/
17231723
public AccessibleText getAccessibleText() {
1724-
return new JEditorPaneAccessibleHypertextSupport();
1724+
JEditorPaneAccessibleHypertextSupport axText = (JEditorPaneAccessibleHypertextSupport) getClientProperty("JEditorPaneAccessibleHypertextSupport");
1725+
if (axText != null && axText.doc != getDocument()) {
1726+
axText.doc.removeDocumentListener(axText);
1727+
axText = null;
1728+
}
1729+
if (axText == null) {
1730+
axText = new JEditorPaneAccessibleHypertextSupport();
1731+
putClientProperty("JEditorPaneAccessibleHypertextSupport", axText);
1732+
}
1733+
return axText;
17251734
}
17261735

17271736
/**
@@ -1999,25 +2008,32 @@ private void buildLinkTable() {
19992008
linksValid = true;
20002009
}
20012010

2011+
private final Document doc;
2012+
20022013
/**
20032014
* Constructs a {@code JEditorPaneAccessibleHypertextSupport}.
20042015
*/
20052016
public JEditorPaneAccessibleHypertextSupport() {
20062017
hyperlinks = new LinkVector();
2007-
Document d = JEditorPane.this.getDocument();
2008-
if (d != null) {
2009-
d.addDocumentListener(new DocumentListener() {
2010-
public void changedUpdate(DocumentEvent theEvent) {
2011-
linksValid = false;
2012-
}
2013-
public void insertUpdate(DocumentEvent theEvent) {
2014-
linksValid = false;
2015-
}
2016-
public void removeUpdate(DocumentEvent theEvent) {
2017-
linksValid = false;
2018-
}
2019-
});
2020-
}
2018+
doc = JEditorPane.this.getDocument();
2019+
}
2020+
2021+
@Override
2022+
public void changedUpdate(DocumentEvent theEvent) {
2023+
linksValid = false;
2024+
super.changedUpdate(theEvent);
2025+
}
2026+
2027+
@Override
2028+
public void insertUpdate(DocumentEvent theEvent) {
2029+
linksValid = false;
2030+
super.insertUpdate(theEvent);
2031+
}
2032+
2033+
@Override
2034+
public void removeUpdate(DocumentEvent theEvent) {
2035+
linksValid = false;
2036+
super.removeUpdate(theEvent);
20212037
}
20222038

20232039
/**
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
25+
import javax.accessibility.AccessibleHypertext;
26+
import javax.swing.JTextPane;
27+
import javax.swing.event.DocumentListener;
28+
import javax.swing.text.DefaultStyledDocument;
29+
import javax.swing.text.Document;
30+
import javax.swing.text.html.HTMLDocument;
31+
32+
/*
33+
* @test
34+
* @bug 8380790
35+
* @summary make sure getAccessibleText() doesn't add DocumentListeners
36+
* @run main GetAccessibleTextAddsDocumentListeners testOriginalComplaint
37+
* @run main GetAccessibleTextAddsDocumentListeners testSetNewHTMLDocument
38+
* @run main GetAccessibleTextAddsDocumentListeners testSetExistingHTMLDocument
39+
* @run main GetAccessibleTextAddsDocumentListeners testDocumentListeners
40+
*/
41+
42+
public class GetAccessibleTextAddsDocumentListeners {
43+
public static void main(String[] args) throws Exception {
44+
GetAccessibleTextAddsDocumentListeners.class.
45+
getMethod(args[0]).invoke(null);
46+
}
47+
48+
public static void testOriginalComplaint() throws Exception {
49+
JTextPane textPane = new JTextPane();
50+
textPane.setContentType("text/html");
51+
for (int a = 0; a < 10_000; a++) {
52+
textPane.getAccessibleContext().getAccessibleText();
53+
}
54+
HTMLDocument doc = (HTMLDocument) textPane.getDocument();
55+
if (doc.getDocumentListeners().length > 1000) {
56+
throw new Exception("too many DocumentListeners");
57+
}
58+
}
59+
60+
/**
61+
* This makes sure getAccessibleText().getLinkCount() is based on the
62+
* current Document (instead of based on an old/stale Document).
63+
*
64+
* see https://github.com/openjdk/jdk/pull/30401#issuecomment-4144874584
65+
*/
66+
public static void testSetNewHTMLDocument() throws Exception {
67+
JTextPane textPane = new JTextPane();
68+
textPane.setContentType("text/html");
69+
70+
// test some baseline expectations:
71+
testLinkCount(textPane);
72+
// now change the document
73+
textPane.setDocument(new HTMLDocument());
74+
75+
testLinkCount(textPane);
76+
}
77+
78+
/**
79+
* Test hyperlink count after calling `p.setDocument(p.getDocument());`
80+
*/
81+
public static void testSetExistingHTMLDocument() throws Exception {
82+
JTextPane textPane = new JTextPane();
83+
textPane.setContentType("text/html");
84+
testLinkCount(textPane);
85+
86+
textPane.setDocument(textPane.getDocument());
87+
testLinkCount(textPane);
88+
}
89+
90+
/**
91+
* This tests AccessibleHypertext.getLinkCount() when a text pane is given
92+
* 0, 1, and 2 link tags.
93+
*
94+
* By calling `getAccessibleText().getLinkCount()` we also trigger code
95+
* that installs listeners in the JTextPane.
96+
*/
97+
private static void testLinkCount(JTextPane textPane) throws Exception {
98+
textPane.setText("");
99+
assertEquals(0, ((AccessibleHypertext) textPane.
100+
getAccessibleContext().getAccessibleText()).getLinkCount());
101+
102+
textPane.setText("<a href=\"x\">y</a>");
103+
assertEquals(1, ((AccessibleHypertext) textPane.
104+
getAccessibleContext().getAccessibleText()).getLinkCount());
105+
106+
textPane.setText("<a href=\"x\">y</a> <a href=\"x\">y</a>");
107+
assertEquals(2, ((AccessibleHypertext) textPane.
108+
getAccessibleContext().getAccessibleText()).getLinkCount());
109+
}
110+
111+
/**
112+
* This switches between a DefaultStyledDocument and an HTMLDocument
113+
* several times and tests whether we ended up with too many
114+
* DocumentListeners
115+
*
116+
* see https://github.com/openjdk/jdk/pull/30401#discussion_r3025612299
117+
*/
118+
public static void testDocumentListeners() throws Exception {
119+
JTextPane textPane = new JTextPane();
120+
121+
// each call to setContentType replaces textPane.getDocument()
122+
textPane.setContentType("text/html");
123+
124+
for (int a = 0; a < 100; a++) {
125+
textPane.setContentType("text/plain");
126+
assertTrue(!(textPane.getAccessibleContext().getAccessibleText()
127+
instanceof AccessibleHypertext));
128+
129+
textPane.setContentType("text/html");
130+
testLinkCount(textPane);
131+
}
132+
133+
int docListenerCount = log("testDocumentListeners_simpleCase",
134+
textPane.getDocument());
135+
assertTrue(docListenerCount < 10);
136+
}
137+
138+
private static void assertEquals(int expected, int actual)
139+
throws Exception {
140+
if (expected != actual) {
141+
throw new Exception("expected: " + expected + ", actual: " + actual);
142+
}
143+
}
144+
145+
private static void assertTrue(boolean b) throws Exception {
146+
if (!b) {
147+
throw new Exception("expected: true, actual: false");
148+
}
149+
}
150+
151+
/**
152+
* This returns the number of DocumentListeners, and it writes them
153+
* to System.out.
154+
*/
155+
private static int log(String name, Document doc) {
156+
DocumentListener[] docListeners;
157+
if (doc instanceof HTMLDocument) {
158+
HTMLDocument htmlDoc = (HTMLDocument) doc;
159+
docListeners = htmlDoc.getDocumentListeners();
160+
} else {
161+
DefaultStyledDocument styledDoc = (DefaultStyledDocument) doc;
162+
docListeners = styledDoc.getDocumentListeners();
163+
}
164+
165+
System.out.println(docListeners.length + " listeners at \"" +
166+
name + "\"");
167+
for (DocumentListener l : docListeners) {
168+
System.out.println("\t" + l.getClass().getName() + " 0x" +
169+
Long.toHexString(System.identityHashCode(l)));
170+
}
171+
return docListeners.length;
172+
}
173+
}

0 commit comments

Comments
 (0)