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

Last change on this file since 1174 was 1089, checked in by pharms, 11 years ago
  • prevent logging of the same GUI elements several times
  • improved efficiency of hash code calculation
  • improved efficiency of reuse of GUI elements
File size: 13.5 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     */
53    private static final int MAXIMUM_LOG_FILE_SIZE = 10000000;
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    /**
[1089]66     * the id of the client of which all messages are logged through this writer
[857]67     */
68    private String clientId;
69
70    /**
[871]71     * the log file into which all messages are currently written
[857]72     */
73    private File logFile;
74
75    /**
[871]76     * an output writer to be used for writing into the log file
[857]77     */
78    private PrintWriter outputWriter;
79
80    /**
[871]81     * the time stamp of the last action taken on this writer (such as logging a message)
[857]82     */
83    private long lastUpdate;
[1089]84   
85    /**
86     * the GUI elements, that were already logged and need therefore not be logged again into
87     * the same file
88     */
89    private Set<HtmlGUIElement> loggedGUIElements = new HashSet<HtmlGUIElement>();
[857]90
91    /**
92     * <p>
[871]93     * initializes the writer with the log file base directory and the id of the client for which
94     * this writer logs the messages.
[857]95     * </p>
96     *
[871]97     * @param logFileBaseDir the log file base directory, or null if the default directory shall
98     *                       be taken
99     * @param clientId       the ID of the client, for which this writer logs
[857]100     */
101    public HtmlMonitorOutputWriter(String logFileBaseDir, String clientId) {
102        if (logFileBaseDir == null) {
103            this.logFileBaseDir = new File(DEFAULT_LOG_FILE_BASE_DIR);
104        }
105        else {
106            this.logFileBaseDir = new File(logFileBaseDir);
107        }
108       
109        this.clientId = clientId;
110       
111        lastUpdate = System.currentTimeMillis();
112    }
113
114    /* (non-Javadoc)
115     * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#init()
116     */
117    @Override
118    public synchronized void init() throws HtmlMonitorException {
119        if (outputWriter != null) {
120            throw new IllegalStateException("already initialized. Call close() first");
121        }
122       
123        synchronized (HtmlMonitorOutputWriter.class) {
124            try {
125                File clientLogDir = new File(logFileBaseDir, clientId);
126           
127                if (!clientLogDir.exists()) {
[872]128                    if (!clientLogDir.mkdirs()) {
129                        throw new HtmlMonitorException("client log file directory " + clientLogDir +
130                                                       " can not be created");
131                    }
[857]132                }
133                else if (!clientLogDir.isDirectory()) {
134                    throw new HtmlMonitorException("client log file directory " + clientLogDir +
135                                                   " already exists as a file");
136                }
137               
138                logFile = new File(clientLogDir, getLogFileName(-1));
139               
140                if (logFile.exists()) {
141                    rotateLogFile();
142                }
143           
144                createLogWriter();
145            }
146            catch (IOException e) {
147                throw new HtmlMonitorException("could not open logfile " + logFile, e);
148            }
149        }
150       
151        lastUpdate = System.currentTimeMillis();
152    }
153
154    /**
155     * <p>
[871]156     * used to calculate a log file name. If the provided index is smaller 0, then no index
157     * is added to the file name. A filename is e.g. "htmlmonitor_12345_001.log".
[857]158     * </p>
159     *
[871]160     * @param index the index of the log file or -1 one, if no index shall be added
161     *
162     * @return the file name as described
[857]163     */
164    private String getLogFileName(int index) {
165        String result = "htmlmonitor_" + clientId;
166       
167        if (index >= 0) {
168            result += "_" + new DecimalFormat("000" ).format(index);
169        }
170       
171        result += ".log";
172       
173        return result;
174    }
175
176    /* (non-Javadoc)
177     * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#start()
178     */
179    @Override
180    public synchronized void start() throws IllegalStateException, HtmlMonitorException {
181        lastUpdate = System.currentTimeMillis();
182    }
183
184    /* (non-Javadoc)
[871]185     * @see HtmlMonitorMessageListener#handleMessage(HtmlClientInfos, HtmlEvent[])
[857]186     */
187    @Override
[1019]188    public void handleMessage(HtmlClientInfos clientInfos,
[1069]189                              HtmlGUIElement  guiStructure,
[1019]190                              HtmlEvent[]     events)
191    {
[857]192        if (outputWriter == null) {
193            throw new IllegalStateException("not initialized. Call init() first");
194        }
195       
[1019]196        if (guiStructure != null) {
197            dumpGuiStructure(guiStructure);
198        }
199       
[857]200        for (HtmlEvent event : events) {
201            dumpEvent(event);
202        }
203       
204        outputWriter.flush();
205       
206        try {
207            considerLogRotate();
208        }
209        catch (IOException e) {
210            throw new IllegalStateException("could not perform log rotation: " + e, e);
211        }
212       
213        lastUpdate = System.currentTimeMillis();
214    }
215
216    /**
217     * <p>
[1022]218     * dumps the GUI structure provided by the parameter into the log file. Calls itself
219     * recursively to traverse the GUI structure.
[1019]220     * </p>
221     *
[1022]222     * @param guiStructure the GUI structure to be logged
[1019]223     */
[1069]224    private void dumpGuiStructure(HtmlGUIElement guiStructure) {
[1089]225        if (!loggedGUIElements.contains(guiStructure)) {
226            outputWriter.print("<component id=\"");
227            outputWriter.print(guiStructure.getId());
228            outputWriter.println("\">");
[1019]229       
[1089]230            if (guiStructure instanceof HtmlServer) {
231                dumpParam("host", ((HtmlServer) guiStructure).getName());
232                dumpParam("port", ((HtmlServer) guiStructure).getPort());
233            }
234            else if (guiStructure instanceof HtmlDocument) {
235                dumpParam("path", ((HtmlDocument) guiStructure).getPath());
236                dumpParam("query", ((HtmlDocument) guiStructure).getQuery());
237                dumpParam("title", ((HtmlDocument) guiStructure).getTitle());
238            }
239            else if (guiStructure instanceof HtmlPageElement) {
240                dumpParam("tagname", ((HtmlPageElement) guiStructure).getTagName());
241                dumpParam("htmlid", ((HtmlPageElement) guiStructure).getHtmlId());
242                dumpParam("index", ((HtmlPageElement) guiStructure).getIndex());
243            }
244           
245            dumpParam("parent", guiStructure.getParentId());
[1019]246       
[1089]247            outputWriter.println("</component>");
[1069]248       
[1089]249            loggedGUIElements.add(guiStructure);
250        }
[1019]251       
252        if (guiStructure.getChildren() != null) {
[1069]253            for (HtmlGUIElement child : guiStructure.getChildren()) {
[1019]254                dumpGuiStructure(child);
255            }
256        }
257    }
258
259    /**
260     * <p>
[871]261     * formats a received event and writes it to the log file. One event results in one line
262     * in the log file containing all infos of the event.
[857]263     * </p>
264     *
[871]265     * @param event to be written to the log file
[857]266     */
267    private void dumpEvent(HtmlEvent event) {
[1019]268        outputWriter.print("<event type=\"");
269        outputWriter.print(event.getEventType());
270        outputWriter.println("\">");
271       
[857]272        if (event.getCoordinates() != null) {
[1019]273            dumpParam("X", event.getCoordinates()[0]);
274            dumpParam("Y", event.getCoordinates()[1]);
[857]275        }
276
[1019]277        dumpParam("key", event.getKey());
[857]278           
279        if (event.getScrollPosition() != null) {
[1019]280            dumpParam("scrollX", event.getScrollPosition()[0]);
281            dumpParam("scrollY", event.getScrollPosition()[1]);
[857]282        }
[942]283
[1019]284        dumpParam("selectedValue", event.getSelectedValue());
[1069]285       
286        if (event.getTarget() != null) {
287            dumpParam("target", event.getTarget().getId());
288        }
289        else {
290            dumpParam("targetDocument", event.getTargetDocument().getId());
291            dumpParam("targetDOMPath", event.getTargetDOMPath());
292        }
[1019]293        dumpParam("timestamp", event.getTime());
294       
295        outputWriter.println("</event>");
[857]296    }
297
298    /**
299     * <p>
[1022]300     * dumps a parameter with the given name and value to the log file. The result is a
301     * tag named param with a name attribute and a value attribute. The value is transformed
302     * to a String if it is no String already. Furthermore, an XML entity replacement is performed
303     * if required.
[857]304     * </p>
305     *
[1022]306     * @param name  the name of the parameter to be dumped
307     * @param value the value of the parameter to be dumped
[857]308     */
[1019]309    private void dumpParam(String name, Object value) {
310        if (value == null) {
311            return;
312        }
[997]313       
[1019]314        String val;
315       
316        if (value instanceof String) {
317            val = (String) value;
318        }
319        else {
320            val = value.toString();
321        }
322       
323        outputWriter.print(" <param name=\"");
324        outputWriter.print(name);
325        outputWriter.print("\" value=\"");
326        outputWriter.print(StringTools.xmlEntityReplacement(val));
327        outputWriter.println("\"/>");
[857]328    }
329
330    /**
331     * <p>
[871]332     * checks, if the log file exeeded the {@link #MAXIMUM_LOG_FILE_SIZE}. If so, the current
333     * log file is closed, the next log file name is determined and this new file is opend for
334     * writing.
[857]335     * </p>
336     */
337    private synchronized void considerLogRotate() throws IOException {
338        if (logFile.length() > MAXIMUM_LOG_FILE_SIZE) {
339            closeLogWriter();
340            rotateLogFile();
341            createLogWriter();
342        }
343    }
344
345    /**
346     * <p>
[871]347     * renames the current log file to a new log file with the next available index. It further
348     * sets the current log file to the default name, i.e. without index.
[857]349     * </p>
350     */
351    private void rotateLogFile() {
352        File clientLogDir = logFile.getParentFile();
353        File checkFile;
354
355        int logFileIndex = -1;
356        do {
357            logFileIndex++;
358           
359            checkFile = new File(clientLogDir, getLogFileName(logFileIndex));
360        }
361        while (checkFile.exists());
362   
[872]363        if (!logFile.renameTo(checkFile)) {
364            Console.printerrln("could not rename log file " + logFile + " to " + checkFile +
365                               ". Will not perform log rotation.");
366        }
367        else {
368            logFileIndex++;
369            logFile = new File(clientLogDir, getLogFileName(-1));
370        }
[857]371    }
372
373    /**
374     * <p>
[871]375     * instantiates a writer to be used for writing messages into the log file.
[857]376     * </p>
377     */
378    private void createLogWriter() throws IOException {
379        FileOutputStream fis = new FileOutputStream(logFile);
380        outputWriter = new PrintWriter(new OutputStreamWriter(fis, "UTF-8"));
[1019]381        outputWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
382        outputWriter.println("<session>");
[1089]383       
384        loggedGUIElements.clear();
[857]385    }
386
387    /**
388     * <p>
[871]389     * closed the current writer if it is open.
[857]390     * </p>
391     */
392    private void closeLogWriter() {
393        if (outputWriter != null) {
[1019]394            outputWriter.println("</session>");
[857]395            outputWriter.flush();
396            outputWriter.close();
397            outputWriter = null;
398        }
399    }
400
401    /* (non-Javadoc)
402     * @see de.ugoe.cs.autoquest.htmlmonitor.HtmlMonitorComponent#stop()
403     */
404    @Override
405    public synchronized void stop() {
406        closeLogWriter();
407        rotateLogFile();
408
409        lastUpdate = System.currentTimeMillis();
410    }
411
412    /**
413     * <p>
[871]414     * return the time stamp of the last activity that happened on this writer.
[857]415     * </p>
416     *
[871]417     * @return as described
[857]418     */
419    public long getLastUpdate() {
420        return lastUpdate;
421    }
422}
Note: See TracBrowser for help on using the repository browser.