From 2f1c463ee37537a31ca64634719cf618496e3500 Mon Sep 17 00:00:00 2001 From: hellokaton Date: Mon, 9 May 2022 22:34:27 +0800 Subject: [PATCH] :art: improve static file handler --- .../main/java/com/hellokaton/blade/Blade.java | 83 ++++++++---------- .../blade/kit/reload/FileChangeDetector.java | 51 ++++++----- .../com/hellokaton/blade/mvc/BladeConst.java | 10 +-- .../blade/mvc/route/RouteMatcher.java | 4 +- .../hellokaton/blade/mvc/ui/ResponseType.java | 1 + .../blade/options/StaticOptions.java | 46 ++++++++++ .../blade/server/HttpServerHandler.java | 14 ++- .../blade/server/HttpServerInitializer.java | 23 ++++- .../hellokaton/blade/server/NettyServer.java | 5 -- .../blade/server/RouteMethodHandler.java | 69 +++++++-------- .../blade/server/StaticFileHandler.java | 23 +++-- .../java/com/hellokaton/blade/BladeTest.java | 16 ++-- .../main/java/com/example/Application.java | 8 +- .../src/main/resources/static/favicon.ico | Bin 0 -> 67646 bytes .../src/main/resources/templates/home.html | 3 +- 15 files changed, 215 insertions(+), 141 deletions(-) create mode 100644 blade-core/src/main/java/com/hellokaton/blade/options/StaticOptions.java create mode 100644 blade-examples/src/main/resources/static/favicon.ico diff --git a/blade-core/src/main/java/com/hellokaton/blade/Blade.java b/blade-core/src/main/java/com/hellokaton/blade/Blade.java index b7dafc70..7a45d4d3 100644 --- a/blade-core/src/main/java/com/hellokaton/blade/Blade.java +++ b/blade-core/src/main/java/com/hellokaton/blade/Blade.java @@ -30,14 +30,12 @@ import com.hellokaton.blade.mvc.hook.WebHook; import com.hellokaton.blade.mvc.http.HttpMethod; import com.hellokaton.blade.mvc.http.session.SessionManager; -import com.hellokaton.blade.mvc.route.DynamicMapping; import com.hellokaton.blade.mvc.route.RouteMatcher; -import com.hellokaton.blade.mvc.route.mapping.dynamic.RegexMapping; -import com.hellokaton.blade.mvc.route.mapping.dynamic.TrieMapping; import com.hellokaton.blade.mvc.ui.template.DefaultEngine; import com.hellokaton.blade.mvc.ui.template.TemplateEngine; import com.hellokaton.blade.options.CorsOptions; import com.hellokaton.blade.options.HttpOptions; +import com.hellokaton.blade.options.StaticOptions; import com.hellokaton.blade.server.NettyServer; import com.hellokaton.blade.server.Server; import lombok.AccessLevel; @@ -54,6 +52,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import static com.hellokaton.blade.mvc.BladeConst.ENV_KEY_FAVICON_DIR; + /** * Blade Core *

@@ -78,13 +78,6 @@ public class Blade { */ private final Set packages = new LinkedHashSet<>(BladeConst.PLUGIN_PACKAGE_NAME); - /** - * All static resource URL prefixes, - * defaults to "/favicon.ico", "/robots.txt", "/static/", "/upload/", "/webjars/", - * which are located under classpath - */ - private final Set statics = new HashSet<>(BladeConst.DEFAULT_STATICS); - /** * The default IOC container implementation */ @@ -121,7 +114,8 @@ public class Blade { private final RouteMatcher routeMatcher = new RouteMatcher(); private CorsOptions corsOptions = null; - private final HttpOptions httpOptions = HttpOptions.create(); + private HttpOptions httpOptions = HttpOptions.create(); + private StaticOptions staticOptions = StaticOptions.create(); /** * Blade environment, which stores the parameters of the application.properties configuration file @@ -280,6 +274,17 @@ public TemplateEngine templateEngine() { return this.templateEngine; } + /** + * setting favicon dir, default is /static + * + * @param faviconDir favicon dir + * @return blade instance + */ + public Blade faviconDir(String faviconDir) { + this.setEnv(ENV_KEY_FAVICON_DIR, faviconDir); + return this; + } + /** * Get RouteMatcher * @@ -294,6 +299,21 @@ public Blade http(Consumer consumer) { return this; } + public Blade http(HttpOptions httpOptions) { + this.httpOptions = httpOptions; + return this; + } + + public Blade staticOptions(Consumer consumer) { + consumer.accept(this.staticOptions); + return this; + } + + public Blade staticOptions(StaticOptions staticOptions) { + this.staticOptions = staticOptions; + return this; + } + public Blade cors(CorsOptions corsOptions) { this.corsOptions = corsOptions; return this; @@ -307,6 +327,10 @@ public HttpOptions httpOptions() { return this.httpOptions; } + public StaticOptions staticOptions() { + return this.staticOptions; + } + /** * Register bean to ioc container * @@ -329,29 +353,6 @@ public Blade register(@NonNull Class cls) { return this; } - /** - * Add multiple static resource file - * the default provides the static, upload - * - * @param folders static resource directory - * @return blade - */ - public Blade addStatics(@NonNull String... folders) { - this.statics.addAll(Arrays.asList(folders)); - return this; - } - - /** - * Set whether to show the file directory, default doesn't show - * - * @param fileList show the file directory - * @return blade - */ - public Blade showFileList(boolean fileList) { - this.environment.set(BladeConst.ENV_KEY_STATIC_LIST, fileList); - return this; - } - /** * Get ioc bean * @@ -415,16 +416,6 @@ public Class bootClass() { return this.bootClass; } - /** - * Get blade statics list. - * e.g: "/favicon.ico", "/robots.txt", "/static/", "/upload/", "/webjars/" - * - * @return return statics - */ - public Set getStatics() { - return this.statics; - } - /** * When set to start blade scan packages * @@ -686,8 +677,8 @@ public Blade start(Class mainCls, String... args) { try { //TODO: add support for Create and Delete if (event.equals(StandardWatchEventKinds.ENTRY_MODIFY)) { - Path destPath = FileChangeDetector.getDestPath(filePath, environment); - Files.copy(filePath, destPath, StandardCopyOption.REPLACE_EXISTING); + Path destPath = FileChangeDetector.getDestPath(filePath, environment, staticOptions); + Files.copy(filePath, Objects.requireNonNull(destPath), StandardCopyOption.REPLACE_EXISTING); } } catch (IOException e) { log.error("Exception when trying to copy updated file"); diff --git a/blade-core/src/main/java/com/hellokaton/blade/kit/reload/FileChangeDetector.java b/blade-core/src/main/java/com/hellokaton/blade/kit/reload/FileChangeDetector.java index 4f018c1e..45e0c9f9 100644 --- a/blade-core/src/main/java/com/hellokaton/blade/kit/reload/FileChangeDetector.java +++ b/blade-core/src/main/java/com/hellokaton/blade/kit/reload/FileChangeDetector.java @@ -2,12 +2,16 @@ import com.hellokaton.blade.Environment; import com.hellokaton.blade.mvc.BladeConst; +import com.hellokaton.blade.options.StaticOptions; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.function.BiConsumer; import java.util.stream.Collectors; @@ -19,18 +23,18 @@ public class FileChangeDetector { private final WatchService watcher; private final Map pathMap = new HashMap<>(); - public FileChangeDetector(String dirPath) throws IOException{ + public FileChangeDetector(String dirPath) throws IOException { watcher = FileSystems.getDefault().newWatchService(); registerAll(Paths.get(dirPath)); } - private void register(Path dir) throws IOException{ + private void register(Path dir) throws IOException { WatchKey key = dir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY); - pathMap.put(key,dir); + pathMap.put(key, dir); } - private void registerAll(Path dir) throws IOException{ - Files.walkFileTree(dir, new SimpleFileVisitor(){ + private void registerAll(Path dir) throws IOException { + Files.walkFileTree(dir, new SimpleFileVisitor() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { register(dir); @@ -39,22 +43,22 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th }); } - public void processEvent(BiConsumer, Path> processor){ - for(;;){ + public void processEvent(BiConsumer, Path> processor) { + for (; ; ) { WatchKey key; - try{ + try { key = watcher.take(); - }catch (InterruptedException e) { + } catch (InterruptedException e) { return; } Path dir = pathMap.get(key); - for (WatchEvent event: key.pollEvents()){ + for (WatchEvent event : key.pollEvents()) { WatchEvent.Kind kind = event.kind(); - Path filePath = dir.resolve(((WatchEvent)event).context()); + Path filePath = dir.resolve(((WatchEvent) event).context()); - if(Files.isDirectory(filePath)) continue; - log.info("File {} changes detected!",filePath.toString()); + if (Files.isDirectory(filePath)) continue; + log.info("File {} changes detected!", filePath.toString()); //copy updated files to target processor.accept(kind, filePath); } @@ -62,23 +66,18 @@ public void processEvent(BiConsumer, Path> processor){ } } - public static Path getDestPath(Path src, Environment env){ - String templateDir = env.get(BladeConst.ENV_KEY_TEMPLATE_PATH,"/templates"); - Optional staticDir = env.get(BladeConst.ENV_KEY_STATIC_DIRS); + public static Path getDestPath(Path src, Environment env, StaticOptions staticOptions) { + String templateDir = env.get(BladeConst.ENV_KEY_TEMPLATE_PATH, "/templates"); List templateOrStaticDirKeyword = new ArrayList<>(); templateOrStaticDirKeyword.add(templateDir); - if(staticDir.isPresent()){ - templateOrStaticDirKeyword.addAll(Arrays.asList(staticDir.get().split(","))); - }else{ - templateOrStaticDirKeyword.addAll(BladeConst.DEFAULT_STATICS); - } + templateOrStaticDirKeyword.addAll(staticOptions.getPaths()); - List result = templateOrStaticDirKeyword.stream().filter(dir -> src.toString().indexOf(dir)!=-1).collect(Collectors.toList()); - if(result.size()!=1){ + List result = templateOrStaticDirKeyword.stream().filter(dir -> src.toString().contains(dir)).collect(Collectors.toList()); + if (result.size() != 1) { log.info("Cannot get dest dir"); - return null; + return null; } - String key = (String)result.get(0); + String key = result.get(0); log.info(BladeConst.CLASSPATH + src.toString().substring(src.toString().indexOf(key))); return Paths.get(BladeConst.CLASSPATH + src.toString().substring(src.toString().indexOf(key))); } diff --git a/blade-core/src/main/java/com/hellokaton/blade/mvc/BladeConst.java b/blade-core/src/main/java/com/hellokaton/blade/mvc/BladeConst.java index a72b7b79..5919b71f 100644 --- a/blade-core/src/main/java/com/hellokaton/blade/mvc/BladeConst.java +++ b/blade-core/src/main/java/com/hellokaton/blade/mvc/BladeConst.java @@ -17,7 +17,6 @@ import com.hellokaton.blade.kit.StringKit; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -38,8 +37,6 @@ public interface BladeConst { String INTERNAL_SERVER_ERROR_HTML = "

500 Internal Server Error


"; String DEFAULT_THREAD_NAME = "_(:3」∠)_"; List PLUGIN_PACKAGE_NAME = new ArrayList<>(Collections.singletonList("com.hellokaton.blade.plugin")); - List DEFAULT_STATICS = new ArrayList<>( - Arrays.asList("/favicon.ico", "/robots.txt", "/static", "/upload", "/webjars/")); String PROP_NAME = "classpath:application.properties"; @@ -53,17 +50,17 @@ public interface BladeConst { String ENV_KEY_TASK_THREAD_COUNT = "app.task.thread-count"; String ENV_KEY_CONTEXT_PATH = "app.context-path"; String ENV_KEY_REQUEST_LOG = "app.request-log"; + String ENV_KEY_FAVICON_DIR = "app.favicon-dir"; String ENV_KEY_HTTP_MAX_CONTENT = "http.max-content-size"; String ENV_KEY_GZIP_ENABLE = "http.gzip.enabled"; String ENV_KEY_SESSION_ENABLED = "http.session.enabled"; String ENV_KEY_SESSION_KEY = "http.session.key"; String ENV_KEY_SESSION_TIMEOUT = "http.session.timeout"; - String ENV_KEY_HTTP_CACHE_TIMEOUT = "http.cache.timeout"; String ENV_KEY_HTTP_REQUEST_COST = "http.request.cost"; String ENV_KEY_PAGE_404 = "mvc.view.404"; String ENV_KEY_PAGE_500 = "mvc.view.500"; - String ENV_KEY_STATIC_DIRS = "mvc.statics"; - String ENV_KEY_STATIC_LIST = "mvc.statics.show-list"; + String ENV_KEY_STATIC_LIST = "static.show-list"; + String ENV_KEY_STATIC_CACHE_SECONDS = "static.cache-seconds"; String ENV_KEY_TEMPLATE_PATH = "mvc.template.path"; String ENV_KEY_SERVER_ADDRESS = "server.address"; String ENV_KEY_SERVER_PORT = "server.port"; @@ -88,6 +85,7 @@ public interface BladeConst { String REQUEST_TO_STATIC_ATTR = "_to_static"; + String FAVICON_PATH = "/favicon.ico"; String NEW_LINE = "\r\n"; int BANNER_PADDING = 60; diff --git a/blade-core/src/main/java/com/hellokaton/blade/mvc/route/RouteMatcher.java b/blade-core/src/main/java/com/hellokaton/blade/mvc/route/RouteMatcher.java index 8863f81c..dd09b35b 100644 --- a/blade-core/src/main/java/com/hellokaton/blade/mvc/route/RouteMatcher.java +++ b/blade-core/src/main/java/com/hellokaton/blade/mvc/route/RouteMatcher.java @@ -159,7 +159,7 @@ public List getBefore(String path) { .flatMap(Collection::stream) .sorted(Comparator.comparingInt(Route::getSort)) .filter(route -> route.getHttpMethod() == HttpMethod.BEFORE) - .filter(route -> matchesPath(route.getPath(), cleanPath)) + .filter(route -> matchesPath(route.getRewritePath(), cleanPath)) .collect(Collectors.toList()); this.giveMatch(path, collect); @@ -178,7 +178,7 @@ public List getAfter(String path) { .flatMap(Collection::stream) .sorted(Comparator.comparingInt(Route::getSort)) .filter(route -> route.getHttpMethod() == HttpMethod.AFTER) - .filter(route -> matchesPath(route.getPath(), cleanPath)) + .filter(route -> matchesPath(route.getRewritePath(), cleanPath)) .collect(Collectors.toList()); this.giveMatch(path, afters); diff --git a/blade-core/src/main/java/com/hellokaton/blade/mvc/ui/ResponseType.java b/blade-core/src/main/java/com/hellokaton/blade/mvc/ui/ResponseType.java index d8b0a7ae..68aaadd8 100644 --- a/blade-core/src/main/java/com/hellokaton/blade/mvc/ui/ResponseType.java +++ b/blade-core/src/main/java/com/hellokaton/blade/mvc/ui/ResponseType.java @@ -12,6 +12,7 @@ public enum ResponseType { XML(HttpConst.CONTENT_TYPE_XML), TEXT(HttpConst.CONTENT_TYPE_TEXT), HTML(HttpConst.CONTENT_TYPE_HTML), + VIEW(HttpConst.CONTENT_TYPE_HTML), STREAM(HttpConst.CONTENT_TYPE_STREAM), PREVIEW(""), ; diff --git a/blade-core/src/main/java/com/hellokaton/blade/options/StaticOptions.java b/blade-core/src/main/java/com/hellokaton/blade/options/StaticOptions.java new file mode 100644 index 00000000..4f3044df --- /dev/null +++ b/blade-core/src/main/java/com/hellokaton/blade/options/StaticOptions.java @@ -0,0 +1,46 @@ +package com.hellokaton.blade.options; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +@Getter +@Setter +public class StaticOptions { + + public static final int DEFAULT_CACHE_SECONDS = 86400 * 30; + public static final Set DEFAULT_STATICS = new HashSet<>( + Arrays.asList("/favicon.ico", "/robots.txt", "/static", "/webjars/")); + + private boolean showList; + private Set paths = DEFAULT_STATICS; + private int cacheSeconds = DEFAULT_CACHE_SECONDS; + + public static StaticOptions create() { + return new StaticOptions(); + } + + public StaticOptions showList() { + this.showList = true; + return this; + } + + public StaticOptions addStatic(String staticPath) { + this.paths.add(staticPath); + return this; + } + + public StaticOptions removeStatic(String staticPath) { + this.paths.remove(staticPath); + return this; + } + + public StaticOptions cacheSeconds(int cacheSeconds) { + this.cacheSeconds = cacheSeconds; + return this; + } + +} diff --git a/blade-core/src/main/java/com/hellokaton/blade/server/HttpServerHandler.java b/blade-core/src/main/java/com/hellokaton/blade/server/HttpServerHandler.java index f584b472..b1435e42 100644 --- a/blade-core/src/main/java/com/hellokaton/blade/server/HttpServerHandler.java +++ b/blade-core/src/main/java/com/hellokaton/blade/server/HttpServerHandler.java @@ -59,10 +59,12 @@ public class HttpServerHandler extends SimpleChannelInboundHandler public static final FastThreadLocal WEB_CONTEXT_THREAD_LOCAL = new FastThreadLocal<>(); - private final StaticFileHandler staticFileHandler = new StaticFileHandler(WebContext.blade()); + static final StaticFileHandler staticFileHandler = new StaticFileHandler(WebContext.blade()); + + static final RouteMatcher routeMatcher = WebContext.blade().routeMatcher(); + private final RouteMethodHandler routeHandler = new RouteMethodHandler(); private final Set notStaticUri = new LRUSet<>(128); - private final RouteMatcher routeMatcher = WebContext.blade().routeMatcher(); private boolean recordRequestLog() { return WebContext.blade().environment() @@ -174,10 +176,16 @@ private boolean isStaticFile(HttpMethod method, String uri) { return false; } - if (WebContext.blade().getStatics().stream().noneMatch(s -> s.equals(uri) || uri.startsWith(s))) { + if (FAVICON_PATH.equals(uri)) { + return true; + } + + Set paths = WebContext.blade().staticOptions().getPaths(); + if (paths.stream().noneMatch(s -> s.equals(uri) || uri.startsWith(s))) { notStaticUri.add(uri); return false; } + return true; } diff --git a/blade-core/src/main/java/com/hellokaton/blade/server/HttpServerInitializer.java b/blade-core/src/main/java/com/hellokaton/blade/server/HttpServerInitializer.java index be8c6f0e..42ebda4e 100644 --- a/blade-core/src/main/java/com/hellokaton/blade/server/HttpServerInitializer.java +++ b/blade-core/src/main/java/com/hellokaton/blade/server/HttpServerInitializer.java @@ -5,6 +5,7 @@ import com.hellokaton.blade.kit.DateKit; import com.hellokaton.blade.options.CorsOptions; import com.hellokaton.blade.options.HttpOptions; +import com.hellokaton.blade.options.StaticOptions; import com.hellokaton.blade.server.decode.FullHttpRequestDecode; import com.hellokaton.blade.server.decode.HttpObjectAggregatorDecode; import io.netty.channel.ChannelInitializer; @@ -34,11 +35,11 @@ public class HttpServerInitializer extends ChannelInitializer { private final HttpServerHandler httpServerHandler; - private final SslContext sslCtx; private CorsConfig corsConfig; private int maxContentSize; private boolean enableGzip; + public static volatile String date = DateKit.gmtDate(LocalDateTime.now()); @@ -46,10 +47,12 @@ public HttpServerInitializer(SslContext sslCtx, Blade blade, ScheduledExecutorSe this.sslCtx = sslCtx; this.httpServerHandler = new HttpServerHandler(); this.mergeCorsConfig(blade.corsOptions()); + this.mergeStaticOptions(blade.staticOptions(), blade.environment()); this.mergeHttpOptions(blade.httpOptions(), blade.environment()); service.scheduleWithFixedDelay(() -> date = DateKit.gmtDate(LocalDateTime.now()), 1000, 1000, TimeUnit.MILLISECONDS); } + @Override protected void initChannel(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); @@ -114,6 +117,24 @@ private void mergeCorsConfig(CorsOptions corsOptions) { this.corsConfig = corsConfigBuilder.build(); } + private void mergeStaticOptions(StaticOptions staticOptions, Environment environment) { + int cacheSeconds = staticOptions.getCacheSeconds(); + if (cacheSeconds > 0 && StaticOptions.DEFAULT_CACHE_SECONDS != cacheSeconds) { + environment.set(ENV_KEY_STATIC_CACHE_SECONDS, cacheSeconds); + } else { + cacheSeconds = environment.getInt(ENV_KEY_STATIC_CACHE_SECONDS, StaticOptions.DEFAULT_CACHE_SECONDS); + staticOptions.setCacheSeconds(cacheSeconds); + } + + boolean showList = staticOptions.isShowList(); + if (showList) { + environment.set(ENV_KEY_STATIC_LIST, true); + } else { + showList = environment.getBoolean(ENV_KEY_STATIC_LIST, Boolean.FALSE); + staticOptions.setShowList(showList); + } + } + private void mergeHttpOptions(HttpOptions httpOptions, Environment environment) { this.maxContentSize = httpOptions.getMaxContentSize(); this.enableGzip = httpOptions.isEnableGzip(); diff --git a/blade-core/src/main/java/com/hellokaton/blade/server/NettyServer.java b/blade-core/src/main/java/com/hellokaton/blade/server/NettyServer.java index 9d09748c..4f131fec 100644 --- a/blade-core/src/main/java/com/hellokaton/blade/server/NettyServer.java +++ b/blade-core/src/main/java/com/hellokaton/blade/server/NettyServer.java @@ -361,11 +361,6 @@ private void initConfig() { // print banner text this.printBanner(); - String statics = environment.get(ENV_KEY_STATIC_DIRS, ""); - if (StringKit.isNotBlank(statics)) { - blade.addStatics(statics.split(",")); - } - String templatePath = environment.get(ENV_KEY_TEMPLATE_PATH, "templates"); if (templatePath.charAt(0) == NettyHttpConst.CHAR_SLASH) { templatePath = templatePath.substring(1); diff --git a/blade-core/src/main/java/com/hellokaton/blade/server/RouteMethodHandler.java b/blade-core/src/main/java/com/hellokaton/blade/server/RouteMethodHandler.java index 25d6b8ab..968ae680 100644 --- a/blade-core/src/main/java/com/hellokaton/blade/server/RouteMethodHandler.java +++ b/blade-core/src/main/java/com/hellokaton/blade/server/RouteMethodHandler.java @@ -44,15 +44,13 @@ /** * Http Server Handler * - * @author biezhi - * 2017/5/31 + * @author hellokaton + * 2022/5/9 */ @Slf4j public class RouteMethodHandler implements RequestHandler { private final RouteMatcher routeMatcher = WebContext.blade().routeMatcher(); - private final StaticFileHandler staticFileHandler = new StaticFileHandler(WebContext.blade()); - private final boolean hasBeforeHook = routeMatcher.hasBeforeHook(); private final boolean hasAfterHook = routeMatcher.hasAfterHook(); @@ -130,7 +128,7 @@ public HttpResponse onView(ViewBody body) { public HttpResponse onStatic(StaticFileBody body) { request.attribute(REQUEST_TO_STATIC_ATTR, body.path()); try { - staticFileHandler.handle(webContext); + HttpServerHandler.staticFileHandler.handle(webContext); } catch (Exception e) { throw new RuntimeException(e); } @@ -231,16 +229,34 @@ private void routeHandle(RouteContext context) { Path path = target.getClass().getAnnotation(Path.class); - boolean responseJson = this.setResponseType(context, path); + ResponseType responseType = ResponseType.EMPTY; + if (null != path && !ResponseType.EMPTY.equals(path.responseType())) { + responseType = path.responseType(); + } + ResponseType routeResponseType = context.route().getResponseType(); + if (null != routeResponseType && !ResponseType.EMPTY.equals(routeResponseType)) { + responseType = routeResponseType; + } + + boolean responseJson = ResponseType.JSON.equals(responseType); + if (responseJson) { + if (!context.isIE()) { + context.contentType(HttpConst.CONTENT_TYPE_JSON); + } else { + context.contentType(HttpConst.CONTENT_TYPE_HTML); + } + } else if (null != responseType && !ResponseType.EMPTY.equals(responseType) + && !ResponseType.PREVIEW.equals(responseType)) { + context.contentType(responseType.contentType()); + } int len = actionMethod.getParameterTypes().length; MethodAccess methodAccess = BladeCache.getMethodAccess(target.getClass()); - Object returnParam = methodAccess.invoke( - target, actionMethod.getName(), len > 0 ? - context.routeParameters() : null); + Object[] args = len > 0 ? context.routeParameters() : null; + Object returnParam = methodAccess.invoke(target, actionMethod.getName(), args); if (null == returnParam) { return; } @@ -250,9 +266,15 @@ private void routeHandle(RouteContext context) { return; } if (returnType == String.class) { - context.body( - ViewBody.of(new ModelAndView(returnParam.toString())) - ); + if (ResponseType.VIEW.equals(responseType)) { + context.body( + ViewBody.of(new ModelAndView(returnParam.toString())) + ); + } else if (ResponseType.HTML.equals(responseType)) { + context.html(returnParam.toString()); + } else if (ResponseType.TEXT.equals(responseType)) { + context.text(returnParam.toString()); + } return; } if (returnType == ModelAndView.class) { @@ -266,29 +288,6 @@ private void routeHandle(RouteContext context) { } } - private boolean setResponseType(RouteContext context, Path path) { - ResponseType responseType = ResponseType.EMPTY; - if (null != path && !ResponseType.EMPTY.equals(path.responseType())) { - responseType = path.responseType(); - } - ResponseType routeResponseType = context.route().getResponseType(); - if (null != routeResponseType && !ResponseType.EMPTY.equals(routeResponseType)) { - responseType = routeResponseType; - } - boolean responseJson = ResponseType.JSON.equals(responseType); - if (responseJson) { - if (!context.isIE()) { - context.contentType(HttpConst.CONTENT_TYPE_JSON); - } else { - context.contentType(HttpConst.CONTENT_TYPE_HTML); - } - } else if (null != routeResponseType && !ResponseType.EMPTY.equals(routeResponseType) - && !ResponseType.PREVIEW.equals(routeResponseType)) { - context.contentType(routeResponseType.contentType()); - } - return responseJson; - } - /** * Invoke WebHook * diff --git a/blade-core/src/main/java/com/hellokaton/blade/server/StaticFileHandler.java b/blade-core/src/main/java/com/hellokaton/blade/server/StaticFileHandler.java index 68e347c6..3d11fcf9 100644 --- a/blade-core/src/main/java/com/hellokaton/blade/server/StaticFileHandler.java +++ b/blade-core/src/main/java/com/hellokaton/blade/server/StaticFileHandler.java @@ -54,8 +54,7 @@ import java.util.regex.Pattern; import static com.hellokaton.blade.kit.BladeKit.*; -import static com.hellokaton.blade.mvc.BladeConst.HTTP_DATE_FORMAT; -import static com.hellokaton.blade.mvc.BladeConst.REQUEST_TO_STATIC_ATTR; +import static com.hellokaton.blade.mvc.BladeConst.*; import static io.netty.handler.codec.http.HttpResponseStatus.*; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; @@ -81,11 +80,11 @@ public class StaticFileHandler implements RequestHandler { /** * default cache 30 days. */ - private final int httpCacheSeconds; + private final int staticFileCacheSeconds; public StaticFileHandler(Blade blade) { this.showFileList = blade.environment().getBoolean(BladeConst.ENV_KEY_STATIC_LIST, false); - this.httpCacheSeconds = blade.environment().getInt(BladeConst.ENV_KEY_HTTP_CACHE_TIMEOUT, 86400 * 30); + this.staticFileCacheSeconds = blade.environment().getInt(BladeConst.ENV_KEY_STATIC_CACHE_SECONDS, 86400 * 30); } /** @@ -98,7 +97,7 @@ public void handle(WebContext webContext) throws Exception { Request request = webContext.getRequest(); ChannelHandlerContext ctx = webContext.getChannelHandlerContext(); - if (!com.hellokaton.blade.mvc.http.HttpMethod.GET.name().equals(request.method())) { + if (!HttpMethod.GET.name().equals(request.method())) { sendError(ctx, METHOD_NOT_ALLOWED); return; } @@ -114,6 +113,12 @@ public void handle(WebContext webContext) throws Exception { } else { uri = URLDecoder.decode(request.uri(), "UTF-8"); } + + if (FAVICON_PATH.equals(uri)) { + String dir = WebContext.blade().getEnv(ENV_KEY_FAVICON_DIR, "/static"); + uri = dir + uri; + } + String method = StringKit.padRight(request.method(), 6); String cleanURL = getCleanURL(request, uri); @@ -386,7 +391,7 @@ private boolean writeJarResource(ChannelHandlerContext ctx, Request request, private boolean executeHttp304(ChannelHandlerContext ctx, Request request, long size, long lastModified) { String ifModifiedSince = request.header(HttpConst.HEADER_IF_MODIFIED_SINCE); - if (StringKit.isNotEmpty(ifModifiedSince) && httpCacheSeconds > 0) { + if (StringKit.isNotEmpty(ifModifiedSince) && staticFileCacheSeconds > 0) { Date ifModifiedSinceDate = format(ifModifiedSince, HTTP_DATE_FORMAT); @@ -436,11 +441,11 @@ private void setDateAndCacheHeaders(HttpResponse response, File fileToCache) { response.headers().set(HttpHeaderNames.DATE, dateFormatter.format(time.getTime())); // Add cache headers - if (httpCacheSeconds > 0) { - time.add(Calendar.SECOND, httpCacheSeconds); + if (staticFileCacheSeconds > 0) { + time.add(Calendar.SECOND, staticFileCacheSeconds); response.headers().set(HttpHeaderNames.EXPIRES, dateFormatter.format(time.getTime())); - response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + httpCacheSeconds); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + staticFileCacheSeconds); response.headers().set( HttpHeaderNames.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified()))); } diff --git a/blade-core/src/test/java/com/hellokaton/blade/BladeTest.java b/blade-core/src/test/java/com/hellokaton/blade/BladeTest.java index f9a20848..6e99b7d0 100644 --- a/blade-core/src/test/java/com/hellokaton/blade/BladeTest.java +++ b/blade-core/src/test/java/com/hellokaton/blade/BladeTest.java @@ -8,6 +8,7 @@ import com.hellokaton.blade.mvc.http.HttpSession; import com.hellokaton.blade.mvc.ui.template.TemplateEngine; import com.hellokaton.blade.options.HttpOptions; +import com.hellokaton.blade.options.StaticOptions; import com.hellokaton.blade.types.BladeClassDefineType; import com.mashape.unirest.http.Unirest; import netty_hello.Hello; @@ -120,11 +121,14 @@ public void testRegister() { @Test public void testAddStatics() { Blade blade = Blade.create(); - blade.addStatics("/assets/", "/public"); + blade.staticOptions(options -> { + options.addStatic("/assets/"); + options.addStatic("/assets/"); + }); - assertEquals(7, blade.getStatics().size()); - assertEquals(Boolean.TRUE, blade.getStatics().contains("/assets/")); - assertEquals(Boolean.FALSE, blade.getStatics().contains("/hello/")); + assertEquals(7, blade.staticOptions().getPaths().size()); + assertEquals(Boolean.TRUE, blade.staticOptions().getPaths().contains("/assets/")); + assertEquals(Boolean.FALSE, blade.staticOptions().getPaths().contains("/hello/")); } @Test @@ -194,8 +198,8 @@ public void testThreadName() { @Test public void testShowFileList() { Blade blade = Blade.create(); - blade.showFileList(false); - assertEquals(Boolean.FALSE, blade.environment().getBooleanOrNull(ENV_KEY_STATIC_LIST)); + blade.staticOptions(StaticOptions::showList); + assertEquals(Boolean.FALSE, blade.staticOptions().isShowList()); } @Test diff --git a/blade-examples/src/main/java/com/example/Application.java b/blade-examples/src/main/java/com/example/Application.java index 54af0d7f..e4e5ee0f 100644 --- a/blade-examples/src/main/java/com/example/Application.java +++ b/blade-examples/src/main/java/com/example/Application.java @@ -17,6 +17,7 @@ import com.hellokaton.blade.mvc.ui.ResponseType; import com.hellokaton.blade.mvc.ui.RestResponse; import com.hellokaton.blade.options.CorsOptions; +import com.hellokaton.blade.options.HttpOptions; import com.hellokaton.blade.security.limit.LimitOptions; import lombok.extern.slf4j.Slf4j; @@ -71,6 +72,11 @@ public String index(Request req) { return token; } + @GET(value = "home", responseType = ResponseType.VIEW) + public String home() { + return "home.html"; + } + @POST public String verifyToken(Request req) { System.out.println("token = " + req.header("X-CSRF-TOKEN")); @@ -110,7 +116,7 @@ public static void main(String[] args) { Blade.create() .cors(corsOptions) -// .http(HttpOptions::enableSession) + .http(HttpOptions::enableSession) .get("/base/:uid", ctx -> { ctx.text(ctx.pathString("uid")); }) diff --git a/blade-examples/src/main/resources/static/favicon.ico b/blade-examples/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..259de9cb345230044e8fd670af5e090b90d68d8e GIT binary patch literal 67646 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 Ty^9;pacpxd&1K*DSGD*5cs$>$ literal 0 HcmV?d00001 diff --git a/blade-examples/src/main/resources/templates/home.html b/blade-examples/src/main/resources/templates/home.html index e4f405c3..dab65bc1 100644 --- a/blade-examples/src/main/resources/templates/home.html +++ b/blade-examples/src/main/resources/templates/home.html @@ -3,9 +3,10 @@ + Title - +

This is home page.

\ No newline at end of file