diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/css/BitSet.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/css/BitSet.java index e3abb11625a..ea053cf7fc3 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/css/BitSet.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/css/BitSet.java @@ -181,10 +181,16 @@ public boolean remove(Object o) { return false; } - T t = cast(o); + Class elementType = getElementType(); - final int element = getIndex(t) / Long.SIZE; - final long bit = 1l << (getIndex(t) % Long.SIZE); + if (!elementType.isInstance(o)) { // if cast failed, it can't be part of this set, so not modified + return false; + } + + T t = elementType.cast(o); + int index = getIndex(t); + int element = index / Long.SIZE; + long bit = 1l << (index % Long.SIZE); if (element >= bits.length) { // not in this Set! @@ -219,18 +225,22 @@ public boolean contains(Object o) { return false; } - final T t = cast(o); + Class elementType = getElementType(); - final int element = getIndex(t) / Long.SIZE; - final long bit = 1l << (getIndex(t) % Long.SIZE); + if (!elementType.isInstance(o)) { + return false; + } + + int index = getIndex(elementType.cast(o)); + int element = index / Long.SIZE; + long bit = 1L << (index % Long.SIZE); return (element < bits.length) && (bits[element] & bit) == bit; } - /** {@inheritDoc} */ @Override public boolean containsAll(Collection c) { - if (this.getClass() != c.getClass()) { + if (this.getClass() != c.getClass()) { // implicit null check here is intended for (Object obj : c) { if (!contains(obj)) { return false; @@ -259,11 +269,9 @@ public boolean containsAll(Collection c) { return true; } - - /** {@inheritDoc} */ @Override public boolean addAll(Collection c) { - if (this.getClass() != c.getClass()) { + if (this.getClass() != c.getClass()) { // implicit null check here is intended boolean modified = false; for (T obj : c) { @@ -336,10 +344,9 @@ public boolean addAll(Collection c) { } - /** {@inheritDoc} */ @Override public boolean retainAll(Collection c) { - if (this.getClass() != c.getClass()) { + if (this.getClass() != c.getClass()) { // implicit null check here is intended boolean modified = false; for (Iterator iterator = this.iterator(); iterator.hasNext();) { @@ -426,11 +433,9 @@ public boolean retainAll(Collection c) { return modified; } - - /** {@inheritDoc} */ @Override public boolean removeAll(Collection c) { - if (this.getClass() != c.getClass()) { + if (this.getClass() != c.getClass()) { // implicit null check here is intended boolean modified = false; for (Object obj : c) { @@ -495,7 +500,6 @@ public boolean removeAll(Collection c) { return modified; } - /** {@inheritDoc} */ @Override public void clear() { @@ -539,31 +543,31 @@ public boolean equals(Object obj) { private boolean equalsBitSet(BitSet other) { int a = this.bits != null ? this.bits.length : 0; int b = other.bits != null ? other.bits.length : 0; + int max = Math.max(a, b); - if (a != b) return false; + for (int i = 0; i < max; i++) { + long m0 = i >= a ? 0 : this.bits[i]; + long m1 = i >= b ? 0 : other.bits[i]; - for(int m=0; m getElementType(); - protected long[] getBits() { + long[] getBits() { return bits; } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/css/ImmutablePseudoClassSetsCache.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/css/ImmutablePseudoClassSetsCache.java new file mode 100644 index 00000000000..3e2e4ae19f1 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/css/ImmutablePseudoClassSetsCache.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.javafx.css; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import javafx.css.PseudoClass; + +/** + * A cache for immutable sets of {@link PseudoClass}es. + */ +public class ImmutablePseudoClassSetsCache { + private static final Map, Set> CACHE = new HashMap<>(); + + /** + * Returns an immutable set of {@link PseudoClass}es. + *

+ * Note: this method may or may not return the same instance for the same set of + * {@link PseudoClass}es. + * + * @param pseudoClasses a set of {@link PseudoClass} to make immutable, cannot be {@code null} + * @return an immutable set of {@link PseudoClass}es, never {@code null} + * @throws NullPointerException when {@code pseudoClasses} is {@code null} or contains {@code null}s + */ + public static Set of(Set pseudoClasses) { + Set cachedSet = CACHE.get(Objects.requireNonNull(pseudoClasses, "pseudoClasses cannot be null")); + + if (cachedSet != null) { + return cachedSet; + } + + Set copy = Set.copyOf(pseudoClasses); + + CACHE.put(copy, copy); + + return copy; + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/css/PseudoClassState.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/css/PseudoClassState.java index 886d2af66d8..a18e1198ea5 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/css/PseudoClassState.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/css/PseudoClassState.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -54,35 +54,6 @@ public PseudoClassState() { } } - /** {@inheritDoc} */ - @Override - public Object[] toArray() { - return toArray(new PseudoClass[size()]); - } - - /** {@inheritDoc} */ - @Override - public T[] toArray(T[] a) { - if (a.length < size()) { - a = (T[]) new PseudoClass[size()]; - } - int index = 0; - while(index < getBits().length) { - final long state = getBits()[index]; - for(int bit=0; bit strings = new ArrayList<>(); @@ -94,12 +65,8 @@ public String toString() { } @Override - protected PseudoClass cast(Object o) { - if (o == null) { - throw new NullPointerException("null arg"); - } - PseudoClass pseudoClass = (PseudoClass) o; - return pseudoClass; + protected Class getElementType() { + return PseudoClass.class; } @Override diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/css/StyleCacheEntry.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/css/StyleCacheEntry.java index 4568cd03bda..519f7e3a688 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/css/StyleCacheEntry.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/css/StyleCacheEntry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -68,8 +68,7 @@ public Key(Set[] pseudoClassStates, Font font) { this.pseudoClassStates = new Set[pseudoClassStates.length]; for (int n=0; n T[] toArray(T[] a) { - if (a.length < size()) { - a = (T[]) new StyleClass[size()]; - } - int index = 0; - while(index < getBits().length) { - final long state = getBits()[index]; - for(int bit=0; bit getElementType() { + return StyleClass.class; } @Override diff --git a/modules/javafx.graphics/src/main/java/javafx/css/Match.java b/modules/javafx.graphics/src/main/java/javafx/css/Match.java index 5ee14a361c5..887d00b4363 100644 --- a/modules/javafx.graphics/src/main/java/javafx/css/Match.java +++ b/modules/javafx.graphics/src/main/java/javafx/css/Match.java @@ -27,7 +27,8 @@ import static javafx.geometry.NodeOrientation.INHERIT; -import java.util.Collections; +import com.sun.javafx.css.ImmutablePseudoClassSetsCache; + import java.util.Objects; import java.util.Set; @@ -58,7 +59,7 @@ public final class Match implements Comparable { this.selector = selector; this.idCount = idCount; this.styleClassCount = styleClassCount; - this.pseudoClasses = Collections.unmodifiableSet(pseudoClasses); + this.pseudoClasses = ImmutablePseudoClassSetsCache.of(pseudoClasses); int nPseudoClasses = pseudoClasses.size(); if (selector instanceof SimpleSelector simple) { if (simple.getNodeOrientation() != INHERIT) { diff --git a/modules/javafx.graphics/src/main/java/javafx/css/SimpleSelector.java b/modules/javafx.graphics/src/main/java/javafx/css/SimpleSelector.java index 6310252feb5..7c770e859b1 100644 --- a/modules/javafx.graphics/src/main/java/javafx/css/SimpleSelector.java +++ b/modules/javafx.graphics/src/main/java/javafx/css/SimpleSelector.java @@ -37,6 +37,7 @@ import java.util.List; import java.util.Set; +import com.sun.javafx.css.ImmutablePseudoClassSetsCache; import com.sun.javafx.css.PseudoClassState; import com.sun.javafx.css.StyleClassSet; @@ -111,6 +112,7 @@ public String getId() { // a mask of bits corresponding to the pseudoclasses (immutable) private final Set pseudoClassState; + // for test purposes Set getPseudoClassStates() { return pseudoClassState; } @@ -181,7 +183,7 @@ public NodeOrientation getNodeOrientation() { } } - this.pseudoClassState = Collections.unmodifiableSet(pcs); + this.pseudoClassState = ImmutablePseudoClassSetsCache.of(pcs); this.nodeOrientation = dir; this.id = id == null ? "" : id; // if id is not null and not empty, then match needs to check id diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/CssStyleHelper.java b/modules/javafx.graphics/src/main/java/javafx/scene/CssStyleHelper.java index b2ef3dc8373..8ffc39a1c2e 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/CssStyleHelper.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/CssStyleHelper.java @@ -39,6 +39,8 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.WritableValue; import com.sun.javafx.css.CascadingStyle; +import com.sun.javafx.css.ImmutablePseudoClassSetsCache; + import javafx.css.CssMetaData; import javafx.css.CssParser; import javafx.css.FontCssMetaData; @@ -80,7 +82,6 @@ final class CssStyleHelper { private static final PlatformLogger LOGGER = com.sun.javafx.util.Logging.getCSSLogger(); private CssStyleHelper() { - this.triggerStates = new PseudoClassState(); } /** @@ -514,7 +515,7 @@ private StyleMap getStyleMap(Styleable styleable) { * * * Called "triggerStates" since they would trigger a CSS update. */ - private PseudoClassState triggerStates = new PseudoClassState(); + private final PseudoClassState triggerStates = new PseudoClassState(); boolean pseudoClassStateChanged(PseudoClass pseudoClass) { return triggerStates.contains(pseudoClass); @@ -553,7 +554,7 @@ private Set[] getTransitionStates(final Node node) { // .foo:hover { -fx-fill: red; } then only the hover state matters // but the transtion state could be [hover, focused] // - final Set[] retainedStates = new PseudoClassState[depth]; + final Set[] retainedStates = new Set[depth]; // // Note Well: The array runs from leaf to root. That is, @@ -564,20 +565,25 @@ private Set[] getTransitionStates(final Node node) { int count = 0; parent = node; + while (parent != null) { // This loop traverses through all ancestors till root - final CssStyleHelper helper = (parent instanceof Node) ? parent.styleHelper : null; - if (helper != null) { - final Set pseudoClassState = parent.pseudoClassStates; - retainedStates[count] = new PseudoClassState(); - retainedStates[count].addAll(pseudoClassState); - // retainAll method takes the intersection of pseudoClassState and helper.triggerStates - retainedStates[count].retainAll(helper.triggerStates); - count += 1; + if (parent.styleHelper != null) { + PseudoClassState pseudoClassState = new PseudoClassState(); + + pseudoClassState.addAll(parent.pseudoClassStates); + pseudoClassState.retainAll(parent.styleHelper.triggerStates); + + retainedStates[count++] = ImmutablePseudoClassSetsCache.of(pseudoClassState); } + parent = parent.getParent(); } - final Set[] transitionStates = new PseudoClassState[count]; + if (count == depth) { + return retainedStates; + } + + final Set[] transitionStates = new Set[count]; System.arraycopy(retainedStates, 0, transitionStates, 0, count); return transitionStates; diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/css/BitSetTest.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/css/BitSetTest.java index 292aa4fe45a..b83323feeb3 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/css/BitSetTest.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/css/BitSetTest.java @@ -161,6 +161,31 @@ void twoNonEmptyBitSetsWithSamePatternAndSizeShouldNotBeConsideredEqualsWhenElem assertNotEquals(set1, set2); } + @Test + void shouldBeEqualAfterGrowAndShrink() { + StyleClassSet set1 = new StyleClassSet(); + StyleClassSet set2 = new StyleClassSet(); + + set1.add(StyleClassSet.getStyleClass("abc")); + set2.add(StyleClassSet.getStyleClass("abc")); + + assertEquals(set1, set2); + + for (int i = 0; i < 1000; i++) { + // grow internal bit set array: + set1.add(StyleClassSet.getStyleClass("" + i)); + + assertNotEquals(set1, set2); + } + + for (int i = 0; i < 1000; i++) { + set1.remove(StyleClassSet.getStyleClass("" + i)); + } + + // still equal despite internal array sizes being different size: + assertEquals(set1, set2); + } + @Test void twoEmptyBitSetsShouldBeEqual() { diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/css/ImmutablePseudoClassSetsCacheTest.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/css/ImmutablePseudoClassSetsCacheTest.java new file mode 100644 index 00000000000..c141aab00ed --- /dev/null +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/css/ImmutablePseudoClassSetsCacheTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package test.com.sun.javafx.css; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.sun.javafx.css.ImmutablePseudoClassSetsCache; +import com.sun.javafx.css.PseudoClassState; + +import javafx.css.PseudoClass; + +public class ImmutablePseudoClassSetsCacheTest { + + @Test + void shouldCacheSets() { + Set myOwnSet = Set.of(PseudoClass.getPseudoClass("a")); + Set pseudoClassState = new PseudoClassState(); + + pseudoClassState.add(PseudoClass.getPseudoClass("a")); + + Set set1 = ImmutablePseudoClassSetsCache.of(new HashSet<>(Set.of(PseudoClass.getPseudoClass("a")))); + Set set2 = ImmutablePseudoClassSetsCache.of(new HashSet<>(myOwnSet)); + Set set3 = ImmutablePseudoClassSetsCache.of(myOwnSet); + Set set4 = ImmutablePseudoClassSetsCache.of(pseudoClassState); + Set set5 = ImmutablePseudoClassSetsCache.of(Set.of(PseudoClass.getPseudoClass("b"))); + + assertEquals(set1, set2); + assertEquals(set2, set3); + assertEquals(set3, set4); + assertNotEquals(set1, set5); + assertNotEquals(set2, set5); + assertNotEquals(set3, set5); + assertNotEquals(set4, set5); + + assertSame(set1, set2); + assertSame(set2, set3); + assertSame(set3, set4); + + assertEquals(myOwnSet, set1); + + // this does not need to be true if this set was not the first one cached + assertNotSame(myOwnSet, set1); + + // tests if hashCode/equals of BitSet respects contract... + assertEquals(myOwnSet.hashCode(), pseudoClassState.hashCode()); + assertEquals(myOwnSet, pseudoClassState); + assertEquals(pseudoClassState, myOwnSet); + } +}