Skip to content

Commit

Permalink
Implement conditional constants
Browse files Browse the repository at this point in the history
Signed-off-by: Pablo Herrera <[email protected]>
  • Loading branch information
Pablete1234 committed Jul 28, 2024
1 parent 726c9ff commit 8bb77b9
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 75 deletions.
132 changes: 132 additions & 0 deletions core/src/main/java/tc/oc/pgm/map/ConditionalChecker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package tc.oc.pgm.map;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.jdom2.Element;
import tc.oc.pgm.util.xml.InvalidXMLException;
import tc.oc.pgm.util.xml.Node;
import tc.oc.pgm.util.xml.XMLUtils;

class ConditionalChecker {

private static final List<AttributeCheck> ATTRIBUTES = List.of(
new AttributeCheck("variant", ConditionalChecker::variant),
new AttributeCheck("has-variant", ConditionalChecker::hasVariant),
new AttributeCheck("constant", ConditionalChecker::constant));
private static final String ALL_ATTRS =
ATTRIBUTES.stream().map(AttributeCheck::key).collect(Collectors.joining("', '", "'", "'"));

/**
* Test if the current context passes the conditions declared in the element
*
* @param ctx the map's context
* @param el The conditional element
* @return true if the conditional
* @throws InvalidXMLException if the element is invalid in any way
*/
static boolean test(MapFilePreprocessor ctx, Element el) throws InvalidXMLException {
Boolean result = null;
for (var check : ATTRIBUTES) {
Boolean attRes = check.apply(ctx, el);
if (attRes != null) result = result == null ? attRes : result && attRes;
}

if (result == null)
throw new InvalidXMLException("Expected at least one of " + ALL_ATTRS + " attributes", el);

return result;
}

private static String[] split(String val) {
return val.split("[\\s,]+");
}

private static boolean variant(MapFilePreprocessor ctx, Element el, String value) {
if (value.indexOf(',') == -1) return value.equals(ctx.getVariant());
return Set.of(split(value)).contains(ctx.getVariant());
}

private static boolean hasVariant(MapFilePreprocessor ctx, Element el, String value) {
if (value.indexOf(',') == -1) return ctx.getVariantIds().contains(value);
return Arrays.stream(split(value)).anyMatch(ctx.getVariantIds()::contains);
}

private static boolean constant(MapFilePreprocessor ctx, Element el, String id)
throws InvalidXMLException {
var value = Node.fromAttr(el, "constant-value");
var cmp = XMLUtils.parseEnum(
Node.fromAttr(el, "constant-comparison"),
Cmp.class,
value == null ? Cmp.DEFINED : Cmp.EQUALS);

var constants = ctx.getConstants();

if (!cmp.requireValue) {
if (value != null)
throw new InvalidXMLException("Comparison type " + cmp + " should not have a value", value);

if (!constants.containsKey(id)) return cmp == Cmp.UNDEFINED;

return switch (cmp) {
case DEFINED -> true;
case DEFINED_DELETE -> constants.get(id) == null;
case DEFINED_VALUE -> constants.get(id) != null;
// Should never happen
default -> throw new IllegalStateException(cmp + " not supported");
};
} else {
String constant = constants.get(id);
if (constant == null) {
if (!constants.containsKey(id))
throw new InvalidXMLException(
"Unknown constant '" + id + "'. Only constants before the conditional may be used.",
el);
return false;
}
if (value == null)
throw new InvalidXMLException("Required attribute 'constant-value' not set", el);

return switch (cmp) {
case EQUALS -> Objects.equals(value.getValue(), constant);
case CONTAINS -> Set.of(split(value.getValue())).contains(constant);
case REGEX -> constant.matches(value.getValue());
case RANGE -> XMLUtils.parseNumericRange(value, Double.class)
.contains(XMLUtils.parseNumber(new Node(el), constant, Double.class, true));
// Should never happen
default -> throw new IllegalStateException(cmp + " not supported");
};
}
}

enum Cmp {
UNDEFINED(false),
DEFINED(false),
DEFINED_DELETE(false),
DEFINED_VALUE(false),
EQUALS(true),
CONTAINS(true),
REGEX(true),
RANGE(true);
private final boolean requireValue;

Cmp(boolean requireValue) {
this.requireValue = requireValue;
}
}

interface ElementPredicate {
boolean test(MapFilePreprocessor ctx, Element el, String value) throws InvalidXMLException;
}

record AttributeCheck(String key, ElementPredicate pred) {
Boolean apply(MapFilePreprocessor ctx, Element el) throws InvalidXMLException {
var attr = el.getAttribute(key);
if (attr == null) return null;

return pred.test(ctx, el, attr.getValue());
}
}
}
126 changes: 59 additions & 67 deletions core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand All @@ -28,19 +26,16 @@
import tc.oc.pgm.api.map.includes.MapIncludeProcessor;
import tc.oc.pgm.util.xml.DocumentWrapper;
import tc.oc.pgm.util.xml.InvalidXMLException;
import tc.oc.pgm.util.xml.Node;
import tc.oc.pgm.util.xml.SAXHandler;
import tc.oc.pgm.util.xml.XMLUtils;

