fix #1211; #1209 - logic and HTML page tune

This commit is contained in:
Gelin Luo 2019-10-02 08:06:12 +10:00
parent 26dacc4101
commit 23ff13fa93
11 changed files with 183 additions and 77 deletions

View File

@ -1,6 +1,7 @@
# ActFramework Change Log
**1.8.29**
* Incorrect message returned by `ActContext.i18n(template, args)` #1211
* Provide a default HTML page for async controller response #1209
* Allow it auto refresh when got 409 error during hot-reloading #1207
* "The response has already been started" error #1208

View File

@ -5,18 +5,18 @@
| aaa | 1.5.4 | 1.5.5 | 1.5.5 | 1.5.5 | 1.5.5 | 1.5.5 | 1.6.0 | 1.6.1 | 1.6.1 |
| beetl | 1.4.7 | 1.4.8 | 1.5.1 | 1.5.2 | 1.5.3 | 1.5.4 | 1.6.0 | 1.6.1 | 1.6.1 |
| beetlsql | 1.5.7 | 1.5.8 | 1.5.9 | 1.5.10 | 1.5.11 | 1.6.0 | 1.7.0 | 1.7.1 | 1.7.1 |
| ebean-java7 | 1.7.5 | 1.7.6 | 1.7.6 | 1.7.7 | 1.7.7 | 1.7.8 | 1.7.8 | 1.7.8 | 1.7.8 |
| ebean(java8) | 1.7.6 | 1.7.7 | 1.7.7 | 1.7.8 | 1.7.8 | 1.7.9 | 1.7.9 | 1.7.9 | 1.7.9 |
| eclipselink(java8) | 1.5.7 | 1.5.8 | 1.5.8 | 1.5.9 | 1.5.9 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.0 |
| excel | 1.5.0 | 1.6.0 | 1.6.1 | 1.6.2 | 1.6.2 | 1.6.2 | 1.7.0 | 1.7.1 | 1.7.1 |
| ebean-java7 | 1.7.5 | 1.7.6 | 1.7.6 | 1.7.7 | 1.7.7 | 1.7.8 | 1.7.8 | 1.7.8 | 1.7.9 |
| ebean(java8) | 1.7.6 | 1.7.7 | 1.7.7 | 1.7.8 | 1.7.8 | 1.7.9 | 1.7.9 | 1.7.9 | 1.7.10 |
| eclipselink(java8) | 1.5.7 | 1.5.8 | 1.5.8 | 1.5.9 | 1.5.9 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.1 |
| excel | 1.5.0 | 1.6.0 | 1.6.1 | 1.6.2 | 1.6.2 | 1.6.2 | 1.7.0 | 1.7.1 | 1.7.2 |
| freemarker | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.7 | 1.3.7 |
| hibernate | 1.5.6 | 1.5.7 | 1.5.7 | 1.5.8 | 1.5.8 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.0 |
| hibernate | 1.5.6 | 1.5.7 | 1.5.7 | 1.5.8 | 1.5.8 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.1 |
| jax-rs(java8) | 1.0.5 | 1.0.5 | 1.0.5 | 1.0.5 | 1.0.5 | 1.0.5 | 1.0.5 | 1.0.5 | 1.0.5 |
| jpa-common | 1.5.6 | 1.5.7 | 1.5.7 | 1.5.8 | 1.5.8 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.0 |
| morphia | 1.6.6 | 1.6.7 | 1.6.7 | 1.7.1 | 1.7.2 | 1.7.2 | 1.7.2 | 1.7.2 | 1.7.2 |
| jpa-common | 1.5.6 | 1.5.7 | 1.5.7 | 1.5.8 | 1.5.8 | 1.6.0 | 1.6.0 | 1.6.0 | 1.6.1 |
| morphia | 1.6.6 | 1.6.7 | 1.6.7 | 1.7.1 | 1.7.2 | 1.7.2 | 1.7.2 | 1.7.2 | 1.7.3 |
| mustache(java8) | 1.4.5 | 1.4.5 | 1.4.6 | 1.4.6 | 1.4.6 | 1.4.6 | 1.4.6 | 1.4.6 | 1.4.6 |
| social | 0.12.5 | 0.12.5 | 0.12.6 | 0.12.6 | 0.12.6 | 0.12.6 | 0.12.6 | 0.12.6 | 0.12.6 |
| sql-common | 1.4.4 | 1.4.5 | 1.4.5 | 1.4.6 | 1.4.6 | 1.5.0 | 1.5.0 | 1.5.0 | 1.5.0 |
| sql-common | 1.4.4 | 1.4.5 | 1.4.5 | 1.4.6 | 1.4.6 | 1.5.0 | 1.5.0 | 1.5.0 | 1.5.1 |
| storage(java8) | 0.13.5 | 0.13.6 | 0.13.6 | 0.13.6 | 0.13.6 | 0.13.6 | 0.14.0 | 0.14.0 | 0.14.0 |
| thymeleaf | 1.3.5 | 1.3.5 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.6 |
| velocity | 1.3.5 | 1.3.5 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.6 | 1.3.7 | 1.3.7 |

