mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-06 12:20:56 +01:00
Add embedded repository to search result hit (#1756)
Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
2
gradle/changelog/hit_embedded_repository.yaml
Normal file
2
gradle/changelog/hit_embedded_repository.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: Added
|
||||
description: Embedded repository in search result hit ([#1756](https://github.com/scm-manager/scm-manager/pull/1756))
|
||||
@@ -31,6 +31,7 @@ import lombok.Getter;
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents an object which matched the search query.
|
||||
@@ -46,6 +47,12 @@ public class Hit {
|
||||
*/
|
||||
String id;
|
||||
|
||||
/**
|
||||
* Repository associated with the hit.
|
||||
* @since 2.23.0
|
||||
*/
|
||||
String repositoryId;
|
||||
|
||||
/**
|
||||
* The score describes how good the match was.
|
||||
*/
|
||||
@@ -57,6 +64,17 @@ public class Hit {
|
||||
*/
|
||||
Map<String, Field> fields;
|
||||
|
||||
/**
|
||||
* Returns optional id of a repository which associated with the hit
|
||||
* or empty if the hit is not associated with any repository.
|
||||
*
|
||||
* @return optional repository id
|
||||
* @since 2.23.0
|
||||
*/
|
||||
public Optional<String> getRepositoryId() {
|
||||
return Optional.ofNullable(repositoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class of hit field types.
|
||||
*/
|
||||
|
||||
@@ -25,20 +25,27 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.damnhandy.uri.template.UriTemplate;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import de.otto.edison.hal.paging.NumberedPaging;
|
||||
import de.otto.edison.hal.paging.PagingRel;
|
||||
import lombok.Data;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Context;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.ObjectFactory;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.search.Hit;
|
||||
import sonia.scm.search.QueryResult;
|
||||
import sonia.scm.web.EdisonHalAppender;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -50,8 +57,26 @@ import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging;
|
||||
@Mapper
|
||||
public abstract class QueryResultMapper extends HalAppenderMapper {
|
||||
|
||||
@Inject
|
||||
private RepositoryManager repositoryManager;
|
||||
|
||||
@Inject
|
||||
private ResourceLinks resourceLinks;
|
||||
|
||||
@VisibleForTesting
|
||||
void setRepositoryManager(RepositoryManager repositoryManager) {
|
||||
this.repositoryManager = repositoryManager;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setResourceLinks(ResourceLinks resourceLinks) {
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
public abstract QueryResultDto map(@Context SearchParameters params, QueryResult result);
|
||||
|
||||
public abstract EmbeddedRepositoryDto map(Repository repository);
|
||||
|
||||
@AfterMapping
|
||||
void setPageValues(@MappingTarget QueryResultDto dto, QueryResult result, @Context SearchParameters params) {
|
||||
int totalHits = (int) result.getTotalHits();
|
||||
@@ -59,6 +84,13 @@ public abstract class QueryResultMapper extends HalAppenderMapper {
|
||||
dto.setPage(params.getPage());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@ObjectFactory
|
||||
EmbeddedRepositoryDto createDto(Repository repository) {
|
||||
String self = resourceLinks.repository().self(repository.getNamespace(), repository.getName());
|
||||
return new EmbeddedRepositoryDto(linkingTo().self(self).build());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@ObjectFactory
|
||||
QueryResultDto createDto(@Context SearchParameters params, QueryResult result) {
|
||||
@@ -103,11 +135,20 @@ public abstract class QueryResultMapper extends HalAppenderMapper {
|
||||
protected HitDto createHitDto(@Context QueryResult queryResult, Hit hit) {
|
||||
Links.Builder links = linkingTo();
|
||||
Embedded.Builder embedded = Embedded.embeddedBuilder();
|
||||
|
||||
hit.getRepositoryId().map(this::repository).ifPresent(r -> embedded.with("repository", r));
|
||||
applyEnrichers(new EdisonHalAppender(links, embedded), hit, queryResult);
|
||||
return new HitDto(links.build(), embedded.build());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private HalRepresentation repository(String id) {
|
||||
Repository repository = repositoryManager.get(id);
|
||||
if (repository != null) {
|
||||
return map(repository);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int computePageTotal(int totalHits, int pageSize) {
|
||||
if (totalHits % pageSize > 0) {
|
||||
return totalHits / pageSize + 1;
|
||||
@@ -117,4 +158,14 @@ public abstract class QueryResultMapper extends HalAppenderMapper {
|
||||
}
|
||||
|
||||
protected abstract HitDto map(@Context QueryResult queryResult, Hit hit);
|
||||
|
||||
@Data
|
||||
public static class EmbeddedRepositoryDto extends HalRepresentation {
|
||||
private String namespace;
|
||||
private String name;
|
||||
private String type;
|
||||
public EmbeddedRepositoryDto(Links links) {
|
||||
super(links);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public class QueryResultFactory {
|
||||
for (SearchableField field : searchableType.getFields()) {
|
||||
field(document, field).ifPresent(f -> fields.put(field.getName(), f));
|
||||
}
|
||||
return new Hit(document.get(FieldNames.ID), scoreDoc.score, fields);
|
||||
return new Hit(document.get(FieldNames.ID), document.get(FieldNames.REPOSITORY), scoreDoc.score, fields);
|
||||
}
|
||||
|
||||
private Optional<Hit.Field> field(Document document, SearchableField field) throws IOException, InvalidTokenOffsetsException {
|
||||
|
||||
@@ -37,6 +37,9 @@ import org.mapstruct.factory.Mappers;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.search.Hit;
|
||||
import sonia.scm.search.IndexNames;
|
||||
import sonia.scm.search.QueryCountResult;
|
||||
@@ -50,6 +53,7 @@ import javax.annotation.Nonnull;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
@@ -60,6 +64,7 @@ import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -68,6 +73,9 @@ class SearchResourceTest {
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private SearchEngine searchEngine;
|
||||
|
||||
@Mock
|
||||
private RepositoryManager repositoryManager;
|
||||
|
||||
private RestDispatcher dispatcher;
|
||||
|
||||
@Mock
|
||||
@@ -75,7 +83,13 @@ class SearchResourceTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUpDispatcher() {
|
||||
ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore();
|
||||
scmPathInfoStore.set(() -> URI.create("/"));
|
||||
|
||||
QueryResultMapper mapper = Mappers.getMapper(QueryResultMapper.class);
|
||||
mapper.setRepositoryManager(repositoryManager);
|
||||
mapper.setResourceLinks(new ResourceLinks(scmPathInfoStore));
|
||||
|
||||
mapper.setRegistry(enricherRegistry);
|
||||
SearchResource resource = new SearchResource(
|
||||
searchEngine, mapper
|
||||
@@ -202,6 +216,27 @@ class SearchResourceTest {
|
||||
assertThat(hits.size()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmbeddedRepository() throws UnsupportedEncodingException, URISyntaxException {
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold("git");
|
||||
heartOfGold.setId("42");
|
||||
when(repositoryManager.get("42")).thenReturn(heartOfGold);
|
||||
|
||||
Hit hit = new Hit("21", "42", 21f, Collections.emptyMap());
|
||||
QueryResult result = new QueryResult(1L, String.class, Collections.singletonList(hit));
|
||||
|
||||
mockQueryResult("hello", result);
|
||||
JsonMockHttpResponse response = search("hello");
|
||||
|
||||
JsonNode hitNode = response.getContentAsJson().get("_embedded").get("hits").get(0);
|
||||
JsonNode repositoryNode = hitNode.get("_embedded").get("repository");
|
||||
|
||||
assertThat(repositoryNode.get("namespace").asText()).isEqualTo(heartOfGold.getNamespace());
|
||||
assertThat(repositoryNode.get("name").asText()).isEqualTo(heartOfGold.getName());
|
||||
assertThat(repositoryNode.get("type").asText()).isEqualTo(heartOfGold.getType());
|
||||
assertThat(repositoryNode.get("_links").get("self").get("href").asText()).isEqualTo("/v2/repositories/hitchhiker/HeartOfGold");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void assertLink(JsonNode links, String self, String s) {
|
||||
@@ -219,7 +254,7 @@ class SearchResourceTest {
|
||||
@Nonnull
|
||||
private Hit hit(int i, Object[] values) {
|
||||
Map<String, Hit.Field> fields = fields(values[i]);
|
||||
return new Hit("" + i, values.length - i, fields);
|
||||
return new Hit("" + i, null, values.length - i, fields);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
||||
@@ -549,6 +549,30 @@ class LuceneQueryBuilderTest {
|
||||
assertThat(fields.get("instantValue").get("value").asText()).isEqualTo(now.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyRepository() throws IOException {
|
||||
try (IndexWriter writer = writer()) {
|
||||
writer.addDocument(simpleDoc("Trillian"));
|
||||
}
|
||||
|
||||
QueryResult result = query(Simple.class, "Trillian");
|
||||
Hit hit = result.getHits().get(0);
|
||||
assertThat(hit.getRepositoryId()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnRepositoryAsPartOfHit() throws IOException {
|
||||
try (IndexWriter writer = writer()) {
|
||||
Document document = simpleDoc("Trillian");
|
||||
document.add(new StoredField(FieldNames.REPOSITORY, "4211"));
|
||||
writer.addDocument(document);
|
||||
}
|
||||
|
||||
QueryResult result = query(Simple.class, "Trillian");
|
||||
Hit hit = result.getHits().get(0);
|
||||
assertThat(hit.getRepositoryId()).contains("4211");
|
||||
}
|
||||
|
||||
private QueryResult query(Class<?> type, String queryString) throws IOException {
|
||||
return query(type, queryString, null, null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user