diff --git a/java/common/build.xml b/java/common/build.xml new file mode 100644 index 00000000..6dd2a885 --- /dev/null +++ b/java/common/build.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/common/src/com/google/airbag/common/AbstractMinidumpServlet.java b/java/common/src/com/google/airbag/common/AbstractMinidumpServlet.java new file mode 100644 index 00000000..27fae88c --- /dev/null +++ b/java/common/src/com/google/airbag/common/AbstractMinidumpServlet.java @@ -0,0 +1,135 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.airbag.common; + +import java.io.IOException; +import java.io.InputStream; +import java.text.NumberFormat; +import java.util.Calendar; +import java.util.Random; +import java.util.SortedMap; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Common part of uploading a file with parameters. A subclass needs to + * provide a ReportStorage implementation. + * + * An upload file is saved in the file storage with parsed parameters from + * the URL. + * + * A separate processor will access the file storage and process these + * reports. + */ +@SuppressWarnings("serial") +public class AbstractMinidumpServlet extends HttpServlet { + // Minidump storage to be instantiated by subclasses in init() method. + protected ReportStorage minidumpStorage; + + // Random number generator + private final Random random = new Random(); + + /** + * Always return success to a GET, to keep out nosy people who go + * directly to the URL. + */ + public void doGet(HttpServletRequest req, HttpServletResponse res) { + res.setStatus(HttpServletResponse.SC_OK); + } + + /** + * Takes the file POSTed to the server and writes it to a file storage. + * Parameters in URL are saved as attributes of the file. + * + * @param req a wrapped HttpServletRequest that represents a multipart + * request + * @return unique ID for this report, can be used to get parameters and + * uploaded file contents from a file storage. If these is a + * collation, returns null. + * + * @throws ServletException + * @throws IOException + */ + protected String saveFile(MultipartRequest req) + throws ServletException, IOException { + // parse mutilpart request + SortedMap params = req.getParameters(); + + //TODO(fqian): verify required fields of a report + InputStream inputs = req.getInputStream(); + + /* It is possible that two or more clients report crashes at the same + * time with same parameters. To reduce the chance of collation, we + * add two internal parameters: + * 1. reporting time, a time string in the form of YYMMDD-HHMMSS; + * 2. a random number; + * + * In theory, there is still a chance to collate, but it is very low. + * When collation happens, the one coming later is dropped. + */ + // 1. add a timestamp to parameters + params.put(NameConstants.REPORTTIME_PNAME, currentTimeString()); + + // 2. get a random number to make the change of collation very small + int r; + synchronized (this.random) { + r = this.random.nextInt(); + } + params.put(NameConstants.RANDOMNUM_PNAME, Integer.toString(r)); + + String fid = this.minidumpStorage.getUniqueId(params); + + assert fid != null; + + if (this.minidumpStorage.reportExists(fid)) { + // collation happens + return null; + } + + this.minidumpStorage.saveAttributes(fid, params); + // save uploaded contents to the storage + this.minidumpStorage.writeStreamToReport(fid, inputs, 0); + + return fid; + } + + /* Gets a string representing the current time using the format: + * YYMMDD-HHMMSS. + */ + private String currentTimeString() { + NumberFormat formatter = NumberFormat.getInstance(); + formatter.setGroupingUsed(false); + formatter.setMinimumIntegerDigits(2); // 2 digits per date component + + // All servers are in Pacific time + Calendar cal = Calendar.getInstance(); + + StringBuffer tstring = new StringBuffer(); + tstring.append(formatter.format(cal.get(Calendar.YEAR))); + // We want January = 1. + tstring.append(formatter.format((cal.get(Calendar.MONTH) + 1))); + tstring.append(formatter.format(cal.get(Calendar.DAY_OF_MONTH))); + tstring.append("-"); + tstring.append(formatter.format(cal.get(Calendar.HOUR_OF_DAY))); + tstring.append(formatter.format(cal.get(Calendar.MINUTE))); + tstring.append(formatter.format(cal.get(Calendar.SECOND))); + + return new String(tstring); + } +} diff --git a/java/common/src/com/google/airbag/common/CrashUtils.java b/java/common/src/com/google/airbag/common/CrashUtils.java new file mode 100644 index 00000000..a010c526 --- /dev/null +++ b/java/common/src/com/google/airbag/common/CrashUtils.java @@ -0,0 +1,188 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.airbag.common; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +/** A utility class used by crash reporting server. */ + +public class CrashUtils { + // A map from numbers to hexadecimal characters. + private static final char[] maps = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', + }; + + private static final String MD_ALGORITHM = "MD5"; + + // delimiter between record + private static final String RECORD_DELIMITER = ";"; + private static final String FIELD_DELIMITER = " "; + private static final int BUFFER_SIZE = 1024; + + /** + * Given a byte array, returns a string of its hex representation. + * Each byte is represented by two characters for its high and low + * parts. For example, if the input is [0x3F, 0x01], this method + * returns 3F01. + * + * @param bs a byte array + * @return a string of hexadecimal characters + */ + public static String bytesToHexString(byte[] bs) { + StringBuffer sb = new StringBuffer(); + for (byte b : bs) { + int high = (b >> 4) & 0x0F; + int low = b & 0x0F; + sb.append(maps[high]); + sb.append(maps[low]); + } + return sb.toString(); + } + + /** + * Given a byte array, computes its message digest using one-way hash + * functions. + * + * @param data a byte array + * @return a string as its signature, or null if no message digest + * algorithm + * supported by the system. + */ + public static String dataSignature(byte[] data) { + try { + MessageDigest md = MessageDigest.getInstance(MD_ALGORITHM); + return bytesToHexString(md.digest(data)); + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + /** + * Compute the signature of a file by calling dataSignature on file + * contents. + * + * @param file a file name which signature to be computed + * @return the method signature of the file, or null if failed to + * read file contents, or message digest algorithm is not supported + */ + public static String fileSignature(File file) { + try { + FileInputStream fis = new FileInputStream(file); + byte[] buf = new byte[BUFFER_SIZE]; + MessageDigest md = MessageDigest.getInstance(MD_ALGORITHM); + while (true) { + int bytesRead = fis.read(buf, 0, BUFFER_SIZE); + if (bytesRead == -1) + break; + md.update(buf, 0, bytesRead); + } + return bytesToHexString(md.digest()); + + } catch (NoSuchAlgorithmException e) { + return null; + } catch (IOException e) { + return null; + } + } + + /** + * Encodes an attribute map to a string. Encoded string format: + * name value1[ value2];name value1[ value2] + * Names and values should be escaped so that there are no + * RECORD_DELIMITER and VALUE_DELIMITER in strings. + * + * @param attributes a maps of attributes name and value to be encoded + * @return a string of encoded attributes + */ + public static + String attributesToString(SortedMap attributes) { + StringBuffer res = new StringBuffer(); + for (Map.Entry e : attributes.entrySet()) { + String name = e.getKey(); + String value = e.getValue(); + + assert name.indexOf(RECORD_DELIMITER) == -1; + assert name.indexOf(FIELD_DELIMITER) == -1; + res.append(name).append(FIELD_DELIMITER); + + assert value.indexOf(RECORD_DELIMITER) == -1; + assert value.indexOf(FIELD_DELIMITER) == -1; + res.append(value).append(RECORD_DELIMITER); + } + return new String(res); + } + + /** + * Decodes a string to a map of attributes. + */ + public static SortedMap stringToAttributes(String s) { + SortedMap map = + new TreeMap(); + String[] records = s.split(RECORD_DELIMITER); + for (String r : records) { + String[] fields = r.trim().split(FIELD_DELIMITER); + if (fields.length != 2) // discard records that has no values + continue; + String name = fields[0].trim(); + String value = fields[1].trim(); + map.put(name, value); + } + return map; + } + + /** + * Copies bytes from an input stream to an output stream, with max bytes. + * + * @param ins an input stream to read + * @param outs an output stream to write + * @param max the maximum number of bytes to copy. If max <= 0, copy bytes + * until the end of input stream + * @return the number of bytes copied + * @throws IOException + */ + public static int copyStream(InputStream ins, OutputStream outs, int max) + throws IOException { + byte[] buf = new byte[BUFFER_SIZE]; + int bytesWritten = 0; + + while (true) { + int bytesToRead = BUFFER_SIZE; + if (max > 0) + bytesToRead = Math.min(BUFFER_SIZE, max - bytesWritten); + + if (bytesToRead <= 0) + break; + + int bytesRead = ins.read(buf, 0, bytesToRead); + if (bytesRead == -1) // end of input stream + break; + outs.write(buf, 0, bytesRead); + bytesWritten += bytesRead; + } + + return bytesWritten; + } +} diff --git a/java/common/src/com/google/airbag/common/MultipartRequest.java b/java/common/src/com/google/airbag/common/MultipartRequest.java new file mode 100644 index 00000000..bd737bfb --- /dev/null +++ b/java/common/src/com/google/airbag/common/MultipartRequest.java @@ -0,0 +1,36 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.google.airbag.common; + +import java.util.SortedMap; +import java.io.InputStream; + +/** + * A common interface for different multipart HttpServletRequest + * implementations. The interface is simple enough to be used by the + * upload server. + */ + +public interface MultipartRequest { + /** + * Returns a sorted map of name to values of an HTTP request. + */ + public SortedMap getParameters(); + + /** + * Returns an input stream of uploading file. + */ + public InputStream getInputStream(); +} diff --git a/java/common/src/com/google/airbag/common/NameConstants.java b/java/common/src/com/google/airbag/common/NameConstants.java new file mode 100644 index 00000000..6ea80692 --- /dev/null +++ b/java/common/src/com/google/airbag/common/NameConstants.java @@ -0,0 +1,42 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.airbag.common; + +/** A class defines URL parameter names. */ + +public class NameConstants { + // URL parameter names + // product name + public static final String PRODUCT_PNAME = "prod"; + // version + public static final String VERSION_PNAME = "ver"; + // application or module + public static final String APPLICATION_PNAME = "app"; + // platform, e.g., win32, linux, mac + public static final String PLATFORM_PNAME = "plat"; + // report format, e.g., dump, xml + public static final String FORMAT_PNAME = "fmt"; + // process uptime + public static final String PROCESSUPTIME_PNAME = "procup"; + // cumulative process uptime + public static final String CUMULATIVEUPTIME_PNAME = "cumup"; + // time when report is created + public static final String REPORTTIME_PNAME = "rept"; + // a random number + public static final String RANDOMNUM_PNAME = "rand"; + // report checksum + public static final String CHECKSUM_PNAME = "sum"; +} diff --git a/java/common/src/com/google/airbag/common/ReportQueue.java b/java/common/src/com/google/airbag/common/ReportQueue.java new file mode 100644 index 00000000..c92f385e --- /dev/null +++ b/java/common/src/com/google/airbag/common/ReportQueue.java @@ -0,0 +1,60 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.airbag.common; + +import java.util.LinkedList; + +/** + * A queue interface for unprocessed report ids. The interface is intended + * for inter-process usage. A report uploading server enqueues new report + * ids, and a processor dequeues ids. + * + * The interface is much simpler than java.util.Queue. An implementation + * should provide a persistent storage of queued ids even when a process + * is killed. + */ + +public interface ReportQueue { + /** + * Enqueue a record id. + * + * @param rid + * @return true if success + */ + public boolean enqueue(String rid); + + /** + * Enqueue a list of ids + * + * @param ids + * @return true if success + */ + public boolean enqueue(LinkedList ids); + + /** + * Checks if a queue is empty + * @return true if the queue is empty + */ + public boolean empty(); + + /** + * Removes several ids from the queue. An implementation decides how + * many ids to be removed. + * + * @return a list of queue + */ + public LinkedList dequeue(); +} diff --git a/java/common/src/com/google/airbag/common/ReportQueueDirImpl.java b/java/common/src/com/google/airbag/common/ReportQueueDirImpl.java new file mode 100644 index 00000000..dc7c922a --- /dev/null +++ b/java/common/src/com/google/airbag/common/ReportQueueDirImpl.java @@ -0,0 +1,115 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.airbag.common; + +import java.util.LinkedList; +import java.io.File; +import java.io.IOException; + +/** + * Implements ReportQueue using directories. Some restrictions: + *
    + *
  • Ids must be valid file names;
  • + *
  • Ids cannot be duplicated, a duplicated id is ignored;
  • + *
  • No guarantees on ordering, in other words, this is not really + * a queue (with FIFO order);
  • + *
+ */ + +public class ReportQueueDirImpl implements ReportQueue { + // maximum number of ids returned by dequque method. + private static final int MAX_DEQUEUED_IDS = 100; + + // the directory name for storing files + private String queueDir; + + /** + * Creates an instance of ReportQueueDirImpl with a directory name. + * @param dirname + */ + public ReportQueueDirImpl(String dirname) + throws IOException + { + this.queueDir = dirname; + File q = new File(dirname); + if (!q.exists()) + q.mkdirs(); + + if (!q.isDirectory()) + throw new IOException("name "+dirname + +" exits already, but not a directory."); + } + + /** Enqueue a report id. */ + public boolean enqueue(String rid) { + //lock on the directory + // add a file named by id + File f = new File(this.queueDir, rid); + try { + return f.createNewFile(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } + } + + /** Enqueue a list of ids. */ + public boolean enqueue(LinkedList ids) { + //lock on the directory + // add a file named by id + for (String rid : ids) { + File f = new File(this.queueDir, rid); + try { + if (!f.createNewFile()) + return false; + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } + } + return true; + } + + /** Checks if the queue is empty. */ + public boolean empty() { + File f = new File(this.queueDir); + String[] ids = f.list(); + if (ids == null) + return true; + else + return ids.length == 0; + } + + /** Remove ids from the queue. */ + public LinkedList dequeue() { + // lock on the directory + LinkedList rids = new LinkedList(); + File d = new File(this.queueDir); + String[] ids = d.list(); + if (ids == null) + return rids; + + for (int i =0; i < Math.min(ids.length, MAX_DEQUEUED_IDS); i++) { + File f = new File(this.queueDir, ids[i]); + f.delete(); + rids.add(ids[i]); + } + + return rids; + } +} diff --git a/java/common/src/com/google/airbag/common/ReportQueueFileImpl.java b/java/common/src/com/google/airbag/common/ReportQueueFileImpl.java new file mode 100644 index 00000000..8d858664 --- /dev/null +++ b/java/common/src/com/google/airbag/common/ReportQueueFileImpl.java @@ -0,0 +1,126 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.google.airbag.common; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.util.LinkedList; +import java.util.logging.Logger; + +/** + * Implementation of ReportQueue using a shared file. File accesses are + * protected by a file lock. When dequeue, all reports in the file were + * returned and the file is truncated to length zero. + */ + +public class ReportQueueFileImpl implements ReportQueue { + private static final Logger logger = + Logger.getLogger(ReportQueueFileImpl.class.getName()); + private String queueFile; + + /** Given a file name, creates an instance of ReportQueueFileImpl. */ + public ReportQueueFileImpl(String fname) { + this.queueFile = fname; + } + + /** Enqueues a report id. */ + public boolean enqueue(String rid) { + try { + RandomAccessFile raf = new RandomAccessFile(this.queueFile, "rw"); + FileChannel fc = raf.getChannel(); + // block thread until lock is obtained + FileLock lock = fc.lock(); + raf.seek(raf.length()); + raf.writeBytes(rid); + raf.writeByte('\n'); + lock.release(); + fc.close(); + raf.close(); + return true; + } catch (FileNotFoundException e) { + logger.severe("Cannot open file "+this.queueFile+" for write."); + } catch (IOException e) { + logger.severe("Cannot write to file "+this.queueFile); + } + return false; + } + + /** Enqueues a list of report ids. */ + public boolean enqueue(LinkedList ids) { + try { + RandomAccessFile raf = new RandomAccessFile(this.queueFile, "rw"); + FileChannel fc = raf.getChannel(); + // block thread until lock is obtained + FileLock lock = fc.lock(); + raf.seek(raf.length()); + for (String rid : ids) { + raf.writeBytes(rid); + raf.writeByte('\n'); + } + lock.release(); + fc.close(); + raf.close(); + return true; + } catch (FileNotFoundException e) { + logger.severe("Cannot open file "+this.queueFile+" for write."); + } catch (IOException e) { + logger.severe("Cannot write to file "+this.queueFile); + } + return false; + } + + public boolean empty() { + // check the length of the file + File f = new File(this.queueFile); + return f.length() == 0L; + } + + public LinkedList dequeue() { + LinkedList ids = new LinkedList(); + + try { + RandomAccessFile raf = new RandomAccessFile(this.queueFile, "rw"); + FileChannel fc = raf.getChannel(); + FileLock flock = fc.lock(); + + while (true) { + String s = raf.readLine(); + if (s == null) + break; + s = s.trim(); + if (s.equals("")) + continue; + + ids.add(s); + } + + fc.truncate(0L); + + // release the lock + flock.release(); + fc.close(); + raf.close(); + } catch (FileNotFoundException e) { + logger.severe("Cannot open file "+this.queueFile+" for write."); + } catch (IOException e) { + logger.severe("Cannot write to file "+this.queueFile); + } + return ids; + } +} diff --git a/java/common/src/com/google/airbag/common/ReportQueueTest.java b/java/common/src/com/google/airbag/common/ReportQueueTest.java new file mode 100644 index 00000000..62f156ea --- /dev/null +++ b/java/common/src/com/google/airbag/common/ReportQueueTest.java @@ -0,0 +1,62 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.airbag.common; + +import java.io.IOException; +import java.util.LinkedList; + +/** + * A test class for ReportQueue implementations, currently tests + * ReportQueueDirImpl and ReportQueueFileImpl. + */ +public class ReportQueueTest { + + public static void main(String[] args) { + ReportQueue rq = new ReportQueueFileImpl("/tmp/rqtest"); + runTest(rq); + + try { + rq = new ReportQueueDirImpl("/tmp/rqdir"); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + runTest(rq); + + System.out.println("OK"); + } + + private static void runTest(ReportQueue rq) { + rq.enqueue("hello"); + rq.enqueue("world"); + + LinkedList v = rq.dequeue(); + assert v.size() == 2; + + assert v.get(0).equals("hello"); + assert v.get(1).equals("world"); + assert rq.empty(); + + v.remove(); + + rq.enqueue(v); + assert !rq.empty(); + + v = rq.dequeue(); + assert v.size() == 1; + assert v.get(0).equals("world"); + } +} diff --git a/java/common/src/com/google/airbag/common/ReportStorage.java b/java/common/src/com/google/airbag/common/ReportStorage.java new file mode 100644 index 00000000..0c01281c --- /dev/null +++ b/java/common/src/com/google/airbag/common/ReportStorage.java @@ -0,0 +1,134 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.google.airbag.common; + +import java.io.InputStream; +import java.io.IOException; +import java.io.FileNotFoundException; + +import java.util.SortedMap; + +/** + * ReportStorage.java + * + *

+ * Provide an abstract layer for storing crash reports and associated meta + * data. + *

+ * + *

+ * The interface is intended to be used by a client in the following way: + * + *

+ *     ReportStorage rs = new ReportStorageImpl(...);
+ *     // Write an uploading file to a storage
+ *     try {
+ *        String rid = rs.getUniqueId(attributes); // params from URL
+ *        rs.saveAttributes(id, attributes);
+ *        if (!rs.reportExists(rid) || allowOverwrite)
+ *           rs.writeStreamToReport(rid, input, 0);
+ *        else
+ *           rs.appendStreamToReport(rid, input, 0);
+ *     } catch (...)
+ *  
+ *     // Read a file from the storage
+ *     try {
+ *        OutputStream os = fs.openReportForRead(fid);
+ *        os.read(...);
+ *        os.close();
+ *     } catch (...)
+ * 
+ */ + +public interface ReportStorage { + /** + * Given a sorted map of attributes (name and value), returns a unique id + * of the crash report. + * + * @param params a sorted map from name to value + * @return a string as the file id + */ + public String getUniqueId(SortedMap params); + + /** + * Given a report id, checks if the report data AND attributes identified + * by this id exists on the storage. + * + * @param id a report id + * @return true if the id represents an existing file + */ + public boolean reportExists(String id); + + /** + * Given a report id and a sorted map of attributes, saves attributes on + * the storage. + * + * @param id a report id + * @param attrs attributes associated with this id + * @return true if attributes are saved successfully + */ + public boolean saveAttributes(String id, SortedMap attrs); + + /** + * Given a report id, returns attributes associated with this report. + * + * @param id a report id + * @return a sorted map from name to value + * @throws FileNotFoundException if fileExists(id) returns false + */ + public SortedMap getAttributes(String id) + throws FileNotFoundException; + + /** + * Writes max bytes from an input stream to a report identified by + * an id. + * + * @param id a report id + * @param input an input stream + * @param max + * maximum bytes to be written, if max is less or equal than 0, it will + * write all bytes from input stream to the file + * @return the number of bytes written + * @throws FileNotFoundException + * if reportExists(id) returns false + * @throws IOException + */ + public int writeStreamToReport(String id, InputStream input, int max) + throws FileNotFoundException, IOException; + + /** + * Opens a report for read. + * + * @param id a report id + * @return an output stream for read + * @throws FileNotFoundException + */ + public InputStream openReportForRead(String id) throws FileNotFoundException; + + /** + * @param id a report id + * @return the checksum of a report. + * @throws FileNotFoundException + */ + public String getChecksum(String id) throws FileNotFoundException; + + /** + * Removes a report. + * + * @param id a report id + * @return true if the report is removed successfully + */ + public boolean removeReport(String id) throws FileNotFoundException; +} diff --git a/java/common/src/com/google/airbag/common/ReportStorageLocalFileImpl.java b/java/common/src/com/google/airbag/common/ReportStorageLocalFileImpl.java new file mode 100644 index 00000000..9a6cf91f --- /dev/null +++ b/java/common/src/com/google/airbag/common/ReportStorageLocalFileImpl.java @@ -0,0 +1,197 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.google.airbag.common; + +import java.util.SortedMap; +import java.util.logging.Logger; + +import java.io.File; +import java.io.InputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.FileNotFoundException; + +/** + *

Implement FileStorage using a local file system.

+ * + *

Given a sorted map of attributes, create a checksum as unique file + * id.

+ * + *

Each file id is associated with two files in the storage: + *

    + *
  1. an attribute file named as .attr;
  2. + *
  3. a data file named as .data;
  4. + *
+ *

+ */ + +public class ReportStorageLocalFileImpl implements ReportStorage { + + // Name extension of attribute file + private static final String ATTR_FILE_EXT = ".attr"; + + // Name extension of data file + private static final String DATA_FILE_EXT = ".data"; + + // Set the maximum file length at 1M + private static final long MAX_ATTR_FILE_LENGTH = 1 << 10; + + // Logging + private static final Logger logger = + Logger.getLogger(ReportStorageLocalFileImpl.class.getName()); + + // Directory name for storing files. + private String directoryName; + + /** + * Creates an instance of ReportStorageLocalFileImpl by providing a + * directory name. + * + * @param dirname a directory for storing files + * @throws IOException + * @throws NullPointerException if dirname is null + */ + public ReportStorageLocalFileImpl(String dirname) throws IOException { + this.directoryName = dirname; + // new File can throw NullPointerException if dirname is null + File dir = new File(dirname); + if (!dir.exists() && !dir.mkdirs()) + throw new IOException("Cannot make dirs for "+dirname); + + if (!dir.canWrite()) + throw new IOException("Cannot write to "+dirname + +", check your permissions."); + } + + /** + * Returns a hashed string of attributes. Attributes are saved + * in the file storage if not exists. + */ + public String getUniqueId(SortedMap attributes) + { + String attr = CrashUtils.attributesToString(attributes); + return CrashUtils.dataSignature(attr.getBytes()); + } + + /** + * Saves attributes associated with a given id. if attributes does not + * match the id (comparing results of getUniqueId(attributes) + * with id), it returns false. Otherwise, attributes are saved. + */ + public boolean saveAttributes(String id, SortedMap attributes) + { + String attr = CrashUtils.attributesToString(attributes); + String digest = CrashUtils.dataSignature(attr.getBytes()); + + if (!digest.equals(id)) + return false; + + try { + File attrFile = new File(this.directoryName, digest+ATTR_FILE_EXT); + + // check if attr file exists + if (!attrFile.exists()) { + FileOutputStream fos = new FileOutputStream(attrFile); + fos.write(attr.getBytes()); + fos.close(); + } + + return true; + } catch (IOException e) { + e.printStackTrace(); + logger.warning(e.toString()); + return false; + } + } + + /** Checks if a report id exists. */ + public boolean reportExists(String id) { + File datafile = new File(this.directoryName, id+DATA_FILE_EXT); + File attrfile = new File(this.directoryName, id+ATTR_FILE_EXT); + return datafile.isFile() && attrfile.isFile(); + } + + /** Returns attributes in a map associated with an id. */ + public SortedMap getAttributes(String id) + throws FileNotFoundException + { + if (!this.reportExists(id)) + throw new FileNotFoundException("no file is identified by "+id); + + File attrfile = new File(this.directoryName, id+ATTR_FILE_EXT); + if (!attrfile.isFile()) + throw new FileNotFoundException("no file is identified by "+id); + + int length = (int) attrfile.length(); + if (length >= MAX_ATTR_FILE_LENGTH) + throw new FileNotFoundException("no file is identified by "+id); + + byte[] content = new byte[length]; + + try { + FileInputStream fis = new FileInputStream(attrfile); + fis.read(content); + fis.close(); + + // verify checksum + String sig = CrashUtils.dataSignature(content); + if (!sig.equals(id)) { + logger.warning("illegal access to "+attrfile); + return null; + } + + // parse contents + return CrashUtils.stringToAttributes(new String(content)); + + } catch (IOException e) { + logger.warning(e.toString()); + return null; + } + } + + public int writeStreamToReport(String id, InputStream input, int max) + throws FileNotFoundException, IOException { + File datafile = new File(this.directoryName, id + DATA_FILE_EXT); + FileOutputStream fos = new FileOutputStream(datafile); + + int bytesCopied = CrashUtils.copyStream(input, fos, max); + + fos.close(); + + return bytesCopied; + } + + public InputStream openReportForRead(String id) throws FileNotFoundException { + File datafile = new File(this.directoryName, id + DATA_FILE_EXT); + return new FileInputStream(datafile); + } + + public String getChecksum(String id) throws FileNotFoundException { + File datafile = new File(this.directoryName, id + DATA_FILE_EXT); + return CrashUtils.fileSignature(datafile); + } + + + public boolean removeReport(String id) { + File datafile = new File(this.directoryName, id + DATA_FILE_EXT); + File attrfile = new File(this.directoryName, id + ATTR_FILE_EXT); + + datafile.delete(); + attrfile.delete(); + + return true; + } +} diff --git a/java/common/src/com/google/airbag/common/ReportStorageTest.java b/java/common/src/com/google/airbag/common/ReportStorageTest.java new file mode 100644 index 00000000..dc17f56a --- /dev/null +++ b/java/common/src/com/google/airbag/common/ReportStorageTest.java @@ -0,0 +1,89 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package com.google.airbag.common; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.TreeMap; + +/** A simple regression test of ReportStorage.java and implementations. + */ + +public class ReportStorageTest { + public static void main(String[] args) { + // use /tmp/test as testing directory + ReportStorage rs = null; + try { + rs = new ReportStorageLocalFileImpl("/tmp/test"); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + + runTest(rs); + + // test passed + System.out.println("OK."); + } + + private static void runTest(ReportStorage rs) { + // test CrashUtil.bytesToHexString + byte[] ba = new byte[4]; + ba[0] = (byte)0xFF; + ba[1] = (byte)0x00; + ba[2] = (byte)0x0F; + ba[3] = (byte)0xF0; + String s = CrashUtils.bytesToHexString(ba); + assert s.equals("FF000FF0"); + + // construct a simple map of attributes + TreeMap params = + new TreeMap(); + params.put("Hello", "World"); + String rid = rs.getUniqueId(params); + assert rid != null; + + boolean b = rs.saveAttributes(rid, params); + assert b; + ba = "hellow, world!".getBytes(); + InputStream in = new ByteArrayInputStream(ba); + + assert rs.reportExists(rid); + + // save contents to storage + try { + rs.writeStreamToReport(rid, in, 0); + } catch (FileNotFoundException e) { + e.printStackTrace(); + System.exit(1); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + + // read contents out + try { + InputStream in1 = rs.openReportForRead(rid); + assert in1.available() == ba.length; + in1.read(ba); + assert(new String(ba).equals("hellow, world!")); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/java/common/src/com/google/airbag/common/SymbolStorage.java b/java/common/src/com/google/airbag/common/SymbolStorage.java new file mode 100644 index 00000000..5f0a3508 --- /dev/null +++ b/java/common/src/com/google/airbag/common/SymbolStorage.java @@ -0,0 +1,86 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.airbag.common; + +import java.io.IOException; +import java.io.InputStream; +import java.util.SortedMap; + +/** + *

SymbolStorage provides a simple interface for storing and retrieving + * symbol files. Symbol files are indexed by a set of attributes: + *

    + *
  • product name
  • + *
  • version (build id)
  • + *
  • platform
  • + *
  • application/module name
  • + *
+ *

+ * + *

+ * The interface is designed for a symbol server supports upload, getid, + * download operations. + *

+ */ + +public interface SymbolStorage { + /** + * Saves a symbol file indexed by a set of attributes. Attributes include + * product, version, platform, application/module, plus a checksum of + * the symbol file. + * + * If a symbol file whose checksum matches the attribute, the input stream + * can be NULL. No contents will be written. This can save some workloads + * of uploading symbols. + * + * @param attrs a map of attributes, it must have 'prod', 'ver', 'plat', + * 'app', and 'sum', values of the first four attributes are used + * as index, and the value of the last attribute is the MD5 checksum + * of the symbol file for verification. + * @param contents symbol file contents. + * @return true if checksum matches + * @throws IOException + */ + public boolean saveSymbolFile(SortedMap attrs, + InputStream contents) throws IOException; + + /** + * Gets the checksum of a symbol file indexed by a set of attributes. + * @param attrs a map of attributes, must include 'prod', 'ver', 'plat', + * and 'app'. + * @return MD5 checksum as a string + * @throws IOException + */ + public String getFileChecksum(SortedMap attrs) + throws IOException; + + /** + * Checks if a file exists already (identified by its checksum). + * @param checksum the file checksum + * @return true if a file with the same checksum exists + */ + public boolean fileExists(String checksum); + + /** + * Gets an input stream of a symbol server indexed by a set of attributes. + * @param attrs a map of attributes, must include 'prod', 'ver', 'plat', + * and 'app'. + * @return an input stream of the symbol file + * @throws IOException + */ + public InputStream openFileForRead(SortedMap attrs) + throws IOException; +} diff --git a/java/common/src/com/google/airbag/common/SymbolStorageLocalFileImpl.java b/java/common/src/com/google/airbag/common/SymbolStorageLocalFileImpl.java new file mode 100644 index 00000000..ed4e0e63 --- /dev/null +++ b/java/common/src/com/google/airbag/common/SymbolStorageLocalFileImpl.java @@ -0,0 +1,203 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.airbag.common; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Implementation of SymbolStorage interface using a local file system. + * + * Given a set of parameters, prod, ver, app, plat, computes its MD5 digest. + * Digest + .attr contains attributes and checksum of the symbol file. + * Symbol file content is stored in a file named by its checksum + .data. + * + * To upload a symbol file, a client can send a query: + *
+ *   /symexists?sum=, the server checks if a symbol file identified by checksum exists;
+ *   /upload?prod=&ver=&plat=&app=&sum= use POST
+ *   method with or without uploading a file.
+ *   
+ *   A client can always call /upload to upload a symbol file. 
+ *   However, a more efficient way is to check whether a file is on the server by calling /symexists.
+ *   If so, the client can just POST the request without actually upload the file content,
+ *   checksum is sufficient.
+ * 
+ *   /getchecksum?prod=&ver=&plat=&app=, returns the checksum
+ *   of the symbol file on the server.
+ *   /download?prod=&ver=&plat=&app=, downloads the symbol file
+ *   on the server.
+ *   
+ *   A client can always use /download to download a symbol file.
+ *   However, if the client maintins its own cache of symbol files, it can call /getchecksum,
+ *   and look up the cache using the checksum. If the cache does not have the file, then it 
+ *   calls /download.
+ * 
+ */ + +public class SymbolStorageLocalFileImpl implements SymbolStorage { + + // Name extension of attribute file + private static final String ATTR_FILE_EXT = ".attr"; + + // Name extension of data file + private static final String DATA_FILE_EXT = ".data"; + + private static final int MAX_ATTR_FILE_LENGTH = 1 << 10; + + // Directory name for storing files. + private String directoryName; + + /** + * Creates an instance of ReportStorageLocalFileImpl by providing a + * directory name. + * + * @param dirname + * @throws IOException + */ + public SymbolStorageLocalFileImpl(String dirname) throws IOException { + this.directoryName = dirname; + // new File can throw NullPointerException if dirname is null + File dir = new File(dirname); + if (!dir.exists() && !dir.mkdirs()) + throw new IOException("Cannot make dirs for "+dirname); + + if (!dir.canWrite()) + throw new IOException("Cannot write to "+dirname + +", check your permissions."); + } + + /** + * Save a symbol file. + */ + public boolean saveSymbolFile(SortedMap attrs, InputStream contents) + throws IOException { + String digest = getAttributesSignature(attrs); + + // get 'sum' value + String checksum = attrs.get(NameConstants.CHECKSUM_PNAME); + if (checksum == null) + return false; + + // attribute file name and data file name + File attrFile = new File(this.directoryName, digest+ATTR_FILE_EXT); + + // use passed in checksum as file name + File dataFile = new File(this.directoryName, checksum+DATA_FILE_EXT); + + // write data to file + FileOutputStream outs = new FileOutputStream(dataFile); + CrashUtils.copyStream(contents, outs, 0); + outs.close(); + + // get signature of input stream + String filesig = CrashUtils.fileSignature(dataFile); + + if (!checksum.equals(filesig)) { + dataFile.delete(); + return false; + } + + // save all attributes with checksum + String fullAttrs = CrashUtils.attributesToString(attrs); + + FileOutputStream fos = new FileOutputStream(attrFile); + fos.write(fullAttrs.getBytes()); + fos.close(); + + return true; + } + + + public String getFileChecksum(SortedMap attrs) throws IOException { + String digest = getAttributesSignature(attrs); + File attrFile = new File(this.directoryName, digest+ATTR_FILE_EXT); + + if (!attrFile.isFile()) + throw new FileNotFoundException(); + + int length = (int) attrFile.length(); + if (length >= MAX_ATTR_FILE_LENGTH) + throw new FileNotFoundException(); + + byte[] content = new byte[length]; + + FileInputStream fis = new FileInputStream(attrFile); + fis.read(content); + fis.close(); + + // parse contents + SortedMap savedAttrs = + CrashUtils.stringToAttributes(new String(content)); + + return savedAttrs.get(NameConstants.CHECKSUM_PNAME); + } + + public boolean fileExists(String checksum) { + File dataFile = new File(this.directoryName, checksum+DATA_FILE_EXT); + return dataFile.isFile(); + } + + public InputStream openFileForRead(SortedMap attrs) + throws IOException { + String checksum = getFileChecksum(attrs); + if (checksum == null) + throw new FileNotFoundException(); + + File dataFile = new File(this.directoryName, checksum + DATA_FILE_EXT); + if (!dataFile.isFile()) + throw new FileNotFoundException(); + + return new FileInputStream(dataFile); + } + + private static final String[] requiredParameters = { + NameConstants.PRODUCT_PNAME, + NameConstants.APPLICATION_PNAME, + NameConstants.PLATFORM_PNAME, + NameConstants.VERSION_PNAME + }; + + private String getAttributesSignature(SortedMap attrs) { + // canonize parameters + SortedMap params = canonizeAttributes(attrs); + String attrString = CrashUtils.attributesToString(params); + return CrashUtils.dataSignature(attrString.getBytes()); + } + /* Canonize attributes, get 'prod', 'ver', 'plat', and 'app' values, + * and put them in a new sorted map. If one of value is missing, + * returns null. + */ + private SortedMap + canonizeAttributes(SortedMap attrs) { + SortedMap params = new TreeMap(); + for (String s : requiredParameters) { + String v = attrs.get(s); + if (v == null) + return null; + else + params.put(s, v); + } + return params; + } + +} diff --git a/java/common/src/com/google/airbag/common/TestReportStorageConsumer.java b/java/common/src/com/google/airbag/common/TestReportStorageConsumer.java new file mode 100644 index 00000000..6b40dee2 --- /dev/null +++ b/java/common/src/com/google/airbag/common/TestReportStorageConsumer.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.airbag.common; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.TreeMap; + +/** + * Test ReportStorage and its implementation by simulating two pocesses + * as producer and consuer. + * + * To use this test with TestReportStorageProducer: + * > java TestReportStorageProducer /tmp/testdir + * + * In another console, + * > java TestReportStorageConsumer /tmp/testdir + * + * Then watch output on both console. + */ +public class TestReportStorageConsumer { + + /** + * @param args + */ + public static void main(String[] args) { + String testdir = args[0]; + ReportStorage rs = null; + try { + rs = new ReportStorageLocalFileImpl(testdir); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + + consume(rs); + } + + private static void consume(ReportStorage rs) { + int i = 0; + TreeMap params = + new TreeMap(); + String v = "hello"; + byte[] buf = new byte[1024]; + while (true) { + params.put(Integer.toString(i), v); + String id = rs.getUniqueId(params); + rs.saveAttributes(id, params); + if (rs.reportExists(id)) { + InputStream is = null; + try { + is = rs.openReportForRead(id); + while (is.read(buf) != -1) { + System.out.print(new String(buf)); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + break; + } catch (IOException e) { + e.printStackTrace(); + break; + } + } + i++; + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + break; + } + } + } +} diff --git a/java/common/src/com/google/airbag/common/TestReportStorageProducer.java b/java/common/src/com/google/airbag/common/TestReportStorageProducer.java new file mode 100644 index 00000000..b52296b6 --- /dev/null +++ b/java/common/src/com/google/airbag/common/TestReportStorageProducer.java @@ -0,0 +1,86 @@ +/* Copyright (C) 2006 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.google.airbag.common; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.TreeMap; + +/** + * Test ReportStorage and its implementation by simulating two pocesses + * as producer and consuer. + * + * To use this test with TestReportStorageConsumer: + * > java TestReportStorageProducer /tmp/testdir + * + * In another console, + * > java TestReportStorageConsumer /tmp/testdir + * + * Then watch output on both console. + */ + +public class TestReportStorageProducer { + + /** + * @param args + */ + public static void main(String[] args) { + String testdir = args[0]; + ReportStorage rs = null; + try { + rs = new ReportStorageLocalFileImpl(testdir); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + + produce(rs); + } + + private static void produce(ReportStorage rs) { + int i = 0; + TreeMap params = + new TreeMap(); + String v = "hello"; + ByteArrayInputStream ba = new ByteArrayInputStream("hello world!".getBytes()); + while (true) { + ba.reset(); + params.put(Integer.toString(i), v); + String id = rs.getUniqueId(params); + rs.saveAttributes(id, params); + if (id == null) { + System.exit(1); + } + try { + rs.writeStreamToReport(id, ba, 0); + } catch (FileNotFoundException e) { + e.printStackTrace(); + break; + } catch (IOException e) { + e.printStackTrace(); + break; + } + i++; + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + break; + } + } + } +}