AbstractKeySpecifier.java

/*
 * SPDX-FileCopyrightText: none
 * SPDX-License-Identifier: CC0-1.0
 */

package gov.nist.secauto.metaschema.core.metapath.impl;

import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
import gov.nist.secauto.metaschema.core.metapath.ICollectionValue;
import gov.nist.secauto.metaschema.core.metapath.ISequence;
import gov.nist.secauto.metaschema.core.metapath.InvalidTypeMetapathException;
import gov.nist.secauto.metaschema.core.metapath.cst.IExpression;
import gov.nist.secauto.metaschema.core.metapath.function.library.ArrayGet;
import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
import gov.nist.secauto.metaschema.core.metapath.function.library.MapGet;
import gov.nist.secauto.metaschema.core.metapath.item.IItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IIntegerItem;
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
import gov.nist.secauto.metaschema.core.metapath.item.function.ArrayException;
import gov.nist.secauto.metaschema.core.metapath.item.function.IArrayItem;
import gov.nist.secauto.metaschema.core.metapath.item.function.IKeySpecifier;
import gov.nist.secauto.metaschema.core.metapath.item.function.IMapItem;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;

import java.util.stream.Stream;

import edu.umd.cs.findbugs.annotations.NonNull;

/**
 * A common base class for all key specifiers.
 */
public abstract class AbstractKeySpecifier implements IKeySpecifier {
  @Override
  public Stream<? extends ICollectionValue> lookup(
      @NonNull IItem targetItem,
      @NonNull DynamicContext dynamicContext,
      @NonNull ISequence<?> focus) {
    Stream<? extends ICollectionValue> result;
    if (targetItem instanceof IArrayItem) {
      result = lookupInArray((IArrayItem<?>) targetItem, dynamicContext, focus);
    } else if (targetItem instanceof IMapItem) {
      result = lookupInMap((IMapItem<?>) targetItem, dynamicContext, focus);
    } else {
      throw new InvalidTypeMetapathException(targetItem,
          String.format("Item type '%s' is not an array or map.", targetItem.getClass().getName()));
    }
    return result;
  }

  /**
   * A dispatch method intended to handle lookups within an array item.
   *
   * @param targetItem
   *          the item to query
   * @param dynamicContext
   *          the dynamic context to use for expression evaluation
   * @param focus
   *          the focus item for expression evaluation
   * @return a stream of collection values matching this key specifier
   */
  @NonNull
  protected abstract Stream<? extends ICollectionValue> lookupInArray(
      @NonNull IArrayItem<?> targetItem,
      @NonNull DynamicContext dynamicContext,
      @NonNull ISequence<?> focus);

  /**
   * A dispatch method intended to handle lookups within a map item.
   *
   * @param targetItem
   *          the item to query
   * @param dynamicContext
   *          the dynamic context to use for expression evaluation
   * @param focus
   *          the focus item for expression evaluation
   * @return a stream of collection values matching this key specifier
   */
  @NonNull
  protected abstract Stream<? extends ICollectionValue> lookupInMap(
      @NonNull IMapItem<?> targetItem,
      @NonNull DynamicContext dynamicContext,
      @NonNull ISequence<?> focus);

  /**
   * Construct a new key specifier supporting name-based lookups.
   *
   * @param name
   *          the name to use for lookups
   * @return the key specifier
   */
  @NonNull
  public static IKeySpecifier newNameKeySpecifier(@NonNull String name) {
    return new AbstractKeySpecifier.NcNameKeySpecifier(name);
  }

  /**
   * Construct a new key specifier supporting integer-based lookups.
   *
   * @param integer
   *          the integer to use for lookups
   * @return the key specifier
   */
  @NonNull
  public static IKeySpecifier newIntegerLiteralKeySpecifier(@NonNull IIntegerItem integer) {
    return new AbstractKeySpecifier.IntegerLiteralKeySpecifier(integer);
  }

  /**
   * Construct a new key specifier supporting wildcard lookups.
   *
   * @return the key specifier
   */
  @NonNull
  public static IKeySpecifier newWildcardKeySpecifier() {
    return new AbstractKeySpecifier.WildcardKeySpecifier();
  }

  /**
   * Construct a new key specifier supporting key-based lookups.
   *
   * @param keyExpression
   *          the expression used to get a key to use for lookups
   * @return the key specifier
   */
  @NonNull
  public static IKeySpecifier newParenthesizedExprKeySpecifier(@NonNull IExpression keyExpression) {
    return new AbstractKeySpecifier.ParenthesizedExprKeySpecifier(keyExpression);
  }

