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

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