StyleIndex.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2024, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML 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 for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*
*/
package net.sourceforge.plantuml.style;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.sourceforge.plantuml.style.parser2.StyleAtomTrie;
import net.sourceforge.plantuml.style.parser2.StyleQuery;
/**
* The fast, queryable counterpart of the old {@code StyleStorage}: every loaded {@link Style}
* is indexed by its query's tags in a
* {@link StyleAtomTrie}, so {@link #findMatching(StyleQuery)} only ever visits
* declarations that could possibly match a query, instead of the old linear scan over every
* style ever loaded checking {@code StyleSignatureBasic#matchAll} one by one -- the actual fix
* for the slow style resolution ({@link StyleBuilder#getMergedStyle},
* {@link StyleBuilder#getMergedStyleSpecial}) this whole {@code parser2} engine was built for.
* The subset-query trie itself already replicates {@code matchAll}'s semantics exactly (level,
* star, {@code SName} set and stereotype set all included) -- see
* {@code net.sourceforge.plantuml.style.parser2.LevelConstraint} and
* {@code net.sourceforge.plantuml.style.parser2.StyleAtom}.
*
* Immutable: {@link #withLoaded(Style)} and {@link #withMuted(Collection)} both return a new
* index, leaving this one exactly as queryable as before, so that a {@link StyleBuilder} cached
* and reused across diagrams (see {@code StyleLoader#loadSkin}) never has a later diagram's own
* {@code <style>} override leak back into the shared, cached instance it was cloned from.
*/
public final class StyleIndex {
private static final StyleIndex EMPTY = new StyleIndex(Collections.<Style> emptyList());
private final List<Style> allStyles;
// Lazily (re)built from allStyles on first query after a with...() call produced this
// index, then reused for every later query -- a style sheet is loaded once but queried
// once per diagram element, so this amortizes the (cheap, a few hundred entries at most)
// build cost across every query this index will ever answer. Racing this on two threads
// only risks the harmless case of building it twice, since every build from the same
// (immutable) allStyles produces an equally valid trie.
private volatile StyleAtomTrie<Style> trieCache;
// Memoizes getMergedStyle by query, exactly like the old StyleBuilder#mergedStyleCache
// used to before this index existed: getMergedStyle is called from some 300 sites across
// the diagram packages, essentially once per rendered element, but any given element kind
// (e.g. "root document sequenceDiagram message arrow") shares the same StyleQuery across
// every one of its occurrences in a diagram -- so without this, resolving it (a trie walk
// down findMatching plus the Style#mergeWith cascade over whatever it finds) is redone
// from scratch for every single occurrence instead of once. Safe to key by StyleQuery
// as-is (it is immutable with proper equals/hashCode) and safe to keep for this index's
// whole lifetime (this index itself is immutable -- withLoaded/withMuted always return a
// new one -- so there is no later mutation this cache could ever go stale against).
private final Map<StyleQuery, Style> mergedStyleCache = new ConcurrentHashMap<StyleQuery, Style>();
private StyleIndex(List<Style> allStyles) {
this.allStyles = allStyles;
}
public static StyleIndex empty() {
return EMPTY;
}
/**
* The counterpart of the old {@code StyleBuilder#loadInternal}'s
* {@code storage.get(signature)}/{@code storage.put(...)} pair, called while loading a .skin
* file: a light-only "root { FontColor black }" and a later, dark-only
* "@media (dark) { root { FontColor white } }" share the exact same {@link StyleQuery}
* and MUST be folded into one {@link Style} right here, at load time -- not left for
* {@link #findMatching} to hand back as two separate entries. Deferring that fold to query
* time is unsound: a cross-selector cascade merge (say, "root" combined with a more specific
* "root document ganttDiagram") uses priority alone to decide the winner once both sides
* already carry a value ({@link DarkString#mergeWith}'s final, non-combining branch), so a
* still-unmerged dark-only "root" entry -- whose priority is always high, since every dark
* declaration in a .skin file is numbered after every light one -- can beat an already-combined
* light+dark value from a less specific selector and silently drop that combined value's light
* half. Pre-merging same-signature entries here, exactly as the old code did, keeps every
* entry this index ever hands out fully self-consistent (both halves set whenever either
* declaration set either), so that later cascade folding only ever has to pick a winner
* between two complete values.
*/
public StyleIndex withLoaded(Style newStyle) {
final List<Style> updated = new ArrayList<Style>(allStyles.size() + 1);
updated.addAll(allStyles);
mergeOrAppend(updated, newStyle);
return new StyleIndex(Collections.unmodifiableList(updated));
}
/**
* The counterpart of the old {@code StyleBuilder#muteStyle}: folds every style in
* {@code newStyles} into a brand new index, this one left untouched -- merging each one, in
* turn, with whatever already shares its exact signature (either already in this index, or
* earlier in {@code newStyles} itself), for the same reason {@link #withLoaded} does.
*/
public StyleIndex withMuted(Collection<Style> newStyles) {
if (newStyles.isEmpty())
return this;
final List<Style> updated = new ArrayList<Style>(allStyles.size() + newStyles.size());
updated.addAll(allStyles);
for (Style modifiedStyle : newStyles)
mergeOrAppend(updated, modifiedStyle);
return new StyleIndex(Collections.unmodifiableList(updated));
}
/**
* Folds {@code newStyle} into whichever element of {@code list} already carries the exact same
* signature, in place (so the merged entry keeps its original position, exactly like
* {@code Map#put} on an existing key leaves a {@code LinkedHashMap}'s iteration order alone) --
* or appends it as a new entry when no such element exists yet.
*/
private static void mergeOrAppend(List<Style> list, Style newStyle) {
final StyleQuery signature = newStyle.getQuery();
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getQuery().equals(signature)) {
list.set(i, list.get(i).mergeWith(newStyle, MergeStrategy.OVERWRITE_EXISTING_VALUE));
return;
}
}
list.add(newStyle);
}
private StyleAtomTrie<Style> trie() {
StyleAtomTrie<Style> result = trieCache;
if (result == null) {
result = new StyleAtomTrie<Style>();
for (Style style : allStyles) {
final StyleQuery asDeclaration = style.getQuery();
result.insert(asDeclaration.getAtoms(), asDeclaration.getLevelConstraint(), style);
}
trieCache = result;
}
return result;
}
/**
* Every loaded style whose query is a subset of {@code query}'s, in the order they were
* loaded or muted in.
*/
public List<Style> findMatching(StyleQuery query) {
return trie().findMatching(query);
}
Style getMergedStyle(StyleQuery query) {
final Style cached = mergedStyleCache.get(query);
if (cached != null)
return cached;
// Not computeIfAbsent: computeMergedStyle legitimately returns null when nothing
// matches (see below), and computeIfAbsent never records a null result, so a
// no-match query would otherwise be recomputed on every single call forever. A
// plain get/put pair costs one extra (harmless, idempotent) recomputation on a
// race between two callers instead -- still infinitely better than never caching
// a hit at all.
final Style computed = computeMergedStyle(query);
if (computed != null)
mergedStyleCache.put(query, computed);
return computed;
}
private Style computeMergedStyle(StyleQuery query) {
Style mergedStyle = null;
for (Style style : findMatching(query)) {
if (mergedStyle == null)
mergedStyle = style;
else
mergedStyle = mergedStyle.mergeWith(style, MergeStrategy.OVERWRITE_EXISTING_VALUE);
}
return mergedStyle;
}
/** Every style ever loaded or muted into this index, in that same order. */
public List<Style> getAllStyles() {
return allStyles;
}
}