public class MapFilePreprocessor {

private static final ThreadLocal<SAXBuilder> DOCUMENT_FACTORY =
ThreadLocal.withInitial(
() -> {
final SAXBuilder builder = new SAXBuilder();
builder.setSAXHandlerFactory(SAXHandler.FACTORY);
return builder;
});
private static final ThreadLocal<SAXBuilder> DOCUMENT_FACTORY = ThreadLocal.withInitial(() -> {
final SAXBuilder builder = new SAXBuilder();
builder.setSAXHandlerFactory(SAXHandler.FACTORY);
return builder;
});

private static final Pattern CONSTANT_PATTERN = Pattern.compile("\\$\\{(.+?)}");

Expand Down Expand Up @@ -79,27 +74,16 @@ public Document getDocument()
variantIds.add(XMLUtils.parseRequiredId(variant));
}

document.runWithoutVisitation(
() -> {
MapInclude global = includeProcessor.getGlobalInclude();
if (global != null) {
document.getRootElement().addContent(0, global.getContent());
includes.add(global);
}

preprocessChildren(document.getRootElement());
source.setIncludes(includes);
});

for (Element constant :
XMLUtils.flattenElements(document.getRootElement(), "constants", "constant", 0)) {
boolean isDelete = XMLUtils.parseBoolean(constant.getAttribute("delete"), false);
String text = constant.getText();
if ((text == null || text.isEmpty()) != isDelete)
throw new InvalidXMLException(
"Delete attribute cannot be combined with having an inner text", constant);
constants.put(XMLUtils.parseRequiredId(constant), isDelete ? null : constant.getText());
}
document.runWithoutVisitation(() -> {
MapInclude global = includeProcessor.getGlobalInclude();
if (global != null) {
document.getRootElement().addContent(0, global.getContent());
includes.add(global);
}

preprocessChildren(document.getRootElement());
source.setIncludes(includes);
});

// If no constants are set, assume we can skip the step
if (!constants.isEmpty()) {
Expand All @@ -109,25 +93,31 @@ public Document getDocument()
return document;
}

String getVariant() {
return variant;
}

Set<String> getVariantIds() {
return variantIds;
}

Map<String, String> getConstants() {
return constants;
}

