source: trunk/autoquest-htmlmonitor/src/main/java/de/ugoe/cs/autoquest/htmlmonitor/HtmlMonitorOutputWriter.java @ 1227

Last change on this file since 1227 was 1227, checked in by pharms, 11 years ago
  • changed log file directory structure for better distinguishing the monitored applications
  • extended session timeout to potentially reduce log file sizes
  • increased log file maximum sizes to reduce storage consumption due do GUI model duplication
File size: 14.4 KB
RevLine 
[927]1//   Copyright 2012 Georg-August-Universität Göttingen, Germany
2//
3//   Licensed under the Apache License, Version 2.0 (the "License");
4//   you may not use this file except in compliance with the License.
5//   You may obtain a copy of the License at
6//
7//       http://www.apache.org/licenses/LICENSE-2.0
8//
9//   Unless required by applicable law or agreed to in writing, software
10//   distributed under the License is distributed on an "AS IS" BASIS,
11//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//   See the License for the specific language governing permissions and
13//   limitations under the License.
[857]14
15package de.ugoe.cs.autoquest.htmlmonitor;
16
17import java.io.File;
18import java.io.FileOutputStream;
19import java.io.IOException;
20import java.io.OutputStreamWriter;
21import java.io.PrintWriter;
22import java.text.DecimalFormat;
[1089]23import java.util.HashSet;
24import java.util.Set;
[857]25
[1019]26import de.ugoe.cs.util.StringTools;
[872]27import de.ugoe.cs.util.console.Console;
28
[857]29/**
30 * <p>
[871]31 * dumps messages to a log file belonging to a specific client id. In the provided base log
32 * directory, it creates a subdirectory with the client id. In this directory it creates
33 * appropriate log files. The name of each finished log file starts with the "htmlmonitor_"
34 * followed by the client id and an index of the log file. An unfinished log file has no index yet.
35 * A log file is finished if
36 * <ul>
37 *   <li>the client session is closed by a timeout</li>
38 *   <li>the HTML monitor is normally shut down</li>
39 *   <li>on startup an unfinished log file is detected.</li>
40 *   <li>the {@link #MAXIMUM_LOG_FILE_SIZE} is reached</li>
41 * </ul>
[857]42 * </p>
43 *
44 * @author Patrick Harms
45 * @version 1.0
46 *
47 */
48public class HtmlMonitorOutputWriter implements HtmlMonitorComponent, HtmlMonitorMessageListener {
49   
50    /**
[871]51     * the maximum size of an individual log file
[857]52     */
[1227]53    private static final int MAXIMUM_LOG_FILE_SIZE = 50000000;
[857]54
55    /**
[871]56     * the default log base directory if none is provided through the constructor
[857]57     */
58    private static final String DEFAULT_LOG_FILE_BASE_DIR = "logs";
59
60    /**
[871]61     * the currently used log file base directory
[857]62     */
63    private File logFileBaseDir;
64
65    /**
[1227]66     * the id of the web application used by the client
67     */
68    private String webAppId;
69
70    /**
[1089]71     * the id of the client of which all messages are logged through this writer
[857]72     */
73    private String clientId;
74
75    /**
[871]76     * the log file into which all messages are currently written
[857]77     */
78    private File logFile;
79
80    /**
[871]81     * an output writer to be used for writing into the log file
[857]82     */
83    private PrintWriter outputWriter;
84
85    /**
[871]86     * the time stamp of the last action taken on this writer (such as logging a message)
[857]87     */
88    private long lastUpdate;
[1089]89   
90    /**
91     * the GUI elements, that were already logged and need therefore not be logged again into
92     * the same file
93     */
94    private Set<HtmlGUIElement> loggedGUIElements = new HashSet<HtmlGUIElement>();
[857]95
96    /**
97     * <p>
[871]98     * initializes the writer with the log file base directory and the id of the client for which
99     * this writer logs the messages.
[857]100     * </p>
101     *
[871]102     * @param logFileBaseDir the log file base directory, or null if the default directory shall
103     *                       be taken
[1227]104     * @param webAppId       the ID of the web application used by the client
[871]105     * @param clientId       the ID of the client, for which this writer logs
[857]106     */
[1227]107    public HtmlMonitorOutputWriter(String logFileBaseDir, String webAppId, String clientId) {
[857]108        if (logFileBaseDir == null) {
109            this.logFileBaseDir = new File(DEFAULT_LOG_FILE_BASE_DIR);
110        }
111        else {
112            this.logFileBaseDir = new File(logFileBaseDir);
113        }
114       
[1227]115        this.webAppId = webAppId;
[857]116        this.clientId = clientId;
117       
118        lastUpdate = System.currentTimeMillis();
119    }
120
121    /* (non-Javadoc)
122     * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#init()
123     */
124    @Override
125    public synchronized void init() throws HtmlMonitorException {
126        if (outputWriter != null) {
127            throw new IllegalStateException("already initialized. Call close() first");
128        }
129       
130        synchronized (HtmlMonitorOutputWriter.class) {
131            try {
[1227]132                File clientLogDir = new File(logFileBaseDir, webAppId);
133                clientLogDir = new File(clientLogDir, clientId);
134               
[857]135                if (!clientLogDir.exists()) {
[872]136                    if (!clientLogDir.mkdirs()) {
137                        throw new HtmlMonitorException("client log file directory " + clientLogDir +
138                                                       " can not be created");
139                    }
[857]140                }
141                else if (!clientLogDir.isDirectory()) {
142                    throw new HtmlMonitorException("client log file directory " + clientLogDir +
143                                                   " already exists as a file");
144                }
145               
[1227]146                handleOldLogFiles(new File(logFileBaseDir, clientId), clientLogDir);
147               
[857]148                logFile = new File(clientLogDir, getLogFileName(-1));
149               
150                if (logFile.exists()) {
151                    rotateLogFile();
152                }
153           
154                createLogWriter();
155            }
156            catch (IOException e) {
157                throw new HtmlMonitorException("could not open logfile " + logFile, e);
158            }
159        }
160       
161        lastUpdate = System.currentTimeMillis();
162    }
163
164    /**
165     * <p>
[871]166     * used to calculate a log file name. If the provided index is smaller 0, then no index
167     * is added to the file name. A filename is e.g. "htmlmonitor_12345_001.log".
[857]168     * </p>
169     *
[871]170     * @param index the index of the log file or -1 one, if no index shall be added
171     *
172     * @return the file name as described
[857]173     */
174    private String getLogFileName(int index) {
175        String result = "htmlmonitor_" + clientId;
176       
177        if (index >= 0) {
178            result += "_" + new DecimalFormat("000" ).format(index);
179        }
180       
181        result += ".log";
182       
183        return result;
184    }
185
186    /* (non-Javadoc)
187     * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#start()
188     */
189    @Override
190    public synchronized void start() throws IllegalStateException, HtmlMonitorException {
191        lastUpdate = System.currentTimeMillis();
192    }
193
194    /* (non-Javadoc)
[871]195     * @see HtmlMonitorMessageListener#handleMessage(HtmlClientInfos, HtmlEvent[])
[857]196     */
197    @Override
[1019]198    public void handleMessage(HtmlClientInfos clientInfos,
[1069]199                              HtmlGUIElement  guiStructure,
[1019]200                              HtmlEvent[]     events)
201    {
[857]202        if (outputWriter == null) {
203            throw new IllegalStateException("not initialized. Call init() first");
204        }
205       
[1019]206        if (guiStructure != null) {
207            dumpGuiStructure(guiStructure);
208        }
209       
[857]210        for (HtmlEvent event : events) {
211            dumpEvent(event);
212        }
213       
214        outputWriter.flush();
215       
216        try {
217            considerLogRotate();
218        }
219        catch (IOException e) {
220            throw new IllegalStateException("could not perform log rotation: " + e, e);
221        }
222       
223        lastUpdate = System.currentTimeMillis();
224    }
225
226    /**
227     * <p>
[1022]228     * dumps the GUI structure provided by the parameter into the log file. Calls itself
229     * recursively to traverse the GUI structure.
[1019]230     * </p>
231     *
[1022]232     * @param guiStructure the GUI structure to be logged
[1019]233     */
[1069]234    private void dumpGuiStructure(HtmlGUIElement guiStructure) {
[1089]235        if (!loggedGUIElements.contains(guiStructure)) {
236            outputWriter.print("<component id=\"");
237            outputWriter.print(guiStructure.getId());
238            outputWriter.println("\">");
[1019]239       
[1089]240            if (guiStructure instanceof HtmlServer) {
241                dumpParam("host", ((HtmlServer) guiStructure).getName());
242                dumpParam("port", ((HtmlServer) guiStructure).getPort());
243            }
244            else if (guiStructure instanceof HtmlDocument) {
245                dumpParam("path", ((HtmlDocument) guiStructure).getPath());
246                dumpParam("query", ((HtmlDocument) guiStructure).getQuery());
247                dumpParam("title", ((HtmlDocument) guiStructure).getTitle());
248            }
249            else if (guiStructure instanceof HtmlPageElement) {
250                dumpParam("tagname", ((HtmlPageElement) guiStructure).getTagName());
251                dumpParam("htmlid", ((HtmlPageElement) guiStructure).getHtmlId());
252                dumpParam("index", ((HtmlPageElement) guiStructure).getIndex());
253            }
254           
255            dumpParam("parent", guiStructure.getParentId());
[1019]256       
[1089]257            outputWriter.println("</component>");
[1069]258       
[1089]259            loggedGUIElements.add(guiStructure);
260        }
[1019]261       
262        if (guiStructure.getChildren() != null) {
[1069]263            for (HtmlGUIElement child : guiStructure.getChildren()) {
[1019]264                dumpGuiStructure(child);
265            }
266        }
267    }
268
269    /**
270     * <p>
[871]271     * formats a received event and writes it to the log file. One event results in one line
272     * in the log file containing all infos of the event.
[857]273     * </p>
274     *
[871]275     * @param event to be written to the log file
[857]276     */
277    private void dumpEvent(HtmlEvent event) {
[1019]278        outputWriter.print("<event type=\"");
279        outputWriter.print(event.getEventType());
280        outputWriter.println("\">");
281       
[857]282        if (event.getCoordinates() != null) {
[1019]283            dumpParam("X", event.getCoordinates()[0]);
284            dumpParam("Y", event.getCoordinates()[1]);
[857]285        }
286
[1019]287        dumpParam("key", event.getKey());
[857]288           
289        if (event.getScrollPosition() != null) {
[1019]290            dumpParam("scrollX", event.getScrollPosition()[0]);
291            dumpParam("scrollY", event.getScrollPosition()[1]);
[857]292        }
[942]293
[1019]294        dumpParam("selectedValue", event.getSelectedValue());
[1069]295       
296        if (event.getTarget() != null) {
297            dumpParam("target", event.getTarget().getId());
298        }
299        else {
300            dumpParam("targetDocument", event.getTargetDocument().getId());
301            dumpParam("targetDOMPath", event.getTargetDOMPath());
302        }
[1019]303        dumpParam("timestamp", event.getTime());
304       
305        outputWriter.println("</event>");
[857]306    }
307
308    /**
309     * <p>
[1022]310     * dumps a parameter with the given name and value to the log file. The result is a
311     * tag named param with a name attribute and a value attribute. The value is transformed
312     * to a String if it is no String already. Furthermore, an XML entity replacement is performed
313     * if required.
[857]314     * </p>
315     *
[1022]316     * @param name  the name of the parameter to be dumped
317     * @param value the value of the parameter to be dumped
[857]318     */
[1019]319    private void dumpParam(String name, Object value) {
320        if (value == null) {
321            return;
322        }
[997]323       
[1019]324        String val;
325       
326        if (value instanceof String) {
327            val = (String) value;
328        }
329        else {
[1227]330            val = String.valueOf(value);
[1019]331        }
332       
333        outputWriter.print(" <param name=\"");
334        outputWriter.print(name);
335        outputWriter.print("\" value=\"");
336        outputWriter.print(StringTools.xmlEntityReplacement(val));
337        outputWriter.println("\"/>");
[857]338    }
339
340    /**
341     * <p>
[871]342     * checks, if the log file exeeded the {@link #MAXIMUM_LOG_FILE_SIZE}. If so, the current
343     * log file is closed, the next log file name is determined and this new file is opend for
344     * writing.
[857]345     * </p>
346     */
347    private synchronized void considerLogRotate() throws IOException {
348        if (logFile.length() > MAXIMUM_LOG_FILE_SIZE) {
349            closeLogWriter();
350            rotateLogFile();
351            createLogWriter();
352        }
353    }
354
355    /**
356     * <p>
[871]357     * renames the current log file to a new log file with the next available index. It further
358     * sets the current log file to the default name, i.e. without index.
[857]359     * </p>
360     */
361    private void rotateLogFile() {
362        File clientLogDir = logFile.getParentFile();
363        File checkFile;
364
365        int logFileIndex = -1;
366        do {
367            logFileIndex++;
368           
369            checkFile = new File(clientLogDir, getLogFileName(logFileIndex));
370        }
371        while (checkFile.exists());
372   
[872]373        if (!logFile.renameTo(checkFile)) {
374            Console.printerrln("could not rename log file " + logFile + " to " + checkFile +
375                               ". Will not perform log rotation.");
376        }
377        else {
378            logFileIndex++;
379            logFile = new File(clientLogDir, getLogFileName(-1));
380        }
[857]381    }
382
383    /**
384     * <p>
[871]385     * instantiates a writer to be used for writing messages into the log file.
[857]386     * </p>
387     */
388    private void createLogWriter() throws IOException {
389        FileOutputStream fis = new FileOutputStream(logFile);
390        outputWriter = new PrintWriter(new OutputStreamWriter(fis, "UTF-8"));
[1019]391        outputWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
392        outputWriter.println("<session>");
[1089]393       
394        loggedGUIElements.clear();
[857]395    }
396
397    /**
398     * <p>
[871]399     * closed the current writer if it is open.
[857]400     * </p>
401     */
402    private void closeLogWriter() {
403        if (outputWriter != null) {
[1019]404            outputWriter.println("</session>");
[857]405            outputWriter.flush();
406            outputWriter.close();
407            outputWriter = null;
408        }
409    }
410
[1227]411    /**
412     * <p>
413     * this method moves old logfiles of the same client resisting in the wrong old directory
414     * structure to the new one.
415     * </p>
416     *
417     * @param file
418     * @param clientLogDir
419     */
420    private void handleOldLogFiles(File oldLogDir, File newLogDir) {
421        if (oldLogDir.exists() && oldLogDir.isDirectory()) {
422            for (File oldLogFile : oldLogDir.listFiles()) {
423                oldLogFile.renameTo(new File(newLogDir, oldLogFile.getName()));
424            }
425           
426            oldLogDir.delete();
427        }
428    }
429
[857]430    /* (non-Javadoc)
431     * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#stop()
432     */
433    @Override
434    public synchronized void stop() {
435        closeLogWriter();
436        rotateLogFile();
437
438        lastUpdate = System.currentTimeMillis();
439    }
440
441    /**
442     * <p>
[871]443     * return the time stamp of the last activity that happened on this writer.
[857]444     * </p>
445     *
[871]446     * @return as described
[857]447     */
448    public long getLastUpdate() {
449        return lastUpdate;
450    }
451}
Note: See TracBrowser for help on using the repository browser.