diff --git a/PERFORMANCE.md b/PERFORMANCE.md new file mode 100644 index 00000000000..c01af9986c3 --- /dev/null +++ b/PERFORMANCE.md @@ -0,0 +1,5 @@ += Ideas for Performance Changes + +* DomNode: Use Copy on Write for listeners to safe on object holders, but CopyOnWriteArrayList is not good enough +* DomNode: Remove synchronized and find something better without extra cost for more objects, maybe a ReentrantLock at the SGMLPage + diff --git a/src/main/java/org/htmlunit/DefaultPageCreator.java b/src/main/java/org/htmlunit/DefaultPageCreator.java index 48d46615217..1f97c7af6df 100644 --- a/src/main/java/org/htmlunit/DefaultPageCreator.java +++ b/src/main/java/org/htmlunit/DefaultPageCreator.java @@ -120,12 +120,11 @@ public static PageType determinePageType(final String contentType) { } final String contentTypeLC = org.htmlunit.util.StringUtils - .toRootLowerCaseWithCache(contentType); + .toRootLowerCase(contentType); if (MimeType.isJavascriptMimeType(contentTypeLC)) { return PageType.JAVASCRIPT; } - switch (contentTypeLC) { case MimeType.TEXT_HTML: case "image/svg+xml": diff --git a/src/main/java/org/htmlunit/css/ComputedCssStyleDeclaration.java b/src/main/java/org/htmlunit/css/ComputedCssStyleDeclaration.java index dd79bf9080c..41ffcda5298 100644 --- a/src/main/java/org/htmlunit/css/ComputedCssStyleDeclaration.java +++ b/src/main/java/org/htmlunit/css/ComputedCssStyleDeclaration.java @@ -314,7 +314,7 @@ public String getStyleAttribute(final String name) { final String value = element.getValue(); if (!"content".equals(name) && !value.contains("url")) { - return org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(value); + return org.htmlunit.util.StringUtils.toRootLowerCase(value); } return value; } diff --git a/src/main/java/org/htmlunit/css/CssStyleSheet.java b/src/main/java/org/htmlunit/css/CssStyleSheet.java index 189b67abf73..5408439f523 100644 --- a/src/main/java/org/htmlunit/css/CssStyleSheet.java +++ b/src/main/java/org/htmlunit/css/CssStyleSheet.java @@ -625,8 +625,8 @@ public static boolean selects(final BrowserVersion browserVersion, final String a = element.getAttribute(beginHyphenAttributeCondition.getLocalName()); if (beginHyphenAttributeCondition.isCaseInSensitive()) { return selectsHyphenSeparated( - org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(v), - org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(a)); + org.htmlunit.util.StringUtils.toRootLowerCase(v), + org.htmlunit.util.StringUtils.toRootLowerCase(a)); } return selectsHyphenSeparated(v, a); @@ -636,8 +636,8 @@ public static boolean selects(final BrowserVersion browserVersion, final String a2 = element.getAttribute(oneOfAttributeCondition.getLocalName()); if (oneOfAttributeCondition.isCaseInSensitive()) { return selectsOneOf( - org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(v2), - org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(a2)); + org.htmlunit.util.StringUtils.toRootLowerCase(v2), + org.htmlunit.util.StringUtils.toRootLowerCase(a2)); } return selectsOneOf(v2, a2); diff --git a/src/main/java/org/htmlunit/css/ElementCssStyleDeclaration.java b/src/main/java/org/htmlunit/css/ElementCssStyleDeclaration.java index d76fa82c043..90a52a3af40 100644 --- a/src/main/java/org/htmlunit/css/ElementCssStyleDeclaration.java +++ b/src/main/java/org/htmlunit/css/ElementCssStyleDeclaration.java @@ -86,7 +86,7 @@ public String getStyleAttribute(final String name) { if (element != null && element.getValue() != null) { final String value = element.getValue(); if (!value.contains("url")) { - return StringUtils.toRootLowerCaseWithCache(value); + return StringUtils.toRootLowerCase(value); } return value; } diff --git a/src/main/java/org/htmlunit/html/BaseFrameElement.java b/src/main/java/org/htmlunit/html/BaseFrameElement.java index c16fa08a411..fa4150ffa17 100644 --- a/src/main/java/org/htmlunit/html/BaseFrameElement.java +++ b/src/main/java/org/htmlunit/html/BaseFrameElement.java @@ -380,7 +380,7 @@ public final void setSrcAttribute(final String attribute) { @Override protected void setAttributeNS(final String namespaceURI, final String qualifiedName, String attributeValue, final boolean notifyAttributeChangeListeners, final boolean notifyMutationObserver) { - final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(qualifiedName); + final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName); if (null != attributeValue && SRC_ATTRIBUTE.equals(qualifiedNameLC)) { attributeValue = attributeValue.trim(); } diff --git a/src/main/java/org/htmlunit/html/DefaultElementFactory.java b/src/main/java/org/htmlunit/html/DefaultElementFactory.java index 0e2535cb250..e9ace114929 100644 --- a/src/main/java/org/htmlunit/html/DefaultElementFactory.java +++ b/src/main/java/org/htmlunit/html/DefaultElementFactory.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -28,6 +27,7 @@ import org.htmlunit.BrowserVersion; import org.htmlunit.SgmlPage; import org.htmlunit.javascript.configuration.JavaScriptConfiguration; +import org.htmlunit.util.OrderedFastHashMap; import org.xml.sax.Attributes; /** @@ -779,23 +779,25 @@ public HtmlElement createElementNS(final SgmlPage page, final String namespaceUR * @return the map of attribute values for {@link HtmlElement}s */ static Map toMap(final SgmlPage page, final Attributes attributes) { - if (attributes == null) { - return null; - } + final int length = attributes == null ? 0 : attributes.getLength(); + final Map attributeMap = new OrderedFastHashMap<>(length); - final Map attributeMap = new LinkedHashMap<>(attributes.getLength()); - for (int i = 0; i < attributes.getLength(); i++) { + for (int i = 0; i < length; i++) { final String qName = attributes.getQName(i); + // browsers consider only first attribute (ex:
...
) if (!attributeMap.containsKey(qName)) { String namespaceURI = attributes.getURI(i); + if (namespaceURI != null && namespaceURI.isEmpty()) { namespaceURI = null; } + final DomAttr newAttr = new DomAttr(page, namespaceURI, qName, attributes.getValue(i), true); attributeMap.put(qName, newAttr); } } + return attributeMap; } diff --git a/src/main/java/org/htmlunit/html/DomElement.java b/src/main/java/org/htmlunit/html/DomElement.java index a296829fe27..27812ab320c 100644 --- a/src/main/java/org/htmlunit/html/DomElement.java +++ b/src/main/java/org/htmlunit/html/DomElement.java @@ -25,7 +25,6 @@ import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -52,6 +51,7 @@ import org.htmlunit.cssparser.parser.selector.Selector; import org.htmlunit.cssparser.parser.selector.SelectorList; import org.htmlunit.cssparser.parser.selector.SelectorSpecificity; +import org.htmlunit.cyberneko.util.FastHashMap; import org.htmlunit.javascript.AbstractJavaScriptEngine; import org.htmlunit.javascript.HtmlUnitContextFactory; import org.htmlunit.javascript.JavaScriptEngine; @@ -59,6 +59,7 @@ import org.htmlunit.javascript.host.event.EventTarget; import org.htmlunit.javascript.host.event.MouseEvent; import org.htmlunit.javascript.host.event.PointerEvent; +import org.htmlunit.util.OrderedFastHashMap; import org.htmlunit.util.StringUtils; import org.w3c.dom.Attr; import org.w3c.dom.DOMException; @@ -104,7 +105,7 @@ public class DomElement extends DomNamespaceNode implements Element { private NamedAttrNodeMapImpl attributes_; /** The map holding the namespaces, keyed by URI. */ - private final Map namespaces_ = new HashMap<>(); + private FastHashMap namespaces_; /** Cache for the styles. */ private String styleString_; @@ -128,13 +129,18 @@ public DomElement(final String namespaceURI, final String qualifiedName, final S final Map attributes) { super(namespaceURI, qualifiedName, page); - if (attributes != null && !attributes.isEmpty()) { + if (attributes != null) { attributes_ = new NamedAttrNodeMapImpl(this, isAttributeCaseSensitive(), attributes); - for (final DomAttr entry : attributes_.values()) { + + for (final DomAttr entry : attributes.values()) { entry.setParentNode(this); final String attrNamespaceURI = entry.getNamespaceURI(); final String prefix = entry.getPrefix(); + if (attrNamespaceURI != null && prefix != null) { + if (namespaces_ == null) { + namespaces_ = new FastHashMap<>(1, 0.5f); + } namespaces_.put(attrNamespaceURI, prefix); } } @@ -384,7 +390,7 @@ String getQualifiedName(final String namespaceURI, final String localName) { qualifiedName = localName; } else { - final String prefix = namespaces_.get(namespaceURI); + final String prefix = namespaces_ == null ? null : namespaces_.get(namespaceURI); if (prefix == null) { qualifiedName = null; } @@ -532,6 +538,9 @@ protected void setAttributeNS(final String namespaceURI, final String qualifiedN attributes_.put(qualifiedName, newAttr); if (namespaceURI != null) { + if (namespaces_ == null) { + namespaces_ = new FastHashMap<>(1, 0.5f); + } namespaces_.put(namespaceURI, newAttr.getPrefix()); } } @@ -1656,9 +1665,7 @@ class NamedAttrNodeMapImpl implements Map, NamedNodeMap, Serial private static final DomAttr[] EMPTY_ARRAY = new DomAttr[0]; protected static final NamedAttrNodeMapImpl EMPTY_MAP = new NamedAttrNodeMapImpl(); - private final Map map_ = new LinkedHashMap<>(); - private boolean dirty_; - private DomAttr[] attrPositions_ = EMPTY_ARRAY; + private final OrderedFastHashMap map_; private final DomElement domNode_; private final boolean caseSensitive_; @@ -1666,6 +1673,7 @@ private NamedAttrNodeMapImpl() { super(); domNode_ = null; caseSensitive_ = true; + map_ = new OrderedFastHashMap<>(0); } NamedAttrNodeMapImpl(final DomElement domNode, final boolean caseSensitive) { @@ -1675,12 +1683,30 @@ private NamedAttrNodeMapImpl() { } domNode_ = domNode; caseSensitive_ = caseSensitive; + map_ = new OrderedFastHashMap<>(0); } NamedAttrNodeMapImpl(final DomElement domNode, final boolean caseSensitive, final Map attributes) { - this(domNode, caseSensitive); - putAll(attributes); + super(); + if (domNode == null) { + throw new IllegalArgumentException("Provided domNode can't be null."); + } + domNode_ = domNode; + caseSensitive_ = caseSensitive; + + // we expect a special map here, if we don't get it... we have to create us one + if (caseSensitive && attributes instanceof OrderedFastHashMap) { + // no need to rework the map at all, we are case sensitive, so + // we keep all attributes and we got the right map from outside too + map_ = (OrderedFastHashMap) attributes; + } + else { + // this is more expensive but atypical, so we don't have to care that much + map_ = new OrderedFastHashMap<>(attributes.size()); + // this will create a new map with all case lowercased and + putAll(attributes); + } } /** @@ -1703,7 +1729,7 @@ private String fixName(final String name) { if (caseSensitive_) { return name; } - return StringUtils.toRootLowerCaseWithCache(name); + return StringUtils.toRootLowerCase(name); } /** @@ -1725,11 +1751,7 @@ public Node item(final int index) { if (index < 0 || index >= map_.size()) { return null; } - if (dirty_) { - attrPositions_ = map_.values().toArray(attrPositions_); - dirty_ = false; - } - return attrPositions_[index]; + return map_.getValue(index); } /** @@ -1773,7 +1795,6 @@ public Node setNamedItemNS(final Node node) throws DOMException { @Override public DomAttr put(final String key, final DomAttr value) { final String name = fixName(key); - dirty_ = true; return map_.put(name, value); } @@ -1784,7 +1805,6 @@ public DomAttr put(final String key, final DomAttr value) { public DomAttr remove(final Object key) { if (key instanceof String) { final String name = fixName((String) key); - dirty_ = true; return map_.remove(name); } return null; @@ -1795,7 +1815,6 @@ public DomAttr remove(final Object key) { */ @Override public void clear() { - dirty_ = true; map_.clear(); } diff --git a/src/main/java/org/htmlunit/html/DomNamespaceNode.java b/src/main/java/org/htmlunit/html/DomNamespaceNode.java index 145d67f4d67..ae2f0c7974a 100644 --- a/src/main/java/org/htmlunit/html/DomNamespaceNode.java +++ b/src/main/java/org/htmlunit/html/DomNamespaceNode.java @@ -14,12 +14,11 @@ */ package org.htmlunit.html; -import java.util.Locale; - import org.htmlunit.SgmlPage; import org.htmlunit.WebAssert; import org.htmlunit.html.xpath.XPathHelper; import org.htmlunit.javascript.host.dom.Document; +import org.htmlunit.util.StringUtils; /** * Intermediate base class for DOM Nodes that have namespaces. That includes HtmlElement and HtmlAttr. @@ -61,7 +60,7 @@ protected DomNamespaceNode(final String namespaceURI, final String qualifiedName prefix_ = qualifiedName_.substring(0, colonPosition); } - localNameLC_ = localName_.toLowerCase(Locale.ROOT); + localNameLC_ = StringUtils.toRootLowerCase(localName_); } /** diff --git a/src/main/java/org/htmlunit/html/DomNode.java b/src/main/java/org/htmlunit/html/DomNode.java index 96e1c4a56ad..e5e1d9dd652 100644 --- a/src/main/java/org/htmlunit/html/DomNode.java +++ b/src/main/java/org/htmlunit/html/DomNode.java @@ -24,10 +24,8 @@ import java.io.StringWriter; import java.nio.charset.Charset; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -58,7 +56,6 @@ import org.htmlunit.javascript.JavaScriptEngine; import org.htmlunit.javascript.host.event.Event; import org.htmlunit.javascript.host.html.HTMLDocument; -import org.htmlunit.util.SerializableLock; import org.htmlunit.util.StringUtils; import org.htmlunit.xml.XmlPage; import org.htmlunit.xpath.xml.utils.PrefixResolver; @@ -163,14 +160,10 @@ public abstract class DomNode implements Cloneable, Serializable, Node { private boolean attachedToPage_; - private final Object listeners_lock_ = new SerializableLock(); - /** The listeners which are to be notified of characterData change. */ - private Collection characterDataListeners_; - private List characterDataListenersList_; + private List characterDataListeners_; + private List domListeners_; - private Collection domListeners_; - private List domListenersList_; private Map userData_; /** @@ -1671,12 +1664,11 @@ protected void notifyIncorrectness(final String message) { public void addDomChangeListener(final DomChangeListener listener) { WebAssert.notNull("listener", listener); - synchronized (listeners_lock_) { + synchronized (this) { if (domListeners_ == null) { - domListeners_ = new LinkedHashSet<>(); + domListeners_ = new ArrayList<>(); } domListeners_.add(listener); - domListenersList_ = null; } } @@ -1690,10 +1682,9 @@ public void addDomChangeListener(final DomChangeListener listener) { public void removeDomChangeListener(final DomChangeListener listener) { WebAssert.notNull("listener", listener); - synchronized (listeners_lock_) { + synchronized (this) { if (domListeners_ != null) { domListeners_.remove(listener); - domListenersList_ = null; } } } @@ -1711,8 +1702,9 @@ protected void fireNodeAdded(final DomChangeEvent event) { while (toInform != null) { final List listeners = toInform.safeGetDomListeners(); if (listeners != null) { - for (final DomChangeListener listener : listeners) { - listener.nodeAdded(event); + // iterate by index and safe on an iterator copy + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).nodeAdded(event); } } toInform = toInform.getParentNode(); @@ -1729,12 +1721,11 @@ protected void fireNodeAdded(final DomChangeEvent event) { public void addCharacterDataChangeListener(final CharacterDataChangeListener listener) { WebAssert.notNull("listener", listener); - synchronized (listeners_lock_) { + synchronized (this) { if (characterDataListeners_ == null) { - characterDataListeners_ = new LinkedHashSet<>(); + characterDataListeners_ = new ArrayList<>(); } characterDataListeners_.add(listener); - characterDataListenersList_ = null; } } @@ -1748,10 +1739,9 @@ public void addCharacterDataChangeListener(final CharacterDataChangeListener lis public void removeCharacterDataChangeListener(final CharacterDataChangeListener listener) { WebAssert.notNull("listener", listener); - synchronized (listeners_lock_) { + synchronized (this) { if (characterDataListeners_ != null) { characterDataListeners_.remove(listener); - characterDataListenersList_ = null; } } } @@ -1769,8 +1759,9 @@ protected void fireCharacterDataChanged(final CharacterDataChangeEvent event) { final List listeners = toInform.safeGetCharacterDataListeners(); if (listeners != null) { - for (final CharacterDataChangeListener listener : listeners) { - listener.characterDataChanged(event); + // iterate by index and safe on an iterator copy + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).characterDataChanged(event); } } toInform = toInform.getParentNode(); @@ -1790,8 +1781,9 @@ protected void fireNodeDeleted(final DomChangeEvent event) { while (toInform != null) { final List listeners = toInform.safeGetDomListeners(); if (listeners != null) { - for (final DomChangeListener listener : listeners) { - listener.nodeDeleted(event); + // iterate by index and safe on an iterator copy + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).nodeDeleted(event); } } toInform = toInform.getParentNode(); @@ -1799,26 +1791,14 @@ protected void fireNodeDeleted(final DomChangeEvent event) { } private List safeGetDomListeners() { - synchronized (listeners_lock_) { - if (domListeners_ == null) { - return null; - } - if (domListenersList_ == null) { - domListenersList_ = new ArrayList<>(domListeners_); - } - return domListenersList_; + synchronized (this) { + return domListeners_ == null ? null : new ArrayList<>(domListeners_); } } private List safeGetCharacterDataListeners() { - synchronized (listeners_lock_) { - if (characterDataListeners_ == null) { - return null; - } - if (characterDataListenersList_ == null) { - characterDataListenersList_ = new ArrayList<>(characterDataListeners_); - } - return characterDataListenersList_; + synchronized (this) { + return characterDataListeners_ == null ? null : new ArrayList<>(characterDataListeners_); } } diff --git a/src/main/java/org/htmlunit/html/HtmlButton.java b/src/main/java/org/htmlunit/html/HtmlButton.java index f809c8a0bcd..80da8daf006 100644 --- a/src/main/java/org/htmlunit/html/HtmlButton.java +++ b/src/main/java/org/htmlunit/html/HtmlButton.java @@ -324,7 +324,7 @@ public final String getOnBlurAttribute() { @Override protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue, final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) { - final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(qualifiedName); + final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName); if (NAME_ATTRIBUTE.equals(qualifiedNameLC)) { if (newNames_.isEmpty()) { newNames_ = new HashSet<>(); diff --git a/src/main/java/org/htmlunit/html/HtmlCheckBoxInput.java b/src/main/java/org/htmlunit/html/HtmlCheckBoxInput.java index 318be9e6946..c5766d684cf 100644 --- a/src/main/java/org/htmlunit/html/HtmlCheckBoxInput.java +++ b/src/main/java/org/htmlunit/html/HtmlCheckBoxInput.java @@ -204,7 +204,7 @@ void handleFocusLostValueChanged() { @Override protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue, final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) { - final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(qualifiedName); + final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName); if (VALUE_ATTRIBUTE.equals(qualifiedNameLC)) { super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners, diff --git a/src/main/java/org/htmlunit/html/HtmlElement.java b/src/main/java/org/htmlunit/html/HtmlElement.java index 508353bb051..f18b3a37a11 100644 --- a/src/main/java/org/htmlunit/html/HtmlElement.java +++ b/src/main/java/org/htmlunit/html/HtmlElement.java @@ -21,8 +21,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -148,7 +146,7 @@ public String value() { protected static final String ATTRIBUTE_CHECKED = "checked"; /** The listeners which are to be notified of attribute changes. */ - private final Collection attributeListeners_; + private final List attributeListeners_ = new ArrayList<>(); /** The owning form for lost form children. */ private HtmlForm owningForm_; @@ -182,7 +180,6 @@ protected HtmlElement(final String qualifiedName, final SgmlPage page, protected HtmlElement(final String namespaceURI, final String qualifiedName, final SgmlPage page, final Map attributes) { super(namespaceURI, qualifiedName, page, attributes); - attributeListeners_ = new LinkedHashSet<>(); } /** @@ -236,7 +233,7 @@ protected void setAttributeNS(final String namespaceURI, final String qualifiedN */ protected static void notifyAttributeChangeListeners(final HtmlAttributeChangeEvent event, final HtmlElement element, final String oldAttributeValue, final boolean notifyMutationObservers) { - final Collection listeners = element.attributeListeners_; + final List listeners = element.attributeListeners_; if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) { synchronized (listeners) { for (final HtmlAttributeChangeListener listener : listeners) { diff --git a/src/main/java/org/htmlunit/html/HtmlImage.java b/src/main/java/org/htmlunit/html/HtmlImage.java index 7e607994305..ea57a9fcee4 100644 --- a/src/main/java/org/htmlunit/html/HtmlImage.java +++ b/src/main/java/org/htmlunit/html/HtmlImage.java @@ -137,7 +137,7 @@ protected void setAttributeNS(final String namespaceURI, final String qualifiedN final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) { final HtmlPage htmlPage = getHtmlPageOrNull(); - final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(qualifiedName); + final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName); if (SRC_ATTRIBUTE.equals(qualifiedNameLC) && value != ATTRIBUTE_NOT_DEFINED && htmlPage != null) { final String oldValue = getAttributeNS(namespaceURI, qualifiedNameLC); if (!oldValue.equals(value)) { diff --git a/src/main/java/org/htmlunit/html/HtmlInput.java b/src/main/java/org/htmlunit/html/HtmlInput.java index 6699ea7f634..fd5555360e0 100644 --- a/src/main/java/org/htmlunit/html/HtmlInput.java +++ b/src/main/java/org/htmlunit/html/HtmlInput.java @@ -625,7 +625,7 @@ static Page executeOnChangeHandlerIfAppropriate(final HtmlElement htmlElement) { @Override protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue, final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) { - final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(qualifiedName); + final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName); if (NAME_ATTRIBUTE.equals(qualifiedNameLC)) { if (newNames_.isEmpty()) { newNames_ = new HashSet<>(); @@ -1143,7 +1143,7 @@ public final void setFormNoValidate(final boolean noValidate) { public final String getType() { final BrowserVersion browserVersion = getPage().getWebClient().getBrowserVersion(); String type = getTypeAttribute(); - type = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(type); + type = org.htmlunit.util.StringUtils.toRootLowerCase(type); return isSupported(type, browserVersion) ? type : "text"; } @@ -1167,11 +1167,11 @@ public HtmlInput changeType(String newType, final boolean setThroughAttribute) { final BrowserVersion browser = webClient.getBrowserVersion(); if (!currentType.equalsIgnoreCase(newType)) { if (newType != null && browser.hasFeature(JS_INPUT_SET_TYPE_LOWERCASE)) { - newType = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(newType); + newType = org.htmlunit.util.StringUtils.toRootLowerCase(newType); } if (!isSupported(org.htmlunit.util.StringUtils - .toRootLowerCaseWithCache(newType), browser)) { + .toRootLowerCase(newType), browser)) { if (setThroughAttribute) { newType = "text"; } diff --git a/src/main/java/org/htmlunit/html/HtmlPage.java b/src/main/java/org/htmlunit/html/HtmlPage.java index 7104f2ab60e..6d5de90b56e 100644 --- a/src/main/java/org/htmlunit/html/HtmlPage.java +++ b/src/main/java/org/htmlunit/html/HtmlPage.java @@ -51,6 +51,7 @@ import java.util.SortedSet; import java.util.TreeSet; import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; @@ -162,10 +163,8 @@ public class HtmlPage extends SgmlPage { private transient Charset originalCharset_; private final Object lock_ = new SerializableLock(); // used for synchronization - private Map> idMap_ - = Collections.synchronizedMap(new HashMap<>()); - private Map> nameMap_ - = Collections.synchronizedMap(new HashMap<>()); + private Map> idMap_ = new ConcurrentHashMap<>(); + private Map> nameMap_ = new ConcurrentHashMap<>(); private SortedSet frameElements_ = new TreeSet<>(documentPositionComparator); private int parserCount_; @@ -597,7 +596,7 @@ public DOMImplementation getImplementation() { @Override public DomElement createElement(String tagName) { if (tagName.indexOf(':') == -1) { - tagName = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(tagName); + tagName = org.htmlunit.util.StringUtils.toRootLowerCase(tagName); } return getWebClient().getPageCreator().getHtmlParser().getFactory(tagName) .createElementNS(this, null, tagName, null, true); @@ -1975,8 +1974,8 @@ protected HtmlPage clone() { final HtmlPage result = (HtmlPage) super.clone(); result.elementWithFocus_ = null; - result.idMap_ = Collections.synchronizedMap(new HashMap<>()); - result.nameMap_ = Collections.synchronizedMap(new HashMap<>()); + result.idMap_ = new ConcurrentHashMap<>(); + result.nameMap_ = new ConcurrentHashMap<>(); return result; } @@ -1995,11 +1994,14 @@ public HtmlPage cloneNode(final boolean deep) { // if deep, clone the kids too, and re initialize parts of the clone if (deep) { - synchronized (lock_) { - result.attributeListeners_ = null; - } + // this was previously synchronized but that makes not sense, why + // lock the source against a copy only one has a reference too, + // because result is a local reference + result.attributeListeners_ = null; + result.selectionRanges_ = new ArrayList<>(3); - result.afterLoadActions_ = new ArrayList<>(); + // the original one is synchronized so we should do that here too, shouldn't we? + result.afterLoadActions_ = Collections.synchronizedList(new ArrayList<>()); result.frameElements_ = new TreeSet<>(documentPositionComparator); for (DomNode child = getFirstChild(); child != null; child = child.getNextSibling()) { result.appendChild(child.cloneNode(true)); diff --git a/src/main/java/org/htmlunit/html/HtmlRadioButtonInput.java b/src/main/java/org/htmlunit/html/HtmlRadioButtonInput.java index 636bbde98bf..9a5ccdae0f9 100644 --- a/src/main/java/org/htmlunit/html/HtmlRadioButtonInput.java +++ b/src/main/java/org/htmlunit/html/HtmlRadioButtonInput.java @@ -273,7 +273,7 @@ void handleFocusLostValueChanged() { @Override protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue, final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) { - final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(qualifiedName); + final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName); if (VALUE_ATTRIBUTE.equals(qualifiedNameLC)) { super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners, diff --git a/src/main/java/org/htmlunit/html/HtmlScript.java b/src/main/java/org/htmlunit/html/HtmlScript.java index 683bc50eb62..88ecaab07e7 100644 --- a/src/main/java/org/htmlunit/html/HtmlScript.java +++ b/src/main/java/org/htmlunit/html/HtmlScript.java @@ -157,7 +157,7 @@ public boolean mayBeDisplayed() { @Override protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue, final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) { - final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(qualifiedName); + final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName); // special additional processing for the 'src' if (namespaceURI != null || !SRC_ATTRIBUTE.equals(qualifiedNameLC)) { super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners, diff --git a/src/main/java/org/htmlunit/html/HtmlSelect.java b/src/main/java/org/htmlunit/html/HtmlSelect.java index 8d6193c9673..d11aa6d6501 100644 --- a/src/main/java/org/htmlunit/html/HtmlSelect.java +++ b/src/main/java/org/htmlunit/html/HtmlSelect.java @@ -666,7 +666,7 @@ public final String getOnChangeAttribute() { @Override protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue, final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) { - final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(qualifiedName); + final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName); if (DomElement.NAME_ATTRIBUTE.equals(qualifiedNameLC)) { if (newNames_.isEmpty()) { newNames_ = new HashSet<>(); diff --git a/src/main/java/org/htmlunit/html/HtmlTextArea.java b/src/main/java/org/htmlunit/html/HtmlTextArea.java index 79518ffd938..6bf6532853f 100644 --- a/src/main/java/org/htmlunit/html/HtmlTextArea.java +++ b/src/main/java/org/htmlunit/html/HtmlTextArea.java @@ -541,7 +541,7 @@ public boolean isReadOnly() { @Override protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue, final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) { - final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(qualifiedName); + final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName); if (DomElement.NAME_ATTRIBUTE.equals(qualifiedNameLC)) { if (newNames_.isEmpty()) { newNames_ = new HashSet<>(); diff --git a/src/main/java/org/htmlunit/html/parser/neko/HtmlUnitNekoDOMBuilder.java b/src/main/java/org/htmlunit/html/parser/neko/HtmlUnitNekoDOMBuilder.java index 0292d2d36e5..1ed67ab5fae 100644 --- a/src/main/java/org/htmlunit/html/parser/neko/HtmlUnitNekoDOMBuilder.java +++ b/src/main/java/org/htmlunit/html/parser/neko/HtmlUnitNekoDOMBuilder.java @@ -26,12 +26,9 @@ import java.nio.charset.Charset; import java.util.ArrayDeque; import java.util.Deque; -import java.util.HashMap; import java.util.Locale; -import java.util.Map; import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Triple; import org.htmlunit.BrowserVersion; import org.htmlunit.ObjectInstantiationException; @@ -42,10 +39,12 @@ import org.htmlunit.cyberneko.HTMLEventInfo; import org.htmlunit.cyberneko.HTMLScanner; import org.htmlunit.cyberneko.HTMLTagBalancingListener; +import org.htmlunit.cyberneko.util.FastHashMap; import org.htmlunit.cyberneko.xerces.parsers.AbstractSAXParser; import org.htmlunit.cyberneko.xerces.xni.Augmentations; import org.htmlunit.cyberneko.xerces.xni.QName; import org.htmlunit.cyberneko.xerces.xni.XMLAttributes; +import org.htmlunit.cyberneko.xerces.xni.XMLString; import org.htmlunit.cyberneko.xerces.xni.XNIException; import org.htmlunit.cyberneko.xerces.xni.parser.XMLInputSource; import org.htmlunit.cyberneko.xerces.xni.parser.XMLParserConfiguration; @@ -76,6 +75,7 @@ import org.htmlunit.html.parser.HTMLParserListener; import org.htmlunit.javascript.host.html.HTMLBodyElement; import org.htmlunit.javascript.host.html.HTMLDocument; +import org.htmlunit.util.StringUtils; import org.w3c.dom.Node; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; @@ -106,64 +106,64 @@ final class HtmlUnitNekoDOMBuilder extends AbstractSAXParser implements ContentHandler, LexicalHandler, HTMLTagBalancingListener, HTMLParserDOMBuilder { - // cache Neko Elements for performance and memory - private static final Map, HTMLElements> ELEMENTS; - static { - ELEMENTS = new HashMap<>(); - - Triple key; - HTMLElements value; + // cache Neko Elements for performance and memory efficiency + private static final FastHashMap, HTMLElements> + HTMLELEMENTS_CACHE = new FastHashMap<>(); - final short unknownId = HTMLElements.UNKNOWN; - final short isindexId = unknownId + 1; + static { + // continue short code enumeration + final short isIndexShortCode = HTMLElements.UNKNOWN + 1; - final short commandId = isindexId + 1; - final short mainId = commandId + 1; + final short commandShortCode = isIndexShortCode + 1; + final short mainShortCode = commandShortCode + 1; // isIndex is special - we have to add it here because all browsers moving this to // the body (even if it is not supported) - final HTMLElements.Element isIndex = new HTMLElements.Element(isindexId, "ISINDEX", + final HTMLElements.Element isIndex = new HTMLElements.Element(isIndexShortCode, "ISINDEX", HTMLElements.Element.CONTAINER, HTMLElements.BODY, null); - final HTMLElements.Element isIndexSupported = new HTMLElements.Element(isindexId, "ISINDEX", - HTMLElements.Element.BLOCK, HTMLElements.BODY, new short[] {isindexId}); + final HTMLElements.Element isIndexSupported = new HTMLElements.Element(isIndexShortCode, "ISINDEX", + HTMLElements.Element.BLOCK, HTMLElements.BODY, new short[] {isIndexShortCode}); - final HTMLElements.Element command = new HTMLElements.Element(commandId, "COMMAND", + final HTMLElements.Element command = new HTMLElements.Element(commandShortCode, "COMMAND", HTMLElements.Element.EMPTY, new short[] {HTMLElements.BODY, HTMLElements.HEAD}, null); - final HTMLElements.Element main = new HTMLElements.Element(mainId, "MAIN", + final HTMLElements.Element main = new HTMLElements.Element(mainShortCode, "MAIN", HTMLElements.Element.INLINE, HTMLElements.BODY, null); + Triple key; + HTMLElements value; + // !COMMAND_TAG !ISINDEX_TAG !MAIN_TAG key = Triple.of(Boolean.FALSE, Boolean.FALSE, Boolean.FALSE); value = new HTMLElements(); value.setElement(isIndex); - ELEMENTS.put(key, value); + HTMLELEMENTS_CACHE.put(key, value); // !COMMAND_TAG !ISINDEX_TAG MAIN_TAG key = Triple.of(Boolean.FALSE, Boolean.FALSE, Boolean.TRUE); value = new HTMLElements(); value.setElement(main); value.setElement(isIndex); - ELEMENTS.put(key, value); + HTMLELEMENTS_CACHE.put(key, value); // !COMMAND_TAG ISINDEX_TAG !MAIN_TAG key = Triple.of(Boolean.FALSE, Boolean.TRUE, Boolean.FALSE); value = new HTMLElements(); value.setElement(isIndexSupported); - ELEMENTS.put(key, value); + HTMLELEMENTS_CACHE.put(key, value); // !COMMAND_TAG ISINDEX_TAG MAIN_TAG key = Triple.of(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE); value = new HTMLElements(); value.setElement(isIndexSupported); value.setElement(main); - ELEMENTS.put(key, value); + HTMLELEMENTS_CACHE.put(key, value); // COMMAND_TAG !ISINDEX_TAG !MAIN_TAG key = Triple.of(Boolean.TRUE, Boolean.FALSE, Boolean.FALSE); value = new HTMLElements(); value.setElement(command); value.setElement(isIndex); - ELEMENTS.put(key, value); + HTMLELEMENTS_CACHE.put(key, value); // COMMAND_TAG !ISINDEX_TAG MAIN_TAG key = Triple.of(Boolean.TRUE, Boolean.FALSE, Boolean.TRUE); @@ -171,14 +171,14 @@ final class HtmlUnitNekoDOMBuilder extends AbstractSAXParser value.setElement(command); value.setElement(isIndex); value.setElement(main); - ELEMENTS.put(key, value); + HTMLELEMENTS_CACHE.put(key, value); // COMMAND_TAG ISINDEX_TAG !MAIN_TAG key = Triple.of(Boolean.TRUE, Boolean.TRUE, Boolean.FALSE); value = new HTMLElements(); value.setElement(command); value.setElement(isIndexSupported); - ELEMENTS.put(key, value); + HTMLELEMENTS_CACHE.put(key, value); // COMMAND_TAG ISINDEX_TAG MAIN_TAG key = Triple.of(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE); @@ -186,7 +186,7 @@ final class HtmlUnitNekoDOMBuilder extends AbstractSAXParser value.setElement(command); value.setElement(isIndexSupported); value.setElement(main); - ELEMENTS.put(key, value); + HTMLELEMENTS_CACHE.put(key, value); } private enum HeadParsed { YES, SYNTHESIZED, NO } @@ -202,7 +202,7 @@ private enum HeadParsed { YES, SYNTHESIZED, NO } private final int initialSize_; private DomNode currentNode_; private final boolean createdByJavascript_; - private final StringBuilder characters_ = new StringBuilder(); + private final XMLString characters_ = new XMLString(); private HtmlUnitNekoDOMBuilder.HeadParsed headParsed_ = HeadParsed.NO; private HtmlElement body_; private boolean lastTagWasSynthesized_; @@ -284,7 +284,7 @@ public void pushInputString(final String html) { * @return the configuration */ private static XMLParserConfiguration createConfiguration(final BrowserVersion browserVersion) { - final HTMLElements elements = ELEMENTS.get( + final HTMLElements elements = HTMLELEMENTS_CACHE.get( Triple.of(browserVersion.hasFeature(HTML_COMMAND_TAG), browserVersion.hasFeature(HTML_ISINDEX_TAG), browserVersion.hasFeature(HTML_MAIN_TAG))); @@ -329,7 +329,7 @@ public void startElement(String namespaceURI, final String localName, final Stri } handleCharacters(); - final String tagLower = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(localName); + final String tagLower = org.htmlunit.util.StringUtils.toRootLowerCase(localName); if (page_.isParsingHtmlSnippet() && ("html".equals(tagLower) || "body".equals(tagLower))) { // we have to push the current node on the stack to make sure // the endElement call is able to remove a node from the stack @@ -528,6 +528,9 @@ private static boolean isTableChild(final String nodeName) { } private static boolean isTableCell(final String nodeName) { + if (nodeName == null || nodeName.length() != 2) { + return false; + } return "td".equals(nodeName) || "th".equals(nodeName); } @@ -547,7 +550,7 @@ public void endElement(final QName element, final Augmentations augs) public void endElement(final String namespaceURI, final String localName, final String qName) throws SAXException { - final String tagLower = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(localName); + final String tagLower = org.htmlunit.util.StringUtils.toRootLowerCase(localName); handleCharacters(); @@ -607,54 +610,57 @@ public void ignorableWhitespace(final char[] ch, final int start, final int leng * Picks up the character data accumulated so far and add it to the current element as a text node. */ private void handleCharacters() { - if (characters_.length() != 0) { - if (currentNode_ instanceof HtmlHtml) { - // In HTML, the node only has two possible children: - // the and the ; any text is ignored. - characters_.setLength(0); - } - else { - // Use the normal behavior: append a text node for the accumulated text. - final String textValue = characters_.toString(); - final DomText text = new DomText(page_, textValue); - characters_.setLength(0); - - if (StringUtils.isNotBlank(textValue)) { - // malformed HTML: some text => text comes before the table - if (currentNode_ instanceof HtmlTableRow) { - final HtmlTableRow row = (HtmlTableRow) currentNode_; - final HtmlTable enclosingTable = row.getEnclosingTable(); - if (enclosingTable != null) { // may be null when called from Range.createContextualFragment - if (enclosingTable.getPreviousSibling() instanceof DomText) { - final DomText domText = (DomText) enclosingTable.getPreviousSibling(); - domText.setTextContent(domText.getWholeText() + textValue); - } - else { - enclosingTable.insertBefore(text); - } - } - } - else if (currentNode_ instanceof HtmlTable) { - final HtmlTable enclosingTable = (HtmlTable) currentNode_; - if (enclosingTable.getPreviousSibling() instanceof DomText) { - final DomText domText = (DomText) enclosingTable.getPreviousSibling(); - domText.setTextContent(domText.getWholeText() + textValue); - } - else { - enclosingTable.insertBefore(text); - } - } - else if (currentNode_ instanceof HtmlImage) { - currentNode_.getParentNode().appendChild(text); + // make the code easier to read because we remove a nesting level + if (characters_.length() == 0) { + return; + } + + if (currentNode_ instanceof HtmlHtml) { + // In HTML, the node only has two possible children: + // the and the ; any text is ignored. + characters_.clear(); + return; + } + + // Use the normal behavior: append a text node for the accumulated text. + final String textValue = characters_.toString(); + final DomText textNode = new DomText(page_, textValue); + characters_.clear(); + + if (org.apache.commons.lang3.StringUtils.isNotBlank(textValue)) { + // malformed HTML: some text => text comes before the table + if (currentNode_ instanceof HtmlTableRow) { + final HtmlTableRow row = (HtmlTableRow) currentNode_; + final HtmlTable enclosingTable = row.getEnclosingTable(); + if (enclosingTable != null) { // may be null when called from Range.createContextualFragment + if (enclosingTable.getPreviousSibling() instanceof DomText) { + final DomText domText = (DomText) enclosingTable.getPreviousSibling(); + domText.setTextContent(domText.getWholeText() + textValue); } else { - appendChild(currentNode_, text); + enclosingTable.insertBefore(textNode); } } + } + else if (currentNode_ instanceof HtmlTable) { + final HtmlTable enclosingTable = (HtmlTable) currentNode_; + if (enclosingTable.getPreviousSibling() instanceof DomText) { + final DomText domText = (DomText) enclosingTable.getPreviousSibling(); + domText.setTextContent(domText.getWholeText() + textValue); + } else { - appendChild(currentNode_, text); + enclosingTable.insertBefore(textNode); } } + else if (currentNode_ instanceof HtmlImage) { + currentNode_.getParentNode().appendChild(textNode); + } + else { + appendChild(currentNode_, textNode); + } + } + else { + appendChild(currentNode_, textNode); } } @@ -809,9 +815,9 @@ else if ("html".equals(lp)) { private static void copyAttributes(final DomElement to, final XMLAttributes attrs) { final int length = attrs.getLength(); + for (int i = 0; i < length; i++) { - final String attrName = org.htmlunit.util.StringUtils - .toRootLowerCaseWithCache(attrs.getLocalName(i)); + final String attrName = StringUtils.toRootLowerCase(attrs.getLocalName(i)); if (to.getAttributes().getNamedItem(attrName) == null) { to.setAttribute(attrName, attrs.getValue(i)); if (attrName.startsWith("on") && to.getPage().getWebClient().isJavaScriptEngineEnabled() diff --git a/src/main/java/org/htmlunit/html/parser/neko/HtmlUnitNekoHtmlParser.java b/src/main/java/org/htmlunit/html/parser/neko/HtmlUnitNekoHtmlParser.java index e2ca6ad643c..c8b2494042f 100644 --- a/src/main/java/org/htmlunit/html/parser/neko/HtmlUnitNekoHtmlParser.java +++ b/src/main/java/org/htmlunit/html/parser/neko/HtmlUnitNekoHtmlParser.java @@ -284,7 +284,7 @@ public ElementFactory getElementFactory(final SgmlPage page, final String namesp String tagName = qualifiedName; final int index = tagName.indexOf(':'); if (index == -1) { - tagName = StringUtils.toRootLowerCaseWithCache(tagName); + tagName = StringUtils.toRootLowerCase(tagName); } else { tagName = tagName.substring(index + 1); diff --git a/src/main/java/org/htmlunit/javascript/host/Element.java b/src/main/java/org/htmlunit/javascript/host/Element.java index 10d786831f2..b221e27d4c2 100644 --- a/src/main/java/org/htmlunit/javascript/host/Element.java +++ b/src/main/java/org/htmlunit/javascript/host/Element.java @@ -141,7 +141,7 @@ public void setDomNode(final DomNode domNode) { //Should be called only on construction. final DomElement htmlElt = (DomElement) domNode; for (final DomAttr attr : htmlElt.getAttributesMap().values()) { - final String eventName = StringUtils.toRootLowerCaseWithCache(attr.getName()); + final String eventName = StringUtils.toRootLowerCase(attr.getName()); if (eventName.startsWith("on")) { createEventHandler(eventName.substring(2), attr.getValue()); } @@ -236,7 +236,7 @@ public HTMLCollection getElementsByTagName(final String tagName) { final boolean caseSensitive; final DomNode dom = getDomNodeOrNull(); if (dom == null) { - searchTagName = StringUtils.toRootLowerCaseWithCache(tagName); + searchTagName = StringUtils.toRootLowerCase(tagName); caseSensitive = false; } else { @@ -246,7 +246,7 @@ public HTMLCollection getElementsByTagName(final String tagName) { caseSensitive = true; } else { - searchTagName = StringUtils.toRootLowerCaseWithCache(tagName); + searchTagName = StringUtils.toRootLowerCase(tagName); caseSensitive = false; } } diff --git a/src/main/java/org/htmlunit/javascript/host/html/HTMLBodyElement.java b/src/main/java/org/htmlunit/javascript/host/html/HTMLBodyElement.java index 7d0b73c01d1..4c262ee8e06 100644 --- a/src/main/java/org/htmlunit/javascript/host/html/HTMLBodyElement.java +++ b/src/main/java/org/htmlunit/javascript/host/html/HTMLBodyElement.java @@ -67,7 +67,7 @@ public void jsConstructor() { public void createEventHandlerFromAttribute(final String attributeName, final String value) { // when many body tags are found while parsing, attributes of // different tags are added and should create an event handler when needed - if (StringUtils.toRootLowerCaseWithCache(attributeName).startsWith("on")) { + if (StringUtils.toRootLowerCase(attributeName).startsWith("on")) { createEventHandler(attributeName.substring(2), value); } } diff --git a/src/main/java/org/htmlunit/javascript/host/html/HTMLDocument.java b/src/main/java/org/htmlunit/javascript/host/html/HTMLDocument.java index 4ce5cfb881d..e10e1bdec1c 100644 --- a/src/main/java/org/htmlunit/javascript/host/html/HTMLDocument.java +++ b/src/main/java/org/htmlunit/javascript/host/html/HTMLDocument.java @@ -818,7 +818,7 @@ public Attr createAttribute(final String attributeName) { String name = attributeName; if (StringUtils.isNotEmpty(name) && getBrowserVersion().hasFeature(JS_DOCUMENT_CREATE_ATTRUBUTE_LOWER_CASE)) { - name = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(name); + name = org.htmlunit.util.StringUtils.toRootLowerCase(name); } return super.createAttribute(name); diff --git a/src/main/java/org/htmlunit/javascript/host/html/HTMLElement.java b/src/main/java/org/htmlunit/javascript/host/html/HTMLElement.java index ffeb3067ac8..dfb7071cce9 100644 --- a/src/main/java/org/htmlunit/javascript/host/html/HTMLElement.java +++ b/src/main/java/org/htmlunit/javascript/host/html/HTMLElement.java @@ -515,13 +515,13 @@ public String getLocalName() { if (prefix != null) { // create string builder only if needed (performance) final StringBuilder localName = new StringBuilder( - org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(prefix)) + org.htmlunit.util.StringUtils.toRootLowerCase(prefix)) .append(':') .append(org.htmlunit.util.StringUtils - .toRootLowerCaseWithCache(domNode.getLocalName())); + .toRootLowerCase(domNode.getLocalName())); return localName.toString(); } - return org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(domNode.getLocalName()); + return org.htmlunit.util.StringUtils.toRootLowerCase(domNode.getLocalName()); } return domNode.getLocalName(); } diff --git a/src/main/java/org/htmlunit/javascript/host/html/HTMLSpanElement.java b/src/main/java/org/htmlunit/javascript/host/html/HTMLSpanElement.java index 2f7b6fa233a..57313bb4814 100644 --- a/src/main/java/org/htmlunit/javascript/host/html/HTMLSpanElement.java +++ b/src/main/java/org/htmlunit/javascript/host/html/HTMLSpanElement.java @@ -63,7 +63,7 @@ public void setDomNode(final DomNode domNode) { super.setDomNode(domNode); final BrowserVersion browser = getBrowserVersion(); if (browser.hasFeature(HTMLBASEFONT_END_TAG_FORBIDDEN)) { - switch (StringUtils.toRootLowerCaseWithCache(domNode.getLocalName())) { + switch (StringUtils.toRootLowerCase(domNode.getLocalName())) { case "basefont": case "keygen": endTagForbidden_ = true; diff --git a/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java b/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java index 8ccb5e89b2d..b2539e4cc56 100644 --- a/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java +++ b/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java @@ -976,7 +976,7 @@ void doSend() { for (final Entry header : new TreeMap<>(webRequest_.getAdditionalHeaders()).entrySet()) { final String name = org.htmlunit.util.StringUtils - .toRootLowerCaseWithCache(header.getKey()); + .toRootLowerCase(header.getKey()); if (isPreflightHeader(name, header.getValue())) { if (builder.length() != 0) { builder.append(','); @@ -1177,7 +1177,7 @@ private boolean isPreflightAuthorized(final WebResponse preflightResponse) { if (HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS.equalsIgnoreCase(pair.getName())) { String value = pair.getValue(); if (value != null) { - value = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(value); + value = org.htmlunit.util.StringUtils.toRootLowerCase(value); final String[] values = org.htmlunit.util.StringUtils.splitAtComma(value); for (String part : values) { part = part.trim(); @@ -1190,7 +1190,7 @@ private boolean isPreflightAuthorized(final WebResponse preflightResponse) { } for (final Entry header : webRequest_.getAdditionalHeaders().entrySet()) { - final String key = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(header.getKey()); + final String key = org.htmlunit.util.StringUtils.toRootLowerCase(header.getKey()); if (isPreflightHeader(key, header.getValue()) && !accessControlValues.contains(key)) { return false; @@ -1252,7 +1252,7 @@ public void setRequestHeader(final String name, final String value) { * @return {@code true} if the header can be set from JavaScript */ static boolean isAuthorizedHeader(final String name) { - final String nameLowerCase = org.htmlunit.util.StringUtils.toRootLowerCaseWithCache(name); + final String nameLowerCase = org.htmlunit.util.StringUtils.toRootLowerCase(name); if (PROHIBITED_HEADERS_.contains(nameLowerCase)) { return false; } diff --git a/src/main/java/org/htmlunit/javascript/host/xml/XMLSerializer.java b/src/main/java/org/htmlunit/javascript/host/xml/XMLSerializer.java index 167ec15c469..92c6b75ae7e 100644 --- a/src/main/java/org/htmlunit/javascript/host/xml/XMLSerializer.java +++ b/src/main/java/org/htmlunit/javascript/host/xml/XMLSerializer.java @@ -224,7 +224,7 @@ else if (foredNamespace != null) { } } if (!startTagClosed) { - final String tagName = StringUtils.toRootLowerCaseWithCache(nodeName); + final String tagName = StringUtils.toRootLowerCase(nodeName); if (NON_EMPTY_TAGS.contains(tagName)) { builder.append(">'); } diff --git a/src/main/java/org/htmlunit/svg/SvgElementFactory.java b/src/main/java/org/htmlunit/svg/SvgElementFactory.java index a0bbd4d8fc7..cf56c5ae4f1 100644 --- a/src/main/java/org/htmlunit/svg/SvgElementFactory.java +++ b/src/main/java/org/htmlunit/svg/SvgElementFactory.java @@ -61,7 +61,7 @@ public class SvgElementFactory implements ElementFactory { try { for (final Class klass : CLASSES_) { final String key = klass.getField("TAG_NAME").get(null).toString(); - ELEMENTS_.put(StringUtils.toRootLowerCaseWithCache(key), klass); + ELEMENTS_.put(StringUtils.toRootLowerCase(key), klass); } } catch (final Exception e) { @@ -94,7 +94,7 @@ public DomElement createElementNS(final SgmlPage page, final String namespaceURI final Attributes attributes, final boolean checkBrowserCompatibility) { final Map attributeMap = toMap(page, attributes); - qualifiedNameLC = StringUtils.toRootLowerCaseWithCache(qualifiedNameLC); + qualifiedNameLC = StringUtils.toRootLowerCase(qualifiedNameLC); String tagNameLC = qualifiedNameLC; if (tagNameLC.indexOf(':') != -1) { tagNameLC = tagNameLC.substring(tagNameLC.indexOf(':') + 1); diff --git a/src/main/java/org/htmlunit/util/MimeType.java b/src/main/java/org/htmlunit/util/MimeType.java index 474d4d11edb..643e55ca9fa 100644 --- a/src/main/java/org/htmlunit/util/MimeType.java +++ b/src/main/java/org/htmlunit/util/MimeType.java @@ -14,8 +14,7 @@ */ package org.htmlunit.util; -import java.util.HashMap; -import java.util.Map; +import org.htmlunit.cyberneko.util.FastHashMap; /** * Utility holding information about association between MIME type and file extensions. @@ -51,7 +50,37 @@ public final class MimeType { /** "image/png". */ public static final String IMAGE_PNG = "image/png"; - private static final Map type2extension = buildMap(); + private static final FastHashMap type2extension = buildMap(); + + /** + * A map to avoid lowercase conversion and a check check if this is one of + * our mimetype we know. The value is not used. + */ + private static final FastHashMap lookupMap = new FastHashMap<>(2 * 16 + 1, 0.7f); + + static { + lookupMap.put("application/javascript", true); + lookupMap.put("application/x-ecmascript", true); + lookupMap.put("application/x-javascript", true); + lookupMap.put("text/ecmascript", true); + lookupMap.put("application/ecmascript", true); + lookupMap.put("text/javascript1.0", true); + lookupMap.put("text/javascript1.1", true); + lookupMap.put("text/javascript1.2", true); + lookupMap.put("text/javascript1.3", true); + lookupMap.put("text/javascript1.4", true); + lookupMap.put("text/javascript1.5", true); + lookupMap.put("text/jscript", true); + lookupMap.put("text/livescript", true); + lookupMap.put("text/x-ecmascript", true); + lookupMap.put("text/x-javascript", true); + + // have uppercase ready too, keys() is safe for + // concurrent modification + for (final String k : lookupMap.keys()) { + lookupMap.put(k.toUpperCase(), true); + } + } /** * See @@ -64,7 +93,7 @@ public static boolean isJavascriptMimeType(final String mimeType) { if (mimeType == null) { return false; } - final String mimeTypeLC = StringUtils.toRootLowerCaseWithCache(mimeType); + final String mimeTypeLC = StringUtils.toRootLowerCase(mimeType); return TEXT_JAVASCRIPT.equals(mimeTypeLC) || "application/javascript".equals(mimeTypeLC) @@ -95,7 +124,16 @@ public static boolean isObsoleteJavascriptMimeType(final String mimeType) { if (mimeType == null) { return false; } - final String mimeTypeLC = StringUtils.toRootLowerCaseWithCache(mimeType); + + // go a cheap route first + if (lookupMap.get(mimeType) != null) { + return true; + } + + // this is our fallback in case we have not found the usual casing + // our target is ASCII, we can lowercase the normal way because + // matching some languages with strange rules does not matter, cheaper! + final String mimeTypeLC = mimeType.toLowerCase(); return "application/javascript".equals(mimeTypeLC) || "application/ecmascript".equals(mimeTypeLC) @@ -114,8 +152,8 @@ public static boolean isObsoleteJavascriptMimeType(final String mimeType) { || "text/x-javascript".equals(mimeTypeLC); } - private static Map buildMap() { - final Map map = new HashMap<>(); + private static FastHashMap buildMap() { + final FastHashMap map = new FastHashMap<>(2 * 11 + 1, 0.7f); map.put("application/pdf", "pdf"); map.put("application/x-javascript", "js"); map.put("image/gif", "gif"); @@ -127,6 +165,12 @@ private static Map buildMap() { map.put(MimeType.TEXT_HTML, "html"); map.put(TEXT_PLAIN, "txt"); map.put("image/x-icon", "ico"); + + // have uppercase ready too, keys() is safe for + // concurrent modification + for (final String k : map.keys()) { + map.put(k.toUpperCase(), map.get(k)); + } return map; } @@ -143,11 +187,17 @@ private MimeType() { * @return {@code null} if none is known */ public static String getFileExtension(final String contentType) { - final String value = type2extension.get(StringUtils.toRootLowerCaseWithCache(contentType)); - if (value == null) { + if (contentType == null) { return "unknown"; } - return value; + String value = type2extension.get(contentType); + if (value == null) { + // fallback + final String uppercased = contentType.toLowerCase(); + value = type2extension.get(uppercased); + } + + return value == null ? "unknown" : value; } } diff --git a/src/main/java/org/htmlunit/util/OrderedFastHashMap.java b/src/main/java/org/htmlunit/util/OrderedFastHashMap.java new file mode 100644 index 00000000000..a5b5d8fa362 --- /dev/null +++ b/src/main/java/org/htmlunit/util/OrderedFastHashMap.java @@ -0,0 +1,996 @@ +/* + * Copyright (c) 2002-2024 Gargoyle Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.htmlunit.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; + +/** + * Simple and efficient linked map or better ordered map implementation to + * replace the default linked list which is heavy. + * + * This map does not support null and it is not thread-safe. It implements the + * map interface but only for compatibility reason in the sense of replacing a + * regular map. Iterator and streaming methods are either not implemented or + * less efficient. + * + * It goes the extra mile to avoid the overhead of wrapper objects. + * + * Because you typically know what you do, we run minimal index checks only and + * rely on the default exceptions by Java. Why should we do things twice? + * + * Important Note: This is meant for small maps because to save on memory + * allocation and churn, we are not keeping a wrapper for a reference from the + * map to the list, only from the list to the map. Hence when you remove a key, + * we have to iterate the entire list. Mostly, half of it most likely, but still + * expensive enough. When you have something small like 10 to 20 entries, this + * won't matter that much especially when a remove might be a rare event. + * + * This is based on FashHashMap from XLT which is based on a version from: + * https://github.com/mikvor/hashmapTest/blob/master/src/main/java/map/objobj/ObjObjMap.java + * No concrete license specified at the source. The project is public domain. + * + * @param the type of the key + * @param the type of the value + * + * @author René Schwietzke + */ +public class OrderedFastHashMap implements Map, Serializable { + // our placeholders in the map + private static Object FREE_KEY_ = null; + private static Object REMOVED_KEY_ = new Object(); + + // Fill factor, must be between (0 and 1) + private static final double FILLFACTOR_ = 0.7d; + + // The map with the key value pairs */ + private Object[] mapData_; + + // We will resize a map once it reaches this size + private int mapThreshold_; + + // Current map size + private int mapSize_; + + // the list to impose order, the list refers to the key and value + // position in the map, hence needs an update every time the + // map sees an update (in regards to positions). + private int[] orderedList_; + + // the size of the orderedList, in case we proactivly sized + // it larger + private int orderedListSize_; + + /** + * Default constructor which create an ordered map with default size. + */ + public OrderedFastHashMap() { + this(8); + } + + /** + * Custom constructor to get a map with a custom size and fill factor. We are + * not spending time on range checks, rather use a default if things are wrong. + * + * @param size the size to use, must 0 or positive, negative values default to 0 + */ + public OrderedFastHashMap(final int size) { + if (size > 0) { + final int capacity = arraySize(size, FILLFACTOR_); + + this.mapData_ = new Object[capacity << 1]; + this.mapThreshold_ = (int) (capacity * FILLFACTOR_); + + this.orderedList_ = new int[capacity]; + } + else { + this.mapData_ = new Object[0]; + this.mapThreshold_ = 0; + + this.orderedList_ = new int[0]; + } + } + + /** + * Get a value for a key, any key type is permitted due to + * the nature of the Map interface. + * + * @param key the key + * @return the value or null, if the key does not exist + */ + @Override + public V get(final Object key) { + final int length = this.mapData_.length; + + // nothing in it + if (length == 0) { + return null; + } + + int ptr = (key.hashCode() & ((length >> 1) - 1)) << 1; + Object k = mapData_[ptr]; + + if (k == FREE_KEY_) { + return null; // end of chain already + } + + // we checked FREE + if (k.hashCode() == key.hashCode() && k.equals(key)) { + return (V) this.mapData_[ptr + 1]; + } + + // we have not found it, search longer + final int originalPtr = ptr; + while (true) { + ptr = (ptr + 2) & (length - 1); // that's next index + + // if we searched the entire array, we can stop + if (originalPtr == ptr) { + return null; + } + + k = this.mapData_[ptr]; + + if (k == FREE_KEY_) { + return null; + } + + if (k != REMOVED_KEY_) { + if (k.hashCode() == key.hashCode() && k.equals(key)) { + return (V) this.mapData_[ptr + 1]; + } + } + } + } + + /** + * Adds a key and value to the internal position structure + * + * @param key the key + * @param value the value to store + * @param listPosition defines where to add the new key/value pair + * + * @return the old value or null if they key was not known before + */ + private V put(final K key, final V value, final Position listPosition) { + if (mapSize_ >= mapThreshold_) { + rehash(this.mapData_.length == 0 ? 4 : this.mapData_.length << 1); + } + + int ptr = getStartIndex(key) << 1; + Object k = mapData_[ptr]; + + if (k == FREE_KEY_) { + // end of chain already + mapData_[ptr] = key; + mapData_[ptr + 1] = value; + + // ok, remember position, it is a new entry + orderedListAdd(listPosition, ptr); + + mapSize_++; + + return null; + } + else if (k.equals(key)) { + // we check FREE and REMOVED prior to this call + final Object ret = mapData_[ptr + 1]; + mapData_[ptr + 1] = value; + + /// existing entry, no need to update the position + + return (V) ret; + } + + int firstRemoved = -1; + if (k == REMOVED_KEY_) { + firstRemoved = ptr; // we may find a key later + } + + while (true) { + ptr = (ptr + 2) & (this.mapData_.length - 1); // that's next index calculation + k = mapData_[ptr]; + + if (k == FREE_KEY_) { + if (firstRemoved != -1) { + ptr = firstRemoved; + } + mapData_[ptr] = key; + mapData_[ptr + 1] = value; + + // ok, remember position, it is a new entry + orderedListAdd(listPosition, ptr); + + mapSize_++; + + return null; + } + else if (k.equals(key)) { + final Object ret = mapData_[ptr + 1]; + mapData_[ptr + 1] = value; + + // same key, different value, this does not change the order + + return (V) ret; + } + else if (k == REMOVED_KEY_) { + if (firstRemoved == -1) { + firstRemoved = ptr; + } + } + } + } + + /** + * Remove a key from the map. Returns the stored value or + * null of the key is not known. + * + * @param key the key to remove + * @return the stored value or null if the key does not exist + */ + @Override + public V remove(final Object key) { + final int length = this.mapData_.length; + // it is empty + if (length == 0) { + return null; + } + + int ptr = getStartIndex(key) << 1; + Object k = this.mapData_[ptr]; + + if (k == FREE_KEY_) { + return null; // end of chain already + } + else if (k.equals(key)) { + // we check FREE and REMOVED prior to this call + this.mapSize_--; + + if (this.mapData_[(ptr + 2) & (length - 1)] == FREE_KEY_) { + this.mapData_[ptr] = FREE_KEY_; + } + else { + this.mapData_[ptr] = REMOVED_KEY_; + } + + final V ret = (V) this.mapData_[ptr + 1]; + this.mapData_[ptr + 1] = null; + + // take this out of the list + orderedListRemove(ptr); + + return ret; + } + + while (true) { + ptr = (ptr + 2) & (length - 1); // that's next index calculation + k = this.mapData_[ptr]; + + if (k == FREE_KEY_) { + return null; + } + else if (k.equals(key)) { + this.mapSize_--; + if (this.mapData_[(ptr + 2) & (length - 1)] == FREE_KEY_) { + this.mapData_[ptr] = FREE_KEY_; + } + else { + this.mapData_[ptr] = REMOVED_KEY_; + } + + final V ret = (V) this.mapData_[ptr + 1]; + this.mapData_[ptr + 1] = null; + + // take this out of the list + orderedListRemove(ptr); + + return ret; + } + } + } + + /** + * Returns the size of the map, effectively the number of entries. + * + * @return the size of the map + */ + public int size() { + return mapSize_; + } + + /** + * Rehash the map. + * + * @param newCapacity the new size of the map + */ + private void rehash(final int newCapacity) { + this.mapThreshold_ = (int) (newCapacity / 2 * FILLFACTOR_); + + final Object[] oldData = this.mapData_; + + this.mapData_ = new Object[newCapacity]; + + // we just have to grow it and not touch it at all after that, + // just use it as source for the new map via the old + final int[] oldOrderedList = this.orderedList_; + final int oldOrderedListSize = this.orderedListSize_; + this.orderedList_ = new int[newCapacity]; + + this.mapSize_ = 0; + this.orderedListSize_ = 0; + + // we use our ordered list as source and the old + // array as reference + // we basically rebuild the map and the ordering + // from scratch + for (int i = 0; i < oldOrderedListSize; i++) { + final int pos = oldOrderedList[i]; + + // get us the old data + final K key = (K) oldData[pos]; + final V value = (V) oldData[pos + 1]; + + // write the old to the new map without updating + // the positioning + put(key, value, Position.LAST); + } + } + + /** + * Returns a list of all keys in order of addition. + * This is an expensive operation, because we get a static + * list back that is not backed by the implementation. Changes + * to the returned list are not reflected in the map. + * + * @return a list of keys as inserted into the map + */ + public List keys() { + final List result = new ArrayList<>(this.orderedListSize_); + + for (int i = 0; i < this.orderedListSize_; i++) { + final int pos = this.orderedList_[i]; + final Object o = this.mapData_[pos]; + result.add((K) o); + } + + return result; + } + + /** + * Returns a list of all values ordered by when the key was + * added. This is an expensive operation, because we get a static + * list back that is not backed by the implementation. Changes + * to the returned list are not reflected in the map. + * + * @return a list of values + */ + public List values() { + final List result = new ArrayList<>(this.orderedListSize_); + + for (int i = 0; i < this.orderedListSize_; i++) { + final int pos = this.orderedList_[i]; + final Object o = this.mapData_[pos + 1]; + result.add((V) o); + } + + return result; + } + + /** + * Clears the map, reuses the data structure by clearing it out. It won't shrink + * the underlying arrays! + */ + public void clear() { + this.mapSize_ = 0; + this.orderedListSize_ = 0; + Arrays.fill(this.mapData_, FREE_KEY_); + // Arrays.fill(this.orderedList, 0); + } + + /** + * Get us the start index from where we search or insert into the map + * + * @param key the key to calculate the position for + * @return the start position + */ + private int getStartIndex(final Object key) { + // key is not null here + return key.hashCode() & ((this.mapData_.length >> 1) - 1); + } + + /** + * Return the least power of two greater than or equal to the specified value. + * + *

+ * Note that this function will return 1 when the argument is 0. + * + * @param x a long integer smaller than or equal to 262. + * @return the least power of two greater than or equal to the specified value. + */ + private static long nextPowerOfTwo(final long x) { + if (x == 0) { + return 1; + } + + long r = x - 1; + r |= r >> 1; + r |= r >> 2; + r |= r >> 4; + r |= r >> 8; + r |= r >> 16; + + return (r | r >> 32) + 1; + } + + /** + * Returns the least power of two smaller than or equal to 230 and + * larger than or equal to Math.ceil( expected / f ). + * + * @param expected the expected number of elements in a hash table. + * @param f the load factor. + * @return the minimum possible size for a backing array. + * @throws IllegalArgumentException if the necessary size is larger than + * 230. + */ + private static int arraySize(final int expected, final double f) { + final long s = Math.max(2, nextPowerOfTwo((long) Math.ceil(expected / f))); + + if (s > (1 << 30)) { + throw new IllegalArgumentException( + "Too large (" + expected + " expected elements with load factor " + f + ")"); + } + return (int) s; + } + + /** + * Returns an entry consisting of key and value at a given position. + * This position relates to the ordered key list that maintain the + * addition order for this map. + * + * @param index the position to fetch + * @return an entry of key and value + * @throws IndexOutOfBoundsException when the ask for the position is invalid + */ + public Entry getEntry(final int index) { + if (index < 0 || index >= this.orderedListSize_) { + throw new IndexOutOfBoundsException(String.format("Index: %s, Size: %s", index, this.orderedListSize_)); + } + + final int pos = this.orderedList_[index]; + return new Entry(this.mapData_[pos], this.mapData_[pos + 1]); + } + + /** + * Returns the key at a certain position of the ordered list that + * keeps the addition order of this map. + * + * @param index the position to fetch + * @return the key at this position + * @throws IndexOutOfBoundsException when the ask for the position is invalid + */ + public K getKey(final int index) { + if (index < 0 || index >= this.orderedListSize_) { + throw new IndexOutOfBoundsException(String.format("Index: %s, Size: %s", index, this.orderedListSize_)); + } + + final int pos = this.orderedList_[index]; + return (K) this.mapData_[pos]; + } + + /** + * Returns the value at a certain position of the ordered list that + * keeps the addition order of this map. + * + * @param index the position to fetch + * @return the value at this position + * @throws IndexOutOfBoundsException when the ask for the position is invalid + */ + public V getValue(final int index) { + if (index < 0 || index >= this.orderedListSize_) { + throw new IndexOutOfBoundsException(String.format("Index: %s, Size: %s", index, this.orderedListSize_)); + } + + final int pos = this.orderedList_[index]; + return (V) this.mapData_[pos + 1]; + } + + /** + * Removes a key and value from this map based on the position + * in the backing list, rather by key as usual. + * + * @param index the position to remove the data from + * @return the value stored + * @throws IndexOutOfBoundsException when the ask for the position is invalid + */ + public V remove(final int index) { + if (index < 0 || index >= this.orderedListSize_) { + throw new IndexOutOfBoundsException(String.format("Index: %s, Size: %s", index, this.orderedListSize_)); + } + + final int pos = this.orderedList_[index]; + final K key = (K) this.mapData_[pos]; + + return remove(key); + } + + @Override + public V put(final K key, final V value) { + return this.put(key, value, Position.LAST); + } + + public V addFirst(final K key, final V value) { + return this.put(key, value, Position.FIRST); + } + + public V add(final K key, final V value) { + return this.put(key, value, Position.LAST); + } + + public V addLast(final K key, final V value) { + return this.put(key, value, Position.LAST); + } + + public V getFirst() { + return getValue(0); + } + + public V getLast() { + return getValue(this.orderedListSize_ - 1); + } + + public V removeFirst() { + if (this.orderedListSize_ > 0) { + final int pos = this.orderedList_[0]; + final K key = (K) this.mapData_[pos]; + return remove(key); + } + else { + return null; + } + } + + public V removeLast() { + if (this.orderedListSize_ > 0) { + final int pos = this.orderedList_[this.orderedListSize_ - 1]; + final K key = (K) this.mapData_[pos]; + return remove(key); + } + else { + return null; + } + } + + /** + * Checks of a key is in the map. + * + * @param key the key to check + * @return true of the key is in the map, false otherwise + */ + @Override + public boolean containsKey(final Object key) { + return get(key) != null; + } + + @Override + public boolean containsValue(final Object value) { + // that is expensive, we have to iterate everything + for (int i = 0; i < this.orderedListSize_; i++) { + final int pos = this.orderedList_[i] + 1; + final Object v = this.mapData_[pos]; + + // do we match? + if (v == value || v.equals(value)) { + return true; + } + } + + return false; + } + + @Override + public boolean isEmpty() { + return this.mapSize_ == 0; + } + + @Override + public Set> entrySet() { + final Set> set = new OrderedEntrySet<>(this); + return set; + } + + @Override + public Set keySet() { + final Set set = new OrderedKeySet<>(this); + return set; + } + + /** + * Just reverses the ordering of the map as created so far. + */ + public void reverse() { + // In-place reversal + final int to = this.orderedListSize_ / 2; + + for (int i = 0; i < to; i++) { + // Swapping the elements + final int j = this.orderedList_[i]; + this.orderedList_[i] = this.orderedList_[this.orderedListSize_ - i - 1]; + this.orderedList_[this.orderedListSize_ - i - 1] = j; + } + } + + /** + * This set does not support any modifications through its interface. All such + * methods will throw {@link UnsupportedOperationException}. + */ + static class OrderedEntrySet implements Set> { + private final OrderedFastHashMap backingMap_; + + OrderedEntrySet(final OrderedFastHashMap backingMap) { + this.backingMap_ = backingMap; + } + + @Override + public int size() { + return this.backingMap_.size(); + } + + @Override + public boolean isEmpty() { + return this.backingMap_.isEmpty(); + } + + @Override + public boolean contains(final Object o) { + if (o instanceof Map.Entry) { + final Map.Entry ose = (Map.Entry) o; + final Object k = ose.getKey(); + final Object v = ose.getValue(); + + final Object value = this.backingMap_.get(k); + if (value != null) { + return v.equals(value); + } + } + + return false; + } + + @Override + public Object[] toArray() { + final Object[] array = new Object[this.backingMap_.orderedListSize_]; + return toArray(array); + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(final T[] a) { + final T[] array; + if (a.length >= this.backingMap_.orderedListSize_) { + array = a; + } + else { + array = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), + this.backingMap_.orderedListSize_); + } + + for (int i = 0; i < this.backingMap_.orderedListSize_; i++) { + array[i] = (T) this.backingMap_.getEntry(i); + } + + return (T[]) array; + } + + @Override + public Iterator> iterator() { + return new OrderedEntryIterator(); + } + + @Override + public boolean add(final Map.Entry e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(final Collection> c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + class OrderedEntryIterator implements Iterator> { + private int pos_ = 0; + + @Override + public boolean hasNext() { + return pos_ < backingMap_.orderedListSize_; + } + + @Override + public Map.Entry next() { + if (pos_ < backingMap_.orderedListSize_) { + return backingMap_.getEntry(pos_++); + } + throw new NoSuchElementException(); + } + } + } + + static class OrderedKeySet implements Set { + private final OrderedFastHashMap backingMap_; + + OrderedKeySet(final OrderedFastHashMap backingMap) { + this.backingMap_ = backingMap; + } + + @Override + public int size() { + return this.backingMap_.size(); + } + + @Override + public boolean isEmpty() { + return this.backingMap_.isEmpty(); + } + + @Override + public boolean contains(final Object o) { + return this.backingMap_.containsKey(o); + } + + @Override + public Object[] toArray() { + final Object[] array = new Object[this.backingMap_.orderedListSize_]; + return toArray(array); + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(final T[] a) { + final T[] array; + + if (a.length >= this.backingMap_.orderedListSize_) { + array = a; + } + else { + array = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), + this.backingMap_.orderedListSize_); + } + + for (int i = 0; i < this.backingMap_.orderedListSize_; i++) { + array[i] = (T) this.backingMap_.getKey(i); + } + + return (T[]) array; + } + + @Override + public Iterator iterator() { + return new OrderedKeyIterator(); + } + + class OrderedKeyIterator implements Iterator { + private int pos_ = 0; + + @Override + public boolean hasNext() { + return this.pos_ < backingMap_.orderedListSize_; + } + + @Override + public K next() { + if (this.pos_ < backingMap_.orderedListSize_) { + return backingMap_.getKey(this.pos_++); + } + throw new NoSuchElementException(); + } + } + + @Override + public boolean add(final K e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(final Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(final Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + } + + @Override + public void putAll(final Map src) { + if (src == this) { + throw new IllegalArgumentException("Cannot add myself"); + } + + for (final Map.Entry entry : src.entrySet()) { + put(entry.getKey(), entry.getValue(), Position.LAST); + } + } + + private void orderedListAdd(final Position listPosition, final int position) { + // the list should still have room, otherwise the map was + // grown already and the ordering list with it + if (listPosition == Position.FIRST) { + System.arraycopy(this.orderedList_, 0, this.orderedList_, 1, this.orderedList_.length - 1); + this.orderedList_[0] = position; + this.orderedListSize_++; + } + else if (listPosition == Position.LAST) { + this.orderedList_[this.orderedListSize_] = position; + this.orderedListSize_++; + } + else { + // if none, we are rebuilding the map and don't have to do a thing + } + } + + private void orderedListRemove(final int position) { + // find the positional information + int i = 0; + for ( ; i < this.orderedListSize_; i++) { + if (this.orderedList_[i] == position) { + this.orderedList_[i] = -1; + if (i < this.orderedListSize_) { + // not the last element, compact + System.arraycopy(this.orderedList_, i + 1, this.orderedList_, i, this.orderedListSize_ - i); + } + this.orderedListSize_--; + + return; + } + } + + if (i == this.orderedListSize_) { + throw new IllegalArgumentException(String.format("Position %s was not in order list", position)); + } + } + + @Override + public String toString() { + final int maxLen = 10; + + return String.format( + "mapData=%s, mapFillFactor=%s, mapThreshold=%s, mapSize=%s,%norderedList=%s, orderedListSize=%s", + mapData_ != null ? Arrays.asList(mapData_).subList(0, Math.min(mapData_.length, maxLen)) : null, + FILLFACTOR_, mapThreshold_, mapSize_, + orderedList_ != null + ? Arrays.toString(Arrays.copyOf(orderedList_, Math.min(orderedList_.length, maxLen))) + : null, + orderedListSize_); + } + + /** + * Helper for identifying if we need to position our new entry differently. + */ + private enum Position { + NONE, FIRST, LAST + } + + /** + * Well, we need that to satisfy the map implementation concept. + * + * @param the key + * @param the value + */ + static class Entry implements Map.Entry { + private final K key_; + private final V value_; + + Entry(final K key, final V value) { + this.key_ = key; + this.value_ = value; + } + + @Override + public K getKey() { + return key_; + } + + @Override + public V getValue() { + return value_; + } + + @Override + public V setValue(final V value) { + throw new UnsupportedOperationException("This map does not support write-through via an entry"); + } + + @Override + public int hashCode() { + return Objects.hashCode(key_) ^ Objects.hashCode(value_); + } + + @Override + public String toString() { + return key_ + "=" + value_; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + + if (o instanceof Map.Entry) { + final Map.Entry e = (Map.Entry) o; + + if (Objects.equals(key_, e.getKey()) && Objects.equals(value_, e.getValue())) { + return true; + } + } + + return false; + } + + } +} diff --git a/src/main/java/org/htmlunit/util/StringUtils.java b/src/main/java/org/htmlunit/util/StringUtils.java index ba17b944ad3..9045675c266 100644 --- a/src/main/java/org/htmlunit/util/StringUtils.java +++ b/src/main/java/org/htmlunit/util/StringUtils.java @@ -15,9 +15,9 @@ package org.htmlunit.util; import java.nio.charset.Charset; -import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -49,8 +49,8 @@ public final class StringUtils { + "\\s*((0|[1-9]\\d?|100)(.\\d*)?)%\\s*\\)"); private static final Pattern ILLEGAL_FILE_NAME_CHARS = Pattern.compile("\\\\|/|\\||:|\\?|\\*|\"|<|>|\\p{Cntrl}"); - private static final Map CamelizeCache_ = new HashMap<>(); - private static final Map RootLowercaseCache_ = new HashMap<>(); + private static final Map CamelizeCache_ = new ConcurrentHashMap<>(); + private static final Map RootLowercaseCache_ = new ConcurrentHashMap<>(); /** * Disallow instantiation of this class. @@ -348,20 +348,15 @@ public static String cssCamelize(final String string) { return result; } - public static String toRootLowerCaseWithCache(final String string) { - if (string == null) { - return null; - } - - String result = RootLowercaseCache_.get(string); - if (null != result) { - return result; - } - - result = string.toLowerCase(Locale.ROOT); - RootLowercaseCache_.put(string, result); - - return result; + /** + * Lowercases a string by checking and check for null first. There + * is no cache involved and the ROOT locale is used to convert it. + * + * @param s the string to lowercase + * @return the lowercased string + */ + public static String toRootLowerCase(final String s) { + return s == null ? null : s.toLowerCase(Locale.ROOT); } /** diff --git a/src/main/java/org/htmlunit/util/XmlUtils.java b/src/main/java/org/htmlunit/util/XmlUtils.java index 5725eff9cf8..b1ed4d6f40f 100644 --- a/src/main/java/org/htmlunit/util/XmlUtils.java +++ b/src/main/java/org/htmlunit/util/XmlUtils.java @@ -20,7 +20,6 @@ import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; -import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -274,7 +273,7 @@ private static DomNode createFrom(final SgmlPage page, final Node source, final namedNodeMapToSaxAttributes(nodeAttributes, attributesOrderMap, source)); } - final Map attributes = new LinkedHashMap<>(); + final OrderedFastHashMap attributes = new OrderedFastHashMap<>(); for (int i = 0; i < nodeAttributes.getLength(); i++) { final int orderedIndex = Platform.getIndex(nodeAttributes, attributesOrderMap, source, i); final Attr attribute = (Attr) nodeAttributes.item(orderedIndex); diff --git a/src/test/java/org/htmlunit/util/OrderedFastHashMapTest.java b/src/test/java/org/htmlunit/util/OrderedFastHashMapTest.java new file mode 100644 index 00000000000..1cb7f70c2b1 --- /dev/null +++ b/src/test/java/org/htmlunit/util/OrderedFastHashMapTest.java @@ -0,0 +1,1516 @@ +/* + * Copyright (c) 2002-2024 Gargoyle Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.htmlunit.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.Test; + +public class OrderedFastHashMapTest { + + @Test + public void ctr() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + assertEquals(0, m.size()); + assertEquals(0, m.entrySet().size()); + assertEquals(0, m.keys().size()); + assertEquals(0, m.values().size()); + + assertNull(m.get("test")); + } + + @Test + public void simplePut() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("K", "V"); + + assertEquals("V", m.get("K")); + assertEquals("K", m.getKey(0)); + assertEquals("V", m.getValue(0)); + assertEquals("V", m.getFirst()); + assertEquals("V", m.getLast()); + + assertEquals("K", m.getEntry(0).getKey()); + assertEquals("V", m.getEntry(0).getValue()); + } + + @Test + public void allowAnEmptyStart() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(0); + assertNull(m.get("K")); + assertNull(m.remove("K")); + + m.put("K", "V"); + + assertEquals("V", m.get("K")); + assertEquals("K", m.getKey(0)); + assertEquals("V", m.getValue(0)); + assertEquals("V", m.getFirst()); + assertEquals("V", m.getLast()); + + assertEquals("K", m.getEntry(0).getKey()); + assertEquals("V", m.getEntry(0).getValue()); + } + + @Test + public void simpleMultiPut() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("K", "VK"); + m.put("B", "VB"); + m.put("A", "VA"); + m.put("Z", "VZ"); + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + t.accept("K", 0); + t.accept("B", 1); + t.accept("A", 2); + t.accept("Z", 3); + + // adding something last + m.put("0", "V0"); + m.put("z", "Vz"); + + t.accept("K", 0); + t.accept("B", 1); + t.accept("A", 2); + t.accept("Z", 3); + t.accept("0", 4); + t.accept("z", 5); + } + + @Test + public void putAgainDoesNotChangeOrder() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("K", "VK"); + m.put("B", "VB"); + m.put("A", "VA"); + m.put("Z", "VZ"); + + m.put("B", "VB"); + m.put("K", "VK"); + m.put("Z", "VZ"); + m.put("A", "VA"); + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + t.accept("K", 0); + t.accept("B", 1); + t.accept("A", 2); + t.accept("Z", 3); + } + + @Test + public void getDoesNotChangeOrder() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("K", "VK"); + m.put("B", "VB"); + m.put("A", "VA"); + m.put("Z", "VZ"); + + assertEquals("VZ", m.get("Z")); + assertEquals("VA", m.get("A")); + assertEquals("VB", m.get("B")); + assertEquals("VZ", m.get("Z")); + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + t.accept("K", 0); + t.accept("B", 1); + t.accept("A", 2); + t.accept("Z", 3); + } + + @Test + public void growSmall() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(3); + for (int i = 0; i < 10; i++) { + m.add(String.valueOf(i), "V" + String.valueOf(i)); + } + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + for (int i = 0; i < 10; i++) { + t.accept(String.valueOf(i), i); + } + } + + @Test + public void growBig() { + final List keys = new ArrayList<>(); + final Random r = new Random(); + + for (int i = 0; i < 200; i++) { + final String key = RandomUtils.randomString(r, 3, 10); + keys.add(key); + } + + // ok, we got random keys, our values will be V + its key + final OrderedFastHashMap m = new OrderedFastHashMap<>(10); + for (final String key : keys) { + m.put(key, "V" + key); + } + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + + // ok, order will match when asking for keys + for (int i = 0; i < 200; i++) { + t.accept(keys.get(i), i); + } + } + + @Test + public void keys() { + final OrderedFastHashMap f = new OrderedFastHashMap<>(3); + f.put("aa", 1); + f.put("bb", 2); + f.put("cc", 3); + f.put("dd", 4); + f.put("ee", 5); + + final List k = f.keys(); + assertEquals(5, k.size()); + assertTrue(k.contains("aa")); + assertTrue(k.contains("bb")); + assertTrue(k.contains("cc")); + assertTrue(k.contains("dd")); + assertTrue(k.contains("ee")); + + // remove from the middle + assertEquals(Integer.valueOf(3), f.remove("cc")); + f.remove("c"); + final List k2 = f.keys(); + assertEquals(4, k2.size()); + assertTrue(k2.contains("aa")); + assertTrue(k2.contains("bb")); + assertTrue(k2.contains("dd")); + assertTrue(k2.contains("ee")); + + // add to the end, remove unknown + f.put("zz", 10); + f.remove("c"); + final List k3 = f.keys(); + assertEquals(5, k3.size()); + assertTrue(k3.contains("aa")); + assertTrue(k3.contains("bb")); + assertTrue(k3.contains("dd")); + assertTrue(k3.contains("ee")); + assertTrue(k3.contains("zz")); + + // ask for something unknown + assertNull(f.get("unknown")); + } + + @Test + public void values() { + final OrderedFastHashMap f = new OrderedFastHashMap<>(3); + f.put("aa", 1); + f.put("bb", 2); + f.put("cc", 3); + f.put("dd", 4); + f.put("ee", 5); + + // initial values + final List values = f.values(); + assertEquals(5, values.size()); + assertTrue(values.contains(1)); + assertTrue(values.contains(2)); + assertTrue(values.contains(3)); + assertTrue(values.contains(4)); + assertTrue(values.contains(5)); + + // something removed + assertEquals(Integer.valueOf(3), f.remove("cc")); + // something unknnown removed + f.remove("c"); + final List v2 = f.values(); + assertEquals(4, v2.size()); + assertTrue(v2.contains(1)); + assertTrue(v2.contains(2)); + assertTrue(v2.contains(4)); + assertTrue(v2.contains(5)); + } + + @Test + public void remove_simple_only_one() { + final OrderedFastHashMap m1 = new OrderedFastHashMap<>(); + // remove instantly + m1.put("b", "value"); + assertEquals("value", m1.remove("b")); + assertEquals(0, m1.size()); + } + + @Test + public void remove_simple_first() { + // remove first + final OrderedFastHashMap m1 = new OrderedFastHashMap<>(); + m1.put("b", "bvalue"); + m1.put("c", "cvalue"); + assertEquals("bvalue", m1.remove("b")); + assertEquals("cvalue", m1.get("c")); + assertEquals(1, m1.size()); + } + + @Test + public void remove_simple_from_the_middle() { + // remove the one in the middle + final OrderedFastHashMap m1 = new OrderedFastHashMap<>(); + m1.put("a", "avalue"); + m1.put("b", "bvalue"); + m1.put("c", "cvalue"); + assertEquals("bvalue", m1.remove("b")); + assertEquals("avalue", m1.get("a")); + assertEquals("cvalue", m1.get("c")); + assertEquals(2, m1.size()); + assertEquals("a", m1.getKey(0)); + assertEquals("c", m1.getKey(1)); + } + + @Test + public void remove_simple_unknown() { + // remove unknown + final OrderedFastHashMap m1 = new OrderedFastHashMap<>(); + assertNull(m1.remove("a")); + m1.put("b", "value"); + assertNull(m1.remove("a")); + } + + @Test + public void remove_complex() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(3); + m.put("b", "Vb1"); + m.put("a", "Va"); + m.put("d", "Vd1"); + m.put("c", "Vc"); + m.put("e", "Ve"); + + m.remove("b"); + m.remove("d"); + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + + assertEquals(3, m.size()); + t.accept("a", 0); + t.accept("c", 1); + t.accept("e", 2); + assertNull(m.get("d")); + assertNull(m.get("b")); + + // remove again + assertNull(m.remove("b")); + assertNull(m.remove("d")); + + m.put("d", "Vd"); + m.put("b", "Vb"); + + t.accept("a", 0); + t.accept("c", 1); + t.accept("e", 2); + t.accept("d", 3); + t.accept("b", 4); + } + + @Test + public void removeRandomlyToEmpty() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(15); + final Set keys = new HashSet<>(); + final Random r = new Random(); + + for (int i = 0; i < 1000; i++) { + final String key = RandomUtils.randomString(r, 1, 10); + keys.add(key); // just in case we have double keys generated + m.put(key, "V" + key); + } + assertEquals(keys.size(), m.size()); + + keys.forEach(key -> { + assertEquals("V" + key, m.get(key)); + m.remove(key); + assertNull(m.get(key)); + }); + assertEquals(0, m.size()); + } + + @Test + public void removeTryingToCoverEdges_Last() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + for (int i = 0; i < 200; i++) { + // we add two, but immediately remove the last again + m.put(String.valueOf(i), "V" + String.valueOf(i)); + m.put(String.valueOf(i + 1), "any"); + assertEquals("any", m.remove(String.valueOf(i + 1))); + } + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + + assertEquals(200, m.size()); + for (int i = 0; i < 200; i++) { + t.accept(String.valueOf(i), i); + } + } + + @Test + public void removeTryingToCoverEdges_First() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + for (int i = 0; i < 4000; i = i + 2) { + m.put(String.valueOf(i), "any"); + m.put(String.valueOf(i + 1), "V" + String.valueOf(i + 1)); + assertEquals("any", m.remove(String.valueOf(i))); + } + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + + assertEquals(2000, m.size()); + for (int i = 0; i < 4000; i = i + 2) { + t.accept(String.valueOf(i + 1), i / 2); + } + } + + @Test + public void removeTryingToCoverEdges_Middle() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + for (int i = 0; i < 3 * 1972; i = i + 3) { + m.put(String.valueOf(i), "V" + String.valueOf(i)); + m.put(String.valueOf(i + 1), "any"); + m.put(String.valueOf(i + 2), "V" + String.valueOf(i + 2)); + assertEquals("any", m.remove(String.valueOf(i + 1))); + } + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + + assertEquals(2 * 1972, m.size()); + for (int i = 0, pos = 0; i < 3 * 1972; i = i + 3, pos = pos + 2) { + t.accept(String.valueOf(i), pos); + t.accept(String.valueOf(i + 2), pos + 1); + } + } + + @Test + public void removeByIndex_first() { + final OrderedFastHashMap m = new OrderedFastHashMap(); + m.put("a", 1); + m.put("b", 2); + m.put("c", 3); + m.remove(0); + + assertEquals("b", m.getKey(0)); + assertEquals("c", m.getKey(1)); + assertEquals(2, m.size()); + } + + @Test + public void removeByIndex_second() { + final OrderedFastHashMap m = new OrderedFastHashMap(); + m.put("a", 1); + m.put("b", 2); + m.put("c", 3); + m.remove(1); + + assertEquals("a", m.getKey(0)); + assertEquals("c", m.getKey(1)); + assertEquals(2, m.size()); + } + + @Test + public void removeByIndex_last() { + final OrderedFastHashMap m = new OrderedFastHashMap(); + m.put("a", 1); + m.put("b", 2); + m.put("c", 3); + m.remove(2); + + assertEquals("a", m.getKey(0)); + assertEquals("b", m.getKey(1)); + assertEquals(2, m.size()); + } + + @Test + public void removeByIndex_middle_to_empty() { + final OrderedFastHashMap m = new OrderedFastHashMap(); + m.put("a", 1); + m.put("b", 2); + m.put("c", 3); + m.remove(1); + m.remove(1); + m.remove(0); + + assertEquals(0, m.size()); + } + + @Test + public void clear() { + final OrderedFastHashMap m = new OrderedFastHashMap(); + m.put("a", 1); + assertEquals(1, m.size()); + + m.clear(); + assertEquals(0, m.size()); + assertEquals(0, m.keys().size()); + assertEquals(0, m.values().size()); + assertNull(m.get("a")); + + m.put("b", 2); + assertEquals(1, m.size()); + m.put("a", 3); + assertEquals(2, m.size()); + + m.clear(); + assertEquals(0, m.size()); + assertEquals(0, m.keys().size()); + assertEquals(0, m.values().size()); + + m.put("a", 1); + m.put("b", 2); + m.put("c", 3); + m.put("c", 3); + assertEquals(3, m.size()); + assertEquals(3, m.keys().size()); + assertEquals(3, m.values().size()); + + assertEquals(Integer.valueOf(1), m.get("a")); + assertEquals(Integer.valueOf(2), m.get("b")); + assertEquals(Integer.valueOf(3), m.get("c")); + } + + @Test + public void removeLast() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + final Random r1 = new Random(144L); + + // empty is null + assertNull(m.removeLast()); + + for (int i = 0; i < 3 * 1972; i++) { + final String key = RandomUtils.randomString(r1, 20); + final String value = "V" + key; + + m.put(key, value); + assertEquals(value, m.removeLast()); + m.put(key, value); + } + assertEquals(3 * 1972, m.size()); + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + + final Random r2 = new Random(144L); + for (int i = 0; i < 3 * 1972; i++) { + final String key = RandomUtils.randomString(r2, 20); + t.accept(key, i); + } + + final Random r3 = new Random(144L); + for (int i = 0; i < 3 * 1972; i++) { + final String key = RandomUtils.randomString(r3, 2, 10); + m.removeLast(); + } + assertEquals(0, m.size()); + } + + @Test + public void removeFirst_Empty() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + assertNull(m.removeFirst()); + assertEquals(0, m.size()); + } + + @Test + public void removeFirst_One() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("u", "Vu"); + assertEquals("Vu", m.removeFirst()); + assertEquals(0, m.size()); + } + + @Test + public void removeFirst_Two() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("u", "Vu"); + m.put("a", "Va"); + assertEquals("Vu", m.removeFirst()); + assertEquals(1, m.size()); + assertEquals("a", m.getKey(0)); + assertEquals("Va", m.getValue(0)); + } + + @Test + public void removeFirst_WithGrowth() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(3); + final Random r = new Random(98765244L); + final List keys = new ArrayList<>(); + + final BiConsumer t = (k, index) -> { + assertEquals("V" + k, m.get(k)); + assertEquals(k, m.getKey(index)); + assertEquals("V" + k, m.getValue(index)); + }; + + for (int i = 1; i < 1992; i++) { + keys.clear(); + m.clear(); + + // setup entries + for (int e = 0; e < i; e++) { + final String k = RandomUtils.randomString(r, 20); + keys.add(k); + } + + // add these all and remove first + for (final String k : keys) { + m.put(k, "V" + k); + } + // remove first + final String rk = keys.remove(0); + assertEquals("V" + rk, m.removeFirst()); + + // add back + m.put(rk, "V" + rk); + keys.add(rk); + + // verify + assertEquals(keys.size(), m.size()); + + // order of things + for (int e = 0; e < keys.size(); e++) { + t.accept(keys.get(e), e); + } + } + } + + @Test + public void addFirst_to_empty() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.addFirst("a", "1"); + assertEquals(1, m.size()); + assertEquals("a", m.getKey(0)); + assertEquals("1", m.getValue(0)); + } + + + @Test + public void addFirst_twice() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.addFirst("a", "1"); + m.addFirst("b", "2"); + assertEquals(2, m.size()); + assertEquals("a", m.getKey(1)); + assertEquals("1", m.getValue(1)); + assertEquals("b", m.getKey(0)); + assertEquals("2", m.getValue(0)); + } + + @Test + public void addFirst__second_to_normal_added() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.add("a", "1"); + m.addFirst("b", "2"); + assertEquals(2, m.size()); + assertEquals("a", m.getKey(1)); + assertEquals("1", m.getValue(1)); + assertEquals("b", m.getKey(0)); + assertEquals("2", m.getValue(0)); + } + + @Test + public void addLast_to_empty() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.addLast("a", "1"); + assertEquals(1, m.size()); + assertEquals("a", m.getKey(0)); + assertEquals("1", m.getValue(0)); + } + + @Test + public void addLast_twice() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.addLast("a", "1"); + m.addLast("b", "2"); + assertEquals(2, m.size()); + assertEquals("a", m.getKey(0)); + assertEquals("1", m.getValue(0)); + assertEquals("b", m.getKey(1)); + assertEquals("2", m.getValue(1)); + } + + @Test + public void addLast_to_normally_added() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.add("a", "1"); + m.addLast("b", "2"); + assertEquals(2, m.size()); + assertEquals("a", m.getKey(0)); + assertEquals("1", m.getValue(0)); + assertEquals("b", m.getKey(1)); + assertEquals("2", m.getValue(1)); + } + + @Test + public void containsKey_Empty() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + assertFalse(m.containsKey("akey")); + } + + @Test + public void containsKey_True_single() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("akey", "any"); + assertTrue(m.containsKey("akey")); + } + + @Test + public void containsKey_True_many() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("akey1", "any"); + m.put("akey2", "any"); + m.put("akey3", "any"); + m.put("akey4", "any"); + assertTrue(m.containsKey("akey2")); + assertTrue(m.containsKey(new String("akey2"))); + } + + @Test + public void containsKey_True_content_based() { + // same hash and different content + final OrderedFastHashMap, String> m = new OrderedFastHashMap<>(); + final MockKey mockKey1 = new MockKey<>(10, "akey1"); + m.put(mockKey1, "any1"); + m.put(new MockKey(10, "akey2"), "any2"); + m.put(new MockKey(10, "akey3"), "any3"); + m.put(new MockKey(10, "akey4"), "any4"); + assertTrue(m.containsKey(mockKey1)); + assertTrue(m.containsKey(new MockKey<>(10, "akey1"))); + } + + @Test + public void containsKey_False_single() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("akey", "any"); + assertFalse(m.containsKey("akey1")); + } + + @Test + public void containsKey_False_many() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("akey1", "any"); + m.put("akey2", "any"); + m.put("akey3", "any"); + assertFalse(m.containsKey("akey")); + } + + @Test + public void containsKey_False_content_based() { + // same hash but different content + final OrderedFastHashMap, String> m = new OrderedFastHashMap<>(); + m.put(new MockKey(10, "akey2"), "any2"); + m.put(new MockKey(10, "akey3"), "any3"); + m.put(new MockKey(10, "akey4"), "any4"); + assertFalse(m.containsKey(new MockKey<>(10, "akey"))); + + // different hash, same content + assertFalse(m.containsKey(new MockKey<>(114, "akey2"))); + assertFalse(m.containsKey(new MockKey<>(113, "akey3"))); + assertFalse(m.containsKey(new MockKey<>(112, "akey4"))); + } + + @Test + public void containsValue_Empty() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + assertFalse(m.containsValue("avalue")); + } + + @Test + public void containsValue_True_single() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("akey3", "any"); + assertTrue(m.containsValue("any")); + } + + @Test + public void containsValue_True_content_based() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("akey1", "any1"); + m.put("akey2", "any2"); + m.put("akey3", "any3"); + assertTrue(m.containsValue("any1")); + assertTrue(m.containsValue("any2")); + assertTrue(m.containsValue("any3")); + assertTrue(m.containsValue(new String("any1"))); + assertTrue(m.containsValue(new String("any2"))); + assertTrue(m.containsValue(new String("any3"))); + } + + @Test + public void containsValue_False_single() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("akey3", "any"); + assertFalse(m.containsValue("asdf")); + } + + @Test + public void containsValue_False_many() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("akey1", "any1"); + m.put("akey2", "any2"); + m.put("akey3", "any3"); + // not mistaking the key as value + assertFalse(m.containsValue("akey1")); + assertFalse(m.containsValue("akey2")); + assertFalse(m.containsValue("akey3")); + + // just some other values + assertFalse(m.containsValue("any5")); + assertFalse(m.containsValue("any6")); + assertFalse(m.containsValue("any7")); + } + + @Test + public void isEmpty_empty() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + assertTrue(m.isEmpty()); + } + + @Test + public void isEmpty_size_zero() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(0); + assertTrue(m.isEmpty()); + } + + @Test + public void isEmpty_false() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("aaa", "a"); + assertFalse(m.isEmpty()); + } + + @Test + public void isEmpty_after_clear() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("aaa", "a"); + m.clear(); + assertTrue(m.isEmpty()); + } + + @Test + public void entry() { + final Map m = new OrderedFastHashMap<>(); + m.put("K1", "V1"); + m.put("K2", "V1"); + final Entry e = m.entrySet().iterator().next(); + + Iterator> i = m.entrySet().iterator(); + final Entry e1 = i.next(); // K1, V1 + final Entry e2 = i.next(); // K2, V1 + + m.put("K1", "V2"); + i = m.entrySet().iterator(); + final Entry e3 = i.next(); // K1, V2 + + // making sure we have all sub methods covered + assertEquals("K1", e.getKey()); + assertEquals("V1", e.getValue()); + assertEquals(e.getKey().hashCode() ^ e.getValue().hashCode(), e.hashCode()); + assertEquals("K1=V1", e.toString()); + + assertTrue(e.equals(e)); + + assertTrue(e1.equals(e)); + assertTrue(e.equals(e1)); + + assertFalse(e.equals(e2)); + assertFalse(e2.equals(e)); + + assertFalse(e.equals(e2)); + assertFalse(e3.equals(e)); + + assertFalse(e.equals("k")); + + assertThrows(UnsupportedOperationException.class, () -> e.setValue("foo")); + } + + @Test + public void entrySet_empty() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + final Set> set = m.entrySet(); + assertEquals(0, set.size()); + assertTrue(set.isEmpty()); + } + + @Test + public void entrySet_zero_size() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(0); + final Set> set = m.entrySet(); + assertEquals(0, set.size()); + assertTrue(set.isEmpty()); + } + + @Test + public void entrySet() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("K1", "V1"); + final Set> set = m.entrySet(); + assertEquals(1, set.size()); + assertFalse(set.isEmpty()); + + final Entry e1 = set.iterator().next(); + assertEquals("K1", e1.getKey()); + assertEquals("V1", e1.getValue()); + + final Map map = new OrderedFastHashMap<>(); + map.put("K1", "V1"); + map.put("K2", "V1"); + final Iterator> i = map.entrySet().iterator(); + final Entry e2 = i.next(); + final Entry e3 = i.next(); + + assertTrue(set.contains(e1)); + assertTrue(set.contains(e2)); + + assertFalse(set.contains("a")); + assertFalse(set.contains(e3)); + } + + @Test + public void entrySet_large() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + IntStream.rangeClosed(0, 11).forEach(i -> m.put("K" + i, "V" + i)); + final Set> set = m.entrySet(); + assertEquals(12, set.size()); + assertFalse(set.isEmpty()); + + final Iterator> iter = set.iterator(); + IntStream.rangeClosed(0, 11).forEach(i -> { + assertTrue(iter.hasNext()); + final Entry e = iter.next(); + assertEquals("K" + i, e.getKey()); + assertEquals("V" + i, e.getValue()); + }); + assertFalse(iter.hasNext()); + + // overread + assertThrows(NoSuchElementException.class, () -> iter.next()); + } + + @Test + public void entrySet_toArray_empty() { + final Map m = new OrderedFastHashMap<>(); + final Set> set = m.entrySet(); + assertEquals(0, set.toArray().length); + assertEquals(0, set.toArray(new Map.Entry[0]).length); + assertEquals(10, set.toArray(new String[10]).length); + + // unfilled despite length 10 + final String[] a = set.toArray(new String[10]); + for (int i = 0; i < 10; i++) { + assertNull(a[i]); + } + } + + @Test + public void entrySet_toArray_zero_sized() { + final Map m = new OrderedFastHashMap<>(0); + final Set> set = m.entrySet(); + assertEquals(0, set.toArray().length); + assertEquals(0, set.toArray(new Map.Entry[0]).length); + assertEquals(10, set.toArray(new String[10]).length); + + // unfilled despite length 10 + final String[] a = set.toArray(new String[10]); + for (int i = 0; i < 10; i++) { + assertNull(a[i]); + } + } + + @Test + public void entrySet_toArray_normal() { + final Map m = new OrderedFastHashMap<>(); + m.put("K1", "V1"); + final Set> set = m.entrySet(); + assertEquals(1, set.toArray().length); + assertEquals(1, set.toArray(new Map.Entry[0]).length); + assertEquals(1, set.toArray(new Map.Entry[1]).length); + + assertNotNull(set.toArray()[0]); + assertEquals("K1", ((Map.Entry) set.toArray()[0]).getKey()); + assertEquals("V1", ((Map.Entry) set.toArray()[0]).getValue()); + + assertEquals("K1", set.toArray(new Map.Entry[0])[0].getKey()); + assertEquals("V1", set.toArray(new Map.Entry[0])[0].getValue()); + + assertEquals("K1", set.toArray(new Map.Entry[1])[0].getKey()); + assertEquals("V1", set.toArray(new Map.Entry[1])[0].getValue()); + } + + @Test + public void entrySet_toArray_get_same_back() { + // ensure we get our array back when it is sufficiently sized + final Map m = new OrderedFastHashMap<>(); + m.put("K1", "V1"); + final Set> set = m.entrySet(); + + // right sized + final Map.Entry[] array1 = new Map.Entry[1]; + assertSame(array1, set.toArray(array1)); + + // oversized + final Map.Entry[] array2 = new Map.Entry[10]; + assertSame(array2, set.toArray(array2)); + } + + @Test + public void entrySet_toArray_large() { + final Map m = new OrderedFastHashMap<>(); + IntStream.rangeClosed(0, 44).forEach(i -> m.put("K" + i, "V" + i)); + final Object[] a1 = m.entrySet().toArray(); + final Map.Entry[] a2 = m.entrySet().toArray(new Map.Entry[45]); + assertEquals(45, a1.length); + assertEquals(45, a2.length); + + for (int i = 0; i <= 44; i++) { + assertEquals("K" + i, ((Map.Entry) a1[i]).getKey()); + assertEquals("V" + i, ((Map.Entry) a1[i]).getValue()); + + assertEquals("K" + i, a2[i].getKey()); + assertEquals("V" + i, a2[i].getValue()); + } + } + + @Test + public void keySet_empty() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + final Set set = m.keySet(); + assertEquals(0, set.size()); + assertTrue(set.isEmpty()); + } + + @Test + public void keySet_size_zero() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(0); + final Set set = m.keySet(); + assertEquals(0, set.size()); + assertTrue(set.isEmpty()); + } + + @Test + public void keySet_one() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("K1", "V1"); + final Set set = m.keySet(); + assertEquals(1, set.size()); + assertFalse(set.isEmpty()); + + final String e1 = set.iterator().next(); + assertEquals("K1", e1); + } + + @Test + public void keySet_large() { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + IntStream.rangeClosed(0, 44).forEach(i -> m.put("K" + i, "V" + i)); + final Set set = m.keySet(); + assertEquals(45, set.size()); + assertFalse(set.isEmpty()); + + final Iterator iter = set.iterator(); + IntStream.rangeClosed(0, 44).forEach(i -> { + assertTrue(iter.hasNext()); + final String e = iter.next(); + assertEquals("K" + i, e); + assertTrue(set.contains("K" + i)); + }); + assertFalse(iter.hasNext()); + + // overread + assertThrows(NoSuchElementException.class, () -> iter.next()); + } + + @Test + public void keySet_toArray_empty() { + final Map m = new OrderedFastHashMap<>(); + final Set set = m.keySet(); + assertEquals(0, set.toArray().length); + assertEquals(0, set.toArray(new String[0]).length); + assertEquals(10, set.toArray(new String[10]).length); + + // unfilled despite length 10 + final String[] a = set.toArray(new String[10]); + for (int i = 0; i < 10; i++) { + assertNull(a[i]); + } + } + + @Test + public void keySet_toArray_one() { + final Map m = new OrderedFastHashMap<>(); + m.put("K1", "V1"); + final Set set = m.keySet(); + assertEquals(1, set.toArray().length); + assertEquals(1, set.toArray(new String[0]).length); + assertEquals(1, set.toArray(new String[1]).length); + + assertEquals("K1", set.toArray()[0]); + assertEquals("K1", set.toArray(new String[0])[0]); + assertEquals("K1", set.toArray(new String[1])[0]); + } + + @Test + public void keySet_toArray_same_array() { + // ensure we get our array back when it is sufficiently sized + final Map m = new OrderedFastHashMap<>(); + m.put("K1", "V1"); + final Set set = m.keySet(); + + // right sized + final String[] array1 = new String[1]; + assertSame(array1, set.toArray(array1)); + + // oversized + final String[] array2 = new String[10]; + assertSame(array2, set.toArray(array2)); + } + + @Test + public void keySet_toArray_many() { + final Map m = new OrderedFastHashMap<>(); + IntStream.rangeClosed(0, 44).forEach(i -> m.put("K" + i, "V" + i)); + final Object[] a1 = m.keySet().toArray(); + final String[] a2 = m.keySet().toArray(new String[45]); + assertEquals(45, a1.length); + assertEquals(45, a2.length); + + for (int i = 0; i <= 44; i++) { + assertEquals("K" + i, a1[i]); + assertEquals("K" + i, a2[i]); + } + } + + @Test + public void putAll_cannot_add_self() { + // we cannot add ourselves! + final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.putAll(m); + }); + assertEquals("Cannot add myself", thrown.getMessage()); + } + + @Test + public void putAll_empty() { + // empty + final Map src = new HashMap<>(); + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.putAll(src); + assertEquals(0, m.size()); + } + + @Test + public void putAll_target_empty() { + // target empty + final Map src = new LinkedHashMap<>(); + IntStream.rangeClosed(0, 10).forEach(i -> src.put("K" + i, "V" + i)); + + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.putAll(src); + assertEquals(11, m.size()); + + final Iterator> iter = m.entrySet().iterator(); + IntStream.rangeClosed(0, 10).forEach(i -> { + assertTrue(iter.hasNext()); + final Entry e = iter.next(); + assertEquals("K" + i, e.getKey()); + assertEquals("V" + i, e.getValue()); + }); + assertFalse(iter.hasNext()); + } + + @Test + public void putAll_source_empty() { + // src empty + final Map src = new LinkedHashMap<>(); + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("K1", "V1"); + + m.putAll(src); + assertEquals(1, m.size()); + + final Iterator> iter = m.entrySet().iterator(); + assertTrue(iter.hasNext()); + final Entry e = iter.next(); + assertEquals("K1", e.getKey()); + assertEquals("V1", e.getValue()); + assertFalse(iter.hasNext()); + } + + @Test + public void putAll_target_not_empty() { + // target not empty + final Map src = new LinkedHashMap<>(); + IntStream.rangeClosed(0, 17).forEach(i -> src.put("SRCK" + i, "SRCV" + i)); + + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + IntStream.rangeClosed(100, 111).forEach(i -> m.put("K" + i, "V" + i)); + + m.putAll(src); + assertEquals(12 + 18, m.size()); + + final Iterator> iter = m.entrySet().iterator(); + IntStream.rangeClosed(100, 111).forEach(i -> { + assertTrue(iter.hasNext()); + final Entry e = iter.next(); + assertEquals("K" + i, e.getKey()); + assertEquals("V" + i, e.getValue()); + }); + assertTrue(iter.hasNext()); + IntStream.rangeClosed(0, 17).forEach(i -> { + assertTrue(iter.hasNext()); + final Entry e = iter.next(); + assertEquals("SRCK" + i, e.getKey()); + assertEquals("SRCV" + i, e.getValue()); + }); + assertFalse(iter.hasNext()); + } + + @Test + public void putAll_same_type_not_same_object() { + // same type but not same map + final OrderedFastHashMap src = new OrderedFastHashMap<>(); + IntStream.rangeClosed(19, 99).forEach(i -> src.put("K" + i, "V" + i)); + + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + IntStream.rangeClosed(0, 18).forEach(i -> m.put("K" + i, "V" + i)); + + m.putAll(src); + assertEquals(100, m.size()); + + final Set> set = m.entrySet(); + final Iterator> iter = set.iterator(); + IntStream.rangeClosed(0, 99).forEach(i -> { + assertTrue(iter.hasNext()); + final Entry e = iter.next(); + assertEquals("K" + i, e.getKey()); + assertEquals("K" + i, e.getKey()); + assertEquals("V" + i, e.getValue()); + }); + assertFalse(iter.hasNext()); + } + + @Test + public void putAll_to_another_map() { + // I can be added to other Map.putAll + final Map src = new OrderedFastHashMap<>(); + final Map m = new HashMap<>(); + m.putAll(src); + assertEquals(0, m.size()); + } + + @Test + public void collision() { + final OrderedFastHashMap, String> f = new OrderedFastHashMap, String>(13); + IntStream.range(0, 15).forEach(i -> { + f.put(new MockKey(12, "k" + i), "v" + i); + }); + + assertEquals(15, f.size()); + + IntStream.range(0, 15).forEach(i -> { + assertEquals("v" + i, f.get(new MockKey(12, "k" + i))); + }); + + // round 2 + IntStream.range(0, 20).forEach(i -> { + f.put(new MockKey(12, "k" + i), "v" + i); + }); + + assertEquals(20, f.size()); + + IntStream.range(0, 20).forEach(i -> { + assertEquals("v" + i, f.get(new MockKey(12, "k" + i))); + }); + + // round 3 + IntStream.range(0, 10).forEach(i -> { + assertEquals("v" + i, f.remove(new MockKey(12, "k" + i))); + }); + IntStream.range(10, 20).forEach(i -> { + assertEquals("v" + i, f.get(new MockKey(12, "k" + i))); + }); + } + + /** + * Overflow initial size with collision keys. Some hash code for all keys. + */ + @Test + public void overflow() { + final OrderedFastHashMap, Integer> m = new OrderedFastHashMap<>(5); + final Map, Integer> data = IntStream.range(0, 152).mapToObj(Integer::valueOf) + .collect(Collectors.toMap(i -> new MockKey(1, "k" + i), i -> i)); + + // add all + data.forEach((k, v) -> m.put(k, v)); + + // verify + data.forEach((k, v) -> assertEquals(v, m.get(k))); + assertEquals(152, m.size()); + assertEquals(152, m.keys().size()); + assertEquals(152, m.values().size()); + } + + /** + * Try to test early growth and potential problems when growing. Based on + * infinite loop observations. + */ + @Test + public void growFromSmall_InfiniteLoopIsssue() { + for (int initSize = 0; initSize < 100; initSize++) { + final OrderedFastHashMap m = new OrderedFastHashMap<>(initSize); + + for (int i = 0; i < 300; i++) { + // add one + m.put(i, i); + + // ask for all + for (int j = 0; j <= i; j++) { + assertEquals(Integer.valueOf(j), m.get(j)); + } + + // ask for something else + for (int j = -1; j >= -100; j--) { + assertNull(m.get(j)); + } + } + } + } + + /** + * Try to hit all slots with bad hashcodes. + */ + @Test + public void hitEachSlot() { + final OrderedFastHashMap, Integer> m = new OrderedFastHashMap<>(15); + + final Map, Integer> data = IntStream.range(0, 150).mapToObj(Integer::valueOf) + .collect(Collectors.toMap(i -> new MockKey(i, "k1" + i), i -> i)); + + // add the same hash codes again but other keys + data.putAll(IntStream.range(0, 150).mapToObj(Integer::valueOf) + .collect(Collectors.toMap(i -> new MockKey(i, "k2" + i), i -> i))); + // add all + data.forEach((k, v) -> m.put(k, v)); + // verify + data.forEach((k, v) -> assertEquals(v, m.get(k))); + assertEquals(300, m.size()); + assertEquals(300, m.keys().size()); + assertEquals(300, m.values().size()); + + // remove all + data.forEach((k, v) -> m.remove(k)); + // verify + assertEquals(0, m.size()); + assertEquals(0, m.keys().size()); + assertEquals(0, m.values().size()); + + // add all + final List> keys = data.keySet().stream().collect(Collectors.toList()); + keys.stream().sorted().forEach(k -> m.put(k, data.get(k))); + // put in different order + Collections.shuffle(keys); + keys.forEach(k -> m.put(k, data.get(k) + 42)); + + // verify + data.forEach((k, v) -> assertEquals(Integer.valueOf(v + 42), m.get(k))); + assertEquals(300, m.size()); + assertEquals(300, m.keys().size()); + assertEquals(300, m.values().size()); + + // remove in different order + Collections.shuffle(keys); + keys.forEach(k -> m.remove(k)); + + // verify + data.forEach((k, v) -> assertNull(m.get(k))); + assertEquals(0, m.size()); + assertEquals(0, m.keys().size()); + assertEquals(0, m.values().size()); + } + + @Test + public void reverse_empty() { + // can reverse empty without exception + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.reverse(); + } + + @Test + public void reverse_zero_sized() { + // can reverse 0 sized map + final OrderedFastHashMap m = new OrderedFastHashMap<>(0); + m.reverse(); + } + + @Test + public void reverse_one() { + // reverse one entry map + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("k0", "v0"); + m.reverse(); + assertEquals("k0", m.getEntry(0).getKey()); + assertEquals("v0", m.getEntry(0).getValue()); + } + + @Test + public void reverse_two() { + // reverse two entries map + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("k0", "v0"); + m.put("k1", "v1"); + m.reverse(); + assertEquals("k1", m.getEntry(0).getKey()); + assertEquals("v1", m.getEntry(0).getValue()); + assertEquals("k0", m.getEntry(1).getKey()); + assertEquals("v0", m.getEntry(1).getValue()); + } + + @Test + public void reverse_three() { + // reverse three entries map + final OrderedFastHashMap m = new OrderedFastHashMap<>(); + m.put("k0", "v0"); + m.put("k1", "v1"); + m.put("k2", "v2"); + m.reverse(); + assertEquals("k2", m.getEntry(0).getKey()); + assertEquals("v2", m.getEntry(0).getValue()); + assertEquals("k1", m.getEntry(1).getKey()); + assertEquals("v1", m.getEntry(1).getValue()); + assertEquals("k0", m.getEntry(2).getKey()); + assertEquals("v0", m.getEntry(2).getValue()); + } + + @Test + public void reverse_many_odd() { + // many entries, odd + final OrderedFastHashMap m = new OrderedFastHashMap<>(15); + IntStream.range(0, 117).mapToObj(Integer::valueOf).forEach(i -> m.put(i, i)); + m.reverse(); + + IntStream.range(0, 117).mapToObj(Integer::valueOf).forEach(i -> { + final OrderedFastHashMap.Entry e = m.getEntry(m.size() - i - 1); + assertEquals(i, e.getKey()); + assertEquals(i, e.getValue()); + }); + } + + @Test + public void reverse_many_even() { + // many entries, even + final OrderedFastHashMap m = new OrderedFastHashMap<>(15); + IntStream.range(0, 80).mapToObj(Integer::valueOf).forEach(i -> m.put(i, i)); + m.reverse(); + + IntStream.range(0, 80).mapToObj(Integer::valueOf).forEach(i -> { + final OrderedFastHashMap.Entry e = m.getEntry(m.size() - i - 1); + assertEquals(i, e.getKey()); + assertEquals(i, e.getValue()); + }); + } + + static class MockKey> implements Comparable> { + public final T key_; + public final int hash_; + + MockKey(final int hash, final T key) { + this.hash_ = hash; + this.key_ = key; + } + + @Override + public int hashCode() { + return hash_; + } + + @Override + public boolean equals(final Object o) { + final MockKey t = (MockKey) o; + return hash_ == o.hashCode() && key_.equals(t.key_); + } + + @Override + public String toString() { + return "MockKey [key=" + key_ + ", hash=" + hash_ + "]"; + } + + @Override + public int compareTo(final MockKey o) { + return o.key_.compareTo(this.key_); + } + + } +} diff --git a/src/test/java/org/htmlunit/util/RandomUtils.java b/src/test/java/org/htmlunit/util/RandomUtils.java new file mode 100644 index 00000000000..aae411ae723 --- /dev/null +++ b/src/test/java/org/htmlunit/util/RandomUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2002-2024 Gargoyle Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.htmlunit.util; + +import java.util.Random; + +public final class RandomUtils { + private static final String LOWERCHARS = "abcdefghijklmnopqrstuvwxyz"; + private static final String UPPERCHARS = LOWERCHARS.toUpperCase(); + private static final String CHARS = LOWERCHARS + UPPERCHARS; + + /** + * Private ctor to keep Checkstyle happy + */ + private RandomUtils() { + + } + + /** + * Fixed length random string of lower case and uppercase chars. + * + * @param random the random number generator + * @param length the length of the resulting string + * @return a random string of the desired length + */ + public static String randomString(final Random random, final int length) { + return randomString(random, length, length); + } + + /** + * Variable length random string of lower case and upper case chars. + * + * @param random the random generator + * @param from min length + * @param to max length (inclusive) + * @return a random string + */ + public static String randomString(final Random random, final int from, final int to) { + final int length = random.nextInt(to - from + 1) + from; + + final StringBuilder sb = new StringBuilder(to); + + for (int i = 0; i < length; i++) { + final int pos = random.nextInt(CHARS.length()); + sb.append(CHARS.charAt(pos)); + } + + return sb.toString(); + } +}