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

com.google.gerrit.index.IndexedField Maven / Gradle / Ivy

The newest version!
// Copyright (C) 2022 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 com.google.auto.value.AutoValue;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeToken;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.converter.ProtoConverter;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.SchemaFieldDefs.Getter;
import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.SchemaFieldDefs.Setter;
import com.google.gerrit.proto.Protos;
import com.google.protobuf.MessageLite;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.StreamSupport;

/**
 * Definition of a field stored in the secondary index.
 *
 * 

Each IndexedField, stored in index, may have multiple {@link SearchSpec} which defines how it * can be searched and how the index tokens are generated. * *

Index implementations may choose to store IndexedField and {@link SearchSpec} (search tokens) * separately, however {@link com.google.gerrit.index.query.IndexedQuery} always issues the queries * to {@link SearchSpec}. * *

This allows index implementations to store IndexedField once, while enabling multiple * tokenization strategies on the same IndexedField with {@link SearchSpec} * * @param input type from which documents are created and search results are returned. * @param type that should be extracted from the input object when converting to an index * document. */ // TODO(mariasavtchouk): revisit the class name after migration is done. @SuppressWarnings("serial") @AutoValue public abstract class IndexedField { public static final TypeToken INTEGER_TYPE = new TypeToken<>() {}; public static final TypeToken> ITERABLE_INTEGER_TYPE = new TypeToken<>() {}; public static final TypeToken LONG_TYPE = new TypeToken<>() {}; public static final TypeToken> ITERABLE_LONG_TYPE = new TypeToken<>() {}; public static final TypeToken STRING_TYPE = new TypeToken<>() {}; public static final TypeToken> ITERABLE_STRING_TYPE = new TypeToken<>() {}; public static final TypeToken BYTE_ARRAY_TYPE = new TypeToken<>() {}; public static final TypeToken> ITERABLE_BYTE_ARRAY_TYPE = new TypeToken<>() {}; public static final TypeToken TIMESTAMP_TYPE = new TypeToken<>() {}; // Should not be used directly, only used to check if the proto is stored private static final TypeToken MESSAGE_TYPE = new TypeToken<>() {}; public static Builder builder(String name, TypeToken fieldType) { return new AutoValue_IndexedField.Builder() .name(name) .fieldType(fieldType) .stored(false) .required(false); } public static Builder> iterableStringBuilder(String name) { return builder(name, IndexedField.ITERABLE_STRING_TYPE); } public static Builder stringBuilder(String name) { return builder(name, IndexedField.STRING_TYPE); } public static Builder integerBuilder(String name) { return builder(name, IndexedField.INTEGER_TYPE); } public static Builder longBuilder(String name) { return builder(name, IndexedField.LONG_TYPE); } public static Builder> iterableIntegerBuilder(String name) { return builder(name, IndexedField.ITERABLE_INTEGER_TYPE); } public static Builder timestampBuilder(String name) { return builder(name, IndexedField.TIMESTAMP_TYPE); } public static Builder byteArrayBuilder(String name) { return builder(name, IndexedField.BYTE_ARRAY_TYPE); } public static Builder> iterableByteArrayBuilder(String name) { return builder(name, IndexedField.ITERABLE_BYTE_ARRAY_TYPE); } /** * Defines how {@link IndexedField} can be searched and how the index tokens are generated. * *

Multiple {@link SearchSpec} can be defined on a single {@link IndexedField}. * *

