Java tutorial
// Copyright (C) 2001-2016 Tuma Solutions, LLC // Process Dashboard - Data Automation Tool for high-maturity processes // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 3 // of the License, or (at your option) any later version. // // Additional permissions also apply; see the README-license.txt // file in the project root directory for more information. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, see <http://www.gnu.org/licenses/>. // // The author(s) may be contacted at: // processdash@tuma-solutions.com // processdash-devel@lists.sourceforge.net package net.sourceforge.processdash.ev.ui; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.lang.ref.WeakReference; import java.net.URI; import java.text.DateFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.table.TableModel; import org.jfree.chart.JFreeChart; import org.jfree.data.xy.AbstractXYDataset; import org.jfree.data.xy.XYDataset; import org.jfree.ui.RectangleEdge; import org.w3c.dom.Element; import net.sourceforge.processdash.DashboardContext; import net.sourceforge.processdash.Settings; import net.sourceforge.processdash.ev.DefaultTaskLabeler; import net.sourceforge.processdash.ev.EVDependencyCalculator; import net.sourceforge.processdash.ev.EVHierarchicalFilter; import net.sourceforge.processdash.ev.EVLabelFilter; import net.sourceforge.processdash.ev.EVMetadata; import net.sourceforge.processdash.ev.EVMetrics; import net.sourceforge.processdash.ev.EVSchedule; import net.sourceforge.processdash.ev.EVScheduleFiltered; import net.sourceforge.processdash.ev.EVScheduleRollup; import net.sourceforge.processdash.ev.EVTask; import net.sourceforge.processdash.ev.EVTaskDependency; import net.sourceforge.processdash.ev.EVTaskFilter; import net.sourceforge.processdash.ev.EVTaskList; import net.sourceforge.processdash.ev.EVTaskListData; import net.sourceforge.processdash.ev.EVTaskListMerged; import net.sourceforge.processdash.ev.EVTaskListRollup; import net.sourceforge.processdash.ev.MilestoneList; import net.sourceforge.processdash.ev.ui.TaskScheduleChartUtil.ChartItem; import net.sourceforge.processdash.ev.ui.TaskScheduleChartUtil.ChartListPurpose; import net.sourceforge.processdash.ev.ui.chart.AbstractEVChart; import net.sourceforge.processdash.ev.ui.chart.AbstractEVTimeSeriesChart; import net.sourceforge.processdash.ev.ui.chart.HelpAwareEvChart; import net.sourceforge.processdash.ev.ui.chart.HtmlEvChart; import net.sourceforge.processdash.i18n.Resources; import net.sourceforge.processdash.net.cache.CachedURLObject; import net.sourceforge.processdash.net.cms.CMSSnippetEnvironment; import net.sourceforge.processdash.net.http.TinyCGIException; import net.sourceforge.processdash.net.http.WebServer; import net.sourceforge.processdash.templates.ExtensionManager; import net.sourceforge.processdash.ui.lib.HTMLTableWriter; import net.sourceforge.processdash.ui.lib.HTMLTreeTableWriter; import net.sourceforge.processdash.ui.lib.TreeTableModel; import net.sourceforge.processdash.ui.lib.chart.XYDatasetFilter; import net.sourceforge.processdash.ui.snippet.SnippetEnvironment; import net.sourceforge.processdash.ui.snippet.SnippetWidget; import net.sourceforge.processdash.ui.web.CGIChartBase; import net.sourceforge.processdash.ui.web.reports.ExcelReport; import net.sourceforge.processdash.util.FastDateFormat; import net.sourceforge.processdash.util.FileUtils; import net.sourceforge.processdash.util.HTMLUtils; import net.sourceforge.processdash.util.HTTPUtils; import net.sourceforge.processdash.util.OrderedListMerger; import net.sourceforge.processdash.util.StringUtils; /** CGI script for reporting earned value data in HTML. */ public class EVReport extends CGIChartBase { public static final String CHART_PARAM = "chart"; public static final String CHARTS_PARAM = "charts"; public static final String SINGLE_CHART_PARAM = "showChart"; public static final String CHART_OPTIONS_PARAM = "chartOptions"; public static final String TABLE_PARAM = "table"; public static final String XML_PARAM = "xml"; public static final String XLS_PARAM = "xls"; public static final String CSV_PARAM = "csv"; static final String MS_PROJ_XML_PARAM = "msProjXml"; public static final String MERGED_PARAM = "merged"; public static final String TIME_CHART = "time"; public static final String VALUE_CHART = "value"; public static final String VALUE_CHART2 = "value2"; public static final String COMBINED_CHART = "combined"; public static final String FAKE_MODEL_NAME = "/ "; private static final String CUSTOMIZE_PARAM = "customize"; static final String CUSTOMIZE_HIDE_BASELINE = "hideBaseline"; static final String CUSTOMIZE_HIDE_PLAN_LINE = "hidePlanLine"; static final String CUSTOMIZE_HIDE_REPLAN_LINE = "hideReplanLine"; static final String CUSTOMIZE_HIDE_FORECAST_LINE = "hideForecastLine"; static final String CUSTOMIZE_HIDE_NAMES = "hideAssignedTo"; static final String CUSTOMIZE_LABEL_FILTER = EVReportSettings.LABEL_FILTER_PARAM; static final String TASK_STYLE_PARAM = "taskStyle"; private static Resources resources = Resources.getDashBundle("EV"); private static Logger logger = Logger.getLogger(EVReport.class.getName()); boolean drawingChart; @Override protected void doPost() throws IOException { parseFormData(); super.doPost(); } /** Write the CGI header. */ protected void writeHeader() { drawingChart = (parameters.get(CHART_PARAM) != null); if (parameters.get(XML_PARAM) != null || parameters.get(MS_PROJ_XML_PARAM) != null || parameters.get(XLS_PARAM) != null || parameters.get(CSV_PARAM) != null) return; if (drawingChart) super.writeHeader(); else writeHtmlHeader(); } /* In its most common use, this script will generate HTML tables * of an earned value model, accompanied by one or two charts. * These charts (of course) will require additional HTTP requests, * yet we do not want to recalculate the evmodel three times. * Thus, we calculate it once and save it in the following static * fields. */ /** The name of the task list we most recently computed */ static String lastTaskListName = null; /** The earned value model for that task list */ static WeakReference<EVTaskList> lastEVModel = null; /** The instant in time when that task list was calculated. */ static long lastRecalcTime = 0; /** We'll consider the above cached EV model to be stale (needing * recalculation) if it was calculated more than this many * milliseconds ago. */ public static final long MAX_DELAY = 10000L; /** Settings information to use for generating the report */ EVReportSettings settings; /** The earned value model we are currently drawing */ EVTaskList evModel = null; /** The name of the task list that creates that earned value model */ String taskListName = null; /** True if we are rendering a snippet, to be embedded on a larger page */ boolean isSnippet = false; /** Generate CGI output. */ protected void writeContents() throws IOException { // load settings information for the report settings = new EVReportSettings(getDataRepository(), parameters, getPrefix()); // possibly store settings data if requested. if (parameters.get(CUSTOMIZE_PARAM) != null) { storeCustomizationSettings(); return; } // load the user requested earned value model. getEVModel(); String chartType = getParameter(CHART_PARAM); if (chartType == null) { String tableType = getParameter(TABLE_PARAM); if (tableType == null) { if (parameters.get(XML_PARAM) != null) writeXML(); else if (parameters.get(XLS_PARAM) != null) writeXls(); else if (parameters.get(CSV_PARAM) != null) writeCsv(); else if (parameters.get(MS_PROJ_XML_PARAM) != null) writeMSProjXml(); else if (parameters.get(CHARTS_PARAM) != null) writeChartsPage(); else if (parameters.get(CHART_OPTIONS_PARAM) != null) writeChartOptions(); else writeHTML(); } else if (TIME_CHART.equals(tableType)) writeTimeTable(); else if (VALUE_CHART2.equals(tableType)) writeValueTable2(); else if (VALUE_CHART.equals(tableType)) writeValueTable(); else if (COMBINED_CHART.equals(tableType)) writeCombinedTable(); else throw new TinyCGIException(400, "unrecognized table type parameter"); } else if (TIME_CHART.equals(chartType)) writeTimeChart(); else if (VALUE_CHART.equals(chartType)) writeValueChart(); else if (COMBINED_CHART.equals(chartType)) writeCombinedChart(); else throw new TinyCGIException(400, "unrecognized chart type parameter"); } private void getEVModel() throws TinyCGIException { taskListName = settings.getTaskListName(); if (taskListName == null) throw new TinyCGIException(400, "schedule name missing"); else if (FAKE_MODEL_NAME.equals(taskListName)) { evModel = null; return; } long now = System.currentTimeMillis(); synchronized (EVReport.class) { if (drawingChart && (now - lastRecalcTime < MAX_DELAY) && taskListName.equals(lastTaskListName)) { evModel = lastEVModel.get(); if (evModel != null) return; } } evModel = EVTaskList.openExisting(taskListName, getDataRepository(), getPSPProperties(), getObjectCache(), false); // change notification not required if (evModel == null) throw new TinyCGIException(404, "Not Found", "No such task/schedule"); EVDependencyCalculator depCalc = new EVDependencyCalculator(getDataRepository(), getPSPProperties(), getObjectCache()); evModel.setDependencyCalculator(depCalc); evModel.setTaskLabeler(new DefaultTaskLabeler(getDashboardContext())); if (settings.getBool(CUSTOMIZE_HIDE_BASELINE)) evModel.disableBaselineData(); evModel.recalc(); synchronized (EVReport.class) { lastTaskListName = taskListName; lastRecalcTime = now; lastEVModel = new WeakReference<EVTaskList>(evModel); } } /** Generate a page of XML data for the Task and Schedule templates. */ public void writeXML() throws IOException { if (evModel.isEmpty()) { out.print("Status: 404 Not Found\r\n\r\n"); out.flush(); } else { outStream.write("Content-type: application/xml\r\n".getBytes()); String owner = getOwner(); if (owner != null) outStream.write((CachedURLObject.OWNER_HEADER_FIELD + ": " + owner + "\r\n").getBytes()); outStream.write("\r\n".getBytes()); if (evModel instanceof EVTaskListRollup && parameters.containsKey(MERGED_PARAM)) { evModel = new EVTaskListMerged(evModel, false, settings.shouldMergePreserveLeaves(), null); } outStream.write(XML_HEADER.getBytes("UTF-8")); outStream.write(evModel.getAsXML(true).getBytes("UTF-8")); outStream.flush(); } } private static final String XML_HEADER = "<?xml version='1.0' encoding='UTF-8'?>"; /** Generate a excel spreadsheet to display EV charts. */ public void writeXls() throws IOException { if (evModel == null || evModel.isEmpty()) { out.print("Status: 404 Not Found\r\n\r\n"); out.flush(); } else if ("Excel97".equalsIgnoreCase(Settings.getVal("excel.exportChartsMethod"))) { out.print("Content-type: application/vnd.ms-excel\r\n\r\n"); out.flush(); FileUtils.copyFile(EVReport.class.getResourceAsStream("evCharts97.xls"), outStream); } else { out = new PrintWriter(new OutputStreamWriter(outStream, "us-ascii")); out.print("Content-type: application/vnd.ms-excel\r\n\r\n"); BufferedReader in = new BufferedReader( new InputStreamReader(EVReport.class.getResourceAsStream("evCharts2002.mht"), "us-ascii")); scanAndCopyLines(in, "Single File Web Page", true, false); out.print("This document, generated by the Process Dashboard,\r\n" + "is designed to work with Excel 2002 and higher. If\r\n" + "you are using an earlier version of Excel, try\r\n" + "adding the following line to your pspdash.ini file:\r\n" + "excel.exportChartsMethod=Excel97\r\n"); boolean needsOptimizedLine = (evModel.getSchedule() instanceof EVScheduleRollup); if (needsOptimizedLine == false) { // find the data series immediately preceeding the series // describing the optimized line. Copy it all to output. scanAndCopyLines(in, "'EV Data'!$D$2:$D$10", true, true); scanAndCopyLines(in, "</x:Series>", true, true); // Now skip over and discard the series describing the // optimized line. scanAndCopyLines(in, "</x:Series>", false, false); } String line; while ((line = scanAndCopyLines(in, "http://localhost:2468/++/", true, false)) != null) { writeUrlLine(line); } out.flush(); } } private String scanAndCopyLines(BufferedReader in, String lookFor, boolean printLines, boolean printLast) throws IOException { String line; while ((line = in.readLine()) != null) { boolean found = (line.indexOf(lookFor) != -1); if ((!found && printLines) || (found && printLast)) { out.print(line); out.print("\r\n"); } if (found) return line; } return null; } private void writeUrlLine(String line) { String path = (String) env.get("PATH_INFO"); String newBase = getRequestURLBase() + path; // need to escape the result for quoted printable display. Since it's // a URL, the only unsafe character it might contain is the '=' sign. newBase = StringUtils.findAndReplace(newBase, "=", "=3D"); // replace the generic host/path information in the URL with the // specific information we've built. line = StringUtils.findAndReplace(line, "http://localhost:2468/++", newBase); while (line.length() > 76) { int pos = line.indexOf('=', 70); if (pos == -1 || pos > 73) pos = 73; out.print(line.substring(0, pos)); out.print("=\r\n"); line = line.substring(pos); } out.print(line); out.print("\r\n"); } /** Generate a file of comma-separated data for use by MS Project. */ private void writeCsv() throws IOException { if (evModel == null || evModel.isEmpty()) { out.print("Status: 404 Not Found\r\n\r\n"); out.flush(); return; } writeContentDispositionHeader(".csv"); out.print("Content-type: text/plain\r\n\r\n"); boolean simpleCsv = Settings.getBool("ev.simpleCsvOutput", false); List columns = null; if (simpleCsv) { columns = createSimpleCsvColumns(); } else { columns = createCsvColumns(); writeCsvColumnHeaders(columns); } TreeTableModel merged = evModel.getMergedModel(false, false, null); EVTask root = (EVTask) merged.getRoot(); prepCsvColumns(columns, root, root, 1); writeCsvRows(columns, root, 1); } private void writeContentDispositionHeader(String filenameSuffix) throws IOException { DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); String displayName = EVTaskList.getDisplayName(taskListName); String filename = FileUtils.makeSafe(displayName) + "-" + fmt.format(new Date()) + filenameSuffix; out.flush(); outStream.write(("Content-Disposition: attachment; filename=\"" + filename + "\"\r\n") .getBytes(HTTPUtils.DEFAULT_CHARSET)); } private void writeCsvColumnHeaders(List columns) { for (Iterator i = columns.iterator(); i.hasNext();) { CsvColumn c = (CsvColumn) i.next(); out.print(c.header); if (i.hasNext()) out.print(","); else out.print("\r\n"); } } private List createSimpleCsvColumns() { List result = new LinkedList(); result.add(new CsvColumn("Outline_Level") { public void write(EVTask node, int depth) { out.print(depth); } }); result.add(new CsvColumn("Name") { public void write(EVTask node, int depth) { writeStringCsvField(node.getName()); } }); result.add(new CsvDateColumn("Finish_Date") { public Date getNodeDate(EVTask node) { Date result = node.getActualDate(); if (result == null) result = node.getPlanDate(); return result; } }); result.add(new CsvHoursColumn("Duration") { public double getNodeMinutes(EVTask node) { return node.getPlanValue(); } }); result.add(new CsvColumn("Resource_Names") { public void writeNode(EVTask node, int depth) { writeStringCsvField(""); } public void writeLeaf(EVTask node, int depth) { writeStringCsvField(node.getAssignedToText()); } }); result.add(new CsvHoursColumn("Actual_Duration") { public double getNodeMinutes(EVTask node) { return node.getActualDirectTime(); } }); result.add(new CsvColumn("Percent_Complete") { public void writeNode(EVTask node, int depth) { out.print("0"); } public void writeLeaf(EVTask node, int depth) { out.print(cleanupPercentComplete(node.getPercentCompleteText())); } }); return result; } private List createCsvColumns() { List result = new LinkedList(); final Map idNumbers = new HashMap(); final Map taskIDs = new HashMap(); result.add(new CsvColumn("ID") { int id = 1; public void doPrepWork(EVTask root, EVTask node, int depth) { idNumbers.put(node, new Integer(id++)); } public void write(EVTask node, int depth) { out.print(idNumbers.get(node)); } }); result.add(new CsvColumn("Name") { public void write(EVTask node, int depth) { writeStringCsvField(node.getName()); } }); result.add(new CsvColumn("Outline_Level") { public void write(EVTask node, int depth) { out.print(depth); } }); result.add(new CsvColumn("Predecessors") { public void doPrepWork(EVTask root, EVTask node, int depth) { // populate the taskIDs map so we can look up dashboard tasks // by their taskID later. List nodeIDs = node.getTaskIDs(); if (nodeIDs != null) for (Iterator i = nodeIDs.iterator(); i.hasNext();) { String id = (String) i.next(); taskIDs.put(id, node); } } public void write(EVTask node, int depth) { List dependencies = node.getDependencies(); if (dependencies == null || dependencies.isEmpty()) return; List predIDs = new LinkedList(); for (Iterator i = dependencies.iterator(); i.hasNext();) { // find the dashboard task named by each dependency. EVTaskDependency d = (EVTaskDependency) i.next(); String dashTaskID = d.getTaskID(); Object predTask = taskIDs.get(dashTaskID); if (predTask == null) continue; // look up the ID number we assigned to it in this CSV // export file, and add that ID number to our list. Object csvIdNumber = idNumbers.get(predTask); if (csvIdNumber == null) continue; predIDs.add(csvIdNumber); } if (!predIDs.isEmpty()) writeStringCsvField(StringUtils.join(predIDs, ",")); } }); // result.add(new CsvDateColumn("Start_Date") { // public Date getNodeDate(EVTask node) { // return node.getPlanStartDate(); // } // }); result.add(new CsvDateColumn("Finish_Date") { public Date getNodeDate(EVTask node) { Date result = node.getActualDate(); if (result == null) result = node.getPlanDate(); return result; } }); result.add(new CsvColumn("Percent_Complete") { public void writeNode(EVTask node, int depth) { out.print("0"); } public void writeLeaf(EVTask node, int depth) { out.print(cleanupPercentComplete(node.getPercentCompleteText())); } }); // result.add(new CsvDateColumn("Actual_Start") { // public Date getNodeDate(EVTask node) { // return nullToNA(node.getActualStartDate()); // } // }); // // result.add(new CsvDateColumn("Actual_Finish") { // public Date getNodeDate(EVTask node) { // return nullToNA(node.getActualDate()); // } // }); result.add(new CsvHoursColumn("Duration") { // "Scheduled_Work") { public double getNodeMinutes(EVTask node) { return node.getPlanValue(); } }); result.add(new CsvHoursColumn("Actual_Duration") { // "Actual_Work") { public double getNodeMinutes(EVTask node) { return node.getActualDirectTime(); } }); result.add(new CsvColumn("Resource_Names") { public void writeLeaf(EVTask node, int depth) { writeStringCsvField(node.getAssignedToText()); } }); return result; } private void prepCsvColumns(List columns, EVTask root, EVTask node, int depth) { for (Iterator i = columns.iterator(); i.hasNext();) { CsvColumn c = (CsvColumn) i.next(); c.doPrepWork(root, node, depth); } for (int i = 0; i < node.getNumChildren(); i++) prepCsvColumns(columns, root, node.getChild(i), depth + 1); } private void writeCsvRows(List columns, EVTask node, int depth) { for (Iterator i = columns.iterator(); i.hasNext();) { CsvColumn c = (CsvColumn) i.next(); c.write(node, depth); if (i.hasNext()) out.print(","); else out.print("\r\n"); } for (int i = 0; i < node.getNumChildren(); i++) writeCsvRows(columns, node.getChild(i), depth + 1); } private void writeStringCsvField(String s) { out.print("\""); if (s != null) out.print(StringUtils.findAndReplace(s, "\"", "\"\"")); out.print("\""); } private void writeDateCsvField(Date d) { if (d == null) out.print(" "); else if (d == EVSchedule.NEVER) out.print("NA"); else out.print(CSV_DATE_FORMAT.format(d)); } private static final DateFormat CSV_DATE_FORMAT = DateFormat.getDateInstance(DateFormat.SHORT); private class CsvColumn { String header; public CsvColumn(String header) { this.header = header; } public void doPrepWork(EVTask root, EVTask node, int depth) { } public void write(EVTask node, int depth) { if (node.isLeaf()) writeLeaf(node, depth); else writeNode(node, depth); } public void writeLeaf(EVTask node, int depth) { } public void writeNode(EVTask node, int depth) { } } private abstract class CsvHoursColumn extends CsvColumn { public CsvHoursColumn(String header) { super(header); } public void writeLeaf(EVTask node, int depth) { out.print(HOURS_FMT.format(getNodeMinutes(node) / 60)); out.print("h"); } public void writeNode(EVTask node, int depth) { out.print("0h"); } public abstract double getNodeMinutes(EVTask node); } private NumberFormat HOURS_FMT = NumberFormat.getInstance(); private abstract class CsvDateColumn extends CsvColumn { public CsvDateColumn(String header) { super(header); } public void writeNode(EVTask node, int depth) { out.print(" "); } public void writeLeaf(EVTask node, int depth) { writeDateCsvField(getNodeDate(node)); } // public Date nullToNA(Date d) { // return (d == null ? EVSchedule.NEVER : d); // } public abstract Date getNodeDate(EVTask node); } /** Generate an XML document in Microsoft Project mspdi format. */ public void writeMSProjXml() throws IOException { MSProjectXmlWriter writer = new MSProjectXmlWriter(); EVTaskFilter taskFilter = settings.getEffectiveFilter(evModel); EVTaskListMerged mergedModel = new EVTaskListMerged(evModel, false, true, taskFilter); writer.setTaskList(mergedModel); String taskListID = evModel.getID(); String metadataPrefix = "/Task-Schedule-MS-Project/" + taskListID; writer.setMetadata(getDataRepository().getSubcontext(metadataPrefix)); if (parameters.containsKey("dateStyle")) writer.setDateStyle(getParameter("dateStyle")); if (parameters.containsKey("showSaveAs")) writeContentDispositionHeader(".xml"); outStream.write("Content-type: application/xml\r\n\r\n".getBytes(HTTPUtils.DEFAULT_CHARSET)); writer.write(outStream); outStream.flush(); } // handle the storage and retrieval of customization settings. public void storeCustomizationSettings() throws IOException { out.println("<html><head><script>"); if (parameters.containsKey("OK")) { settings.store(CUSTOMIZE_HIDE_BASELINE, true); settings.store(CUSTOMIZE_HIDE_PLAN_LINE, true); settings.store(CUSTOMIZE_HIDE_REPLAN_LINE, true); settings.store(CUSTOMIZE_HIDE_FORECAST_LINE, true); settings.store(CUSTOMIZE_HIDE_NAMES, true); settings.store(CUSTOMIZE_LABEL_FILTER, false); saveChartOrderingPreference(); out.println("window.opener.location.reload();"); } out.println("window.close();"); out.println("</script></head>"); // the text below generally will never appear to the user (the // javascript should close this window immediately) out.println("<body>Changes saved.</body></html>"); } /** Generate a page of HTML displaying the Task and Schedule templates, * and including img tags referencing charts. */ public void writeHTML() throws IOException { isSnippet = (env.containsKey(SnippetEnvironment.SNIPPET_ID)); String namespace = (isSnippet ? "$$$_" : ""); String taskListDisplayName = EVTaskList.cleanupName(taskListName); String taskListHTML = HTMLUtils.escapeEntities(taskListDisplayName); String title = resources.format("Report.Title_FMT", taskListHTML); EVTaskFilter taskFilter = settings.getEffectiveFilter(evModel); EVSchedule s = getEvSchedule(taskFilter); EVTaskDataWriter taskDataWriter = getEffectiveTaskDataWriter(); StringBuffer header = new StringBuffer(HEADER_HTML); StringUtils.findAndReplace(header, TITLE_VAR, title); if (taskFilter != null && isSnippet == false) header.append(FILTER_HEADER_HTML); out.print(header); out.print(taskDataWriter.getHeaderItems()); out.print("</head><body>"); out.print(isSnippet ? "<h2>" : "<h1>"); out.print(title); if (!exportingToExcel()) { interpOutLink(SHOW_WEEK_LINK, EVReportSettings.PURPOSE_WEEK); interpOutLink(SHOW_MONTH_LINK, EVReportSettings.PURPOSE_WEEK); printAlternateViewLinks(); interpOutLink(SHOW_CHARTS_LINK, EVReportSettings.PURPOSE_OTHER); } printCustomizationLink(); out.print(isSnippet ? "</h2>" : "</h1>"); if (!isSnippet) printFilterInfo(out, taskFilter, exportingToExcel()); if (!exportingToExcel()) { writeImageHtml(taskFilter != null); out.print("<div style='clear:both'></div>"); writeCharts(evModel, s, taskFilter, settings.getBool(CUSTOMIZE_HIDE_NAMES), 350, 300, ChartListPurpose.ReportMain, null, null); out.print("<div style='clear:both'> </div>"); out.print(HTMLTreeTableWriter.TREE_ICON_HEADER); } EVMetrics m = s.getMetrics(); printScheduleErrors(out, m.getErrors()); boolean hidePlan = settings.getBool(CUSTOMIZE_HIDE_PLAN_LINE); boolean hideReplan = settings.getBool(CUSTOMIZE_HIDE_REPLAN_LINE); boolean hideForecast = settings.getBool(CUSTOMIZE_HIDE_FORECAST_LINE); out.print("<table name='STATS'>"); for (int i = 0; i < m.getRowCount(); i++) writeMetric(m, i, hidePlan, hideReplan, hideForecast); out.print("</table>"); out.print("<h2><a name='" + namespace + "tasks'></a>" + getResource("TaskList.Title")); printTaskStyleLinks(taskDataWriter, namespace); out.print("</h2>\n"); taskDataWriter.write(out, evModel, taskFilter, settings, namespace); out.print("<h2>" + getResource("Schedule.Title") + "</h2>\n"); writeScheduleTable(s); if (isExporting() && !isSnippet) writeExportFooter(out); out.print("<p class='doNotPrint'>"); if (!isSnippet && !exportingToExcel()) interpOutLink(EXPORT_TEXT_LINK); if (!parameters.containsKey("EXPORT")) { if (taskFilter == null) { interpOutLink(EXPORT_CHARTS_LINK); interpOutLink(EXPORT_MSPROJ_LINK); } if (!isSnippet) { String link = EXPORT_ARCHIVE_LINK; String filenamePat = HTMLUtils.urlEncode(resources.getString("Report.Archive_Filename")); link = StringUtils.findAndReplace(link, "FILENAME", filenamePat); interpOutLink(link); } } out.print("</p>"); out.print("</body></html>"); } protected static void printScheduleErrors(PrintWriter out, Map errors) { if (errors != null && errors.size() > 0) { out.print("<table border><tr>"); out.print("<td style='text-align: left; background-color: #ff5050'><h2>"); out.print(getResource("Report.Errors_Heading")); out.print("</h2><b>"); out.print(getResource("Error_Dialog.Head")); out.print("<ul>"); Iterator i = errors.keySet().iterator(); while (i.hasNext()) { String message = (String) i.next(); String helpSet = null; String helpTopic = null; String helpText = null; Matcher m = ERROR_HELP_PATTERN.matcher(message); if (m.matches()) { message = m.group(1); helpSet = m.group(2); helpTopic = m.group(3); helpText = m.group(4).trim(); } out.print("\n<li>" + HTMLUtils.escapeEntities(message)); if (helpTopic != null) { out.print(" <i>(<a target='_blank' href='/" + helpSet + "/frame.html?" + helpTopic + "'>"); out.print(HTMLUtils.escapeEntities(helpText)); out.print("</a>)</i>"); } out.print("</li>"); } out.print("\n</ul>"); if (!EVMetrics.isWarningOnly(errors)) out.print(getResource("Error_Dialog.Foot")); out.print("</b></td></tr></table>\n"); } } private static final Pattern ERROR_HELP_PATTERN = Pattern.compile("(.+)\\n#(\\S+)/(\\S+) (.+)"); protected static void writeExportFooter(PrintWriter out) { out.print("<p><i>"); out.print(HTMLUtils.escapeEntities(resources.format("Report.Export_Date_Footer_FMT", new Date()))); out.print("</i></p>\n"); } private static List<EVTaskDataWriter> taskDataWriters = null; private static List<EVTaskDataWriter> getTaskDataWriters() { if (taskDataWriters == null) { List<EVTaskDataWriter> result = new ArrayList(); result.add(new TreeViewTaskDataWriter()); result.add(new FlatViewTaskDataWriter()); TreeMap<String, EVTaskDataWriter> customWriters = new TreeMap(); for (Object extObj : ExtensionManager.getExecutableExtensions("ev-task-writer", null)) { if (extObj instanceof EVTaskDataWriter) { EVTaskDataWriter custom = (EVTaskDataWriter) extObj; customWriters.put(custom.getID(), custom); } } result.addAll(customWriters.values()); taskDataWriters = Collections.unmodifiableList(result); } return taskDataWriters; } private EVTaskDataWriter getEffectiveTaskDataWriter() { List<EVTaskDataWriter> writers = getTaskDataWriters(); // force flat view in export to excel mode if (exportingToExcel()) return writers.get(1); // look for a parameter indicating which writer to use String paramStyle = getParameter(TASK_STYLE_PARAM); if (paramStyle != null) { for (EVTaskDataWriter w : writers) if (paramStyle.equals(w.getID())) return w; } // return the default (tree) writer return writers.get(0); } private void writeImageHtml(boolean showCombined) throws IOException { boolean exporting = isExporting(); Map realParameters = this.parameters; Map imgParams = new HashMap(); this.parameters = imgParams; imgParams.put("initGradColor", "#bebdff"); imgParams.put("finalGradColor", "#bebdff"); imgParams.put("html", "t"); imgParams.put("noBorder", "t"); if (exporting) imgParams.put("EXPORT", realParameters.get("EXPORT")); out.write("<pre>"); if (showCombined) { imgParams.put("width", "720"); if (!exporting) imgParams.put("href", getChartDrillDownUrl("pdash.ev.cumCombinedChart")); writeCombinedChart(); } else { if (!exporting) imgParams.put("href", getChartDrillDownUrl("pdash.ev.cumValueChart")); writeValueChart(); imgParams.put("width", "320"); imgParams.put("hideLegend", "t"); imgParams.remove("title"); if (!exporting) imgParams.put("href", getChartDrillDownUrl("pdash.ev.cumDirectTimeChart")); writeTimeChart(); } out.write("</pre>\n"); this.parameters = realParameters; } private EVSchedule getEvSchedule(EVTaskFilter taskFilter) { if (taskFilter == null) return evModel.getSchedule(); else return new EVScheduleFiltered(evModel, taskFilter); } private void interpOutLink(String html) { interpOutLink(html, EVReportSettings.PURPOSE_OTHER); } private void interpOutLink(String html, int purpose) { interpOutLink(html, purpose, resources); } private void interpOutLink(String html, int purpose, Resources resources) { html = StringUtils.findAndReplace(html, "@@@", settings.getEffectivePrefix()); String query = settings.getQueryString(purpose); html = StringUtils.findAndReplace(html, "???", query); if (StringUtils.hasValue(query)) html = StringUtils.findAndReplace(html, "??&", query + "&"); else html = StringUtils.findAndReplace(html, "??&", "?"); html = resources.interpolate(html, HTMLUtils.ESC_ENTITIES); out.print(html); } public static void printFilterInfo(PrintWriter out, EVTaskFilter filter, boolean textOnly) { if (filter == null) return; String labelFilter = filter.getAttribute(EVLabelFilter.LABEL_FILTER_ATTR); String pathFilter = filter.getAttribute(EVHierarchicalFilter.HIER_FILTER_ATTR); if (labelFilter == null && pathFilter == null) return; out.print("<h2>"); if (labelFilter != null) { if (!textOnly) out.print("<img border=0 src='/Images/filter.png' " + "style='margin-right:2px' " + "width='16' height='23' title=\""); out.print(resources.getHTML("Report.Filter_Tooltip")); out.print(textOnly ? " - " : "\">"); out.print(HTMLUtils.escapeEntities(labelFilter)); } out.print(" "); if (pathFilter != null) { if (!textOnly) out.print("<img border=0 src='/Images/hier.png' " + "style='margin-right:2px' " + "width='16' height='23' title=\""); out.print(resources.getHTML("Report.Filter_Tooltip")); out.print(textOnly ? " - " : "\">"); out.print(HTMLUtils.escapeEntities(pathFilter)); } out.println("</h2>"); } private void printAlternateViewLinks() { List<Element> altViews = ExtensionManager.getXmlConfigurationElements("ev-report-view"); if (altViews != null) for (Element view : altViews) printAlternateViewLink(view); } private void printAlternateViewLink(Element view) { // Get the URI of the report String uri = view.getAttribute("href"); if (uri.startsWith("/")) uri = uri.substring(1); // Get the resource bundle for messages String resourcePrefix = view.getAttribute("resources"); Resources res = Resources.getDashBundle(resourcePrefix); // Construct the link HTML and print it String linkHtml = StringUtils.findAndReplace(SHOW_ALT_LINK, "[URI]", uri); interpOutLink(linkHtml, EVReportSettings.PURPOSE_OTHER, res); } private void printCustomizationLink() { if (!parameters.containsKey("EXPORT")) { out.print("<span " + HEADER_LINK_STYLE + ">" + "<span class='doNotPrint'><a href='"); out.print(settings.getEffectivePrefix()); out.print("ev-customize.shtm?tlid="); out.print(HTMLUtils.urlEncode(evModel.getID())); if ((evModel instanceof EVTaskListRollup)) out.print("&isRollup"); if (!parameters.containsKey(EVReportSettings.LABEL_FILTER_PARAM) && EVLabelFilter.taskListContainsLabelData(evModel, getDataRepository())) out.print("&showLabelFilter"); if (evModel.getMetadata(EVMetadata.Baseline.SNAPSHOT_ID) != null) out.print("&hasBaseline"); out.print("' target='customize' onClick='PdashEV.openCustomizeWindow();'>"); out.print(HTMLUtils.escapeEntities(resources.getDlgString("Customize"))); out.print("</a></span></span>"); } } private void printTaskStyleLinks(EVTaskDataWriter taskDataWriter, String namespace) { if (exportingToExcel()) return; StringBuffer href = new StringBuffer(); String uri = (String) env.get(CMSSnippetEnvironment.CURRENT_FRAME_URI); if (uri == null) { href.append("ev.class"); } else { href.append(uri); Matcher m = TASK_STYLE_PARAM_PATTERN.matcher(uri); if (m.find()) HTMLUtils.removeParam(href, m.group()); } HTMLUtils.appendQuery(href, settings.getQueryString(EVReportSettings.PURPOSE_TASK_STYLE)); href.append(href.indexOf("?") == -1 ? '?' : '&'); href.append(namespace).append(TASK_STYLE_PARAM).append('='); for (EVTaskDataWriter w : getTaskDataWriters()) { if (w == taskDataWriter) continue; out.print("<span " + HEADER_LINK_STYLE + ">" + "<span class='doNotPrint'>" + "<a id=\"" + namespace + "taskstyle" + w.getID() + "\" href=\"" + href + w.getID() + "#" + namespace + "tasks\">"); out.print(HTMLUtils.escapeEntities(w.getDisplayName())); out.print("</a></span></span>"); } } private Pattern TASK_STYLE_PARAM_PATTERN = Pattern.compile("[^&?]*" + TASK_STYLE_PARAM); protected void writeMetric(EVMetrics m, int i, boolean hidePlan, boolean hideReplan, boolean hideForecast) { String metricID = (String) m.getValueAt(i, EVMetrics.METRIC_ID); if (hidePlan && (metricID.indexOf("Plan_") != -1)) return; if (hideReplan && (metricID.indexOf("Replan_") != -1)) return; if (hideForecast && metricID.indexOf("Forecast_") != -1) return; String name = (String) m.getValueAt(i, EVMetrics.NAME); if (name == null) return; String number = (String) m.getValueAt(i, EVMetrics.SHORT); String interpretation = (String) m.getValueAt(i, EVMetrics.MEDIUM); String explanation = (String) m.getValueAt(i, EVMetrics.FULL); boolean writeInterpretation = !number.equals(interpretation); boolean writeExplanation = !exportingToExcel(); boolean printExplanation = Settings.getBool("ev.printMetricsExplanations", true); out.write("<tr><td valign='top'><b>"); out.write(name); out.write(": </b></td><td valign='top'>"); out.write(number); out.write("</td><td colspan='5' valign='top'>"); if (printExplanation) out.write("<i class='doNotPrint'>"); else out.write("<i>"); if (writeInterpretation || writeExplanation) out.write("("); if (writeInterpretation) out.write(interpretation); if (writeInterpretation && writeExplanation) out.write(" "); if (writeExplanation) { out.write("<a class='doNotPrint' href='#' " + "onclick='togglePopupInfo(this); return false;'>"); out.write(encodeHTML(resources.getDlgString("More"))); out.write("</a>)<div class='popupInfo'><table width='300' " + "onclick='togglePopupInfo(this.parentNode)'><tr><td>"); out.write(HTMLUtils.escapeEntities(explanation)); out.write("</td></tr></table></div>"); } else if (writeInterpretation) { out.write(")"); } out.write("</i>"); if (writeExplanation && printExplanation) { out.write("<i class='printOnly' style='margin-bottom:1em'>("); out.write(HTMLUtils.escapeEntities(explanation)); out.write(")</i>"); } out.write("</td></tr>\n"); } private boolean exportingToExcel() { return ExcelReport.EXPORT_TAG.equals(getParameter("EXPORT")); } static final String TITLE_VAR = "%title%"; static final String REDUNDANT_EXCEL_HEADER = "<style>.timeFmt { vnd.ms-excel.numberformat: [h]\\:mm }</style>\n"; static final String POPUP_HEADER = "<link rel=stylesheet type='text/css' href='/lib/popup.css'>\n" + "<script type='text/javascript' src='/lib/popup.js'></script>\n"; static final String SORTTABLE_HEADER = "<link rel=stylesheet type='text/css' href='/lib/sorttable.css'>\n" + "<script type='text/javascript' src='/lib/sorttable.js'></script>\n"; static final String SORTTREE_HEADER = "<script type='text/javascript' src='/reports/evTreeSort.js'></script>\n"; static final String SIMPLE_HEADER_HTML = HTMLUtils.HTML_TRANSITIONAL_DOCTYPE + "<html><head><title>%title%</title>\n" + "<link rel=stylesheet type='text/css' href='/style.css'>\n" + "<script type='text/javascript' src='/lib/overlib.js'></script>\n" + "<script type='text/javascript' src='/reports/ev.js'></script>\n" + "<link rel=stylesheet type='text/css' href='/reports/ev.css'>\n"; static final String HEADER_HTML = SIMPLE_HEADER_HTML + HTMLTreeTableWriter.TREE_HEADER_ITEMS + REDUNDANT_EXCEL_HEADER + POPUP_HEADER + SORTTABLE_HEADER + SORTTREE_HEADER; public static final String FILTER_HEADER_HTML = "<link rel=stylesheet type='text/css' href='/reports/filter-style.css'>\n"; static final String HEADER_LINK_STYLE = " style='font-size: medium; " + "font-style: italic; font-weight: normal; margin-left: 0.5cm' "; static final String EXPORT_TEXT_LINK = "<a href=\"@@@excel.iqy\"><i>" + "${Report.Export_Text}</i></a> "; static final String EXPORT_CHARTS_LINK = "<a href='@@@ev.xls'><i>" + "${Report.Export_Charts}</i></a> "; static final String EXPORT_MSPROJ_LINK = "<a href='@@@ev-project-instr.htm'><i>" + "${Report.Export_Project}</i></a> "; static final String EXPORT_ARCHIVE_LINK = "<a href='../dash/archive.class?filename=FILENAME'><i>" + "${Report.Export_Archive}</i></a> "; static final String SHOW_WEEK_LINK = "<span " + HEADER_LINK_STYLE + ">" + "<span class='doNotPrint'><a href='week.class???'>" + "${Report.Show_Weekly_View}</a></span></span>"; static final String SHOW_MONTH_LINK = "<span " + HEADER_LINK_STYLE + ">" + "<span class='doNotPrint'><a href='month???'>" + "${Report.Show_Monthly_View}</a></span></span>"; static final String SHOW_ALT_LINK = "<span " + HEADER_LINK_STYLE + ">" + "<span class='doNotPrint'><a href='../[URI]???'>" + "${Link_Text}</a></span></span>"; static final String SHOW_CHARTS_LINK = "<span " + HEADER_LINK_STYLE + ">" + "<span class='doNotPrint'><a href='ev.class??&charts'>" + "${Report.Charts_Link}</a></span></span>"; static final String EXCEL_TIME_TD = "<td class='timeFmt'>"; private static class FlatViewTaskDataWriter implements EVTaskDataWriter { public String getID() { return "flat"; } public String getDisplayName() { return resources.getString("Report.Flat_View"); } public String getHeaderItems() { return ""; } public void write(Writer out, EVTaskList taskList, EVTaskFilter filter, EVReportSettings settings, String namespace) throws IOException { writeTaskTable(out, taskList, filter, settings, namespace); } } private static void writeTaskTable(Writer out, EVTaskList taskList, EVTaskFilter filter, EVReportSettings settings, String namespace) throws IOException { HTMLTableWriter writer = new HTMLTableWriter(); boolean showTimingIcons = taskList instanceof EVTaskListData && !settings.isExporting(); TableModel table = customizeTaskTableWriter(writer, taskList, filter, settings, showTimingIcons); writer.setTableAttributes("class='sortable' id='" + namespace + "task' border='1'"); writer.writeTable(out, table); } private static class TreeViewTaskDataWriter implements EVTaskDataWriter { public String getID() { return "tree"; } public String getDisplayName() { return resources.getString("Report.Tree_View"); } public String getHeaderItems() { return ""; } public void write(Writer out, EVTaskList taskList, EVTaskFilter filter, EVReportSettings settings, String namespace) throws IOException { writeTaskTree(out, taskList, filter, settings, namespace); } } private static void writeTaskTree(Writer out, EVTaskList taskList, EVTaskFilter filter, EVReportSettings settings, String namespace) throws IOException { TreeTableModel tree = taskList.getMergedModel(true, settings.shouldMergePreserveLeaves(), filter); HTMLTreeTableWriter writer = new HTMLTreeTableWriter(); customizeTaskTableWriter(writer, taskList, null, settings, false); writer.setTreeName(namespace + "t"); writer.setExpandAllTooltip(resources.getHTML("Report.Expand_All_Tooltip")); writer.setTableAttributes("class='needsTreeSortLinks' id='" + namespace + "task' border='1'"); writer.setShowDepth(Settings.getInt("ev.showHierarchicalDepth", 3) - 1); writer.writeTree(out, tree); } void writeScheduleTable(EVSchedule s) throws IOException { HTMLTableWriter writer = new HTMLTableWriter(); customizeTableWriter(writer, s, s.getColumnTooltips()); setupRenderers(writer, EVSchedule.COLUMN_FORMATS); writer.setSkipColumn(EVSchedule.NOTES_COLUMN, true); writer.setTableName("SCHEDULE"); writer.writeTable(out, s); } private static TableModel customizeTaskTableWriter(HTMLTableWriter writer, EVTaskList taskList, EVTaskFilter filter, EVReportSettings settings, boolean showTimingIcons) { TableModel table = taskList.getSimpleTableModel(filter); boolean hidePlan = settings.getBool(CUSTOMIZE_HIDE_PLAN_LINE); boolean hideReplan = settings.getBool(CUSTOMIZE_HIDE_REPLAN_LINE); boolean hideForecast = settings.getBool(CUSTOMIZE_HIDE_FORECAST_LINE); boolean hideNames = settings.getBool(CUSTOMIZE_HIDE_NAMES); customizeTableWriter(writer, table, EVTaskList.toolTips); writer.setTableName("TASK"); writer.setSkipColumn(EVTaskList.PLAN_CUM_TIME_COLUMN, true); writer.setSkipColumn(EVTaskList.PLAN_CUM_VALUE_COLUMN, true); writer.setSkipColumn(EVTaskList.NOTES_COLUMN, true); setupTaskTableRenderers(writer, showTimingIcons, settings.exportingToExcel(), hideNames, taskList.getNodeTypeSpecs()); if (!(taskList instanceof EVTaskListRollup) || hideNames) writer.setSkipColumn(EVTaskList.ASSIGNED_TO_COLUMN, true); if (hidePlan) writer.setSkipColumn(EVTaskList.PLAN_DATE_COLUMN, true); if (hideReplan) writer.setSkipColumn(EVTaskList.REPLAN_DATE_COLUMN, true); if (hideForecast) writer.setSkipColumn(EVTaskList.FORECAST_DATE_COLUMN, true); return table; } /** Install renderers that are appropriate for a task table. */ static HTMLTableWriter setupTaskTableRenderers(HTMLTableWriter writer, boolean showTimingIcons, boolean exportingToExcel, boolean hideNames, Set nodeTypeSpecs) { setupRenderers(writer, EVTaskList.COLUMN_FORMATS); writer.setCellRenderer(EVTaskList.MILESTONE_COLUMN, new MilestoneCellRenderer()); writer.setCellRenderer(EVTaskList.DEPENDENCIES_COLUMN, new DependencyCellRenderer(exportingToExcel, hideNames)); if (showTimingIcons) writer.setCellRenderer(EVTaskList.TASK_COLUMN, new TaskNameWithTimingIconRenderer()); if (nodeTypeSpecs != null && !nodeTypeSpecs.isEmpty()) writer.setCellRenderer(EVTaskList.NODE_TYPE_COLUMN, new NodeTypeCellRenderer(nodeTypeSpecs)); return writer; } private static void setupRenderers(HTMLTableWriter w, Object[] formats) { w.setCellRenderer(EV_CELL_RENDERER); for (int col = 0; col < formats.length; col++) { Object cellRenderer = RENDERERS.get(formats[col]); if (cellRenderer == null) cellRenderer = EV_CELL_RENDERER; w.setCellRenderer(col, (HTMLTableWriter.CellRenderer) cellRenderer); } } private static void customizeTableWriter(HTMLTableWriter writer, TableModel t, String[] toolTips) { writer.setTableAttributes("border='1'"); writer.setHeaderRenderer(new HTMLTableWriter.DefaultHTMLHeaderCellRenderer(toolTips)); for (int i = t.getColumnCount(); i-- > 0;) if (t.getColumnName(i).endsWith(" ")) writer.setSkipColumn(i, true); } // Override the inherited definition of this function with a no-op. protected void buildData() { } /** Store a parameter value if that named parameter doesn't already * have a value */ private void maybeWriteParam(String name, String value) { if (parameters.get(name) == null) parameters.put(name, value); } XYDataset xydata; /** Generate jpeg data for the plan-vs-actual time chart */ public void writeTimeChart() throws IOException { // Create the data for the chart to draw. xydata = evModel.getSchedule().getTimeChartData(); // possibly hide lines on the chart, at user request. boolean hidePlan = settings.getBool(CUSTOMIZE_HIDE_PLAN_LINE); boolean hideReplan = settings.getBool(CUSTOMIZE_HIDE_REPLAN_LINE); boolean hideForecast = settings.getBool(CUSTOMIZE_HIDE_FORECAST_LINE); if (hidePlan || hideReplan || hideForecast) xydata = new XYDatasetFilter(xydata).setSeriesHidden("Plan", hidePlan) .setSeriesHidden("Replan", hideReplan).setSeriesHidden("Forecast", hideForecast) .setSeriesHidden("Optimized_Forecast", hideForecast); // Alter the appearance of the chart. maybeWriteParam("title", resources.getString("Report.Time_Chart_Title")); super.writeContents(); } /** Generate jpeg data for the plan-vs-actual earned value chart */ public void writeValueChart() throws IOException { // Create the data for the chart to draw. xydata = evModel.getSchedule().getValueChartData(); // possibly hide lines on the chart, at user request. boolean hidePlan = settings.getBool(CUSTOMIZE_HIDE_PLAN_LINE); boolean hideReplan = settings.getBool(CUSTOMIZE_HIDE_REPLAN_LINE); boolean hideForecast = settings.getBool(CUSTOMIZE_HIDE_FORECAST_LINE); if (hidePlan || hideReplan || hideForecast) xydata = new XYDatasetFilter(xydata).setSeriesHidden("Plan", hidePlan) .setSeriesHidden("Replan", hideReplan).setSeriesHidden("Forecast", hideForecast) .setSeriesHidden("Optimized_Forecast", hideForecast); // Alter the appearance of the chart. maybeWriteParam("title", resources.getString("Report.EV_Chart_Title")); super.writeContents(); } public void writeCombinedChart() throws IOException { // Create the data for the chart to draw. EVSchedule s = getEvSchedule(settings.getEffectiveFilter(evModel)); xydata = s.getCombinedChartData(); // possibly hide lines on the chart, at user request. boolean hidePlan = settings.getBool(CUSTOMIZE_HIDE_PLAN_LINE); if (hidePlan) xydata = new XYDatasetFilter(xydata).setSeriesHidden("Plan_Value", hidePlan); // Alter the appearance of the chart. maybeWriteParam("title", "Cost & Schedule"); super.writeContents(); } /** Create a time series chart. */ public JFreeChart createChart() { JFreeChart chart = AbstractEVTimeSeriesChart.createEVReportChart(xydata); if (parameters.get("hideLegend") == null) chart.getLegend().setPosition(RectangleEdge.RIGHT); return chart; } protected void writeChartsPage() { String taskListDisplayName = EVTaskList.cleanupName(taskListName); String taskListHTML = HTMLUtils.escapeEntities(taskListDisplayName); String title = resources.format("Report.Charts_Title_FMT", taskListHTML); EVTaskFilter taskFilter = settings.getEffectiveFilter(evModel); boolean hideNames = settings.getBool(CUSTOMIZE_HIDE_NAMES); StringBuffer header = new StringBuffer(SIMPLE_HEADER_HTML); StringUtils.findAndReplace(header, TITLE_VAR, title); if (taskFilter != null) header.append(FILTER_HEADER_HTML); out.print(header); out.print("</head><body>"); out.print("<h1>"); out.print(title); out.print("</h1>"); printFilterInfo(out, taskFilter, false); EVSchedule s = getEvSchedule(taskFilter); String singleChartId = getParameter(SINGLE_CHART_PARAM); if (singleChartId == null) writeChartsGalleryPage(taskFilter, hideNames, s); else writeSingleChartPage(taskFilter, hideNames, s, singleChartId); out.print("</body></html>\n"); } private void writeChartsGalleryPage(EVTaskFilter taskFilter, boolean hideNames, EVSchedule s) { if (!isExporting()) { out.write("<p class='doNotPrint'><i>" + resources.getHTML("Report.Charts_More_Detail") + "</i></p>"); } // display all of the charts that are relevant to this task list. writeCharts(evModel, s, taskFilter, hideNames, 400, 300, ChartListPurpose.ReportAll, null, null); // add space to the bottom of the page so the chart tooltips don't // get truncated. out.print("<div style='height: 1in; clear:both'> </div>\n"); } protected void writeSingleChartPage(EVTaskFilter taskFilter, boolean hideNames, EVSchedule s, String singleChartId) { out.write("<div class='singleChartWrapper'>\n"); // display the single chart that was requested via query parameters out.write("<div class='singleChartHolder'>\n"); Map<String, String> chartHelp = new HashMap<String, String>(); List<ChartItem> allCharts = writeCharts(evModel, s, taskFilter, hideNames, 800, 500, ChartListPurpose.ReportAll, singleChartId, chartHelp); out.write("</div>\n"); // singleChartHolder // display a drop-down selector that can be used to select a // different chart to display out.write("<div class='singleChartSelector'><form>\n<b>"); out.write(resources.getHTML("Report.Select_Chart")); out.write("</b> <select id='chartSelector' name='chartSelector' " + "onchange='PdashEV.selectSingleChart(this)'>\n"); out.write("<option value='ALL'>" + resources.getHTML("Report.Show_Gallery") + "</option>\n"); out.write("<option value=''> </option>\n"); for (ChartItem chart : allCharts) { out.write("<option value='"); String chartId = getChartId(chart); out.write(HTMLUtils.escapeEntities(chartId)); if (chartId.equals(singleChartId)) out.write("' selected='selected"); out.write("'>"); out.write(HTMLUtils.escapeEntities(chart.name)); out.write("</option>\n"); } out.write("</select></form></div>\n"); // singleChartSelector out.write("</div>"); // singleChartWrapper // write out the help topic for the current chart, if one is found. String chartHelpUri = chartHelp.get(singleChartId); if (chartHelpUri != null) { try { String helpContent = getRequestAsString(chartHelpUri); helpContent = fixChartHelpContent(helpContent, chartHelpUri, chartHelp); out.write("<div style='clear:both'></div>\n"); out.write("<div class='singleChartHelp'>\n"); out.write(helpContent); out.write("</div>"); // singleChartHelp } catch (Exception e) { } } } private String fixChartHelpContent(String helpContent, String helpBaseUri, Map<String, String> chartHelp) { // discard headers and footers from the help content int cutStart = helpContent.indexOf("</h1>"); if (cutStart != -1) helpContent = helpContent.substring(cutStart + 5); int cutEnd = helpContent.lastIndexOf("</body"); if (cutEnd != -1) helpContent = helpContent.substring(0, cutEnd); // create a map of the chart help topics Map<String, String> chartUrls = new HashMap<String, String>(); for (Map.Entry<String, String> e : chartHelp.entrySet()) { String chartId = e.getKey(); String chartUrl = getChartDrillDownUrl(chartId); String helpUri = e.getValue(); String helpName = hrefFileName(helpUri); chartUrls.put(helpName, chartUrl); } // find and fix all the hrefs in this help topic: // * If any hrefs point to the help topic for a different chart, // rewrite the href so it actually loads the "drill-down page" // for that chart instead. // * For links that point to some non-chart help topic, rewrite the // href to be absolute (so the help-relative URI won't break) StringBuilder html = new StringBuilder(helpContent); int pos = 0; while (true) { // find the next href in the document. pos = html.indexOf("href=", pos); if (pos == -1) break; // no more hrefs to fix pos += 6; int beg = pos; // the first character of the href value itself char delim = html.charAt(beg - 1); int end = html.indexOf(String.valueOf(delim), beg); if (end == -1) continue; // invalid href syntax. Skip to the next one. // extract the href value String oneHref = html.substring(beg, end); // extract the final portion of the path name String oneName = hrefFileName(oneHref); // see if that name refers to one of the charts we can display String chartUrl = chartUrls.get(oneName); if (chartUrl != null) { // replace the href with a chart drill-down URL html.replace(beg, end, chartUrl); pos = beg + chartUrl.length(); } else { try { // make the URL absolute, and set a "target" attribute // so it will open in another window. URI base = new URI(helpBaseUri); URI target = base.resolve(oneHref); String newUri = target.toString(); html.replace(beg, end, newUri); html.insert(beg - 6, "target='evHelp' "); pos = beg + newUri.length() + 16; } catch (Exception e) { // problems resolving the URI? Turn the link into an // anchor so it can't be clicked on anymore. html.replace(beg - 6, beg - 2, "name"); } } } return html.toString(); } private String hrefFileName(String href) { int slashPos = href.lastIndexOf('/'); return href.substring(slashPos + 1); } protected List<ChartItem> writeCharts(EVTaskList evModel, EVSchedule schedule, EVTaskFilter filter, boolean hideNames, int width, int height, ChartListPurpose p, String singleChartId, Map<String, String> chartHelpMap) { DashboardContext ctx = getDashboardContext(); Object exportMarker = parameters.get("EXPORT"); boolean filterInEffect = (filter != null); boolean isRollup = (evModel instanceof EVTaskListRollup); List<ChartItem> chartList = TaskScheduleChartUtil.getChartsForTaskList(evModel.getID(), getDataRepository(), filterInEffect, isRollup, hideNames, p); for (Iterator i = chartList.iterator(); i.hasNext();) { ChartItem chart = (ChartItem) i.next(); if (chart == null) { i.remove(); continue; } try { SnippetWidget w = chart.snip.getWidget("view", null); if (w instanceof HtmlEvChart) { String chartId = getChartId(chart); if (chartHelpMap != null && w instanceof HelpAwareEvChart) { String helpUri = ((HelpAwareEvChart) w).getHelpUri(); if (StringUtils.hasValue(helpUri)) chartHelpMap.put(chartId, helpUri); } if (singleChartId != null && !singleChartId.equals(chartId)) continue; Map environment = TaskScheduleChartUtil.getEnvironment(evModel, schedule, filter, chart.snip, ctx); Map params = TaskScheduleChartUtil.getParameters(chart.settings); params.put("title", chart.name); params.put("width", Integer.toString(width)); params.put("height", Integer.toString(height)); params.put("noBorder", "t"); if (hideNames) params.put(CUSTOMIZE_HIDE_NAMES, "t"); if (exportMarker != null) params.put("EXPORT", exportMarker); else if (singleChartId == null) params.put("href", getChartDrillDownUrl(chartId)); // write the chart to an in-memory buffer. If any errors // occur, we will fall out to the exception handler. StringWriter buf = new StringWriter(); ((HtmlEvChart) w).writeChartAsHtml(buf, environment, params); // The generation of the chart was successful. Write out // the chart, surrounded by a DIV to control layout. out.write("<div class='evChartItem' style='width:" + width + "px; height:" + height + "px'>"); out.write(buf.toString()); out.write("</div>"); } else { i.remove(); } out.write(" "); } catch (Exception e) { logger.log(Level.SEVERE, "Unexpected error when displaying " + "EV snippet widget with id '" + chart.snip.getId() + "'", e); } } return chartList; } private String getChartDrillDownUrl(String chartId) { String result = "ev.class" + settings.getQueryString(EVReportSettings.PURPOSE_OTHER); result = HTMLUtils.appendQuery(result, CHARTS_PARAM); if (chartId != null) result = HTMLUtils.appendQuery(result, SINGLE_CHART_PARAM, chartId); return result; } private void writeChartOptions() { String taskListId = getParameter("tlid"); boolean isRollup = parameters.containsKey("isRollup"); List<ChartItem> chartList = TaskScheduleChartUtil.getChartsForTaskList(taskListId, getDataRepository(), false, isRollup, false, ChartListPurpose.ReportAll); Iterator<ChartItem> i = chartList.iterator(); out.write("<div id='chartOrderBlock'>\n"); out.write("<div>"); out.write(resources.getString("Report.Customize.Show_On_Report_HTML")); out.write(" <span class='chartOrderTooltip' title='"); out.write(resources.getHTML("Report.Customize.Charts_Hidden_Tooltip")); out.write("'>*</span></div>\n"); out.write("<div class='chartOrderItem standardChartItem'>"); out.write(resources.getHTML("Report.Customize.Standard_Chart_Name")); out.write("</div>\n"); out.write("<div id='chartOrderBlockShow'>\n"); while (i.hasNext()) { if (!writeChartOrderingItem(i.next())) break; } out.write("</div>\n"); // chartOrderBlockShow out.write("<div>" + resources.getString("Report.Customize.Show_On_More_Charts_HTML") + "<input type='hidden' name='chartOrder' " + "value='" + TaskScheduleChartSettings.SECONDARY_CHART_MARKER + "'></div>\n"); out.write("<div id='chartOrderBlockHide'>\n"); while (i.hasNext()) { writeChartOrderingItem(i.next()); } out.write("</div>\n"); // chartOrderBlockHide out.write("</div>\n"); // chartOrderBlock } private boolean writeChartOrderingItem(ChartItem chart) { if (chart == null) return false; try { SnippetWidget w = chart.snip.getWidget("view", null); if (w instanceof HtmlEvChart) { out.write("<div class='chartOrderItem'>"); out.write("<input type='hidden' name='chartOrder' value='"); out.write(HTMLUtils.escapeEntities(getChartId(chart))); out.write("'>"); out.write(HTMLUtils.escapeEntities(chart.name)); out.write("</div>\n"); } } catch (Exception e) { } return true; } private String getChartId(ChartItem chart) { if (chart.settings != null) return chart.settings.getSettingsIdentifier(); else return chart.snip.getId(); } private void saveChartOrderingPreference() { String taskListID = getParameter("tlid"); String[] chartOrder = (String[]) parameters.get("chartOrder_ALL"); if (taskListID != null && chartOrder != null) { TaskScheduleChartSettings.savePreferredChartOrdering(taskListID, Arrays.asList(chartOrder), getDataRepository()); } } public void writeTimeTable() { if (evModel != null) writeChartData(evModel.getSchedule().getTimeChartData(), 3); else writeFakeChartData("Plan", "Actual", "Forecast", null); } public void writeValueTable() { if (evModel != null) writeChartData(evModel.getSchedule().getValueChartData(), 3); else writeFakeChartData("Plan", "Actual", "Forecast", null); } public void writeValueTable2() { if (evModel != null) { EVSchedule s = evModel.getSchedule(); int maxSeries = 3; if (s instanceof EVScheduleRollup) maxSeries = 4; writeChartData(s.getValueChartData(), maxSeries); } else writeFakeChartData("Plan", "Actual", "Forecast", "Optimized"); } public void writeCombinedTable() { if (evModel != null) writeChartData(evModel.getSchedule().getCombinedChartData(), 3); else writeFakeChartData("Plan Value", "Actual Value", "Actual Time", null); } private void writeFakeChartData(String a, String b, String c, String d) { int maxSeries = (d == null ? 3 : 4); writeChartData(new FakeChartData(new String[] { a, b, c, d }), maxSeries); } /** Display excel-based data for drawing a chart */ protected void writeChartData(XYDataset xydata, int maxSeries) { // First, print the table header. out.print("<html><body><table border>\n"); int seriesCount = xydata.getSeriesCount(); if (seriesCount > maxSeries) seriesCount = maxSeries; if (parameters.get("nohdr") == null) { out.print("<tr><td>" + getResource("Schedule.Date_Label") + "</td>"); // print out the series names in the data source. for (int i = 0; i < seriesCount; i++) out.print("<td>" + AbstractEVChart.getNameForSeries(xydata, i) + "</td>"); // if the data source came up short, fill in default // column headers. if (seriesCount < 1) out.print("<td>" + getResource("Schedule.Plan_Label") + "</td>"); if (seriesCount < 2) out.print("<td>" + getResource("Schedule.Actual_Label") + "</td>"); if (seriesCount < 3) out.print("<td>" + getResource("Schedule.Forecast_Label") + "</td>"); if (seriesCount < 4 && maxSeries == 4) out.print("<td>" + getResource("Schedule.Optimized_Label") + "</td>"); out.println("</tr>"); } FastDateFormat f = FastDateFormat.getInstance("yyyy-MM-dd HH:mm"); for (int series = 0; series < seriesCount; series++) { int itemCount = xydata.getItemCount(series); for (int item = 0; item < itemCount; item++) { // print the date for the data item. out.print("<tr><td>"); out.print(f.format(new Date(xydata.getX(series, item).longValue()))); out.print("</td>"); // tab to the appropriate column for (int i = 0; i < series; i++) out.print("<td></td>"); // print out the Y value for the data item. out.print("<td>"); out.print(xydata.getYValue(series, item)); out.print("</td>"); // finish out the table row. for (int i = series + 1; i < seriesCount; i++) out.print("<td></td>"); out.println("</tr>"); } } if (seriesCount < maxSeries) { Date d = new Date(); if (seriesCount > 0) d = new Date(xydata.getX(0, 0).longValue()); StringBuffer s = new StringBuffer(); s.append("<tr><td>").append(f.format(d)).append("</td><td>"); if (seriesCount < 1) s.append("0"); s.append("</td><td>"); if (seriesCount < 2) s.append("0"); s.append("</td><td>"); if (seriesCount < 3) s.append("0"); if (maxSeries == 4) { s.append("</td><td>"); if (seriesCount < 4) s.append("0"); } s.append("</td></tr>\n"); out.print(s.toString()); out.print(s.toString()); } out.println("</table></body></html>"); } private String cleanupPercentComplete(String percent) { if (percent == null || percent.length() == 0) percent = "0"; else if (percent.endsWith("%")) percent = percent.substring(0, percent.length() - 1); return percent; } private static class EVCellRenderer extends HTMLTableWriter.DefaultHTMLTableCellRenderer { public String getInnerHtml(Object value, int row, int column) { if (value instanceof Date) value = EVSchedule.formatDate((Date) value); return super.getInnerHtml(value, row, column); } protected String getSortKey(Object value) { return null; } public String getAttributes(Object value, int row, int column) { return getSortAttribute(getSortKey(value)); } } private static class EVDateCellRenderer extends EVCellRenderer { protected String getSortKey(Object value) { return getDateSortKey(value); } } static String getDateSortKey(Object value) { if (value instanceof Date) return Long.toString(((Date) value).getTime()); else // The dates we display are typically completion dates. // if a date is missing, that implies not yet completed; // such dates should sort after all reasonable date values return "9999999999999"; } private static class EVPercentCellRenderer extends EVCellRenderer { protected String getSortKey(Object value) { if (value == null || "".equals(value)) return "0"; else return StringUtils.findAndReplace(value.toString(), "%", ""); } } private static class EVTimeCellRenderer extends EVCellRenderer { public String getAttributes(Object value, int r, int c) { return "class='timeFmt' " + super.getAttributes(value, r, c); } protected String getSortKey(Object value) { if (value == null || "".equals(value)) return "0"; else return value.toString().replace(':', '.'); } } static final EVCellRenderer EV_CELL_RENDERER = new EVCellRenderer(); static final EVTimeCellRenderer TIME_CELL_RENDERER = new EVTimeCellRenderer(); private static Map RENDERERS; static { Map r = new HashMap(); r.put(EVTaskList.COLUMN_FMT_OTHER, EV_CELL_RENDERER); r.put(EVTaskList.COLUMN_FMT_TIME, TIME_CELL_RENDERER); r.put(EVTaskList.COLUMN_FMT_DATE, new EVDateCellRenderer()); r.put(EVTaskList.COLUMN_FMT_PERCENT, new EVPercentCellRenderer()); RENDERERS = Collections.unmodifiableMap(r); } static class TaskNameWithTimingIconRenderer extends HTMLTableWriter.DefaultHTMLTableCellRenderer { public String getInnerHtml(Object value, int row, int column) { return super.getInnerHtml(value, row, column) + getTimingLink((String) value); } } static class NodeTypeCellRenderer extends EVCellRenderer { List phaseOrder; public NodeTypeCellRenderer(Set nodeTypeSpecs) { phaseOrder = new ArrayList(); phaseOrder.add(EVTask.MISSING_NODE_TYPE); phaseOrder.add(null); phaseOrder.add(""); phaseOrder.addAll(OrderedListMerger.merge(nodeTypeSpecs)); } protected String getSortKey(Object value) { return Integer.toString(phaseOrder.indexOf(value)); } } static class MilestoneCellRenderer implements HTMLTableWriter.CellRenderer { public String getInnerHtml(Object value, int row, int column) { if (value == null) return null; else return HTMLUtils.escapeEntities(value.toString()); } public String getAttributes(Object value, int row, int column) { if (value == null) return getSortAttribute("99999"); MilestoneList l = (MilestoneList) value; StringBuilder result = new StringBuilder(); result.append(getSortAttribute(Integer.toString(l.getMinSortOrdinal()))); if (l.isMissedMilestone()) { String errMsg = l.getMissedMilestoneMessage(); result.append(" class=\"behindSchedule\" title=\"").append(HTMLUtils.escapeEntities(errMsg)) .append("\""); } return result.toString(); } } static class DependencyCellRenderer implements HTMLTableWriter.CellRenderer { boolean plainText; boolean hideNames; public DependencyCellRenderer(boolean plainText, boolean hideNames) { this.plainText = plainText; this.hideNames = hideNames; } public String getInnerHtml(Object value, int row, int column) { TaskDependencyAnalyzer.HTML analyzer = new TaskDependencyAnalyzer.HTML(value, hideNames); int status = analyzer.getStatus(); if (status == TaskDependencyAnalyzer.NO_DEPENDENCIES) return null; else if (plainText) return analyzer.getRes("Text"); StringBuffer result = new StringBuffer(); result.append("<a style='text-decoration: none' href='#'" + " onclick='togglePopupInfo(this); return false;'>"); result.append(analyzer.getHtmlIndicator()); result.append("</a><div class='popupInfo'>"); result.append(analyzer.getHtmlTable("onclick='togglePopupInfo(this.parentNode)'")); result.append("</div>"); return result.toString(); } public String getAttributes(Object value, int row, int column) { TaskDependencyAnalyzer.HTML analyzer = new TaskDependencyAnalyzer.HTML(value, hideNames); return "style='text-align:center' " + getSortAttribute(analyzer.getSortKey()); } } static String getSortAttribute(String sortKey) { if (sortKey == null) return null; else return SORTKEY_ATTRIBUTE + "='" + HTMLUtils.escapeEntities(sortKey) + "'"; } private static final String SORTKEY_ATTRIBUTE = "sortkey"; /** encode a snippet of text with appropriate HTML entities */ final static String encodeHTML(String text) { if (text == null) return ""; else return HTMLUtils.escapeEntities(text); } final static String getResource(String key) { return encodeHTML(resources.getString(key)).replace('\n', ' '); } static String getTimingLink(String path) { if (path == null || path.length() == 0) return ""; return " <a class=doNotPrint href=\"" + WebServer.urlEncodePath(path) + "//control/setPath.class?start\"><img border=\"0\" title=\"" + resources.getHTML("Start_timing") + "\" src=\"/control/startTiming.png\"></A>"; } private class FakeChartData extends AbstractXYDataset { private String[] seriesNames; public FakeChartData(String[] seriesNames) { this.seriesNames = seriesNames; } @Override public int getSeriesCount() { return 4; } @Override public String getSeriesKey(int series) { return seriesNames[series]; } public int getItemCount(int series) { return 2; } public Number getX(int series, int item) { if (item == 0) return new Integer(0); else return new Integer(24 * 60 * 60 * 1000); } public Number getY(int series, int item) { if (item == 0) return new Integer(0); else switch (series) { case 0: return new Integer(100); case 1: return new Integer(60); case 2: return new Integer(30); default: return new Integer(5); } } } }