uglifyCssBundle();
/**
* Generate js bundle version for cache control.
diff --git a/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java b/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java
index e5c621c10..ab029aa69 100644
--- a/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java
+++ b/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java
@@ -9,7 +9,6 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -21,9 +20,13 @@ import org.apache.commons.lang3.Validate;
import org.pf4j.PluginWrapper;
import org.pf4j.RuntimeMode;
import org.springframework.core.io.Resource;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
+import org.springframework.util.StreamUtils;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
@@ -133,55 +136,52 @@ public class PluginServiceImpl implements PluginService {
}
@Override
- public Mono uglifyJsBundle() {
- return Mono.fromSupplier(() -> {
- StringBuilder jsBundle = new StringBuilder();
- List pluginNames = new ArrayList<>();
- for (PluginWrapper pluginWrapper : pluginManager.getStartedPlugins()) {
- String pluginName = pluginWrapper.getPluginId();
- pluginNames.add(pluginName);
- Resource jsBundleResource =
- BundleResourceUtils.getJsBundleResource(pluginManager, pluginName,
- BundleResourceUtils.JS_BUNDLE);
- if (jsBundleResource != null) {
- try {
- jsBundle.append(
- jsBundleResource.getContentAsString(StandardCharsets.UTF_8));
- jsBundle.append("\n");
- } catch (IOException e) {
- log.error("Failed to read js bundle of plugin [{}]", pluginName, e);
- }
+ public Flux uglifyJsBundle() {
+ var startedPlugins = List.copyOf(pluginManager.getStartedPlugins());
+ String plugins = """
+ this.enabledPluginNames = [%s];
+ """.formatted(startedPlugins.stream()
+ .map(PluginWrapper::getPluginId)
+ .collect(Collectors.joining("','", "'", "'")));
+ return Flux.fromIterable(startedPlugins)
+ .mapNotNull(pluginWrapper -> {
+ var pluginName = pluginWrapper.getPluginId();
+ return BundleResourceUtils.getJsBundleResource(pluginManager, pluginName,
+ BundleResourceUtils.JS_BUNDLE);
+ })
+ .flatMap(resource -> {
+ try {
+ // Specifying bufferSize as resource content length is
+ // to append line breaks at the end of each plugin
+ return DataBufferUtils.read(resource, DefaultDataBufferFactory.sharedInstance,
+ (int) resource.contentLength())
+ .doOnNext(dataBuffer -> {
+ // add a new line after each plugin bundle to avoid syntax error
+ dataBuffer.write("\n".getBytes(StandardCharsets.UTF_8));
+ });
+ } catch (IOException e) {
+ log.error("Failed to read plugin bundle resource", e);
+ return Flux.empty();
}
- }
-
- String plugins = """
- this.enabledPluginNames = [%s];
- """.formatted(pluginNames.stream()
- .collect(Collectors.joining("','", "'", "'")));
- return jsBundle + plugins;
- });
+ })
+ .concatWith(Flux.defer(() -> {
+ var dataBuffer = DefaultDataBufferFactory.sharedInstance
+ .wrap(plugins.getBytes(StandardCharsets.UTF_8));
+ return Flux.just(dataBuffer);
+ }));
}
@Override
- public Mono uglifyCssBundle() {
- return Mono.fromSupplier(() -> {
- StringBuilder cssBundle = new StringBuilder();
- for (PluginWrapper pluginWrapper : pluginManager.getStartedPlugins()) {
+ public Flux uglifyCssBundle() {
+ return Flux.fromIterable(pluginManager.getStartedPlugins())
+ .mapNotNull(pluginWrapper -> {
String pluginName = pluginWrapper.getPluginId();
- Resource cssBundleResource =
- BundleResourceUtils.getJsBundleResource(pluginManager, pluginName,
- BundleResourceUtils.CSS_BUNDLE);
- if (cssBundleResource != null) {
- try {
- cssBundle.append(
- cssBundleResource.getContentAsString(StandardCharsets.UTF_8));
- } catch (IOException e) {
- log.error("Failed to read css bundle of plugin [{}]", pluginName, e);
- }
- }
- }
- return cssBundle.toString();
- });
+ return BundleResourceUtils.getJsBundleResource(pluginManager, pluginName,
+ BundleResourceUtils.CSS_BUNDLE);
+ })
+ .flatMap(resource -> DataBufferUtils.read(resource,
+ DefaultDataBufferFactory.sharedInstance, StreamUtils.BUFFER_SIZE)
+ );
}
@Override
@@ -259,7 +259,7 @@ public class PluginServiceImpl implements PluginService {
if (!VersionUtils.satisfiesRequires(systemVersion, requires)) {
throw new UnsatisfiedAttributeValueException(String.format(
"Plugin requires a minimum system version of [%s], but the current version is "
- + "[%s].",
+ + "[%s].",
requires, systemVersion),
"problemDetail.plugin.version.unsatisfied.requires",
new String[] {requires, systemVersion});
diff --git a/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java b/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java
index c11e2c778..84305941d 100644
--- a/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java
+++ b/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java
@@ -1,6 +1,7 @@
package run.halo.app.core.extension.endpoint;
import static java.util.Objects.requireNonNull;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
@@ -16,6 +17,7 @@ import static org.springframework.web.reactive.function.BodyInserters.fromMultip
import com.github.zafarkhaja.semver.Version;
import java.io.IOException;
import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -33,11 +35,15 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.server.ServerWebInputException;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
import run.halo.app.core.extension.Plugin;
import run.halo.app.core.extension.Setting;
import run.halo.app.core.extension.service.PluginService;
@@ -130,8 +136,8 @@ class PluginEndpointTest {
verify(client).list(same(Plugin.class), argThat(
predicate -> predicate.test(expectPlugin)
- && !predicate.test(unexpectedPlugin1)
- && !predicate.test(unexpectedPlugin2)),
+ && !predicate.test(unexpectedPlugin1)
+ && !predicate.test(unexpectedPlugin2)),
any(), anyInt(), anyInt());
}
@@ -158,8 +164,8 @@ class PluginEndpointTest {
verify(client).list(same(Plugin.class), argThat(
predicate -> predicate.test(expectPlugin)
- && !predicate.test(unexpectedPlugin1)
- && !predicate.test(unexpectedPlugin2)),
+ && !predicate.test(unexpectedPlugin1)
+ && !predicate.test(unexpectedPlugin2)),
any(), anyInt(), anyInt());
}
@@ -382,4 +388,90 @@ class PluginEndpointTest {
plugin.setSpec(spec);
return plugin;
}
+
+ @Nested
+ class BufferedPluginBundleResourceTest {
+ private final PluginEndpoint.BufferedPluginBundleResource bufferedPluginBundleResource =
+ new PluginEndpoint.BufferedPluginBundleResource();
+
+ private static Flux getDataBufferFlux(String x) {
+ var buffer = DefaultDataBufferFactory.sharedInstance
+ .wrap(x.getBytes(StandardCharsets.UTF_8));
+ return Flux.just(buffer);
+ }
+
+ @Test
+ void writeAndGetJsResourceTest() {
+ bufferedPluginBundleResource.getJsBundle("1",
+ () -> getDataBufferFlux("first line\nnext line"))
+ .as(StepVerifier::create)
+ .consumeNextWith(resource -> {
+ try {
+ String content = resource.getContentAsString(StandardCharsets.UTF_8);
+ assertThat(content).isEqualTo("first line\nnext line");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .verifyComplete();
+
+ // version is matched, should return cached content
+ bufferedPluginBundleResource.getJsBundle("1",
+ () -> getDataBufferFlux("first line\nnext line-1"))
+ .as(StepVerifier::create)
+ .consumeNextWith(resource -> {
+ try {
+ String content = resource.getContentAsString(StandardCharsets.UTF_8);
+ assertThat(content).isEqualTo("first line\nnext line");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .verifyComplete();
+
+ // new version should return new content
+ bufferedPluginBundleResource.getJsBundle("2",
+ () -> getDataBufferFlux("first line\nnext line-2"))
+ .as(StepVerifier::create)
+ .consumeNextWith(resource -> {
+ try {
+ String content = resource.getContentAsString(StandardCharsets.UTF_8);
+ assertThat(content).isEqualTo("first line\nnext line-2");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ void writeAndGetCssResourceTest() {
+ bufferedPluginBundleResource.getCssBundle("1",
+ () -> getDataBufferFlux("first line\nnext line"))
+ .as(StepVerifier::create)
+ .consumeNextWith(resource -> {
+ try {
+ String content = resource.getContentAsString(StandardCharsets.UTF_8);
+ assertThat(content).isEqualTo("first line\nnext line");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .verifyComplete();
+
+ // version is matched, should return cached content
+ bufferedPluginBundleResource.getCssBundle("1",
+ () -> getDataBufferFlux("first line\nnext line-1"))
+ .as(StepVerifier::create)
+ .consumeNextWith(resource -> {
+ try {
+ String content = resource.getContentAsString(StandardCharsets.UTF_8);
+ assertThat(content).isEqualTo("first line\nnext line");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .verifyComplete();
+ }
+ }
}