Add embedded repository to search result hit (#1756)

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
Sebastian Sdorra
2021-08-04 16:29:23 +02:00
committed by GitHub
parent 7dd61953ac
commit 7c10926244
6 changed files with 133 additions and 3 deletions

View File

@@ -0,0 +1,2 @@
- type: Added
description: Embedded repository in search result hit ([#1756](https://github.com/scm-manager/scm-manager/pull/1756))

View File

@@ -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.
*/

View File

@@ -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);
}
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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);
}