com.google.gerrit.index.Schema Maven / Gradle / Ivy
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.index;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
/** Specific version of a secondary index schema. */
public class Schema {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
  public static class Builder {
    private final List> searchFields = new ArrayList<>();
    private final List> indexedFields = new ArrayList<>();
    private Optional version = Optional.empty();
    @CanIgnoreReturnValue
    public Builder version(int version) {
      this.version = Optional.of(version);
      return this;
    }
    @CanIgnoreReturnValue
    public Builder add(Schema schema) {
      this.indexedFields.addAll(schema.getIndexFields().values());
      this.searchFields.addAll(schema.getSchemaFields().values());
      if (!version.isPresent()) {
        version(schema.getVersion() + 1);
      }
      return this;
    }
    @SafeVarargs
    @CanIgnoreReturnValue
    public final Builder addSearchSpecs(IndexedField.SearchSpec... searchSpecs) {
      return addSearchSpecs(ImmutableList.copyOf(searchSpecs));
    }
    @CanIgnoreReturnValue
    public Builder addSearchSpecs(ImmutableList.SearchSpec> searchSpecs) {
      for (IndexedField.SearchSpec searchSpec : searchSpecs) {
        checkArgument(
            this.indexedFields.contains(searchSpec.getField()),
            "%s spec can only be added to the schema that contains %s field",
            searchSpec.getName(),
            searchSpec.getField().name());
      }
      this.searchFields.addAll(searchSpecs);
      return this;
    }
    @SafeVarargs
    @CanIgnoreReturnValue
    public final Builder addIndexedFields(IndexedField... fields) {
      return addIndexedFields(ImmutableList.copyOf(fields));
    }
    @CanIgnoreReturnValue
    public Builder addIndexedFields(ImmutableList> indexedFields) {
      this.indexedFields.addAll(indexedFields);
      return this;
    }
    @SafeVarargs
    @CanIgnoreReturnValue
    public final Builder remove(IndexedField.SearchSpec... searchSpecs) {
      this.searchFields.removeAll(Arrays.asList(searchSpecs));
      return this;
    }
    @SafeVarargs
    @CanIgnoreReturnValue
    public final Builder remove(IndexedField... indexedFields) {
      for (IndexedField field : indexedFields) {
        ImmutableMap.SearchSpec> searchSpecs =
            field.getSearchSpecs();
        checkArgument(
            !searchSpecs.values().stream().anyMatch(this.searchFields::contains),
            "Field %s can be only removed from schema after all of its searches are removed.",
            field.name());
      }
      this.indexedFields.removeAll(Arrays.asList(indexedFields));
      return this;
    }
    public Schema build() {
      checkState(version.isPresent());
      return new Schema<>(
          version.get(), ImmutableList.copyOf(indexedFields), ImmutableList.copyOf(searchFields));
    }
  }
  public static class Values {
    private final SchemaField field;
    private final Iterable> values;
    private Values(SchemaField field, Iterable> values) {
      this.field = field;
      this.values = values;
    }
    public SchemaField getField() {
      return field;
    }
    public Iterable> getValues() {
      return values;
    }
  }
  @CanIgnoreReturnValue
  private static  SchemaField checkSame(SchemaField f1, SchemaField f2) {
    checkState(f1 == f2, "Mismatched %s fields: %s != %s", f1.getName(), f1, f2);
    return f1;
  }
  private final ImmutableSet storedFields;
  private final ImmutableMap> schemaFields;
  private final ImmutableMap> indexedFields;
  private int version;
  private Schema(
      int version,
      ImmutableList> indexedFields,
      ImmutableList> schemaFields) {
    this.version = version;
    this.indexedFields =
        indexedFields.stream().collect(toImmutableMap(IndexedField::name, Function.identity()));
    this.schemaFields =
        schemaFields.stream().collect(toImmutableMap(SchemaField::getName, Function.identity()));
    Set duplicateKeys =
        Sets.intersection(this.schemaFields.keySet(), this.indexedFields.keySet());
    checkArgument(
        duplicateKeys.isEmpty(),
        "DuplicateKeys found %s, indexFields:%s, schemaFields: %s",
        duplicateKeys,
        this.indexedFields.keySet(),
        this.schemaFields.keySet());
    this.storedFields =
        schemaFields.stream()
            .filter(SchemaField::isStored)
            .map(SchemaField::getName)
            .collect(toImmutableSet());
  }
  public final int getVersion() {
    return version;
  }
  /**
   * Get all fields in this schema.
   *
   * This is primarily useful for iteration. Most callers should prefer one of the helper methods
   * {@link #getField(SchemaField, SchemaField...)} or {@link #hasField(SchemaField)} to looking up
   * fields by name
   *
   * @return all fields in this schema indexed by name.
   */
  public final ImmutableMap> getSchemaFields() {
    return schemaFields;
  }
  public final ImmutableMap> getIndexFields() {
    return indexedFields;
  }
  /**
   * Returns names of {@link SchemaField} fields in this schema where {@link SchemaField#isStored()}
   * is true.
   */
  public final ImmutableSet getStoredFields() {
    return storedFields;
  }
  /**
   * Look up fields in this schema.
   *
   * @param first the preferred field to look up.
   * @param rest additional fields to look up.
   * @return the first field in the schema matching {@code first} or {@code rest}, in order, or
   *     absent if no field matches.
   */
  @SafeVarargs
  public final Optional> getField(
      SchemaField first, SchemaField... rest) {
    SchemaField field = getSchemaField(first);
    if (field != null) {
      return Optional.of(checkSame(field, first));
    }
    for (SchemaField f : rest) {
      field = getSchemaField(first);
      if (field != null) {
        return Optional.of(checkSame(field, f));
      }
    }
    return Optional.empty();
  }
  /**
   * Check whether a field is present in this schema.
   *
   * @param field field to look up.
   * @return whether the field is present.
   */
  public final boolean hasField(SchemaField field) {
    SchemaField f = getSchemaField(field);
    if (f == null) {
      return false;
    }
    checkSame(f, field);
    return true;
  }
  public final boolean hasField(String fieldName) {
    return this.getSchemaField(fieldName) != null;
  }
  private SchemaField getSchemaField(SchemaField field) {
    return getSchemaField(field.getName());
  }
  public SchemaField getSchemaField(String fieldName) {
    return schemaFields.get(fieldName);
  }
  private @Nullable Values fieldValues(
      T obj, SchemaField f, ImmutableSet skipFields) {
    if (skipFields.contains(f.getName())) {
      return null;
    }
    Object v;
    try {
      v = f.get(obj);
    } catch (StorageException e) {
      // StorageException is thrown when the object is not found. On this case,
      // it is pointless to make further attempts for each field, so propagate
      // the exception to return an empty list.
      logger.atSevere().withCause(e).log("error getting field %s of %s", f.getName(), obj);
      throw e;
    } catch (RuntimeException e) {
      logger.atSevere().withCause(e).log("error getting field %s of %s", f.getName(), obj);
      return null;
    }
    if (v == null) {
      return null;
    } else if (f.isRepeatable()) {
      return new Values<>(f, (Iterable>) v);
    } else {
      return new Values<>(f, Collections.singleton(v));
    }
  }
  /**
   * Build all fields in the schema from an input object.
   *
   * Null values are omitted, as are fields which cause errors, which are logged. If any of the
   * fields cause a StorageException, the whole operation fails and the exception is propagated to
   * the caller.
   *
   * @param obj input object.
   * @param skipFields set of field names to skip when indexing the document
   * @return all non-null field values from the object.
   */
  public final ImmutableList> buildFields(T obj, ImmutableSet skipFields) {
    return schemaFields.values().stream()
        .map(f -> fieldValues(obj, f, skipFields))
        .filter(Objects::nonNull)
        .collect(toImmutableList());
  }
  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
        .addValue(indexedFields.keySet())
        .addValue(schemaFields.keySet())
        .toString();
  }
}