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.parser2;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import net.sourceforge.plantuml.stereo.Stereogroup;
import net.sourceforge.plantuml.stereo.Stereostyles;
import net.sourceforge.plantuml.stereo.Stereotype;
import net.sourceforge.plantuml.style.SName;
import net.sourceforge.plantuml.style.Style;
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 SortedSet<StyleAtom> atoms;
	private final LevelConstraint levelConstraint;

	private StyleQuery(SortedSet<StyleAtom> atoms, LevelConstraint levelConstraint) {
		this.atoms = atoms;
		this.levelConstraint = levelConstraint;
	}
	
	public static StyleQuery activityArrow() {
		return StyleQuery.of(Arrays.asList(SName.root, SName.element, SName.activityDiagram, SName.activity, SName.arrow));
	}

	public static StyleQuery of(Collection<SName> names, Collection<String> stereotypes,
			LevelConstraint levelConstraint) {
		final SortedSet<StyleAtom> atoms = new TreeSet<StyleAtom>();
		for (SName name : names)
			atoms.add(StyleAtom.of(name));
		for (String stereotype : stereotypes)
			atoms.add(StyleAtom.ofStereotype(stereotype));
		return new StyleQuery(atoms, levelConstraint);
	}

	public static StyleQuery of(Collection<SName> names, Collection<String> stereotypes) {
		return of(names, stereotypes, LevelConstraint.none());
	}

	public static StyleQuery of(Collection<SName> names) {
		return of(names, Collections.<String>emptySet(), LevelConstraint.none());
	}

	/** No tag, no depth constraint -- mirroring {@code StyleSignature.empty()}. */
	public static StyleQuery empty() {
		return of(Collections.<SName>emptySet());
	}

	/**
	 * 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) {
		final SortedSet<StyleAtom> result = new TreeSet<StyleAtom>(atoms);
		result.add(StyleAtom.ofStereotype(stereotype));
		return new StyleQuery(result, levelConstraint);
	}

	public StyleQuery withTOBECHANGED(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 stereogroup}'s own stereotype's
	 * labels at once, the same way {@link #withTOBECHANGED(Stereotype)} does -- mirroring
	 * {@code StyleSignature.withTOBECHANGED(Stereogroup)}.
	 */
	public StyleQuery withTOBECHANGED(Stereogroup stereogroup) {
		if (stereogroup == null)
			return this;

		return withTOBECHANGED(stereogroup.buildStereotype());
	}

	/**
	 * This same query, additionally requiring every one of {@code stereo}'s labels at once, the
	 * same way {@link #withTOBECHANGED(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 SortedSet<StyleAtom> withStereotypeTag = new TreeSet<StyleAtom>(atoms);
		withStereotypeTag.add(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 with(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));
	}

	/**
	 * This same query, additionally requiring {@code name} -- mirroring
	 * {@code StyleSignature.addSName(SName)}.
	 */
	public StyleQuery addSName(SName name) {
		final SortedSet<StyleAtom> result = new TreeSet<StyleAtom>(atoms);
		result.add(StyleAtom.of(name));
		return new StyleQuery(result, levelConstraint);
	}

	public StyleQuery addClickable(Url url) {
		if (url == null)
			return this;

		final SortedSet<StyleAtom> result = new TreeSet<StyleAtom>(atoms);
		result.add(StyleAtom.of(SName.clickable));
		return new StyleQuery(result, 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}.
	 */
	public StyleQuery mergeWith(StyleQuery other) {
		final SortedSet<StyleAtom> result = new TreeSet<StyleAtom>(atoms);
		result.addAll(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;
	}

	public SortedSet<StyleAtom> getAtoms() {
		return atoms;
	}

	public LevelConstraint getLevelConstraint() {
		return levelConstraint;
	}

	/**
	 * Every stereotype atom this query carries, in ascending order -- mirroring
	 * {@code StyleSignature.getStereotypes()}.
	 */
	public 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;
	}

}