diff --git a/.gitignore b/.gitignore index 2899decbc..cc2d857cf 100755 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ deploy **/.act* git.log +.DS_Store diff --git a/ARCHETYPES.md b/ARCHETYPES.md index 53ff90859..2cfbfff0d 100644 --- a/ARCHETYPES.md +++ b/ARCHETYPES.md @@ -12,13 +12,13 @@ mvn archetype:generate -B \ -DartifactId=helloworld \ -DarchetypeGroupId=org.actframework \ -DarchetypeArtifactId=archetype-quickstart \ - -DarchetypeVersion=1.9.0.2 + -DarchetypeVersion=1.9.1.0 ``` #### Create Project interactively ``` -mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId=archetype-quickstart -DarchetypeVersion=1.9.0.2 +mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId=archetype-quickstart -DarchetypeVersion=1.9.1.0 ``` ## Hello Service @@ -33,13 +33,13 @@ mvn archetype:generate -B \ -DartifactId=helloworld \ -DarchetypeGroupId=org.actframework \ -DarchetypeArtifactId=archetype-simple-restful-service \ - -DarchetypeVersion=1.9.0.2 + -DarchetypeVersion=1.9.1.0 ``` #### Create Project interactively ``` -mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId=archetype-simple-restful-service -DarchetypeVersion=1.9.0.2 +mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId=archetype-simple-restful-service -DarchetypeVersion=1.9.1.0 ``` ## Bookmark @@ -56,13 +56,13 @@ mvn archetype:generate -B \ -DartifactId=bookmark \ -DarchetypeGroupId=org.actframework \ -DarchetypeArtifactId=archetype-bookmark \ - -DarchetypeVersion=1.9.0.2 + -DarchetypeVersion=1.9.1.0 ``` #### Create Project interactively ``` -mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId=archetype-bookmark -DarchetypeVersion=1.9.0.2 +mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId=archetype-bookmark -DarchetypeVersion=1.9.1.0 ``` ## Chatroom @@ -77,13 +77,11 @@ mvn archetype:generate -B \ -DartifactId=chatroom \ -DarchetypeGroupId=org.actframework \ -DarchetypeArtifactId=archetype-chatroom \ - -DarchetypeVersion=1.9.0.2 + -DarchetypeVersion=1.9.1.0 ``` #### Create Project interactively ``` -mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId=archetype-chatroom -DarchetypeVersion=1.9.0.2 +mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId=archetype-chatroom -DarchetypeVersion=1.9.1.0 ``` - - diff --git a/CHANGELOG.md b/CHANGELOG.md index f3c57037f..d199ba896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # ActFramework Change Log +**1.9.2** +* Act-test: It shall not prepend url context when specified url starts from `http` #1427 +* 716a67d0 2020-10-13 | Bump junit from 4.11 to 4.13.1 in /legacy-testapp [dependabot[bot]] +* 3231c87e 2021-05-11 | update title of td to make all the paths are consistent [benstone] +* 3745329d 2021-05-10 | fix bug of url.context not show in e404 page [Benstone Zhang] +* ce255015 2021-05-10 | fix #1392: GetTimeTest failed for non-english locale [Benstone Zhang] +* 1b50ebd4 2022-06-17 | Bump fastjson from 1.2.75 to 1.2.83 [dependabot[bot]] +* b922637a 2022-11-25 | Bump undertow-core from 2.1.6.Final to 2.2.19.Final [dependabot[bot]] +* 8d51b646 2022-09-01 | Bump jsoup from 1.14.2 to 1.15.3 [dependabot[bot]] +* 92001dda 2022-09-15 | Bump snakeyaml from 1.26 to 1.31 in /legacy-testapp [dependabot[bot]] +* 686e89ea 2022-09-15 | Bump snakeyaml from 1.26 to 1.31 [dependabot[bot]] +* 0fbe8fc7 2022-11-19 | vuln-fix: Temporary File Information Disclosure [Jonathan Leitschuh] +* 40e0127e 2021-06-04 | Bump snakeyaml from 1.17 to 1.26 in /legacy-testapp [dependabot[bot]] +* 032d4efe 2021-08-24 | Bump undertow-core from 2.1.3.Final to 2.1.6.Final [dependabot[bot]] +* 2581faa1 2021-06-28 | fix index error when process string substitution [benstone] +* f0704ecb 2021-08-23 | Bump jsoup from 1.12.1 to 1.14.2 [dependabot[bot]] +* packaging project stuck when ehcache has been added into project dependency #1399 +* @Before priority BUG #1407 +* Add "The Wall of Coding Wisdoms" into default Zen list #1388 +* Response content type get overridden - case 2 #1387 +* Response content type get overridden #1386 +* Add act version info into API doc page and test report page #1380 +* URL Path parameter binding failure #1379 +* Incorrect Chinese characters found in `@Configuration` variable injected from properties file encoded with UTF-8 #1377 + +**1.9.1** 02/Jan/2021 +* Shutdown app gracefully in Runtime shutdown hook +* Act-Test - support sending request with file array #1375 +* It reports `UNKNOWN` for OS when running act on macOS #1373 +* Hot reload not working for Bundle Resource properties #1372 +* Cannot start ActFramework: port xxxxx is occupied #1370 +* ApacheMultipartParser NullPointerException #1369 +* EhCache raised `ClassCastException` after reload in dev mode #1368 +* act-test: number verification logic error #1361 +* Error encountered requesting `/asset/extjs-all.js` #1359 +* 500 Error but not error stack in console log #1358 +* Support Java 14 Record class #1354 +* Allow the developers to specify ECJ compiler options #1353 +* `null` file object inject from form field into the request handler argument list #1352 +* When it detect error during generating request handler instance it shall mark it as a fatal error #1347 +* Support customised resource bundle encoding #1329 + **1.9.0a** 28/Jun/2020 * Add `@Inject` to CliDispatcher constructor - allow it be injected in - e.g. - HelpPage * The error triggered during rendering response get warned twice #1341 diff --git a/README.md b/README.md index 6c93cb94b..4fe0c13eb 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,17 @@ ## Install -Add `act-starter-parent` into into your pom.xml file +Add `act-starter-parent` into your pom.xml file ```xml org.actframework act-starter-parent - 1.9.0.1 + 1.9.2.0 ``` -Or use maven archetype to start an new project: +Or use maven archetype to start a new project: ``` mvn archetype:generate -B \ @@ -28,13 +28,13 @@ mvn archetype:generate -B \ -DartifactId=helloworld \ -DarchetypeGroupId=org.actframework \ -DarchetypeArtifactId=archetype-quickstart \ - -DarchetypeVersion=1.9.0.2 + -DarchetypeVersion=1.9.2.0 ``` **tips** don't forget replace the `groupId`, `artifactId` and `appName` in the above script, or you can use interactive mode to generate your project: ``` -mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId=archetype-quickstart -DarchetypeVersion=1.9.0.2 +mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId=archetype-quickstart -DarchetypeVersion=1.9.2.0 ``` **Note** There are more ActFramework application archetypes for use. Please get them [here](ARCHETYPES.md). @@ -135,7 +135,7 @@ mvn archetype:generate -DarchetypeGroupId=org.actframework -DarchetypeArtifactId - **[Powerful view architecture with multiple render engine support](http://actframework.org/doc/templating.md)** -- **[An unbelievable automate testing framework that never presented in any other MVC frameworks](https://thinking.studio/blog/declarative-testing-with-act-framework/)** +- **[An unbelievable automate testing framework that never presented in any other MVC frameworks](https://www.youtube.com/watch?v=_UyfsdY4pSU&t=783s)** - **Commonly used tools** diff --git a/VERSION_MATRIX.md b/VERSION_MATRIX.md index f7c5abaac..8b75fb2ec 100644 --- a/VERSION_MATRIX.md +++ b/VERSION_MATRIX.md @@ -1,26 +1,26 @@ # Version Matrix -| act | 1.8.26 | 1.8.27 | 1.8.28 | 1.8.29 | 1.8.30a | 1.8.31 | 1.8.32 | 1.9.0a | -| --- | ----: | ----: | ----: | ----: | -----: | -----: | -----: | -----: | -| aaa | 1.6.0 | 1.6.1 | 1.6.1 | 1.7.0 | 1.7.0 | 1.7.3 | 1.8.0 | 1.10.0 | -| beetl | 1.6.0 | 1.6.1 | 1.6.1 | 1.7.0 | 1.7.0 | 1.7.1 | 1.7.2 | 1.8.0 | -| beetlsql | 1.7.0 | 1.7.1 | 1.7.1 | 1.8.0 | 1.8.0 | 1.8.1 | 1.8.2 | 1.8.2 | -| ebean-java7 | 1.7.8 | 1.7.8 | 1.7.9 | 1.8.0 | 1.8.0 | 1.8.0 | 1.8.0 | 1.8.0 | -| ebean(java8) | 1.7.9 | 1.7.9 | 1.7.10 | 1.8.0 | 1.8.0 | 1.8.0 | 1.8.0 | 1.8.0 | -| eclipselink(java8) | 1.6.0 | 1.6.0 | 1.6.1 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | -| excel-java7 | | | | 1.8.0 | 1.8.0 | 1.8.1 | 1.9.1 | end | -| excel | 1.7.0 | 1.7.1 | 1.7.2 | 1.8.0 | 1.8.0 | 1.8.1 | 1.9.1 | 1.9.2 | -| freemarker | 1.3.6 | 1.3.7 | 1.3.7 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.1 | -| hibernate | 1.6.0 | 1.6.0 | 1.6.1 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | -| jax-rs(java8) | 1.0.5 | 1.0.5 | 1.0.5 | 1.1.0 | 1.1.0 | 1.1.0 | 1.1.0 | 1.1.0 | -| jpa-common | 1.6.0 | 1.6.0 | 1.6.1 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | -| morphia | 1.7.2 | 1.7.2 | 1.7.3 | 1.8.0 | 1.8.1 | 1.8.2 | 1.9.0 | 1.10.0 | -| mustache(java8) | 1.4.6 | 1.4.6 | 1.4.6 | 1.5.0 | 1.5.0 | 1.5.0 | 1.5.0 | 1.5.0 | -| social | 0.12.6 | 0.12.6 | 0.12.6 | 0.13.0 | 0.13.0 | 0.13.0 | 0.13.0 | 0.14.0 | -| sql-common | 1.5.0 | 1.5.0 | 1.5.1 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.0 | -| storage(java8) | 0.14.0 | 0.14.0 | 0.14.0 | 0.15.0 | 0.15.0 | 0.15.0 | 0.16.0 | 0.16.0 | -| thymeleaf | 1.3.6 | 1.3.6 | 1.3.6 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.0 | -| velocity | 1.3.6 | 1.3.7 | 1.3.7 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.1 | 1.4.1 | +| act 1.8.28 | 1.8.29 | 1.8.30a | 1.8.31 | 1.8.32 | 1.9.0a | 1.9.1b | +| --- ----: | ----: | -----: | -----: | -----: | -----: | -----: | +| aaa 1.6.1 | 1.7.0 | 1.7.0 | 1.7.3 | 1.8.0 | 1.10.0 | 1.10.0 | +| beetl 1.6.1 | 1.7.0 | 1.7.0 | 1.7.1 | 1.7.2 | 1.8.0 | 1.8.0 | +| beetlsql 1.7.1 | 1.8.0 | 1.8.0 | 1.8.1 | 1.8.2 | 1.8.2 | 1.8.2 | +| ebean-java7 1.7.9 | 1.8.0 | 1.8.0 | 1.8.0 | 1.8.0 | 1.8.0 | 1.8.0 | +| ebean(java8) 1.7.10 | 1.8.0 | 1.8.0 | 1.8.0 | 1.8.0 | 1.8.0 | 1.8.0 | +| eclipselink(java8) 1.6.1 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | +| excel-java7 | 1.8.0 | 1.8.0 | 1.8.1 | 1.9.1 | end | end | +| excel 1.7.2 | 1.8.0 | 1.8.0 | 1.8.1 | 1.9.1 | 1.9.2 | 1.9.2 | +| freemarker 1.3.7 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.1 | 1.4.1 | +| hibernate 1.6.1 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | +| jax-rs(java8) 1.0.5 | 1.1.0 | 1.1.0 | 1.1.0 | 1.1.0 | 1.1.0 | 1.1.0 | +| jpa-common 1.6.1 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | 1.7.0 | +| morphia 1.7.3 | 1.8.0 | 1.8.1 | 1.8.2 | 1.9.0 | 1.10.0 | 1.10.0 | +| mustache(java8) 1.4.6 | 1.5.0 | 1.5.0 | 1.5.0 | 1.5.0 | 1.5.0 | 1.5.0 | +| social 0.12.6 | 0.13.0 | 0.13.0 | 0.13.0 | 0.13.0 | 0.14.0 | 0.14.0 | +| sql-common 1.5.1 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.0 | +| storage(java8) 0.14.0 | 0.15.0 | 0.15.0 | 0.15.0 | 0.16.0 | 0.16.0 | 0.16.0 | +| thymeleaf 1.3.6 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.0 | +| velocity 1.3.7 | 1.4.0 | 1.4.0 | 1.4.0 | 1.4.1 | 1.4.1 | 1.4.1 | ## Note diff --git a/legacy-testapp/pom.xml b/legacy-testapp/pom.xml index d601c3a99..3f646430e 100644 --- a/legacy-testapp/pom.xml +++ b/legacy-testapp/pom.xml @@ -61,7 +61,7 @@ org.actframework testapp jar - 1.8 + 1.9 ACT TEST APP Test ActFramework @@ -75,9 +75,9 @@ UTF-8 UTF-8 - 1.9.0-SNAPSHOT - [0.13.0, 2.0.0) - 1.9.1-SNAPSHOT + 1.9.2-SNAPSHOT + [0.16.0, 2.0.0) + 1.10.0 testapp.TestApp @@ -178,7 +178,7 @@ junit junit - 4.11 + 4.13.1 test @@ -210,7 +210,7 @@ org.yaml snakeyaml - 1.17 + 1.31 diff --git a/legacy-testapp/run.sh b/legacy-testapp/run.sh index aadf7eecf..12820c4b8 100755 --- a/legacy-testapp/run.sh +++ b/legacy-testapp/run.sh @@ -1,5 +1,5 @@ #!/bin/sh -mvn2 clean package +mvn -Dmaven.test.skip=true clean package cd target/dist unzip *.zip ./start & diff --git a/pom.xml b/pom.xml index 2837620c7..ab3ee4f18 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ org.actframework act jar - 1.9.1a-SNAPSHOT + 1.9.3-SNAPSHOT ACT Framework The ACT full stack MVC framework @@ -50,7 +50,7 @@ 1.2 1.3.3 3.22.0 - 1.2.71 + 1.2.83 1.1.2 0.7 1.18 @@ -64,23 +64,23 @@ 2.14.6 1.0.0.Final 2.10.6 - 1.12.1 + 1.15.3 4.7.2 - 1.25.0 - 1.8.1 + 1.30.0 + 1.8.2 1.13.2 - 1.13.2 - 1.13.2 + 1.13.4 + 1.13.3 1.11.1 1.5.1 0.0.1 1.11.9 - 1.3.0 + 1.3.1 1.1.0.Final - 2.1.3.Final - 1.26 + 2.2.19.Final + 1.31 3.4.0 @@ -97,50 +97,6 @@ ${project.artifactId}-${project.version} - - - src/main/resources - true - - **/*.version - **/*.api-book - **/*.properties - **/*.css - **/*.csv - **/*.html - **/*.js - **/*.json - **/*.list - **/*.tag - **/*.txt - **/*.xml - **/*.yml - **/*.yaml - **/*.tag - **/*.svg - rythm/** - *.flf - - - - src/main/resources - false - - **/*.gif - **/*.png - **/*.jpg - **/*.jpeg - **/*.doc - **/*.docx - **/*.xls - **/*.xlsx - **/*.ppt - **/*.pptx - **/*.pdf - **/*.ico - - - diff --git a/src/main/java/act/Act.java b/src/main/java/act/Act.java index 17dd024e5..18375256c 100644 --- a/src/main/java/act/Act.java +++ b/src/main/java/act/Act.java @@ -370,23 +370,44 @@ public static void shutdown() { shutdown(app(), 0); } + public static void shutdownNow() { + shutdown(app(), 0, false); + } + public static void shutdown(final App app) { shutdown(app, 0); } public static void shutdown(final App app, final int exitCode) { + shutdown(app, exitCode, true); + } + + private static void shutdown(final App app, final int exitCode, boolean async) { if (null == appManager) { return; } - new Thread() { - @Override - public void run() { + if (async) { + new Thread() { + @Override + public void run() { + try { + if (!appManager.unload(app)) { + app.destroy(); + } + } finally { + shutdownAct(exitCode); + } + } + }.start(); + } else { + try { if (!appManager.unload(app)) { app.destroy(); } + } finally { shutdownAct(exitCode); } - }.start(); + } } public static RequestServerRestart requestRestart() { @@ -1205,7 +1226,12 @@ public static int httpPort() { return app().config().httpPort(); } + private static boolean shutdownStarted = false; private static void shutdownAct(int exitCode) { + if (shutdownStarted) { + return; + } + shutdownStarted = true; clearPidFile(); shutdownNetworkLayer(); destroyApplicationManager(); @@ -1219,6 +1245,7 @@ private static void shutdownAct(int exitCode) { destroyMetricPlugin(); unloadConfig(); destroyNetworkLayer(); + LOGGER.info("All components shutdown, bye!"); if (0 != exitCode) { System.exit(exitCode); } @@ -1260,6 +1287,7 @@ private static void writePidFile() { @Override public void run() { clearPidFile(); + Act.shutdownNow(); } }); } catch (Exception e) { @@ -1271,7 +1299,7 @@ private static void clearPidFile() { String pidFile = pidFile(); try { File file = new File(pidFile); - if (!file.delete()) { + if (null != file && file.canRead() && !file.delete()) { file.deleteOnExit(); } } catch (Exception e) { diff --git a/src/main/java/act/ActResponse.java b/src/main/java/act/ActResponse.java index b8f1915a8..8e4a8c80e 100644 --- a/src/main/java/act/ActResponse.java +++ b/src/main/java/act/ActResponse.java @@ -30,6 +30,7 @@ import org.osgl.storage.impl.SObject; import org.osgl.util.E; import org.osgl.util.IO; +import org.osgl.util.S; import java.io.File; import java.net.URL; @@ -185,7 +186,11 @@ protected final boolean isCommitted() { @Override protected void _setContentType(String type) { + if (S.eq(this.contentType, type)) { + return; + } this.contentType = type; + this.fmt = H.Format.resolve(type); } protected String _getContentType() { diff --git a/src/main/java/act/SysUtilAdmin.java b/src/main/java/act/SysUtilAdmin.java index 63564975a..95ea5f865 100644 --- a/src/main/java/act/SysUtilAdmin.java +++ b/src/main/java/act/SysUtilAdmin.java @@ -385,7 +385,7 @@ private static boolean isBinary(InputStream in) { @Command(value = "act.zen", help = "give me the zen words") public static String zen() { - return Zen.wordsOfTheDay(); + return Zen.wordsOfTheDay(false); } diff --git a/src/main/java/act/Zen.java b/src/main/java/act/Zen.java index 020799897..d1410b35b 100644 --- a/src/main/java/act/Zen.java +++ b/src/main/java/act/Zen.java @@ -27,18 +27,26 @@ import org.osgl.mvc.annotation.GetAction; import org.osgl.util.C; import org.osgl.util.IO; -import org.rythmengine.utils.S; +import org.osgl.util.S; import java.net.URL; import java.util.ArrayList; import java.util.List; import javax.inject.Singleton; +/** + * Wisdom quotes about programming. + * + * The list come from: + * * https://phauer.com/2020/wall-coding-wisdoms-quotes/ + * * https://zen-of-python.info/ + */ @Singleton @ExpressController public final class Zen { - private static final List WORDS = loadWords(); + private static final List WORDS = loadQuotes(false); + private static final List WORDS_FOR_BANNER = loadQuotes(true); private Info.Unit zenInfo; @@ -47,8 +55,8 @@ public Zen() { zenInfo = new Info.Unit("zen", zenTxt); } - public static String wordsOfTheDay() { - return $.random(WORDS); + public static String wordsOfTheDay(boolean forBanner) { + return $.random(forBanner ? WORDS_FOR_BANNER : WORDS); } @GetAction("zen") @@ -57,9 +65,9 @@ public void zen(ActionContext context) { zenInfo.applyTo(context); } - private static List loadWords() { + private static List loadQuotes(boolean forBanner) { URL url = Act.getResource("act_zen.txt"); - List words = C.newList(defaultWords()); + List words = C.newList(defaultQuotes()); if (null != url) { try { List myWords = IO.readLines(url.openStream()); @@ -72,38 +80,89 @@ private static List loadWords() { } List retVal = new ArrayList<>(words.size()); for (String s : words) { - if (s.contains("\n")) { - s = s.replaceAll("\n", "\n "); - } else if (s.contains("\\n")) { - s = s.replaceAll("\\\\n", "\n "); + if (forBanner) { + if (s.contains("\n")) { + s = s.replaceAll("\n", "\n "); + } else if (s.contains("\\n")) { + s = s.replaceAll("\\\\n", "\n "); + } } - retVal.add(s); + retVal.add(S.join("\n", processQuote(s, forBanner))); } return retVal; } - private static List defaultWords() { + private static List processQuote(String quote, boolean forBanner) { + if (quote.indexOf('―') < 0) { + return S.fastSplit(quote, "\n"); + } + S.Pair pair = S.binarySplit(quote, '―'); + String source = pair._2; + S.List lines = S.fastSplit(pair._1, "\n"); + int maxLen = 0; + for (String line: lines) { + maxLen = Math.max(maxLen, line.length()); + } + final int bannerGap = forBanner ? " ".length() : 0; + return lines.append("\n" + S.padLeft("- " + source, Math.max(5, maxLen - source.length() - 3 + bannerGap))); + } + + private static List defaultQuotes() { return C.listOf( - "Beautiful is better than ugly.", - "Explicit is better than implicit.", - "Simple is better than complex.", - "Complex is better than complicated.", - "Flat is better than nested.", - "Sparse is better than dense.", - "Readability counts.", + "Premature optimization is the root of all evil. ― Donald Knuth", + "Rules of optimization: " + + "\n\t1. Don't! " + + "\n\t2. Don't… yet. " + + "\n\t3. Profile before optimizing. ― Michael Jackson", + "As a programmer, never underestimate your ability to come up with ridiculously complex solutions " + + "for simple problems. ― Thomas Fuchs", + "Get your data structures correct first, and the rest of the program will write itself. ― David Jones", + "The #1 rule of distribute computing: " + + "\n\tDon't distribute your computing! " + + "- At least if you can in any way avoid it. ― DHH (David Heinemeier Hanson)", + "Scalability. The #1 problem people don’t actually have but still solve. ― Eberhard Wolff", + "Shared + Mutable = Danger! ― Andrey Breslav", + "Prefer duplication over the wrong abstraction. ― Sandi Metz", + "1. Avoid premature distribution. " + + "\n2. Avoid premature abstraction. " + + "\n\nBoth offer a lure of purity, cleanliness and scalability but add complexity and " + + "operational/cognitive overhead. ― Karl Isenberg", + "Data structures, not algorithms, are central to programming. " + + "― Rob Pike’s 5. Rules of Programming", + "Compassionate Tech Values: \n" + + "\n\tEgo < Humility. " + + "\n\tElitism < Inclusion. " + + "\n\tCompetition < Cooperation. " + + "\n\tBeing Smart < Learning. " + + "\n\tBeing a Rockstar < Being a Mentor. ― April Wensel", + "KISS > DRY ― Philipp Hauer", + "Beautiful is better than ugly. ― Zen of Python", + "Explicit is better than implicit. ― Zen of Python", + "Simple is better than complex. ― Zen of Python", + "Complex is better than complicated. ― Zen of Python", + "Flat is better than nested. ― Zen of Python", + "Sparse is better than dense. ― Zen of Python", + "Readability counts. ― Zen of Python", "Special cases aren't special enough to break the rules. \n" + - "Although practicality beats purity.", - "Errors should never pass silently \n" + - "Unless explicitly silenced.", - "In the face of ambiguity, refuse the temptation to guess.", - "There should be one-- and preferably only one --obvious way to do it.\n" + - "Although that way may not be obvious at first unless you're Dutch.", + "Although practicality beats purity. ― Zen of Python", + "Errors should never pass silently" + + "\nUnless explicitly silenced. ― Zen of Python", + "In the face of ambiguity, refuse the temptation to guess. ― Zen of Python", + "There should be one -- and preferably only one -- obvious way to do it.\n" + + "Although that way may not be obvious at first unless you're Dutch." + + " ― Zen of Python", "Now is better than never. \n" + - "Although never is often better than *right* now.", - "If the implementation is hard to explain, it's a bad idea.", - "If the implementation is easy to explain, it may be a good idea.", - "Namespaces are one honking great idea -- let's do more of those!", - "Simple things should be simple, complex things should be possible." + "Although never is often better than *right* now. ― Zen of Python", + "If the implementation is hard to explain, it's a bad idea. ― Zen of Python", + "If the implementation is easy to explain, it may be a good idea. ― Zen of Python", + "Namespaces are one honking great idea -- let's do more of those! ― Zen of Python", + "Simple things should be simple, complex things should be possible. ― Zen of Python" ); } + + public static void main(String[] args) { + System.out.println(S.join("\n", WORDS)); + System.out.printf(S.repeat('*').times(100)); + System.out.println(S.join("\n", WORDS_FOR_BANNER)); + } } diff --git a/src/main/java/act/apidoc/ApiManager.java b/src/main/java/act/apidoc/ApiManager.java index 1383bd864..2dc0d7473 100644 --- a/src/main/java/act/apidoc/ApiManager.java +++ b/src/main/java/act/apidoc/ApiManager.java @@ -33,11 +33,10 @@ import act.handler.RequestHandlerBase; import act.handler.builtin.ResourceGetter; import act.handler.builtin.controller.RequestHandlerProxy; -import act.inject.util.ResourceLoader; import act.route.RouteSource; import act.route.Router; -import act.util.FastJsonFileSerializer; import act.util.FastJsonSObjectSerializer; +import act.view.ActNotFound; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializeConfig; @@ -55,7 +54,7 @@ import org.osgl.http.H; import org.osgl.logging.LogManager; import org.osgl.logging.Logger; -import org.osgl.storage.ISObject; +import org.osgl.mvc.result.NotFound; import org.osgl.storage.impl.SObject; import org.osgl.util.*; @@ -70,6 +69,8 @@ */ public class ApiManager extends AppServiceBase { + public static final String SYS_PROP_ENABLE_API_MANAGER_ON_TEST_MODE = "act-enable-api-manager-on-test-mode"; + private static final String FILENAME = ".act.api-book"; static final Logger LOGGER = LogManager.get(ApiManager.class); @@ -96,7 +97,7 @@ public static boolean inProgress() { public ApiManager(final App app) { super(app); - this.enabled = app.config().apiDocEnabled() && !Act.isTest(); + this.enabled = app.config().apiDocEnabled() && (!Act.isTest() || enabledOnTestMode()); if (!this.enabled) { return; } @@ -112,7 +113,7 @@ public void run() { } }); Router router = app.isDev() ? app.router() : app.sysRouter(); - router.addMapping(H.Method.GET, "/~/apibook/endpoints", new GetEndpointsHandler(this)); + router.addMapping(H.Method.GET, "/~/apibook/endpoints", new GetEndpointListHandler(this)); router.addMapping(H.Method.GET, "/~/apibook/modules", new GetModulesHandler(this)); ResourceGetter apidocHandler = new ResourceGetter("asset/~act/apibook/index.html"); router.addMapping(H.Method.GET, "/~/api", apidocHandler); @@ -459,16 +460,25 @@ private void exploreDeclaration( } } - private class GetEndpointsHandler extends RequestHandlerBase { + private class GetEndpointListHandler extends RequestHandlerBase { private ApiManager api; - public GetEndpointsHandler(ApiManager api) { + public GetEndpointListHandler(ApiManager api) { this.api = api; } @Override public void handle(ActionContext context) { + String id = context.paramVal("id"); + if (S.notBlank(id)) { + Endpoint endpoint = api.endpoint(id); + if (null == endpoint) { + throw ActNotFound.create(); + } + renderJson(endpoint).apply(context.req(), context.prepareRespForResultEvaluation()); + return; + } String module = context.paramVal("module"); Collection endpoints = S.notBlank(module) ? api.moduleLookup.get(module) : api.endpoints; renderJson(endpoints).apply(context.req(), context.prepareRespForResultEvaluation()); @@ -509,4 +519,7 @@ public String toString() { } + private static boolean enabledOnTestMode() { + return $.bool(System.getProperty(SYS_PROP_ENABLE_API_MANAGER_ON_TEST_MODE)); + } } diff --git a/src/main/java/act/apidoc/Endpoint.java b/src/main/java/act/apidoc/Endpoint.java index bbadd9fb4..529254785 100644 --- a/src/main/java/act/apidoc/Endpoint.java +++ b/src/main/java/act/apidoc/Endpoint.java @@ -467,7 +467,7 @@ private void explore(RequestHandler handler) { } catch (Exception e) { // so we don't have an overwritten method, that's fine, just ignore the exception } - Map typeParamLookup = C.Map(); + Map typeParamLookup = C.newMap(); if (controllerClass.getGenericSuperclass() instanceof ParameterizedType) { typeParamLookup = Generics.buildTypeParamImplLookup(controllerClass); } @@ -528,9 +528,13 @@ private void exploreParamInfo(Method method, Map typeParamLookup, sample = resolver.resolve(info.defaultValue, info.beanSpec.rawType()); } if (H.Method.GET == this.httpMethod) { - String query = generateSampleQuery(info.beanSpec.withoutName(), typeParamLookup, info.bindName, new HashSet(), C.newList()); - if (S.notBlank(query)) { - sampleQuery.add(query); + try { + String query = generateSampleQuery(info.beanSpec.withoutName(), typeParamLookup, info.bindName, new HashSet(), C.newList()); + if (S.notBlank(query)) { + sampleQuery.add(query); + } + } catch (Exception e) { + LOGGER.warn("error generating sample query for method: %s", info.beanSpec); } } else { sampleData.put(info.bindName, sample); @@ -642,6 +646,8 @@ private String generateSampleJson(BeanSpec spec, Map typeParamLoo private String generateSampleQuery(BeanSpec spec, Map typeParamLookup, String bindName, Set typeChain, List nameChain) { Class type = spec.rawType(); + typeParamLookup = new HashMap<>(typeParamLookup); + Generics.buildTypeParamImplLookup(type, typeParamLookup); String specName = spec.name(); if (S.notBlank(specName)) { nameChain.add(specName); @@ -776,6 +782,12 @@ public static Object generateSampleData( return o; } Class classType = spec.rawType(); + if (type instanceof ParameterizedType) { + ParameterizedType ptype = (ParameterizedType) type; + Type[] actualTypeArguments = ptype.getActualTypeArguments(); + TypeVariable[] typeVariables = classType.getTypeParameters(); + Generics.buildTypeParamImplLookup("", actualTypeArguments, typeVariables, typeParamLookup); + } SampleDataProviderManager sampleDataProviderManager = Act.getInstance(SampleDataProviderManager.class); SampleData.Category anno = spec.getAnnotation(SampleData.Category.class); SampleDataCategory category = null != anno ? anno.value() : null; diff --git a/src/main/java/act/app/ActionContext.java b/src/main/java/act/app/ActionContext.java index 0d7f7e031..ec70c4137 100644 --- a/src/main/java/act/app/ActionContext.java +++ b/src/main/java/act/app/ActionContext.java @@ -73,6 +73,7 @@ public class ActionContext extends ActContext.Base implements Des private static final Logger LOGGER = LogManager.get(ActionContext.class); public static final String ATTR_EXCEPTION = "__exception__"; + // used along with CollectionLoader to load multi-file uploads public static final String ATTR_CURRENT_FILE_INDEX = "__file_id__"; public static final String REQ_BODY = "_body"; diff --git a/src/main/java/act/app/App.java b/src/main/java/act/app/App.java index a401ae4c0..37f483ae3 100644 --- a/src/main/java/act/app/App.java +++ b/src/main/java/act/app/App.java @@ -84,6 +84,7 @@ import org.osgl.$; import org.osgl.Lang; import org.osgl.cache.CacheService; +import org.osgl.cache.CacheServiceProvider; import org.osgl.http.HttpConfig; import org.osgl.logging.LogManager; import org.osgl.logging.Logger; @@ -660,16 +661,19 @@ protected void releaseResources() { if (null == daemonRegistry) { return; } - info("App shutting down ...."); + info("Shutting down app [%s]....", name()); if (Act.isDev()) { for (HotReloadListener listener : hotReloadListeners) { listener.preHotReload(); } if (null != classLoader && config().i18nEnabled()) { + debug("clearing resource bundle with classLoader: %s", classLoader); + ResourceBundle.clearCache(App.class.getClassLoader()); // clear resource bundle cache for Act I18n ResourceBundle.clearCache(classLoader); // clear resource bundle cache for Rythm I18n ResourceBundle.clearCache(I18N.class.getClassLoader()); + I18N.clearBundleCache(); } } @@ -1616,6 +1620,7 @@ private void initSessionManager() { private void initCache() { if (isDev()) { + CacheServiceProvider.Impl.setClassLoader(this.classLoader); config().cacheServiceProvider().reset(); } cache = cache(config().cacheName()); diff --git a/src/main/java/act/app/AppClassLoader.java b/src/main/java/act/app/AppClassLoader.java index 67afa07c4..0daa65919 100644 --- a/src/main/java/act/app/AppClassLoader.java +++ b/src/main/java/act/app/AppClassLoader.java @@ -514,7 +514,7 @@ private Class loadAppClass(String name, boolean resolve) throws ClassNotFound } return c; } catch (VerifyError e) { - File f = File.createTempFile(name, ".class"); + File f = java.nio.file.Files.createTempFile(name, ".class").toFile(); IO.write(baNew, f); throw e; } diff --git a/src/main/java/act/app/AppCompiler.java b/src/main/java/act/app/AppCompiler.java index e8a53dd9b..d610e3bfb 100644 --- a/src/main/java/act/app/AppCompiler.java +++ b/src/main/java/act/app/AppCompiler.java @@ -72,6 +72,7 @@ protected void releaseResources() { private void configureCompilerOptions() { Map map = new HashMap<>(); + map.putAll((Map)System.getProperties()); opt(map, OPTION_ReportMissingSerialVersion, IGNORE); opt(map, OPTION_LineNumberAttribute, GENERATE); opt(map, OPTION_SourceFileAttribute, GENERATE); diff --git a/src/main/java/act/app/AppInterceptorManager.java b/src/main/java/act/app/AppInterceptorManager.java index da72bbad2..f1325d40c 100644 --- a/src/main/java/act/app/AppInterceptorManager.java +++ b/src/main/java/act/app/AppInterceptorManager.java @@ -40,10 +40,10 @@ * Manage interceptors at App level */ public class AppInterceptorManager extends AppServiceBase { - private List beforeInterceptors = new ArrayList<>(); - private List afterInterceptors = new ArrayList<>(); - private List exceptionInterceptors = new ArrayList<>(); - private List finallyInterceptors = new ArrayList<>(); + private final List beforeInterceptors = new ArrayList<>(); + private final List afterInterceptors = new ArrayList<>(); + private final List exceptionInterceptors = new ArrayList<>(); + private final List finallyInterceptors = new ArrayList<>(); final GroupInterceptorWithResult BEFORE_INTERCEPTOR = new GroupInterceptorWithResult(beforeInterceptors); final GroupAfterInterceptor AFTER_INTERCEPTOR = new GroupAfterInterceptor(afterInterceptors); diff --git a/src/main/java/act/app/CliServer.java b/src/main/java/act/app/CliServer.java index 9fe740147..b6af40b74 100644 --- a/src/main/java/act/app/CliServer.java +++ b/src/main/java/act/app/CliServer.java @@ -23,6 +23,7 @@ import act.Act; import act.Destroyable; import act.cli.CliSession; +import act.conf.AppConfig; import act.exception.PortOccupiedException; import org.osgl.exception.ConfigurationException; import org.osgl.exception.UnexpectedException; @@ -127,6 +128,9 @@ void start() { return; } try { + if (Act.isTest()) { + AppConfig.clearRandomServerSocket(port); + } serverSocket = new ServerSocket(port); running.set(true); // start server thread diff --git a/src/main/java/act/cli/builtin/Help.java b/src/main/java/act/cli/builtin/Help.java index 719a085f8..5164561d0 100644 --- a/src/main/java/act/cli/builtin/Help.java +++ b/src/main/java/act/cli/builtin/Help.java @@ -20,8 +20,6 @@ * #L% */ -import static org.osgl.$.T2; - import act.cli.CliCmdInfo; import act.cli.CliContext; import act.cli.CliDispatcher; @@ -39,6 +37,8 @@ import java.util.List; import java.util.SortedSet; +import static org.osgl.$.T2; + public class Help extends CliHandlerBase { public static final Help INSTANCE = new Help(); diff --git a/src/main/java/act/conf/AppConfig.java b/src/main/java/act/conf/AppConfig.java index 147a93a66..ec0b68776 100644 --- a/src/main/java/act/conf/AppConfig.java +++ b/src/main/java/act/conf/AppConfig.java @@ -21,6 +21,7 @@ */ import static act.conf.AppConfigKey.*; +import static java.util.ResourceBundle.Control.FORMAT_DEFAULT; import static org.osgl.http.H.Header.Names.X_XSRF_TOKEN; import act.Act; @@ -78,10 +79,14 @@ import java.awt.*; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.ServerSocket; +import java.net.URL; +import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -291,13 +296,13 @@ protected T enableApiDoc(boolean b) { public boolean apiDocEnabled() { if (null == apiDoc) { - this.apiDoc = get(API_DOC_EABLED, Act.isDev()); + this.apiDoc = get(API_DOC_ENABLED, Act.isDev()); } return this.apiDoc; } private void _mergeApiDocEnabled(AppConfig conf) { - if (!hasConfiguration(API_DOC_EABLED)) { + if (!hasConfiguration(API_DOC_ENABLED)) { this.apiDoc = conf.apiDoc; } } @@ -547,12 +552,14 @@ protected T confPrivateKey(String key) { this.confPrivateKey = key; return me(); } + private String confPrivateKey() { if (S.blank(confPrivateKey)) { confPrivateKey = get(CONF_PRIVATE_KEY, ""); } return confPrivateKey; } + private void _mergeConfPrivateId(AppConfig conf) { if (!hasConfiguration(CONF_PRIVATE_KEY)) { confPrivateKey = conf.confPrivateKey; @@ -629,7 +636,7 @@ protected T corsHeadersExpose(String s) { public String corsExposeHeaders() { if (null == corsHeadersExpose) { - corsHeadersExpose = get(CORS_HEADERS_EXPOSE,""); + corsHeadersExpose = get(CORS_HEADERS_EXPOSE, ""); if (S.blank(corsHeadersExpose)) { corsHeadersExpose = corsHeaders(); if (S.notBlank(corsHeadersExpose)) { @@ -1285,16 +1292,19 @@ private void _mergeXForwardedProtocol(AppConfig conf) { } private String xmlRootTag; + protected T xmlRootTag(String tag) { this.xmlRootTag = tag; return me(); } + public String xmlRootTag() { if (null == xmlRootTag) { xmlRootTag = get(XML_ROOT, "xml"); } return xmlRootTag; } + private void _mergeXmlRootTag(AppConfig conf) { if (!hasConfiguration(XML_ROOT)) { this.xmlRootTag = conf.xmlRootTag; @@ -1634,16 +1644,19 @@ private void _mergeLocaleCookieName(AppConfig conf) { } private Boolean mockServer; + protected T mockServer(boolean enabled) { mockServer = enabled; return me(); } + public boolean mockServer() { if (null == mockServer) { mockServer = get(MOCK_SERVER_ENABLED, app.isDev()); } return mockServer; } + private void _mergeMockServer(AppConfig config) { if (!hasConfiguration(MOCK_SERVER_ENABLED)) { mockServer = config.mockServer; @@ -1912,16 +1925,19 @@ private void _mergeJobPoolSize(AppConfig conf) { } private Boolean jsonBodyPatch; + protected T jsonBodyPatch(boolean enabled) { jsonBodyPatch = enabled; return me(); } + public boolean allowJsonBodyPatch() { if (null == jsonBodyPatch) { jsonBodyPatch = get(JSON_BODY_PATCH, true); } return jsonBodyPatch; } + private void _mergeJsonBodyPatch(AppConfig conf) { if (!hasConfiguration(JSON_BODY_PATCH)) { jsonBodyPatch = conf.jsonBodyPatch; @@ -2000,8 +2016,8 @@ protected T httpPort(int port) { public int httpPort() { if (-1 == httpPort) { - if ("test".equalsIgnoreCase(Act.profile())) { - httpPort = randomPort(); + if (Act.isTest()) { + httpPort = chooseRandomDefaultHttpPort(); } else { httpPort = get(HTTP_PORT, 5460); } @@ -2015,6 +2031,47 @@ private void _mergeHttpPort(AppConfig conf) { } } + private static void clearRandomServerSockets() { + for (ServerSocket ss : randomServerSockets.values()) { + IO.close(ss); + } + randomServerSockets.clear(); + } + + public static void clearRandomServerSocket(int port) { + ServerSocket ss = randomServerSockets.remove(port); + IO.close(ss); + } + + private static Map randomServerSockets = new HashMap<>(); + + private static int chooseRandomDefaultHttpPort() { + int maxTry = 10; + while (maxTry-- > 0) { + clearRandomServerSockets(); + boolean ok = true; + int httpPort = randomPort(); + Act.LOGGER.debug("Random port detected: " + httpPort); + for (int i = 1; i < 4; ++i) { + int port = httpPort + i; + ServerSocket ss = null; + try { + ss = new ServerSocket(port); + randomServerSockets.put(port, ss); + Act.LOGGER.debug("Successfully bind to port: " + port); + } catch (IOException e) { + ok = false; + break; + } + } + if (ok) { + Act.LOGGER.info("Default port allocated for testing: " + httpPort); + return httpPort; + } + } + throw new IllegalStateException("Unable to find random HTTP port"); + } + private static int randomPort() { ServerSocket ss = null; try { @@ -2590,16 +2647,19 @@ private void _mergeSourceVersion(AppConfig conf) { } private Boolean selfHealing; + protected T selfHealing(boolean on) { selfHealing = on; return me(); } + public boolean selfHealing() { if (null == selfHealing) { selfHealing = get(SYS_SELF_HEALING, false); } return selfHealing; } + private void _mergeSelfHealing(AppConfig conf) { if (!hasConfiguration(SYS_SELF_HEALING)) { selfHealing = conf.selfHealing; @@ -2654,7 +2714,7 @@ public boolean test(String s) { return false; } if (s.contains("$")) { - for (String pkg: scanList) { + for (String pkg : scanList) { if (s.startsWith(pkg + "$")) { return true; } @@ -2967,6 +3027,7 @@ private void _mergeRenderJsonOutputCharset(AppConfig config) { private String serverHeader; private static final String DEF_SERVER_HEADER = "act/" + Act.VERSION.getProjectVersion(); private static String DEF_APP_SERVER_HEADER = appServerHeader(); + private static String appServerHeader() { App app = Act.app(); if (null == app) { @@ -2999,16 +3060,19 @@ private void _mergeServerHeader(AppConfig config) { } private Boolean serverHeaderUseApp; + protected T serverHeaderUseApp(boolean b) { serverHeaderUseApp = b; return me(); } + private boolean serverHeaderUseApp() { if (null == serverHeaderUseApp) { serverHeaderUseApp = get(AppConfigKey.SERVER_HEADER_USE_APP, true); } return serverHeaderUseApp; } + private void _mergeServerHeaderUseApp(AppConfig config) { if (!hasConfiguration(SERVER_HEADER_USE_APP)) { serverHeaderUseApp = config.serverHeaderUseApp; @@ -3135,16 +3199,19 @@ private void _mergeSessionTtl(AppConfig conf) { private boolean sessionPassThrough; private boolean sessionPassThroughSet; // use this to save auto-box of sessionPassThrough flag + protected T sessionPassThrough(boolean b) { sessionPassThrough = b; return me(); } + public boolean sessionPassThrough() { if (!sessionPassThroughSet) { sessionPassThrough = get(SESSION_PASS_THROUGH, false); } return sessionPassThrough; } + private void _mergeSessionPassThrough(AppConfig config) { if (!hasConfiguration(SESSION_PASS_THROUGH)) { sessionPassThrough = config.sessionPassThrough; @@ -3310,16 +3377,19 @@ private void _mergeSessionHeaderPayloadPrefix(AppConfig config) { } private String sessionQueryParamName; + protected T sessionQueryParamName(String paramName) { sessionQueryParamName = paramName; return me(); } + public String getSessionQueryParamName() { if (null == sessionQueryParamName) { sessionQueryParamName = get(SESSION_QUERY_PARAM_NAME, sessionHeader()); } return sessionQueryParamName; } + private void _mergeSessionQueryParamName(AppConfig config) { if (!hasConfiguration(SESSION_QUERY_PARAM_NAME)) { sessionQueryParamName = config.sessionQueryParamName; @@ -3395,8 +3465,7 @@ private void _mergeSecretRotate(AppConfig config) { /** * Set `secret.rotate.period` in terms of minute * - * @param period - * the minutes between two secret rotate happening + * @param period the minutes between two secret rotate happening * @return this config object * @see AppConfigKey#SECRET_ROTATE_PERIOD */ @@ -3777,6 +3846,74 @@ private void _mergeResourceFiltering(AppConfig conf) { } } + private static final ResourceBundle.Control DEF_RBC = ResourceBundle.Control.getControl(FORMAT_DEFAULT); + private String resourceBundleEncoding = null; + protected T resourceBundleEncoding(String encoding) { + resourceBundleEncoding = encoding; + return me(); + } + public String resourceBundleEncoding() { + if (null == resourceBundleEncoding) { + resourceBundleEncoding = get(RESOURCE_BUNDLE_ENCODING, "default"); + } + return resourceBundleEncoding; + } + private void _mergeResourceBundleEncoding(AppConfig conf) { + if (!hasConfiguration(RESOURCE_BUNDLE_ENCODING)) { + this.resourceBundleEncoding = conf.resourceBundleEncoding; + this.resourceBundleControl = null; + } + } + private ResourceBundle.Control resourceBundleControl; + public ResourceBundle.Control resourceBundleControl() { + synchronized (DEF_RBC) { + if (null != resourceBundleControl) { + return resourceBundleControl; + } + final String encoding = resourceBundleEncoding(); + if ("default".equals(encoding)) { + resourceBundleControl = DEF_RBC; + } else { + resourceBundleControl = new ResourceBundle.Control() { + @Override + public ResourceBundle newBundle( + String baseName, Locale locale, + String format, ClassLoader loader, + boolean reload + ) throws IllegalAccessException, InstantiationException, IOException { + // The below is a copy of the default implementation. + String bundleName = toBundleName(baseName, locale); + String resourceName = toResourceName(bundleName, "properties"); + ResourceBundle bundle = null; + InputStream stream = null; + if (reload) { + URL url = loader.getResource(resourceName); + if (url != null) { + URLConnection connection = url.openConnection(); + if (connection != null) { + connection.setUseCaches(false); + stream = connection.getInputStream(); + } + } + } else { + stream = loader.getResourceAsStream(resourceName); + } + if (stream != null) { + try { + // Only this line is changed to make it to read properties files as UTF-8. + bundle = new PropertyResourceBundle(new InputStreamReader(stream, encoding)); + } finally { + stream.close(); + } + } + return bundle; + } + }; + } + } + return resourceBundleControl; + } + private Integer uploadInMemoryCacheThreshold; protected T uploadInMemoryCacheThreshold(int l) { @@ -3892,8 +4029,7 @@ private void loadJarProperties(Properties p) { * settings has lower priority as it's hardcoded thus only when configuration file * does not provided the settings, the app configurator will take effect * - * @param conf - * the application configurator + * @param conf the application configurator */ public void _merge(AppConfigurator conf) { app.emit(SysEventId.CONFIG_PREMERGE); diff --git a/src/main/java/act/conf/AppConfigKey.java b/src/main/java/act/conf/AppConfigKey.java index b52bfc36c..b1b768fb9 100644 --- a/src/main/java/act/conf/AppConfigKey.java +++ b/src/main/java/act/conf/AppConfigKey.java @@ -58,7 +58,7 @@ public enum AppConfigKey implements ConfigKey { * * Default value: `true` when app running in `dev` mode, or `false` otherwise */ - API_DOC_EABLED("api_doc.enabled"), + API_DOC_ENABLED("api_doc.enabled"), /** * `act.api_doc.built_in.hide` turns on/off built-in endpoints in @@ -992,6 +992,18 @@ public T val(Map configuration) { */ RESOLVER_TEMPLATE_PATH("resolver.template_path.impl"), + /** + * `resource_bundle.encoding` specifies encoding of resource bundles. + * + * This configuration allows override the default resource bundle + * encoding setting used by specific Java runtime: + * - Before Java 9: ISO-8859-1 + * - Java 9 and above: UTF-8 + * + * Default value: `null` meaning follow JDK default encoding setting + */ + RESOURCE_BUNDLE_ENCODING("resource_bundle.encoding"), + /** * `resource.filtering` * diff --git a/src/main/java/act/controller/Controller.java b/src/main/java/act/controller/Controller.java index 89afd51ff..f21ceb202 100644 --- a/src/main/java/act/controller/Controller.java +++ b/src/main/java/act/controller/Controller.java @@ -1811,7 +1811,7 @@ public static Result inferPrimitiveResult( if (H.Format.UNKNOWN.isSameTypeWith(fmt)) { actionContext.resp().contentType("application/octet-stream"); } - return new RenderBinary((byte[]) v); + return new RenderBinary((byte[]) v).contentType(fmt); } else { H.Format fmt = actionContext.accept(); String fmtName = fmt.name(); @@ -1975,6 +1975,13 @@ public static Result inferResult(HandlerMethodMetaInfo meta, Object v, ActionCon return inferToTemplate(v, context); } + H.Format appSpecifiedContentType = H.Format.UNKNOWN; + if ($.not(context.paramVal("_accept"))) { + appSpecifiedContentType = context.resp().lastContentType(); + } + if (null != appSpecifiedContentType && H.Format.UNKNOWN != appSpecifiedContentType) { + context.accept(appSpecifiedContentType); + } H.Format accept = context.accept(); boolean requireJSON = accept.isSameTypeWithAny(H.Format.JSON, H.Format.UNKNOWN); boolean requireXML = !requireJSON && accept.isSameTypeWith(H.Format.XML); diff --git a/src/main/java/act/controller/meta/InterceptorMethodMetaInfo.java b/src/main/java/act/controller/meta/InterceptorMethodMetaInfo.java index d80a9762e..2ccefedc3 100644 --- a/src/main/java/act/controller/meta/InterceptorMethodMetaInfo.java +++ b/src/main/java/act/controller/meta/InterceptorMethodMetaInfo.java @@ -22,6 +22,7 @@ import act.Constants; import act.handler.builtin.controller.Handler; +import org.osgl.mvc.annotation.Before; import org.osgl.util.C; import org.osgl.util.S; @@ -35,7 +36,7 @@ public class InterceptorMethodMetaInfo extends HandlerMethodMetaInfo whiteList = C.newSet(); private Set blackList = C.newSet(); - private Integer priority; + private int priority = 0; protected InterceptorMethodMetaInfo(InterceptorMethodMetaInfo copy, ControllerClassMetaInfo clsInfo) { super(copy, clsInfo); @@ -119,7 +120,7 @@ public String toString() { @Override protected S.Buffer toStrBuffer(S.Buffer sb) { S.Buffer prependix = S.newBuffer(); - if (null != priority) { + if (0 != priority) { prependix.append("p[") .append(priority).append("] "); } diff --git a/src/main/java/act/data/ApacheMultipartParser.java b/src/main/java/act/data/ApacheMultipartParser.java index c1e0b0293..e320d0973 100644 --- a/src/main/java/act/data/ApacheMultipartParser.java +++ b/src/main/java/act/data/ApacheMultipartParser.java @@ -29,6 +29,7 @@ import org.osgl.http.H; import org.osgl.storage.ISObject; import org.osgl.util.E; +import org.osgl.util.IO; import java.io.IOException; import java.io.InputStream; @@ -54,7 +55,6 @@ public Map parse(ActionContext context) { FileItemIteratorImpl iter = new FileItemIteratorImpl(body, request.header("content-type"), request.characterEncoding()); while (iter.hasNext()) { FileItemStream item = iter.next(); - ISObject sobj = UploadFileStorageService.store(item, context.app()); String fieldName = item.getFieldName(); if (item.isFormField()) { // must resolve encoding @@ -66,8 +66,10 @@ public Map parse(ActionContext context) { _encoding = contentTypeEncoding.encoding; } } - mergeValueInMap(result, fieldName, sobj.asString(Charset.forName(_encoding))); + String val = IO.read(item.openStream()).encoding(Charset.forName(_encoding)).toString(); + mergeValueInMap(result, fieldName, val); } else { + ISObject sobj = UploadFileStorageService.store(item, context.app()); context.addUpload(item.getFieldName(), sobj); mergeValueInMap(result, fieldName, fieldName); } diff --git a/src/main/java/act/data/ContentTypeWithEncoding.java b/src/main/java/act/data/ContentTypeWithEncoding.java index b8e4a905f..520a89a0c 100644 --- a/src/main/java/act/data/ContentTypeWithEncoding.java +++ b/src/main/java/act/data/ContentTypeWithEncoding.java @@ -33,7 +33,7 @@ public ContentTypeWithEncoding(String contentType, String encoding) { public static ContentTypeWithEncoding parse(String contentType) { if( contentType == null ) { - return new ContentTypeWithEncoding("text/html".intern(), null); + return new ContentTypeWithEncoding("text/html", null); } else { String[] contentTypeParts = contentType.split(";"); String _contentType = contentTypeParts[0].trim().toLowerCase(); diff --git a/src/main/java/act/handler/builtin/ResourceGetter.java b/src/main/java/act/handler/builtin/ResourceGetter.java index 635c3c768..5ada53687 100644 --- a/src/main/java/act/handler/builtin/ResourceGetter.java +++ b/src/main/java/act/handler/builtin/ResourceGetter.java @@ -33,6 +33,7 @@ import act.handler.RequestHandler; import act.handler.builtin.controller.FastRequestHandler; import act.util.$$; +import act.util.StringUtils; import org.osgl.$; import org.osgl.http.H; import org.osgl.mvc.result.NotFound; @@ -273,7 +274,7 @@ protected void handle(String path, ActionContext context) { resp.send(file); } else { String content = IO.readContentAsString(file); - content = $$.processStringSubstitution(content); + content = StringUtils.processStringSubstitution(content, true); resp.writeContent(content); } } else if (largeResource.contains(path)) { diff --git a/src/main/java/act/handler/builtin/controller/RequestHandlerProxy.java b/src/main/java/act/handler/builtin/controller/RequestHandlerProxy.java index 2bb76361e..8265505b3 100644 --- a/src/main/java/act/handler/builtin/controller/RequestHandlerProxy.java +++ b/src/main/java/act/handler/builtin/controller/RequestHandlerProxy.java @@ -254,6 +254,7 @@ public void handle(ActionContext context) { if (context.resp().isClosed()) { logger.error(e, "Error committing result"); } else { + logger.error(e, "Error handling request: " + context.req().url()); if (null == result) { if (e instanceof IllegalArgumentException) { String errorMsg = e.getLocalizedMessage(); diff --git a/src/main/java/act/handler/builtin/controller/impl/ReflectedHandlerInvoker.java b/src/main/java/act/handler/builtin/controller/impl/ReflectedHandlerInvoker.java index 4dd3a5d61..6523d9027 100644 --- a/src/main/java/act/handler/builtin/controller/impl/ReflectedHandlerInvoker.java +++ b/src/main/java/act/handler/builtin/controller/impl/ReflectedHandlerInvoker.java @@ -378,7 +378,16 @@ private ReflectedHandlerInvoker(M handlerMetaInfo, App app) { // ResponseContentType takes priority of JsonView ResponseContentType contentType = controllerClass.getAnnotation(ResponseContentType.class); if (null != contentType) { - forceResponseContentType = contentType.value().format(); + String s = contentType.contentType(); + if (S.notBlank(s)) { + forceResponseContentType = H.Format.resolve(s); + } + if (null == forceResponseContentType || forceResponseContentType == H.Format.UNKNOWN) { + forceResponseContentType = contentType.mediaType().format(); + } + if (null == forceResponseContentType || forceResponseContentType == H.Format.UNKNOWN) { + forceResponseContentType = contentType.value().format(); + } } DownloadFilename downloadFilename = ReflectedInvokerHelper.getAnnotation(DownloadFilename.class, method); @@ -395,7 +404,17 @@ private ReflectedHandlerInvoker(M handlerMetaInfo, App app) { } contentType = ReflectedInvokerHelper.getAnnotation(ResponseContentType.class, method); if (null != contentType) { - forceResponseContentType = contentType.value().format(); + forceResponseContentType = null; + String s = contentType.contentType(); + if (S.notBlank(s)) { + forceResponseContentType = H.Format.resolve(s); + } + if (null == forceResponseContentType || forceResponseContentType == H.Format.UNKNOWN) { + forceResponseContentType = contentType.mediaType().format(); + } + if (null == forceResponseContentType || forceResponseContentType == H.Format.UNKNOWN) { + forceResponseContentType = contentType.value().format(); + } } ResponseStatus status = ReflectedInvokerHelper.getAnnotation(ResponseStatus.class, method); @@ -1167,7 +1186,7 @@ private static Result transform(Object retVal, ReflectedHandlerInvoker invoker, invoker.checkTemplate(context); return result; } - HandlerMethodMetaInfo handlerMetaInfo = invoker.handler; + final HandlerMethodMetaInfo handlerMetaInfo = invoker.handler; final boolean hasReturn = handlerMetaInfo.hasReturn() && !handlerMetaInfo.returnTypeInfo().isResult(); if (null == retVal && hasReturn) { // ActFramework respond 404 Not Found when diff --git a/src/main/java/act/i18n/I18n.java b/src/main/java/act/i18n/I18n.java index 0377ffa34..c6fab5a49 100644 --- a/src/main/java/act/i18n/I18n.java +++ b/src/main/java/act/i18n/I18n.java @@ -98,7 +98,8 @@ private static String _i18n(boolean ignoreError, Locale locale, String bundleNam if (null != app && null != app.classLoader()) { classLoader = app.classLoader(); } - bundle = ResourceBundle.getBundle(bundleName, $.requireNotNull(locale), classLoader); + logger.debug("loading resource bundle[%s] with classLoader[%s]", bundleName, classLoader); + bundle = ResourceBundle.getBundle(bundleName, $.requireNotNull(locale), classLoader, app.config().resourceBundleControl()); } catch (MissingResourceException e) { if (!ignoreError) { logger.warn("Cannot find bundle: %s", bundleName); diff --git a/src/main/java/act/inject/param/CollectionLoader.java b/src/main/java/act/inject/param/CollectionLoader.java index ed37ad274..6f3c0e0c5 100644 --- a/src/main/java/act/inject/param/CollectionLoader.java +++ b/src/main/java/act/inject/param/CollectionLoader.java @@ -97,20 +97,19 @@ public Object load(Object bean, ActContext context, boolean noDefaultValue) { List nodes = node.list(); if (nodes.size() > 0) { String value = nodes.get(0).value(); - //if (S.notBlank(value)) { - for (int i = 0; i < nodes.size(); ++i) { - ParamTreeNode elementNode = nodes.get(i); - if (!elementNode.isLeaf()) { - throw new BadRequest("cannot parse param: expect leaf node, found: \n%s", node.debug()); - } - context.attribute(ActionContext.ATTR_CURRENT_FILE_INDEX, i); - if (null != binder) { - collection.add(binder.resolve(null, elementNode.value(), context)); - } else { - collection.add(resolver.resolve(elementNode.value())); - } + for (int i = 0; i < nodes.size(); ++i) { + ParamTreeNode elementNode = nodes.get(i); + if (!elementNode.isLeaf()) { + throw new BadRequest("cannot parse param: expect leaf node, found: \n%s", node.debug()); + } + context.attribute(ActionContext.ATTR_CURRENT_FILE_INDEX, i); + if (null != binder) { + collection.add(binder.resolve(null, elementNode.value(), context)); + } else { + collection.add(resolver.resolve(elementNode.value())); } - //} + } + context.removeAttribute(ActionContext.ATTR_CURRENT_FILE_INDEX); } } else if (node.isMap()) { Set childrenKeys = node.mapKeys(); diff --git a/src/main/java/act/route/Router.java b/src/main/java/act/route/Router.java index d78ad18c1..0f91867a2 100644 --- a/src/main/java/act/route/Router.java +++ b/src/main/java/act/route/Router.java @@ -620,7 +620,7 @@ public String urlBase(ActionContext context) { } } - private String ensureUrlContext(String path) { + public String ensureUrlContext(String path) { String urlContext = appConfig.urlContext(); if (null == urlContext || path.startsWith(urlContext)) { if ("/".equals(path)) { @@ -640,13 +640,14 @@ private String ensureUrlContext(String path) { return S.concat(urlContext, path); } - public String fullUrl(String path, Object... args) { - path = S.fmt(path, args); + public String fullUrl(final String pathTemplate, Object... args) { + String path = S.fmt(pathTemplate, args); if (path.startsWith("//") || path.startsWith("http")) { return path; } - if (path.contains(".") || path.contains("(")) { - path = reverseRoute(path); + if (!path.contains("/") && (path.contains(".") || path.contains("("))) { + String reversedPath = reverseRoute(path); + path = null != reversedPath ? reversedPath : path; } S.Buffer sb = S.newBuffer(urlBase()); path = ensureUrlContext(path); @@ -1018,7 +1019,6 @@ private Node(int id, AppConfig config) { this.id = keyword.hashCode(); this.root = parent.root; this.macroLookup = parent.macroLookup; - this.varNames.addAll(parent.varNames); } Node(String name, Node parent) { @@ -1027,7 +1027,6 @@ private Node(int id, AppConfig config) { this.id = name.hashCode(); this.root = parent.root; this.macroLookup = parent.macroLookup; - this.varNames.addAll(parent.varNames); parseDynaName(name); } @@ -1166,7 +1165,7 @@ private Node child(String name, ActionContext context) { } } if (MATCH_ALL == targetNode.patternTrait) { - context.urlPathParam(targetNode.varNames.get(varIdCounter.get().getAndIncrement()), name); + context.urlPathParam(targetNode.varNames.get(varIdCounter.get().get()), name); return targetNode; } Pattern pattern = targetNode.pattern; diff --git a/src/main/java/act/test/RequestBuilder.java b/src/main/java/act/test/RequestBuilder.java index efb053488..f7f813da4 100644 --- a/src/main/java/act/test/RequestBuilder.java +++ b/src/main/java/act/test/RequestBuilder.java @@ -100,7 +100,7 @@ class RequestBuilder { } String reqUrl = requestSpec.url; if (null != session) { - if (S.notBlank(session.scenario().urlContext) && !reqUrl.startsWith("/")) { + if (S.notBlank(session.scenario().urlContext) && !reqUrl.startsWith("/") && !reqUrl.startsWith("http")) { reqUrl = S.pathConcat(session.scenario().urlContext, '/', reqUrl); } } @@ -155,32 +155,66 @@ class RequestBuilder { MultipartBody.Builder formBuilder = new MultipartBody.Builder(); for (Map.Entry entry : requestSpec.parts.entrySet()) { String key = entry.getKey(); - String val = S.string(entry.getValue()); - byte[] content = null; - H.Format fileFormat = null; - String path = S.pathConcat("upload", '/', val); - File uploadFile = Act.app().testResource(path); - if (uploadFile.exists()) { - fileFormat = FileGetter.contentType(path); - content = IO.readContent(uploadFile); - } else { - path = S.pathConcat("test/upload", '/', val); - URL fileUrl = Act.getResource(path); - if (null != fileUrl) { - String filePath = fileUrl.getFile(); - fileFormat = FileGetter.contentType(filePath); - content = $.convert(fileUrl).to(byte[].class); + Object obj = entry.getValue(); + if (obj instanceof String) { + String val = S.string(entry.getValue()); + byte[] content = null; + H.Format fileFormat = null; + String path = S.pathConcat("upload", '/', val); + File uploadFile = Act.app().testResource(path); + if (uploadFile.exists()) { + fileFormat = FileGetter.contentType(path); + content = IO.readContent(uploadFile); + } else { + path = S.pathConcat("test/upload", '/', val); + URL fileUrl = Act.getResource(path); + if (null != fileUrl) { + String filePath = fileUrl.getFile(); + fileFormat = FileGetter.contentType(filePath); + content = $.convert(fileUrl).to(byte[].class); + } + } + if (null != content) { + String checksum = IO.checksum(content); + RequestBody fileBody = RequestBody.create(MediaType.parse(fileFormat.contentType()), content); + String attachmentName = val.contains("/") ? S.cut(val).afterLast("/") : val; + formBuilder.addFormDataPart(key, attachmentName, fileBody); + session.cache("checksum-last", checksum); + session.cache("checksum-" + val, checksum); + } else { + formBuilder.addFormDataPart(key, val); + } + } else if (obj instanceof Collection) { + Collection col = (Collection) obj; + for (Object element : col) { + String val = S.string(element); + byte[] content = null; + H.Format fileFormat = null; + String path = S.pathConcat("upload", '/', val); + File uploadFile = Act.app().testResource(path); + if (uploadFile.exists()) { + fileFormat = FileGetter.contentType(path); + content = IO.readContent(uploadFile); + } else { + path = S.pathConcat("test/upload", '/', val); + URL fileUrl = Act.getResource(path); + if (null != fileUrl) { + String filePath = fileUrl.getFile(); + fileFormat = FileGetter.contentType(filePath); + content = $.convert(fileUrl).to(byte[].class); + } + } + if (null != content) { + String checksum = IO.checksum(content); + RequestBody fileBody = RequestBody.create(MediaType.parse(fileFormat.contentType()), content); + String attachmentName = val.contains("/") ? S.cut(val).afterLast("/") : val; + formBuilder.addFormDataPart(key, attachmentName, fileBody); + session.cache("checksum-last", checksum); + session.cache("checksum-" + val, checksum); + } else { + formBuilder.addFormDataPart(key, val); + } } - } - if (null != content) { - String checksum = IO.checksum(content); - RequestBody fileBody = RequestBody.create(MediaType.parse(fileFormat.contentType()), content); - String attachmentName = val.contains("/") ? S.cut(val).afterLast("/") : val; - formBuilder.addFormDataPart(key, attachmentName, fileBody); - session.cache("checksum-last", checksum); - session.cache("checksum-" + val, checksum); - } else { - formBuilder.addFormDataPart(key, val); } } body = formBuilder.build(); diff --git a/src/main/java/act/test/TestSession.java b/src/main/java/act/test/TestSession.java index 3fcbddb71..918c27b62 100644 --- a/src/main/java/act/test/TestSession.java +++ b/src/main/java/act/test/TestSession.java @@ -66,7 +66,7 @@ */ public class TestSession extends LogSupport { - private static ThreadLocal current = new ThreadLocal<>(); + private final static ThreadLocal current = new ThreadLocal<>(); static TestSession current() { return current.get(); @@ -644,6 +644,30 @@ void verifyValue(String name, Object value, Object test) { } verifyValue(name, value, test); } + } else if (value instanceof Long) { + Long lng = (Long) value; + Long expected = null; + if (test instanceof Long) { + expected = (Long) test; + } else { + String s = S.string(test); + s = S.isIntOrLong(s) ? s : processStringSubstitution(s); + ErrorMessage.errorIfNot(S.isIntOrLong(s), "Cannot verify %s value [%s] against test", name, value, test); + expected = $.convert(s).toLong(); + } + ErrorMessage.errorIfNot(lng.equals(expected), "Cannot verify %s value [%s] against test [%s]", name, value, test); + } else if (value instanceof Integer) { + Integer integer = (Integer) value; + Integer expected = null; + if (test instanceof Integer) { + expected = (Integer) test; + } else { + String s = S.string(test); + s = S.isInt(s) ? s : processStringSubstitution(s); + ErrorMessage.errorIfNot(S.isInt(s), "Cannot verify %s value [%s] against test", name, value, test); + expected = $.convert(s).toInteger(); + } + ErrorMessage.errorIfNot(integer.equals(expected), "Cannot verify %s value [%s] against test [%s]", name, value, test); } else if (value instanceof Number) { Number found = (Number) value; Number expected = null; @@ -652,11 +676,8 @@ void verifyValue(String name, Object value, Object test) { } else { String s = S.string(test); s = S.isNumeric(s) ? s : processStringSubstitution(s); - if (S.isNumeric(S.string(s))) { - expected = $.convert(s).to(Double.class); - } else { - ErrorMessage.error("Cannot verify %s value [%s] against test [%s]", name, value, test); - } + ErrorMessage.errorIfNot(S.isNumeric(S.string(s)), "Cannot verify %s value [%s] against test [%s]", name, value, test); + expected = $.convert(s).to(Double.class); } double delta = Math.abs(expected.doubleValue() - found.doubleValue()); if ((delta / found.doubleValue()) > 0.001) { diff --git a/src/main/java/act/test/verifier/DateTimeVerifier.java b/src/main/java/act/test/verifier/DateTimeVerifier.java index 87a85c3ca..2ea835221 100644 --- a/src/main/java/act/test/verifier/DateTimeVerifier.java +++ b/src/main/java/act/test/verifier/DateTimeVerifier.java @@ -31,6 +31,7 @@ import java.text.DateFormat; import java.util.Date; +import java.util.Locale; public abstract class DateTimeVerifier extends Verifier { @@ -112,12 +113,12 @@ private static Long tryWithFormat(String s, String pattern, String... otherPatte if (null != l) { return l; } - l = tryWithFormat(s, DateTimeFormat.forPattern(pattern)); + l = tryWithFormat(s, DateTimeFormat.forPattern(pattern).withLocale(Locale.ENGLISH)); if (null != l) { return l; } for (String op : otherPatterns) { - l = tryWithFormat(s, DateTimeFormat.forPattern(op)); + l = tryWithFormat(s, DateTimeFormat.forPattern(op).withLocale(Locale.ENGLISH)); if (null != l) { return l; } diff --git a/src/main/java/act/util/Banner.java b/src/main/java/act/util/Banner.java index cfd6fed08..c7d923539 100644 --- a/src/main/java/act/util/Banner.java +++ b/src/main/java/act/util/Banner.java @@ -106,7 +106,7 @@ public static String banner(AppDescriptor appDescriptor) { sb.append("\n OS: ").append(OS.get()); sb.append("\n jdk: ").append(VM.INFO).append(" ").append(VM.SPEC_VERSION); sb.append("\n"); - sb.append("\n zen: ").append(Zen.wordsOfTheDay()); + sb.append("\n zen: ").append(Zen.wordsOfTheDay(true)); sb.append("\n"); return sb.toString(); diff --git a/src/main/java/act/util/SimpleBean.java b/src/main/java/act/util/SimpleBean.java index 0beda8944..01aceb3ef 100644 --- a/src/main/java/act/util/SimpleBean.java +++ b/src/main/java/act/util/SimpleBean.java @@ -35,6 +35,7 @@ import org.osgl.util.E; import org.osgl.util.S; +import java.lang.reflect.Modifier; import java.util.*; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; @@ -190,6 +191,9 @@ public void scanFinished(String className) { private static class SimpleBeanByteCodeVisitor extends ByteCodeVisitor { + private static final String RECORD = "java/lang/Record"; + private static final String SIMPLE_BEAN = "act/util/SimpleBean"; + private static final String ALIAS_DESC = Type.getType(Alias.class).getDescriptor(); private static final String LABEL_DESC = Type.getType(Label.class).getDescriptor(); private String className; @@ -198,18 +202,24 @@ private static class SimpleBeanByteCodeVisitor extends ByteCodeVisitor { private Map> publicFields = new LinkedHashMap<>(); private Map aliases = new LinkedHashMap<>(); private Map labels = new LinkedHashMap<>(); + private boolean isRecord; @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if (isPublic(access)) { isPublicClass = true; className = Type.getObjectType(name).getClassName(); + isRecord = RECORD.equals(superName); + if (isRecord) { + interfaces = new String[] {SIMPLE_BEAN}; + } } super.visit(version, access, name, signature, superName, interfaces); } @Override public FieldVisitor visitField(int access, final String name, String desc, String signature, Object value) { + if (isRecord) access = Modifier.PUBLIC; FieldVisitor fv = super.visitField(access, name, desc, signature, value); if (isPublicClass && AsmTypes.isPublic(access) && !AsmTypes.isStatic(access)) { publicFields.put(name, $.T2(desc, signature)); diff --git a/src/main/java/act/util/StringUtils.java b/src/main/java/act/util/StringUtils.java index dbfd6c34b..29b3bd64f 100644 --- a/src/main/java/act/util/StringUtils.java +++ b/src/main/java/act/util/StringUtils.java @@ -32,7 +32,7 @@ import java.util.List; import java.util.Map; -class StringUtils { +public class StringUtils { static $.Transformer evaluator = new $.Transformer() { @Override @@ -44,10 +44,18 @@ public String transform(String s) { public static String processStringSubstitution(String s) { - return processStringSubstitution(s, evaluator); + return processStringSubstitution(s, evaluator, false); + } + + public static String processStringSubstitution(String s, boolean ignoreError) { + return processStringSubstitution(s, evaluator, ignoreError); } public static String processStringSubstitution(String s, $.Func1 evaluator) { + return processStringSubstitution(s, evaluator, false); + } + + public static String processStringSubstitution(String s, $.Func1 evaluator, boolean ignoreError) { if (S.blank(s)) { return ""; } @@ -59,11 +67,22 @@ public static String processStringSubstitution(String s, $.Func1 int z = n; StringBuilder buf = S.builder(); while (true) { - buf.append(s.substring(a, z)); + buf.append(s, a, z); n = s.indexOf("}", z); a = n + 1; String key = s.substring(z + 2, a - 1); - buf.append(evaluator.apply(key)); + if (S.notEmpty(key)) { + String val = key; + try { + val = evaluator.apply(key); + } catch (RuntimeException e) { + if (!ignoreError) throw e; + buf.append("${").append(key).append("}"); + } + buf.append(val); + } else { + buf.append("${}"); + } n = s.indexOf("${", a); if (n < 0) { buf.append(s.substring(a)); diff --git a/src/main/java/act/view/ActErrorResult.java b/src/main/java/act/view/ActErrorResult.java index a935cd1e9..8cc87770a 100644 --- a/src/main/java/act/view/ActErrorResult.java +++ b/src/main/java/act/view/ActErrorResult.java @@ -163,14 +163,12 @@ protected void populateSourceInfo(AsmContext context) { $.Function unsupported = new $.Transformer() { @Override public Result transform(Throwable throwable) { - Act.LOGGER.warn(throwable, "Error:"); return ActNotImplemented.create(throwable); } }; x.put(ToBeImplemented.class, new $.Transformer() { @Override public Result transform(Throwable throwable) { - Act.LOGGER.warn(throwable, "Error:"); return ActToBeImplemented.create(); } }); @@ -179,28 +177,24 @@ public Result transform(Throwable throwable) { x.put(IllegalStateException.class, new $.Transformer() { @Override public Result transform(Throwable throwable) { - Act.LOGGER.warn(throwable, "Error:"); return ActConflict.create(throwable); } }); x.put(ResourceNotFoundException.class, new $.Transformer() { @Override public Result transform(Throwable throwable) { - Act.LOGGER.warn(throwable, "Error:"); return ActNotFound.create(throwable); } }); x.put(AccessDeniedException.class, new $.Transformer() { @Override public Result transform(Throwable throwable) { - Act.LOGGER.warn(throwable, "Error:"); return ActForbidden.create(throwable); } }); $.Transformer badRequest = new $.Transformer() { @Override public Result transform(Throwable throwable) { - Act.LOGGER.warn(throwable, "Error:"); return ActBadRequest.create(throwable); } }; diff --git a/src/main/java/act/view/rythm/RythmView.java b/src/main/java/act/view/rythm/RythmView.java index 7de9777ba..4817b204a 100644 --- a/src/main/java/act/view/rythm/RythmView.java +++ b/src/main/java/act/view/rythm/RythmView.java @@ -127,6 +127,7 @@ private RythmEngine createEngine(App app) { p.put(ENGINE_CLASS_LOADER_PARENT_IMPL.getKey(), app.classLoader()); p.put(HOME_TMP.getKey(), createTempHome(app)); p.put(I18N_LOCALE.getKey(), config.locale()); + p.put(RESOURCE_BUNDLE_ENCODING.getKey(), config.resourceBundleEncoding()); Map map = config.rawConfiguration(); for (Object k : map.keySet()) { diff --git a/src/main/java/act/xio/undertow/UndertowCookieAdaptor.java b/src/main/java/act/xio/undertow/UndertowCookieAdaptor.java index 9d6ddb677..227c5f0a7 100644 --- a/src/main/java/act/xio/undertow/UndertowCookieAdaptor.java +++ b/src/main/java/act/xio/undertow/UndertowCookieAdaptor.java @@ -150,4 +150,31 @@ public Cookie setComment(String comment) { hc.comment(comment); return this; } + + // TODO - remove this method when we moved to Java 8 + @Override + public int compareTo(final Object other) { + final Cookie o = (Cookie) other; + int retVal = 0; + + // compare names + if (getName() == null && o.getName() != null) return -1; + if (getName() != null && o.getName() == null) return 1; + retVal = (getName() == null && o.getName() == null) ? 0 : getName().compareTo(o.getName()); + if (retVal != 0) return retVal; + + // compare paths + if (getPath() == null && o.getPath() != null) return -1; + if (getPath() != null && o.getPath() == null) return 1; + retVal = (getPath() == null && o.getPath() == null) ? 0 : getPath().compareTo(o.getPath()); + if (retVal != 0) return retVal; + + // compare domains + if (getDomain() == null && o.getDomain() != null) return -1; + if (getDomain() != null && o.getDomain() == null) return 1; + retVal = (getDomain() == null && o.getDomain() == null) ? 0 : getDomain().compareTo(o.getDomain()); + if (retVal != 0) return retVal; + + return 0; // equal + } } diff --git a/src/main/java/act/xio/undertow/UndertowNetwork.java b/src/main/java/act/xio/undertow/UndertowNetwork.java index 7b6370e74..ed6183a2c 100644 --- a/src/main/java/act/xio/undertow/UndertowNetwork.java +++ b/src/main/java/act/xio/undertow/UndertowNetwork.java @@ -21,6 +21,7 @@ */ import act.Act; +import act.conf.AppConfig; import act.controller.meta.ActionMethodMetaInfo; import act.ws.WebSocketConnectionManager; import act.xio.Network; @@ -33,6 +34,7 @@ import io.undertow.server.DefaultByteBufferPool; import io.undertow.server.HttpHandler; import io.undertow.server.protocol.http.HttpOpenListener; +import org.osgl.$; import org.osgl.logging.LogManager; import org.osgl.logging.Logger; import org.osgl.util.E; @@ -45,6 +47,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; +import java.net.ServerSocket; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyStore; @@ -95,6 +98,10 @@ protected void setUpClient(NetworkHandler client, int port, boolean secure) thro openListener.setRootHandler(handler); ChannelListener> acceptListener = ChannelListeners.openListenerAdapter(openListener); + if (Act.isTest()) { + debug("Try clearing random server socket: " + port); + AppConfig.clearRandomServerSocket(port); + } if (!secure) { AcceptingChannel server = worker.createStreamConnectionServer(new InetSocketAddress(port), acceptListener, socketOptions); server.resumeAccepts(); @@ -146,7 +153,8 @@ private XnioWorker createWorker() throws IOException { .set(Options.CONNECTION_LOW_WATER, 1000000) .set(Options.TCP_NODELAY, true) .set(Options.CORK, true) - .getMap()); + .getMap() + ); } private OptionMap createSocketOptions() { diff --git a/src/main/resources/aaa.authenticate.list b/src/main/resources/aaa.authenticate.list index 9e187701d..2a4b57674 100644 --- a/src/main/resources/aaa.authenticate.list +++ b/src/main/resources/aaa.authenticate.list @@ -7,10 +7,17 @@ -act.conf.CsrfConfAdmin -act.session.ShortLifeTokenGenerator -# The following endpoints are only public on dev mode +# The following endpoints are public on dev mode [dev]-act.apidoc. [dev]-act.Info [dev]-act.i18n. [dev]-act.job. [dev]-act.test. [dev]-act.controller.builtin.HelpPage + +# The following endpoints are public on test mode +[test]-act.apidoc. +[test]-act.Info +[test]-act.i18n. +[test]-act.job. +[test]-act.test. diff --git a/src/main/resources/asset/~act/apibook/apibook.tag b/src/main/resources/asset/~act/apibook/apibook.tag index 75c3622ec..0924423d2 100644 --- a/src/main/resources/asset/~act/apibook/apibook.tag +++ b/src/main/resources/asset/~act/apibook/apibook.tag @@ -9,7 +9,7 @@ }
-

API Book - { sysInfo.appName }

+

API Book - { sysInfo.appName }

actframework-{ sysInfo.actVersion }
@@ -17,6 +17,7 @@
+