aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfeng.qian.moz <feng.qian.moz@4c0a9323-5329-0410-9bdc-e9ce6186880e>2006-08-29 01:41:20 +0000
committerfeng.qian.moz <feng.qian.moz@4c0a9323-5329-0410-9bdc-e9ce6186880e>2006-08-29 01:41:20 +0000
commit68b748fc58cceaedb5c2a918237785bddfb0c27c (patch)
tree07198005915c229d0cbbc09b3993b947614278c0
parentInitial import, which includes the Windows client-side dump_syms tool, and (diff)
downloadbreakpad-68b748fc58cceaedb5c2a918237785bddfb0c27c.tar.xz
Common files for reporting and symbol server
git-svn-id: http://google-breakpad.googlecode.com/svn/trunk@5 4c0a9323-5329-0410-9bdc-e9ce6186880e
-rw-r--r--java/common/build.xml47
-rw-r--r--java/common/src/com/google/airbag/common/AbstractMinidumpServlet.java135
-rw-r--r--java/common/src/com/google/airbag/common/CrashUtils.java188
-rw-r--r--java/common/src/com/google/airbag/common/MultipartRequest.java36
-rw-r--r--java/common/src/com/google/airbag/common/NameConstants.java42
-rw-r--r--java/common/src/com/google/airbag/common/ReportQueue.java60
-rw-r--r--java/common/src/com/google/airbag/common/ReportQueueDirImpl.java115
-rw-r--r--java/common/src/com/google/airbag/common/ReportQueueFileImpl.java126
-rw-r--r--java/common/src/com/google/airbag/common/ReportQueueTest.java62
-rw-r--r--java/common/src/com/google/airbag/common/ReportStorage.java134
-rw-r--r--java/common/src/com/google/airbag/common/ReportStorageLocalFileImpl.java197
-rw-r--r--java/common/src/com/google/airbag/common/ReportStorageTest.java89
-rw-r--r--java/common/src/com/google/airbag/common/SymbolStorage.java86
-rw-r--r--java/common/src/com/google/airbag/common/SymbolStorageLocalFileImpl.java203
-rw-r--r--java/common/src/com/google/airbag/common/TestReportStorageConsumer.java87
-rw-r--r--java/common/src/com/google/airbag/common/TestReportStorageProducer.java86
16 files changed, 1693 insertions, 0 deletions
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 @@
+<project name="AirbagShared" default="compile" basedir=".">
+ <property name="src.home" value="${basedir}/src"/>
+ <property name="lib.home" value="${basedir}/../third-party"/>
+ <property name="dist.home" value="${basedir}/dist"/>
+ <property name="app.name" value="airbag-common"/>
+ <property name="doc.home" value="${basedir}/docs"/>
+ <property name="build.home" value="${basedir}/build"/>
+
+ <property name="compile.debug" value="true"/>
+ <property name="compile.deprecation" value="true"/>
+ <property name="compile.optimize" value="false"/>
+
+ <path id="compile.classpath">
+ <pathelement location="${lib.home}/servlet-api.jar"/>
+ </path>
+
+ <target name="compile"
+ description="Compile Java sources">
+ <mkdir dir="${build.home}"/>
+ <javac srcdir="${src.home}"
+ destdir="${build.home}"
+ debug="${compile.debug}"
+ deprecation="${compile.deprecation}"
+ optimize="${compile.optimize}">
+ <classpath refid="compile.classpath"/>
+ </javac>
+ </target>
+
+ <target name="javadoc" depends="compile"
+ description="Create Javadoc API documentation">
+ <mkdir dir="${dist.home}/docs"/>
+ <javadoc sourcepath="${src.home}"
+ destdir="${dist.home}/docs"
+ packagenames="*">
+ <classpath refid="compile.classpath"/>
+ </javadoc>
+ </target>
+
+ <target name="dist" depends="compile,javadoc"
+ description="Create binary distribution">
+ <mkdir dir="${dist.home}/docs"/>
+
+ <!-- Create JAR file -->
+ <jar jarfile="${dist.home}/${app.name}.jar"
+ basedir="${build.home}"/>
+ </target>
+</project>
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<String, String> 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<String, String> attributes) {
+ StringBuffer res = new StringBuffer();
+ for (Map.Entry<String, String> 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<String, String> stringToAttributes(String s) {
+ SortedMap<String, String> map =
+ new TreeMap<String, String>();
+ 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<String, String> 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 <b>java.util.Queue</b>. 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<String> 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<String> 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:
+ * <ul>
+ * <li>Ids must be valid file names;</li>
+ * <li>Ids cannot be duplicated, a duplicated id is ignored;</li>
+ * <li>No guarantees on ordering, in other words, this is not really
+ * a queue (with FIFO order);</li>
+ * </ul>
+ */
+
+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<String> 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<String> dequeue() {
+ // lock on the directory
+ LinkedList<String> rids = new LinkedList<String>();
+ 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<String> 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<String> dequeue() {
+ LinkedList<String> ids = new LinkedList<String>();
+
+ 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<String> 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
+ *
+ * <p>
+ * Provide an abstract layer for storing crash reports and associated meta
+ * data.
+ * </p>
+ *
+ * <p>
+ * The interface is intended to be used by a client in the following way:
+ *
+ * <pre>
+ * 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 (...)
+ * </pre>
+ */
+
+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<String, String> 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<String, String> 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<String, String> getAttributes(String id)
+ throws FileNotFoundException;
+
+ /**
+ * Writes <i>max</i> bytes from an input stream to a report identified by
+ * an <i>id</i>.
+ *
+ * @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;
+
+/**
+ * <p>Implement FileStorage using a local file system.</p>
+ *
+ * <p>Given a sorted map of attributes, create a checksum as unique file
+ * id.</p>
+ *
+ * <p>Each file id is associated with two files in the storage:
+ * <ol>
+ * <li>an attribute file named as <id>.attr;</li>
+ * <li>a data file named as <id>.data;</li>
+ * </ol>
+ * </p>
+ */
+
+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<String, String> 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<String, String> 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<String, String> 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<String, String> params =
+ new TreeMap<String, String>();
+ 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;
+
+/**
+ * <p>SymbolStorage provides a simple interface for storing and retrieving
+ * symbol files. Symbol files are indexed by a set of attributes:
+ * <ul>
+ * <li>product name</li>
+ * <li>version (build id)</li>
+ * <li>platform</li>
+ * <li>application/module name</li>
+ * </ul>
+ * </p>
+ *
+ * <p>
+ * The interface is designed for a symbol server supports upload, getid,
+ * download operations.
+ * </p>
+ */
+
+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<String, String> 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<String, String> 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<String, String> 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:
+ * <pre>
+ * /symexists?sum=<checksum>, the server checks if a symbol file identified by checksum exists;
+ * /upload?prod=<product>&ver=<version>&plat=<platform>&app=<module>&sum=<checksum> 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=<product>&ver=<version>&plat=<platform>&app=<module>, returns the checksum
+ * of the symbol file on the server.
+ * /download?prod=<product>&ver=<version>&plat=<platform>&app=<module>, 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.
+ * </pre>
+ */
+
+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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> attrs) {
+ // canonize parameters
+ SortedMap<String, String> 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<String, String>
+ canonizeAttributes(SortedMap<String, String> attrs) {
+ SortedMap<String, String> params = new TreeMap<String, String>();
+ 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<String, String> params =
+ new TreeMap<String, String>();
+ 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<String, String> params =
+ new TreeMap<String, String>();
+ 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;
+ }
+ }
+ }
+}