mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-03 05:09:10 +01:00
Prepare search api for different types (#1732)
We introduced a new annotation '@IndexedType' which gets collected by the scm-annotation-processor. All classes which are annotated are index and searchable. This opens the search api for plugins.
This commit is contained in:
@@ -132,7 +132,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self()));
|
||||
builder.single(link("importLog", resourceLinks.repository().importLog("IMPORT_LOG_ID").replace("IMPORT_LOG_ID", "{logId}")));
|
||||
|
||||
builder.single(link("search", resourceLinks.search().search()));
|
||||
builder.single(link("search", resourceLinks.search().search("INDEXED_TYPE").replace("INDEXED_TYPE", "{type}")));
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
@@ -1125,8 +1125,8 @@ class ResourceLinks {
|
||||
this.searchLinkBuilder = new LinkBuilder(pathInfo, SearchResource.class);
|
||||
}
|
||||
|
||||
public String search() {
|
||||
return searchLinkBuilder.method("search").parameters().href();
|
||||
public String search(String type) {
|
||||
return searchLinkBuilder.method("search").parameters(type).href();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.Size;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
@@ -55,6 +56,9 @@ public class SearchParameters {
|
||||
@DefaultValue("10")
|
||||
private int pageSize = 10;
|
||||
|
||||
@PathParam("type")
|
||||
private String type;
|
||||
|
||||
String getSelfLink() {
|
||||
return uriInfo.getAbsolutePath().toASCIIString();
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.search.IndexNames;
|
||||
import sonia.scm.search.QueryResult;
|
||||
import sonia.scm.search.SearchEngine;
|
||||
@@ -62,7 +61,7 @@ public class SearchResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@Path("{type}")
|
||||
@Produces(VndMediaType.QUERY_RESULT)
|
||||
@Operation(
|
||||
summary = "Query result",
|
||||
@@ -103,7 +102,7 @@ public class SearchResource {
|
||||
QueryResult result = engine.search(IndexNames.DEFAULT)
|
||||
.start(params.getPage() * params.getPageSize())
|
||||
.limit(params.getPageSize())
|
||||
.execute(Repository.class, params.getQuery());
|
||||
.execute(params.getType(), params.getQuery());
|
||||
|
||||
return mapper.map(params, result);
|
||||
}
|
||||
|
||||
@@ -120,6 +120,11 @@ public class DefaultExtensionProcessor implements ExtensionProcessor
|
||||
return collector.getWebElements();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Class<?>> getIndexedTypes() {
|
||||
return collector.getIndexedTypes();
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
|
||||
@@ -223,6 +223,10 @@ public final class ExtensionCollector
|
||||
return webElements;
|
||||
}
|
||||
|
||||
public Set<Class<?>> getIndexedTypes() {
|
||||
return indexedTypes;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -255,7 +259,7 @@ public final class ExtensionCollector
|
||||
private void collectExtensions(ClassLoader defaultClassLoader, ScmModule module) {
|
||||
for (ClassElement extension : module.getExtensions()) {
|
||||
if (isRequirementFulfilled(extension)) {
|
||||
Class<?> extensionClass = loadExtension(defaultClassLoader, extension);
|
||||
Class<?> extensionClass = load(defaultClassLoader, extension);
|
||||
appendExtension(extensionClass);
|
||||
}
|
||||
}
|
||||
@@ -265,25 +269,34 @@ public final class ExtensionCollector
|
||||
Set<Class<?>> classes = new HashSet<>();
|
||||
for (ClassElement element : classElements) {
|
||||
if (isRequirementFulfilled(element)) {
|
||||
Class<?> loadedClass = loadExtension(defaultClassLoader, element);
|
||||
Class<?> loadedClass = load(defaultClassLoader, element);
|
||||
classes.add(loadedClass);
|
||||
}
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
private Collection<Class<?>> collectIndexedTypes(ClassLoader defaultClassLoader, Iterable<ClassElement> descriptors) {
|
||||
Set<Class<?>> types = new HashSet<>();
|
||||
for (ClassElement descriptor : descriptors) {
|
||||
Class<?> loadedClass = load(defaultClassLoader, descriptor);
|
||||
types.add(loadedClass);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
private Set<WebElementExtension> collectWebElementExtensions(ClassLoader defaultClassLoader, Iterable<WebElementDescriptor> descriptors) {
|
||||
Set<WebElementExtension> webElementExtensions = new HashSet<>();
|
||||
for (WebElementDescriptor descriptor : descriptors) {
|
||||
if (isRequirementFulfilled(descriptor)) {
|
||||
Class<?> loadedClass = loadExtension(defaultClassLoader, descriptor);
|
||||
Class<?> loadedClass = load(defaultClassLoader, descriptor);
|
||||
webElementExtensions.add(new WebElementExtension(loadedClass, descriptor));
|
||||
}
|
||||
}
|
||||
return webElementExtensions;
|
||||
}
|
||||
|
||||
private Class<?> loadExtension(ClassLoader classLoader, ClassElement extension) {
|
||||
private Class<?> load(ClassLoader classLoader, ClassElement extension) {
|
||||
try {
|
||||
return classLoader.loadClass(extension.getClazz());
|
||||
} catch (ClassNotFoundException ex) {
|
||||
@@ -320,6 +333,7 @@ public final class ExtensionCollector
|
||||
restResources.addAll(collectClasses(classLoader, module.getRestResources()));
|
||||
|
||||
webElements.addAll(collectWebElementExtensions(classLoader, module.getWebElements()));
|
||||
indexedTypes.addAll(collectIndexedTypes(classLoader, module.getIndexedTypes()));
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
@@ -327,6 +341,8 @@ public final class ExtensionCollector
|
||||
/** Field description */
|
||||
private final Set<WebElementExtension> webElements = Sets.newHashSet();
|
||||
|
||||
private final Set<Class<?>> indexedTypes = Sets.newHashSet();
|
||||
|
||||
/** Field description */
|
||||
private final Set<Class> restResources = Sets.newHashSet();
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ public class IndexUpdateListener implements ServletContextListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IndexUpdateListener.class);
|
||||
|
||||
@VisibleForTesting
|
||||
static final int INDEX_VERSION = 1;
|
||||
static final int INDEX_VERSION = 2;
|
||||
|
||||
private final AdministrationContext administrationContext;
|
||||
private final IndexQueue queue;
|
||||
@@ -87,13 +87,23 @@ public class IndexUpdateListener implements ServletContextListener {
|
||||
@Override
|
||||
public void contextInitialized(ServletContextEvent servletContextEvent) {
|
||||
Optional<IndexLog> indexLog = indexLogStore.get(IndexNames.DEFAULT, Repository.class);
|
||||
if (!indexLog.isPresent()) {
|
||||
if (indexLog.isPresent()) {
|
||||
int version = indexLog.get().getVersion();
|
||||
if (version < INDEX_VERSION) {
|
||||
LOG.debug("repository index {} is older then {}, start reindexing of all repositories", version, INDEX_VERSION);
|
||||
indexAll();
|
||||
}
|
||||
} else {
|
||||
LOG.debug("could not find log entry for repository index, start reindexing of all repositories");
|
||||
administrationContext.runAsAdmin(ReIndexAll.class);
|
||||
indexLogStore.log(IndexNames.DEFAULT, Repository.class, INDEX_VERSION);
|
||||
indexAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void indexAll() {
|
||||
administrationContext.runAsAdmin(ReIndexAll.class);
|
||||
indexLogStore.log(IndexNames.DEFAULT, Repository.class, INDEX_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextDestroyed(ServletContextEvent servletContextEvent) {
|
||||
// we have nothing to destroy
|
||||
@@ -117,6 +127,8 @@ public class IndexUpdateListener implements ServletContextListener {
|
||||
@Override
|
||||
public void run() {
|
||||
try (Index index = queue.getQueuedIndex(IndexNames.DEFAULT)) {
|
||||
// delete v1 types
|
||||
index.deleteByTypeName(Repository.class.getName());
|
||||
for (Repository repository : repositoryManager.getAll()) {
|
||||
store(index, repository);
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
/*
|
||||
* 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 org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.index.IndexableField;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
|
||||
@Singleton
|
||||
public class DocumentConverter {
|
||||
|
||||
private final Map<Class<?>, TypeConverter> typeConverter = new ConcurrentHashMap<>();
|
||||
|
||||
Document convert(Object object) {
|
||||
TypeConverter converter = typeConverter.computeIfAbsent(object.getClass(), this::createTypeConverter);
|
||||
try {
|
||||
return converter.convert(object);
|
||||
} catch (IllegalAccessException | InvocationTargetException ex) {
|
||||
throw new SearchEngineException("failed to create document", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private TypeConverter createTypeConverter(Class<?> type) {
|
||||
List<FieldConverter> fieldConverters = new ArrayList<>();
|
||||
collectFields(fieldConverters, type);
|
||||
return new TypeConverter(fieldConverters);
|
||||
}
|
||||
|
||||
private void collectFields(List<FieldConverter> fieldConverters, Class<?> type) {
|
||||
Class<?> parent = type.getSuperclass();
|
||||
if (parent != null) {
|
||||
collectFields(fieldConverters, parent);
|
||||
}
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
Indexed indexed = field.getAnnotation(Indexed.class);
|
||||
if (indexed != null) {
|
||||
IndexableFieldFactory fieldFactory = IndexableFields.create(field, indexed);
|
||||
Method getter = findGetter(type, field);
|
||||
fieldConverters.add(new FieldConverter(field, getter, indexed, fieldFactory));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Method findGetter(Class<?> type, Field field) {
|
||||
String name = createGetterName(field);
|
||||
try {
|
||||
return type.getMethod(name);
|
||||
} catch (NoSuchMethodException ex) {
|
||||
throw new NonReadableFieldException("could not find getter for field", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String createGetterName(Field field) {
|
||||
String fieldName = field.getName();
|
||||
String prefix = "get";
|
||||
if (field.getType() == Boolean.TYPE) {
|
||||
prefix = "is";
|
||||
}
|
||||
return prefix + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1);
|
||||
}
|
||||
|
||||
private static class TypeConverter {
|
||||
|
||||
private final List<FieldConverter> fieldConverters;
|
||||
|
||||
private TypeConverter(List<FieldConverter> fieldConverters) {
|
||||
this.fieldConverters = fieldConverters;
|
||||
}
|
||||
|
||||
public Document convert(Object object) throws IllegalAccessException, InvocationTargetException {
|
||||
Document document = new Document();
|
||||
for (FieldConverter fieldConverter : fieldConverters) {
|
||||
for (IndexableField field : fieldConverter.convert(object)) {
|
||||
document.add(field);
|
||||
}
|
||||
}
|
||||
return document;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FieldConverter {
|
||||
|
||||
private final Method getter;
|
||||
private final IndexableFieldFactory fieldFactory;
|
||||
private final String name;
|
||||
|
||||
private FieldConverter(Field field, Method getter, Indexed indexed, IndexableFieldFactory fieldFactory) {
|
||||
this.getter = getter;
|
||||
this.fieldFactory = fieldFactory;
|
||||
this.name = createName(field, indexed);
|
||||
}
|
||||
|
||||
private String createName(Field field, Indexed indexed) {
|
||||
String nameFromAnnotation = indexed.name();
|
||||
if (Strings.isNullOrEmpty(nameFromAnnotation)) {
|
||||
return field.getName();
|
||||
}
|
||||
return nameFromAnnotation;
|
||||
}
|
||||
|
||||
Iterable<IndexableField> convert(Object object) throws IllegalAccessException, InvocationTargetException {
|
||||
Object value = getter.invoke(object);
|
||||
if (value != null) {
|
||||
return fieldFactory.create(name, value);
|
||||
}
|
||||
return emptySet();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 org.apache.lucene.index.IndexableField;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
|
||||
final class FieldConverter {
|
||||
|
||||
private final Method getter;
|
||||
private final IndexableFieldFactory fieldFactory;
|
||||
private final String name;
|
||||
|
||||
FieldConverter(Field field, Method getter, Indexed indexed, IndexableFieldFactory fieldFactory) {
|
||||
this.getter = getter;
|
||||
this.fieldFactory = fieldFactory;
|
||||
this.name = createName(field, indexed);
|
||||
}
|
||||
|
||||
private String createName(Field field, Indexed indexed) {
|
||||
String nameFromAnnotation = indexed.name();
|
||||
if (Strings.isNullOrEmpty(nameFromAnnotation)) {
|
||||
return field.getName();
|
||||
}
|
||||
return nameFromAnnotation;
|
||||
}
|
||||
|
||||
Iterable<IndexableField> convert(Object object) throws IllegalAccessException, InvocationTargetException {
|
||||
Object value = getter.invoke(object);
|
||||
if (value != null) {
|
||||
return fieldFactory.create(name, value);
|
||||
}
|
||||
return emptySet();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,27 +33,32 @@ import org.apache.lucene.index.Term;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static sonia.scm.search.FieldNames.*;
|
||||
import static sonia.scm.search.FieldNames.ID;
|
||||
import static sonia.scm.search.FieldNames.PERMISSION;
|
||||
import static sonia.scm.search.FieldNames.REPOSITORY;
|
||||
import static sonia.scm.search.FieldNames.TYPE;
|
||||
import static sonia.scm.search.FieldNames.UID;
|
||||
|
||||
public class LuceneIndex implements Index {
|
||||
|
||||
private final DocumentConverter converter;
|
||||
private final SearchableTypeResolver resolver;
|
||||
private final IndexWriter writer;
|
||||
|
||||
LuceneIndex(DocumentConverter converter, IndexWriter writer) {
|
||||
this.converter = converter;
|
||||
LuceneIndex(SearchableTypeResolver resolver, IndexWriter writer) {
|
||||
this.resolver = resolver;
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(Id id, String permission, Object object) {
|
||||
String uid = createUid(id, object.getClass());
|
||||
Document document = converter.convert(object);
|
||||
SearchableType type = resolver.resolve(object);
|
||||
String uid = createUid(id, type);
|
||||
Document document = type.getTypeConverter().convert(object);
|
||||
try {
|
||||
field(document, UID, uid);
|
||||
field(document, ID, id.getValue());
|
||||
id.getRepository().ifPresent(repository -> field(document, REPOSITORY, repository));
|
||||
field(document, TYPE, object.getClass().getName());
|
||||
field(document, TYPE, type.getName());
|
||||
if (!Strings.isNullOrEmpty(permission)) {
|
||||
field(document, PERMISSION, permission);
|
||||
}
|
||||
@@ -63,7 +68,7 @@ public class LuceneIndex implements Index {
|
||||
}
|
||||
}
|
||||
|
||||
private String createUid(Id id, Class<?> type) {
|
||||
private String createUid(Id id, SearchableType type) {
|
||||
return id.asString() + "/" + type.getName();
|
||||
}
|
||||
|
||||
@@ -73,8 +78,9 @@ public class LuceneIndex implements Index {
|
||||
|
||||
@Override
|
||||
public void delete(Id id, Class<?> type) {
|
||||
SearchableType searchableType = resolver.resolve(type);
|
||||
try {
|
||||
writer.deleteDocuments(new Term(UID, createUid(id, type)));
|
||||
writer.deleteDocuments(new Term(UID, createUid(id, searchableType)));
|
||||
} catch (IOException e) {
|
||||
throw new SearchEngineException("failed to delete document from index", e);
|
||||
}
|
||||
@@ -91,10 +97,16 @@ public class LuceneIndex implements Index {
|
||||
|
||||
@Override
|
||||
public void deleteByType(Class<?> type) {
|
||||
SearchableType searchableType = resolver.resolve(type);
|
||||
deleteByTypeName(searchableType.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByTypeName(String typeName) {
|
||||
try {
|
||||
writer.deleteDocuments(new Term(TYPE, type.getName()));
|
||||
writer.deleteDocuments(new Term(TYPE, typeName));
|
||||
} catch (IOException ex) {
|
||||
throw new SearchEngineException("failed to delete documents by repository " + type + " from index", ex);
|
||||
throw new SearchEngineException("failed to delete documents by repository " + typeName + " from index", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
|
||||
public class LuceneIndexFactory {
|
||||
|
||||
private final SearchableTypeResolver typeResolver;
|
||||
private final IndexOpener indexOpener;
|
||||
|
||||
@Inject
|
||||
public LuceneIndexFactory(SearchableTypeResolver typeResolver, IndexOpener indexOpener) {
|
||||
this.typeResolver = typeResolver;
|
||||
this.indexOpener = indexOpener;
|
||||
}
|
||||
|
||||
public LuceneIndex create(String name, IndexOptions options) {
|
||||
try {
|
||||
return new LuceneIndex(typeResolver, indexOpener.openForWrite(name, options));
|
||||
} catch (IOException ex) {
|
||||
throw new SearchEngineException("failed to open index " + name, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,32 +46,37 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Optional;
|
||||
|
||||
public class LuceneQueryBuilder extends QueryBuilder {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LuceneQueryBuilder.class);
|
||||
|
||||
private static final Map<Class<?>, SearchableType> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private final IndexOpener opener;
|
||||
private final SearchableTypeResolver resolver;
|
||||
private final String indexName;
|
||||
private final Analyzer analyzer;
|
||||
|
||||
LuceneQueryBuilder(IndexOpener opener, String indexName, Analyzer analyzer) {
|
||||
LuceneQueryBuilder(IndexOpener opener, SearchableTypeResolver resolver, String indexName, Analyzer analyzer) {
|
||||
this.opener = opener;
|
||||
this.resolver = resolver;
|
||||
this.indexName = indexName;
|
||||
this.analyzer = analyzer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<Class<?>> resolveByName(String typeName) {
|
||||
return resolver.resolveClassByName(typeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected QueryResult execute(QueryParams queryParams) {
|
||||
String queryString = Strings.nullToEmpty(queryParams.getQueryString());
|
||||
|
||||
SearchableType searchableType = CACHE.computeIfAbsent(queryParams.getType(), SearchableTypes::create);
|
||||
SearchableType searchableType = resolver.resolve(queryParams.getType());
|
||||
|
||||
Query query = Queries.filter(createQuery(searchableType, queryParams, queryString), queryParams);
|
||||
Query parsedQuery = createQuery(searchableType, queryParams, queryString);
|
||||
Query query = Queries.filter(parsedQuery, searchableType, queryParams);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("execute lucene query: {}", query);
|
||||
}
|
||||
|
||||
@@ -29,16 +29,18 @@ import javax.inject.Inject;
|
||||
public class LuceneQueryBuilderFactory {
|
||||
|
||||
private final IndexOpener indexOpener;
|
||||
private final SearchableTypeResolver searchableTypeResolver;
|
||||
private final AnalyzerFactory analyzerFactory;
|
||||
|
||||
@Inject
|
||||
public LuceneQueryBuilderFactory(IndexOpener indexOpener, AnalyzerFactory analyzerFactory) {
|
||||
public LuceneQueryBuilderFactory(IndexOpener indexOpener, SearchableTypeResolver searchableTypeResolver, AnalyzerFactory analyzerFactory) {
|
||||
this.indexOpener = indexOpener;
|
||||
this.searchableTypeResolver = searchableTypeResolver;
|
||||
this.analyzerFactory = analyzerFactory;
|
||||
}
|
||||
|
||||
public LuceneQueryBuilder create(String name, IndexOptions options) {
|
||||
return new LuceneQueryBuilder(indexOpener, name, analyzerFactory.create(options));
|
||||
return new LuceneQueryBuilder(indexOpener, searchableTypeResolver, name, analyzerFactory.create(options));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,28 +25,21 @@
|
||||
package sonia.scm.search;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
|
||||
public class LuceneSearchEngine implements SearchEngine {
|
||||
|
||||
private final IndexOpener indexOpener;
|
||||
private final DocumentConverter converter;
|
||||
private final LuceneIndexFactory indexFactory;
|
||||
private final LuceneQueryBuilderFactory queryBuilderFactory;
|
||||
|
||||
@Inject
|
||||
public LuceneSearchEngine(IndexOpener indexOpener, DocumentConverter converter, LuceneQueryBuilderFactory queryBuilderFactory) {
|
||||
this.indexOpener = indexOpener;
|
||||
this.converter = converter;
|
||||
public LuceneSearchEngine(LuceneIndexFactory indexFactory, LuceneQueryBuilderFactory queryBuilderFactory) {
|
||||
this.indexFactory = indexFactory;
|
||||
this.queryBuilderFactory = queryBuilderFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index getOrCreate(String name, IndexOptions options) {
|
||||
try {
|
||||
return new LuceneIndex(converter, indexOpener.openForWrite(name, options));
|
||||
} catch (IOException ex) {
|
||||
throw new SearchEngineException("failed to open index", ex);
|
||||
}
|
||||
return indexFactory.create(name, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -36,7 +36,7 @@ final class Queries {
|
||||
private Queries() {
|
||||
}
|
||||
|
||||
private static Query typeQuery(Class<?> type) {
|
||||
private static Query typeQuery(SearchableType type) {
|
||||
return new TermQuery(new Term(FieldNames.TYPE, type.getName()));
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@ final class Queries {
|
||||
return new TermQuery(new Term(FieldNames.REPOSITORY, repositoryId));
|
||||
}
|
||||
|
||||
static Query filter(Query query, QueryBuilder.QueryParams params) {
|
||||
static Query filter(Query query, SearchableType searchableType, QueryBuilder.QueryParams params) {
|
||||
BooleanQuery.Builder builder = new BooleanQuery.Builder()
|
||||
.add(query, MUST)
|
||||
.add(typeQuery(params.getType()), MUST);
|
||||
.add(typeQuery(searchableType), MUST);
|
||||
params.getRepositoryId().ifPresent(repo -> builder.add(repositoryQuery(repo), MUST));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ public class QueuedIndex implements Index {
|
||||
tasks.add(index -> index.deleteByType(type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByTypeName(String typeName) {
|
||||
tasks.add(index -> index.deleteByTypeName(typeName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
IndexQueueTaskWrapper wrappedTask = new IndexQueueTaskWrapper(
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import lombok.Value;
|
||||
import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
|
||||
|
||||
@@ -35,17 +36,40 @@ import java.util.Map;
|
||||
public class SearchableType {
|
||||
|
||||
Class<?> type;
|
||||
String name;
|
||||
String[] fieldNames;
|
||||
Map<String,Float> boosts;
|
||||
Map<String, PointsConfig> pointsConfig;
|
||||
List<SearchableField> fields;
|
||||
TypeConverter typeConverter;
|
||||
|
||||
SearchableType(Class<?> type, String[] fieldNames, Map<String, Float> boosts, Map<String, PointsConfig> pointsConfig, List<SearchableField> fields) {
|
||||
SearchableType(Class<?> type,
|
||||
String[] fieldNames,
|
||||
Map<String, Float> boosts,
|
||||
Map<String, PointsConfig> pointsConfig,
|
||||
List<SearchableField> fields,
|
||||
TypeConverter typeConverter) {
|
||||
this.type = type;
|
||||
this.name = name(type);
|
||||
this.fieldNames = fieldNames;
|
||||
this.boosts = Collections.unmodifiableMap(boosts);
|
||||
this.pointsConfig = Collections.unmodifiableMap(pointsConfig);
|
||||
this.fields = Collections.unmodifiableList(fields);
|
||||
this.typeConverter = typeConverter;
|
||||
}
|
||||
|
||||
private String name(Class<?> type) {
|
||||
IndexedType annotation = type.getAnnotation(IndexedType.class);
|
||||
if (annotation == null) {
|
||||
throw new IllegalArgumentException(
|
||||
type.getName() + " has no " + IndexedType.class.getSimpleName() + " annotation"
|
||||
);
|
||||
}
|
||||
String nameFromAnnotation = annotation.value();
|
||||
if (Strings.isNullOrEmpty(nameFromAnnotation)) {
|
||||
String simpleName = type.getSimpleName();
|
||||
return Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1);
|
||||
}
|
||||
return nameFromAnnotation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.annotations.VisibleForTesting;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
@Singleton
|
||||
class SearchableTypeResolver {
|
||||
|
||||
private final Map<Class<?>, SearchableType> classToSearchableType = new HashMap<>();
|
||||
private final Map<String, Class<?>> nameToClass = new HashMap<>();
|
||||
|
||||
@Inject
|
||||
public SearchableTypeResolver(PluginLoader pluginLoader) {
|
||||
this(pluginLoader.getExtensionProcessor().getIndexedTypes());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
SearchableTypeResolver(Class<?>... indexedTypes) {
|
||||
this(Arrays.asList(indexedTypes));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
SearchableTypeResolver(Iterable<Class<?>> indexedTypes) {
|
||||
fillMaps(convert(indexedTypes));
|
||||
}
|
||||
|
||||
private void fillMaps(Iterable<SearchableType> types) {
|
||||
for (SearchableType type : types) {
|
||||
classToSearchableType.put(type.getType(), type);
|
||||
nameToClass.put(type.getName(), type.getType());
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Set<SearchableType> convert(Iterable<Class<?>> indexedTypes) {
|
||||
return StreamSupport.stream(indexedTypes.spliterator(), false)
|
||||
.map(SearchableTypes::create)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public SearchableType resolve(Object object) {
|
||||
return resolve(object.getClass());
|
||||
}
|
||||
|
||||
public SearchableType resolve(Class<?> type) {
|
||||
SearchableType searchableType = classToSearchableType.get(type);
|
||||
if (searchableType == null) {
|
||||
throw notFound(entity("type", type.getName()));
|
||||
}
|
||||
return searchableType;
|
||||
}
|
||||
|
||||
public Optional<Class<?>> resolveClassByName(String typeName) {
|
||||
return Optional.ofNullable(nameToClass.get(typeName));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,7 +63,7 @@ final class SearchableTypes {
|
||||
}
|
||||
}
|
||||
|
||||
return new SearchableType(type, fieldsNames, boosts, pointsConfig, fields);
|
||||
return new SearchableType(type, fieldsNames, boosts, pointsConfig, fields, TypeConverters.create(type));
|
||||
}
|
||||
|
||||
private static void collectFields(Class<?> type, List<SearchableField> fields) {
|
||||
|
||||
61
scm-webapp/src/main/java/sonia/scm/search/TypeConverter.java
Normal file
61
scm-webapp/src/main/java/sonia/scm/search/TypeConverter.java
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.index.IndexableField;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
final class TypeConverter {
|
||||
|
||||
private final List<FieldConverter> fieldConverters;
|
||||
|
||||
TypeConverter(List<FieldConverter> fieldConverters) {
|
||||
this.fieldConverters = fieldConverters;
|
||||
}
|
||||
|
||||
public Document convert(Object object) {
|
||||
try {
|
||||
return doConversion(object);
|
||||
} catch (IllegalAccessException | InvocationTargetException ex) {
|
||||
throw new SearchEngineException("failed to create document", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Document doConversion(Object object) throws IllegalAccessException, InvocationTargetException {
|
||||
Document document = new Document();
|
||||
for (FieldConverter fieldConverter : fieldConverters) {
|
||||
for (IndexableField field : fieldConverter.convert(object)) {
|
||||
document.add(field);
|
||||
}
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
final class TypeConverters {
|
||||
|
||||
private TypeConverters() {
|
||||
}
|
||||
|
||||
static TypeConverter create(Class<?> type) {
|
||||
List<FieldConverter> fieldConverters = new ArrayList<>();
|
||||
collectFields(fieldConverters, type);
|
||||
return new TypeConverter(fieldConverters);
|
||||
}
|
||||
|
||||
private static void collectFields(List<FieldConverter> fieldConverters, Class<?> type) {
|
||||
Class<?> parent = type.getSuperclass();
|
||||
if (parent != null) {
|
||||
collectFields(fieldConverters, parent);
|
||||
}
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
Indexed indexed = field.getAnnotation(Indexed.class);
|
||||
if (indexed != null) {
|
||||
IndexableFieldFactory fieldFactory = IndexableFields.create(field, indexed);
|
||||
Method getter = findGetter(type, field);
|
||||
fieldConverters.add(new FieldConverter(field, getter, indexed, fieldFactory));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Method findGetter(Class<?> type, Field field) {
|
||||
String name = createGetterName(field);
|
||||
try {
|
||||
return type.getMethod(name);
|
||||
} catch (NoSuchMethodException ex) {
|
||||
throw new NonReadableFieldException("could not find getter for field", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String createGetterName(Field field) {
|
||||
String fieldName = field.getName();
|
||||
String prefix = "get";
|
||||
if (field.getType() == Boolean.TYPE) {
|
||||
prefix = "is";
|
||||
}
|
||||
return prefix + fieldName.substring(0, 1).toUpperCase(Locale.ENGLISH) + fieldName.substring(1);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user