| // 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.checkState; | |
| import static com.google.common.collect.ImmutableList.toImmutableList; | |
| import com.google.common.base.MoreObjects; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.exceptions.StorageException; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| /** Specific version of a secondary index schema. */ | |
| public class Schema<T> { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| public static class Builder<T> { | |
| private final List<FieldDef<T, ?>> fields = new ArrayList<>(); | |
| private boolean useLegacyNumericFields; | |
| public Builder<T> add(Schema<T> schema) { | |
| this.fields.addAll(schema.getFields().values()); | |
| return this; | |
| } | |
| @SafeVarargs | |
| public final Builder<T> add(FieldDef<T, ?>... fields) { | |
| this.fields.addAll(Arrays.asList(fields)); | |
| return this; | |
| } | |
| @SafeVarargs | |
| public final Builder<T> remove(FieldDef<T, ?>... fields) { | |
| this.fields.removeAll(Arrays.asList(fields)); | |
| return this; | |
| } | |
| public Builder<T> legacyNumericFields(boolean useLegacyNumericFields) { | |
| this.useLegacyNumericFields = useLegacyNumericFields; | |
| return this; | |
| } | |
| public Schema<T> build() { | |
| return new Schema<>(useLegacyNumericFields, ImmutableList.copyOf(fields)); | |
| } | |
| } | |
| public static class Values<T> { | |
| private final FieldDef<T, ?> field; | |
| private final Iterable<?> values; | |
| private Values(FieldDef<T, ?> field, Iterable<?> values) { | |
| this.field = field; | |
| this.values = values; | |
| } | |
| public FieldDef<T, ?> getField() { | |
| return field; | |
| } | |
| public Iterable<?> getValues() { | |
| return values; | |
| } | |
| } | |
| private static <T> FieldDef<T, ?> checkSame(FieldDef<T, ?> f1, FieldDef<T, ?> f2) { | |
| checkState(f1 == f2, "Mismatched %s fields: %s != %s", f1.getName(), f1, f2); | |
| return f1; | |
| } | |
| private final ImmutableMap<String, FieldDef<T, ?>> fields; | |
| private final ImmutableMap<String, FieldDef<T, ?>> storedFields; | |
| private final boolean useLegacyNumericFields; | |
| private int version; | |
| public Schema(boolean useLegacyNumericFields, Iterable<FieldDef<T, ?>> fields) { | |
| this(0, useLegacyNumericFields, fields); | |
| } | |
| public Schema(int version, boolean useLegacyNumericFields, Iterable<FieldDef<T, ?>> fields) { | |
| this.version = version; | |
| ImmutableMap.Builder<String, FieldDef<T, ?>> b = ImmutableMap.builder(); | |
| ImmutableMap.Builder<String, FieldDef<T, ?>> sb = ImmutableMap.builder(); | |
| for (FieldDef<T, ?> f : fields) { | |
| b.put(f.getName(), f); | |
| if (f.isStored()) { | |
| sb.put(f.getName(), f); | |
| } | |
| } | |
| this.fields = b.build(); | |
| this.storedFields = sb.build(); | |
| this.useLegacyNumericFields = useLegacyNumericFields; | |
| } | |
| public final int getVersion() { | |
| return version; | |
| } | |
| public final boolean useLegacyNumericFields() { | |
| return useLegacyNumericFields; | |
| } | |
| /** | |
| * Get all fields in this schema. | |
| * | |
| * <p>This is primarily useful for iteration. Most callers should prefer one of the helper methods | |
| * {@link #getField(FieldDef, FieldDef...)} or {@link #hasField(FieldDef)} to looking up fields by | |
| * name | |
| * | |
| * @return all fields in this schema indexed by name. | |
| */ | |
| public final ImmutableMap<String, FieldDef<T, ?>> getFields() { | |
| return fields; | |
| } | |
| /** @return all fields in this schema where {@link FieldDef#isStored()} is true. */ | |
| public final ImmutableMap<String, FieldDef<T, ?>> 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<FieldDef<T, ?>> getField(FieldDef<T, ?> first, FieldDef<T, ?>... rest) { | |
| FieldDef<T, ?> field = fields.get(first.getName()); | |
| if (field != null) { | |
| return Optional.of(checkSame(field, first)); | |
| } | |
| for (FieldDef<T, ?> f : rest) { | |
| field = fields.get(f.getName()); | |
| 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(FieldDef<T, ?> field) { | |
| FieldDef<T, ?> f = fields.get(field.getName()); | |
| if (f == null) { | |
| return false; | |
| } | |
| checkSame(f, field); | |
| return true; | |
| } | |
| private Values<T> fieldValues(T obj, FieldDef<T, ?> f) { | |
| 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. | |
| * | |
| * <p>Null values are omitted, as are fields which cause errors, which are logged. | |
| * | |
| * @param obj input object. | |
| * @return all non-null field values from the object. | |
| */ | |
| public final Iterable<Values<T>> buildFields(T obj) { | |
| try { | |
| return fields.values().stream() | |
| .map(f -> fieldValues(obj, f)) | |
| .filter(Objects::nonNull) | |
| .collect(toImmutableList()); | |
| } catch (StorageException e) { | |
| return ImmutableList.of(); | |
| } | |
| } | |
| @Override | |
| public String toString() { | |
| return MoreObjects.toStringHelper(this).addValue(fields.keySet()).toString(); | |
| } | |
| public void setVersion(int version) { | |
| this.version = version; | |
| } | |
| } |