
com.streamsets.systemexplorer.api.ExplorerSchema Maven / Gradle / Ivy
/*
* Copyright 2022 StreamSets 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
*
* http://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 com.streamsets.systemexplorer.api;
import com.streamsets.pipeline.api.impl.Utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
*
* Class that defines the metadata explorer schema for a stage.
*
*
* A sub-class defining a metadata explorer schema must be set the stage {@code @StageDef}
* for stages supporting schema definition.
*
*
* - Schema element names must be a single word.
* - Schema element names must be unique.
* - Schema elements may have multiple types (via the {@code also()} method).
*
*
* Explorer schemas are immutable.
*
* Example of Explorer Schema for Snowflake:
* {@code
* public class SnowflakeExplorerSchema extends ExplorerSchema {
* public SnowflakeExplorerSchema() {
* super(
* new Atom("warehouse", "warehouse.svg"),
* new Atom("role", "role.svg"),
* new Set(
* "database",
* "database,svg",
* new Set(
* "schema",
* "schema.svg",
* new Set("table", "table.svg", new Atom("column", "column.svg")).also("view", "view.svg"),
* )
* )
* );
* }
* }
* }
*
* For searching, when searching 'table' schema elements it may also return 'view' elements in the same
* children set.
*
*/
public class ExplorerSchema {
/**
*
* Empty schema.
*
*/
public static final class None extends ExplorerSchema {
public static None NONE = new None();
private None() {}
}
private static final Pattern WORD_PATTERN = Pattern.compile("\\w+");
/**
*
* Base schema element class.
*
*/
public static abstract class Element {
public static final String NAME_ATTR = "name";
private final String name;
private final String icon;
private final boolean cacheable;
private final Map additionalNamesIcons;
private final List attributes;
protected Element(
boolean root, String name, String icon, boolean cacheable,
Map additionalNamesIcons,
List attributes
) {
Utils.checkNotNull(attributes, "attributes");
Utils.checkArgument(!attributes.isEmpty(), "attributes");
Utils.checkArgument(
attributes.stream().anyMatch(a -> a.name.equals(NAME_ATTR)),
String.format("%s is required", NAME_ATTR)
);
Utils.checkArgument(
new HashSet<>(attributes).size() == attributes.size(),
String.format(
"Attribute names should be unique: %s",
attributes.stream().map(a -> a.name).collect(Collectors.joining(","))
)
);
this.name = (root) ? name : checkName(name);
this.icon = icon;
this.cacheable = cacheable;
this.additionalNamesIcons = Collections.unmodifiableMap(additionalNamesIcons.entrySet().stream().map(
e -> new String[]{checkName(e.getKey()), e.getValue()}
).collect(Collectors.toMap(a -> a[0], a -> a[1])));
this.attributes = Collections.unmodifiableList(attributes);
}
String checkName(String name) {
Utils.checkArgument(WORD_PATTERN.matcher(name).matches(),
Utils.format("ExplorerSchema '{}' invalid schema element name '{}', must be a word",
getClass().getSimpleName(),
name
));
return name;
}
/**
* Returns names of the supported element attributes.
* It's never null nor empty, and it's first element is always ExplorerSchema.Element.NAME_ATTR
*
* @return list of attribute names.
*/
public List getAttributes() {
return attributes;
}
public boolean isCacheable() {
return cacheable;
}
/**
*
* Returns the element name.
*
*/
public String getName() {
return name;
}
/**
*
* Returns the element icon.
*
*/
public String getIcon() {
return icon;
}
/**
*
* Returns the additional (name, icon) pairs associated with this element.
*
*/
public Map getAdditionalNamesIcons() {
return additionalNamesIcons;
}
/**
*
* If this element is an {@code Atom} it returns an {@code Optional} with it,
* else it returns an empty {@code Optional}.
*
*/
public Optional asAtom() {
return Optional.ofNullable((this instanceof Atom) ? (Atom) this : null);
}
/**
*
* If this element is an {@code Set} it returns an {@code Optional} with it,
* else it returns an empty {@code Optional}.
*
*/
public Optional asSet() {
return Optional.ofNullable((this instanceof Set) ? (Set) this : null);
}
}
public static class Attribute {
private final String name;
private final String label;
public Attribute(final String name, final String label) {
Utils.checkArgument(name != null, "name");
Utils.checkArgument(label != null, "label");
this.name = name;
this.label = label;
}
public String getName() {
return name;
}
public String getLabel() {
return label;
}
}
/**
*
* A leaf schema element.
*
*/
public static class Atom extends Element {
/**
*
* Creates an Atom schema element with no icon.
*
*/
private Atom(String name, String icon, boolean cacheable, Map additionalNames, List attributeNames) {
super(false, name, Utils.checkNotNull(icon, "icon"), cacheable, additionalNames, attributeNames);
}
@Override
public String toString() {
return "Atom{" + "name='" + getName() + '\'' + ", icon='" + getIcon() + '\'' + '}';
}
}
/**
*
* A schema element that has one or more children schema element.
*
*/
public static class Set extends Element {
private final List elements;
private final boolean recursive;
private Set(boolean root, String name, String icon, boolean cacheable, boolean recursive, List elements, Map additionaNamesIcons, List attributes) {
super(root, name, Utils.checkNotNull(icon, "icon"), cacheable, additionaNamesIcons, attributes);
Utils.checkNotNull(elements, "elements");
List list = new ArrayList<>();
if (recursive) {
Utils.checkArgument(elements.size() == 0, "elements");
} else {
list.addAll(elements);
}
this.elements = Collections.unmodifiableList(list);
this.recursive = recursive;
}
/**
*
* Returns the children schema elements.
*
*/
public List getElements() {
return elements;
}
public boolean isRecursive() {
return recursive;
}
@Override
public String toString() {
return "Set{" + "names='" + getName() + '\'' + ", icons='" + getIcon() + "', elements ='" + elements + '\'' +'}';
}
}
public static class ElementBuilder {
public static LastAtomBuilder atom(final String name) {
return new ExplorerSchema.ElementBuilder().createAtomBuilder(name);
}
public static LastSetBuilder set(final String name) {
return new ExplorerSchema.ElementBuilder().createSetBuilder(name);
}
private ElementBuilder() {
}
private LastAtomBuilder createAtomBuilder(final String name) {
return new LastAtomBuilder(name);
}
private LastSetBuilder createSetBuilder(final String name) {
return new LastSetBuilder(name);
}
public static abstract class BaseElementBuilder {
protected E element;
private BaseElementBuilder(final E element) {
this.element = element;
}
protected List addAttribute(final String attributeName, final String attributeLabel) {
Utils.checkNotNull(attributeName, "attributeName");
Utils.checkArgument(
element.getAttributes().stream().noneMatch(a -> a.name.equals(attributeName)),
String.format("%s has already been added to the list of attribute names", attributeName)
);
ArrayList attributes = new ArrayList<>(element.getAttributes());
attributes.add(new Attribute(attributeName, attributeLabel));
return attributes;
}
protected Map addAdditionalNameIcon(final String name, final String icon) {
Utils.checkNotNull(name, "name");
Utils.checkNotNull(icon, "icon");
Map additionalNamesIcons = new HashMap<>(element.getAdditionalNamesIcons());
additionalNamesIcons.put(name, icon);
return additionalNamesIcons;
}
}
public static abstract class BaseAtomBuilder extends BaseElementBuilder {
private BaseAtomBuilder(final String name) {
super(
new Atom(name, "", true, new HashMap<>(),
Collections.singletonList(new Attribute(Element.NAME_ATTR, Element.NAME_ATTR)))
);
}
public C icon(final String icon) {
element = new Atom(element.getName(), icon, element.isCacheable(), element.getAdditionalNamesIcons(), element.getAttributes());
return (C) this;
}
public C cacheable(final boolean cacheable) {
element = new Atom(element.getName(), element.getIcon(), cacheable, element.getAdditionalNamesIcons(), element.getAttributes());
return (C) this;
}
public C also(final String name) {
return also(name, "");
}
public C also(final String name, final String icon) {
element = new Atom(element.getName(), element.getIcon(), element.isCacheable(), addAdditionalNameIcon(name, icon), element.getAttributes());
return (C) this;
}
public C attribute(final String attributeName, final String attributeLabel) {
element = new Atom(element.getName(), element.getIcon(), element.isCacheable(), element.getAdditionalNamesIcons(), addAttribute(attributeName, attributeLabel));
return (C) this;
}
public C attribute(final String attributeName) {
return attribute(attributeName, attributeName);
}
}
public static class AtomBuilder extends BaseAtomBuilder> {
private final P parent;
private AtomBuilder(final P parent, final String name) {
super(name);
this.parent = parent;
}
public P endAtom() {
return parent;
}
}
public static class LastAtomBuilder extends BaseAtomBuilder {
private LastAtomBuilder(final String name) {
super(name);
}
public Atom endAtom() {
return new Atom(element.getName(), element.getIcon(), element.isCacheable(), element.getAdditionalNamesIcons(), element.getAttributes());
}
}
public static class BaseSetBuilder extends BaseElementBuilder {
protected final List builders = new ArrayList<>();
public BaseSetBuilder(final String name) {
super(new Set(false, name, "", true, false, Collections.emptyList(), Collections.emptyMap(), Collections.singletonList(new Attribute(Element.NAME_ATTR, Element.NAME_ATTR))));
}
public C icon(final String icon) {
element = new Set(false, element.getName(), icon, element.isCacheable(), element.isRecursive(), Collections.emptyList(), element.getAdditionalNamesIcons(), element.getAttributes());
return (C) this;
}
public C cacheable(final boolean cacheable) {
element = new Set(false, element.getName(), element.getIcon(), cacheable, element.isRecursive(), Collections.emptyList(), element.getAdditionalNamesIcons(), element.getAttributes());
return (C) this;
}
public C also(final String name) {
return also(name, "");
}
public C also(final String name, final String icon) {
element = new Set(false, element.getName(), element.getIcon(), element.isCacheable(), element.isRecursive(), Collections.emptyList(), addAdditionalNameIcon(name, icon), element.getAttributes());
return (C) this;
}
public C attribute(final String attributeName, final String attributeLabel) {
element = new Set(false, element.getName(), element.getIcon(), element.isCacheable(), element.isRecursive(), Collections.emptyList(), element.getAdditionalNamesIcons(), addAttribute(attributeName, attributeLabel));
return (C) this;
}
public C attribute(final String attributeName) {
return attribute(attributeName, attributeName);
}
public AtomBuilder atom(final String name) {
AtomBuilder atomBuilder = new AtomBuilder<>((C) this, name);
builders.add(atomBuilder);
return atomBuilder;
}
public SetBuilder set(final String name) {
SetBuilder elementBuilder = new SetBuilder<>((C) this, name);
builders.add(elementBuilder);
return elementBuilder;
}
protected Set build(final List elements) {
return new Set(false, element.getName(), element.getIcon(), element.isCacheable(), element.isRecursive(), elements, element.getAdditionalNamesIcons(), element.getAttributes());
}
}
public static class SetBuilder extends BaseSetBuilder> {
private final P parent;
public SetBuilder(final P parent, final String name) {
super(name);
this.parent = parent;
}
public P endSet() {
element = build(builders.stream().map(b -> b.element).collect(Collectors.toList()));
return parent;
}
}
public static class LastSetBuilder extends BaseSetBuilder {
public LastSetBuilder(final String name) {
super(name);
}
public Set endSet() {
return build(builders.stream().map(b -> b.element).collect(Collectors.toList()));
}
}
}
private final Set root;
private final java.util.Set elementNames;
private final Map> parents;
private ExplorerSchema() {
root = null;
elementNames = Collections.emptySet();
parents = Collections.emptyMap();
}
/**
*
* Constructor.
*
*/
protected ExplorerSchema(Element element, Element... elements) {
Utils.checkNotNull(element, "element");
List elementList = new ArrayList<>();
elementList.add(element);
elementList.addAll(Arrays.asList(elements));
root = new Set(
true, "", "", true, false, elementList, Collections.emptyMap(),
Collections.singletonList(new Attribute(Element.NAME_ATTR, Element.NAME_ATTR))
);
List names = new ArrayList<>();
visit(root, e -> names.addAll(getSchemaElementNames(e)));
java.util.Set dupNames = names
.stream()
.filter(name -> Collections.frequency(names, name) > 1)
.collect(Collectors.toSet());
if (!dupNames.isEmpty()) {
throw new IllegalArgumentException(
String.format("ExplorerSchema '%s' has duplicate elements: '%s'", this.getClass().getSimpleName(), dupNames)
);
}
elementNames = Collections.unmodifiableSet(new HashSet<>(names));
parents = elementNames
.stream()
.collect(Collectors.toMap(
name -> name,
name -> {
List list = new ArrayList<>();
findParents(getRoot().getElements(), name, list);
return Collections.unmodifiableList(list);
})
);
}
/**
*
* If the visitor functions returns false it means the visit should mend
*
*/
private void visit(ExplorerSchema.Element element, Consumer visitor) {
visitor.accept(element);
element.asSet().ifPresent(set -> set.getElements().forEach(e -> visit(e, visitor)));
}
private java.util.List getSchemaElementNames(ExplorerSchema.Element element) {
return (element.getName().isEmpty())
? Collections.emptyList()
: Stream.concat(
Stream.of(element.getName()),
element.getAdditionalNamesIcons().keySet().stream()
).collect(Collectors.toList());
}
private static boolean findParents(
List schemaElements,
String name,
List parents) {
boolean found = false;
for (ExplorerSchema.Element schemaElement : schemaElements) {
found |= schemaElement.getName().equals(name);
if (!found) {
if (schemaElement instanceof ExplorerSchema.Set) {
if (findParents(((ExplorerSchema.Set)schemaElement).getElements(), name, parents)) {
parents.add(0, schemaElement.getName());
return true;
}
}
}
}
return found;
}
/**
*
* Returns the schema definition.
*
*/
public Set getRoot() {
return root;
}
/**
*
* Returns a set with all the schema elements names, it includes the element names defined via {@code also()}.
*
*/
public java.util.Set getElementNames() {
return elementNames;
}
/**
*
* Returns a list with the parents of a given element, in order from the top.
*
*/
public List getParents(String elementName) {
return parents.get(Utils.checkNotNull(elementName, "elementName"));
}
private Element findElement(Element element, String name) {
if (element.getName().equals(name)) {
return element;
}
if (element.asSet().isPresent()) {
for (Element child : element.asSet().get().getElements()) {
element = findElement(child, name);
if (element != null) {
return element;
}
}
}
return null;
}
private java.util.Set getChildrenName(Element element, int upToChildDepth) {
if (upToChildDepth == 0 || element.asAtom().isPresent()) {
return Collections.emptySet();
}
java.util.Set names = new HashSet<>();
element.asSet().get().getElements().forEach(e -> {
names.add(e.getName());
names.addAll(getChildrenName(e, upToChildDepth - 1));
});
return names;
}
/**
*
* Returns the children names of an element up to the given child depth.
*
*/
public java.util.Set getChildrenNames(String name, int upToChildDepth) {
Utils.checkNotNull(name, "name");
Utils.checkArgument(upToChildDepth >=0, "upToChildDepth must be >= 0");
Element element = findElement(getRoot(), name);
if (element == null) {
throw new IllegalArgumentException(Utils.format(
"ExplorerSchema '{}' element '{}' does not exist",
getClass().getSimpleName(),
name
));
}
return getChildrenName(element, upToChildDepth);
}
@Override
public String toString() {
return "ExplorerSchema{" + "root=" + root + '}';
}
}