/*
* Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The contents of this file are subject to the terms of either the Universal Permissive License
* v 1.0 as shown at http://oss.oracle.com/licenses/upl
*
* or the following license:
*
* Redistribution and use in source and binary forms, with or without modification, are permitted
* provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials provided with
* the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to
* endorse or promote products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjdk.jmc.flightrecorder.ui.overview;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.BrowserFunction;
import org.eclipse.swt.browser.CloseWindowListener;
import org.eclipse.swt.browser.OpenWindowListener;
import org.eclipse.swt.browser.ProgressAdapter;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.browser.WindowEvent;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.openjdk.jmc.common.IState;
import org.openjdk.jmc.common.IWritableState;
import org.openjdk.jmc.flightrecorder.rules.IRule;
import org.openjdk.jmc.flightrecorder.rules.Result;
import org.openjdk.jmc.flightrecorder.rules.Severity;
import org.openjdk.jmc.flightrecorder.rules.report.html.internal.HtmlResultGroup;
import org.openjdk.jmc.flightrecorder.rules.report.html.internal.HtmlResultProvider;
import org.openjdk.jmc.flightrecorder.rules.report.html.internal.RulesHtmlToolkit;
import org.openjdk.jmc.flightrecorder.rules.util.RulesToolkit;
import org.openjdk.jmc.flightrecorder.ui.DataPageDescriptor;
import org.openjdk.jmc.flightrecorder.ui.FlightRecorderUI;
import org.openjdk.jmc.flightrecorder.ui.IPageContainer;
import org.openjdk.jmc.ui.misc.DisplayToolkit;
/**
* This class handles creation of a HTML/JS based result report, it has two modes, Single Page and
* Full Report. Single Page is used by the ResultPage PageBookView and the Full Report by the Result
* Overview page.
*/
public class ResultReportUi {
private static final String OVERVIEW_MAKE_SCALABLE = "overview.makeScalable();"; //$NON-NLS-1$
private static final String OVERVIEW_UPDATE_PAGE_HEADERS_VISIBILITY = "overview.updatePageHeadersVisibility();"; //$NON-NLS-1$
private static final Pattern HTML_ANCHOR_PATTERN = Pattern.compile("(.*?)"); //$NON-NLS-1$
private static final String OPEN_BROWSER_WINDOW = "openWindowByUrl"; //$NON-NLS-1$
private static class Linker extends BrowserFunction {
private Iterable resultGroups;
private IPageContainer editor;
public Linker(Browser browser, String name, Iterable resultGroups, IPageContainer editor) {
super(browser, name);
this.resultGroups = resultGroups;
this.editor = editor;
}
@Override
public Object function(Object[] arguments) {
if (arguments.length != 1 && !(arguments[0] instanceof String)) {
return null;
}
String id = arguments[0].toString();
for (HtmlResultGroup group : resultGroups) {
if (group instanceof PageDescriptorResultGroup && id.equals(group.getId())) {
editor.navigateTo(((PageDescriptorResultGroup) group).getDescriptor());
return null;
} else {
if (hasPageAsChild(group, id)) {
return null;
}
}
}
return null;
}
private boolean hasPageAsChild(HtmlResultGroup parent, String id) {
if (parent instanceof PageDescriptorResultGroup && id.equals(parent.getId())) {
editor.navigateTo(((PageDescriptorResultGroup) parent).getDescriptor());
return true;
}
if (!parent.hasChildren()) {
return false;
} else {
for (HtmlResultGroup child : parent.getChildren()) {
if (hasPageAsChild(child, id)) {
return true;
}
}
}
return false;
}
}
private class Expander extends BrowserFunction {
public Expander(Browser browser, String name) {
super(browser, name);
}
@Override
public Object function(Object[] arguments) {
resultExpandedStates.put(arguments[0].toString(), (Boolean) arguments[1]);
return null;
}
}
public class OpenWindowFunction extends BrowserFunction {
public OpenWindowFunction (final Browser browser, final String name) {
super(browser, name);
}
public Object function (Object[] arguments) {
final String url = String.valueOf(arguments[0]);
final String title = String.valueOf(arguments[1]);
openBrowserByUrl(url, title);
return null;
}
}
private static class PageContainerResultProvider implements HtmlResultProvider {
private IPageContainer editor;
public PageContainerResultProvider(IPageContainer editor) {
this.editor = editor;
}
@Override
public Collection getResults(Collection topics) {
return editor.getRuleManager().getResults(topics);
}
}
private static class PageDescriptorResultGroup implements HtmlResultGroup {
private DataPageDescriptor descriptor;
private List children;
public PageDescriptorResultGroup(DataPageDescriptor descriptor) {
this.descriptor = descriptor;
children = new ArrayList<>();
for (DataPageDescriptor dpdChild : descriptor.getChildren()) {
children.add(new PageDescriptorResultGroup(dpdChild));
}
}
@Override
public List getChildren() {
return children;
}
@Override
public boolean hasChildren() {
return !children.isEmpty();
}
@Override
public Collection getTopics() {
return Stream.of(descriptor.getTopics()).collect(Collectors.toList());
}
@Override
public String getId() {
return Integer.toString(descriptor.hashCode());
}
@Override
public String getName() {
return descriptor.getName();
}
@Override
public String getImage() {
ImageDescriptor image = descriptor.getImageDescriptor();
if (image == null) {
return null;
}
ImageLoader loader = new ImageLoader();
ByteArrayOutputStream out = new ByteArrayOutputStream();
loader.data = new ImageData[] {image.getImageData()};
loader.save(out, SWT.IMAGE_PNG);
return Base64.getEncoder().encodeToString(out.toByteArray());
}
public DataPageDescriptor getDescriptor() {
return descriptor;
}
public static Collection build(Collection descriptors) {
return descriptors.stream().map(dpd -> new PageDescriptorResultGroup(dpd)).collect(Collectors.toList());
}
}
private final HashMap resultExpandedStates = new HashMap<>();
private Boolean showOk = false;
private Boolean isLoaded = false;
private Browser browser;
private IPageContainer editor;
private Collection descriptors;
private boolean isSinglePage = false;
private void openBrowserByUrl(final String url, final String title) {
final Display display = Display.getDefault();
final Shell shell = new Shell(display);
shell.setText(title);
shell.setLayout(new FillLayout());
final Browser browser = new Browser(shell, SWT.NONE);
initializeBrowser(display, browser, shell);
shell.open();
browser.setUrl(url);
}
private void initializeBrowser(final Display display, final Browser browser, final Shell shell) {
browser.addOpenWindowListener(new OpenWindowListener() {
public void open(WindowEvent event) {
initializeBrowser(display, browser, shell);
event.browser = browser;
}
});
browser.addCloseWindowListener(new CloseWindowListener() {
public void close(WindowEvent event) {
Browser browser = (Browser)event.widget;
Shell shell = browser.getShell();
shell.close();
}
});
}
/*
* We replace the anchors in the HTML when running in the JMC UI to make
* it possible to follow them. See JMC-5419 for more information.
*/
private static String adjustAnchorFollowAction(String html) {
Map map = new HashMap<>();
Matcher m = HTML_ANCHOR_PATTERN.matcher(html);
while (m.find()) {
map.put(m.group(1), m.group(2));
}
for(Map.Entry e: map.entrySet()){
html = html.replace(e.getKey(), openWindowMethod(e.getKey(), e.getValue()));
}
return html;
}
private static String openWindowMethod(String url, String name){
return new StringBuilder().append("#\" onclick=\"").append(OPEN_BROWSER_WINDOW).append("(").append("\u0027") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
.append(url).append("\u0027").append(',').append("\u0027") //$NON-NLS-1$ //$NON-NLS-2$
.append(name).append("\u0027").append(");return false;").toString(); //$NON-NLS-1$//$NON-NLS-2$
}
public ResultReportUi(boolean isSinglePage) {
this.isSinglePage = isSinglePage;
}
public List getHtml(IPageContainer editor) {
List overviewHtml = new ArrayList<>(1);
String adjustedHtml = adjustAnchorFollowAction(RulesHtmlToolkit.generateStructuredHtml(new PageContainerResultProvider(editor), descriptors,
resultExpandedStates, true));
overviewHtml.add(adjustedHtml);
return overviewHtml;
}
public void setShowOk(boolean showOk) {
this.showOk = showOk;
if (!isSinglePage) {
try {
// FIXME: Avoid implicit dependency on HTML/javascript template. Generate script in RulesHtmlToolkit instead
browser.evaluate(String.format("overview.showOk(%b);", showOk)); //$NON-NLS-1$
boolean allOk = editor.getRuleManager().getScoreStream(topics.toArray(new String[topics.size()]))
.allMatch(d -> d > RulesHtmlToolkit.IN_PROGRESS && d < Severity.INFO.getLimit()) && !showOk;
browser.evaluate(String.format("overview.allOk(%b);", allOk)); //$NON-NLS-1$
} catch (SWTException swte) {
String html = RulesHtmlToolkit.generateStructuredHtml(new PageContainerResultProvider(editor),
descriptors, resultExpandedStates, false);
String adjustedHtml = adjustAnchorFollowAction(html);
browser.setText(adjustedHtml);
}
}
}
boolean getShowOk() {
return showOk;
}
private ConcurrentLinkedQueue resultEventQueue = new ConcurrentLinkedQueue<>();
private Collection topics = RulesToolkit.getAllTopics();
private Collection results;
public void updateRule(IRule rule) {
// FIXME: Possible race condition where elements may be added to the queue without being consumed
if (resultEventQueue.isEmpty()) {
resultEventQueue.add(rule);
DisplayToolkit.safeAsyncExec(() -> {
if (browser.isDisposed()) {
return;
}
// FIXME: Avoid implicit dependency on HTML/javascript template. Generate script in RulesHtmlToolkit instead
StringBuilder script = new StringBuilder();
while (!resultEventQueue.isEmpty()) {
IRule next = resultEventQueue.poll();
Result result = editor.getRuleManager().getResult(next);
if (result == null) {
continue;
}
long score = Math.round(result.getScore());
String adjustedHtml = adjustAnchorFollowAction(RulesHtmlToolkit.getDescription(result));
String quoteEscape = adjustedHtml.replaceAll("\\\"", "\\\\\""); //$NON-NLS-1$ //$NON-NLS-2$
String description = quoteEscape.replaceAll("\n", ""); //$NON-NLS-1$ //$NON-NLS-2$
script.append(String.format("overview.updateResult(\"%s\", %d, \"%s\");", //$NON-NLS-1$
result.getRule().getId(), score, description));
}
String[] topicsArray = topics.toArray(new String[topics.size()]);
if (!isSinglePage) {
boolean allOk = editor.getRuleManager().getScoreStream(topicsArray)
.allMatch(d -> d > RulesHtmlToolkit.IN_PROGRESS && d < Severity.INFO.getLimit()) && !showOk;
script.append(String.format("overview.allOk(%b);", allOk)); //$NON-NLS-1$
}
boolean allIgnored = editor.getRuleManager().getScoreStream(topicsArray)
.allMatch(d -> d == Result.IGNORE);
script.append(String.format("overview.allIgnored(%b);", allIgnored)); //$NON-NLS-1$
try {
if (isLoaded) {
browser.evaluate(script.toString());
browser.evaluate(OVERVIEW_UPDATE_PAGE_HEADERS_VISIBILITY);
} else {
final String finalScript = script.toString();
browser.addProgressListener(new ProgressAdapter() {
@Override
public void completed(ProgressEvent event) {
browser.evaluate(finalScript);
isLoaded = true;
browser.evaluate(OVERVIEW_UPDATE_PAGE_HEADERS_VISIBILITY);
browser.removeProgressListener(this);
}
});
}
} catch (IllegalArgumentException | SWTException e) {
try {
FlightRecorderUI.getDefault().getLogger().log(Level.INFO,
"Could not update single result, redrawing html view. " + e.getMessage()); //$NON-NLS-1$
String html = isSinglePage ? RulesHtmlToolkit.generateSinglePageHtml(results)
: RulesHtmlToolkit.generateStructuredHtml(new PageContainerResultProvider(editor),
descriptors, resultExpandedStates, false);
String adjustedHtml = adjustAnchorFollowAction(html);
browser.setText(adjustedHtml);
} catch (Exception e1) {
FlightRecorderUI.getDefault().getLogger().log(Level.WARNING, "Could not update Result Overview", //$NON-NLS-1$
e1);
}
}
});
} else {
resultEventQueue.add(rule);
}
}
public void setResults(Collection results) {
this.results = results;
}
public boolean createHtmlOverview(Browser browser, IPageContainer editor, IState state) {
this.browser = browser;
this.editor = editor;
descriptors = PageDescriptorResultGroup.build(FlightRecorderUI.getDefault().getPageManager().getRootPages());
try {
this.showOk = Boolean.valueOf(state.getChild("report").getChild("showOk").getAttribute("value")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
} catch (NullPointerException npe) {
}
browser.addListener(SWT.MenuDetect, new Listener() {
@Override
public void handleEvent(Event event) {
event.doit = false;
}
});
try {
String html = isSinglePage ? RulesHtmlToolkit.generateSinglePageHtml(results)
: RulesHtmlToolkit.generateStructuredHtml(new PageContainerResultProvider(editor), descriptors,
resultExpandedStates, false);
String adjustedHtml = adjustAnchorFollowAction(html);
browser.setText(adjustedHtml, true);
browser.setJavascriptEnabled(true);
browser.addProgressListener(new ProgressAdapter() {
@Override
public void completed(ProgressEvent event) {
new OpenWindowFunction(browser, OPEN_BROWSER_WINDOW);
new Linker(browser, "linker", descriptors, editor); //$NON-NLS-1$
new Expander(browser, "expander"); //$NON-NLS-1$
browser.execute(String.format("overview.showOk(%b);", showOk)); //$NON-NLS-1$
if (isSinglePage) {
browser.execute(OVERVIEW_MAKE_SCALABLE);
}
browser.evaluate(OVERVIEW_UPDATE_PAGE_HEADERS_VISIBILITY);
isLoaded = true;
}
});
} catch (IOException | IllegalArgumentException e) {
FlightRecorderUI.getDefault().getLogger().log(Level.WARNING, "Could not create Report Overview", e); //$NON-NLS-1$
return false;
}
return true;
}
public void saveTo(IWritableState state) {
state.createChild("report").createChild("showOk").putString("value", showOk.toString()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}