All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cloud.security.ams.dcn.engine.AnyOfNode Maven / Gradle / Ivy

Go to download

Client Library for integrating Jakarta EE applications with SAP Authorization Management Service (AMS)

The newest version!
/************************************************************************
 * © 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cloud.security.ams.dcn.engine;

import static com.sap.cloud.security.ams.dcl.client.el.Call.QualifiedNames.OR;

import com.sap.cloud.security.ams.dcl.client.el.Call;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

/** {@link EvaluationNode} implementation of the n-ary logical AND operator. */
public class AnyOfNode extends EvaluationNode {
  private final Set subNodes = new HashSet<>();
  private AnyOfConditionWriter conditionWriter;

  /**
   * Creates a new instance having the given sub nodes. By default, a call to {@link #toCondition()}
   * will convert the node into a {@link Call} object with name {@link Call.QualifiedNames#OR} while
   * recursively applying {@link #toCondition()} to its sub nodes. If there are no sub nodes, the
   * {@link AllOfNode} node will be converted to false. If there is only a single sub node, the node
   * will be converted to the condition of that sub node. To override this behavior, use {@link
   * #writtenBy} to set an alternate condition writer.
   *
   * @param subNodes the sub nodes
   */
  AnyOfNode(Collection subNodes) {
    this(
        subNodes,
        (nodes) ->
            switch (nodes.size()) {
              case 0 -> false;
              case 1 -> nodes.iterator().next().toCondition();
              default ->
                  Call.createFrom(OR, nodes.stream().map(EvaluationNode::toCondition).toList());
            });
  }

  private AnyOfNode(Collection subNodes, AnyOfConditionWriter conditionWriter) {
    this.subNodes.addAll(subNodes);
    this.conditionWriter = conditionWriter;
  }

  /**
   * Sets an alternate condition writer for this node which is called by {@link #toCondition()}.
   * This is used to apply post-evaluation transformations which establish OPA-compatibility.
   *
   * @param conditionWriter the condition writer
   * @return this node
   */
  AnyOfNode writtenBy(AnyOfConditionWriter conditionWriter) {
    this.conditionWriter = conditionWriter;
    return this;
  }

  @Override
  public EvaluationNode deepSimplify() {
    return new AnyOfNode(
            subNodes.stream().map(EvaluationNode::deepSimplify).toList(), conditionWriter)
        .simplify();
  }

  /**
   * Simplifies this node by merging sub nodes and removing redundant nodes. While sub nodes that
   * evaluate to {@link ValueNode#FALSE} are ignored, a single sub node that evaluates to {@link
   * ValueNode#TRUE} makes this method return {@link ValueNode#TRUE} as well. Sub nodes that are of
   * type {@link AnyOfNode} are removed and their sub nodes are handled as if they were direct sub
   * nodes of this node. All remaining sub nodes are merged with each other if possible (see {@link
   * EvaluationNode#orMerge}) and collected into a set of new sub nodes. If the set of new sub nodes
   * is empty, this method returns {@link ValueNode#FALSE}. If the set of new sub nodes contains
   * only a single node, this method returns that node. Otherwise, this method returns a new {@link
   * AnyOfNode} instance with the new sub nodes.
   *
   * @return the simplified node
   */
  @Override
  public EvaluationNode simplify() {
    Set newNodes = new HashSet<>();
    Map mergedNodes = new HashMap<>();
    Consumer mergeOrCollect =
        (node) -> {
          Optional mergeKey = node.mergeKey();
          if (mergeKey.isPresent()) {
            mergedNodes.merge(mergeKey.get(), node, EvaluationNode::orMerge);
          } else {
            newNodes.add(node);
          }
        };

    for (EvaluationNode node : subNodes) {
      if (node instanceof AnyOfNode anyOfNode) {
        anyOfNode.subNodes.forEach(mergeOrCollect);
      } else if (ValueNode.TRUE.equals(node)) {
        return ValueNode.TRUE;
      } else if (!ValueNode.FALSE.equals(node)) {
        mergeOrCollect.accept(node);
      }
    }
    if (mergedNodes.containsValue(ValueNode.TRUE)) {
      return ValueNode.TRUE;
    }
    newNodes.addAll(mergedNodes.values().stream().filter(n -> !ValueNode.FALSE.equals(n)).toList());

    return newNodes.isEmpty()
        ? ValueNode.FALSE
        : newNodes.size() == 1
            ? newNodes.iterator().next()
            : new AnyOfNode(newNodes, conditionWriter);
  }

  /**
   * Evaluates this node by recursively evaluating its sub nodes first. If any sub node evaluates to
   * {@link ValueNode#TRUE}, this method immediately returns {@link ValueNode#TRUE} as well. While
   * sub nodes evaluating to {@link ValueNode#FALSE} are ignored, any sub node evaluating to {@link
   * ValueNode#IGNORE} makes this method return {@link ValueNode#IGNORE} as well. Otherwise, if all
   * remaining sub node evaluate to {@link ValueNode#UNSET}, this method returns {@link
   * ValueNode#UNSET} as well. Finally, if there are any sub nodes left, this method creates a new
   * {@link AllOfNode} instance from these nodes and returns its simplification. If no sub nodes are
   * left, this method returns {@link ValueNode#FALSE}.
   *
   * @param valueAccessor accessor for attribute values
   * @return the node resulting from evaluation
   */
  @Override
  public EvaluationNode evaluate(ValueAccessor valueAccessor) {
    Set newNodes = new HashSet<>();
    boolean hasUnset = false;
    boolean hasIgnore = false;
    for (EvaluationNode node : subNodes) {
      EvaluationNode newNode = node.evaluate(valueAccessor);
      if (ValueNode.TRUE.equals(newNode)) {
        return ValueNode.TRUE;
      }
      if (ValueNode.UNSET.equals(newNode)) {
        hasUnset = true;
      } else if (ValueNode.IGNORE.equals(newNode)) {
        hasIgnore = true;
      } else if (!ValueNode.FALSE.equals(newNode)) {
        newNodes.add(newNode);
      }
    }

    if (hasIgnore) {
      return ValueNode.IGNORE;
    }
    if (newNodes.isEmpty()) {
      return hasUnset ? ValueNode.UNSET : ValueNode.FALSE;
    }
    return new AnyOfNode(newNodes, conditionWriter).simplify();
  }

  /**
   * Uses this node's {@link AnyOfConditionWriter} to convert the subtree rooted at this node to a
   * condition object. The default {@link AnyOfConditionWriter} can be overridden by calling {@link
   * #writtenBy} beforehand.
   *
   * @return condition object
   */
  @Override
  public Object toCondition() {
    return conditionWriter.apply(subNodes);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    AnyOfNode anyOfNode = (AnyOfNode) o;
    return subNodes.equals(anyOfNode.subNodes);
  }

  @Override
  public int hashCode() {
    return subNodes.hashCode();
  }

  @Override
  public String toString() {
    return "AnyOf{" + subNodes + '}';
  }

  @FunctionalInterface
  public interface AnyOfConditionWriter {
    Object apply(Set subNodes);
  }
}