BIN
doc/img/gh1209a.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -0,0 +1,59 @@
package act.handler.builtin.controller;
/*-
* #%L
* ACT Framework
* %%
* Copyright (C) 2014 - 2019 ActFramework
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import act.Act;
import act.util.SimpleProgressGauge;
/**
* Used for track async request handler method progress
*/
public class ControllerProgressGauge extends SimpleProgressGauge {
@Override
public void fail(String error) {
this.error = error;
// do not trigger update event yet
}
@Override
public void stepBy(int steps) {
currentSteps += steps;
if (!isDone()) {
triggerUpdateEvent();
}
}
@Override
public void stepTo(int steps) {
if (currentSteps != steps) {
currentSteps = steps;
if (!isDone()) {
triggerUpdateEvent();
}
}
}
public void commitFinalState() {
triggerUpdateEvent(true);
Act.LOGGER.info("!!!!!!!!!!!!!!!!! final state committed");
}
}

View File

@ -73,6 +73,7 @@ import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import javax.enterprise.context.ApplicationScoped;
import javax.validation.ConstraintViolation;
@ -655,19 +656,27 @@ public class ReflectedHandlerInvoker<M extends HandlerMethodMetaInfo> extends Lo
if (async) {
final JobManager jobManager = context.app().jobManager();
final String jobId = jobManager.randomJobId();
final String reqInfo = context.req().toString();
jobManager.prepare(jobId, new TrackableWorker() {
@Override
protected void run(ProgressGauge progressGauge) {
if (isTraceEnabled()) {
trace("about invoking request handler[%s] asynchronously: %s", reqInfo, jobId);
}
try {
Object o = invoke(context, controller, params);
if (null == o) {
o = "done";
}
jobManager.cacheResult(jobId, o, method);
context.progress().commitFinalState();
} catch (Exception e) {
warn(e, "Error executing async handler: " + method);
context.progress().fail(e.getMessage());
jobManager.cacheResult(jobId, C.Map("failed", true), method);
String errMsg = S.buffer("Error handling request: ").append(reqInfo).toString();
warn(e, errMsg);
ControllerProgressGauge gauge = context.progress();
gauge.fail(errMsg);
jobManager.cacheResult(jobId, C.Map("error", e.getLocalizedMessage()), method);
gauge.commitFinalState();
}
}
});
@ -675,12 +684,14 @@ public class ReflectedHandlerInvoker<M extends HandlerMethodMetaInfo> extends Lo
WebSocketConnectionManager wscm = app.getInstance(WebSocketConnectionManager.class);
wscm.subscribe(context.session(), SimpleProgressGauge.wsJobProgressTag(jobId));
jobManager.now(jobId);
if (context.req().accept() == H.Format.HTML) {
boolean isHtml = context.req().accept() == H.Format.HTML;
if (isHtml) {
context.templatePath("/act/asyncJob.html");
context.renderArg("jobId", jobId);
return RenderTemplate.get();
} else {
return new RenderJSON(C.Map("jobId", jobId, "jobResultUrl", S.concat("/~/jobs/", jobId, "/result")));
String resultUrl = context.router().fullUrl("/~/jobs/%s/result", jobId);
return new RenderJSON(C.Map("jobId", jobId, "resultUrl", resultUrl));
}
}

View File

@ -88,21 +88,26 @@ public class I18n {
bundleName = act_messages.class.getName();
}
ResourceBundle bundle;
String msg = msgId;
try {
bundle = ResourceBundle.getBundle(bundleName, $.requireNotNull(locale), Act.app().classLoader());
} catch (MissingResourceException e) {
return msgId;
}
String msg = msgId;
if (ignoreError) {
if (bundle.containsKey(msgId)) {
msg = bundle.getString(msgId);
if (ignoreError) {
logger.warn("Cannot find bundle: %s", bundleName);
}
} else {
try {
msg = bundle.getString(msgId);
} catch (MissingResourceException e) {
logger.warn("Cannot find i18n message key: %s", msgId);
bundle = null;
}
if (null != bundle) {
if (ignoreError) {
if (bundle.containsKey(msgId)) {
msg = bundle.getString(msgId);
}
} else {
try {
msg = bundle.getString(msgId);
} catch (MissingResourceException e) {
logger.warn("Cannot find i18n message key: %s", msgId);
}
}
}
int len = args.length;
@ -116,6 +121,13 @@ public class I18n {
resolvedArgs[i] = arg;
}
}
if (msg.contains("%")) {
// try String.format first
String result = S.fmt(msg, resolvedArgs);
if (S.neq(result, msg)) {
return result;
}
}
MessageFormat formatter = new MessageFormat(msg, locale);
msg = formatter.format(resolvedArgs);
}

View File

@ -28,6 +28,7 @@ import act.app.ActionContext;
import act.app.App;
import act.cli.CliContext;
import act.conf.AppConfig;
import act.handler.builtin.controller.ControllerProgressGauge;
import act.i18n.I18n;
import act.mail.MailerContext;
import act.view.Template;
@ -277,7 +278,7 @@ public interface ActContext<CTX_TYPE extends ActContext> extends ParamValueProvi
private Locale locale;
private int fieldOutputVarCount;
private boolean noTemplateCache;
private volatile ProgressGauge progress;
private volatile ControllerProgressGauge progress;
protected String jobId;
private Method handlerMethod;
private Method currentMethod;
@ -720,11 +721,11 @@ public interface ActContext<CTX_TYPE extends ActContext> extends ParamValueProvi
app().jobManager().setJobProgressGauge(jobId, progress());
}
public ProgressGauge progress() {
public ControllerProgressGauge progress() {
if (null == progress) {
synchronized (this) {
if (null == progress) {
progress = new SimpleProgressGauge();
progress = new ControllerProgressGauge();
}
}
}

View File

@ -83,12 +83,12 @@ public class SimpleProgressGauge extends DestroyableBase implements ProgressGaug
private String id;
private boolean markedAsDown;
private int maxHint;
private int currentSteps;
private String error;
protected int maxHint;
protected int currentSteps;
protected String error;
private Map<String, Object> payload = new HashMap<>();
private transient int percent;
private ProgressGauge delegate;
protected ProgressGauge delegate;
private List<Listener> listeners = new ArrayList<>();
private ReadWriteLock listenerListLock = new ReentrantReadWriteLock();
@ -161,12 +161,7 @@ public class SimpleProgressGauge extends DestroyableBase implements ProgressGaug
@Override
public void step() {
if (null != delegate) {
delegate.step();
} else {
currentSteps++;
triggerUpdateEvent();
}
this.stepBy(1);
}
@Override
@ -310,11 +305,11 @@ public class SimpleProgressGauge extends DestroyableBase implements ProgressGaug
return this.payload;
}
private void triggerUpdateEvent() {
protected void triggerUpdateEvent() {
triggerUpdateEvent(false);
}
private void triggerUpdateEvent(boolean forceTriggerEvent) {
protected void triggerUpdateEvent(boolean forceTriggerEvent) {
if (forceTriggerEvent || percentageChanged()) {
Lock lock = listenerListLock.readLock();
lock.lock();

View File

@ -15,7 +15,7 @@
font-family: "Noto Sans", Tahoma, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
}
#test-result {
#job-result {
display: none;
}
#in-progress {
@ -120,10 +120,17 @@
font-weight: bold;
margin-top: 4px;
}
pre#result {
#result-detail {
margin-top: 30px;
padding: 10px;
border: 1px solid #aaa;
font-family: "Envy Code R", "Fira Code", "Source Code Pro Semibold", Monaco, Courier, monospace;
font-weight: bold;
font-size: 13px;
}
#result-detail.error {
border-color: #ff4848;
color: #ff4848;
}
</style>
</head>
@ -136,44 +143,14 @@
<div id="progress"></div>
<div id="progress-text"></div>
</div>
<ul id="error-scenario-list" class="FAIL">
</ul>
</div>
<div id="test-result">
<div id="job-result">
<h1>Job Result</h1>
<div class="version">@(_app.name().capFirst())-@(_app.version().getVersion())</div>
<pre id="result" style="margin-top: 30px"></pre>
<pre id="result-detail"></pre>
</div>
<script>
var getResultHandle = false
function testCallback(data) {
var ws = $.createWebSocket('/~/ws/jobs/@jobId/progress', function(e) {
alert("Unknown error")
})
ws.onmessage = function(msg) {
if (getResultHandle) {
clearTimeout(getResultHandle)
getResultHandle = false
}
if (!msg.data) {
alert("no job progress")
return
}
var data = JSON.parse(msg.data)
var gauge = data.act_job_progress
if (gauge.done || gauge.isFailed) {
$('#progress-text').text('100/100')
$('#progress').width(800)
setTimeout(getResult, 100)
getResult()
} else {
$('#progress-text').text((gauge.currentSteps + 1) + '/' + (gauge.maxHint))
$('#progress').width(gauge.progressPercent * 800 / 100)
}
}
}
testCallback()
function getResult() {
function getResult(fail) {
$.getJSON('/~/jobs/@jobId/result', function(data) {
if (data.failed) {
favicon = '/~/asset/img/test-failure.ico?v=@act.Act.VERSION.getBuildNumber()'
@ -182,12 +159,30 @@
}
$('#favicon-link').attr('href', favicon)
var result = JSON.stringify(data, null, 2)
$('#result').text(result)
$('#result-detail').text(result)
$('#in-progress').hide()
$('#test-result').show()
if (fail) {
$('#result-detail').addClass('error')
}
$('#job-result').show()
})
}
getResultHandle = setTimeout(getResult, 1000)
function monitorProgress() {
var ws = $.createWebSocket('/~/ws/jobs/@jobId/progress')
ws.onmessage = function(msg) {
var data = JSON.parse(msg.data)
var gauge = data.act_job_progress
if (gauge.done || gauge.failed) {
$('#progress-text').text('100/100')
$('#progress').width(800)
getResult(gauge.failed)
} else {
$('#progress-text').text((gauge.currentSteps + 1) + '/' + (gauge.maxHint))
$('#progress').width(gauge.progressPercent * 800 / 100)
}
}
}
monitorProgress()
</script>
</body>
</html>

View File

@ -0,0 +1,20 @@
package ghissues;
import act.app.ActionContext;
import act.controller.annotation.UrlContext;
import org.osgl.mvc.annotation.GetAction;
@UrlContext("1211")
public class Gh1211 extends BaseController {
@GetAction
public String test(ActionContext context) {
return context.i18n("hello %s", "world");
}
@GetAction("2")
public String test2(ActionContext context) {
return context.i18n("hello {0}", "world");
}
}

View File

@ -0,0 +1,12 @@
Scenario(1211):
interactions:
- description: test case 1
request:
get: 1211
response:
result: hello world
- description: test case 2
request:
get: 1211/2
response:
result: hello world