Line data Source code
1 : // Copyright (C) 2013 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.index;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 : import static com.google.common.base.Preconditions.checkState;
19 : import static com.google.common.collect.ImmutableList.toImmutableList;
20 : import static com.google.common.collect.ImmutableMap.toImmutableMap;
21 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
22 :
23 : import com.google.common.base.MoreObjects;
24 : import com.google.common.collect.ImmutableList;
25 : import com.google.common.collect.ImmutableMap;
26 : import com.google.common.collect.ImmutableSet;
27 : import com.google.common.collect.Sets;
28 : import com.google.common.flogger.FluentLogger;
29 : import com.google.gerrit.common.Nullable;
30 : import com.google.gerrit.exceptions.StorageException;
31 : import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
32 : import java.util.ArrayList;
33 : import java.util.Arrays;
34 : import java.util.Collections;
35 : import java.util.List;
36 : import java.util.Objects;
37 : import java.util.Optional;
38 : import java.util.Set;
39 : import java.util.function.Function;
40 :
41 : /** Specific version of a secondary index schema. */
42 : public class Schema<T> {
43 154 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
44 :
45 154 : public static class Builder<T> {
46 154 : private final List<SchemaField<T, ?>> searchFields = new ArrayList<>();
47 154 : private final List<IndexedField<T, ?>> indexedFields = new ArrayList<>();
48 :
49 154 : private Optional<Integer> version = Optional.empty();
50 :
51 : public Builder<T> version(int version) {
52 154 : this.version = Optional.of(version);
53 154 : return this;
54 : }
55 :
56 : public Builder<T> add(Schema<T> schema) {
57 154 : this.indexedFields.addAll(schema.getIndexFields().values());
58 154 : this.searchFields.addAll(schema.getSchemaFields().values());
59 154 : if (!version.isPresent()) {
60 154 : version(schema.getVersion() + 1);
61 : }
62 154 : return this;
63 : }
64 :
65 : @SafeVarargs
66 : public final Builder<T> add(FieldDef<T, ?>... fields) {
67 154 : return add(ImmutableList.copyOf(fields));
68 : }
69 :
70 : public final Builder<T> add(ImmutableList<FieldDef<T, ?>> fields) {
71 154 : this.searchFields.addAll(fields);
72 154 : return this;
73 : }
74 :
75 : @SafeVarargs
76 : public final Builder<T> remove(FieldDef<T, ?>... fields) {
77 154 : this.searchFields.removeAll(Arrays.asList(fields));
78 154 : return this;
79 : }
80 :
81 : @SafeVarargs
82 : public final Builder<T> addSearchSpecs(IndexedField<T, ?>.SearchSpec... searchSpecs) {
83 154 : return addSearchSpecs(ImmutableList.copyOf(searchSpecs));
84 : }
85 :
86 : public Builder<T> addSearchSpecs(ImmutableList<IndexedField<T, ?>.SearchSpec> searchSpecs) {
87 154 : for (IndexedField<T, ?>.SearchSpec searchSpec : searchSpecs) {
88 154 : checkArgument(
89 154 : this.indexedFields.contains(searchSpec.getField()),
90 : "%s spec can only be added to the schema that contains %s field",
91 154 : searchSpec.getName(),
92 154 : searchSpec.getField().name());
93 154 : }
94 154 : this.searchFields.addAll(searchSpecs);
95 154 : return this;
96 : }
97 :
98 : @SafeVarargs
99 : public final Builder<T> addIndexedFields(IndexedField<T, ?>... fields) {
100 154 : return addIndexedFields(ImmutableList.copyOf(fields));
101 : }
102 :
103 : public Builder<T> addIndexedFields(ImmutableList<IndexedField<T, ?>> indexedFields) {
104 154 : this.indexedFields.addAll(indexedFields);
105 154 : return this;
106 : }
107 :
108 : @SafeVarargs
109 : public final Builder<T> remove(IndexedField<T, ?>.SearchSpec... searchSpecs) {
110 153 : this.searchFields.removeAll(Arrays.asList(searchSpecs));
111 153 : return this;
112 : }
113 :
114 : @SafeVarargs
115 : public final Builder<T> remove(IndexedField<T, ?>... indexedFields) {
116 153 : for (IndexedField<T, ?> field : indexedFields) {
117 153 : ImmutableMap<String, ? extends IndexedField<T, ?>.SearchSpec> searchSpecs =
118 153 : field.getSearchSpecs();
119 153 : checkArgument(
120 153 : !searchSpecs.values().stream().anyMatch(this.searchFields::contains),
121 : "Field %s can be only removed from schema after all of its searches are removed.",
122 153 : field.name());
123 : }
124 153 : this.indexedFields.removeAll(Arrays.asList(indexedFields));
125 153 : return this;
126 : }
127 :
128 : public Schema<T> build() {
129 154 : checkState(version.isPresent());
130 154 : return new Schema<>(
131 154 : version.get(), ImmutableList.copyOf(indexedFields), ImmutableList.copyOf(searchFields));
132 : }
133 : }
134 :
135 : public static class Values<T> {
136 : private final SchemaField<T, ?> field;
137 : private final Iterable<?> values;
138 :
139 7 : private Values(SchemaField<T, ?> field, Iterable<?> values) {
140 7 : this.field = field;
141 7 : this.values = values;
142 7 : }
143 :
144 : public SchemaField<T, ?> getField() {
145 7 : return field;
146 : }
147 :
148 : public Iterable<?> getValues() {
149 7 : return values;
150 : }
151 : }
152 :
153 : private static <T> SchemaField<T, ?> checkSame(SchemaField<T, ?> f1, SchemaField<T, ?> f2) {
154 116 : checkState(f1 == f2, "Mismatched %s fields: %s != %s", f1.getName(), f1, f2);
155 116 : return f1;
156 : }
157 :
158 : private final ImmutableSet<String> storedFields;
159 :
160 : private final ImmutableMap<String, SchemaField<T, ?>> schemaFields;
161 : private final ImmutableMap<String, IndexedField<T, ?>> indexedFields;
162 :
163 : private int version;
164 :
165 : private Schema(
166 : int version,
167 : ImmutableList<IndexedField<T, ?>> indexedFields,
168 154 : ImmutableList<SchemaField<T, ?>> schemaFields) {
169 154 : this.version = version;
170 :
171 154 : this.indexedFields =
172 154 : indexedFields.stream().collect(toImmutableMap(IndexedField::name, Function.identity()));
173 154 : this.schemaFields =
174 154 : schemaFields.stream().collect(toImmutableMap(SchemaField::getName, Function.identity()));
175 :
176 154 : Set<String> duplicateKeys =
177 154 : Sets.intersection(this.schemaFields.keySet(), this.indexedFields.keySet());
178 154 : checkArgument(
179 154 : duplicateKeys.isEmpty(),
180 : "DuplicateKeys found %s, indexFields:%s, schemaFields: %s",
181 : duplicateKeys,
182 154 : this.indexedFields.keySet(),
183 154 : this.schemaFields.keySet());
184 154 : this.storedFields =
185 154 : schemaFields.stream()
186 154 : .filter(SchemaField::isStored)
187 154 : .map(SchemaField::getName)
188 154 : .collect(toImmutableSet());
189 154 : }
190 :
191 : public final int getVersion() {
192 154 : return version;
193 : }
194 :
195 : /**
196 : * Get all fields in this schema.
197 : *
198 : * <p>This is primarily useful for iteration. Most callers should prefer one of the helper methods
199 : * {@link #getField(SchemaField, SchemaField...)} or {@link #hasField(SchemaField)} to looking up
200 : * fields by name
201 : *
202 : * @return all fields in this schema indexed by name.
203 : */
204 : public final ImmutableMap<String, SchemaField<T, ?>> getSchemaFields() {
205 154 : return schemaFields;
206 : }
207 :
208 : public final ImmutableMap<String, IndexedField<T, ?>> getIndexFields() {
209 154 : return indexedFields;
210 : }
211 :
212 : /** Returns all fields in this schema where {@link FieldDef#isStored()} is true. */
213 : public final ImmutableSet<String> getStoredFields() {
214 151 : return storedFields;
215 : }
216 :
217 : /**
218 : * Look up fields in this schema.
219 : *
220 : * @param first the preferred field to look up.
221 : * @param rest additional fields to look up.
222 : * @return the first field in the schema matching {@code first} or {@code rest}, in order, or
223 : * absent if no field matches.
224 : */
225 : @SafeVarargs
226 : public final Optional<SchemaField<T, ?>> getField(
227 : SchemaField<T, ?> first, SchemaField<T, ?>... rest) {
228 0 : SchemaField<T, ?> field = getSchemaField(first);
229 0 : if (field != null) {
230 0 : return Optional.of(checkSame(field, first));
231 : }
232 0 : for (SchemaField<T, ?> f : rest) {
233 0 : field = getSchemaField(first);
234 0 : if (field != null) {
235 0 : return Optional.of(checkSame(field, f));
236 : }
237 : }
238 0 : return Optional.empty();
239 : }
240 :
241 : /**
242 : * Check whether a field is present in this schema.
243 : *
244 : * @param field field to look up.
245 : * @return whether the field is present.
246 : */
247 : public final boolean hasField(SchemaField<T, ?> field) {
248 116 : SchemaField<T, ?> f = getSchemaField(field);
249 116 : if (f == null) {
250 12 : return false;
251 : }
252 116 : checkSame(f, field);
253 116 : return true;
254 : }
255 :
256 : public final boolean hasField(String fieldName) {
257 2 : return this.getSchemaField(fieldName) != null;
258 : }
259 :
260 : private SchemaField<T, ?> getSchemaField(SchemaField<T, ?> field) {
261 116 : return getSchemaField(field.getName());
262 : }
263 :
264 : public SchemaField<T, ?> getSchemaField(String fieldName) {
265 116 : return schemaFields.get(fieldName);
266 : }
267 :
268 : private @Nullable Values<T> fieldValues(
269 : T obj, SchemaField<T, ?> f, ImmutableSet<String> skipFields) {
270 7 : if (skipFields.contains(f.getName())) {
271 4 : return null;
272 : }
273 :
274 : Object v;
275 : try {
276 7 : v = f.get(obj);
277 0 : } catch (StorageException e) {
278 : // StorageException is thrown when the object is not found. On this case,
279 : // it is pointless to make further attempts for each field, so propagate
280 : // the exception to return an empty list.
281 0 : logger.atSevere().withCause(e).log("error getting field %s of %s", f.getName(), obj);
282 0 : throw e;
283 0 : } catch (RuntimeException e) {
284 0 : logger.atSevere().withCause(e).log("error getting field %s of %s", f.getName(), obj);
285 0 : return null;
286 7 : }
287 7 : if (v == null) {
288 7 : return null;
289 7 : } else if (f.isRepeatable()) {
290 7 : return new Values<>(f, (Iterable<?>) v);
291 : } else {
292 7 : return new Values<>(f, Collections.singleton(v));
293 : }
294 : }
295 :
296 : /**
297 : * Build all fields in the schema from an input object.
298 : *
299 : * <p>Null values are omitted, as are fields which cause errors, which are logged.
300 : *
301 : * @param obj input object.
302 : * @param skipFields set of field names to skip when indexing the document
303 : * @return all non-null field values from the object.
304 : */
305 : public final Iterable<Values<T>> buildFields(T obj, ImmutableSet<String> skipFields) {
306 : try {
307 7 : return schemaFields.values().stream()
308 7 : .map(f -> fieldValues(obj, f, skipFields))
309 7 : .filter(Objects::nonNull)
310 7 : .collect(toImmutableList());
311 :
312 0 : } catch (StorageException e) {
313 0 : return ImmutableList.of();
314 : }
315 : }
316 :
317 : @Override
318 : public String toString() {
319 0 : return MoreObjects.toStringHelper(this)
320 0 : .addValue(indexedFields.keySet())
321 0 : .addValue(schemaFields.keySet())
322 0 : .toString();
323 : }
324 : }
|