  private static class NcNameKeySpecifier
      extends AbstractKeySpecifier {
    @NonNull
    private final String name;

    public NcNameKeySpecifier(@NonNull String name) {
      this.name = name;
    }

    @NonNull
    public String getName() {
      return name;
    }

    @Override
    protected Stream<? extends IItem> lookupInArray(
        IArrayItem<?> targetItem,
        DynamicContext dynamicContext,
        ISequence<?> focus) {
      throw new InvalidTypeMetapathException(targetItem,
          String.format("A name-based lookup '%s' is not appropriate for an array.", getName()));
    }

    @Override
    protected Stream<? extends ICollectionValue> lookupInMap(
        IMapItem<?> targetItem,
        DynamicContext dynamicContext,
        ISequence<?> focus) {
      return ObjectUtils.notNull(Stream.ofNullable(MapGet.get(targetItem, IStringItem.valueOf(name))));
    }
  }

  private static class IntegerLiteralKeySpecifier
      extends AbstractKeySpecifier {
    private final int index;

    public IntegerLiteralKeySpecifier(@NonNull IIntegerItem literal) {
      index = literal.asInteger().intValueExact();
    }

    @Override
    protected Stream<? extends ICollectionValue> lookupInArray(
        IArrayItem<?> targetItem,
        DynamicContext dynamicContext,
        ISequence<?> focus) {
      try {
        return ObjectUtils.notNull(Stream.ofNullable(ArrayGet.get(targetItem, index)));
      } catch (IndexOutOfBoundsException ex) {
        throw new ArrayException(
            ArrayException.INDEX_OUT_OF_BOUNDS,
            String.format("The index '%d' is outside the range of values for the array size '%d'.",
                index + 1,
                targetItem.size()),
            ex);
      }
    }

    @Override
    protected Stream<? extends ICollectionValue> lookupInMap(
        IMapItem<?> targetItem,
        DynamicContext dynamicContext,
        ISequence<?> focus) {
      return ObjectUtils.notNull(Stream.ofNullable(MapGet.get(targetItem, IIntegerItem.valueOf(index))));
    }
  }

  private static class WildcardKeySpecifier
      extends AbstractKeySpecifier {

    public WildcardKeySpecifier() {
      // do nothing
    }

    @Override
    protected Stream<? extends ICollectionValue> lookupInArray(
        IArrayItem<?> targetItem,
        DynamicContext dynamicContext,
        ISequence<?> focus) {
      return ObjectUtils.notNull(targetItem.stream());
    }

    @Override
    protected Stream<? extends ICollectionValue> lookupInMap(
        IMapItem<?> targetItem,
        DynamicContext dynamicContext,
        ISequence<?> focus) {
      return ObjectUtils.notNull(targetItem.values().stream());
    }
  }

  private static class ParenthesizedExprKeySpecifier
      extends AbstractKeySpecifier {
    @NonNull
    private final IExpression keyExpression;

    public ParenthesizedExprKeySpecifier(@NonNull IExpression keyExpression) {
      this.keyExpression = keyExpression;
    }

    public IExpression getKeyExpression() {
      return keyExpression;
    }

    @Override
    protected Stream<ICollectionValue> lookupInArray(
        IArrayItem<?> targetItem,
        DynamicContext dynamicContext,
        ISequence<?> focus) {
      ISequence<IAnyAtomicItem> keys = FnData.fnData(getKeyExpression().accept(dynamicContext, focus));

      return ObjectUtils.notNull(keys.stream()
          .flatMap(key -> {
            if (key instanceof IIntegerItem) {
              int index = ((IIntegerItem) key).asInteger().intValueExact();
              try {
                return Stream.ofNullable(ArrayGet.get(targetItem, index));
              } catch (IndexOutOfBoundsException ex) {
                throw new ArrayException(
                    ArrayException.INDEX_OUT_OF_BOUNDS,
                    String.format("The index %d is outside the range of values for the array size '%d'.",
                        index + 1,
                        targetItem.size()),
                    ex);
              }
            }
            throw new InvalidTypeMetapathException(targetItem,
                String.format("The key '%s' of type '%s' is not appropriate for an array lookup.",
                    key.asString(),
                    key.getClass().getName()));

          }));
    }

    @Override
    protected Stream<ICollectionValue> lookupInMap(
        IMapItem<?> targetItem,
        DynamicContext dynamicContext,
        ISequence<?> focus) {
      ISequence<? extends IAnyAtomicItem> keys
          = ObjectUtils.requireNonNull(FnData.fnData(getKeyExpression().accept(dynamicContext, focus)));

      return ObjectUtils.notNull(keys.stream()
          .flatMap(key -> {
            assert key != null;
            return Stream.ofNullable(MapGet.get(targetItem, key));
          }));
    }
  }
}