mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-22 22:50:11 +01:00
Return separate links for searchable types instead of single templated link (#1733)
The search link of the index resource is now an array of links instead of single templated link. The array contains one link for each searchable type. Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
@@ -37,6 +37,8 @@ import sonia.scm.group.GroupPermissions;
|
||||
import sonia.scm.initialization.InitializationFinisher;
|
||||
import sonia.scm.initialization.InitializationStep;
|
||||
import sonia.scm.plugin.PluginPermissions;
|
||||
import sonia.scm.search.SearchEngine;
|
||||
import sonia.scm.search.SearchableType;
|
||||
import sonia.scm.security.AnonymousMode;
|
||||
import sonia.scm.security.Authentications;
|
||||
import sonia.scm.security.PermissionPermissions;
|
||||
@@ -45,9 +47,11 @@ import sonia.scm.web.EdisonHalAppender;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static de.otto.edison.hal.Embedded.embeddedBuilder;
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Link.linkBuilder;
|
||||
|
||||
public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
|
||||
@@ -55,13 +59,19 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
private final SCMContextProvider scmContextProvider;
|
||||
private final ScmConfiguration configuration;
|
||||
private final InitializationFinisher initializationFinisher;
|
||||
private final SearchEngine searchEngine;
|
||||
|
||||
@Inject
|
||||
public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider, ScmConfiguration configuration, InitializationFinisher initializationFinisher) {
|
||||
public IndexDtoGenerator(ResourceLinks resourceLinks,
|
||||
SCMContextProvider scmContextProvider,
|
||||
ScmConfiguration configuration,
|
||||
InitializationFinisher initializationFinisher,
|
||||
SearchEngine searchEngine) {
|
||||
this.resourceLinks = resourceLinks;
|
||||
this.scmContextProvider = scmContextProvider;
|
||||
this.configuration = configuration;
|
||||
this.initializationFinisher = initializationFinisher;
|
||||
this.searchEngine = searchEngine;
|
||||
}
|
||||
|
||||
public IndexDto generate() {
|
||||
@@ -132,7 +142,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("INDEXED_TYPE").replace("INDEXED_TYPE", "{type}")));
|
||||
builder.array(searchLinks());
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
@@ -141,6 +151,15 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
||||
return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion());
|
||||
}
|
||||
|
||||
private List<Link> searchLinks() {
|
||||
return searchEngine.getSearchableTypes().stream()
|
||||
.map(SearchableType::getName)
|
||||
.map(typeName ->
|
||||
linkBuilder("search", resourceLinks.search().query(typeName)).withName(typeName).build()
|
||||
)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private IndexDto handleInitialization(Links.Builder builder, Embedded.Builder embeddedBuilder) {
|
||||
Links.Builder initializationLinkBuilder = Links.linkingTo();
|
||||
Embedded.Builder initializationEmbeddedBuilder = embeddedBuilder();
|
||||
|
||||
@@ -1125,8 +1125,8 @@ class ResourceLinks {
|
||||
this.searchLinkBuilder = new LinkBuilder(pathInfo, SearchResource.class);
|
||||
}
|
||||
|
||||
public String search(String type) {
|
||||
return searchLinkBuilder.method("search").parameters(type).href();
|
||||
public String query(String type) {
|
||||
return searchLinkBuilder.method("query").parameters(type).href();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ public class SearchResource {
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{type}")
|
||||
@Path("query/{type}")
|
||||
@Produces(VndMediaType.QUERY_RESULT)
|
||||
@Operation(
|
||||
summary = "Query result",
|
||||
@@ -98,7 +98,7 @@ public class SearchResource {
|
||||
name = "pageSize",
|
||||
description = "The maximum number of results per page (defaults to 10)"
|
||||
)
|
||||
public QueryResultDto search(@Valid @BeanParam SearchParameters params) {
|
||||
public QueryResultDto query(@Valid @BeanParam SearchParameters params) {
|
||||
QueryResult result = engine.search(IndexNames.DEFAULT)
|
||||
.start(params.getPage() * params.getPageSize())
|
||||
.limit(params.getPageSize())
|
||||
|
||||
@@ -51,7 +51,7 @@ public class LuceneIndex implements Index {
|
||||
|
||||
@Override
|
||||
public void store(Id id, String permission, Object object) {
|
||||
SearchableType type = resolver.resolve(object);
|
||||
LuceneSearchableType type = resolver.resolve(object);
|
||||
String uid = createUid(id, type);
|
||||
Document document = type.getTypeConverter().convert(object);
|
||||
try {
|
||||
@@ -68,7 +68,7 @@ public class LuceneIndex implements Index {
|
||||
}
|
||||
}
|
||||
|
||||
private String createUid(Id id, SearchableType type) {
|
||||
private String createUid(Id id, LuceneSearchableType type) {
|
||||
return id.asString() + "/" + type.getName();
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public class LuceneIndex implements Index {
|
||||
|
||||
@Override
|
||||
public void delete(Id id, Class<?> type) {
|
||||
SearchableType searchableType = resolver.resolve(type);
|
||||
LuceneSearchableType searchableType = resolver.resolve(type);
|
||||
try {
|
||||
writer.deleteDocuments(new Term(UID, createUid(id, searchableType)));
|
||||
} catch (IOException e) {
|
||||
@@ -97,7 +97,7 @@ public class LuceneIndex implements Index {
|
||||
|
||||
@Override
|
||||
public void deleteByType(Class<?> type) {
|
||||
SearchableType searchableType = resolver.resolve(type);
|
||||
LuceneSearchableType searchableType = resolver.resolve(type);
|
||||
deleteByTypeName(searchableType.getName());
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.apache.lucene.search.TopDocs;
|
||||
import org.apache.lucene.search.TopScoreDocCollector;
|
||||
import org.apache.lucene.search.WildcardQuery;
|
||||
import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -73,7 +74,10 @@ public class LuceneQueryBuilder extends QueryBuilder {
|
||||
protected QueryResult execute(QueryParams queryParams) {
|
||||
String queryString = Strings.nullToEmpty(queryParams.getQueryString());
|
||||
|
||||
SearchableType searchableType = resolver.resolve(queryParams.getType());
|
||||
LuceneSearchableType searchableType = resolver.resolve(queryParams.getType());
|
||||
searchableType.getPermission().ifPresent(
|
||||
permission -> SecurityUtils.getSubject().checkPermission(permission)
|
||||
);
|
||||
|
||||
Query parsedQuery = createQuery(searchableType, queryParams, queryString);
|
||||
Query query = Queries.filter(parsedQuery, searchableType, queryParams);
|
||||
@@ -105,7 +109,7 @@ public class LuceneQueryBuilder extends QueryBuilder {
|
||||
return topScoreCollector.topDocs(queryParams.getStart(), queryParams.getLimit());
|
||||
}
|
||||
|
||||
private Query createQuery(SearchableType searchableType, QueryParams queryParams, String queryString) {
|
||||
private Query createQuery(LuceneSearchableType searchableType, QueryParams queryParams, String queryString) {
|
||||
try {
|
||||
if (queryString.contains(":")) {
|
||||
return createExpertQuery(searchableType, queryParams);
|
||||
@@ -116,14 +120,14 @@ public class LuceneQueryBuilder extends QueryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
private Query createExpertQuery(SearchableType searchableType, QueryParams queryParams) throws QueryNodeException {
|
||||
private Query createExpertQuery(LuceneSearchableType searchableType, QueryParams queryParams) throws QueryNodeException {
|
||||
StandardQueryParser parser = new StandardQueryParser(analyzer);
|
||||
|
||||
parser.setPointsConfigMap(searchableType.getPointsConfig());
|
||||
return parser.parse(queryParams.getQueryString(), "");
|
||||
}
|
||||
|
||||
public Query createBestGuessQuery(SearchableType searchableType, QueryBuilder.QueryParams queryParams) {
|
||||
public Query createBestGuessQuery(LuceneSearchableType searchableType, QueryBuilder.QueryParams queryParams) {
|
||||
String[] fieldNames = searchableType.getFieldNames();
|
||||
if (fieldNames == null || fieldNames.length == 0) {
|
||||
throw new NoDefaultQueryFieldsFoundException(searchableType.getType());
|
||||
|
||||
@@ -24,19 +24,35 @@
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class LuceneSearchEngine implements SearchEngine {
|
||||
|
||||
private final SearchableTypeResolver resolver;
|
||||
private final LuceneIndexFactory indexFactory;
|
||||
private final LuceneQueryBuilderFactory queryBuilderFactory;
|
||||
|
||||
@Inject
|
||||
public LuceneSearchEngine(LuceneIndexFactory indexFactory, LuceneQueryBuilderFactory queryBuilderFactory) {
|
||||
public LuceneSearchEngine(SearchableTypeResolver resolver, LuceneIndexFactory indexFactory, LuceneQueryBuilderFactory queryBuilderFactory) {
|
||||
this.resolver = resolver;
|
||||
this.indexFactory = indexFactory;
|
||||
this.queryBuilderFactory = queryBuilderFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<SearchableType> getSearchableTypes() {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
return resolver.getSearchableTypes()
|
||||
.stream()
|
||||
.filter(type -> type.getPermission().map(subject::isPermitted).orElse(true))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index getOrCreate(String name, IndexOptions options) {
|
||||
return indexFactory.create(name, options);
|
||||
|
||||
@@ -29,42 +29,41 @@ import lombok.Value;
|
||||
import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Value
|
||||
public class SearchableType {
|
||||
public class LuceneSearchableType implements SearchableType {
|
||||
|
||||
private static final float DEFAULT_BOOST = 1f;
|
||||
|
||||
Class<?> type;
|
||||
String name;
|
||||
String[] fieldNames;
|
||||
Map<String,Float> boosts;
|
||||
Map<String, PointsConfig> pointsConfig;
|
||||
String permission;
|
||||
List<SearchableField> fields;
|
||||
String[] fieldNames;
|
||||
Map<String, Float> boosts;
|
||||
Map<String, PointsConfig> pointsConfig;
|
||||
TypeConverter typeConverter;
|
||||
|
||||
SearchableType(Class<?> type,
|
||||
String[] fieldNames,
|
||||
Map<String, Float> boosts,
|
||||
Map<String, PointsConfig> pointsConfig,
|
||||
List<SearchableField> fields,
|
||||
TypeConverter typeConverter) {
|
||||
LuceneSearchableType(Class<?> type, IndexedType annotation, List<SearchableField> fields) {
|
||||
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;
|
||||
this.name = name(type, annotation);
|
||||
this.permission = Strings.emptyToNull(annotation.permission());
|
||||
this.fields = fields;
|
||||
this.fieldNames = fieldNames(fields);
|
||||
this.boosts = boosts(fields);
|
||||
this.pointsConfig = pointsConfig(fields);
|
||||
this.typeConverter = TypeConverters.create(type);
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
public Optional<String> getPermission() {
|
||||
return Optional.ofNullable(permission);
|
||||
}
|
||||
|
||||
private String name(Class<?> type, IndexedType annotation) {
|
||||
String nameFromAnnotation = annotation.value();
|
||||
if (Strings.isNullOrEmpty(nameFromAnnotation)) {
|
||||
String simpleName = type.getSimpleName();
|
||||
@@ -72,4 +71,32 @@ public class SearchableType {
|
||||
}
|
||||
return nameFromAnnotation;
|
||||
}
|
||||
|
||||
private String[] fieldNames(List<SearchableField> fields) {
|
||||
return fields.stream()
|
||||
.filter(SearchableField::isDefaultQuery)
|
||||
.map(SearchableField::getName)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
private Map<String, Float> boosts(List<SearchableField> fields) {
|
||||
Map<String, Float> map = new HashMap<>();
|
||||
for (SearchableField field : fields) {
|
||||
if (field.isDefaultQuery() && field.getBoost() != DEFAULT_BOOST) {
|
||||
map.put(field.getName(), field.getBoost());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
private Map<String, PointsConfig> pointsConfig(List<SearchableField> fields) {
|
||||
Map<String, PointsConfig> map = new HashMap<>();
|
||||
for (SearchableField field : fields) {
|
||||
PointsConfig config = field.getPointsConfig();
|
||||
if (config != null) {
|
||||
map.put(field.getName(), config);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ final class Queries {
|
||||
private Queries() {
|
||||
}
|
||||
|
||||
private static Query typeQuery(SearchableType type) {
|
||||
private static Query typeQuery(LuceneSearchableType type) {
|
||||
return new TermQuery(new Term(FieldNames.TYPE, type.getName()));
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ final class Queries {
|
||||
return new TermQuery(new Term(FieldNames.REPOSITORY, repositoryId));
|
||||
}
|
||||
|
||||
static Query filter(Query query, SearchableType searchableType, QueryBuilder.QueryParams params) {
|
||||
static Query filter(Query query, LuceneSearchableType searchableType, QueryBuilder.QueryParams params) {
|
||||
BooleanQuery.Builder builder = new BooleanQuery.Builder()
|
||||
.add(query, MUST)
|
||||
.add(typeQuery(searchableType), MUST);
|
||||
|
||||
@@ -50,9 +50,9 @@ public class QueryResultFactory {
|
||||
private final Analyzer analyzer;
|
||||
private final Highlighter highlighter;
|
||||
private final IndexSearcher searcher;
|
||||
private final SearchableType searchableType;
|
||||
private final LuceneSearchableType searchableType;
|
||||
|
||||
public QueryResultFactory(Analyzer analyzer, IndexSearcher searcher, SearchableType searchableType, Query query) {
|
||||
public QueryResultFactory(Analyzer analyzer, IndexSearcher searcher, LuceneSearchableType searchableType, Query query) {
|
||||
this.analyzer = analyzer;
|
||||
this.searcher = searcher;
|
||||
this.searchableType = searchableType;
|
||||
@@ -61,7 +61,7 @@ public class QueryResultFactory {
|
||||
|
||||
private Highlighter createHighlighter(Query query) {
|
||||
return new Highlighter(
|
||||
new SimpleHTMLFormatter("**", "**"),
|
||||
new SimpleHTMLFormatter("<>", "</>"),
|
||||
new QueryScorer(query)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ import javax.annotation.Nonnull;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -44,7 +46,7 @@ import static sonia.scm.NotFoundException.notFound;
|
||||
@Singleton
|
||||
class SearchableTypeResolver {
|
||||
|
||||
private final Map<Class<?>, SearchableType> classToSearchableType = new HashMap<>();
|
||||
private final Map<Class<?>, LuceneSearchableType> classToSearchableType = new HashMap<>();
|
||||
private final Map<String, Class<?>> nameToClass = new HashMap<>();
|
||||
|
||||
@Inject
|
||||
@@ -62,26 +64,30 @@ class SearchableTypeResolver {
|
||||
fillMaps(convert(indexedTypes));
|
||||
}
|
||||
|
||||
private void fillMaps(Iterable<SearchableType> types) {
|
||||
for (SearchableType type : types) {
|
||||
private void fillMaps(Iterable<LuceneSearchableType> types) {
|
||||
for (LuceneSearchableType type : types) {
|
||||
classToSearchableType.put(type.getType(), type);
|
||||
nameToClass.put(type.getName(), type.getType());
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Set<SearchableType> convert(Iterable<Class<?>> indexedTypes) {
|
||||
private Set<LuceneSearchableType> convert(Iterable<Class<?>> indexedTypes) {
|
||||
return StreamSupport.stream(indexedTypes.spliterator(), false)
|
||||
.map(SearchableTypes::create)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public SearchableType resolve(Object object) {
|
||||
public Collection<LuceneSearchableType> getSearchableTypes() {
|
||||
return Collections.unmodifiableCollection(classToSearchableType.values());
|
||||
}
|
||||
|
||||
public LuceneSearchableType resolve(Object object) {
|
||||
return resolve(object.getClass());
|
||||
}
|
||||
|
||||
public SearchableType resolve(Class<?> type) {
|
||||
SearchableType searchableType = classToSearchableType.get(type);
|
||||
public LuceneSearchableType resolve(Class<?> type) {
|
||||
LuceneSearchableType searchableType = classToSearchableType.get(type);
|
||||
if (searchableType == null) {
|
||||
throw notFound(entity("type", type.getName()));
|
||||
}
|
||||
|
||||
@@ -24,46 +24,25 @@
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class SearchableTypes {
|
||||
|
||||
private static final float DEFAULT_BOOST = 1f;
|
||||
|
||||
private SearchableTypes() {
|
||||
}
|
||||
|
||||
static SearchableType create(Class<?> type) {
|
||||
static LuceneSearchableType create(Class<?> type) {
|
||||
List<SearchableField> fields = new ArrayList<>();
|
||||
collectFields(type, fields);
|
||||
return createSearchableType(type, fields);
|
||||
}
|
||||
|
||||
private static SearchableType createSearchableType(Class<?> type, List<SearchableField> fields) {
|
||||
String[] fieldsNames = fields.stream()
|
||||
.filter(SearchableField::isDefaultQuery)
|
||||
.map(SearchableField::getName)
|
||||
.toArray(String[]::new);
|
||||
|
||||
Map<String, Float> boosts = new HashMap<>();
|
||||
Map<String, PointsConfig> pointsConfig = new HashMap<>();
|
||||
for (SearchableField field : fields) {
|
||||
if (field.isDefaultQuery() && field.getBoost() != DEFAULT_BOOST) {
|
||||
boosts.put(field.getName(), field.getBoost());
|
||||
}
|
||||
PointsConfig config = field.getPointsConfig();
|
||||
if (config != null) {
|
||||
pointsConfig.put(field.getName(), config);
|
||||
}
|
||||
IndexedType annotation = type.getAnnotation(IndexedType.class);
|
||||
if (annotation == null) {
|
||||
throw new IllegalArgumentException(
|
||||
type.getName() + " has no " + IndexedType.class.getSimpleName() + " annotation"
|
||||
);
|
||||
}
|
||||
|
||||
return new SearchableType(type, fieldsNames, boosts, pointsConfig, fields, TypeConverters.create(type));
|
||||
collectFields(type, fields);
|
||||
return new LuceneSearchableType(type, annotation, fields);
|
||||
}
|
||||
|
||||
private static void collectFields(Class<?> type, List<SearchableField> fields) {
|
||||
|
||||
Reference in New Issue
Block a user