// Copyright 2012 Georg-August-Universität Göttingen, Germany // // 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 de.ugoe.cs.autoquest.genericeventmonitor; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.text.DecimalFormat; import java.util.HashSet; import java.util.Map; import java.util.Set; import de.ugoe.cs.util.StringTools; import de.ugoe.cs.util.console.Console; /** *

* dumps messages to a log file belonging to a specific app and client id. In the provided base log * directory, it creates a subdirectory for the app and a subsubdirectory for the client id. In this * subsubdirectory it creates appropriate log files. The name of each finished log file starts with * "genericevents_" followed by the client id and an index of the log file. An unfinished log file * has no index yet. A log file is finished if *

*

* * @author Patrick Harms * @version 1.0 * */ public class OutputWriter implements GenericEventMonitorComponent, GenericEventMonitorMessageListener { /** * the maximum size of an individual log file */ private static final int MAXIMUM_LOG_FILE_SIZE = 100000000; /** * the default log base directory if none is provided through the constructor */ private static final String DEFAULT_LOG_FILE_BASE_DIR = "logs"; /** * the currently used log file base directory */ private File logFileBaseDir; /** * the id of the client of which all messages are logged through this writer */ private String clientId; /** * the id of the application that the client uses */ private String appId; /** * the log file into which all messages are currently written */ private File logFile; /** * an output writer to be used for writing into the log file */ private PrintWriter outputWriter; /** * the time stamp of the last action taken on this writer (such as logging a message) */ private long lastUpdate; /** * the id of the targets, that were already logged and need therefore not be logged again into * the same file */ private Set loggedTargets = new HashSet<>(); /** *

* initializes the writer with the log file base directory, the id of the client for which * this writer logs the messages and the id of the app that is used by the client. *

* * @param logFileBaseDir the log file base directory, or null if the default directory shall * be taken * @param appId the ID of the app used by the client * @param clientId the ID of the client, for which this writer logs */ public OutputWriter(String logFileBaseDir, String appId, String clientId) { if (logFileBaseDir == null) { this.logFileBaseDir = new File(DEFAULT_LOG_FILE_BASE_DIR); } else { this.logFileBaseDir = new File(logFileBaseDir); } this.appId = appId; this.clientId = clientId; lastUpdate = System.currentTimeMillis(); } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.genericeventmonitor.GenericEventMonitorComponent#init() */ @Override public synchronized void init() throws GenericEventMonitorException { if (outputWriter != null) { throw new IllegalStateException("already initialized. Call close() first"); } synchronized (OutputWriter.class) { try { File clientLogDir = new File(new File(logFileBaseDir, appId), clientId); if (!clientLogDir.exists()) { if (!clientLogDir.mkdirs()) { throw new GenericEventMonitorException("client log file directory " + clientLogDir + " cannot be created"); } } else if (!clientLogDir.isDirectory()) { throw new GenericEventMonitorException("client log file directory " + clientLogDir + " already exists as a file"); } logFile = new File(clientLogDir, getLogFileName(-1)); if (logFile.exists()) { rotateLogFile(); } createLogWriter(); } catch (IOException e) { throw new GenericEventMonitorException("could not open logfile " + logFile, e); } } lastUpdate = System.currentTimeMillis(); } /** *

* used to calculate a log file name. If the provided index is smaller 0, then no index * is added to the file name. A filename is e.g. "genericevents_12345_001.log". *

* * @param index the index of the log file or -1 one, if no index shall be added * * @return the file name as described */ private String getLogFileName(int index) { String result = "genericevents_" + clientId; if (index >= 0) { result += "_" + new DecimalFormat("000" ).format(index); } result += ".log"; return result; } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.genericeventmonitor.GenericEventMonitorComponent#start() */ @Override public synchronized void start() throws IllegalStateException, GenericEventMonitorException { lastUpdate = System.currentTimeMillis(); } /* (non-Javadoc) * @see GenericEventMonitorMessageListener#handleEvents(ClientInfos, GenericEvent[]) */ @Override public void handleEvents(ClientInfos clientInfos, GenericEvent[] events) { if (outputWriter == null) { throw new IllegalStateException("not initialized. Call init() first"); } for (GenericEvent event : events) { dumpEvent(event); } outputWriter.flush(); try { considerLogRotate(); } catch (IOException e) { throw new IllegalStateException("could not perform log rotation: " + e, e); } lastUpdate = System.currentTimeMillis(); } /** *

* formats a received event and writes it to the log file. One event results in one line * in the log file containing all infos of the event. *

* * @param event to be written to the log file */ private void dumpEvent(GenericEvent event) { if (event.getTarget() != null) { ensureGuiElementDumped(event.getTarget()); } outputWriter.print(""); dumpParam("timestamp", event.getTime()); dumpParam("targetId", event.getTarget().getId()); for (Map.Entry parameter : event.getParameters().entrySet()) { dumpParam(parameter.getKey(), parameter.getValue()); } outputWriter.println(""); } /** *

* ensures that a target element and its corresponding target element tree are logged *

* * @param target the GUI structure to be logged */ private void ensureGuiElementDumped(GenericEventTarget target) { if (!loggedTargets.contains(target.getId())) { GenericEventTarget parent = target; // get the root while (parent.getParent() != null) { parent = parent.getParent(); } dumpTargetStructure(parent); } } /** *

* dumps the target provided by the parameter into the log file. Calls itself * recursively to traverse the target structure. *

* * @param target the target structure to be logged */ private void dumpTargetStructure(GenericEventTarget target) { dumpTarget(target); if (target.getChildren() != null) { for (GenericEventTarget child : target.getChildren()) { dumpTargetStructure(child); } } } /** *

* dumps the target provided by the parameter into the log file. *

* * @param guiElement the GUI element to be logged */ private void dumpTarget(GenericEventTarget target) { if (!loggedTargets.contains(target.getId())) { outputWriter.print(""); if (target.getParent() != null) { dumpParam("parent", target.getParent().getId()); } for (Map.Entry parameter : target.getParameters().entrySet()) { dumpParam(parameter.getKey(), parameter.getValue()); } outputWriter.println(""); loggedTargets.add(target.getId()); } } /** *

* dumps a parameter with the given name and value to the log file. The result is a * tag named param with a name attribute and a value attribute. The value is transformed * to a String if it is no String already. Furthermore, an XML entity replacement is performed * if required. *

* * @param name the name of the parameter to be dumped * @param value the value of the parameter to be dumped */ private void dumpParam(String name, Object value) { if (value == null) { return; } String val; if (value instanceof String) { val = (String) value; } else { val = String.valueOf(value); } outputWriter.print(" "); } /** *

* checks, if the log file exceeded the {@link #MAXIMUM_LOG_FILE_SIZE}. If so, the current * log file is closed, the next log file name is determined and this new file is opened for * writing. *

*/ private synchronized void considerLogRotate() throws IOException { if (logFile.length() > MAXIMUM_LOG_FILE_SIZE) { closeLogWriter(); rotateLogFile(); createLogWriter(); } } /** *

* renames the current log file to a new log file with the next available index. It further * sets the current log file to the default name, i.e. without index. *

*/ private void rotateLogFile() { File clientLogDir = logFile.getParentFile(); File checkFile; int logFileIndex = -1; do { logFileIndex++; checkFile = new File(clientLogDir, getLogFileName(logFileIndex)); } while (checkFile.exists()); if (!logFile.renameTo(checkFile)) { Console.printerrln("could not rename log file " + logFile + " to " + checkFile + ". Will not perform log rotation."); } else { logFileIndex++; logFile = new File(clientLogDir, getLogFileName(-1)); } } /** *

* instantiates a writer to be used for writing messages into the log file. *

*/ private void createLogWriter() throws IOException { FileOutputStream fis = new FileOutputStream(logFile); outputWriter = new PrintWriter(new OutputStreamWriter(fis, "UTF-8")); outputWriter.println(""); outputWriter.print(""); loggedTargets.clear(); } /** *

* parses the POM properties if accessible and returns them. If they are not available, * unknown is returned. *

*/ private String getMonitorVersion() { InputStream properties = this.getClass().getClassLoader().getResourceAsStream ("META-INF/maven/de.ugoe.cs.autoquest/autoquest-generic-event-monitor/pom.properties"); if (properties != null) { try { StringBuffer result = new StringBuffer(); BufferedReader reader = new BufferedReader(new InputStreamReader(properties)); String line = null; do { line = reader.readLine(); if (line != null) { if (result.length() > 0) { result.append(" "); } result.append(line); } } while (line != null); reader.close(); return result.toString(); } catch (IOException e) { return "unknown"; } } else { return "unknown"; } } /** *

* closed the current writer if it is open. *

*/ private void closeLogWriter() { if (outputWriter != null) { outputWriter.println("
"); outputWriter.flush(); outputWriter.close(); outputWriter = null; } } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.genericeventmonitor.GenericEventMonitorComponent#stop() */ @Override public synchronized void stop() { closeLogWriter(); rotateLogFile(); lastUpdate = System.currentTimeMillis(); } /** *

* return the time stamp of the last activity that happened on this writer. *

* * @return as described */ public long getLastUpdate() { return lastUpdate; } }