source: trunk/autoquest-generic-event-monitor/src/main/java/de/ugoe/cs/autoquest/genericeventmonitor/OutputWriter.java @ 2155

Last change on this file since 2155 was 2155, checked in by pharms, 7 years ago
  • Property svn:mime-type set to text/plain
File size: 15.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.genericeventmonitor;
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.Map;
28import java.util.Set;
29
30import de.ugoe.cs.util.StringTools;
31import de.ugoe.cs.util.console.Console;
32
33/**
34 * <p>
35 * dumps messages to a log file belonging to a specific app and client id. In the provided base log
36 * directory, it creates a subdirectory for the app and a subsubdirectory for the client id. In this
37 * subsubdirectory it creates appropriate log files. The name of each finished log file starts with
38 * "genericevents_" followed by the client id and an index of the log file. An unfinished log file
39 * has no index yet. A log file is finished if
40 * <ul>
41 *   <li>the client session is closed by a timeout</li>
42 *   <li>the generic event monitor is normally shut down</li>
43 *   <li>on startup an unfinished log file is detected.</li>
44 *   <li>the {@link #MAXIMUM_LOG_FILE_SIZE} is reached</li>
45 * </ul>
46 * </p>
47 *
48 * @author Patrick Harms
49 * @version 1.0
50 *
51 */
52public class OutputWriter implements GenericEventMonitorComponent, GenericEventMonitorMessageListener
53{
54   
55    /**
56     * the maximum size of an individual log file
57     */
58    private static final int MAXIMUM_LOG_FILE_SIZE = 100000000;
59
60    /**
61     * the default log base directory if none is provided through the constructor
62     */
63    private static final String DEFAULT_LOG_FILE_BASE_DIR = "logs";
64
65    /**
66     * the currently used log file base directory
67     */
68    private File logFileBaseDir;
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 id of the application that the client uses
77     */
78    private String appId;
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 id of the targets, that were already logged and need therefore not be logged again into
97     * the same file
98     */
99    private Set<String> loggedTargets = new HashSet<>();
100
101    /**
102     * <p>
103     * initializes the writer with the log file base directory, the id of the client for which
104     * this writer logs the messages and the id of the app that is used by the client.
105     * </p>
106     *
107     * @param logFileBaseDir the log file base directory, or null if the default directory shall
108     *                       be taken
109     * @param appId          the ID of the app used by the client
110     * @param clientId       the ID of the client, for which this writer logs
111     */
112    public OutputWriter(String logFileBaseDir, String appId, String clientId) {
113        if (logFileBaseDir == null) {
114            this.logFileBaseDir = new File(DEFAULT_LOG_FILE_BASE_DIR);
115        }
116        else {
117            this.logFileBaseDir = new File(logFileBaseDir);
118        }
119       
120        this.appId = appId;
121        this.clientId = clientId;
122       
123        lastUpdate = System.currentTimeMillis();
124    }
125
126    /* (non-Javadoc)
127     * @see de.ugoe.cs.autoquest.genericeventmonitor.GenericEventMonitorComponent#init()
128     */
129    @Override
130    public synchronized void init() throws GenericEventMonitorException {
131        if (outputWriter != null) {
132            throw new IllegalStateException("already initialized. Call close() first");
133        }
134       
135        synchronized (OutputWriter.class) {
136            try {
137                File clientLogDir = new File(new File(logFileBaseDir, appId), clientId);
138               
139                if (!clientLogDir.exists()) {
140                    if (!clientLogDir.mkdirs()) {
141                        throw new GenericEventMonitorException("client log file directory " +
142                                                              clientLogDir + " cannot be created");
143                    }
144                }
145                else if (!clientLogDir.isDirectory()) {
146                    throw new GenericEventMonitorException("client log file directory " +
147                                                          clientLogDir + " already exists as a file");
148                }
149               
150                logFile = new File(clientLogDir, getLogFileName(-1));
151               
152                if (logFile.exists()) {
153                    rotateLogFile();
154                }
155           
156                createLogWriter();
157            }
158            catch (IOException e) {
159                throw new GenericEventMonitorException("could not open logfile " + logFile, e);
160            }
161        }
162       
163        lastUpdate = System.currentTimeMillis();
164    }
165
166    /**
167     * <p>
168     * used to calculate a log file name. If the provided index is smaller 0, then no index
169     * is added to the file name. A filename is e.g. "genericevents_12345_001.log".
170     * </p>
171     *
172     * @param index the index of the log file or -1 one, if no index shall be added
173     *
174     * @return the file name as described
175     */
176    private String getLogFileName(int index) {
177        String result = "genericevents_" + clientId;
178       
179        if (index >= 0) {
180            result += "_" + new DecimalFormat("000" ).format(index);
181        }
182       
183        result += ".log";
184       
185        return result;
186    }
187
188    /* (non-Javadoc)
189     * @see de.ugoe.cs.autoquest.genericeventmonitor.GenericEventMonitorComponent#start()
190     */
191    @Override
192    public synchronized void start() throws IllegalStateException, GenericEventMonitorException {
193        lastUpdate = System.currentTimeMillis();
194    }
195
196    /* (non-Javadoc)
197     * @see GenericEventMonitorMessageListener#handleEvents(ClientInfos, GenericEvent[])
198     */
199    @Override
200    public void handleEvents(ClientInfos          clientInfos,
201                             GenericEvent[]       events)
202    {
203        if (outputWriter == null) {
204            throw new IllegalStateException("not initialized. Call init() first");
205        }
206       
207        for (GenericEvent event : events) {
208            dumpEvent(event);
209        }
210       
211        outputWriter.flush();
212       
213        try {
214            considerLogRotate();
215        }
216        catch (IOException e) {
217            throw new IllegalStateException("could not perform log rotation: " + e, e);
218        }
219       
220        lastUpdate = System.currentTimeMillis();
221    }
222
223    /**
224     * <p>
225     * formats a received event and writes it to the log file. One event results in one line
226     * in the log file containing all infos of the event.
227     * </p>
228     *
229     * @param event to be written to the log file
230     */
231    private void dumpEvent(GenericEvent event) {
232        if (event.getTarget() != null) {
233            ensureGuiElementDumped(event.getTarget());
234        }
235       
236        outputWriter.print("<event type=\"");
237        outputWriter.print(event.getEventType());
238        outputWriter.println("\">");
239       
240        dumpParam("timestamp", event.getTime());
241        dumpParam("targetId", event.getTarget().getId());
242       
243        for (Map.Entry<String, String> parameter : event.getParameters().entrySet()) {
244            dumpParam(parameter.getKey(), parameter.getValue());
245        }
246       
247        outputWriter.println("</event>");
248    }
249
250    /**
251     * <p>
252     * ensures that a target element and its corresponding target element tree are logged
253     * </p>
254     *
255     * @param target the GUI structure to be logged
256     */
257    private void ensureGuiElementDumped(GenericEventTarget target) {
258        if (!loggedTargets.contains(target.getId())) {
259            GenericEventTarget parent = target;
260           
261            // get the root
262            while (parent.getParent() != null) {
263                parent = parent.getParent();
264            }
265           
266            dumpTargetStructure(parent);
267        }
268    }
269
270    /**
271     * <p>
272     * dumps the target provided by the parameter into the log file. Calls itself
273     * recursively to traverse the target structure.
274     * </p>
275     *
276     * @param target the target structure to be logged
277     */
278    private void dumpTargetStructure(GenericEventTarget target) {
279        dumpTarget(target);
280       
281        if (target.getChildren() != null) {
282            for (GenericEventTarget child : target.getChildren()) {
283                dumpTargetStructure(child);
284            }
285        }
286    }
287
288    /**
289     * <p>
290     * dumps the target provided by the parameter into the log file.
291     * </p>
292     *
293     * @param guiElement the GUI element to be logged
294     */
295    private void dumpTarget(GenericEventTarget target) {
296        if (!loggedTargets.contains(target.getId())) {
297            outputWriter.print("<target id=\"");
298            outputWriter.print(target.getId());
299            outputWriter.println("\">");
300       
301            if (target.getParent() != null) {
302                dumpParam("parent", target.getParent().getId());
303            }
304           
305            for (Map.Entry<String, String> parameter : target.getParameters().entrySet()) {
306                dumpParam(parameter.getKey(), parameter.getValue());
307            }
308       
309            outputWriter.println("</target>");
310       
311            loggedTargets.add(target.getId());
312        }
313    }
314
315    /**
316     * <p>
317     * dumps a parameter with the given name and value to the log file. The result is a
318     * tag named param with a name attribute and a value attribute. The value is transformed
319     * to a String if it is no String already. Furthermore, an XML entity replacement is performed
320     * if required.
321     * </p>
322     *
323     * @param name  the name of the parameter to be dumped
324     * @param value the value of the parameter to be dumped
325     */
326    private void dumpParam(String name, Object value) {
327        if (value == null) {
328            return;
329        }
330       
331        String val;
332       
333        if (value instanceof String) {
334            val = (String) value;
335        }
336        else {
337            val = String.valueOf(value);
338        }
339       
340        outputWriter.print(" <param name=\"");
341        outputWriter.print(name);
342        outputWriter.print("\" value=\"");
343        outputWriter.print(StringTools.xmlEntityReplacement(val));
344        outputWriter.println("\"/>");
345    }
346
347    /**
348     * <p>
349     * checks, if the log file exceeded the {@link #MAXIMUM_LOG_FILE_SIZE}. If so, the current
350     * log file is closed, the next log file name is determined and this new file is opened for
351     * writing.
352     * </p>
353     */
354    private synchronized void considerLogRotate() throws IOException {
355        if (logFile.length() > MAXIMUM_LOG_FILE_SIZE) {
356            closeLogWriter();
357            rotateLogFile();
358            createLogWriter();
359        }
360    }
361
362    /**
363     * <p>
364     * renames the current log file to a new log file with the next available index. It further
365     * sets the current log file to the default name, i.e. without index.
366     * </p>
367     */
368    private void rotateLogFile() {
369        File clientLogDir = logFile.getParentFile();
370        File checkFile;
371
372        int logFileIndex = -1;
373        do {
374            logFileIndex++;
375           
376            checkFile = new File(clientLogDir, getLogFileName(logFileIndex));
377        }
378        while (checkFile.exists());
379   
380        if (!logFile.renameTo(checkFile)) {
381            Console.printerrln("could not rename log file " + logFile + " to " + checkFile +
382                               ". Will not perform log rotation.");
383        }
384        else {
385            logFileIndex++;
386            logFile = new File(clientLogDir, getLogFileName(-1));
387        }
388    }
389
390    /**
391     * <p>
392     * instantiates a writer to be used for writing messages into the log file.
393     * </p>
394     */
395    private void createLogWriter() throws IOException {
396        FileOutputStream fis = new FileOutputStream(logFile);
397        outputWriter = new PrintWriter(new OutputStreamWriter(fis, "UTF-8"));
398        outputWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
399        outputWriter.print("<session appId=\"");
400        outputWriter.print(StringTools.xmlEntityReplacement(this.appId));
401        outputWriter.print("\" clientId=\"");
402        outputWriter.print(StringTools.xmlEntityReplacement(this.clientId));
403        outputWriter.print("\" monitorInfos=\"");
404        outputWriter.print(StringTools.xmlEntityReplacement(getMonitorVersion()));
405       
406        outputWriter.println("\">");
407       
408        loggedTargets.clear();
409    }
410
411    /**
412     * <p>
413     * parses the POM properties if accessible and returns them. If they are not available,
414     * unknown is returned.
415     * </p>
416     */
417    private String getMonitorVersion() {
418        InputStream properties = this.getClass().getClassLoader().getResourceAsStream
419            ("META-INF/maven/de.ugoe.cs.autoquest/autoquest-generic-event-monitor/pom.properties");
420       
421        if (properties != null) {
422            try {
423                StringBuffer result = new StringBuffer();
424                BufferedReader reader = new BufferedReader(new InputStreamReader(properties));
425               
426                String line = null;
427               
428                do {
429                    line = reader.readLine();
430                   
431                    if (line != null) {
432                        if (result.length() > 0) {
433                            result.append("  ");
434                        }
435                       
436                        result.append(line);
437                    }
438                }
439                while (line != null);
440               
441                reader.close();
442                return result.toString();
443            }
444            catch (IOException e) {
445                return "unknown";
446            }
447        }
448        else {
449            return "unknown";
450        }
451    }
452
453    /**
454     * <p>
455     * closed the current writer if it is open.
456     * </p>
457     */
458    private void closeLogWriter() {
459        if (outputWriter != null) {
460            outputWriter.println("</session>");
461            outputWriter.flush();
462            outputWriter.close();
463            outputWriter = null;
464        }
465    }
466
467    /* (non-Javadoc)
468     * @see de.ugoe.cs.autoquest.genericeventmonitor.GenericEventMonitorComponent#stop()
469     */
470    @Override
471    public synchronized void stop() {
472        closeLogWriter();
473        rotateLogFile();
474
475        lastUpdate = System.currentTimeMillis();
476    }
477
478    /**
479     * <p>
480     * return the time stamp of the last activity that happened on this writer.
481     * </p>
482     *
483     * @return as described
484     */
485    public long getLastUpdate() {
486        return lastUpdate;
487    }
488}
Note: See TracBrowser for help on using the repository browser.