Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
ruibaby committed Jul 1, 2024
2 parents 02ebe1d + 4ea4bdf commit 40e453d
Show file tree
Hide file tree
Showing 17 changed files with 463 additions and 204 deletions.
21 changes: 21 additions & 0 deletions api/src/main/java/run/halo/app/search/SearchService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package run.halo.app.search;

import reactor.core.publisher.Mono;

/**
* Search service is used to search content.
*
* @author johnniang
* @since 2.17.0
*/
public interface SearchService {

/**
* Perform search.
*
* @param option search option must not be null
* @return search result
*/
Mono<SearchResult> search(SearchOption option);

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import run.halo.app.plugin.event.SpringPluginStartedEvent;
import run.halo.app.plugin.event.SpringPluginStoppedEvent;
import run.halo.app.plugin.event.SpringPluginStoppingEvent;
import run.halo.app.search.SearchService;
import run.halo.app.theme.DefaultTemplateNameResolver;
import run.halo.app.theme.ViewNameResolver;
import run.halo.app.theme.finders.FinderRegistry;
Expand Down Expand Up @@ -136,6 +137,11 @@ public ApplicationContext create(String pluginId) {
);
});

rootContext.getBeanProvider(SearchService.class)
.ifUnique(searchService ->
beanFactory.registerSingleton("searchService", searchService)
);

sw.stop();

sw.start("LoadComponents");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public PluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping(
}

