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

Last change on this file since 1821 was 1746, checked in by pharms, 10 years ago
  • added logging of user agent
File size: 16.9 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 = 100000000;
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 user agent used by the client
77     */
78    private String userAgent;
79
80    /**
81     * the log file into which all messages are currently written
82     */
83    private File logFile;
84
85    /**
86     * an output writer to be used for writing into the log file
87     */
88    private PrintWriter outputWriter;
89
90    /**
91     * the time stamp of the last action taken on this writer (such as logging a message)
92     */
93    private long lastUpdate;
94   
95    /**
96     * the GUI elements, that were already logged and need therefore not be logged again into
97     * the same file
98     */
99    private Set<HtmlGUIElement> loggedGUIElements = new HashSet<HtmlGUIElement>();
100
101    /**
102     * <p>
103     * initializes the writer with the log file base directory and the id of the client for which
104     * this writer logs the messages.
105     * </p>
106     *
107     * @param logFileBaseDir the log file base directory, or null if the default directory shall
108     *                       be taken
109     * @param webAppId       the ID of the web application used by the client
110     * @param clientId       the ID of the client, for which this writer logs
111     */
112    public HtmlMonitorOutputWriter(String logFileBaseDir,
113                                   String webAppId,
114                                   String clientId,
115                                   String userAgent)
116    {
117        if (logFileBaseDir == null) {
118            this.logFileBaseDir = new File(DEFAULT_LOG_FILE_BASE_DIR);
119        }
120        else {
121            this.logFileBaseDir = new File(logFileBaseDir);
122        }
123       
124        this.webAppId = webAppId;
125        this.clientId = clientId;
126        this.userAgent = userAgent;
127       
128        lastUpdate = System.currentTimeMillis();
129    }
130
131    /* (non-Javadoc)
132     * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#init()
133     */
134    @Override
135    public synchronized void init() throws HtmlMonitorException {
136        if (outputWriter != null) {
137            throw new IllegalStateException("already initialized. Call close() first");
138        }
139       
140        synchronized (HtmlMonitorOutputWriter.class) {
141            try {
142                File clientLogDir = new File(logFileBaseDir, webAppId);
143                clientLogDir = new File(clientLogDir, clientId);
144               
145                if (!clientLogDir.exists()) {
146                    if (!clientLogDir.mkdirs()) {
147                        throw new HtmlMonitorException("client log file directory " + clientLogDir +
148                                                       " can not be created");
149                    }
150                }
151                else if (!clientLogDir.isDirectory()) {
152                    throw new HtmlMonitorException("client log file directory " + clientLogDir +
153                                                   " already exists as a file");
154                }
155               
156                handleOldLogFiles(new File(logFileBaseDir, clientId), clientLogDir);
157               
158                logFile = new File(clientLogDir, getLogFileName(-1));
159               
160                if (logFile.exists()) {
161                    rotateLogFile();
162                }
163           
164                createLogWriter();
165            }
166            catch (IOException e) {
167                throw new HtmlMonitorException("could not open logfile " + logFile, e);
168            }
169        }
170       
171        lastUpdate = System.currentTimeMillis();
172    }
173
174    /**
175     * <p>
176     * used to calculate a log file name. If the provided index is smaller 0, then no index
177     * is added to the file name. A filename is e.g. "htmlmonitor_12345_001.log".
178     * </p>
179     *
180     * @param index the index of the log file or -1 one, if no index shall be added
181     *
182     * @return the file name as described
183     */
184    private String getLogFileName(int index) {
185        String result = "htmlmonitor_" + clientId;
186       
187        if (index >= 0) {
188            result += "_" + new DecimalFormat("000" ).format(index);
189        }
190       
191        result += ".log";
192       
193        return result;
194    }
195
196    /* (non-Javadoc)
197     * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#start()
198     */
199    @Override
200    public synchronized void start() throws IllegalStateException, HtmlMonitorException {
201        lastUpdate = System.currentTimeMillis();
202    }
203
204    /* (non-Javadoc)
205     * @see HtmlMonitorMessageListener#handleMessage(HtmlClientInfos, HtmlEvent[])
206     */
207    @Override
208    public synchronized void handleMessage(HtmlClientInfos clientInfos,
209                                           HtmlGUIElement  guiStructure,
210                                           HtmlEvent[]     events)
211    {
212        if (outputWriter == null) {
213            throw new IllegalStateException("not initialized. Call init() first");
214        }
215       
216        for (HtmlEvent event : events) {
217            dumpEvent(event);
218        }
219       
220        outputWriter.flush();
221       
222        try {
223            considerLogRotate();
224        }
225        catch (IOException e) {
226            throw new IllegalStateException("could not perform log rotation: " + e, e);
227        }
228       
229        lastUpdate = System.currentTimeMillis();
230    }
231
232    /**
233     * <p>
234     * formats a received event and writes it to the log file. One event results in one line
235     * in the log file containing all infos of the event.
236     * </p>
237     *
238     * @param event to be written to the log file
239     */
240    private void dumpEvent(HtmlEvent event) {
241        if (event.getTarget() != null) {
242            ensureGuiElementDumped(event.getTarget());
243        }
244       
245        outputWriter.print("<event type=\"");
246        outputWriter.print(event.getEventType());
247        outputWriter.println("\">");
248       
249        if (event.getCoordinates() != null) {
250            dumpParam("X", event.getCoordinates()[0]);
251            dumpParam("Y", event.getCoordinates()[1]);
252        }
253
254        dumpParam("key", event.getKey());
255           
256        if (event.getScrollPosition() != null) {
257            dumpParam("scrollX", event.getScrollPosition()[0]);
258            dumpParam("scrollY", event.getScrollPosition()[1]);
259        }
260
261        dumpParam("selectedValue", event.getSelectedValue());
262       
263        if (event.getTarget() != null) {
264            dumpParam("target", event.getTarget().getId());
265        }
266        else {
267            dumpParam("targetDocument", event.getTargetDocument().getId());
268            dumpParam("targetDOMPath", event.getTargetDOMPath());
269        }
270        dumpParam("timestamp", event.getTime());
271       
272        outputWriter.println("</event>");
273    }
274
275    /**
276     * <p>
277     * ensures that a GUI element, its parents as well as all other GUI elements on the same page
278     * are dumped
279     * </p>
280     *
281     * @param guiStructure the GUI structure to be logged
282     */
283    private void ensureGuiElementDumped(HtmlGUIElement guiElement) {
284        if (!loggedGUIElements.contains(guiElement)) {
285           
286            // determine the document as the whole document needs to be dumped. Ensure also that
287            // the server is dumped
288           
289            HtmlGUIElement parent = guiElement;
290            HtmlDocument document = null;
291            HtmlServer server = null;
292           
293            while (parent != null) {
294                if (parent instanceof HtmlDocument) {
295                    document = (HtmlDocument) parent;
296                }
297                else if (parent instanceof HtmlServer) {
298                    server = (HtmlServer) parent;
299                }
300               
301                parent = parent.getParent();
302            }
303           
304            if (server != null) {
305                dumpGuiElement(server);
306            }
307           
308            if (document != null) {
309                dumpGuiStructure(document);
310            }
311        }
312    }
313
314    /**
315     * <p>
316     * dumps the GUI structure provided by the parameter into the log file. Calls itself
317     * recursively to traverse the GUI structure.
318     * </p>
319     *
320     * @param guiStructure the GUI structure to be logged
321     */
322    private void dumpGuiStructure(HtmlGUIElement guiStructure) {
323        dumpGuiElement(guiStructure);
324       
325        if (guiStructure.getChildren() != null) {
326            for (HtmlGUIElement child : guiStructure.getChildren()) {
327                dumpGuiStructure(child);
328            }
329        }
330    }
331
332    /**
333     * <p>
334     * dumps the GUI element provided by the parameter into the log file.
335     * </p>
336     *
337     * @param guiElement the GUI element to be logged
338     */
339    private void dumpGuiElement(HtmlGUIElement guiElement) {
340        if (!loggedGUIElements.contains(guiElement)) {
341            outputWriter.print("<component id=\"");
342            outputWriter.print(guiElement.getId());
343            outputWriter.println("\">");
344       
345            if (guiElement instanceof HtmlServer) {
346                dumpParam("host", ((HtmlServer) guiElement).getName());
347                dumpParam("port", ((HtmlServer) guiElement).getPort());
348            }
349            else if (guiElement instanceof HtmlDocument) {
350                dumpParam("path", ((HtmlDocument) guiElement).getPath());
351                dumpParam("query", ((HtmlDocument) guiElement).getQuery());
352                dumpParam("title", ((HtmlDocument) guiElement).getTitle());
353            }
354            else if (guiElement instanceof HtmlPageElement) {
355                dumpParam("tagname", ((HtmlPageElement) guiElement).getTagName());
356                dumpParam("htmlid", ((HtmlPageElement) guiElement).getHtmlId());
357                dumpParam("index", ((HtmlPageElement) guiElement).getIndex());
358            }
359           
360            if (guiElement.getParent() != null) {
361                dumpParam("parent", guiElement.getParent().getId());
362            }
363       
364            outputWriter.println("</component>");
365       
366            loggedGUIElements.add(guiElement);
367        }
368    }
369
370    /**
371     * <p>
372     * dumps a parameter with the given name and value to the log file. The result is a
373     * tag named param with a name attribute and a value attribute. The value is transformed
374     * to a String if it is no String already. Furthermore, an XML entity replacement is performed
375     * if required.
376     * </p>
377     *
378     * @param name  the name of the parameter to be dumped
379     * @param value the value of the parameter to be dumped
380     */
381    private void dumpParam(String name, Object value) {
382        if (value == null) {
383            return;
384        }
385       
386        String val;
387       
388        if (value instanceof String) {
389            val = (String) value;
390        }
391        else {
392            val = String.valueOf(value);
393        }
394       
395        outputWriter.print(" <param name=\"");
396        outputWriter.print(name);
397        outputWriter.print("\" value=\"");
398        outputWriter.print(StringTools.xmlEntityReplacement(val));
399        outputWriter.println("\"/>");
400    }
401
402    /**
403     * <p>
404     * checks, if the log file exeeded the {@link #MAXIMUM_LOG_FILE_SIZE}. If so, the current
405     * log file is closed, the next log file name is determined and this new file is opend for
406     * writing.
407     * </p>
408     */
409    private synchronized void considerLogRotate() throws IOException {
410        if (logFile.length() > MAXIMUM_LOG_FILE_SIZE) {
411            closeLogWriter();
412            rotateLogFile();
413            createLogWriter();
414        }
415    }
416
417    /**
418     * <p>
419     * renames the current log file to a new log file with the next available index. It further
420     * sets the current log file to the default name, i.e. without index.
421     * </p>
422     */
423    private void rotateLogFile() {
424        File clientLogDir = logFile.getParentFile();
425        File checkFile;
426
427        int logFileIndex = -1;
428        do {
429            logFileIndex++;
430           
431            checkFile = new File(clientLogDir, getLogFileName(logFileIndex));
432        }
433        while (checkFile.exists());
434   
435        if (!logFile.renameTo(checkFile)) {
436            Console.printerrln("could not rename log file " + logFile + " to " + checkFile +
437                               ". Will not perform log rotation.");
438        }
439        else {
440            logFileIndex++;
441            logFile = new File(clientLogDir, getLogFileName(-1));
442        }
443    }
444
445    /**
446     * <p>
447     * instantiates a writer to be used for writing messages into the log file.
448     * </p>
449     */
450    private void createLogWriter() throws IOException {
451        FileOutputStream fis = new FileOutputStream(logFile);
452        outputWriter = new PrintWriter(new OutputStreamWriter(fis, "UTF-8"));
453        outputWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
454        outputWriter.print("<session webapp=\"");
455        outputWriter.print(StringTools.xmlEntityReplacement(this.webAppId));
456        outputWriter.print("\" clientId=\"");
457        outputWriter.print(StringTools.xmlEntityReplacement(this.clientId));
458        outputWriter.print("\" userAgent=\"");
459        outputWriter.print(StringTools.xmlEntityReplacement(this.userAgent));
460        outputWriter.println("\">");
461       
462        loggedGUIElements.clear();
463    }
464
465    /**
466     * <p>
467     * closed the current writer if it is open.
468     * </p>
469     */
470    private void closeLogWriter() {
471        if (outputWriter != null) {
472            outputWriter.println("</session>");
473            outputWriter.flush();
474            outputWriter.close();
475            outputWriter = null;
476        }
477    }
478
479    /**
480     * <p>
481     * this method moves old logfiles of the same client resisting in the wrong old directory
482     * structure to the new one.
483     * </p>
484     *
485     * @param oldLogDir the old log directory
486     * @param newLogDir the new log directory
487     */
488    private void handleOldLogFiles(File oldLogDir, File newLogDir) {
489        if (oldLogDir.exists() && oldLogDir.isDirectory()) {
490            boolean allFilesRenamed = true;
491            for (File oldLogFile : oldLogDir.listFiles()) {
492                allFilesRenamed &= oldLogFile.renameTo(new File(newLogDir, oldLogFile.getName()));
493            }
494           
495            if (allFilesRenamed) {
496                if (!oldLogDir.delete()) {
497                    Console.printerrln("could not move old file directory structure to new one");
498                }
499            }
500        }
501    }
502
503    /* (non-Javadoc)
504     * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#stop()
505     */
506    @Override
507    public synchronized void stop() {
508        closeLogWriter();
509        rotateLogFile();
510
511        lastUpdate = System.currentTimeMillis();
512    }
513
514    /**
515     * <p>
516     * return the time stamp of the last activity that happened on this writer.
517     * </p>
518     *
519     * @return as described
520     */
521    public long getLastUpdate() {
522        return lastUpdate;
523    }
524}
Note: See TracBrowser for help on using the repository browser.