Skip to content

Commit aa709f0

Browse files
author
amvanbaren
committed
Search by publisher
1 parent bc84afc commit aa709f0

File tree

12 files changed

+437
-9
lines changed

12 files changed

+437
-9
lines changed

server/src/main/java/org/eclipse/openvsx/search/DatabaseSearchService.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public boolean isEnabled() {
5858
@CacheEvict(value = CACHE_AVERAGE_REVIEW_RATING, allEntries = true)
5959
public SearchResult search(ISearchService.Options options) {
6060
var matchingExtensions = repositories.findAllActiveExtensions();
61+
matchingExtensions = includeByNamespace(options, matchingExtensions);
6162
matchingExtensions = excludeByNamespace(options, matchingExtensions);
6263
matchingExtensions = excludeByTargetPlatform(options, matchingExtensions);
6364
matchingExtensions = excludeByCategory(options, matchingExtensions);
@@ -141,6 +142,14 @@ private Streamable<Extension> excludeByTargetPlatform(Options options, Streamabl
141142
return matchingExtensions.filter(extension -> extension.getVersions().stream().anyMatch(ev -> ev.getTargetPlatform().equals(options.targetPlatform())));
142143
}
143144

145+
private Streamable<Extension> includeByNamespace(Options options, Streamable<Extension> matchingExtensions) {
146+
if(options.namespace() == null) {
147+
return matchingExtensions;
148+
}
149+
150+
return matchingExtensions.filter(extension -> extension.getNamespace().getName().equalsIgnoreCase(options.namespace()));
151+
}
152+
144153
private Streamable<Extension> excludeByNamespace(Options options, Streamable<Extension> matchingExtensions) {
145154
if(options.namespacesToExclude() == null) {
146155
return matchingExtensions;

server/src/main/java/org/eclipse/openvsx/search/ElasticSearchService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ private ObjectBuilder<BoolQuery> createSearchQuery(BoolQuery.Builder boolQuery,
301301
boolQuery.must(builder -> builder.bool(textBoolQuery -> createTextSearchQuery(textBoolQuery, options)));
302302
}
303303

304+
if (!StringUtils.isEmpty(options.namespace())) {
305+
// Filter by namespace
306+
boolQuery.must(QueryBuilders.term(builder -> builder.field("namespace").value(options.namespace()).caseInsensitive(true)));
307+
}
304308
if (!StringUtils.isEmpty(options.category())) {
305309
// Filter by selected category
306310
boolQuery.must(QueryBuilders.matchPhrase(builder -> builder.field("categories").query(options.category())));

server/src/main/java/org/eclipse/openvsx/search/ISearchService.java

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Collection;
1616
import java.util.List;
1717
import java.util.Objects;
18+
import java.util.regex.Pattern;
1819

1920
/**
2021
* Common interface for all search service implementations.
@@ -62,7 +63,7 @@ public interface ISearchService {
6263
*/
6364
void removeSearchEntries(Collection<Long> ids);
6465

65-
public record Options(
66+
record Options(
6667
String queryString,
6768
String category,
6869
String targetPlatform,
@@ -71,8 +72,63 @@ public record Options(
7172
String sortOrder,
7273
String sortBy,
7374
boolean includeAllVersions,
74-
String[] namespacesToExclude
75+
String[] namespacesToExclude,
76+
String namespace
7577
) {
78+
private static final Pattern PUBLISHER_PATTERN = Pattern.compile("^@?publisher:| @?publisher:");
79+
80+
public Options(
81+
String queryString,
82+
String category,
83+
String targetPlatform,
84+
int requestedSize,
85+
int requestedOffset,
86+
String sortOrder,
87+
String sortBy,
88+
boolean includeAllVersions,
89+
String[] namespacesToExclude
90+
) {
91+
String namespace = null;
92+
if(queryString != null) {
93+
var matcher = PUBLISHER_PATTERN.matcher(queryString);
94+
var results = matcher.results().toList();
95+
if(results.size() > 1) {
96+
requestedSize = 0;
97+
} else if(!results.isEmpty()) {
98+
var first = results.getFirst();
99+
var publisherStartIndex = first.start();
100+
var publisherEndIndex = queryString.indexOf(' ', first.end());
101+
if(publisherEndIndex == -1) {
102+
publisherEndIndex = queryString.length();
103+
}
104+
namespace = queryString.substring(first.end(), publisherEndIndex);
105+
var newQuery = "";
106+
if(publisherStartIndex > 0) {
107+
newQuery += queryString.substring(0, publisherStartIndex);
108+
}
109+
if(publisherEndIndex < queryString.length()) {
110+
newQuery += queryString.substring(publisherEndIndex);
111+
}
112+
113+
queryString = newQuery.trim();
114+
}
115+
}
116+
117+
this(
118+
queryString,
119+
category,
120+
targetPlatform,
121+
requestedSize,
122+
requestedOffset,
123+
sortOrder,
124+
sortBy,
125+
includeAllVersions,
126+
namespacesToExclude,
127+
namespace
128+
);
129+
}
130+
131+
76132
@Override
77133
public boolean equals(Object o) {
78134
if (this == o) return true;
@@ -86,12 +142,13 @@ public boolean equals(Object o) {
86142
&& Objects.equals(targetPlatform, options.targetPlatform)
87143
&& Objects.equals(sortOrder, options.sortOrder)
88144
&& Objects.equals(sortBy, options.sortBy)
89-
&& Arrays.equals(namespacesToExclude, options.namespacesToExclude);
145+
&& Arrays.equals(namespacesToExclude, options.namespacesToExclude)
146+
&& Objects.equals(namespace, options.namespace);
90147
}
91148

92149
@Override
93150
public int hashCode() {
94-
int result = Objects.hash(queryString, category, targetPlatform, requestedSize, requestedOffset, sortOrder, sortBy, includeAllVersions);
151+
int result = Objects.hash(queryString, category, targetPlatform, requestedSize, requestedOffset, sortOrder, sortBy, includeAllVersions, namespace);
95152
result = 31 * result + Arrays.hashCode(namespacesToExclude);
96153
return result;
97154
}

server/src/main/java/org/eclipse/openvsx/search/SearchUtilService.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
package org.eclipse.openvsx.search;
1212

1313
import org.eclipse.openvsx.entities.Extension;
14-
import org.springframework.data.elasticsearch.core.SearchHits;
1514
import org.springframework.stereotype.Component;
1615

1716
import java.util.Collection;
@@ -58,8 +57,8 @@ protected ISearchService getImplementation() {
5857

5958
}
6059

61-
public SearchResult search(ElasticSearchService.Options options) {
62-
return getImplementation().search(options);
60+
public SearchResult search(ISearchService.Options options) {
61+
return options.requestedSize() > 0 ? getImplementation().search(options) : new SearchResult();
6362
}
6463

6564
@Override

server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,109 @@ void testSearch() throws Exception {
585585
})));
586586
}
587587

588+
@Test
589+
void testSearchPublisher() throws Exception {
590+
var extVersions = mockSearch();
591+
extVersions.forEach(extVersion -> Mockito.when(repositories.findLatestVersion(extVersion.getExtension(), null, false, true)).thenReturn(extVersion));
592+
Mockito.when(repositories.findLatestVersions(extVersions.stream().map(ExtensionVersion::getExtension).map(Extension::getId).toList()))
593+
.thenReturn(extVersions);
594+
595+
mockMvc.perform(get("/api/-/search?query={query}&size={size}&offset={offset}", "publisher:foo", "10", "0"))
596+
.andExpect(status().isOk())
597+
.andExpect(content().json(searchJson(s -> {
598+
s.setOffset(0);
599+
s.setTotalSize(1);
600+
var e1 = new SearchEntryJson();
601+
e1.setNamespace("foo");
602+
e1.setName("bar");
603+
e1.setVersion("1.0.0");
604+
e1.setTimestamp("2000-01-01T10:00Z");
605+
e1.setDisplayName("Foo Bar");
606+
s.getExtensions().add(e1);
607+
})));
608+
}
609+
610+
@Test
611+
void testSearchPublisherWithQueryLast() throws Exception {
612+
var extVersions = mockSearch();
613+
extVersions.forEach(extVersion -> Mockito.when(repositories.findLatestVersion(extVersion.getExtension(), null, false, true)).thenReturn(extVersion));
614+
Mockito.when(repositories.findLatestVersions(extVersions.stream().map(ExtensionVersion::getExtension).map(Extension::getId).toList()))
615+
.thenReturn(extVersions);
616+
617+
mockMvc.perform(get("/api/-/search?query={query}&size={size}&offset={offset}", "publisher:foo bar", "10", "0"))
618+
.andExpect(status().isOk())
619+
.andExpect(content().json(searchJson(s -> {
620+
s.setOffset(0);
621+
s.setTotalSize(1);
622+
var e1 = new SearchEntryJson();
623+
e1.setNamespace("foo");
624+
e1.setName("bar");
625+
e1.setVersion("1.0.0");
626+
e1.setTimestamp("2000-01-01T10:00Z");
627+
e1.setDisplayName("Foo Bar");
628+
s.getExtensions().add(e1);
629+
})));
630+
}
631+
632+
@Test
633+
void testSearchPublisherWithQueryFirst() throws Exception {
634+
var extVersions = mockSearch();
635+
extVersions.forEach(extVersion -> Mockito.when(repositories.findLatestVersion(extVersion.getExtension(), null, false, true)).thenReturn(extVersion));
636+
Mockito.when(repositories.findLatestVersions(extVersions.stream().map(ExtensionVersion::getExtension).map(Extension::getId).toList()))
637+
.thenReturn(extVersions);
638+
639+
mockMvc.perform(get("/api/-/search?query={query}&size={size}&offset={offset}", "bar publisher:foo", "10", "0"))
640+
.andExpect(status().isOk())
641+
.andExpect(content().json(searchJson(s -> {
642+
s.setOffset(0);
643+
s.setTotalSize(1);
644+
var e1 = new SearchEntryJson();
645+
e1.setNamespace("foo");
646+
e1.setName("bar");
647+
e1.setVersion("1.0.0");
648+
e1.setTimestamp("2000-01-01T10:00Z");
649+
e1.setDisplayName("Foo Bar");
650+
s.getExtensions().add(e1);
651+
})));
652+
}
653+
654+
@Test
655+
void testSearchPublisherWithMoreQuery() throws Exception {
656+
var extVersions = mockSearch();
657+
extVersions.forEach(extVersion -> Mockito.when(repositories.findLatestVersion(extVersion.getExtension(), null, false, true)).thenReturn(extVersion));
658+
Mockito.when(repositories.findLatestVersions(extVersions.stream().map(ExtensionVersion::getExtension).map(Extension::getId).toList()))
659+
.thenReturn(extVersions);
660+
661+
mockMvc.perform(get("/api/-/search?query={query}&size={size}&offset={offset}", "bar publisher:foo code", "10", "0"))
662+
.andExpect(status().isOk())
663+
.andExpect(content().json(searchJson(s -> {
664+
s.setOffset(0);
665+
s.setTotalSize(1);
666+
var e1 = new SearchEntryJson();
667+
e1.setNamespace("foo");
668+
e1.setName("bar");
669+
e1.setVersion("1.0.0");
670+
e1.setTimestamp("2000-01-01T10:00Z");
671+
e1.setDisplayName("Foo Bar");
672+
s.getExtensions().add(e1);
673+
})));
674+
}
675+
676+
@Test
677+
void testSearchMultiplePublishers() throws Exception {
678+
var extVersions = mockSearch();
679+
extVersions.forEach(extVersion -> Mockito.when(repositories.findLatestVersion(extVersion.getExtension(), null, false, true)).thenReturn(extVersion));
680+
Mockito.when(repositories.findLatestVersions(extVersions.stream().map(ExtensionVersion::getExtension).map(Extension::getId).toList()))
681+
.thenReturn(extVersions);
682+
683+
mockMvc.perform(get("/api/-/search?query={query}&size={size}&offset={offset}", "publisher:bar publisher:foo", "10", "0"))
684+
.andExpect(status().isOk())
685+
.andExpect(content().json(searchJson(s -> {
686+
s.setOffset(0);
687+
s.setTotalSize(0);
688+
})));
689+
}
690+
588691
@Test
589692
void testSearchInactive() throws Exception {
590693
var extVersionsList = mockSearch();
@@ -2184,6 +2287,19 @@ private List<ExtensionVersion> mockSearch() {
21842287
var searchOptions = new ISearchService.Options("foo", null, null, 10, 0, "desc", SortBy.RELEVANCE, false, null);
21852288
Mockito.when(search.search(searchOptions))
21862289
.thenReturn(searchResult);
2290+
2291+
var publisherSearchOptions = new ISearchService.Options("", null, null, 10, 0, "desc", SortBy.RELEVANCE, false, null, "foo");
2292+
Mockito.when(search.search(publisherSearchOptions))
2293+
.thenReturn(searchResult);
2294+
2295+
var publisherWithQuerySearchOptions = new ISearchService.Options("bar", null, null, 10, 0, "desc", SortBy.RELEVANCE, false, null, "foo");
2296+
Mockito.when(search.search(publisherWithQuerySearchOptions))
2297+
.thenReturn(searchResult);
2298+
2299+
var publisherWithMoreQuerySearchOptions = new ISearchService.Options("bar code", null, null, 10, 0, "desc", SortBy.RELEVANCE, false, null, "foo");
2300+
Mockito.when(search.search(publisherWithMoreQuerySearchOptions))
2301+
.thenReturn(searchResult);
2302+
21872303
return List.of(extVersion);
21882304
}
21892305

server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.hamcrest.Matchers;
3535
import org.junit.jupiter.api.Test;
3636
import org.mockito.Mockito;
37+
import org.mockito.stubbing.Answer;
3738
import org.springframework.beans.factory.annotation.Autowired;
3839
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient;
3940
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
@@ -60,6 +61,7 @@
6061
import java.util.zip.ZipFile;
6162

6263
import static org.eclipse.openvsx.entities.FileResource.*;
64+
import static org.mockito.ArgumentMatchers.any;
6365
import static org.mockito.ArgumentMatchers.anyCollection;
6466
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
6567
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -186,6 +188,66 @@ void testFindByIdInactive() throws Exception {
186188
.andExpect(content().json(file("empty-response.json")));
187189
}
188190

191+
@Test
192+
void testFindByPublisher() throws Exception {
193+
var extension = mockSearch(true);
194+
mockExtensionVersions(extension, null, "universal");
195+
196+
mockMvc.perform(post("/vscode/gallery/extensionquery")
197+
.content(file("findpublisher-yaml-query.json"))
198+
.contentType(MediaType.APPLICATION_JSON))
199+
.andExpect(status().isOk())
200+
.andExpect(content().json(file("findname-yaml-response.json")));
201+
}
202+
203+
@Test
204+
void testFindByPublisherFirst() throws Exception {
205+
var extension = mockSearch(true);
206+
mockExtensionVersions(extension, null, "universal");
207+
208+
mockMvc.perform(post("/vscode/gallery/extensionquery")
209+
.content(file("findpublisher-yaml-query-first.json"))
210+
.contentType(MediaType.APPLICATION_JSON))
211+
.andExpect(status().isOk())
212+
.andExpect(content().json(file("findname-yaml-response.json")));
213+
}
214+
215+
@Test
216+
void testFindByPublisherLast() throws Exception {
217+
var extension = mockSearch(true);
218+
mockExtensionVersions(extension, null, "universal");
219+
220+
mockMvc.perform(post("/vscode/gallery/extensionquery")
221+
.content(file("findpublisher-yaml-query-last.json"))
222+
.contentType(MediaType.APPLICATION_JSON))
223+
.andExpect(status().isOk())
224+
.andExpect(content().json(file("findname-yaml-response.json")));
225+
}
226+
227+
@Test
228+
void testFindByPublisherMiddle() throws Exception {
229+
var extension = mockSearch(true);
230+
mockExtensionVersions(extension, null, "universal");
231+
232+
mockMvc.perform(post("/vscode/gallery/extensionquery")
233+
.content(file("findpublisher-yaml-query-middle.json"))
234+
.contentType(MediaType.APPLICATION_JSON))
235+
.andExpect(status().isOk())
236+
.andExpect(content().json(file("findname-yaml-response.json")));
237+
}
238+
239+
@Test
240+
void testFindByMultiplePublishers() throws Exception {
241+
var extension = mockSearch(true);
242+
mockExtensionVersions(extension, null, "universal");
243+
244+
mockMvc.perform(post("/vscode/gallery/extensionquery")
245+
.content(file("findpublisher-yaml-query-multiple.json"))
246+
.contentType(MediaType.APPLICATION_JSON))
247+
.andExpect(status().isOk())
248+
.andExpect(content().json(file("empty-response.json")));
249+
}
250+
189251
@Test
190252
void testFindByName() throws Exception {
191253
var extension = mockSearch(true);
@@ -633,9 +695,22 @@ private Extension mockSearch(String targetPlatform, String namespaceName, boolea
633695
.thenReturn(true);
634696
Mockito.when(search.isEnabled())
635697
.thenReturn(true);
698+
636699
var searchOptions = new ISearchService.Options("yaml", null, targetPlatform, 50, 0, "desc", SortBy.RELEVANCE, false, new String[]{builtInExtensionNamespace});
637-
Mockito.when(search.search(searchOptions))
638-
.thenReturn(searchResult);
700+
var publisherSearchOptions = new ISearchService.Options("", null, targetPlatform, 50, 0, "desc", SortBy.RELEVANCE, false, new String[]{builtInExtensionNamespace}, "redhat");
701+
var publisherWithQueryOptions = new ISearchService.Options("yaml", null, targetPlatform, 50, 0, "desc", SortBy.RELEVANCE, false, new String[]{builtInExtensionNamespace}, "redhat");
702+
var publisherWithMoreQueryOptions = new ISearchService.Options("yaml config", null, targetPlatform, 50, 0, "desc", SortBy.RELEVANCE, false, new String[]{builtInExtensionNamespace}, "redhat");
703+
var searches = List.of(searchOptions, publisherSearchOptions, publisherWithQueryOptions, publisherWithMoreQueryOptions);
704+
Mockito.when(search.search(any(ISearchService.Options.class))).thenAnswer((Answer<SearchResult>) invocationOnMock -> {
705+
var options = invocationOnMock.getArgument(0, ISearchService.Options.class);
706+
if(searches.contains(options)) {
707+
return searchResult;
708+
} else if (options.requestedSize() == 0) {
709+
return new SearchResult();
710+
} else {
711+
return null;
712+
}
713+
});
639714

640715
var extension = mockExtension();
641716
List<Extension> results = active ? List.of(extension) : Collections.emptyList();

0 commit comments

Comments
 (0)