@Bean
public PluginManager pluginManager(ApplicationContext context,
public SpringPluginManager pluginManager(ApplicationContext context,
SystemVersionSupplier systemVersionSupplier,
PluginProperties pluginProperties) {
return new HaloPluginManager(context, pluginProperties, systemVersionSupplier);
Expand Down
25 changes: 4 additions & 21 deletions application/src/main/java/run/halo/app/search/IndexEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,24 @@
import java.util.List;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.stereotype.Component;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
import run.halo.app.infra.exception.RequestBodyValidationException;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.search.post.PostHaloDocumentsProvider;

@Component
public class IndexEndpoint implements CustomEndpoint {

private static final String API_VERSION = "api.halo.run/v1alpha1";

private final ExtensionGetter extensionGetter;
private final SearchService searchService;

private final Validator validator;

public IndexEndpoint(ExtensionGetter extensionGetter, Validator validator) {
this.extensionGetter = extensionGetter;
this.validator = validator;
public IndexEndpoint(SearchService searchService) {
this.searchService = searchService;
}

@Override
Expand Down Expand Up @@ -92,17 +85,7 @@ private Mono<SearchResult> performSearch(SearchOption option) {
option.setFilterExposed(true);
option.setFilterPublished(true);
option.setFilterRecycled(false);
// validate the option
var errors = validator.validateObject(option);
if (errors.hasErrors()) {
return Mono.error(new RequestBodyValidationException(errors));
}
return extensionGetter.getEnabledExtension(SearchEngine.class)
.filter(SearchEngine::available)
.switchIfEmpty(Mono.error(SearchEngineUnavailableException::new))
.flatMap(searchEngine -> Mono.fromSupplier(() ->
searchEngine.search(option)
).subscribeOn(Schedulers.boundedElastic()));
return searchService.search(option);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package run.halo.app.search;

import org.springframework.stereotype.Service;
import org.springframework.validation.Validator;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.infra.exception.RequestBodyValidationException;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;

@Service
public class SearchServiceImpl implements SearchService {

private final Validator validator;

private final ExtensionGetter extensionGetter;

public SearchServiceImpl(Validator validator, ExtensionGetter extensionGetter) {
this.validator = validator;
this.extensionGetter = extensionGetter;
}

@Override
public Mono<SearchResult> search(SearchOption option) {
// validate the option
var errors = validator.validateObject(option);
if (errors.hasErrors()) {
return Mono.error(new RequestBodyValidationException(errors));
}
return extensionGetter.getEnabledExtension(SearchEngine.class)
.filter(SearchEngine::available)
.switchIfEmpty(Mono.error(SearchEngineUnavailableException::new))
.flatMap(searchEngine -> Mono.fromSupplier(() ->
searchEngine.search(option)
).subscribeOn(Schedulers.boundedElastic()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package run.halo.app.plugin;

import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.pf4j.PluginWrapper;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import run.halo.app.search.SearchService;

@SpringBootTest
class DefaultPluginApplicationContextFactoryTest {

@SpyBean
SpringPluginManager pluginManager;

DefaultPluginApplicationContextFactory factory;

@BeforeEach
void setUp() {
factory = new DefaultPluginApplicationContextFactory((SpringPluginManager) pluginManager);
}

@Test
void shouldCreateCorrectly() {
var pw = mock(PluginWrapper.class);
when(pw.getPluginClassLoader()).thenReturn(this.getClass().getClassLoader());
var sp = mock(SpringPlugin.class);
var pluginContext = new PluginContext.PluginContextBuilder()
.name("fake-plugin")
.version("1.0.0")
.build();
when(sp.getPluginContext()).thenReturn(pluginContext);
when(pw.getPlugin()).thenReturn(sp);
when(pluginManager.getPlugin("fake-plugin")).thenReturn(pw);
var context = factory.create("fake-plugin");

assertInstanceOf(PluginApplicationContext.class, context);
assertNotNull(context.getBeanProvider(SearchService.class).getIfUnique());
// TODO Add more assertions here.
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,16 @@
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.server.handler.ResponseStatusExceptionHandler;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.infra.exception.RequestBodyValidationException;

@ExtendWith(MockitoExtension.class)
class IndexEndpointTest {

@Mock
ExtensionGetter extensionGetter;

@Mock
Validator validator;
SearchService searchService;

@InjectMocks
IndexEndpoint endpoint;
Expand All @@ -57,8 +53,8 @@ void shouldResponseBadRequestIfNotRequestBody() {
void shouldResponseBadRequestIfRequestBodyValidationFailed() {
var option = new SearchOption();
var errors = mock(Errors.class);
when(errors.hasErrors()).thenReturn(true);
when(validator.validateObject(any(SearchOption.class))).thenReturn(errors);
when(searchService.search(any(SearchOption.class)))
.thenReturn(Mono.error(new RequestBodyValidationException(errors)));

client.post().uri("/indices/-/search")
.bodyValue(option)
Expand All @@ -70,17 +66,8 @@ void shouldResponseBadRequestIfRequestBodyValidationFailed() {
void shouldSearchCorrectly() {
var option = new SearchOption();
option.setKeyword("halo");

var errors = mock(Errors.class);
when(errors.hasErrors()).thenReturn(false);
when(validator.validateObject(any(SearchOption.class))).thenReturn(errors);

var searchEngine = mock(SearchEngine.class);
when(searchEngine.available()).thenReturn(true);
var searchResult = new SearchResult();
when(searchEngine.search(any(SearchOption.class))).thenReturn(searchResult);
when(extensionGetter.getEnabledExtension(SearchEngine.class))
.thenReturn(Mono.just(searchEngine));
when(searchService.search(any(SearchOption.class))).thenReturn(Mono.just(searchResult));

client.post().uri("/indices/-/search")
.bodyValue(option)
Expand All @@ -89,7 +76,7 @@ void shouldSearchCorrectly() {
.expectBody(SearchResult.class)
.isEqualTo(searchResult);

verify(searchEngine).search(assertArg(o -> {
verify(searchService).search(assertArg(o -> {
assertEquals("halo", o.getKeyword());
// make sure the filters are overwritten
assertTrue(o.getFilterExposed());
Expand All @@ -101,15 +88,8 @@ void shouldSearchCorrectly() {
@Test
void shouldBeCompatibleWithOldSearchApi() {
var searchResult = new SearchResult();
var searchEngine = mock(SearchEngine.class);
when(searchEngine.available()).thenReturn(true);
when(searchEngine.search(any(SearchOption.class))).thenReturn(searchResult);
when(extensionGetter.getEnabledExtension(SearchEngine.class))
.thenReturn(Mono.just(searchEngine));

var errors = mock(Errors.class);
when(errors.hasErrors()).thenReturn(false);
when(validator.validateObject(any(SearchOption.class))).thenReturn(errors);
when(searchService.search(any(SearchOption.class)))
.thenReturn(Mono.just(searchResult));

client.get().uri(uriBuilder -> uriBuilder.path("/indices/post")
.queryParam("keyword", "halo")
Expand All @@ -119,7 +99,7 @@ void shouldBeCompatibleWithOldSearchApi() {
.expectBody(SearchResult.class)
.isEqualTo(searchResult);

verify(searchEngine).search(assertArg(o -> {
verify(searchService).search(assertArg(o -> {
assertEquals("halo", o.getKeyword());
// make sure the filters are overwritten
assertTrue(o.getFilterExposed());
Expand All @@ -130,14 +110,8 @@ void shouldBeCompatibleWithOldSearchApi() {

@Test
void shouldFailWhenSearchEngineIsUnavailable() {
var searchEngine = mock(SearchEngine.class);
when(searchEngine.available()).thenReturn(false);
when(extensionGetter.getEnabledExtension(SearchEngine.class))
.thenReturn(Mono.just(searchEngine));

var errors = mock(Errors.class);
when(errors.hasErrors()).thenReturn(false);
when(validator.validateObject(any(SearchOption.class))).thenReturn(errors);
when(searchService.search(any(SearchOption.class)))
.thenReturn(Mono.error(new SearchEngineUnavailableException()));

client.post().uri("/indices/-/search")
.bodyValue(new SearchOption())
Expand Down
Loading

0 comments on commit 40e453d

Please sign in to comment.