Skip to content

Commit

Permalink
初步实现 Undertow 与 nop-file 的集成支持(可选)
Browse files Browse the repository at this point in the history
  • Loading branch information
flytreeleft committed Jun 23, 2024
1 parent 2c44abc commit 45af491
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ nop:

orm:
init-database-schema: true
enable-tenant-by-default: true
enable-tenant-by-default: false
auto-add-tenant-col: true

datasource:
Expand All @@ -47,7 +47,8 @@ nop:
enabled: true

file:
#store-impl: oss
#store-impl: oss
store-dir: /tmp/nop-files

integration:
oss:
Expand Down
7 changes: 6 additions & 1 deletion nop-undertow/nop-undertow-starter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@
<groupId>io.github.entropy-cloud</groupId>
<artifactId>nop-graphql-core</artifactId>
</dependency>

<dependency>
<groupId>io.github.entropy-cloud</groupId>
<artifactId>nop-graphql-orm</artifactId>
</dependency>

<dependency>
<groupId>io.github.entropy-cloud</groupId>
<artifactId>nop-file-core</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.github.entropy-cloud</groupId>
<artifactId>nop-ioc</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package io.nop.undertow.service;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletionStage;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.nop.api.core.ApiConstants;
import io.nop.api.core.beans.ApiRequest;
import io.nop.api.core.beans.ApiResponse;
import io.nop.api.core.beans.WebContentBean;
import io.nop.api.core.context.ContextProvider;
import io.nop.api.core.util.FutureHelper;
import io.nop.commons.util.StringHelper;
import io.nop.core.exceptions.ErrorMessageManager;
import io.nop.core.lang.json.JsonTool;
import io.nop.file.core.AbstractGraphQLFileService;
import io.nop.file.core.DownloadRequestBean;
import io.nop.file.core.FileConstants;
import io.nop.file.core.MediaTypeHelper;
import io.nop.file.core.UploadRequestBean;
import io.nop.undertow.web.UndertowWebHelper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.EagerFormParsingHandler;
import io.undertow.server.handlers.form.FormData;
import io.undertow.util.Headers;

/**
* @author <a href="mailto:flytreeleft@crazydan.org">flytreeleft</a>
* @date 2024-06-23
*/
public class UndertowFileHandler extends AbstractGraphQLFileService implements HttpHandler {
private final static Pattern DOWNLOAD_URL_MATCHER = Pattern.compile("^"
+ FileConstants.PATH_DOWNLOAD
+ "/([^/\\\\]+)$");

private final HttpHandler next;
private final EagerFormParsingHandler formHandler;

public UndertowFileHandler(HttpHandler next) {
this.next = next;
// https://stackoverflow.com/questions/37839418/multipart-form-data-example-using-undertow#answer-46374193
this.formHandler = new EagerFormParsingHandler(this::handleUpload);
}

@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
UndertowContext.withExchange(exchange, () -> {
String path = exchange.getRequestPath();
String method = exchange.getRequestMethod().toString();

if (FileConstants.PATH_UPLOAD.equals(path) && "POST".equalsIgnoreCase(method)) {
this.formHandler.handleRequest(exchange);
return;
}

Matcher matcher = DOWNLOAD_URL_MATCHER.matcher(path);
if (matcher.matches()) {
String fileId = matcher.group(1);
String contentType = UndertowWebHelper.getQueryParam(exchange, "contentType");

handleDownload(exchange, fileId, contentType);
return;
}

this.next.handleRequest(exchange);
});
}

private void handleUpload(HttpServerExchange exchange) {
String locale = ContextProvider.currentLocale();

CompletionStage<ApiResponse<?>> future;
try {
future = UndertowWebHelper.consumeFormData(exchange, (files, params) -> {
// https://github.com/undertow-io/undertow/blob/main/core/src/test/java/io/undertow/server/handlers/form/MultipartFormDataParserTestCase.java
FormData.FormValue formFile = files.get(0);
String contentType = formFile.getHeaders().getFirst(Headers.CONTENT_TYPE);

InputStream is = formFile.getFileItem().getInputStream();
String fileName = StringHelper.fileFullName(formFile.getFileName());
String mimeType = MediaTypeHelper.getMimeType(contentType, StringHelper.fileExt(fileName));

UploadRequestBean req = new UploadRequestBean(is,
fileName,
formFile.getFileItem().getFileSize(),
mimeType);
req.setBizObjName(params.get(FileConstants.PARAM_BIZ_OBJ_NAME));
req.setFieldName(params.get(FileConstants.PARAM_FIELD_NAME));

return uploadAsync(buildApiRequest(exchange, req));
});
} catch (IOException e) {
future = FutureHelper.success(ErrorMessageManager.instance().buildResponse(locale, e));
}

future.thenAccept(resp -> sendData(resp.getHttpStatus(), resp));
}

private void handleDownload(HttpServerExchange exchange, String fileId, String contentType) {
DownloadRequestBean req = new DownloadRequestBean();
req.setFileId(fileId);
req.setContentType(contentType);

CompletionStage<ApiResponse<WebContentBean>> future = downloadAsync(buildApiRequest(exchange, req));

future.thenAccept(resp -> {
if (!resp.isOk()) {
int status = resp.getHttpStatus();
if (status == 0) {
status = 500;
}

sendData(status, resp);
} else {
sendData(resp.getHttpStatus(), resp.getData());
}
});
}

protected <T> ApiRequest<T> buildApiRequest(HttpServerExchange exchange, T data) {
ApiRequest<T> request = new ApiRequest<>();
request.setData(data);

exchange.getRequestHeaders().forEach(header -> {
String name = header.getHeaderName().toString().toLowerCase(Locale.ENGLISH);
if (shouldIgnoreHeader(name)) {
return;
}

request.setHeader(name, header.getFirst());
});

return request;
}

