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 extends StreamConnection> 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 @@
}
@@ -17,6 +17,7 @@
+