/** * Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.integration.tool.errorreport; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.threeten.bp.Instant; import org.threeten.bp.LocalDateTime; import org.threeten.bp.ZoneOffset; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.integration.tool.GUIFeedback; import com.opengamma.util.ArgumentChecker; /** * Gathers up all of the logs from all installed locations and prepares a ZIP file that can be submitted to the OpenGamma developers as part of a bug report. */ public class BundleErrorReportInfo implements Runnable { private static final Logger s_logger = LoggerFactory.getLogger(BundleErrorReportInfo.class); private static final String[] s_subdirs = new String[] {"Downloads" }; private static String s_userHome; protected static void setUserHome(final String userHome) { s_userHome = userHome; } private final GUIFeedback _feedback; private final String[] _reports; private int _uniqueFile; private ZipOutputStream _zip; /** * Reads the contents of a file. Any lines are trimmed of leading/trailing whitespace. Any lines starting with # are skipped and any blank lines ignored. * * @param pathToFile the file to read, not null * @return the contents of the file, not null */ private static String[] readFile(final String pathToFile) { try (final BufferedReader reader = new BufferedReader(new FileReader(pathToFile))) { final List<String> lines = new ArrayList<String>(); String line; while ((line = reader.readLine()) != null) { line = line.trim(); if (!line.startsWith("#")) { lines.add(line); } } return lines.toArray(new String[lines.size()]); } catch (IOException e) { throw new OpenGammaRuntimeException("Couldn't read properties file - " + pathToFile, e); } } protected BundleErrorReportInfo(final GUIFeedback feedback, final String pathToProperties) { this(feedback, readFile(pathToProperties)); } protected BundleErrorReportInfo(final GUIFeedback feedback, final String[] properties) { _feedback = ArgumentChecker.notNull(feedback, "feedback"); _reports = ArgumentChecker.notNull(properties, "properties"); } /** * Find a preferred sub-directory under the user's home folder. We use the home folder as it is most likely writeable, but most systems have sub-folders called things like "Downloads" or * "Downloaded Files" that the user might prefer things end up in. * * @param path path the check, not null * @return the updated path, or the original if no candidate sub-folder is found */ private String preferredSubFolder(final String path) { final File file = new File(path); if (file.isDirectory()) { for (String subdir : s_subdirs) { final File sd = new File(file, subdir); if (sd.isDirectory()) { return path + File.separator + subdir; } } } return path; } /** * Opens the ZIP output stream ready for each "report" entry to be added. * * @return the path that will be written to, not null */ protected String openReportOutput() { final String home = preferredSubFolder((s_userHome != null) ? s_userHome : System.getProperty("user.home")); final LocalDateTime ldt = Instant.now().atZone(ZoneOffset.systemDefault()).toLocalDateTime(); final String path = String.format("%s%c%s-%04d-%02d-%02d-%02d-%02d-%02d.zip", home, File.separatorChar, "OpenGamma-ErrorReport", ldt.getYear(), ldt.getMonthValue(), ldt.getDayOfMonth(), ldt.getHour(), ldt.getMinute(), ldt.getSecond()).toString(); s_logger.info("Writing {}", path); try { _zip = new ZipOutputStream(new FileOutputStream(path)); } catch (IOException e) { throw new OpenGammaRuntimeException("Couldn't write to " + path, e); } return path; } private String createUniqueName(final String name) { return (++_uniqueFile) + "-" + name; } /** * Copies a file from the local file system to the ZIP output. * * @param source the file to copy from, not null * @param name the name of the entry, not null */ protected void attachFile(final File source, final String name) { try { final ZipEntry ze = new ZipEntry(name); _zip.putNextEntry(ze); final byte[] buffer = new byte[4096]; try (final FileInputStream in = new FileInputStream(source)) { int bytes; while ((bytes = in.read(buffer)) > 0) { _zip.write(buffer, 0, bytes); } } _zip.closeEntry(); } catch (IOException e) { throw new OpenGammaRuntimeException("Couldn't write " + name + " to ZIP file", e); } } /** * Creates a regex pattern that corresponds to wild cards written with * and ? notation. * * @param name the * and ? based pattern * @return the regex pattern */ private Pattern fileNameMatch(final String name) { final StringBuilder sb = new StringBuilder(name.length()); for (int i = 0; i < name.length(); i++) { final char c = name.charAt(i); if (c == '.') { sb.append('\\').append('.'); } else if (c == '?') { sb.append('.'); } else if (c == '*') { sb.append('.').append('*').append('?'); } else { sb.append(c); } } return Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE); } private int attachFiles(final File root, final String path) { final int i = path.indexOf(File.separatorChar); final Pattern match; final String tail; if (i < 0) { match = fileNameMatch(path); tail = null; } else { match = fileNameMatch(path.substring(0, i)); int tailStart = i; do { tailStart++; if (tailStart >= path.length()) { return 0; } } while (path.charAt(tailStart) == File.separatorChar); tail = path.substring(tailStart); } int count = 0; final String[] files = root.list(); if (files != null) { Arrays.sort(files); for (String file : files) { final Matcher m = match.matcher(file); if (m.matches()) { s_logger.trace("Entry {} matched by path", file); final File entry = new File(root, file); if (tail != null) { if (entry.isDirectory()) { count += attachFiles(entry, tail); } } else { if (entry.isFile()) { s_logger.info("Attaching {}", entry); attachFile(entry, createUniqueName(file)); count++; } } } } } return count; } /** * Writes an entry of the form "AttachFiles=<path>". * * @param path the path, including * and ? characters as wildcards * @return the number of files written to the ZIP file */ private int attachFiles(String path) { if (path.startsWith("%TEMP%")) { final String tmpdir = System.getProperty("java.io.tmpdir"); s_logger.trace("Substituting " + tmpdir + " for %TEMP%"); path = tmpdir + path.substring(6); } s_logger.debug("Attaching {}", path); final int i = path.indexOf(File.separatorChar); if (i < 0) { throw new IllegalArgumentException("File path '" + path + "' is not a valid absolute path"); } final File root = new File(path.substring(0, i + 1)); return attachFiles(root, path.substring(i + 1)); } /** * Writes an entry of the form "X=Y", dispatching the call based on the value of X. * * @param report the line from the configuration file, not null * @return the number of files written to the ZIP file */ private int writeReport(String report) { s_logger.debug("Writing {}", report); final int i = report.indexOf('='); if (i < 0) { s_logger.warn("Error in configuration - {}", report); return 0; } final String key = report.substring(0, i).trim(); final String value = report.substring(i + 1).trim(); s_logger.trace("Key = \"{}\", Value = \"{}\"", key, value); if ("AttachFiles".equalsIgnoreCase(key)) { return attachFiles(value); } else { s_logger.warn("Unrecognised option - {}", key); return 0; } } /** * Closes the file opened by {@link #openReportOutput()}. */ protected void closeReportOutput() { try { _zip.close(); } catch (IOException e) { throw new OpenGammaRuntimeException("Couldn't write to ZIP file", e); } } @Override public void run() { _feedback.workRequired(_reports.length + 2); final String pathToReport = openReportOutput(); _feedback.workCompleted(1); int reportCount = 0; for (String report : _reports) { reportCount += writeReport(report); _feedback.workCompleted(1); } closeReportOutput(); _feedback.workCompleted(1); _feedback.done(reportCount + " log(s) written to " + pathToReport); } /** * Logical program entry point. * * @param args the command line arguments, not null * @return the exit code */ protected static int mainImpl(final String[] args) { final GUIFeedback feedback = new GUIFeedback("Packaging error logs for submission"); try { if (args.length != 1) { throw new IllegalArgumentException("Invalid number of arguments - expected path to property file"); } (new BundleErrorReportInfo(feedback, args[0])).run(); return 0; } catch (Throwable t) { s_logger.error("Caught exception", t); feedback.shout((t.getMessage() != null) ? t.getMessage() : "Couldn't package error logs"); return 1; } } /** * Program entry point. The logical program entry point is called and {@link System#exit} called with the exit code. * * @param args the command line arguments, not null */ public static void main(final String[] args) { System.exit(mainImpl(args)); } }