blob: 5811fc2e6a8c467fae8f54fe3b426d784681e880 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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.
*/
package com.android.tools.lint;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* "Multiplexing" reporter which allows output to be split up into a separate
* report for each separate project. It also adds an overview index.
*/
public class MultiProjectHtmlReporter extends HtmlReporter {
private static final String INDEX_NAME = "index.html"; //$NON-NLS-1$
private final File mDir;
public MultiProjectHtmlReporter(LintCliClient client, File dir) throws IOException {
super(client, new File(dir, INDEX_NAME));
mDir = dir;
}
@Override
public void write(int errorCount, int warningCount, List<Warning> allIssues) throws IOException {
Map<Project, List<Warning>> projectToWarnings = new HashMap<Project, List<Warning>>();
for (Warning warning : allIssues) {
List<Warning> list = projectToWarnings.get(warning.project);
if (list == null) {
list = new ArrayList<Warning>();
projectToWarnings.put(warning.project, list);
}
list.add(warning);
}
// Set of unique file names: lowercase names to avoid case conflicts in web environment
Set<String> unique = Sets.newHashSet();
unique.add(INDEX_NAME.toLowerCase(Locale.US));
List<ProjectEntry> projects = Lists.newArrayList();
for (Project project : projectToWarnings.keySet()) {
// TODO: Can I get the project name from the Android manifest file instead?
String projectName = project.getName();
// Produce file names of the form Project.html, Project1.html, Project2.html, etc
int number = 1;
String fileName;
while (true) {
String numberString = number > 1 ? Integer.toString(number) : "";
fileName = String.format("%1$s%2$s.html", projectName, numberString); //$NON-NLS-1$
String lowercase = fileName.toLowerCase(Locale.US);
if (!unique.contains(lowercase)) {
unique.add(lowercase);
break;
}
number++;
}
File output = new File(mDir, fileName);
if (output.exists()) {
boolean deleted = output.delete();
if (!deleted) {
mClient.log(null, "Could not delete old file %1$s", output);
continue;
}
}
if (!output.getParentFile().canWrite()) {
mClient.log(null, "Cannot write output file %1$s", output);
continue;
}
HtmlReporter reporter = new HtmlReporter(mClient, output);
reporter.setBundleResources(mBundleResources);
reporter.setSimpleFormat(mSimpleFormat);
reporter.setUrlMap(mUrlMap);
List<Warning> issues = projectToWarnings.get(project);
int projectErrorCount = 0;
int projectWarningCount = 0;
for (Warning warning: issues) {
if (warning.severity == Severity.ERROR || warning.severity == Severity.FATAL) {
projectErrorCount++;
} else if (warning.severity == Severity.WARNING) {
projectWarningCount++;
}
}
String prefix = project.getReferenceDir().getPath();
String path = project.getDir().getPath();
String relative;
if (path.startsWith(prefix) && path.length() > prefix.length()) {
int i = prefix.length();
if (path.charAt(i) == File.separatorChar) {
i++;
}
relative = path.substring(i);
} else {
relative = projectName;
}
reporter.setTitle(String.format("Lint Report for %1$s", relative));
reporter.setStripPrefix(relative);
reporter.write(projectErrorCount, projectWarningCount, issues);
projects.add(new ProjectEntry(fileName, projectErrorCount, projectWarningCount,
relative));
}
// Write overview index?
writeOverview(errorCount, warningCount, projects);
Closeables.closeQuietly(mWriter);
File index = new File(mDir, INDEX_NAME);
System.out.println();
System.out.println(String.format("Wrote overview index to %1$s", index));
}
private void writeOverview(int errorCount, int warningCount, List<ProjectEntry> projects)
throws IOException {
mWriter.write(
"<html>\n" + //$NON-NLS-1$
"<head>\n" + //$NON-NLS-1$
"<title>" + mTitle + "</title>\n"); //$NON-NLS-1$//$NON-NLS-2$
writeStyleSheet();
mWriter.write(
"</head>\n" + //$NON-NLS-1$
"<body>\n" + //$NON-NLS-1$
"<h1>" + //$NON-NLS-1$
mTitle +
"<div class=\"titleSeparator\"></div>\n" + //$NON-NLS-1$
"</h1>"); //$NON-NLS-1$
// Sort project list in decreasing order of errors, warnings and names
Collections.sort(projects);
mWriter.write(String.format("Check performed at %1$s.",
new Date().toString()));
mWriter.write("<br/>"); //$NON-NLS-1$
mWriter.write(String.format("%1$d errors and %2$d warnings found:\n",
errorCount, warningCount));
mWriter.write("<br/><br/>"); //$NON-NLS-1$
if (errorCount == 0 && warningCount == 0) {
mWriter.write("Congratulations!");
return;
}
String errorUrl = null;
String warningUrl = null;
if (!mSimpleFormat) {
errorUrl = addLocalResources(HtmlReporter.getErrorIconUrl());
warningUrl = addLocalResources(HtmlReporter.getWarningIconUrl());
}
mWriter.write("<table class=\"overview\">\n"); //$NON-NLS-1$
mWriter.write("<tr><th>"); //$NON-NLS-1$
mWriter.write("Project");
mWriter.write("</th><th class=\"countColumn\">"); //$NON-NLS-1$
if (errorUrl != null) {
mWriter.write("<img border=\"0\" align=\"top\" src=\""); //$NON-NLS-1$
mWriter.write(errorUrl);
mWriter.write("\" />\n"); //$NON-NLS-1$
}
mWriter.write("Errors");
mWriter.write("</th><th class=\"countColumn\">"); //$NON-NLS-1$
if (warningUrl != null) {
mWriter.write("<img border=\"0\" align=\"top\" src=\""); //$NON-NLS-1$
mWriter.write(warningUrl);
mWriter.write("\" />\n"); //$NON-NLS-1$
}
mWriter.write("Warnings");
mWriter.write("</th></tr>\n"); //$NON-NLS-1$
for (ProjectEntry entry : projects) {
mWriter.write("<tr><td>"); //$NON-NLS-1$
mWriter.write("<a href=\"");
mWriter.write(entry.fileName); // TODO: Escape?
mWriter.write("\">"); //$NON-NLS-1$
mWriter.write(entry.path);
mWriter.write("</a></td><td class=\"countColumn\">"); //$NON-NLS-1$
mWriter.write(Integer.toString(entry.errorCount));
mWriter.write("</td><td class=\"countColumn\">"); //$NON-NLS-1$
mWriter.write(Integer.toString(entry.warningCount));
mWriter.write("</td></tr>\n"); //$NON-NLS-1$
}
mWriter.write("</table>\n"); //$NON-NLS-1$
}
private static class ProjectEntry implements Comparable<ProjectEntry> {
public final int errorCount;
public final int warningCount;
public final String fileName;
public final String path;
public ProjectEntry(String fileName, int errorCount, int warningCount, String path) {
this.fileName = fileName;
this.errorCount = errorCount;
this.warningCount = warningCount;
this.path = path;
}
@Override
public int compareTo(ProjectEntry other) {
int delta = other.errorCount - errorCount;
if (delta != 0) {
return delta;
}
delta = other.warningCount - warningCount;
if (delta != 0) {
return delta;
}
return path.compareTo(other.path);
}
}
}