1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * 
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * The contents of this file are subject to the terms of either the Universal Permissive License
   7  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   8  *
   9  * or the following license:
  10  *
  11  * Redistribution and use in source and binary forms, with or without modification, are permitted
  12  * provided that the following conditions are met:
  13  * 
  14  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  15  * and the following disclaimer.
  16  * 
  17  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  18  * conditions and the following disclaimer in the documentation and/or other materials provided with
  19  * the distribution.
  20  * 
  21  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  22  * endorse or promote products derived from this software without specific prior written permission.
  23  * 
  24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  25  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  26  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  27  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  30  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  31  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  32  */
  33 package org.openjdk.jmc.flightrecorder.ui.overview;
  34 
  35 import java.io.ByteArrayOutputStream;
  36 import java.io.IOException;
  37 import java.util.ArrayList;
  38 import java.util.Base64;
  39 import java.util.Collection;
  40 import java.util.HashMap;
  41 import java.util.List;
  42 import java.util.Map;
  43 import java.util.concurrent.ConcurrentLinkedQueue;
  44 import java.util.logging.Level;
  45 import java.util.regex.Matcher;
  46 import java.util.regex.Pattern;
  47 import java.util.stream.Collectors;
  48 import java.util.stream.Stream;
  49 
  50 import org.eclipse.jface.resource.ImageDescriptor;
  51 import org.eclipse.swt.SWT;
  52 import org.eclipse.swt.SWTException;
  53 import org.eclipse.swt.browser.Browser;
  54 import org.eclipse.swt.browser.BrowserFunction;
  55 import org.eclipse.swt.browser.CloseWindowListener;
  56 import org.eclipse.swt.browser.OpenWindowListener;
  57 import org.eclipse.swt.browser.ProgressAdapter;
  58 import org.eclipse.swt.browser.ProgressEvent;
  59 import org.eclipse.swt.browser.WindowEvent;
  60 import org.eclipse.swt.graphics.ImageData;
  61 import org.eclipse.swt.graphics.ImageLoader;
  62 import org.eclipse.swt.layout.FillLayout;
  63 import org.eclipse.swt.widgets.Display;
  64 import org.eclipse.swt.widgets.Event;
  65 import org.eclipse.swt.widgets.Listener;
  66 import org.eclipse.swt.widgets.Shell;
  67 import org.openjdk.jmc.common.IState;
  68 import org.openjdk.jmc.common.IWritableState;
  69 import org.openjdk.jmc.flightrecorder.rules.IRule;
  70 import org.openjdk.jmc.flightrecorder.rules.Result;
  71 import org.openjdk.jmc.flightrecorder.rules.Severity;
  72 import org.openjdk.jmc.flightrecorder.rules.report.html.internal.HtmlResultGroup;
  73 import org.openjdk.jmc.flightrecorder.rules.report.html.internal.HtmlResultProvider;
  74 import org.openjdk.jmc.flightrecorder.rules.report.html.internal.RulesHtmlToolkit;
  75 import org.openjdk.jmc.flightrecorder.rules.util.RulesToolkit;
  76 import org.openjdk.jmc.flightrecorder.ui.DataPageDescriptor;
  77 import org.openjdk.jmc.flightrecorder.ui.FlightRecorderUI;
  78 import org.openjdk.jmc.flightrecorder.ui.IPageContainer;
  79 import org.openjdk.jmc.ui.misc.DisplayToolkit;
  80 
  81 /**
  82  * This class handles creation of a HTML/JS based result report, it has two modes, Single Page and
  83  * Full Report. Single Page is used by the ResultPage PageBookView and the Full Report by the Result
  84  * Overview page.
  85  */
  86 public class ResultReportUi {
  87 
  88         private static final String OVERVIEW_MAKE_SCALABLE = "overview.makeScalable();"; //$NON-NLS-1$
  89         private static final String OVERVIEW_UPDATE_PAGE_HEADERS_VISIBILITY = "overview.updatePageHeadersVisibility();"; //$NON-NLS-1$
  90         private static final Pattern HTML_ANCHOR_PATTERN = Pattern.compile("<a href=\"(.*?)\">(.*?)</a>");
  91         private static final String OPEN_BROWSER_WINDOW = "openWindowByUrl";
  92 
  93         private static class Linker extends BrowserFunction {
  94 
  95                 private Iterable<HtmlResultGroup> resultGroups;
  96                 private IPageContainer editor;
  97 
  98                 public Linker(Browser browser, String name, Iterable<HtmlResultGroup> resultGroups, IPageContainer editor) {
  99                         super(browser, name);
 100                         this.resultGroups = resultGroups;
 101                         this.editor = editor;
 102                 }
 103 
 104                 @Override
 105                 public Object function(Object[] arguments) {
 106                         if (arguments.length != 1 && !(arguments[0] instanceof String)) {
 107                                 return null;
 108                         }
 109                         String id = arguments[0].toString();
 110                         for (HtmlResultGroup group : resultGroups) {
 111                                 if (group instanceof PageDescriptorResultGroup && id.equals(group.getId())) {
 112                                         editor.navigateTo(((PageDescriptorResultGroup) group).getDescriptor());
 113                                         return null;
 114                                 } else {
 115                                         if (hasPageAsChild(group, id)) {
 116                                                 return null;
 117                                         }
 118                                 }
 119                         }
 120                         return null;
 121                 }
 122 
 123                 private boolean hasPageAsChild(HtmlResultGroup parent, String id) {
 124                         if (parent instanceof PageDescriptorResultGroup && id.equals(parent.getId())) {
 125                                 editor.navigateTo(((PageDescriptorResultGroup) parent).getDescriptor());
 126                                 return true;
 127                         }
 128                         if (!parent.hasChildren()) {
 129                                 return false;
 130                         } else {
 131                                 for (HtmlResultGroup child : parent.getChildren()) {
 132                                         if (hasPageAsChild(child, id)) {
 133                                                 return true;
 134                                         }
 135                                 }
 136                         }
 137                         return false;
 138                 }
 139 
 140         }
 141 
 142         private class Expander extends BrowserFunction {
 143 
 144                 public Expander(Browser browser, String name) {
 145                         super(browser, name);
 146                 }
 147 
 148                 @Override
 149                 public Object function(Object[] arguments) {
 150                         resultExpandedStates.put(arguments[0].toString(), (Boolean) arguments[1]);
 151                         return null;
 152                 }
 153 
 154         }
 155 
 156         public class OpenWindowFunction extends BrowserFunction {
 157 
 158                 public OpenWindowFunction (final Browser browser, final String name) {
 159                     super(browser, name);
 160                 }
 161                 public Object function (Object[] arguments) {
 162                         final String url = String.valueOf(arguments[0]);
 163                     final String title = String.valueOf(arguments[1]);
 164                     openBrowserByUrl(url, title);
 165                     return null;
 166                 }
 167         }
 168 
 169         private static class PageContainerResultProvider implements HtmlResultProvider {
 170                 private IPageContainer editor;
 171 
 172                 public PageContainerResultProvider(IPageContainer editor) {
 173                         this.editor = editor;
 174                 }
 175 
 176                 @Override
 177                 public Collection<Result> getResults(Collection<String> topics) {
 178                         return editor.getRuleManager().getResults(topics);
 179                 }
 180         }
 181 
 182         private static class PageDescriptorResultGroup implements HtmlResultGroup {
 183                 private DataPageDescriptor descriptor;
 184                 private List<HtmlResultGroup> children;
 185 
 186                 public PageDescriptorResultGroup(DataPageDescriptor descriptor) {
 187                         this.descriptor = descriptor;
 188                         children = new ArrayList<>();
 189                         for (DataPageDescriptor dpdChild : descriptor.getChildren()) {
 190                                 children.add(new PageDescriptorResultGroup(dpdChild));
 191                         }
 192                 }
 193 
 194                 @Override
 195                 public List<HtmlResultGroup> getChildren() {
 196                         return children;
 197                 }
 198 
 199                 @Override
 200                 public boolean hasChildren() {
 201                         return !children.isEmpty();
 202                 }
 203 
 204                 @Override
 205                 public Collection<String> getTopics() {
 206                         return Stream.of(descriptor.getTopics()).collect(Collectors.toList());
 207                 }
 208 
 209                 @Override
 210                 public String getId() {
 211                         return Integer.toString(descriptor.hashCode());
 212                 }
 213 
 214                 @Override
 215                 public String getName() {
 216                         return descriptor.getName();
 217                 }
 218 
 219                 @Override
 220                 public String getImage() {
 221                         ImageDescriptor image = descriptor.getImageDescriptor();
 222                         if (image == null) {
 223                                 return null;
 224                         }
 225                         ImageLoader loader = new ImageLoader();
 226                         ByteArrayOutputStream out = new ByteArrayOutputStream();
 227                         loader.data = new ImageData[] {image.getImageData()};
 228                         loader.save(out, SWT.IMAGE_PNG);
 229                         return Base64.getEncoder().encodeToString(out.toByteArray());
 230                 }
 231 
 232                 public DataPageDescriptor getDescriptor() {
 233                         return descriptor;
 234                 }
 235 
 236                 public static Collection<HtmlResultGroup> build(Collection<DataPageDescriptor> descriptors) {
 237                         return descriptors.stream().map(dpd -> new PageDescriptorResultGroup(dpd)).collect(Collectors.toList());
 238                 }
 239         }
 240 
 241         private final HashMap<String, Boolean> resultExpandedStates = new HashMap<>();
 242         private Boolean showOk = false;
 243         private Boolean isLoaded = false;
 244 
 245         private Browser browser;
 246         private IPageContainer editor;
 247         private Collection<HtmlResultGroup> descriptors;
 248         private boolean isSinglePage = false;
 249 
 250         private void openBrowserByUrl(final String url, final String title) {
 251                 final Display display = Display.getDefault();
 252                 final Shell shell = new Shell(display);
 253                 shell.setText(title);
 254                 shell.setLayout(new FillLayout());
 255                 final Browser browser = new Browser(shell, SWT.NONE);
 256                 initializeBrowser(display, browser, shell);
 257                 shell.open();
 258                 browser.setUrl(url);
 259         }
 260 
 261         private void initializeBrowser(final Display display, final Browser browser, final Shell shell) {
 262                 browser.addOpenWindowListener(new OpenWindowListener() {
 263                         public void open(WindowEvent event) {
 264                                   initializeBrowser(display, browser, shell);
 265                                   event.browser = browser;
 266                             }
 267                 });
 268                 browser.addCloseWindowListener(new CloseWindowListener() {
 269                           public void close(WindowEvent event) {
 270                                   Browser browser = (Browser)event.widget;
 271                               Shell shell = browser.getShell();
 272                               shell.close();
 273                           }
 274             });
 275         }
 276 
 277         /*
 278      * We replace the anchors in the HTML when running in the JMC UI to make
 279      * it possible to follow them. See JMC-5419 for more information.
 280      */
 281         private static String adjustAnchorFollowAction(String html) {
 282                 Map<String, String> map = new HashMap<>();
 283                 Matcher m = HTML_ANCHOR_PATTERN.matcher(html);
 284                 while (m.find()) {
 285                         map.put(m.group(1), m.group(2));
 286                 }
 287                 for(Map.Entry<String, String> e: map.entrySet()){
 288                         html = html.replace(e.getKey(), openWindowMethod(e.getKey(), e.getValue()));
 289                 }
 290                 return html;
 291         }
 292 
 293         private static String openWindowMethod(String url, String name){
 294         return new StringBuilder().append("#\" onclick=\"").append(OPEN_BROWSER_WINDOW).append("(").append("\u0027")
 295                 .append(url).append("\u0027").append(',').append("\u0027")
 296                 .append(name).append("\u0027").append(");return false;").toString();
 297     }
 298 
 299         public ResultReportUi(boolean isSinglePage) {
 300                 this.isSinglePage = isSinglePage;
 301         }
 302 
 303         public List<String> getHtml(IPageContainer editor) {
 304                 List<String> overviewHtml = new ArrayList<>(1);
 305                 String adjustedHtml = adjustAnchorFollowAction(RulesHtmlToolkit.generateStructuredHtml(new PageContainerResultProvider(editor), descriptors,
 306                                 resultExpandedStates, true));
 307                 overviewHtml.add(adjustedHtml);
 308                 return overviewHtml;
 309         }
 310 
 311         public void setShowOk(boolean showOk) {
 312                 this.showOk = showOk;
 313                 if (!isSinglePage) {
 314                         try {
 315                                 // FIXME: Avoid implicit dependency on HTML/javascript template. Generate script in RulesHtmlToolkit instead
 316                                 browser.evaluate(String.format("overview.showOk(%b);", showOk)); //$NON-NLS-1$
 317                                 boolean allOk = editor.getRuleManager().getScoreStream(topics.toArray(new String[topics.size()]))
 318                                                 .allMatch(d -> d > RulesHtmlToolkit.IN_PROGRESS && d < Severity.INFO.getLimit()) && !showOk;
 319                                 browser.evaluate(String.format("overview.allOk(%b);", allOk)); //$NON-NLS-1$
 320                         } catch (SWTException swte) {
 321                                 String html = RulesHtmlToolkit.generateStructuredHtml(new PageContainerResultProvider(editor),
 322                                                 descriptors, resultExpandedStates, false);
 323                                 String adjustedHtml = adjustAnchorFollowAction(html);
 324                                 browser.setText(adjustedHtml);
 325                         }
 326                 }
 327         }
 328 
 329         boolean getShowOk() {
 330                 return showOk;
 331         }
 332 
 333         private ConcurrentLinkedQueue<IRule> resultEventQueue = new ConcurrentLinkedQueue<>();
 334         private Collection<String> topics = RulesToolkit.getAllTopics();
 335         private Collection<Result> results;
 336 
 337         public void updateRule(IRule rule) {
 338                 // FIXME: Possible race condition where elements may be added to the queue without being consumed
 339                 if (resultEventQueue.isEmpty()) {
 340                         resultEventQueue.add(rule);
 341                         DisplayToolkit.safeAsyncExec(() -> {
 342                                 if (browser.isDisposed()) {
 343                                         return;
 344                                 }
 345                                 // FIXME: Avoid implicit dependency on HTML/javascript template. Generate script in RulesHtmlToolkit instead
 346                                 StringBuilder script = new StringBuilder();
 347                                 while (!resultEventQueue.isEmpty()) {
 348                                         IRule next = resultEventQueue.poll();
 349                                         Result result = editor.getRuleManager().getResult(next);
 350                                         if (result == null) {
 351                                                 continue;
 352                                         }
 353                                         long score = Math.round(result.getScore());
 354                                         String adjustedHtml = adjustAnchorFollowAction(RulesHtmlToolkit.getDescription(result));//$NON-NLS-1$ //$NON-NLS-2$
 355                                         String quoteEscape = adjustedHtml.replaceAll("\\\"", "\\\\\""); //$NON-NLS-1$ //$NON-NLS-2$
 356                                         String description = quoteEscape.replaceAll("\n", "</br>"); //$NON-NLS-1$ //$NON-NLS-2$
 357                                         script.append(String.format("overview.updateResult(\"%s\", %d, \"%s\");", //$NON-NLS-1$
 358                                                         result.getRule().getId(), score, description));
 359                                 }
 360                                 String[] topicsArray = topics.toArray(new String[topics.size()]);
 361                                 if (!isSinglePage) {
 362                                         boolean allOk = editor.getRuleManager().getScoreStream(topicsArray)
 363                                                         .allMatch(d -> d > RulesHtmlToolkit.IN_PROGRESS && d < Severity.INFO.getLimit()) && !showOk;
 364                                         script.append(String.format("overview.allOk(%b);", allOk)); //$NON-NLS-1$
 365                                 }
 366                                 boolean allIgnored = editor.getRuleManager().getScoreStream(topicsArray)
 367                                                 .allMatch(d -> d == Result.IGNORE);
 368                                 script.append(String.format("overview.allIgnored(%b);", allIgnored)); //$NON-NLS-1$
 369                                 try {
 370                                         if (isLoaded) {
 371                                                 browser.evaluate(script.toString());
 372                                                 browser.evaluate(OVERVIEW_UPDATE_PAGE_HEADERS_VISIBILITY);
 373                                         } else {
 374                                                 final String finalScript = script.toString();
 375                                                 browser.addProgressListener(new ProgressAdapter() {
 376                                                         @Override
 377                                                         public void completed(ProgressEvent event) {
 378                                                                 browser.evaluate(finalScript);
 379                                                                 isLoaded = true;
 380                                                                 browser.evaluate(OVERVIEW_UPDATE_PAGE_HEADERS_VISIBILITY);
 381                                                                 browser.removeProgressListener(this);
 382                                                         }
 383                                                 });
 384                                         }
 385                                 } catch (IllegalArgumentException | SWTException e) {
 386                                         try {
 387                                                 FlightRecorderUI.getDefault().getLogger().log(Level.INFO,
 388                                                                 "Could not update single result, redrawing html view. " + e.getMessage()); //$NON-NLS-1$
 389                                                 String html = isSinglePage ? RulesHtmlToolkit.generateSinglePageHtml(results)
 390                                                                 : RulesHtmlToolkit.generateStructuredHtml(new PageContainerResultProvider(editor),
 391                                                                                 descriptors, resultExpandedStates, false);
 392                                                 String adjustedHtml = adjustAnchorFollowAction(html);
 393                                                 browser.setText(adjustedHtml);
 394                                         } catch (Exception e1) {
 395                                                 FlightRecorderUI.getDefault().getLogger().log(Level.WARNING, "Could not update Result Overview", //$NON-NLS-1$
 396                                                                 e1);
 397                                         }
 398                                 }
 399                         });
 400                 } else {
 401                         resultEventQueue.add(rule);
 402                 }
 403         }
 404 
 405         public void setResults(Collection<Result> results) {
 406                 this.results = results;
 407         }
 408 
 409         public boolean createHtmlOverview(Browser browser, IPageContainer editor, IState state) {
 410                 this.browser = browser;
 411                 this.editor = editor;
 412                 descriptors = PageDescriptorResultGroup.build(FlightRecorderUI.getDefault().getPageManager().getRootPages());
 413                 try {
 414                         this.showOk = Boolean.valueOf(state.getChild("report").getChild("showOk").getAttribute("value")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 415                 } catch (NullPointerException npe) {
 416                 }
 417                 browser.addListener(SWT.MenuDetect, new Listener() {
 418                         @Override
 419                         public void handleEvent(Event event) {
 420                                 event.doit = false;
 421                         }
 422                 });
 423                 try {
 424                         String html = isSinglePage ? RulesHtmlToolkit.generateSinglePageHtml(results)
 425                                         : RulesHtmlToolkit.generateStructuredHtml(new PageContainerResultProvider(editor), descriptors,
 426                                                         resultExpandedStates, false);
 427                         String adjustedHtml = adjustAnchorFollowAction(html);
 428                         browser.setText(adjustedHtml, true);
 429                         browser.setJavascriptEnabled(true);
 430                         browser.addProgressListener(new ProgressAdapter() {
 431                                 @Override
 432                                 public void completed(ProgressEvent event) {
 433                                         new OpenWindowFunction(browser, OPEN_BROWSER_WINDOW); //$NON-NLS-1$
 434                                         new Linker(browser, "linker", descriptors, editor); //$NON-NLS-1$
 435                                         new Expander(browser, "expander"); //$NON-NLS-1$
 436                                         browser.execute(String.format("overview.showOk(%b);", showOk)); //$NON-NLS-1$
 437                                         if (isSinglePage) {
 438                                                 browser.execute(OVERVIEW_MAKE_SCALABLE);
 439                                         }
 440                                         browser.evaluate(OVERVIEW_UPDATE_PAGE_HEADERS_VISIBILITY);
 441                                         isLoaded = true;
 442                                 }
 443                         });
 444                 } catch (IOException | IllegalArgumentException e) {
 445                         FlightRecorderUI.getDefault().getLogger().log(Level.WARNING, "Could not create Report Overview", e); //$NON-NLS-1$
 446                         return false;
 447                 }
 448                 return true;
 449         }
 450 
 451         public void saveTo(IWritableState state) {
 452                 state.createChild("report").createChild("showOk").putString("value", showOk.toString()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 453         }
 454 
 455 }