001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.junit.viewer.server;
017:
018: import com.google.gwt.junit.client.Benchmark;
019: import com.google.gwt.junit.viewer.client.Report;
020: import com.google.gwt.junit.viewer.client.ReportSummary;
021:
022: import org.w3c.dom.Document;
023:
024: import java.io.File;
025: import java.io.FilenameFilter;
026: import java.util.ArrayList;
027: import java.util.HashMap;
028: import java.util.Iterator;
029: import java.util.List;
030: import java.util.Map;
031:
032: import javax.xml.parsers.DocumentBuilder;
033: import javax.xml.parsers.DocumentBuilderFactory;
034:
035: /**
036: * Serves up benchmark reports created during JUnit execution.
037: *
038: * The benchmark reports are read from the path specified by the system property
039: * named <code>Benchmark.REPORT_PATH</code>. In the case the property is not
040: * set, they are read from the user's current working directory.
041: */
042: public class ReportDatabase {
043:
044: /**
045: * Indicates that a supplied path was invalid.
046: *
047: */
048: public static class BadPathException extends RuntimeException {
049: String path;
050:
051: public BadPathException(String path) {
052: super ("The path " + path + " does not exist.");
053: this .path = path;
054: }
055:
056: public String getPath() {
057: return path;
058: }
059: }
060:
061: private static class ReportEntry {
062: private ReportSummary summary;
063: private Report report;
064: private long lastModified;
065:
066: public ReportEntry(Report report, ReportSummary summary,
067: long lastModified) {
068: this .report = report;
069: this .summary = summary;
070: this .lastModified = lastModified;
071: }
072: }
073:
074: private static class ReportFile {
075: File file;
076: long lastModified;
077:
078: ReportFile(File f) {
079: this .file = f;
080: this .lastModified = f.lastModified();
081: }
082: }
083:
084: /**
085: * The amount of time to go between report updates.
086: */
087: private static final int UPDATE_DURATION_MILLIS = 30000;
088:
089: private static ReportDatabase database = new ReportDatabase();
090:
091: public static ReportDatabase getInstance() {
092: return database;
093: }
094:
095: private static String getReportId(File f) {
096: return f.getName();
097: }
098:
099: /**
100: * A list of all reports by id.
101: */
102: private Map/* <String,ReportEntry> */reports = new HashMap/* <String,ReportEntry> */();
103:
104: /**
105: * The last time we updated our reports.
106: */
107: private long lastUpdateMillis = -1L;
108:
109: /**
110: * Lock for updating from file system. (Guarantees a single update while not
111: * holding reportsLock open).
112: */
113: private Object updateLock = new Object();
114:
115: /**
116: * Are we currently undergoing updating?
117: */
118: private boolean updating = false;
119:
120: /**
121: * Lock for reports.
122: */
123: private Object reportsLock = new Object();
124:
125: /**
126: * The path to read benchmark reports from.
127: */
128: private final String reportPath;
129:
130: private ReportDatabase() throws BadPathException {
131: String path = System.getProperty(Benchmark.REPORT_PATH);
132: if (path == null || path.trim().equals("")) {
133: path = System.getProperty("user.dir");
134: }
135: reportPath = path;
136:
137: if (!new File(reportPath).exists()) {
138: throw new BadPathException(reportPath);
139: }
140: }
141:
142: public Report getReport(String reportId) {
143: synchronized (reportsLock) {
144: ReportEntry entry = (ReportEntry) reports.get(reportId);
145: return entry == null ? null : entry.report;
146: }
147: }
148:
149: public List/* <ReportSummary> */getReportSummaries() {
150:
151: /**
152: * There are probably ways to make this faster, but I've taken basic
153: * precautions to try to make this scale ok with multiple clients.
154: */
155:
156: boolean update = false;
157:
158: // See if we need to do an update
159: // Go ahead and let others continue reading, even if an update is required.
160: synchronized (updateLock) {
161: if (!updating) {
162: long currentTime = System.currentTimeMillis();
163:
164: if (currentTime > lastUpdateMillis
165: + UPDATE_DURATION_MILLIS) {
166: update = updating = true;
167: }
168: }
169: }
170:
171: if (update) {
172: updateReports();
173: }
174:
175: synchronized (reportsLock) {
176: List/* <ReportSummary> */summaries = new ArrayList/* <ReportSummary> */(
177: reports.size());
178: for (Iterator it = reports.values().iterator(); it
179: .hasNext();) {
180: ReportEntry entry = (ReportEntry) it.next();
181: summaries.add(entry.summary);
182: }
183: return summaries;
184: }
185: }
186:
187: private void updateReports() {
188:
189: File path = new File(reportPath);
190:
191: File[] files = path.listFiles(new FilenameFilter() {
192: public boolean accept(File f, String name) {
193: return name.startsWith("report-")
194: && name.endsWith(".xml");
195: }
196: });
197:
198: Map filesToUpdate = new HashMap();
199: Map filesById = new HashMap();
200: for (int i = 0; i < files.length; ++i) {
201: File f = files[i];
202: filesById.put(getReportId(f), new ReportFile(f));
203: }
204:
205: // Lock temporarily so we can determine what needs updating
206: // (This could be a read-lock - not a general read-write lock,
207: // if we moved dead report removal outside of this critical section).
208: synchronized (reportsLock) {
209:
210: // Add reports which need to be updated or are new
211: for (int i = 0; i < files.length; ++i) {
212: File file = files[i];
213: String reportId = getReportId(file);
214: ReportEntry entry = (ReportEntry) reports.get(reportId);
215: if (entry == null
216: || entry.lastModified < file.lastModified()) {
217: filesToUpdate.put(reportId, null);
218: }
219: }
220:
221: // Remove reports which no longer exist
222: for (Iterator it = reports.keySet().iterator(); it
223: .hasNext();) {
224: String id = (String) it.next();
225: if (filesById.get(id) == null) {
226: it.remove();
227: }
228: }
229: }
230:
231: try {
232: DocumentBuilderFactory factory = DocumentBuilderFactory
233: .newInstance();
234: factory.setIgnoringElementContentWhitespace(true);
235: factory.setIgnoringComments(true);
236: DocumentBuilder builder = factory.newDocumentBuilder();
237:
238: for (Iterator it = filesToUpdate.keySet().iterator(); it
239: .hasNext();) {
240: String id = (String) it.next();
241: ReportFile reportFile = (ReportFile) filesById.get(id);
242: String filePath = reportFile.file.getAbsolutePath();
243: Document doc = builder.parse(filePath);
244: Report report = ReportXml.fromXml(doc
245: .getDocumentElement());
246: report.setId(id);
247: ReportSummary summary = report.getSummary();
248: long lastModified = new File(filePath).lastModified();
249: filesToUpdate.put(id, new ReportEntry(report, summary,
250: lastModified));
251: }
252:
253: // Update the reports
254: synchronized (reportsLock) {
255: for (Iterator it = filesToUpdate.keySet().iterator(); it
256: .hasNext();) {
257: String id = (String) it.next();
258: reports.put(id, filesToUpdate.get(id));
259: }
260: }
261: } catch (Exception e) {
262: // Even if we got an error, we'll just try again on the next update
263: // This might happen if a report has only been partially written, for
264: // example.
265: e.printStackTrace();
266: }
267:
268: synchronized (updateLock) {
269: updating = false;
270: lastUpdateMillis = System.currentTimeMillis();
271: }
272: }
273: }
|