Create a more flexible and typesafe id for indexed objects (#1785)

Id's can now be combined with more than just a repository. It is now possible to build a more complex Id such as Comment -> Pull request -> Repository. The id's now bound to a specific type. This makes it harder to accidentally use a id within an index of the wrong type.
This commit is contained in:
Sebastian Sdorra
2021-08-31 11:27:49 +02:00
committed by GitHub
parent 58f792a285
commit 571025032c
16 changed files with 430 additions and 204 deletions

View File

@@ -74,7 +74,7 @@ public class GroupIndexer implements Indexer<Group> {
@Override
public SerializableIndexTask<Group> createDeleteTask(Group group) {
return index -> index.delete().byId(Id.of(group));
return index -> index.delete().byId(Id.of(Group.class, group));
}
@Subscribe(async = false)
@@ -83,7 +83,7 @@ public class GroupIndexer implements Indexer<Group> {
}
public static void store(Index<Group> index, Group group) {
index.store(Id.of(group), GroupPermissions.read(group).asShiroString(), group);
index.store(Id.of(Group.class, group), GroupPermissions.read(group).asShiroString(), group);
}
public static class ReIndexAll extends ReIndexAllTask<Group> {

View File

@@ -90,11 +90,17 @@ public class RepositoryIndexer implements Indexer<Repository> {
@Override
public SerializableIndexTask<Repository> createDeleteTask(Repository repository) {
return index -> index.delete().byRepository(repository);
return index -> {
if (Repository.class.equals(index.getDetails().getType())) {
index.delete().byId(Id.of(Repository.class, repository.getId()));
} else {
index.delete().byRepository(repository);
}
};
}
private static void store(Index<Repository> index, Repository repository) {
index.store(Id.of(repository), RepositoryPermissions.read(repository).asShiroString(), repository);
index.store(Id.of(Repository.class, repository), RepositoryPermissions.read(repository).asShiroString(), repository);
}
public static class ReIndexAll extends ReIndexAllTask<Repository> {

View File

@@ -0,0 +1,66 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.search;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
final class Ids {
private Ids() {
}
static Map<String,String> others(Id<?> id) {
Map<String,String> result = new TreeMap<>();
Map<Class<?>, String> others = id.getOthers();
for (Map.Entry<Class<?>, String> e : others.entrySet()) {
result.put(name(e.getKey()), e.getValue());
}
return result;
}
static String id(String mainId, Map<String,String> others) {
List<String> values = new ArrayList<>();
values.add(mainId);
values.addAll(others.values());
return values.stream()
.map(v -> v.replace(";", "\\;" ))
.collect(Collectors.joining(";"));
}
static String asString(Id<?> id) {
return id(id.getMainId(), others(id));
}
private static String name(Class<?> key) {
return Names.create(key, key.getAnnotation(IndexedType.class));
}
}

View File

@@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.Map;
import java.util.function.Supplier;
import static sonia.scm.search.FieldNames.ID;
@@ -72,14 +73,23 @@ class LuceneIndex<T> implements Index<T>, AutoCloseable {
}
@Override
public void store(Id id, String permission, Object object) {
public void store(Id<T> id, String permission, T object) {
Document document = searchableType.getTypeConverter().convert(object);
String mainId = id.getMainId();
Map<String,String> others = Ids.others(id);
try {
field(document, ID, id.asString());
id.getRepository().ifPresent(repository -> field(document, REPOSITORY, repository));
field(document, ID, Ids.id(mainId, others));
for (Map.Entry<String, String> e : others.entrySet()) {
field(document, "_" + e.getKey(), e.getValue());
}
if (!Strings.isNullOrEmpty(permission)) {
field(document, PERMISSION, permission);
}
writer.updateDocument(idTerm(id), document);
} catch (IOException e) {
throw new SearchEngineException("failed to add document to index", e);
@@ -87,16 +97,19 @@ class LuceneIndex<T> implements Index<T>, AutoCloseable {
}
@Nonnull
private Term idTerm(Id id) {
return new Term(ID, id.asString());
private Term idTerm(Id<T> id) {
String mainId = id.getMainId();
Map<String,String> others = Ids.others(id);
return new Term(ID, Ids.id(mainId, others));
}
private void field(Document document, String type, String name) {
document.add(new StringField(type, name, Field.Store.YES));
}
@Override
public Deleter delete() {
public Deleter<T> delete() {
return new LuceneDeleter();
}
@@ -109,10 +122,10 @@ class LuceneIndex<T> implements Index<T>, AutoCloseable {
}
}
private class LuceneDeleter implements Deleter {
private class LuceneDeleter implements Deleter<T> {
@Override
public void byId(Id id) {
public void byId(Id<T> id) {
try {
long count = writer.deleteDocuments(idTerm(id));
LOG.debug("delete {} document(s) by id {} from index {}", count, id, details);

View File

@@ -28,6 +28,8 @@ import com.google.common.base.Strings;
import lombok.Value;
import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -50,9 +52,9 @@ public class LuceneSearchableType implements SearchableType {
Map<String, PointsConfig> pointsConfig;
TypeConverter typeConverter;
public LuceneSearchableType(Class<?> type, IndexedType annotation, List<LuceneSearchableField> fields) {
public LuceneSearchableType(Class<?> type, @Nonnull IndexedType annotation, List<LuceneSearchableField> fields) {
this.type = type;
this.name = name(type, annotation);
this.name = Names.create(type, annotation);
this.permission = Strings.emptyToNull(annotation.permission());
this.fields = fields;
this.fieldNames = fieldNames(fields);
@@ -65,15 +67,6 @@ public class LuceneSearchableType implements SearchableType {
return Optional.ofNullable(permission);
}
private String name(Class<?> type, IndexedType annotation) {
String nameFromAnnotation = annotation.value();
if (Strings.isNullOrEmpty(nameFromAnnotation)) {
String simpleName = type.getSimpleName();
return Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1);
}
return nameFromAnnotation;
}
private String[] fieldNames(List<LuceneSearchableField> fields) {
return fields.stream()
.filter(LuceneSearchableField::isDefaultQuery)

View File

@@ -0,0 +1,51 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.search;
import com.google.common.base.Strings;
import javax.annotation.Nullable;
final class Names {
private Names() {
}
static String create(Class<?> type) {
return create(type, type.getAnnotation(IndexedType.class));
}
static String create(Class<?> type, @Nullable IndexedType annotation) {
String nameFromAnnotation = null;
if (annotation != null) {
nameFromAnnotation = annotation.value();
}
if (Strings.isNullOrEmpty(nameFromAnnotation)) {
String simpleName = type.getSimpleName();
return Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1);
}
return nameFromAnnotation;
}
}

View File

@@ -74,7 +74,7 @@ public class UserIndexer implements Indexer<User> {
@Override
public SerializableIndexTask<User> createDeleteTask(User item) {
return index -> index.delete().byId(Id.of(item));
return index -> index.delete().byId(Id.of(User.class, item));
}
@Subscribe(async = false)
@@ -83,7 +83,7 @@ public class UserIndexer implements Indexer<User> {
}
private static void store(Index<User> index, User user) {
index.store(Id.of(user), UserPermissions.read(user).asShiroString(), user);
index.store(Id.of(User.class, user), UserPermissions.read(user).asShiroString(), user);
}
public static class ReIndexAll extends ReIndexAllTask<User> {