source: trunk/autoquest-plugin-html/src/main/java/de/ugoe/cs/autoquest/plugin/html/HTMLLogCompressor.java @ 1254

Last change on this file since 1254 was 1254, checked in by pharms, 11 years ago
  • performance improvement for log compression with a large numbers of events
File size: 12.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.plugin.html;
16
17import java.io.File;
18import java.io.FileNotFoundException;
19import java.io.FileOutputStream;
20import java.io.OutputStreamWriter;
21import java.io.PrintWriter;
22import java.io.UnsupportedEncodingException;
23import java.text.DecimalFormat;
24import java.util.HashSet;
25import java.util.LinkedList;
26import java.util.List;
27import java.util.Map;
28import java.util.Set;
29
30import org.xml.sax.SAXException;
31
32import de.ugoe.cs.util.StringTools;
33import de.ugoe.cs.util.console.Console;
34
35/**
36 * <p>
37 * This class can be used to compress a bunch of HTML log files to one file, that contains only
38 * one full GUI model.
39 * </p>
40 *
41 * @author Patrick Harms
42 * @version 1.0
43 *
44 */
45public class HTMLLogCompressor extends AbstractDefaultLogParser {
46   
47    /**
48     * <p>
49     * The output writer into which the compressed variant of the log files is written
50     * </p>
51     */
52    private PrintWriter outputWriter;
53   
54    /**
55     * <p>
56     * the GUI elements, that were already logged and need therefore not be logged again into
57     * the same file
58     * </p>
59     */
60    private Set<String> loggedGUIElementIds = new HashSet<String>();
61
62    /**
63     * <p>
64     * the events that were read
65     * </p>
66     */
67    private List<EventEntry> sortedEvents = new LinkedList<EventEntry>();
68
69    /**
70     * <p>
71     * called to compress all log files in the provided directory. The method reuses
72     * {@link #compressFilesInDirectory(File)}.
73     * </p>
74     *
75     * @param directory the directory containing the log files to be compressed
76     */
77    public void compressFilesInDirectory(String directory) {
78        if (directory == null) {
79            throw new IllegalArgumentException("directory must not be null");
80        }
81
82        compressFilesInDirectory(new File(directory));
83    }
84
85    /**
86     * <p>
87     * called to compress all log files in the provided directory. The directory if processed
88     * recursively. For each directory containing at least one log file the log files are
89     * replaced by one large log file containing the GUI model as well as all events contained in
90     * the compressed log file. If one log file could not be parsed, it is ignored and not
91     * compressed with the other ones.
92     * </p>
93     *
94     * @param directory the directory containing the log files to be compressed
95     */
96    public void compressFilesInDirectory(File directory) {
97        if (directory == null) {
98            throw new IllegalArgumentException("directory must not be null");
99        }
100       
101        if (!directory.exists()) {
102            throw new IllegalArgumentException("directory must denote an existing directory");
103        }
104       
105        if (!directory.isDirectory()) {
106            throw new IllegalArgumentException("directory must denote a directory");
107        }
108       
109        File[] files = directory.listFiles();
110       
111        if ((files == null) || (files.length == 0)) {
112            Console.println("directory is empty");
113            return;
114        }
115       
116        File outFile = null;
117       
118        int logFileIndex = -1;
119        do {
120            outFile = new File(directory, getLogFileName(directory.getName(), logFileIndex));
121            logFileIndex++;
122        }
123        while (outFile.exists());
124       
125        List<File> compressedFiles = new LinkedList<File>();
126       
127        try {
128            FileOutputStream fis = new FileOutputStream(outFile);
129            outputWriter = new PrintWriter(new OutputStreamWriter(fis, "UTF-8"));
130            outputWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
131            outputWriter.println("<session>");
132
133            loggedGUIElementIds.clear();
134            sortedEvents.clear();
135
136            for (File file : files) {
137                if (file.isFile()) {
138                    try {
139                        super.parseFile(file);
140                        Console.println("compressed " + file);
141                        compressedFiles.add(file);
142                    }
143                    catch (Exception e) {
144                        Console.printerrln("could not parse and compress file " + file);
145                        Console.printerrln(e.getMessage());
146                    }
147                }
148            }
149           
150            for (EventEntry event : sortedEvents) {
151                event.dump();
152            }
153
154            outputWriter.println("</session>");
155            outputWriter.flush();
156        }
157        catch (FileNotFoundException e) {
158            Console.printerrln("could not create compressed file " + outFile);
159            compressedFiles.clear();
160        }
161        catch (UnsupportedEncodingException e) {
162            // this should never happen
163            e.printStackTrace();
164            compressedFiles.clear();
165        }
166        finally {
167            if (outputWriter != null) {
168                outputWriter.close();
169                outputWriter = null;
170            }
171        }
172       
173        if (compressedFiles.size() > 0) {
174            for (File file : compressedFiles) {
175                if (!file.delete()) {
176                    Console.printerrln("could not delete compressed file " + file);
177                }
178            }
179       
180            if (outFile.exists()) {
181                File finalOutFile = null;
182           
183                logFileIndex = -1;
184                do {
185                    logFileIndex++;
186                    finalOutFile =
187                        new File(directory, getLogFileName(directory.getName(), logFileIndex));
188                }
189                while (finalOutFile.exists());
190
191                outFile.renameTo(finalOutFile);
192                Console.println("created " + finalOutFile);
193            }
194        }
195        else {
196            outFile.delete();
197        }
198    }
199   
200    /* (non-Javadoc)
201     * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#parseFile(java.lang.String)
202     */
203    @Override
204    public void parseFile(String filename) {
205        throw new IllegalStateException("this method must not be called externally");
206    }
207
208    /* (non-Javadoc)
209     * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#parseFile(java.io.File)
210     */
211    @Override
212    public void parseFile(File file) {
213        throw new IllegalStateException("this method must not be called externally");
214    }
215
216    /* (non-Javadoc)
217     * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#handleGUIElement(String, Map)
218     */
219    @Override
220    protected boolean handleGUIElement(String id, Map<String, String> parameters)
221        throws SAXException
222    {
223        if (!loggedGUIElementIds.contains(id)) {
224            outputWriter.print("<component id=\"");
225            outputWriter.print(id);
226            outputWriter.println("\">");
227       
228            for (Map.Entry<String, String> param : parameters.entrySet()) {
229                dumpParam(param.getKey(), param.getValue());
230            }
231           
232            outputWriter.println("</component>");
233       
234            loggedGUIElementIds.add(id);
235        }
236       
237        return true;
238    }
239
240    /* (non-Javadoc)
241     * @see de.ugoe.cs.autoquest.plugin.html.AbstractDefaultLogParser#handleEvent(String,Map)
242     */
243    @Override
244    protected boolean handleEvent(String type, Map<String, String> parameters) throws SAXException {
245        String timestampStr = parameters.get("timestamp");
246       
247        long timestamp = Long.MAX_VALUE;
248       
249        if (timestampStr != null) {
250            timestamp = Long.parseLong(timestampStr);
251        }
252       
253        EventEntry newEvent = new EventEntry(type, parameters, timestamp);
254       
255        int start = 0;
256        int end = sortedEvents.size();
257        int center = 0;
258        long centerTimestamp;
259       
260        while (start != end) {
261            center = start + ((end - start) / 2);
262           
263            if ((center != start) || (center != end)) {
264                centerTimestamp = sortedEvents.get(center).timestamp;
265           
266                if (centerTimestamp < newEvent.timestamp) {
267                    start = Math.max(center, start + 1);
268                }
269                else if (centerTimestamp > newEvent.timestamp) {
270                    end = Math.min(center, end - 1);
271                }
272                else {
273                    // add the event directly where the center is, as the timestamps of the center
274                    // and the new event are equal
275                    start = end = center;
276                    break;
277                }
278            }
279            else {
280                // add the event to the position denoted by the add index
281                break;
282            }
283        }
284       
285        sortedEvents.add(start, newEvent);
286
287        return true;
288    }
289
290    /**
291     * <p>
292     * dumps a parameter with the given name and value to the log file. The result is a
293     * tag named param with a name attribute and a value attribute. The value is transformed
294     * to a String if it is no String already. Furthermore, an XML entity replacement is performed
295     * if required.
296     * </p>
297     *
298     * @param name  the name of the parameter to be dumped
299     * @param value the value of the parameter to be dumped
300     */
301    private void dumpParam(String name, Object value) {
302        if (value == null) {
303            return;
304        }
305       
306        String val;
307       
308        if (value instanceof String) {
309            val = (String) value;
310        }
311        else {
312            val = String.valueOf(value);
313        }
314       
315        outputWriter.print(" <param name=\"");
316        outputWriter.print(name);
317        outputWriter.print("\" value=\"");
318        outputWriter.print(StringTools.xmlEntityReplacement(val));
319        outputWriter.println("\"/>");
320    }
321
322
323    /**
324     * <p>
325     * used to calculate a log file name. If the provided index is smaller 0, then no index
326     * is added to the file name. A filename is e.g. "htmlmonitor_12345_001.log".
327     * </p>
328     *
329     * @param index the index of the log file or -1 one, if no index shall be added
330     *
331     * @return the file name as described
332     */
333    private String getLogFileName(String clientId, int index) {
334        String result = "htmlmonitor_" + clientId;
335       
336        if (index >= 0) {
337            result += "_" + new DecimalFormat("000" ).format(index);
338        }
339       
340        result += ".log";
341       
342        return result;
343    }
344   
345    /**
346     * <p>
347     * this class is used internally for storing events in a sorted list together with the
348     * timestamps, being the sort criteria.
349     * </p>
350     */
351    private class EventEntry {
352       
353        /**
354         * <p>
355         * the type of the event
356         * </p>
357         */
358        private String type;
359       
360        /**
361         * <p>
362         * the parameters of the event
363         * </p>
364         */
365        private Map<String, String> parameters;
366       
367        /**
368         * <p>
369         * the timestamp of the event
370         * </p>
371         */
372        private long timestamp;
373
374        /**
375         * <p>
376         * creates a new event entry with event type, parameters and the timestamp
377         * </p>
378         */
379        private EventEntry(String type, Map<String, String> parameters, long timestamp) {
380            this.type = type;
381            this.parameters = parameters;
382            this.timestamp = timestamp;
383        }
384       
385        /**
386         * <p>
387         * convenience method for dumping the event into the compressed log file
388         * </p>
389         */
390        private void dump() {
391            outputWriter.print("<event type=\"");
392            outputWriter.print(type);
393            outputWriter.println("\">");
394           
395            for (Map.Entry<String, String> param : parameters.entrySet()) {
396                dumpParam(param.getKey(), param.getValue());
397            }
398           
399            outputWriter.println("</event>");
400        }
401    }
402}
Note: See TracBrowser for help on using the repository browser.