After migrating JRE version 1.8.0_66 to 1.8.0_111 I've encountered an issue with making upcall from JavaScript to JavaFX.
Long story short: while there is a running background thread, WebView/WebEngine refuses to make JS-to-Java calls.
I use WebView to render HTML content which is generated from a domain Data Model (DM). Content contains elements with a handler assigned to it as follows:
<a href='#' onclick='explainHeadWord(this)'>some_word</a>
JS part looks like:
function explainHeadWord(hwElement) {
jsBridge.jsHandleQuery(hwElement.innerHTML);
}
function testBridge() {
jsBridge.jsTest();
}
where jsBridge is an inner Java class of the Controller
public class JSBridge {
public void jsHandleQuery(String headWord) {
log("jsBridge: jsHandleQuery: requested %s", headWord);
handleQuery(headWord);
}
public void jsTest() {
log("jsBridge: jsTest: test succeeded ");
}
}
which is injected as follows:
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
engine.setJavaScriptEnabled(true);
JSObject window = (JSObject) engine.executeScript("window");
window.setMember("jsBridge", new JSBridge());
//engine.executeScript("jsTest()");
//engine.executeScript("explainHeadWord(document.getElementsByTagName('a')[0])");
//engine.executeScript("jsBridge.jsHandleQuery(document.getElementsByTagName('a')[0])");
}
Besides the main DM I have an Index of cross-references which is Map<String, Collection<String>> built from DM, and a trigger method rebuilding that Index in the background each time DM changes. The first approach (which is worked fine on version 1.8.0_66) was based on ExecutorService:
private ExecutorService executor = Executors.newCachedThreadPool();
private Future<Boolean> indexer = executor.submit(() -> false);
...
private void rebuildIndex() {
executor.submit(() -> {
indexer.cancel(true);
indexer = executor.submit(() -> {
fullSearchIndex = getIndex();
if (isIndexingAborted()) return false;
return true;
});
try {
if (indexer.get()) {
log("resetIndex: Done");
updateTableView();
}
} catch (InterruptedException e) {
...
}
});
}
As was expected, clicking on an anchor in a WebView resulted to a JS-call jsBridge.jsHandleQuery(hwElement.innerHTML) and eventually to handleQuery(headWord) method call implemented in Controller. But after migrating JRE to version 1.8.0_111 WebView stopped to respond to an anchor clicking.
I've investigated logs and found that injecting jsBridge was successful as well as executing test scripts commented in the code below line window.setMember(). Clicking on an <a> element led to nothing. But without running test scripts (commented) there were records appeared in the log:
<record>
<date>2017-01-09T02:00:47</date>
<millis>1483920047169</millis>
<sequence>160</sequence>
<logger>com.sun.webkit.WebPage</logger>
<level>FINE</level>
<class>com.sun.webkit.WebPage</class>
<method>fwkAddMessageToConsole</method>
<thread>11</thread>
<message>fwkAddMessageToConsole(): message = TypeError: jsBridge.jsHandleQuery is not a function. (In 'jsBridge.jsHandleQuery(hwElement.innerHTML)', 'jsBridge.jsHandleQuery' is undefined), lineNumber = 26, sourceId = jar:file:/.../jar.jar!/view.js</message>
</record>
And after a moment the background (indexing) thread was complete and a content in the WebView was reloaded clicking on <a> elements starts to respond again - jsBridge.jsHandleQuery was executed.
The indexing thread executes getIndex() method that traverses DM and returns collected into Map data from DM. There's niether any interaction with FX application thread nor WebView depends on Index. Substituting fullSearchIndex = getMockIndex();
private Map<String, Collection<String>> getMockIndex() {
try {
Thread.sleep(20000);
} catch (InterruptedException e) { }
return Collections.emptyMap();
}
in background thread doesn't change <a>'s behavior.
Next step was to refactor background thread into FX style by utilizing
javafx.concurrent.Service but the result is the same.
Will appreciate for pointing at what do I do wrong and how to tackle this issue.
Try to instantiate Bridge outside of the listener, i.e.,
final JSBridge bridge = new JSBridge();
engine.setJavaScriptEnabled(true);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
JSObject window = (JSObject) engine.executeScript("window");
window.setMember("jsBridge", bridge);
}
});
(Worked for me migrating from 1.8.0_91 to 1.8.0_121).
Related
I have been able to call JavaScript from C# inside the MainActivity but I'm trying to do so from an object. The majority of my app runs inside a WebView, my JavaScript calls to my C# Interface invoking an asynchronous function and when it's complete I would like to call back to my JavaScript but am unable to do so. Here is my current setup:
In my MainActivity I setup my WebView as such:
browser = FindViewById<WebView>(Resource.Id.mainView);
browser.SetInitialScale(1);
browser.SetWebChromeClient(new GeoWebChromeClient());
browser.Settings.UseWideViewPort = true;
browser.Settings.LoadWithOverviewMode = true;
if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat)
{
WebView.SetWebContentsDebuggingEnabled(true);
}
browser.Settings.SetGeolocationEnabled(true);
browser.Settings.JavaScriptEnabled = true;
browser.AddJavascriptInterface(new JSCSMedium(this, ref browser), "Android");
browser.LoadUrl("file:///android_asset/web/index.html");
Then inside the JSCSMedium object I have an asynch function:
[Export]
[JavascriptInterface]
public void SyncApps()
{
Task t = Task.Run(() => {
IList<ApplicationInfo> tempApps = Application.Context.PackageManager.GetInstalledApplications(PackageInfoFlags.MatchDirectBootAware);
string packageName = "";
string appName = "";
for (int i = 0; i < tempApps.Count(); i++)
{
packageName = tempApps[i].PackageName;
appName = tempApps[i].LoadLabel(manager);
var root = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
var filePath = System.IO.Path.Combine(root, "system");
filePath = System.IO.Path.Combine(filePath, packageName);
if (!System.IO.Directory.Exists(filePath))
{
System.IO.Directory.CreateDirectory(filePath);
}
filePath = System.IO.Path.Combine(filePath, "icon.png");
if (!System.IO.File.Exists(filePath))
{
Drawable icon = tempApps[i].LoadIcon(Application.Context.PackageManager);
BitmapDrawable bd = (BitmapDrawable)icon;
CreateAppIcon(bd.Bitmap, packageName);
}
Intent intent = Application.Context.PackageManager.GetLaunchIntentForPackage(packageName);
if (intent != null)
{
apps.Add(tempApps[i]);
}
}
});
}
If I don't do the C# as an async function it runs and returns data fine, but this process takes a bit of time and blocks the app temporarily. Inside my MainActivity I can call JavaScript just fine with:
browser.EvaluateJavascript("javascript: alert('fooBar');", null);
But browser is not accessible inside the JSCSMedium. I've tried passing the browser object as a reference and normally but it throws an exception stating that the EvaluateJavascript function must be called on the same thread as where it was instantiated. I've also tried sending a reference of my MainActivity to the JSCSMedium and call a function inside the MainActivity to run the EvaluateJavascript but it seems to do nothing. No error, not crash, just nothing.
The problem is Task.Run forces the code to run in the thread pool, and browser.EvaluateJavascript needs to run on the main thread.
You have at least two options here, depending on your needs:
1) Run the EvaluateJavascript call inside the Task.Run block with something like:
var h = new Handler(Looper.MainLooper);
h.Post(() => browser.EvaluateJavascript("javascript: alert('fooBar');", null));
2) Run the EvaluateJavascript call outside the Task.Run block:
[Export]
[JavascriptInterface]
public async void SyncApps()
{
await Task.Run(() => {
//...
});
browser.EvaluateJavascript("javascript: alert('fooBar');", null);
}
Not really sure if you can change the return type of SyncApps(). If JS doesn't complain, you better change that too.
I'm dealing with a HttpSessionRequiredException and read about (https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc) how to catch it via the #ExceptionHandling annotation.
This does work since my breakpoint hits the spot. Unfortunately the string containing the view name does not trigger any view resolving and therefore no error page is being rendered.
Steps to Reproduce:
Open http://localhost:8080/manager (Session Attributes are being initialized)
Restart Web Application (Session/SessionAttributes are being reset)
Fill out form and press button (launches POST Request via AJAX - see Edit #2)
ExceptionHandler jumps in. Since a HttpSessionRequiredException is being thrown due the missing session attributes
Check Network Panel in Chrome which yields following information on the failed POST:
Controller Class incl. Exception Handler
#Controller
#Log
#RequestMapping(value = "/manager")
#SessionAttributes({"moduleDescriptors", "Environments", "moduleExecutionData"})
public class ManagerController {
private static final String MANAGER_VIEW = "manager";
private final ManagerHelper managerHelper;
private final ModuleExecution moduleExecution;
private final SystemProperties SystemProperties;
#Autowired
public ManagerController(ManagerHelper managerHelper, ModuleExecution moduleExecution,
SystemProperties SystemProperties) {
this.managerHelper = managerHelper;
this.moduleExecution = moduleExecution;
this.SystemProperties = SystemProperties;
}
#GetMapping
public ModelAndView render() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName(MANAGER_VIEW);
modelAndView.addObject("moduleExecutionData", new ModuleExecutionData());
List<ModuleDescriptor> moduleDescriptorsFromServices = managerHelper.getModuleDescriptorsFromServices();
modelAndView.addObject("moduleDescriptors", moduleDescriptorsFromServices);
//TODO: Change varName for Systems
modelAndView.addObject("Environments", SystemProperties.getEnvironments());
log.info("Found " + moduleDescriptorsFromServices.size() + " module descriptors");
return modelAndView;
}
#PostMapping
public String execute(#ModelAttribute ModuleExecutionData moduleExecutionData) {
moduleExecution.execute(moduleExecutionData);
// sessionStatus.setComplete();
return MANAGER_VIEW;
}
#ExceptionHandler(HttpSessionRequiredException.class)
public String sessionError(){
return "sessionError";
}
}
My view lies within all the folder with all working views.
Any idea what I'm doing wrong? I'm configuring spring completely with annotations - not XML.
Edit:
I checked the response and saw that it actually returns the HTML of the View. But this HTML is not rendered. Is it because I made a POST request? Which actually ignores any return HTML?
But that does not make sense - because my original PostHandler does return a view as well and it is rendered.
PostHandler
#PostMapping
public String execute(#ModelAttribute ModuleExecutionData moduleExecutionData) {
moduleExecution.execute(moduleExecutionData);
return MANAGER_VIEW;
}
Edit #2
Here is the POST JavaScript Ajax logic
function postForm(event) {
const moduleId = $(event.currentTarget).data("module-id");
const formData = $("#" + moduleId).serialize();
$.ajax({
type: "post",
data: formData,
url: "/manager",
dataType: "json"
});
}
I'm porting my jxbrowser integrations from 4.9x to 6.11.x, and i've got a problem with the JavaScript - JavaBridge with The IFrame. I register correctly with the follow code, and work as expect when I call the function from js in the main page. But doesn't work when the function is called from js inside the iframe.
browser.addScriptContextListener(new ScriptContextAdapter() {
#Override
public void onScriptContextCreated(ScriptContextEvent event) {
final Browser browser = event.getBrowser();
JSValue window = browser.executeJavaScriptAndReturnValue("window");
window.asObject().setProperty("resolveMsg", new JSFunctionCallback() {
#Override
public Object invoke(Object... params) {
String msgId = (String) params[0];
Builder builder = WrLocMsg.builder(msgId);
return builder.buildOriginalMessage();
}
});
}
});
Resolved by recovery window in this way:
JSValue window = browser.executeJavaScriptAndReturnValue(event.getJSContext().getFrameId(),"window");
I have some ajax Behaviour that should pick some data using JS, and turn it back to Java. Sometimes it works but quite ofen it is just add url parameter and do page refresing/
public abstract class LoggedVKIdBehaviour extends AbstractDefaultAjaxBehavior {
private static final Logger logger = LoggerFactory.getLogger(LoggedVKIdBehaviour.class);
#Override
protected void respond(AjaxRequestTarget target) {
String loggedVkId = RequestCycle.get().getRequest().getRequestParameters().getParameterValue("logged_vkid").toString();
logger.info("ajax has comming with logged VK ID " + loggedVkId);
recived(target, loggedVkId);
}
protected abstract void recived(AjaxRequestTarget target, String loggedVkId);
#Override
public void renderHead(final Component component, IHeaderResponse response) {
super.renderHead(component, response);
Map<String, Object> map = new HashMap<>();
map.put("callbackFunction", getCallbackFunction(CallbackParameter.explicit("logged_vkid")));
//
PackageTextTemplate ptt = new PackageTextTemplate(LoggedVKIdBehaviour.class, "vkid_callback.js");
OnDomReadyHeaderItem onDomReadyHeaderItem = OnDomReadyHeaderItem.forScript(ptt.asString(map));
response.render(onDomReadyHeaderItem);
}
}
js template
var calback = ${callbackFunction};
var logged_vk_id = 11;
function authInfo(response) {
if (response.session) {
logged_vk_id = response.session.mid;
calback(response.session.mid);
console.log("recived callback from VK " + logged_vk_id);
}
}
$(document).ready(function () {
VK.Auth.getLoginStatus(authInfo);
});
it is do recursive redirection like http://localhost:8080/mytool/product/1?logged_vkid=332797331&logged_vkid=332797331&logged_vkid=332797331&logged_vkid=332797331&logged_vkid=332773...
As i understand Ajaj technology - iti asynchronus requests, that shouldn't touch main url at all. So what is the reason for page refreshing?
this is generated Callback function
function (logged_vkid) {
var attrs = {"u":"../wicket/bookmarkable/com.tac.kulik.pages.product.ProductPage?12-1.IBehaviorListener.0-&productID=1"};
var params = [{"name":"logged_vkid","value":logged_vkid}];
attrs.ep = params.concat(attrs.ep || []);
Wicket.Ajax.ajax(attrs);
}
I use wicket 7.2
I did a lot investigations for few days. And found that when i remove
setPageManagerProvider(new NoSerializationPageManagerProvider(this));
Application throw me exepton in polite logs
org.apache.wicket.WicketRuntimeException: A problem occurred while
trying to collect debug information about not serializable object look
like it is could come from aused by: java.io.NotSerializableException:
com.tac.kulik.panel.smaccounts.SMAccountsPanel$1
which means that page tryed to be serialized for SOME REASON but $1 it is mean Anonimous class. I had few class created anonimously to ges some ajax links coming from ListView to be managed on parent panel. So After removing this Anonimous class logic, everything start and run well.
So i am happy, but still don't understand which reason page did serialization after ajax, and what the reason was to refresh whole page.
I'm trying to get response from U2F Token in GWT project using this source code:
public class Test implements EntryPoint {
#Override
public void onModuleLoad() {
Window.alert("Alert 3:"+u2FTest());
}
public static native String u2FTest()/*-{
var respond = {rep: "Clear"};
var RegistrationData = {"challenge":"dG7vN-E440ZnJaKQ7Ynq8AemLHziJfKrBpIBi5OET_0",
"appId":"https://localhost:8443",
"version":"U2F_V2"};
$wnd.u2f.register([RegistrationData], [],
function(data) {if(data.errorCode) {
alert("U2F failed with error: " + data.errorCode);
return;
}
respond.rep=JSON.stringify(data);
alert("Alert 1: "+respond.rep);
});
alert("Alert 2: "+respond.rep);
return respond.rep;
}-*/;
}
for some reasons I get The Alerts like so:
(Alert 2) first with "Clear" result
(Alert 3) with "Clear"
(Alert 1) with Token response
Normally I've to get (Alert 1) with Token response then 2,3. So how can I stop execution until I'll get the token response
Thank you,
Embrace asynchronicity!
public static native void u2FTest(com.google.gwt.core.client.Callback<String, Integer> callback) /*-{
// …
$wnd.u2f.register(regReqs, signReqs, $entry(function(response) {
if (response.errorCode) {
callback.#com.google.gwt.core.client.Callback::onFailure(*)(#java.lang.Integer::valueOf(I)(response.errorCode));
} else {
callback.#com.google.gwt.core.client.Callback::onSuccess(*)(JSON.stringify(response));
}
}));
}*-/;
(don't forget to wrap callbacks in $entry() so that exceptions are routed to the GWT.UnhandledExceptionHandler, if there's one)