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

Last change on this file since 1429 was 1429, checked in by pharms, 10 years ago
  • adapted logging of GUI elements to ensure that all GUI elements on the same page are logged at least once
File size: 16.3 KB
Line 
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.
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;
23import java.util.HashSet;
24import java.util.Set;
25
26import de.ugoe.cs.util.StringTools;
27import de.ugoe.cs.util.console.Console;
28
29/**
30 * <p>
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>
42 * </p>
43 *
44 * @author Patrick Harms
45 * @version 1.0
46 *
47 */
48public class HtmlMonitorOutputWriter implements HtmlMonitorComponent, HtmlMonitorMessageListener {
49   
50    /**
51     * the maximum size of an individual log file
52     */
53    private static final int MAXIMUM_LOG_FILE_SIZE = 50000000;
54
55    /**
56     * the default log base directory if none is provided through the constructor
57     */
58    private static final String DEFAULT_LOG_FILE_BASE_DIR = "logs";
59
60    /**
61     * the currently used log file base directory
62     */
63    private File logFileBaseDir;
64
65    /**
66     * the id of the web application used by the client
67     */
68    private String webAppId;
69
70    /**
71     * the id of the client of which all messages are logged through this writer
72     */
73    private String clientId;
74
75    /**
76     * the log file into which all messages are currently written
77     */
78    private File logFile;
79
80    /**
81     * an output writer to be used for writing into the log file
82     */
83    private PrintWriter outputWriter;
84
85    /**
86     * the time stamp of the last action taken on this writer (such as logging a message)
87     */
88    private long lastUpdate;
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>();
95
96    /**
97     * <p>
98     * initializes the writer with the log file base directory and the id of the client for which
99     * this writer logs the messages.
100     * </p>
101     *
102     * @param logFileBaseDir the log file base directory, or null if the default directory shall
103     *                       be taken
104     * @param webAppId       the ID of the web application used by the client
105     * @param clientId       the ID of the client, for which this writer logs
106     */
107    public HtmlMonitorOutputWriter(String logFileBaseDir, String webAppId, String clientId) {
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       
115        this.webAppId = webAppId;
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 {
132                File clientLogDir = new File(logFileBaseDir, webAppId);
133                clientLogDir = new File(clientLogDir, clientId);
134               
135                if (!clientLogDir.exists()) {
136                    if (!clientLogDir.mkdirs()) {
137                        throw new HtmlMonitorException("client log file directory " + clientLogDir +
138                                                       " can not be created");
139                    }
140                }
141                else if (!clientLogDir.isDirectory()) {
142                    throw new HtmlMonitorException("client log file directory " + clientLogDir +
143                                                   " already exists as a file");
144                }
145               
146                handleOldLogFiles(new File(logFileBaseDir, clientId), clientLogDir);
147               
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>
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".
168     * </p>
169     *
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
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)
195     * @see HtmlMonitorMessageListener#handleMessage(HtmlClientInfos, HtmlEvent[])
196     */
197    @Override
198    public synchronized void handleMessage(HtmlClientInfos clientInfos,
199                                           HtmlGUIElement  guiStructure,
200                                           HtmlEvent[]     events)
201    {
202        if (outputWriter == null) {
203            throw new IllegalStateException("not initialized. Call init() first");
204        }
205       
206        for (HtmlEvent event : events) {
207            dumpEvent(event);
208        }
209       
210        outputWriter.flush();
211       
212        try {
213            considerLogRotate();
214        }
215        catch (IOException e) {
216            throw new IllegalStateException("could not perform log rotation: " + e, e);
217        }
218       
219        lastUpdate = System.currentTimeMillis();
220    }
221
222    /**
223     * <p>
224     * formats a received event and writes it to the log file. One event results in one line
225     * in the log file containing all infos of the event.
226     * </p>
227     *
228     * @param event to be written to the log file
229     */
230    private void dumpEvent(HtmlEvent event) {
231        if (event.getTarget() != null) {
232            ensureGuiElementDumped(event.getTarget());
233        }
234       
235        outputWriter.print("<event type=\"");
236        outputWriter.print(event.getEventType());
237        outputWriter.println("\">");
238       
239        if (event.getCoordinates() != null) {
240            dumpParam("X", event.getCoordinates()[0]);
241            dumpParam("Y", event.getCoordinates()[1]);
242        }
243
244        dumpParam("key", event.getKey());
245           
246        if (event.getScrollPosition() != null) {
247            dumpParam("scrollX", event.getScrollPosition()[0]);
248            dumpParam("scrollY", event.getScrollPosition()[1]);
249        }
250
251        dumpParam("selectedValue", event.getSelectedValue());
252       
253        if (event.getTarget() != null) {
254            dumpParam("target", event.getTarget().getId());
255        }
256        else {
257            dumpParam("targetDocument", event.getTargetDocument().getId());
258            dumpParam("targetDOMPath", event.getTargetDOMPath());
259        }
260        dumpParam("timestamp", event.getTime());
261       
262        outputWriter.println("</event>");
263    }
264
265    /**
266     * <p>
267     * ensures that a GUI element, its parents as well as all other GUI elements on the same page
268     * are dumped
269     * </p>
270     *
271     * @param guiStructure the GUI structure to be logged
272     */
273    private void ensureGuiElementDumped(HtmlGUIElement guiElement) {
274        if (!loggedGUIElements.contains(guiElement)) {
275           
276            // determine the document as the whole document needs to be dumped. Ensure also that
277            // the server is dumped
278           
279            HtmlGUIElement parent = guiElement;
280            HtmlDocument document = null;
281            HtmlServer server = null;
282           
283            while (parent != null) {
284                if (parent instanceof HtmlDocument) {
285                    document = (HtmlDocument) parent;
286                }
287                else if (parent instanceof HtmlServer) {
288                    server = (HtmlServer) parent;
289                }
290               
291                parent = parent.getParent();
292            }
293           
294            if (server != null) {
295                dumpGuiElement(server);
296            }
297           
298            if (document != null) {
299                dumpGuiStructure(document);
300            }
301        }
302    }
303
304    /**
305     * <p>
306     * dumps the GUI structure provided by the parameter into the log file. Calls itself
307     * recursively to traverse the GUI structure.
308     * </p>
309     *
310     * @param guiStructure the GUI structure to be logged
311     */
312    private void dumpGuiStructure(HtmlGUIElement guiStructure) {
313        dumpGuiElement(guiStructure);
314       
315        if (guiStructure.getChildren() != null) {
316            for (HtmlGUIElement child : guiStructure.getChildren()) {
317                dumpGuiStructure(child);
318            }
319        }
320    }
321
322    /**
323     * <p>
324     * dumps the GUI element provided by the parameter into the log file.
325     * </p>
326     *
327     * @param guiElement the GUI element to be logged
328     */
329    private void dumpGuiElement(HtmlGUIElement guiElement) {
330        if (!loggedGUIElements.contains(guiElement)) {
331            outputWriter.print("<component id=\"");
332            outputWriter.print(guiElement.getId());
333            outputWriter.println("\">");
334       
335            if (guiElement instanceof HtmlServer) {
336                dumpParam("host", ((HtmlServer) guiElement).getName());
337                dumpParam("port", ((HtmlServer) guiElement).getPort());
338            }
339            else if (guiElement instanceof HtmlDocument) {
340                dumpParam("path", ((HtmlDocument) guiElement).getPath());
341                dumpParam("query", ((HtmlDocument) guiElement).getQuery());
342                dumpParam("title", ((HtmlDocument) guiElement).getTitle());
343            }
344            else if (guiElement instanceof HtmlPageElement) {
345                dumpParam("tagname", ((HtmlPageElement) guiElement).getTagName());
346                dumpParam("htmlid", ((HtmlPageElement) guiElement).getHtmlId());
347                dumpParam("index", ((HtmlPageElement) guiElement).getIndex());
348            }
349           
350            if (guiElement.getParent() != null) {
351                dumpParam("parent", guiElement.getParent().getId());
352            }
353       
354            outputWriter.println("</component>");
355       
356            loggedGUIElements.add(guiElement);
357        }
358    }
359
360    /**
361     * <p>
362     * dumps a parameter with the given name and value to the log file. The result is a
363     * tag named param with a name attribute and a value attribute. The value is transformed
364     * to a String if it is no String already. Furthermore, an XML entity replacement is performed
365     * if required.
366     * </p>
367     *
368     * @param name  the name of the parameter to be dumped
369     * @param value the value of the parameter to be dumped
370     */
371    private void dumpParam(String name, Object value) {
372        if (value == null) {
373            return;
374        }
375       
376        String val;
377       
378        if (value instanceof String) {
379            val = (String) value;
380        }
381        else {
382            val = String.valueOf(value);
383        }
384       
385        outputWriter.print(" <param name=\"");
386        outputWriter.print(name);
387        outputWriter.print("\" value=\"");
388        outputWriter.print(StringTools.xmlEntityReplacement(val));
389        outputWriter.println("\"/>");
390    }
391
392    /**
393     * <p>
394     * checks, if the log file exeeded the {@link #MAXIMUM_LOG_FILE_SIZE}. If so, the current
395     * log file is closed, the next log file name is determined and this new file is opend for
396     * writing.
397     * </p>
398     */
399    private synchronized void considerLogRotate() throws IOException {
400        if (logFile.length() > MAXIMUM_LOG_FILE_SIZE) {
401            closeLogWriter();
402            rotateLogFile();
403            createLogWriter();
404        }
405    }
406
407    /**
408     * <p>
409     * renames the current log file to a new log file with the next available index. It further
410     * sets the current log file to the default name, i.e. without index.
411     * </p>
412     */
413    private void rotateLogFile() {
414        File clientLogDir = logFile.getParentFile();
415        File checkFile;
416
417        int logFileIndex = -1;
418        do {
419            logFileIndex++;
420           
421            checkFile = new File(clientLogDir, getLogFileName(logFileIndex));
422        }
423        while (checkFile.exists());
424   
425        if (!logFile.renameTo(checkFile)) {
426            Console.printerrln("could not rename log file " + logFile + " to " + checkFile +
427                               ". Will not perform log rotation.");
428        }
429        else {
430            logFileIndex++;
431            logFile = new File(clientLogDir, getLogFileName(-1));
432        }
433    }
434
435    /**
436     * <p>
437     * instantiates a writer to be used for writing messages into the log file.
438     * </p>
439     */
440    private void createLogWriter() throws IOException {
441        FileOutputStream fis = new FileOutputStream(logFile);
442        outputWriter = new PrintWriter(new OutputStreamWriter(fis, "UTF-8"));
443        outputWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
444        outputWriter.println("<session>");
445       
446        loggedGUIElements.clear();
447    }
448
449    /**
450     * <p>
451     * closed the current writer if it is open.
452     * </p>
453     */
454    private void closeLogWriter() {
455        if (outputWriter != null) {
456            outputWriter.println("</session>");
457            outputWriter.flush();
458            outputWriter.close();
459            outputWriter = null;
460        }
461    }
462
463    /**
464     * <p>
465     * this method moves old logfiles of the same client resisting in the wrong old directory
466     * structure to the new one.
467     * </p>
468     *
469     * @param oldLogDir the old log directory
470     * @param newLogDir the new log directory
471     */
472    private void handleOldLogFiles(File oldLogDir, File newLogDir) {
473        if (oldLogDir.exists() && oldLogDir.isDirectory()) {
474            boolean allFilesRenamed = true;
475            for (File oldLogFile : oldLogDir.listFiles()) {
476                allFilesRenamed &= oldLogFile.renameTo(new File(newLogDir, oldLogFile.getName()));
477            }
478           
479            if (allFilesRenamed) {
480                if (!oldLogDir.delete()) {
481                    Console.printerrln("could not move old file directory structure to new one");
482                }
483            }
484        }
485    }
486
487    /* (non-Javadoc)
488     * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#stop()
489     */
490    @Override
491    public synchronized void stop() {
492        closeLogWriter();
493        rotateLogFile();
494
495        lastUpdate = System.currentTimeMillis();
496    }
497
498    /**
499     * <p>
500     * return the time stamp of the last activity that happened on this writer.
501     * </p>
502     *
503     * @return as described
504     */
505    public long getLastUpdate() {
506        return lastUpdate;
507    }
508}
Note: See TracBrowser for help on using the repository browser.