public void sendData(int status, Object data) {
HttpServerExchange exchange = UndertowContext.getExchange();

if (status == 0) {
status = 200;
}

Map<String, Object> headers = new HashMap<>();

Object body;
if (data instanceof WebContentBean) {
WebContentBean contentBean = (WebContentBean) data;

headers.put(ApiConstants.HEADER_CONTENT_TYPE, contentBean.getContentType());

if (!StringHelper.isEmpty(contentBean.getFileName())) {
String encoded = StringHelper.encodeURL(contentBean.getFileName());
headers.put("Content-Disposition", "attachment; filename=" + encoded);
}

body = contentBean.getContent();
} else {
body = JsonTool.stringify(data);
}

UndertowWebHelper.send(exchange, headers, body, status);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
package io.nop.undertow.service;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.nop.api.core.beans.ApiResponse;
import io.nop.api.core.beans.WebContentBean;
import io.nop.api.core.exceptions.NopException;
import io.nop.commons.util.IoHelper;
import io.nop.core.resource.IResource;
import io.nop.graphql.core.IGraphQLExecutionContext;
import io.nop.graphql.core.ast.GraphQLOperationType;
import io.nop.graphql.core.web.GraphQLWebService;
Expand Down Expand Up @@ -112,49 +106,22 @@ protected void handlePageQuery(HttpServerExchange exchange, String method, Strin
}

protected Void outputJson(Map<String, Object> headers, String body, int status) {
headers.put(Headers.CONTENT_TYPE_STRING, WebContentBean.CONTENT_TYPE_JSON);
HttpServerExchange exchange = UndertowContext.getExchange();

return sendData(headers, body, status);
}
headers.put(Headers.CONTENT_TYPE_STRING, WebContentBean.CONTENT_TYPE_JSON);

protected Void outputPageQuery(ApiResponse<?> response, IGraphQLExecutionContext gqlContext) {
WebContentBean contentBean = buildWebContent(response);
UndertowWebHelper.send(exchange, headers, body, status);

return consumeWebContent(response, contentBean, this::sendData);
return null;
}

protected Void sendData(Map<String, Object> headers, Object body, int status) {
protected Void outputPageQuery(ApiResponse<?> response, IGraphQLExecutionContext gqlContext) {
HttpServerExchange exchange = UndertowContext.getExchange();
WebContentBean contentBean = buildWebContent(response);

UndertowWebHelper.setResponseHeader(exchange, headers);
exchange.setStatusCode(status);

if (body instanceof IResource) {
InputStream is = ((IResource) body).getInputStream();
try {
UndertowWebHelper.send(exchange, is);
} finally {
IoHelper.safeCloseObject(is);
}
} else if (body instanceof File) {
InputStream is = null;
try {
is = new FileInputStream((File) body);

UndertowWebHelper.send(exchange, is);
} catch (Exception e) {
throw NopException.adapt(e);
} finally {
IoHelper.safeClose(is);
}
} else if (body instanceof byte[]) {
UndertowWebHelper.send(exchange, (byte[]) body);
} else if (body instanceof InputStream) {
UndertowWebHelper.send(exchange, (InputStream) body);
} else if (body instanceof String) {
UndertowWebHelper.send(exchange, (String) body);
}

return null;
return consumeWebContent(response, contentBean, (headers, body, status) -> {
UndertowWebHelper.send(exchange, headers, body, status);
return null;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import io.nop.api.core.ioc.BeanContainer;
import io.nop.api.core.util.FutureHelper;
import io.nop.api.core.util.OrderedComparator;
import io.nop.commons.util.ClassHelper;
import io.nop.http.api.server.HttpServerHelper;
import io.nop.http.api.server.IHttpServerContext;
import io.nop.http.api.server.IHttpServerFilter;
import io.nop.undertow.UndertowConfigs;
import io.nop.undertow.service.UndertowFileHandler;
import io.nop.undertow.service.UndertowGraphQLHandler;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
Expand Down Expand Up @@ -79,13 +81,23 @@ private HttpHandler createHandler() {
// 对包含预压缩的资源(.gz 后缀),则优先返回其对应的压缩文件
ResourceSupplier resourceSupplier = new PreCompressedResourceSupplier(resourceManager).addEncoding("gzip",
".gz");
HttpHandler resource = new ResourceHandler(resourceSupplier);
HttpHandler resourceHandler = new ResourceHandler(resourceSupplier);

// Note: ResourceHandler 会在新线程中执行后继的 HttpHandler,
// 从而导致在 Nop ContextProvider 中与当前线程绑定的变量无法在
// ResourceHandler 的后继中获取到,因此,必须先在当前线程中执行
// UndertowGraphQLHandler,再将未处理的请求交给 ResourceHandler
HttpHandler handler = new UndertowGraphQLHandler(resource);
HttpHandler handler = new UndertowGraphQLHandler(resourceHandler);

// 若运行环境引入了 nop-file,则启用文件上传和下载支持。
// Note: Undertow 没有扫描和自动注册机制,只能通过环境中是否存在特定的 class
// 来判断是否启用文件上传/下载能力
try {
ClassHelper.forName("io.nop.file.core.AbstractGraphQLFileService");

handler = new UndertowFileHandler(handler);
} catch (ClassNotFoundException ignore) {
}

// 启用对响应的压缩支持
if (UndertowConfigs.CFG_SERVER_COMPRESSION_ENABLED.get()) {
Expand Down
Loading

0 comments on commit 45af491

Please sign in to comment.