// 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.htmlmonitor; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.text.DecimalFormat; import java.util.HashSet; 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 client id. In the provided base log * directory, it creates a subdirectory with the client id. In this directory it creates * appropriate log files. The name of each finished log file starts with the "htmlmonitor_" * 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 HtmlMonitorOutputWriter implements HtmlMonitorComponent, HtmlMonitorMessageListener { /** * the maximum size of an individual log file */ private static final int MAXIMUM_LOG_FILE_SIZE = 50000000; /** * 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 web application used by the client */ private String webAppId; /** * the id of the client of which all messages are logged through this writer */ private String clientId; /** * 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 GUI elements, that were already logged and need therefore not be logged again into * the same file */ private Set loggedGUIElements = new HashSet(); /** *

* initializes the writer with the log file base directory and the id of the client for which * this writer logs the messages. *

* * @param logFileBaseDir the log file base directory, or null if the default directory shall * be taken * @param webAppId the ID of the web application used by the client * @param clientId the ID of the client, for which this writer logs */ public HtmlMonitorOutputWriter(String logFileBaseDir, String webAppId, String clientId) { if (logFileBaseDir == null) { this.logFileBaseDir = new File(DEFAULT_LOG_FILE_BASE_DIR); } else { this.logFileBaseDir = new File(logFileBaseDir); } this.webAppId = webAppId; this.clientId = clientId; lastUpdate = System.currentTimeMillis(); } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#init() */ @Override public synchronized void init() throws HtmlMonitorException { if (outputWriter != null) { throw new IllegalStateException("already initialized. Call close() first"); } synchronized (HtmlMonitorOutputWriter.class) { try { File clientLogDir = new File(logFileBaseDir, webAppId); clientLogDir = new File(clientLogDir, clientId); if (!clientLogDir.exists()) { if (!clientLogDir.mkdirs()) { throw new HtmlMonitorException("client log file directory " + clientLogDir + " can not be created"); } } else if (!clientLogDir.isDirectory()) { throw new HtmlMonitorException("client log file directory " + clientLogDir + " already exists as a file"); } handleOldLogFiles(new File(logFileBaseDir, clientId), clientLogDir); logFile = new File(clientLogDir, getLogFileName(-1)); if (logFile.exists()) { rotateLogFile(); } createLogWriter(); } catch (IOException e) { throw new HtmlMonitorException("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. "htmlmonitor_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 = "htmlmonitor_" + clientId; if (index >= 0) { result += "_" + new DecimalFormat("000" ).format(index); } result += ".log"; return result; } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#start() */ @Override public synchronized void start() throws IllegalStateException, HtmlMonitorException { lastUpdate = System.currentTimeMillis(); } /* (non-Javadoc) * @see HtmlMonitorMessageListener#handleMessage(HtmlClientInfos, HtmlEvent[]) */ @Override public synchronized void handleMessage(HtmlClientInfos clientInfos, HtmlGUIElement guiStructure, HtmlEvent[] events) { if (outputWriter == null) { throw new IllegalStateException("not initialized. Call init() first"); } if (guiStructure != null) { dumpGuiStructure(guiStructure); } for (HtmlEvent event : events) { dumpEvent(event); } outputWriter.flush(); try { considerLogRotate(); } catch (IOException e) { throw new IllegalStateException("could not perform log rotation: " + e, e); } lastUpdate = System.currentTimeMillis(); } /** *

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

* * @param guiStructure the GUI structure to be logged */ private void dumpGuiStructure(HtmlGUIElement guiStructure) { if (!loggedGUIElements.contains(guiStructure)) { outputWriter.print(""); if (guiStructure instanceof HtmlServer) { dumpParam("host", ((HtmlServer) guiStructure).getName()); dumpParam("port", ((HtmlServer) guiStructure).getPort()); } else if (guiStructure instanceof HtmlDocument) { dumpParam("path", ((HtmlDocument) guiStructure).getPath()); dumpParam("query", ((HtmlDocument) guiStructure).getQuery()); dumpParam("title", ((HtmlDocument) guiStructure).getTitle()); } else if (guiStructure instanceof HtmlPageElement) { dumpParam("tagname", ((HtmlPageElement) guiStructure).getTagName()); dumpParam("htmlid", ((HtmlPageElement) guiStructure).getHtmlId()); dumpParam("index", ((HtmlPageElement) guiStructure).getIndex()); } dumpParam("parent", guiStructure.getParentId()); outputWriter.println(""); loggedGUIElements.add(guiStructure); } if (guiStructure.getChildren() != null) { for (HtmlGUIElement child : guiStructure.getChildren()) { dumpGuiStructure(child); } } } /** *

* 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(HtmlEvent event) { outputWriter.print(""); if (event.getCoordinates() != null) { dumpParam("X", event.getCoordinates()[0]); dumpParam("Y", event.getCoordinates()[1]); } dumpParam("key", event.getKey()); if (event.getScrollPosition() != null) { dumpParam("scrollX", event.getScrollPosition()[0]); dumpParam("scrollY", event.getScrollPosition()[1]); } dumpParam("selectedValue", event.getSelectedValue()); if (event.getTarget() != null) { dumpParam("target", event.getTarget().getId()); } else { dumpParam("targetDocument", event.getTargetDocument().getId()); dumpParam("targetDOMPath", event.getTargetDOMPath()); } dumpParam("timestamp", event.getTime()); outputWriter.println(""); } /** *

* 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 exeeded 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 opend 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.println(""); loggedGUIElements.clear(); } /** *

* closed the current writer if it is open. *

*/ private void closeLogWriter() { if (outputWriter != null) { outputWriter.println("
"); outputWriter.flush(); outputWriter.close(); outputWriter = null; } } /** *

* this method moves old logfiles of the same client resisting in the wrong old directory * structure to the new one. *

* * @param oldLogDir the old log directory * @param newLogDir the new log directory */ private void handleOldLogFiles(File oldLogDir, File newLogDir) { if (oldLogDir.exists() && oldLogDir.isDirectory()) { boolean allFilesRenamed = true; for (File oldLogFile : oldLogDir.listFiles()) { allFilesRenamed &= oldLogFile.renameTo(new File(newLogDir, oldLogFile.getName())); } if (allFilesRenamed) { if (!oldLogDir.delete()) { Console.printerrln("could not move old file directory structure to new one"); } } } } /* (non-Javadoc) * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#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; } }