Skip to content

Commit

Permalink
refactor: using databuffer instead of string for plugin bundle resource
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Sep 27, 2023
1 parent e4a50e4 commit c2ff3c3
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 122 deletions.
Expand Up @@ -3,7 +3,6 @@
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static java.util.Comparator.comparing;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
Expand All @@ -22,7 +21,6 @@
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand All @@ -35,6 +33,9 @@
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
Expand All @@ -46,6 +47,7 @@
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Sort;
import org.springframework.http.CacheControl;
Expand All @@ -65,6 +67,7 @@
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
Expand Down Expand Up @@ -255,8 +258,8 @@ public RouterFunction<ServerResponse> endpoint() {
private Mono<ServerResponse> fetchJsBundle(ServerRequest request) {
Optional<String> versionOption = request.queryParam("v");
return versionOption.map(s ->
Mono.fromCallable(() -> bufferedPluginBundleResource
.jsBundleResource(s, () -> pluginService.uglifyJsBundle().block())
Mono.defer(() -> bufferedPluginBundleResource
.getJsBundle(s, pluginService::uglifyJsBundle)
).flatMap(fsRes -> {
var bodyBuilder = ServerResponse.ok()
.cacheControl(MAX_CACHE_CONTROL)
Expand Down Expand Up @@ -284,8 +287,8 @@ private Mono<ServerResponse> fetchJsBundle(ServerRequest request) {
private Mono<ServerResponse> fetchCssBundle(ServerRequest request) {
Optional<String> versionOption = request.queryParam("v");
return versionOption.map(s ->
Mono.fromCallable(() -> bufferedPluginBundleResource.cssBundleResource(s,
() -> pluginService.uglifyCssBundle().block())
Mono.defer(() -> bufferedPluginBundleResource.getCssBundle(s,
pluginService::uglifyCssBundle)
).flatMap(fsRes -> {
var bodyBuilder = ServerResponse.ok()
.cacheControl(MAX_CACHE_CONTROL)
Expand Down Expand Up @@ -672,91 +675,107 @@ private Mono<Path> writeToTempFile(Publisher<DataBuffer> content) {

@Component
static class BufferedPluginBundleResource implements DisposableBean {
private final Object jsBundleLock = new Object();
private final Object cssBundleLock = new Object();

private FileSystemResource jsBundleResource;
private FileSystemResource cssBundleResource;
private final AtomicReference<FileSystemResource> jsBundle = new AtomicReference<>();
private final AtomicReference<FileSystemResource> cssBundle = new AtomicReference<>();

private final ReadWriteLock jsLock = new ReentrantReadWriteLock();
private final ReadWriteLock cssLock = new ReentrantReadWriteLock();

private Path tempDir;

public FileSystemResource jsBundleResource(String v, Supplier<String> bundleSupplier) {
synchronized (jsBundleLock) {
var jsResource = writeAndGetBundleResource(jsBundleResource,
tempFileName(v, ".js"), bundleSupplier);
this.jsBundleResource = jsResource;
return jsResource;
}
public Mono<FileSystemResource> getJsBundle(String version,
Supplier<Flux<DataBuffer>> jsSupplier) {
var fileName = tempFileName(version, ".js");
return Mono.defer(() -> {
try {
jsLock.readLock().lock();
var jsBundleResource = jsBundle.get();
if (getResourceIfNotChange(fileName, jsBundleResource) != null) {
return Mono.just(jsBundleResource);
}
} finally {
jsLock.readLock().unlock();
}

jsLock.writeLock().lock();
try {
return writeBundle(fileName, jsSupplier)
.doOnNext(jsBundle::set);
} finally {
jsLock.writeLock().unlock();
}
}).subscribeOn(Schedulers.boundedElastic());
}

public FileSystemResource cssBundleResource(String v, Supplier<String> bundleSupplier) {
synchronized (cssBundleLock) {
var cssResource = writeAndGetBundleResource(cssBundleResource,
tempFileName(v, ".css"), bundleSupplier);
this.cssBundleResource = cssResource;
return cssResource;
}
public Mono<FileSystemResource> getCssBundle(String version,
Supplier<Flux<DataBuffer>> cssSupplier) {
var fileName = tempFileName(version, ".css");
return Mono.defer(() -> {
try {
cssLock.readLock().lock();
var cssBundleResource = cssBundle.get();
if (getResourceIfNotChange(fileName, cssBundleResource) != null) {
return Mono.just(cssBundleResource);
}
} finally {
cssLock.readLock().unlock();
}

cssLock.writeLock().lock();
try {
return writeBundle(fileName, cssSupplier)
.doOnNext(cssBundle::set);
} finally {
cssLock.writeLock().unlock();
}
}).subscribeOn(Schedulers.boundedElastic());
}

FileSystemResource writeAndGetBundleResource(FileSystemResource resource, String fileName,
Supplier<String> bundleSupplier) {
if (resourceDoesNotExist(resource)) {
return createAndWriteContent(fileName, bundleSupplier, null);
}
if (fileName.equals(resource.getFilename())) {
@Nullable
private Resource getResourceIfNotChange(String fileName, Resource resource) {
if (resource != null && resource.exists() && fileName.equals(resource.getFilename())) {
return resource;
}
return createAndWriteContent(fileName, bundleSupplier, resource);
return null;
}

FileSystemResource createAndWriteContent(String fileName, Supplier<String> bundleSupplier,
@Nullable FileSystemResource fileResource) {
try {
if (fileResource != null) {
Files.deleteIfExists(fileResource.getFile().toPath());
}
var filePath = generateStorePath(fileName);
var newResource = new FileSystemResource(filePath);

String content = defaultString(bundleSupplier.get());
Files.writeString(filePath, content, StandardCharsets.UTF_8);
return newResource;
} catch (IOException e) {
throw new ServerWebInputException("Failed to write bundle file.", null, e);
}
private Mono<FileSystemResource> writeBundle(String fileName,
Supplier<Flux<DataBuffer>> dataSupplier) {
return Mono.defer(
() -> {
var filePath = createTempFileToStore(fileName);
return DataBufferUtils.write(dataSupplier.get(), filePath)
.then(Mono.fromSupplier(() -> new FileSystemResource(filePath)));
});
}

Path generateStorePath(String fileName) {
private synchronized Path createTempFileToStore(String fileName) {
try {
if (tempDirDoesNotExist()) {
if (tempDir == null || !Files.exists(tempDir)) {
this.tempDir = Files.createTempDirectory("halo-plugin-bundle");
}
return Files.createFile(tempDir.resolve(fileName));
var path = tempDir.resolve(fileName);
Files.deleteIfExists(path);
return Files.createFile(path);
} catch (IOException e) {
throw new ServerWebInputException("Failed to create temp file.", null, e);
}
}

boolean tempDirDoesNotExist() {
return tempDir == null || !Files.exists(tempDir);
}

String tempFileName(String v, String suffix) {
private String tempFileName(String v, String suffix) {
Assert.notNull(v, "Version must not be null");
Assert.notNull(suffix, "Suffix must not be null");
return v + suffix;
}

boolean resourceDoesNotExist(Resource resource) {
return resource == null || !resource.exists();
}

@Override
public void destroy() throws Exception {
if (!tempDirDoesNotExist()) {
if (tempDir != null && Files.exists(tempDir)) {
FileSystemUtils.deleteRecursively(tempDir);
}
this.jsBundleResource = null;
this.cssBundleResource = null;
this.jsBundle.set(null);
this.cssBundle.set(null);
}
}
}
@@ -1,6 +1,7 @@
package run.halo.app.core.extension.service;

import java.nio.file.Path;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -46,14 +47,14 @@ public interface PluginService {
*
* @return uglified js bundle
*/
Mono<String> uglifyJsBundle();
Flux<DataBuffer> uglifyJsBundle();

/**
* Uglify css bundle from all enabled plugins to a single css bundle string.
*
* @return uglified css bundle
*/
Mono<String> uglifyCssBundle();
Flux<DataBuffer> uglifyCssBundle();

/**
* <p>Generate js bundle version for cache control.</p>
Expand Down
Expand Up @@ -9,7 +9,6 @@
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;
Expand All @@ -21,9 +20,13 @@
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;
Expand Down Expand Up @@ -133,55 +136,52 @@ public Mono<Plugin> reload(String name) {
}

@Override
public Mono<String> uglifyJsBundle() {
return Mono.fromSupplier(() -> {
StringBuilder jsBundle = new StringBuilder();
List<String> 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<DataBuffer> 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<String> uglifyCssBundle() {
return Mono.fromSupplier(() -> {
StringBuilder cssBundle = new StringBuilder();
for (PluginWrapper pluginWrapper : pluginManager.getStartedPlugins()) {
public Flux<DataBuffer> 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
Expand Down Expand Up @@ -259,7 +259,7 @@ private void satisfiesRequiresVersion(Plugin newPlugin) {
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});
Expand Down

0 comments on commit c2ff3c3

Please sign in to comment.