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

net.pincette.mongo.streams.Project Maven / Gradle / Ivy

The newest version!
package net.pincette.mongo.streams;

import static java.util.Collections.emptySet;
import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static javax.json.JsonValue.FALSE;
import static javax.json.JsonValue.TRUE;
import static net.pincette.json.JsonUtil.asInt;
import static net.pincette.json.JsonUtil.createValue;
import static net.pincette.json.JsonUtil.isInt;
import static net.pincette.json.JsonUtil.isObject;
import static net.pincette.json.JsonUtil.objectValue;
import static net.pincette.json.JsonUtil.toJsonPointer;
import static net.pincette.json.Transform.transform;
import static net.pincette.mongo.Expression.function;
import static net.pincette.rs.Mapper.map;
import static net.pincette.util.Collections.map;
import static net.pincette.util.Collections.set;
import static net.pincette.util.Collections.union;
import static net.pincette.util.Pair.pair;
import static net.pincette.util.Util.allPaths;
import static net.pincette.util.Util.must;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Flow.Processor;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.json.JsonObject;
import javax.json.JsonValue;
import net.pincette.json.Transform.JsonEntry;
import net.pincette.json.Transform.Transformer;
import net.pincette.mongo.Features;
import net.pincette.rs.streams.Message;

/**
 * The $project operator.
 *
 * @author Werner Donné
 */
class Project {
  private static final String ID = "_id";
  private static final JsonValue REMOVE = createValue("REMOVE");
  private static final String REMOVE_VAR = "$$REMOVE";

  private Project() {}

  private static boolean exclude(final JsonValue value) {
    return FALSE.equals(value) || (isInt(value) && asInt(value) == 0);
  }

  private static Set expand(final Set include) {
    return include.stream()
        .flatMap(i -> allPaths(i, "."))
        .map(Project::removeExpression)
        .collect(toSet());
  }

  private static Optional field(
      final String key, final JsonValue value, final Predicate predicate) {
    return objectValue(value)
        .filter(j -> j.size() == 1)
        .map(j -> j.entrySet().iterator().next())
        .map(e -> field(key + "." + e.getKey(), e.getValue(), predicate))
        .orElseGet(() -> Optional.of(value).filter(predicate).map(v -> key));
  }

  private static Set findFields(
      final JsonObject json, final Predicate predicate) {
    return json.entrySet().stream()
        .map(e -> field(e.getKey(), e.getValue(), predicate))
        .filter(Optional::isPresent)
        .map(Optional::get)
        .collect(toSet());
  }

  private static boolean include(final JsonValue value) {
    return TRUE.equals(value) || (isInt(value) && asInt(value) == 1);
  }

  private static JsonObject project(
      final JsonObject json,
      final Set include,
      final Set exclude,
      final Map> updates) {
    final Set allIncludes = expand(union(include, updates.keySet()));

    return transform(
        json,
        new Transformer(
                e ->
                    exclude.contains(e.path)
                        || (!ID.equals(e.path)
                            && !allIncludes.isEmpty()
                            && !shouldInclude(e.path, allIncludes)),
                e -> empty())
            .thenApply(
                new Transformer(
                    e -> updates.containsKey(e.path),
                    e ->
                        Optional.of(updates.get(e.path))
                            .map(function -> function.apply(json))
                            .filter(value -> !value.equals(REMOVE))
                            .map(value -> new JsonEntry(e.path, value)))));
  }

  private static String removeExpression(final String path) {
    return Optional.of(path.indexOf(".$"))
        .filter(i -> i != -1)
        .map(i -> path.substring(0, i))
        .orElse(path);
  }

  private static boolean shouldInclude(final String path, final Set includes) {
    return allPaths(path, ".").anyMatch(includes::contains);
  }

  static Processor, Message> stage(
      final JsonValue expression, final Context context) {
    must(isObject(expression));

    final JsonObject expr = expression.asJsonObject();

    must(!expr.isEmpty() && ofNullable(expr.get(ID)).map(Project::exclude).orElse(true));

    final Set exclude =
        union(findFields(expr, Project::exclude), expr.containsKey(ID) ? set(ID) : emptySet());
    final Set include = findFields(expr, Project::include);
    final Map> updates =
        updates(expr, findFields(expr, Project::update), context.features);

    must(
        exclude.isEmpty()
            || (exclude.size() == 1 && exclude.contains(ID))
            || (include.isEmpty() && updates.isEmpty()));

    return map(m -> m.withValue(project(m.value, include, exclude, updates)));
  }

  private static boolean update(final JsonValue value) {
    return !include(value) && !exclude(value);
  }

  private static Map> updates(
      final JsonObject expression, final Set update, final Features features) {
    return update.stream()
        .map(Project::removeExpression)
        .collect(
            toMap(
                u -> u,
                u ->
                    function(
                        ofNullable(expression.get(u))
                            .orElseGet(() -> expression.getValue(toJsonPointer(u))),
                        map(pair(REMOVE_VAR, REMOVE)),
                        features)));
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy