blob: e7796ed9c173632b1dc20ec70830ed557eab7ea3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2021 SAP AG and IBM Corporation.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* SAP AG - initial API and implementation
* IBM Corporation - paths for icon localization
*******************************************************************************/
package org.eclipse.mat.report.internal;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.mat.query.IQueryContext;
import org.eclipse.mat.query.IResult;
import org.eclipse.mat.report.IOutputter;
import org.eclipse.mat.report.Params;
import org.eclipse.mat.report.RendererRegistry;
import org.eclipse.mat.report.TestSuite;
import org.eclipse.mat.util.FileUtils;
import org.eclipse.mat.util.HTMLUtils;
import org.eclipse.mat.util.MessageUtil;
public class ResultRenderer
{
/* package */static final String DIR_PAGES = "pages"; //$NON-NLS-1$
/* package */static final String DIR_ICONS = "icons"; //$NON-NLS-1$
interface Key
{
String IS_EXPANDABLE = "isExpandable"; //$NON-NLS-1$
String ARTEFACT = "artefact"; //$NON-NLS-1$
}
/* package */class HtmlArtefact
{
private File file;
private PrintWriter writer;
private String pathToRoot;
private String relativeURL;
private HtmlArtefact(AbstractPart part, File directory, String relativeURL, String title) throws IOException
{
this.file = new File(directory, relativeURL.replace('/', File.separatorChar));
// Should the encoding be hard-coded to UTF-8?
String encoding = System.getProperty("file.encoding"); //$NON-NLS-1$
this.writer = new PrintWriter(file, encoding);
this.pathToRoot = ""; //$NON-NLS-1$
for (int ii = 0; ii < relativeURL.length(); ii++)
if (relativeURL.charAt(ii) == '/')
pathToRoot += "../"; //$NON-NLS-1$
this.relativeURL = relativeURL;
artefacts.add(this);
PageSnippets.beginPage(part, this, title, encoding);
}
public HtmlArtefact append(String s)
{
writer.append(s);
return this;
}
public void close()
{
try
{
PageSnippets.endPage(this);
}
finally
{
writer.flush();
writer.close();
writer = null;
}
}
@Override
public String toString()
{
return file.getAbsolutePath();
}
public File getFile()
{
return file;
}
public String getPathToRoot()
{
return pathToRoot;
}
public String getRelativePathName()
{
return relativeURL;
}
}
private TestSuite suite;
private IOutputter html;
private List<HtmlArtefact> artefacts = new ArrayList<HtmlArtefact>();
private File directory;
private Map<URI, String> icon2name = new HashMap<URI, String>();
public ResultRenderer()
{
html = RendererRegistry.instance().match("html", IResult.class); //$NON-NLS-1$
}
public TestSuite getSuite()
{
return suite;
}
public void beginSuite(TestSuite suite, AbstractPart part) throws IOException
{
this.suite = suite;
prepareTempDirectory();
HtmlArtefact index = new HtmlArtefact(part, directory, "index.html", part.spec().getName()); //$NON-NLS-1$
suite.addResult(index.getFile());
part.putObject(Key.ARTEFACT, index);
}
public void endSuite(AbstractPart part) throws IOException
{
renderTableOfContents(part);
for (HtmlArtefact artefact : artefacts)
artefact.close();
copyIcons();
zipResult();
}
private void copyIcons() throws IOException
{
if (!icon2name.isEmpty())
{
File iconDir = new File(directory, DIR_ICONS);
if (!iconDir.mkdir())
ReportPlugin.log(IStatus.WARNING, MessageUtil.format(Messages.FileUtils_FailedToMakeDirectory, iconDir));
for (Map.Entry<URI, String> entry : icon2name.entrySet())
copyResource(entry.getKey().toURL(), new File(iconDir, entry.getValue()));
}
}
public void beginSection(SectionPart section) throws IOException
{
int order = 1;
AbstractPart p = section;
while (p.getParent() != null)
{
p = p.getParent();
order++;
}
HtmlArtefact srcArtefact = (HtmlArtefact) section.getObject(Key.ARTEFACT);
if (srcArtefact == null)
srcArtefact = (HtmlArtefact) section.getParent().getObject(Key.ARTEFACT);
HtmlArtefact artefact = createNewFileIfNecessary(srcArtefact, section, order);
// do not create expansion if
// (a) it is the top-level element
// (b) it is a new file (e.g. the top-level element for a sub-page)
if (order == 1 || srcArtefact != artefact)
{
PageSnippets.heading(artefact, section, order, false, true);
}
else
{
PageSnippets.heading(artefact, section, order, true, false);
PageSnippets.beginExpandableDiv(artefact, section, false);
section.putObject(Key.IS_EXPANDABLE, true);
}
}
public void endSection(SectionPart section)
{
if (section.getObject(Key.IS_EXPANDABLE) != null)
{
HtmlArtefact artefact = (HtmlArtefact) section.getObject(Key.ARTEFACT);
PageSnippets.endDiv(artefact);
}
}
public void process(QueryPart test, IResult result, RenderingInfo rInfo) throws IOException
{
// determine output formatter
String format = test.params().get(Params.FORMAT, "html"); //$NON-NLS-1$
IOutputter outputter = html;
if (result != null)
{
outputter = RendererRegistry.instance().match(format, result.getClass());
if (outputter == null)
{
ReportPlugin.log(IStatus.WARNING, MessageUtil.format(Messages.ResultRenderer_Error_OutputterNotFound,
format, result.getClass().getName()));
outputter = html;
}
}
// Also handle the case where the proper handler can't be found, so continue with html
if ("html".equals(format) || outputter.equals(html)) //$NON-NLS-1$
doProcess(outputter, test, result, rInfo, true);
else
doProcessAlien(format, outputter, test, result, rInfo);
}
public void processLink(LinkedPart linkedPart)
{
HtmlArtefact srcArtefact = (HtmlArtefact) linkedPart.getObject(Key.ARTEFACT);
if (srcArtefact == null)
srcArtefact = (HtmlArtefact) linkedPart.getParent().getObject(Key.ARTEFACT);
String src = srcArtefact.getPathToRoot() + linkedPart.linkedTo.getDataFile().getUrl();
srcArtefact.append("<a href=\"").append(src).append("\">") // //$NON-NLS-1$ //$NON-NLS-2$
.append(linkedPart.spec().getName()).append("</a>"); //$NON-NLS-1$
}
private void doProcessAlien(String format, IOutputter outputter, QueryPart test, IResult result, RenderingInfo info)
throws IOException
{
HtmlArtefact artefact = (HtmlArtefact) test.getObject(Key.ARTEFACT);
if (artefact == null)
artefact = (HtmlArtefact) test.getParent().getObject(Key.ARTEFACT);
String filename = test.getDataFile().getSuggestedFile();
if (filename == null)
filename = test.params().shallow().get(Params.FILENAME);
if (filename == null)
filename = DIR_PAGES + File.separator + FileUtils.toFilename(test.spec().getName(), test.getId(), format);
test.getDataFile().setUrl(filename);
PageSnippets.linkedHeading(artefact, test, 5, filename);
Writer writer = new FileWriter(new File(this.directory, filename));
try
{
outputter.process(info, result, writer);
}
finally
{
writer.close();
}
}
private void doProcess(IOutputter outputter, QueryPart test, IResult result, RenderingInfo rInfo, boolean firstPass)
throws IOException
{
HtmlArtefact srcArtefact = (HtmlArtefact) test.getObject(Key.ARTEFACT);
if (srcArtefact == null)
srcArtefact = (HtmlArtefact) test.getParent().getObject(Key.ARTEFACT);
HtmlArtefact artefact = createNewFileIfNecessary(srcArtefact, test, 5);
String pattern = test.params().shallow().get(Params.Rendering.PATTERN);
boolean isOverviewDetailsPattern = firstPass && Params.Rendering.PATTERN_OVERVIEW_DETAILS.equals(pattern);
if (!isOverviewDetailsPattern)
{
PageSnippets.queryHeading(artefact, test, srcArtefact != artefact);
PageSnippets.beginExpandableDiv(artefact, test, srcArtefact != artefact);
}
boolean isImportant = test.params().shallow().getBoolean(Params.Html.IS_IMPORTANT, false);
if (isImportant)
{
artefact.append("<div class=\"important\">"); //$NON-NLS-1$
}
outputter.embedd(rInfo, result, artefact.writer);
if (isOverviewDetailsPattern)
{
String filename = test.getDataFile().getSuggestedFile();
if (filename == null)
filename = DIR_PAGES + '/' + test.getId() + ".html"; //$NON-NLS-1$
artefact.append("<div>"); //$NON-NLS-1$
PageSnippets.link(artefact, filename, Messages.ResultRenderer_Label_Details);
artefact.append("</div>"); //$NON-NLS-1$
// create new page for the details elements
HtmlArtefact details = new HtmlArtefact(test.getParent(), //
directory, //
filename, //
test.getParent().spec().getName());
test.getDataFile().setUrl(details.getRelativePathName());
// assign output page to all other children
for (AbstractPart part : test.getParent().getChildren())
part.putObject(Key.ARTEFACT, details);
// process this child again (repeat on details page)
doProcess(outputter, test, result, rInfo, false);
}
if (isImportant)
artefact.append("</div>"); //$NON-NLS-1$
if (!isOverviewDetailsPattern)
{
PageSnippets.endDiv(artefact);
}
}
// //////////////////////////////////////////////////////////////
// context interface
// //////////////////////////////////////////////////////////////
/* package */String addIcon(URL icon, AbstractPart part)
{
if (icon == null)
return null;
// URLs are bad for maps as the host has to be resolved by the name server
URI iconKey;
try
{
iconKey = icon.toURI();
}
catch (URISyntaxException e)
{
// Safe enough for a bad icon
return null;
}
String name = icon2name.get(iconKey);
if (name == null)
{
String f = icon.getFile();
int p = f.lastIndexOf('.');
String extension = p < 0 ? f : f.substring(p);
icon2name.put(iconKey, name = "i" + icon2name.size() + extension); //$NON-NLS-1$
}
HtmlArtefact artefact = ((HtmlArtefact) part.getObject(Key.ARTEFACT));
return artefact.getPathToRoot() + DIR_ICONS + "/" + name; //$NON-NLS-1$
}
/* package */File getOutputDirectory(AbstractPart part)
{
HtmlArtefact artefact = (HtmlArtefact) part.getObject(Key.ARTEFACT);
return artefact == null ? directory : artefact.getFile().getParentFile();
}
/* package */String getPathToRoot(AbstractPart part)
{
HtmlArtefact artefact = (HtmlArtefact) part.getObject(Key.ARTEFACT);
return artefact == null ? "" : artefact.getPathToRoot(); //$NON-NLS-1$
}
/* package */IQueryContext getQueryContext()
{
return suite.getQueryContext();
}
// //////////////////////////////////////////////////////////////
// private parts
// //////////////////////////////////////////////////////////////
private static final String PREFIX = "$nl$/META-INF/html/"; //$NON-NLS-1$
@SuppressWarnings("nls")
private void prepareTempDirectory() throws IOException
{
directory = FileUtils.createTempDirectory("report", null);
copyResource(PREFIX + "styles.css", new File(directory, "styles.css"));
copyResource(PREFIX + "styles-dark.css", new File(directory, "styles-dark.css"));
copyResource(PREFIX + "code.js", new File(directory, "code.js"));
File imgDir = new File(directory, "img");
if (!imgDir.mkdir())
ReportPlugin.log(IStatus.WARNING, MessageUtil.format(Messages.FileUtils_FailedToMakeDirectory, imgDir));
copyResource(PREFIX + "img/open.gif", new File(imgDir, "open.gif"));
copyResource(PREFIX + "img/success.gif", new File(imgDir, "success.gif"));
copyResource(PREFIX + "img/warning.gif", new File(imgDir, "warning.gif"));
copyResource(PREFIX + "img/error.gif", new File(imgDir, "error.gif"));
copyResource(PREFIX + "img/empty.gif", new File(imgDir, "empty.gif"));
copyResource(PREFIX + "img/fork.gif", new File(imgDir, "fork.gif"));
copyResource(PREFIX + "img/line.gif", new File(imgDir, "line.gif"));
copyResource(PREFIX + "img/corner.gif", new File(imgDir, "corner.gif"));
copyResource(PREFIX + "img/opened.gif", new File(imgDir, "opened.gif"));
copyResource(PREFIX + "img/closed.gif", new File(imgDir, "closed.gif"));
copyResource(PREFIX + "img/nochildren.gif", new File(imgDir, "nochildren.gif"));
File pagesDir = new File(directory, DIR_PAGES);
if (!pagesDir.mkdir())
ReportPlugin.log(IStatus.WARNING, MessageUtil.format(Messages.FileUtils_FailedToMakeDirectory, pagesDir));
}
private void copyResource(String resource, File target) throws FileNotFoundException, IOException
{
IPath path = new Path(resource);
InputStream resourceStream = FileLocator.openStream(ReportPlugin.getDefault().getBundle(), path, true);
if (resourceStream == null)
throw new FileNotFoundException(resource);
try
{
OutputStream out = new FileOutputStream(target);
try
{
FileUtils.copy(resourceStream, out);
}
finally
{
out.close();
}
}
finally
{
resourceStream.close();
}
}
private void copyResource(URL resource, File target) throws FileNotFoundException, IOException
{
InputStream in = resource.openStream();
try
{
OutputStream out = new FileOutputStream(target);
try
{
FileUtils.copy(in, out);
}
finally
{
out.close();
}
}
finally
{
in.close();
}
}
private HtmlArtefact createNewFileIfNecessary(HtmlArtefact artefact, AbstractPart part, int order)
throws IOException
{
boolean isSeparateFile = part.params().shallow().getBoolean(Params.Html.SEPARATE_FILE, false);
boolean isEmbedded = part.params().shallow().getBoolean("$embedded", false); //$NON-NLS-1$
if (isSeparateFile || isEmbedded)
{
String filename = part.getDataFile().getSuggestedFile();
if (filename == null)
filename = part.params().shallow().get(Params.FILENAME);
if (filename == null)
filename = DIR_PAGES + '/' + FileUtils.toFilename(part.spec().getName(), part.getId(), "html"); //$NON-NLS-1$
part.getDataFile().setUrl(filename);
HtmlArtefact newArtefact = new HtmlArtefact(part, directory, filename, part.spec().getName());
if (!isEmbedded)
PageSnippets.linkedHeading(artefact, part, order, newArtefact.getRelativePathName());
artefact = newArtefact;
}
part.putObject(Key.ARTEFACT, artefact);
part.getDataFile().setUrl(artefact.getRelativePathName());
return artefact;
}
// //////////////////////////////////////////////////////////////
// render table of contents into HTML page
// //////////////////////////////////////////////////////////////
private void renderTableOfContents(AbstractPart part) throws IOException
{
HtmlArtefact toc = new HtmlArtefact(null, directory, "toc.html", Messages.ResultRenderer_Label_TableOfContents); //$NON-NLS-1$
toc.append("<h1>" + Messages.ResultRenderer_Label_TableOfContents + "</h1>\n"); //$NON-NLS-1$ //$NON-NLS-2$
renderResult(toc, part, 0);
}
@SuppressWarnings("nls")
private void renderResult(HtmlArtefact toc, AbstractPart parent, int depth)
{
toc.append("<ul class=\"collapsible_").append(depth < 3 ? "opened" : "closed").append("\">");
for (AbstractPart part : parent.getChildren())
{
toc.append("<li>");
if (part.getStatus() != null)
toc.append("<img src=\"img/").append(part.getStatus().name().toLowerCase(Locale.ENGLISH) + ".gif\" alt=\"").append(part.getStatus().toString()).append("\"> ");
HtmlArtefact page = (HtmlArtefact) part.getObject(Key.ARTEFACT);
AbstractPart p = part;
while (page == null)
page = (HtmlArtefact) (p = p.getParent()).getObject(Key.ARTEFACT);
PageSnippets.beginLink(toc, page.getRelativePathName() + "#" + part.getId());
toc.append(HTMLUtils.escapeText(part.spec().getName()));
PageSnippets.endLink(toc);
if (!part.children.isEmpty())
renderResult(toc, part, depth + 1);
toc.append("</li>");
}
toc.append("</ul>");
}
// //////////////////////////////////////////////////////////////
// zip directory
// //////////////////////////////////////////////////////////////
/**
* Do two zip files have the same content?
* Ignore dates.
* For speed just check sizes, CRC etc.
* @param existingZip
* @param targetZip
* @return true if they are the same
*/
boolean sameZipContents(File existingZip, File targetZip)
{
if (targetZip.length() == existingZip.length())
{
/*
* Compare the files. They are zips, but the file dates inside might differ.
*/
try (ZipInputStream bis = new ZipInputStream(new BufferedInputStream(new FileInputStream(existingZip)));
ZipInputStream bis2 = new ZipInputStream(new BufferedInputStream(new FileInputStream(targetZip))))
{
for (;;)
{
ZipEntry ze1 = bis.getNextEntry();
ZipEntry ze2 = bis2.getNextEntry();
if (ze1 == null)
return ze2 == null;
if (ze2 == null)
return false;
// Don't bother to check contents
if (ze1.isDirectory() != ze2.isDirectory())
return false;
if (!ze1.getName().equals(ze2.getName()))
return false;
if (ze1.getSize() != ze2.getSize())
return false;
if (ze1.getCrc() != ze2.getCrc())
return false;
}
}
catch (IOException e)
{
// If a problem reading, then they are not the same
}
return false;
}
return false;
}
/**
* Zip up a directory.
* Copes with an existing zip file which is not writable - e.g. on multi-user
* systems where another user has generated a report with different permissions.
* Find the first file name in the sequence which can be written to or deleted.
* Create a new file, remove if it has the same contents as the previous one.
* java_pid13384.0001_System_Overview.zip
* java_pid13384.0001_System_Overview_1.zip
* java_pid13384.0001_System_Overview_2.zip
* ..
* java_pid13384.0001_System_Overview_9.zip
* @throws IOException
*/
private void zipResult() throws IOException
{
File targetZip = suite.getOutput();
// See if we can open the requested file for output
final int MAX_RENAMES = 99;
File originalZip = targetZip;
File existingZip = null;
ZipOutputStream zos = null;
for (int n = 1; targetZip.exists() && n <= MAX_RENAMES; ++n)
{
// Perhaps we can overwrite
if (targetZip.canWrite() && !targetZip.isDirectory())
{
try
{
// Open for output
zos = new ZipOutputStream(new FileOutputStream(targetZip));
/*
* We can overwrite an existing file.
* Consider this:
* report.zip - can't write
* report_1.zip - can't write
* report_2.zip - exists, can overwrite
* At the end, should we delete the new report_2.zip
* if it is the same as report_1.zip ?
* This seems good, so keep existingZip.
*/
break;
}
catch (IOException e)
{
// Ignore as we will try a new name
}
}
/*
* Don't delete the existing file if we can't open it for write.
* It might be read-only, or a directory, and the user
* might have a reason for keeping it.
*/
String fn = originalZip.getName();
int dot = fn.lastIndexOf('.');
String fn2 = fn.substring(0, dot) + "_" + n + fn.substring(dot); //$NON-NLS-1$
/*
* Use the immediately preceding file for comparison - in case
* the same user generates the file twice.
*/
existingZip = targetZip;
targetZip = new File(originalZip.getParentFile(), fn2);
}
if (zos == null)
zos = new ZipOutputStream(new FileOutputStream(targetZip));
try
{
zipDir(directory.getPath().length() + 1, directory, zos);
}
finally
{
zos.close();
}
/**
* See if the new file is the same as the old one.
*/
if (existingZip != null && sameZipContents(existingZip, targetZip))
{
if (targetZip.delete())
targetZip = existingZip;
}
suite.addResult(targetZip);
}
private void zipDir(int commonPath, File zipDir, ZipOutputStream zos) throws IOException
{
String[] dirList = zipDir.list();
if (dirList == null)
return;
byte[] readBuffer = new byte[2156];
int bytesIn = 0;
for (int i = 0; i < dirList.length; i++)
{
File f = new File(zipDir, dirList[i]);
if (f.isDirectory())
{
zipDir(commonPath, f, zos);
}
else
{
FileInputStream fis = new FileInputStream(f);
try
{
String path = f.getPath().substring(commonPath);
ZipEntry anEntry = new ZipEntry(path);
zos.putNextEntry(anEntry);
while ((bytesIn = fis.read(readBuffer)) != -1)
{
zos.write(readBuffer, 0, bytesIn);
}
}
finally
{
fis.close();
}
}
}
}
}