StyleQuery.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.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import net.sourceforge.plantuml.stereo.Stereostyles;
import net.sourceforge.plantuml.stereo.Stereotype;
import net.sourceforge.plantuml.text.Guillemet;
import net.sourceforge.plantuml.url.Url;
/**
* What a {@link StyleAtomTrie} is asked to resolve: the element's tags (its
* {@link SName} set plus any stereotypes, e.g. what the legacy
* {@code StyleSignatureBasic} calls {@code snames}/{@code stereotypes}) and its
* {@link LevelConstraint} (its mindmap/wbs depth and whether this is an
* ancestor-inheritance lookup -- see {@link LevelConstraint}).
*/
public final class StyleQuery {
private final AtomArray atoms;
private final LevelConstraint levelConstraint;
private StyleQuery(AtomArray atoms, LevelConstraint levelConstraint) {
this.atoms = atoms;
this.levelConstraint = levelConstraint;
}
public static StyleQuery of3(SName... names) {
AtomArray atoms = AtomArray.empty();
for (SName name : names)
atoms = atoms.plus(StyleAtom.of(name));
return new StyleQuery(atoms, LevelConstraint.none());
}
/** No tag, no depth constraint -- mirroring {@code StyleSignature.empty()}. */
public static StyleQuery empty() {
return of3();
}
/**
* This same query, additionally requiring {@code stereotype} -- e.g. so a
* "frequent use" factory that starts from a plain, stereotype-less query (the
* common case, since most such factories are called with no stereotype in hand
* yet) can still fold one in once the caller has it, the way
* {@code StyleSignature.addStereotype} does for the legacy signature.
* {@code stereotype} is cleaned exactly as {@link StyleAtom#ofStereotype}
* cleans it (see there for why), so passing it in raw, un-lower-cased text is
* fine. Requiring several stereotypes at once (an element tagged with more than
* one, e.g. {@code <<foo>><<bar>>}) is just calling this again on the result,
* since a {@link TreeSet} silently dedupes a stereotype already present.
*/
public StyleQuery withStereotype(String stereotype) {
return new StyleQuery(atoms.plus(StyleAtom.ofStereotype(stereotype)), levelConstraint);
}
public StyleQuery withStereotype(Stereotype stereotype) {
if (stereotype == null || stereotype.getStyleNames().size() == 0)
return this;
final List<String> labels = stereotype.getLabels(Guillemet.NONE);
if (labels.size() == 0)
return this;
StyleQuery result = this;
for (String s : labels)
result = result.withStereotype(s);
return result;
}
/**
* This same query, additionally requiring every one of {@code stereo}'s labels
* at once, the same way {@link #withStereotype(Stereotype)} does -- but also
* requiring the special {@link SName#stereotype} tag, mirroring
* {@code StyleSignature.forStereotypeItself}: used to resolve the
* CSS-class-like rule for the stereotype itself (e.g. {@code <<foo>>}'s own
* {@code .foo { ... }} declaration), as opposed to a plain element carrying
* that stereotype.
*/
public StyleQuery forStereotypeItself(Stereotype stereo) {
if (stereo == null || stereo.getStyleNames().size() == 0)
return this;
final List<String> labels = stereo.getLabels(Guillemet.NONE);
if (labels.size() == 0)
return this;
final AtomArray withStereotypeTag = atoms.plus(StyleAtom.of(SName.stereotype));
StyleQuery result = new StyleQuery(withStereotypeTag, levelConstraint);
for (String name : labels)
result = result.withStereotype(name);
return result;
}
/**
* This same query, additionally requiring every stereo-style name in
* {@code stereostyles} -- mirroring {@code StyleSignature.with(Stereostyles)}.
*/
public StyleQuery withStereostyles(Stereostyles stereostyles) {
if (stereostyles.isEmpty())
return this;
StyleQuery result = this;
for (String name : stereostyles.getStyleNames())
result = result.withStereotype(name);
return result;
}
/**
* This same query, additionally constrained to depth {@code level} (an exact
* match unless the query was already starred) -- mirroring
* {@code StyleSignature.addLevel(int)}.
*/
public StyleQuery addLevel(int level) {
return new StyleQuery(atoms, LevelConstraint.of(level, levelConstraint.isStar()));
}
/**
* This same query, additionally starred (an ancestor-inheritance lookup,
* matching only declarations that are themselves starred) while keeping
* whatever depth was already set -- mirroring {@code StyleSignature.addStar()}
* / {@code StyleKey.addStar()}.
*/
public StyleQuery addStar() {
return new StyleQuery(atoms, LevelConstraint.of(levelConstraint.getLevel(), true));
}
public StyleQuery add(SName name) {
return new StyleQuery(atoms.plus(StyleAtom.of(name)), levelConstraint);
}
public StyleQuery add(SName name1, SName name2) {
return new StyleQuery(atoms.plus(StyleAtom.of(name1), StyleAtom.of(name2)), levelConstraint);
}
public StyleQuery add(SName name1, SName name2, SName name3) {
return new StyleQuery(atoms.plus(StyleAtom.of(name1), StyleAtom.of(name2), StyleAtom.of(name3)),
levelConstraint);
}
public StyleQuery addSNames(SName... names) {
AtomArray result = atoms;
for (SName name : names)
result = result.plus(StyleAtom.of(name));
return new StyleQuery(result, levelConstraint);
}
public StyleQuery addClickable(Url url) {
if (url == null)
return this;
return new StyleQuery(atoms.plus(StyleAtom.of(SName.clickable)), levelConstraint);
}
/**
* This query, unioned with {@code other}: every atom either carries, plus the
* least restrictive combination of their two {@link LevelConstraint}s (deepest
* level, starred if either side is) -- mirroring {@code StyleKey.mergeWith}.
*/
StyleQuery mergeWith(StyleQuery other) {
final AtomArray result = atoms.plusAll(other.atoms);
final int mergedLevel = Math.max(levelConstraint.getLevel(), other.levelConstraint.getLevel());
final boolean mergedStar = levelConstraint.isStar() || other.levelConstraint.isStar();
return new StyleQuery(result, LevelConstraint.of(mergedLevel, mergedStar));
}
/**
* This query, unioned in turn with each of {@code others}'s own signature --
* mirroring {@code StyleSignature.mergeWith(List<Style>)}.
*/
public StyleQuery mergeWith(List<Style> others) {
StyleQuery result = this;
for (Style other : others)
result = result.mergeWith(other.getQuery());
return result;
}
/**
* Whether this query requires at least one stereotype -- that is, whether any part of it comes
* from the diagram's own text rather than from the finite set of {@link SName} tags the code
* itself can name. See {@code StyleIndex}, which caches the two families separately for
* exactly that reason.
*/
public boolean hasStereotype() {
return atoms.hasStereotype();
}
AtomArray getAtoms() {
return atoms;
}
LevelConstraint getLevelConstraint() {
return levelConstraint;
}
/**
* Every stereotype atom this query carries, in ascending order -- mirroring
* {@code StyleSignature.getStereotypes()}.
*/
Set<String> getStereotypes() {
final Set<String> result = new LinkedHashSet<String>();
for (StyleAtom atom : atoms)
if (atom.isName() == false)
result.add(atom.getStereotype());
return result;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof StyleQuery == false)
return false;
final StyleQuery other = (StyleQuery) obj;
return this.atoms.equals(other.atoms) && this.levelConstraint.equals(other.levelConstraint);
}
@Override
public int hashCode() {
return atoms.hashCode() * 31 + levelConstraint.hashCode();
}
@Override
public String toString() {
return atoms + " " + levelConstraint;
}
}