private void preprocessChildren(Element parent) throws InvalidXMLException {
for (int i = 0; i < parent.getContentSize(); i++) {
Content content = parent.getContent(i);
if (!(content instanceof Element)) continue;

Element child = (Element) content;
List<Content> replacement = null;

switch (child.getName()) {
case "include":
replacement = processIncludeElement(child);
break;
case "if":
replacement = processConditional(child, true);
break;
case "unless":
replacement = processConditional(child, false);
break;
}
if (!(content instanceof Element child)) continue;

List<Content> replacement =
switch (child.getName()) {
case "include" -> processIncludeElement(child);
case "if" -> processConditional(child, true);
case "unless" -> processConditional(child, false);
case "constant" -> processConstant(child);
default -> null;
};

if (replacement != null) {
parent.removeContent(i);
Expand All @@ -141,25 +131,29 @@ private void preprocessChildren(Element parent) throws InvalidXMLException {

private List<Content> processIncludeElement(Element element) throws InvalidXMLException {
MapInclude include = includeProcessor.getMapInclude(element);
if (include != null) {
includes.add(include);
return include.getContent();
}
return Collections.emptyList();
if (include == null) return List.of();
includes.add(include);
return include.getContent();
}

private List<Content> processConditional(Element el, boolean shouldContain)
throws InvalidXMLException {
private List<Content> processConditional(Element el, boolean expect) throws InvalidXMLException {
return ConditionalChecker.test(this, el) == expect ? el.cloneContent() : List.of();
}

private List<Content> processConstant(Element el) throws InvalidXMLException {
boolean isDelete = XMLUtils.parseBoolean(el.getAttribute("delete"), false);
String text = el.getTextNormalize();
if ((text == null || text.isEmpty()) != isDelete)
throw new InvalidXMLException(
"Delete attribute cannot be combined with having an inner text", el);

Node node = Node.fromRequiredAttr(el, "variant", "has-variant");
List<String> filter = Arrays.asList(node.getValue().split("[\\s,]+"));
var id = XMLUtils.parseRequiredId(el);
var value = isDelete ? null : text;

boolean contains =
"variant".equals(node.getName())
? filter.contains(this.variant)
: filter.stream().anyMatch(variantIds::contains);
boolean fallback = XMLUtils.parseBoolean(el.getAttribute("fallback"), false);
if (!fallback || !constants.containsKey(id)) constants.put(id, value);

return contains == shouldContain ? el.cloneContent() : Collections.emptyList();
return List.of();
}

private void postprocessChildren(Element parent) throws InvalidXMLException {
Expand All @@ -177,11 +171,9 @@ private void postprocessChildren(Element parent) throws InvalidXMLException {

for (int i = 0; i < parent.getContentSize(); i++) {
Content content = parent.getContent(i);
if (content instanceof Element) {
postprocessChildren((Element) content);
} else if (content instanceof Text) {
Text text = (Text) content;

if (content instanceof Element el) {
postprocessChildren(el);
} else if (content instanceof Text text) {
String result = postprocessString(parent, text.getText());
if (result == null) {
parent.removeContent(text);
Expand All @@ -196,7 +188,7 @@ private void postprocessChildren(Element parent) throws InvalidXMLException {
private @Nullable String postprocessString(Element el, String text) throws InvalidXMLException {
Matcher matcher = CONSTANT_PATTERN.matcher(text);

StringBuffer result = new StringBuffer();
StringBuilder result = new StringBuilder();
while (matcher.find()) {
String constant = matcher.group(1);
String replacement = constants.get(constant);
Expand Down
17 changes: 9 additions & 8 deletions util/src/main/java/tc/oc/pgm/util/xml/DocumentWrapper.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package tc.oc.pgm.util.xml;

import com.google.common.collect.Sets;
import java.util.Set;
import java.util.function.Consumer;
import org.jdom2.Attribute;
Expand All @@ -12,7 +11,8 @@

public class DocumentWrapper extends Document {

private static final Set<String> IGNORED = Sets.newHashSet("variant", "tutorial", "edition");
private static final Set<String> IGNORED =
Set.of("constants", "edition", "name", "tutorial", "variant");

private boolean visitingAllowed = true;

Expand Down Expand Up @@ -57,15 +57,16 @@ private void checkVisited(Element el, Consumer<Node> unvisited) {
unvisited.accept(Node.fromNullable(attribute));
}

boolean canIgnore = el == getRootElement();

for (int i = 0; i < el.getContentSize(); i++) {
Content c = el.getContent(i);
if (!(c instanceof InheritingElement)) continue;
InheritingElement child = (InheritingElement) c;
if (!(c instanceof InheritingElement child)) continue;
if (child.getNamespace() != Namespace.NO_NAMESPACE) continue;
if (canIgnore && IGNORED.contains(child.getName())) continue;

if (child.getNamespace() == Namespace.NO_NAMESPACE && !IGNORED.contains(child.getName())) {
if (!child.wasVisited()) unvisited.accept(Node.fromNullable(child));
else checkVisited(child, unvisited);
}
if (!child.wasVisited()) unvisited.accept(Node.fromNullable(child));
else checkVisited(child, unvisited);
}
}

Expand Down

0 comments on commit 8bb77b9

Please sign in to comment.