Perspective.java
/*
* Copyright (c) 2021 Mārtiņš Avots (Martins Avots) and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0, or the MIT License,
* which is available at https://spdx.org/licenses/MIT.html.
*
* SPDX-License-Identifier: EPL-2.0 OR MIT
*/
package net.splitcells.dem.lang.perspective;
import net.splitcells.dem.data.set.list.List;
import net.splitcells.dem.data.set.list.Lists;
import net.splitcells.dem.lang.Xml;
import net.splitcells.dem.lang.annotations.ReturnsThis;
import net.splitcells.dem.lang.namespace.NameSpace;
import net.splitcells.dem.lang.namespace.NameSpaces;
import org.w3c.dom.Node;
import java.util.Collection;
import java.util.Optional;
import static net.splitcells.dem.data.set.list.Lists.list;
import static net.splitcells.dem.data.set.list.Lists.toList;
import static net.splitcells.dem.lang.namespace.NameSpaces.*;
import static net.splitcells.dem.lang.perspective.PerspectiveI.perspective;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Interface for adhoc and dynamic trees.
* <p>
* There is no distinction between text, attributes and elements like in XML, as there is no
* actual meaning in this distinction. In XML this is used for rendering and
* helps to distinct between text and elements in XSL. In Perspective this distinction is
* done via name spaces.
* <p>
* IDEA Create alternative to XSL.
* <p></p>
* A perspective is like an variable. An variable may only hold one value,
* that may be a list of values. A perspective holds a value and a list of perspectives.
* In other words, a Perspective is an structure for variables.
* <p></p>
* It has a name in a certain scope which is the namespace.
* The name is only valid in this scope and may restrict the possible values of the perspective.
* In other words the namespace may have an type encoded in it, that is described externally.
* A perspective has a value, if it only contains exactly one value.
* A perspective has children, if it contains multiple values.
*/
public interface Perspective extends PerspectiveView {
List<Perspective> children();
default Perspective withText(String text) {
return withValues(perspective(text, STRING));
}
default Perspective withProperty(String name, NameSpace nameSpace, String value) {
return withValue(perspective(name, nameSpace)
.withValue(perspective(value, STRING)));
}
default Perspective withProperty(String name, String value) {
return withValue(perspective(name)
.withValue(perspective(value, STRING)));
}
default Perspective withValues(Perspective... args) {
children().addAll(list(args));
return this;
}
default List<Perspective> propertiesWithValue(String name, NameSpace nameSpace, String value) {
return propertyInstances(name, nameSpace).stream()
.filter(property -> property.value().get().name().equals(value))
.collect(toList());
}
default String toStringPathsDescription() {
return toStringPathsDescription(toStringPaths());
}
static String toStringPathsDescription(List<String> paths) {
return paths
.stream()
.reduce((a, b) -> a + "\n" + b)
.orElse("");
}
default List<String> toStringPaths() {
if (children().isEmpty()) {
return list(name());
}
return children().stream()
.map(child -> child.toStringPaths().stream()
.map(childS -> name() + " " + childS)
.collect(toList()))
.flatMap(Collection::stream)
.collect(toList());
}
default List<Perspective> propertyInstances(String name, NameSpace nameSpace) {
return children().stream()
.filter(property -> name.equals(property.name()))
.filter(property -> nameSpace.equals(property.nameSpace()))
.filter(property -> property.children().size() == 1)
.filter(property -> STRING.equals(property.children().get(0).nameSpace()))
.collect(Lists.toList());
}
default Optional<Perspective> propertyInstance(String name, NameSpace nameSpace) {
final var propertyInstances = propertyInstances(name, nameSpace);
assertThat(propertyInstances).hasSizeLessThan(2);
if (propertyInstances.isEmpty()) {
return Optional.ofNullable(null);
}
return Optional.of(propertyInstances.get(0));
}
default Optional<Perspective> childNamed(String name, NameSpace nameSpace) {
final var children = children().stream()
.filter(child -> nameSpace.equals(child.nameSpace()) && name.equals(child.name()))
.collect(toList());
if (children.isEmpty()) {
return Optional.ofNullable(null);
}
return Optional.of(children.get(0));
}
default Perspective withChildren(List<Perspective> argChildren) {
argChildren.forEach(children()::add);
return this;
}
default Perspective withChild(Perspective arg) {
children().add(arg);
return this;
}
@Deprecated
default Perspective withValue(Perspective arg) {
children().add(arg);
return this;
}
@Override
default Node toDom() {
final Node dom;
// HACK Use generic rendering specifics based on argument.
if (STRING.equals(nameSpace()) && children().isEmpty()) {
dom = Xml.textNode(name());
} else {
dom = Xml.rElement(nameSpace(), name());
}
children().forEach(child -> dom.appendChild(child.toDom()));
return dom;
}
@ReturnsThis
default Perspective withPath(Perspective path, String propertyName, NameSpace nameSpace) {
return withPath(this, path, propertyName, nameSpace);
}
private static Perspective withPath(Perspective current, Perspective path, String propertyName, NameSpace nameSpace) {
final var propertyInstances = path.propertyInstances(propertyName, nameSpace);
if (propertyInstances.isEmpty()) {
return current;
}
assertThat(propertyInstances).hasSize(1);
final var element = propertyInstances.get(0);
final var propertyValue = element.value().get().name();
final var propertyHosters = current.children().stream()
.filter(child -> child.propertiesWithValue(propertyName, nameSpace, propertyValue).size() == 1)
.collect(toList());
final Perspective child;
if (propertyHosters.isEmpty()) {
// HACK Use generic rendering specifics based on argument.
child = perspective(NameSpaces.VAL, NATURAL)
.withProperty(NameSpaces.NAME, NATURAL, propertyValue);
final var elementLinking = path.childNamed(LINK, DEN);
if (elementLinking.isPresent()) {
child.withChild(elementLinking.get());
}
current.withChild(child);
} else {
assertThat(propertyHosters).hasSize(1);
child = propertyHosters.get(0);
}
path.children().stream()
.filter(pathChild -> !child.propertyInstances(propertyName, nameSpace).isEmpty())
.forEach(pathChild -> withPath(child, pathChild, propertyName, nameSpace));
return current;
}
}