Depending on the implementation, indexes can choose to store {@link IndexedField} and {@link * SearchSpec} separately. The searches are issues to {@link SearchSpec}. */ public class SearchSpec implements SchemaField { private final String name; private final SearchOption searchOption; public SearchSpec(String name, SearchOption searchOption) { checkName(name); this.name = name; this.searchOption = searchOption; } @Override public boolean isStored() { return getField().stored(); } @Override public boolean isRepeatable() { return getField().repeatable(); } @Override @Nullable public T get(I obj) { return getField().get(obj); } @Override public String getName() { return name; } @Override public FieldType getType() { SearchOption searchOption = getSearchOption(); TypeToken fieldType = getField().fieldType(); if (searchOption.equals(SearchOption.STORE_ONLY)) { return FieldType.STORED_ONLY; } else if ((fieldType.equals(IndexedField.INTEGER_TYPE) || fieldType.equals(IndexedField.ITERABLE_INTEGER_TYPE)) && searchOption.equals(SearchOption.EXACT)) { return FieldType.INTEGER; } else if (fieldType.equals(IndexedField.INTEGER_TYPE) && searchOption.equals(SearchOption.RANGE)) { return FieldType.INTEGER_RANGE; } else if (fieldType.equals(IndexedField.LONG_TYPE)) { return FieldType.LONG; } else if (fieldType.equals(IndexedField.TIMESTAMP_TYPE)) { return FieldType.TIMESTAMP; } else if (fieldType.equals(IndexedField.STRING_TYPE) || fieldType.equals(IndexedField.ITERABLE_STRING_TYPE)) { if (searchOption.equals(SearchOption.EXACT)) { return FieldType.EXACT; } else if (searchOption.equals(SearchOption.FULL_TEXT)) { return FieldType.FULL_TEXT; } else if (searchOption.equals(SearchOption.PREFIX)) { return FieldType.PREFIX; } } throw new IllegalArgumentException( String.format( "search spec [%s, %s] is not supported on field [%s, %s]", getName(), getSearchOption(), getField().name(), getField().fieldType())); } @Override public boolean setIfPossible(I object, StoredValue doc) { return getField().setIfPossible(object, doc); } /** * Returns {@link SearchOption} enabled on this field. * * @return {@link SearchOption} */ public SearchOption getSearchOption() { return searchOption; } /** * Returns {@link IndexedField} on which this spec was created. * * @return original {@link IndexedField} of this spec. */ public IndexedField getField() { return IndexedField.this; } @CanIgnoreReturnValue private String checkName(String name) { CharMatcher m = CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyz0123456789_"); checkArgument(name != null && m.matchesAllOf(name), "illegal field name: %s", name); return name; } } /** * Adds {@link SearchSpec} to this {@link IndexedField} * * @param name the name to use for in the search. * @param searchOption the tokenization option, enabled by the new {@link SearchSpec} * @return the added {@link SearchSpec}. */ public SearchSpec addSearchSpec(String name, SearchOption searchOption) { SearchSpec searchSpec = new SearchSpec(name, searchOption); checkArgument( !searchSpecs.containsKey(searchSpec.getName()), "Can not add search spec %s, because it is already defined on field %s", searchSpec.getName(), name()); searchSpecs.put(searchSpec.getName(), searchSpec); return searchSpec; } public SearchSpec exact(String name) { return addSearchSpec(name, SearchOption.EXACT); } public SearchSpec fullText(String name) { return addSearchSpec(name, SearchOption.FULL_TEXT); } public SearchSpec range(String name) { return addSearchSpec(name, SearchOption.RANGE); } public SearchSpec integerRange(String name) { checkState(fieldType().equals(INTEGER_TYPE)); // we currently store all integer range fields, this may change in the future checkState(stored()); return addSearchSpec(name, SearchOption.RANGE); } public SearchSpec integer(String name) { checkState(fieldType().equals(INTEGER_TYPE) || fieldType().equals(ITERABLE_INTEGER_TYPE)); return addSearchSpec(name, SearchOption.EXACT); } public SearchSpec longSearch(String name) { checkState(fieldType().equals(LONG_TYPE) || fieldType().equals(ITERABLE_LONG_TYPE)); return addSearchSpec(name, SearchOption.EXACT); } public SearchSpec prefix(String name) { return addSearchSpec(name, SearchOption.PREFIX); } public SearchSpec storedOnly(String name) { checkState(stored()); return addSearchSpec(name, SearchOption.STORE_ONLY); } public SearchSpec timestamp(String name) { checkState(fieldType().equals(TIMESTAMP_TYPE)); return addSearchSpec(name, SearchOption.RANGE); } /** A builder for {@link IndexedField}. */ @AutoValue.Builder public abstract static class Builder { public abstract IndexedField.Builder name(String name); public abstract IndexedField.Builder description(Optional description); public abstract IndexedField.Builder description(String description); public abstract Builder required(boolean required); public Builder required() { required(true); return this; } /** Allow reading the actual data from the index. */ public abstract Builder stored(boolean stored); public Builder stored() { stored(true); return this; } abstract Builder repeatable(boolean repeatable); public abstract Builder size(Optional value); public abstract Builder size(Integer value); public abstract Builder getter(Getter getter); public abstract Builder fieldSetter(Optional> setter); abstract TypeToken fieldType(); public abstract Builder fieldType(TypeToken type); public abstract Builder protoConverter( Optional> value); abstract IndexedField autoBuild(); // not public public final IndexedField build() { boolean isRepeatable = fieldType().isSubtypeOf(Iterable.class); repeatable(isRepeatable); IndexedField field = autoBuild(); checkName(field.name()); checkArgument(!field.size().isPresent() || field.size().get() > 0); return field; } public final IndexedField build(Getter getter, Setter setter) { return this.getter(getter).fieldSetter(Optional.of(setter)).build(); } public final IndexedField build( Getter getter, Setter setter, ProtoConverter protoConverter) { return this.getter(getter) .fieldSetter(Optional.of(setter)) .protoConverter(Optional.of(protoConverter)) .build(); } public final IndexedField build(Getter getter) { return this.getter(getter).fieldSetter(Optional.empty()).build(); } @CanIgnoreReturnValue private static String checkName(String name) { String allowedCharacters = "abcdefghijklmnopqrstuvwxyz0123456789_"; CharMatcher m = CharMatcher.anyOf(allowedCharacters + allowedCharacters.toUpperCase(Locale.US)); checkArgument(name != null && m.matchesAllOf(name), "illegal field name: %s", name); return name; } } private Map searchSpecs = new HashMap<>(); /** * The name to store this field under. * *

The name should use the UpperCamelCase format, see {@link Builder#checkName}. */ public abstract String name(); /** Optional description of the field data. */ public abstract Optional description(); /** * True if this field is mandatory. Default is false. * *

This property is not enforced by the common indexing logic. It is up to the index * implementations to enforce that the field is required. */ public abstract boolean required(); /** Allow reading the actual data from the index. Default is false. */ public abstract boolean stored(); /** True if this field is repeatable. */ public abstract boolean repeatable(); /** * Optional size constrain on the field. The size is not constrained if this property is {@link * Optional#empty()} * *

This property is not enforced by the common indexing logic. It is up to the index * implementations to enforce the size. * *

If the field is {@link #repeatable()}, the constraint applies to each element separately. */ public abstract Optional size(); /** See {@link Getter} */ public abstract Getter getter(); /** See {@link Setter} */ public abstract Optional> fieldSetter(); /** * The {@link TypeToken} describing the contents of the field. See static constants for the common * supported types. * * @return {@link TypeToken} of this field. */ public abstract TypeToken fieldType(); /** If the {@link #fieldType()} is proto, the converter to use on byte/proto conversions. */ public abstract Optional> protoConverter(); /** * Returns all {@link SearchSpec}, enabled on this field. * *

Note: weather or not a search is supported by the index depends on {@link Schema} version. */ public ImmutableMap getSearchSpecs() { return ImmutableMap.copyOf(searchSpecs); } /** * Get the field contents from the input object. * * @param input input object. * @return the field value(s) to index. */ @Nullable public T get(I input) { try { return getter().get(input); } catch (IOException e) { throw new StorageException(e); } } @SuppressWarnings("unchecked") public boolean setIfPossible(I object, StoredValue doc) { if (!fieldSetter().isPresent()) { return false; } if (this.fieldType().equals(STRING_TYPE)) { fieldSetter().get().set(object, (T) doc.asString()); return true; } else if (this.fieldType().equals(ITERABLE_STRING_TYPE)) { fieldSetter().get().set(object, (T) doc.asStrings()); return true; } else if (this.fieldType().equals(INTEGER_TYPE)) { fieldSetter().get().set(object, (T) doc.asInteger()); return true; } else if (this.fieldType().equals(ITERABLE_INTEGER_TYPE)) { fieldSetter().get().set(object, (T) doc.asIntegers()); return true; } else if (this.fieldType().equals(LONG_TYPE)) { fieldSetter().get().set(object, (T) doc.asLong()); return true; } else if (this.fieldType().equals(ITERABLE_LONG_TYPE)) { fieldSetter().get().set(object, (T) doc.asLongs()); return true; } else if (this.fieldType().equals(BYTE_ARRAY_TYPE)) { fieldSetter().get().set(object, (T) doc.asByteArray()); return true; } else if (this.fieldType().equals(ITERABLE_BYTE_ARRAY_TYPE)) { fieldSetter().get().set(object, (T) doc.asByteArrays()); return true; } else if (this.fieldType().equals(TIMESTAMP_TYPE)) { checkState(!repeatable(), "can't repeat timestamp values"); fieldSetter().get().set(object, (T) doc.asTimestamp()); return true; } else if (isProtoType()) { MessageLite proto = doc.asProto(); if (proto != null) { fieldSetter().get().set(object, (T) proto); return true; } byte[] bytes = doc.asByteArray(); if (bytes != null && protoConverter().isPresent()) { fieldSetter().get().set(object, (T) parseProtoFrom(bytes)); return true; } } else if (isProtoIterableType()) { Iterable protos = doc.asProtos(); if (protos != null) { fieldSetter().get().set(object, (T) protos); return true; } Iterable bytes = doc.asByteArrays(); if (bytes != null && protoConverter().isPresent()) { fieldSetter().get().set(object, (T) decodeProtos(bytes)); return true; } } return false; } /** Returns true if the {@link #fieldType} is a proto message. */ public boolean isProtoType() { if (repeatable()) { return false; } return MESSAGE_TYPE.isSupertypeOf(fieldType()); } /** Returns true if the {@link #fieldType} is a list of proto messages. */ public boolean isProtoIterableType() { if (!repeatable()) { return false; } if (!(fieldType().getType() instanceof ParameterizedType)) { return false; } ParameterizedType parameterizedType = (ParameterizedType) fieldType().getType(); if (parameterizedType.getActualTypeArguments().length != 1) { return false; } Type type = parameterizedType.getActualTypeArguments()[0]; return MESSAGE_TYPE.isSupertypeOf(type); } private ImmutableList decodeProtos(Iterable raw) { return StreamSupport.stream(raw.spliterator(), false) .map(bytes -> parseProtoFrom(bytes)) .collect(toImmutableList()); } private MessageLite parseProtoFrom(byte[] bytes) { return Protos.parseUnchecked(protoConverter().get().getParser(), bytes); } }