diff --git a/src/main/java/org/htmlunit/html/HtmlPage.java b/src/main/java/org/htmlunit/html/HtmlPage.java
index 4899a4b1a9..d8eef2f797 100644
--- a/src/main/java/org/htmlunit/html/HtmlPage.java
+++ b/src/main/java/org/htmlunit/html/HtmlPage.java
@@ -154,6 +154,10 @@ public class HtmlPage extends SgmlPage {
private Map idMap_ = new ConcurrentHashMap<>();
private Map nameMap_ = new ConcurrentHashMap<>();
+ // The id/name lookup index is built lazily on first use. Until then,
+ // notifyNodeAdded / fireAttributeChange skip the per-element index updates.
+ // Reads must call ensureMappedElementsBuilt() before consulting idMap_/nameMap_.
+ private boolean mappedElementsBuilt_;
private List frameElements_ = new ArrayList<>();
private int parserCount_;
@@ -631,6 +635,7 @@ public ProcessingInstruction createProcessingInstruction(final String namespaceU
@Override
public DomElement getElementById(final String elementId) {
if (elementId != null) {
+ ensureMappedElementsBuilt();
final MappedElementIndexEntry elements = idMap_.get(elementId);
if (elements != null) {
return elements.first();
@@ -1708,6 +1713,7 @@ public E getHtmlElementById(final String elementId) thro
*/
public List getElementsById(final String elementId) {
if (elementId != null) {
+ ensureMappedElementsBuilt();
final MappedElementIndexEntry elements = idMap_.get(elementId);
if (elements != null) {
return new ArrayList<>(elements.elements());
@@ -1728,6 +1734,7 @@ public List getElementsById(final String elementId) {
@SuppressWarnings("unchecked")
public E getElementByName(final String name) throws ElementNotFoundException {
if (name != null) {
+ ensureMappedElementsBuilt();
final MappedElementIndexEntry elements = nameMap_.get(name);
if (elements != null) {
return (E) elements.first();
@@ -1746,6 +1753,7 @@ public E getElementByName(final String name) throws Eleme
*/
public List getElementsByName(final String name) {
if (name != null) {
+ ensureMappedElementsBuilt();
final MappedElementIndexEntry elements = nameMap_.get(name);
if (elements != null) {
return new ArrayList<>(elements.elements());
@@ -1765,6 +1773,7 @@ public List getElementsByIdAndOrName(final String idAndOrName) {
if (idAndOrName == null) {
return Collections.emptyList();
}
+ ensureMappedElementsBuilt();
final MappedElementIndexEntry list1 = idMap_.get(idAndOrName);
final MappedElementIndexEntry list2 = nameMap_.get(idAndOrName);
final List list = new ArrayList<>();
@@ -1841,12 +1850,32 @@ void notifyNodeRemoved(final DomNode node) {
* @param recurse indicates if children must be added too
*/
void addMappedElement(final DomElement element, final boolean recurse) {
+ // Index is built lazily; skip while not built. ensureMappedElementsBuilt()
+ // walks the tree once and populates everything on first read.
+ if (!mappedElementsBuilt_) {
+ return;
+ }
if (isAncestorOf(element)) {
addElement(idMap_, element, DomElement.ID_ATTRIBUTE, recurse);
addElement(nameMap_, element, DomElement.NAME_ATTRIBUTE, recurse);
}
}
+ private void ensureMappedElementsBuilt() {
+ if (mappedElementsBuilt_) {
+ return;
+ }
+ final DomElement root = getDocumentElement();
+ if (root != null) {
+ addElement(idMap_, root, DomElement.ID_ATTRIBUTE, true);
+ addElement(nameMap_, root, DomElement.NAME_ATTRIBUTE, true);
+ }
+ // Flip the flag only after the maps are populated, so a partial
+ // failure mid-walk leaves us with built_=false and the next read
+ // tries again rather than seeing a half-populated index.
+ mappedElementsBuilt_ = true;
+ }
+
private void addElement(final Map map, final DomElement element,
final String attribute, final boolean recurse) {
final String value = element.getAttribute(attribute);
@@ -1882,6 +1911,10 @@ private void addElement(final Map map, final Do
* @param descendant indicates of the element was descendant of this HtmlPage, but now its parent might be null
*/
void removeMappedElement(final DomElement element, final boolean recurse, final boolean descendant) {
+ // see addMappedElement: while the index is unbuilt, removals are also no-ops.
+ if (!mappedElementsBuilt_) {
+ return;
+ }
if (descendant || isAncestorOf(element)) {
removeElement(idMap_, element, DomElement.ID_ATTRIBUTE, recurse);
removeElement(nameMap_, element, DomElement.NAME_ATTRIBUTE, recurse);
@@ -1998,6 +2031,7 @@ protected HtmlPage clone() {
result.idMap_ = new ConcurrentHashMap<>();
result.nameMap_ = new ConcurrentHashMap<>();
+ result.mappedElementsBuilt_ = false;
return result;
}