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

com.sap.cloud.security.ams.dcn.engine.AllOfNode 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.AND;

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 AllOfNode extends EvaluationNode {
  private final Set subNodes = new HashSet<>();
  private AllOfConditionWriter 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#AND}
   * while recursively applying {@link #toCondition()} to its sub nodes. If there are no sub nodes,
   * the {@link AllOfNode} node will be converted to true. 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
   */
  AllOfNode(Collection subNodes) {
    this(
        subNodes,
        (nodes) ->
            switch (nodes.size()) {
              case 0 -> true;
              case 1 -> nodes.iterator().next().toCondition();
              default ->
                  Call.createFrom(AND, nodes.stream().map(EvaluationNode::toCondition).toList());
            });
  }

  private AllOfNode(Collection subNodes, AllOfConditionWriter 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
   */
  AllOfNode writtenBy(AllOfConditionWriter conditionWriter) {
    this.conditionWriter = conditionWriter;
    return this;
  }

  @Override
  public EvaluationNode deepSimplify() {
    return new AllOfNode(
            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#TRUE} are ignored, a single sub node that evaluates to {@link
   * ValueNode#FALSE} makes this method return {@link ValueNode#FALSE} as well. Sub nodes that are
   * of type {@link AllOfNode} 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#andMerge}) and collected into a set of new sub nodes. If the set of new
   * sub nodes is empty, this method returns {@link ValueNode#TRUE}. If the set of new sub nodes
   * contains only a single node, this method returns that node. Otherwise, this method returns a
   * new {@link AllOfNode} 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::andMerge);
          } else {
            newNodes.add(node);
          }
        };

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

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

  /**
   * Evaluates this node by recursively evaluating its sub nodes first. If any sub node evaluates to
   * {@link ValueNode#FALSE}, this method immediately returns {@link ValueNode#FALSE} as well. While
   * sub nodes evaluating to {@link ValueNode#TRUE} are ignored, any sub node evaluating to {@link
   * ValueNode#UNSET} makes this method return {@link ValueNode#UNSET} as well. Otherwise, if all
   * remaining sub node evaluate to {@link ValueNode#IGNORE}, this method returns {@link
   * ValueNode#IGNORE} 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#TRUE}.
   *
   * @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.FALSE.equals(newNode)) {
        return ValueNode.FALSE;
      }
      if (ValueNode.UNSET.equals(newNode)) {
        hasUnset = true;
      } else if (ValueNode.IGNORE.equals(newNode)) {
        hasIgnore = true;
      } else if (!ValueNode.TRUE.equals(newNode)) {
        newNodes.add(newNode);
      }
    }

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

  /**
   * Uses this node's {@link AllOfConditionWriter} to convert the subtree rooted at this node to a
   * condition object. The default {@link AllOfConditionWriter} 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;

    AllOfNode allOfNode = (AllOfNode) o;
    return subNodes.equals(allOfNode.subNodes);
  }

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

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

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