Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions server/src/main/java/org/eclipse/openvsx/RegistryAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,10 @@ public ResponseEntity<SearchResultJson> search(
String sortBy,
@RequestParam(defaultValue = "false")
@Parameter(description = "Whether to include information on all available versions for each returned entry")
boolean includeAllVersions
boolean includeAllVersions,
@RequestParam(required = false)
@Parameter(description = "Filter to only return web-only extensions (extensionKind=web)", schema = @Schema(type = "boolean"))
Boolean webOnly
) {
if (size < 0) {
var json = SearchResultJson.error(negativeSizeMessage());
Expand All @@ -774,7 +777,7 @@ public ResponseEntity<SearchResultJson> search(
return new ResponseEntity<>(json, HttpStatus.BAD_REQUEST);
}

var options = new ISearchService.Options(query, category, targetPlatform, size, offset, sortOrder, sortBy, includeAllVersions, null);
var options = new ISearchService.Options(query, category, targetPlatform, size, offset, sortOrder, sortBy, includeAllVersions, null, null, webOnly);
var resultOffset = 0;
var resultSize = 0;
var resultExtensions = new ArrayList<SearchEntryJson>(size);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public ExtensionSearch toSearch(ExtensionVersion latest, List<String> targetPlat
search.setTimestamp(latest.getTimestamp().toEpochSecond(ZoneOffset.UTC));
search.setCategories(latest.getCategories());
search.setTags(latest.getTags());
search.setExtensionKind(latest.getExtensionKind());

return search;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public SearchResult search(ISearchService.Options options) {
matchingExtensions = excludeByTargetPlatform(options, matchingExtensions);
matchingExtensions = excludeByCategory(options, matchingExtensions);
matchingExtensions = excludeByQueryString(options, matchingExtensions);
matchingExtensions = filterByWebOnly(options, matchingExtensions);

var sortedExtensions = sortExtensions(options, matchingExtensions);
var totalHits = sortedExtensions.size();
Expand Down Expand Up @@ -159,6 +160,17 @@ private Streamable<Extension> excludeByNamespace(Options options, Streamable<Ext
return matchingExtensions.filter(extension -> !namespacesToExclude.contains(extension.getNamespace().getName()));
}

private Streamable<Extension> filterByWebOnly(Options options, Streamable<Extension> matchingExtensions) {
if(options.webOnly() == null || !options.webOnly()) {
return matchingExtensions;
}

return matchingExtensions.filter(extension -> {
var latest = repositories.findLatestVersion(extension, null, false, true);
return latest.getExtensionKind() != null && latest.getExtensionKind().contains("web");
});
}

@Override
@CacheEvict(value = CACHE_DATABASE_SEARCH, allEntries = true)
public void updateSearchIndex(boolean clear) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,10 @@ private ObjectBuilder<BoolQuery> createSearchQuery(BoolQuery.Builder boolQuery,
boolQuery.mustNot(QueryBuilders.term(builder -> builder.field("namespace.keyword").value(namespaceToExclude)));
}
}
if (options.webOnly() != null && options.webOnly()) {
// Filter by web-only extensions
boolQuery.must(QueryBuilders.matchPhrase(builder -> builder.field("extensionKind").query("web")));
}

return boolQuery;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public class ExtensionSearch {

private List<String> tags;

private List<String> extensionKind;

public long getId() {
return id;
}
Expand Down Expand Up @@ -159,6 +161,14 @@ public void setTags(List<String> tags) {
this.tags = tags;
}

public List<String> getExtensionKind() {
return extensionKind;
}

public void setExtensionKind(List<String> extensionKind) {
this.extensionKind = extensionKind;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -176,14 +186,15 @@ public boolean equals(Object o) {
&& Objects.equals(description, that.description)
&& Objects.equals(rating, that.rating)
&& Objects.equals(categories, that.categories)
&& Objects.equals(tags, that.tags);
&& Objects.equals(tags, that.tags)
&& Objects.equals(extensionKind, that.extensionKind);
}

@Override
public int hashCode() {
return Objects.hash(
id, relevance, name, namespace, extensionId, targetPlatforms, displayName, description, timestamp,
rating, downloadCount, categories, tags
rating, downloadCount, categories, tags, extensionKind
);
}
}
66 changes: 49 additions & 17 deletions server/src/main/java/org/eclipse/openvsx/search/ISearchService.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ record Options(
String sortBy,
boolean includeAllVersions,
String[] namespacesToExclude,
String namespace
String namespace,
Boolean webOnly
) {
private static final Pattern PUBLISHER_PATTERN = Pattern.compile("^@?publisher:| @?publisher:");

Expand All @@ -88,7 +89,38 @@ public Options(
boolean includeAllVersions,
String[] namespacesToExclude
) {
String namespace = null;
this(queryString, category, targetPlatform, requestedSize, requestedOffset, sortOrder, sortBy, includeAllVersions, namespacesToExclude, null, null);
}

public Options(
String queryString,
String category,
String targetPlatform,
int requestedSize,
int requestedOffset,
String sortOrder,
String sortBy,
boolean includeAllVersions,
String[] namespacesToExclude,
String namespace
) {
this(queryString, category, targetPlatform, requestedSize, requestedOffset, sortOrder, sortBy, includeAllVersions, namespacesToExclude, namespace, null);
}

public Options(
String queryString,
String category,
String targetPlatform,
int requestedSize,
int requestedOffset,
String sortOrder,
String sortBy,
boolean includeAllVersions,
String[] namespacesToExclude,
String namespace,
Boolean webOnly
) {
String extractedNamespace = null;
if(queryString != null) {
var matcher = PUBLISHER_PATTERN.matcher(queryString);
var results = matcher.results().toList();
Expand All @@ -101,7 +133,7 @@ public Options(
if(publisherEndIndex == -1) {
publisherEndIndex = queryString.length();
}
namespace = queryString.substring(first.end(), publisherEndIndex);
extractedNamespace = queryString.substring(first.end(), publisherEndIndex);
var newQuery = "";
if(publisherStartIndex > 0) {
newQuery += queryString.substring(0, publisherStartIndex);
Expand All @@ -114,18 +146,17 @@ public Options(
}
}

this(
queryString,
category,
targetPlatform,
requestedSize,
requestedOffset,
sortOrder,
sortBy,
includeAllVersions,
namespacesToExclude,
namespace
);
this.queryString = queryString;
this.category = category;
this.targetPlatform = targetPlatform;
this.requestedSize = requestedSize;
this.requestedOffset = requestedOffset;
this.sortOrder = sortOrder;
this.sortBy = sortBy;
this.includeAllVersions = includeAllVersions;
this.namespacesToExclude = namespacesToExclude;
this.namespace = namespace != null ? namespace : extractedNamespace;
this.webOnly = webOnly;
}


Expand All @@ -143,12 +174,13 @@ public boolean equals(Object o) {
&& Objects.equals(sortOrder, options.sortOrder)
&& Objects.equals(sortBy, options.sortBy)
&& Arrays.equals(namespacesToExclude, options.namespacesToExclude)
&& Objects.equals(namespace, options.namespace);
&& Objects.equals(namespace, options.namespace)
&& Objects.equals(webOnly, options.webOnly);
}

@Override
public int hashCode() {
int result = Objects.hash(queryString, category, targetPlatform, requestedSize, requestedOffset, sortOrder, sortBy, includeAllVersions, namespace);
int result = Objects.hash(queryString, category, targetPlatform, requestedSize, requestedOffset, sortOrder, sortBy, includeAllVersions, namespace, webOnly);
result = 31 * result + Arrays.hashCode(namespacesToExclude);
return result;
}
Expand Down
56 changes: 56 additions & 0 deletions server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,62 @@ void testSearchInactive() throws Exception {
.andExpect(content().string("{\"offset\":0,\"totalSize\":1,\"extensions\":[]}"));
}

@Test
void testSearchWebOnly() throws Exception {
var extVersions = mockSearch();
extVersions.forEach(extVersion -> Mockito.when(repositories.findLatestVersion(extVersion.getExtension(), null, false, true)).thenReturn(extVersion));
Mockito.when(repositories.findLatestVersions(extVersions.stream().map(ExtensionVersion::getExtension).map(Extension::getId).toList()))
.thenReturn(extVersions);

var searchOptions = new ISearchService.Options("foo", null, null, 10, 0, "desc", SortBy.RELEVANCE, false, null, null, true);
var searchResult = new SearchResult(1, List.of(new ExtensionSearch()));
searchResult.getHits().get(0).setId(1);
Mockito.when(search.search(searchOptions))
.thenReturn(searchResult);

mockMvc.perform(get("/api/-/search?query={query}&size={size}&offset={offset}&webOnly={webOnly}", "foo", "10", "0", "true"))
.andExpect(status().isOk())
.andExpect(content().json(searchJson(s -> {
s.setOffset(0);
s.setTotalSize(1);
var e1 = new SearchEntryJson();
e1.setNamespace("foo");
e1.setName("bar");
e1.setVersion("1.0.0");
e1.setTimestamp("2000-01-01T10:00Z");
e1.setDisplayName("Foo Bar");
s.getExtensions().add(e1);
})));
}

@Test
void testSearchWebOnlyFalse() throws Exception {
var extVersions = mockSearch();
extVersions.forEach(extVersion -> Mockito.when(repositories.findLatestVersion(extVersion.getExtension(), null, false, true)).thenReturn(extVersion));
Mockito.when(repositories.findLatestVersions(extVersions.stream().map(ExtensionVersion::getExtension).map(Extension::getId).toList()))
.thenReturn(extVersions);

var searchOptions = new ISearchService.Options("foo", null, null, 10, 0, "desc", SortBy.RELEVANCE, false, null, null, false);
var searchResult = new SearchResult(1, List.of(new ExtensionSearch()));
searchResult.getHits().get(0).setId(1);
Mockito.when(search.search(searchOptions))
.thenReturn(searchResult);

mockMvc.perform(get("/api/-/search?query={query}&size={size}&offset={offset}&webOnly={webOnly}", "foo", "10", "0", "false"))
.andExpect(status().isOk())
.andExpect(content().json(searchJson(s -> {
s.setOffset(0);
s.setTotalSize(1);
var e1 = new SearchEntryJson();
e1.setNamespace("foo");
e1.setName("bar");
e1.setVersion("1.0.0");
e1.setTimestamp("2000-01-01T10:00Z");
e1.setDisplayName("Foo Bar");
s.getExtensions().add(e1);
})));
}

@Test
void testGetQueryExtensionName() throws Exception {
mockExtensionVersion();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,51 @@ void testSortByRating() {
assertThat(getIdFromExtensionHits(hits, 3)).isEqualTo(getIdFromExtensionName("java"));
}

@Test
void testWebOnlyFilter() {
var ext1 = mockExtension("web-ext", 4.0, 1, 0, "test", List.of("Other"), List.of("web"));
var ext2 = mockExtension("desktop-ext", 4.0, 1, 0, "test", List.of("Other"), List.of("ui", "workspace"));
var ext3 = mockExtension("hybrid-ext", 4.0, 1, 0, "test", List.of("Other"), List.of("web", "ui"));
var ext4 = mockExtension("no-kind-ext", 4.0, 1, 0, "test", List.of("Other"), null);
Mockito.when(repositories.findAllActiveExtensions()).thenReturn(Streamable.of(List.of(ext1, ext2, ext3, ext4)));

var searchOptionsAll = searchOptions(null, null, 50, 0, null, null, null);
var resultAll = search.search(searchOptionsAll);
assertThat(resultAll.getTotalHits()).isEqualTo(4);

var searchOptionsWebOnly = searchOptions(null, null, 50, 0, null, null, true);
var resultWebOnly = search.search(searchOptionsWebOnly);
assertThat(resultWebOnly.getTotalHits()).isEqualTo(2);

var hits = resultWebOnly.getHits();
assertThat(getIdFromExtensionHits(hits, 0)).isEqualTo(getIdFromExtensionName("web-ext"));
assertThat(getIdFromExtensionHits(hits, 1)).isEqualTo(getIdFromExtensionName("hybrid-ext"));
}

@Test
void testWebOnlyFilterWithQuery() {
var ext1 = mockExtension("web-javascript", 4.0, 1, 0, "test", List.of("Other"), List.of("web"));
var ext2 = mockExtension("desktop-javascript", 4.0, 1, 0, "test", List.of("Other"), List.of("ui"));
var ext3 = mockExtension("web-typescript", 4.0, 1, 0, "test", List.of("Other"), List.of("web"));
Mockito.when(repositories.findAllActiveExtensions()).thenReturn(Streamable.of(List.of(ext1, ext2, ext3)));

var searchOptions = searchOptions("javascript", null, 50, 0, null, null, true);
var result = search.search(searchOptions);
assertThat(result.getTotalHits()).isEqualTo(1);
assertThat(getIdFromExtensionHits(result.getHits(), 0)).isEqualTo(getIdFromExtensionName("web-javascript"));
}

@Test
void testWebOnlyFilterFalse() {
var ext1 = mockExtension("web-ext", 4.0, 1, 0, "test", List.of("Other"), List.of("web"));
var ext2 = mockExtension("desktop-ext", 4.0, 1, 0, "test", List.of("Other"), List.of("ui"));
Mockito.when(repositories.findAllActiveExtensions()).thenReturn(Streamable.of(List.of(ext1, ext2)));

var searchOptions = searchOptions(null, null, 50, 0, null, null, false);
var result = search.search(searchOptions);
assertThat(result.getTotalHits()).isEqualTo(2);
}

// ---------- UTILITY ----------//

private ISearchService.Options searchOptions(
Expand All @@ -336,6 +381,18 @@ private ISearchService.Options searchOptions(
Integer requestedOffset,
String sortOrder,
String sortBy
) {
return searchOptions(queryString, category, requestedSize, requestedOffset, sortOrder, sortBy, null);
}

private ISearchService.Options searchOptions(
String queryString,
String category,
Integer requestedSize,
Integer requestedOffset,
String sortOrder,
String sortBy,
Boolean webOnly
) {
if(requestedSize == null) {
requestedSize = 18;
Expand All @@ -356,7 +413,9 @@ private ISearchService.Options searchOptions(
sortOrder,
sortBy,
false,
null
null,
null,
webOnly
);
}

Expand All @@ -370,6 +429,11 @@ long getIdFromExtensionName(String extensionName) {

private Extension mockExtension(String name, double averageRating, long ratingCount, int downloadCount,
String namespaceName, List<String> categories) {
return mockExtension(name, averageRating, ratingCount, downloadCount, namespaceName, categories, null);
}

private Extension mockExtension(String name, double averageRating, long ratingCount, int downloadCount,
String namespaceName, List<String> categories, List<String> extensionKind) {
var extension = new Extension();
extension.setName(name);
extension.setId(name.hashCode());
Expand All @@ -384,6 +448,7 @@ private Extension mockExtension(String name, double averageRating, long ratingCo
var extVer = new ExtensionVersion();
extVer.setTargetPlatform(TargetPlatform.NAME_UNIVERSAL);
extVer.setCategories(categories);
extVer.setExtensionKind(extensionKind);
extVer.setTimestamp(LocalDateTime.parse("2021-10-01T00:00"));
extVer.setActive(true);
extVer.setExtension(extension);
Expand Down