Chromium Code Reviews| Index: chrome/android/java/src/org/chromium/chrome/browser/omaha/ResponseParser.java |
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omaha/ResponseParser.java b/chrome/android/java/src/org/chromium/chrome/browser/omaha/ResponseParser.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..04d6fd647989594f33d7bbb87c99c0fe8fe816c5 |
| --- /dev/null |
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/omaha/ResponseParser.java |
| @@ -0,0 +1,342 @@ |
| +// Copyright 2013 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +package org.chromium.chrome.browser.omaha; |
| + |
| +import android.text.TextUtils; |
| +import android.util.Log; |
| + |
| +import org.xml.sax.Attributes; |
| +import org.xml.sax.InputSource; |
| +import org.xml.sax.SAXException; |
| +import org.xml.sax.SAXParseException; |
| +import org.xml.sax.helpers.DefaultHandler; |
| + |
| +import java.io.IOException; |
| +import java.io.StringReader; |
| +import java.util.ArrayList; |
| +import java.util.HashMap; |
| +import java.util.List; |
| +import java.util.Map; |
| +import java.util.Stack; |
| + |
| +import javax.xml.parsers.ParserConfigurationException; |
| +import javax.xml.parsers.SAXParser; |
| +import javax.xml.parsers.SAXParserFactory; |
| + |
| +/** |
| + * Parses XML responses from the Omaha Update Server. |
| + * |
| + * Expects XML formatted like: |
| + * <?xml version="1.0" encoding="UTF-8"?> |
| + * <response protocol="3.0" server="prod"> |
| + * <daystart elapsed_seconds="65524"/> |
| + * <app appid="{appid}" status="ok"> |
| + * <updatecheck status="ok"> |
| + * <urls> |
| + * <url codebase="https://market.android.com/details?id=com.google.android.apps.chrome/"/> |
| + * </urls> |
| + * <manifest version="0.16.4130.199"> |
| + * <packages> |
| + * <package hash="0" name="dummy.apk" required="true" size="0"/> |
| + * </packages> |
| + * <actions> |
| + * <action event="install" run="dummy.apk"/> |
| + * <action event="postinstall"/> |
| + * </actions> |
| + * </manifest> |
| + * </updatecheck> |
| + * <ping status="ok"/> |
| + * </app> |
| + * </response> |
| + * |
| + * The appid is dependent on the version of Clank that we're using. |
| + */ |
| +public class ResponseParser extends DefaultHandler { |
| + private static final String TAG = "ResponseParser"; |
| + |
| + private static final class Node { |
| + public final String tag; |
| + public final Map<String, String> attributes; |
| + public final List<Node> children; |
| + |
| + public Node(String tagName) { |
| + tag = tagName; |
| + attributes = new HashMap<String, String>(); |
| + children = new ArrayList<Node>(); |
| + } |
| + } |
| + |
| + // Tags that we care to parse from the response. |
| + private static final String TAG_APP = "app"; |
| + private static final String TAG_DAYSTART = "daystart"; |
| + private static final String TAG_EVENT = "event"; |
| + private static final String TAG_MANIFEST = "manifest"; |
| + private static final String TAG_PING = "ping"; |
| + private static final String TAG_RESPONSE = "response"; |
| + private static final String TAG_UPDATECHECK = "updatecheck"; |
| + private static final String TAG_URL = "url"; |
| + private static final String TAG_URLS = "urls"; |
| + |
| + private final Node mRootNode; |
| + private final Stack<Node> mTagStack; |
| + |
| + private final String mAppId; |
| + private final boolean mSentInstallEvent; |
| + |
| + private Integer mDaystartSeconds; |
| + private String mMarketVersion; |
|
nyquist
2013/11/11 17:43:41
mPlayVersion ?
gone
2013/11/11 18:22:34
Done.
|
| + private String mUrl; |
| + private String mAppStatus; |
| + private String mUpdateStatus; |
| + |
| + private boolean mParsedInstallEvent; |
| + private boolean mParsedPing; |
| + private boolean mParsedUpdateCheck; |
| + |
| + public ResponseParser(String serverResponse, String appId, boolean sentInstallEvent) |
| + throws RequestFailureException { |
| + mRootNode = new Node(null); |
| + mTagStack = new Stack<Node>(); |
| + mTagStack.push(mRootNode); |
| + |
| + mAppId = appId; |
| + mSentInstallEvent = sentInstallEvent; |
| + |
| + try { |
| + SAXParserFactory factory = SAXParserFactory.newInstance(); |
| + SAXParser saxParser = factory.newSAXParser(); |
| + saxParser.parse(new InputSource(new StringReader(serverResponse)), this); |
| + } catch (IOException e) { |
| + throw new RequestFailureException("Hit IOException", e); |
| + } catch (ParserConfigurationException e) { |
| + throw new RequestFailureException("Hit ParserConfigurationException", e); |
| + } catch (SAXParseException e) { |
| + throw new RequestFailureException("Hit SAXParseException", e); |
| + } catch (SAXException e) { |
| + throw new RequestFailureException("Hit SAXException", e); |
| + } |
| + |
| + if (mTagStack.peek() != mRootNode) { |
| + throw new RequestFailureException("XML was malformed."); |
| + } |
| + |
| + parseRootNode(); |
| + } |
| + |
| + public int getDaystartSeconds() { |
|
waffles
2013/11/11 17:34:27
I'm curious about how Chrome on these platforms us
gone
2013/11/11 18:22:34
We actually used to use it to send a single ping p
|
| + return mDaystartSeconds; |
| + } |
| + |
| + public String getMarketVersion() { |
| + return mMarketVersion; |
| + } |
| + |
| + public String getURL() { |
| + return mUrl; |
| + } |
| + |
| + public String getUpdateStatus() { |
| + return mUpdateStatus; |
| + } |
| + |
| + @Override |
| + public void startElement(String uri, String localName, String qName, Attributes attributes) |
| + throws SAXException { |
| + if (mTagStack.empty()) { |
| + throw new SAXException("Tag stack is empty when it shouldn't be."); |
| + } |
| + |
| + Node currentNode = new Node(qName); |
| + mTagStack.peek().children.add(currentNode); |
| + mTagStack.push(currentNode); |
| + |
| + for (int i = 0; i < attributes.getLength(); ++i) { |
| + String attributeName = attributes.getLocalName(i); |
| + String attributeValue = attributes.getValue(attributeName); |
| + currentNode.attributes.put(attributeName, attributeValue); |
| + } |
| + } |
| + |
| + @Override |
| + public void endElement(String uri, String localName, String qName) throws SAXException { |
| + if (mTagStack.empty()) { |
| + throw new SAXException("Tried closing empty stack with " + qName); |
| + } else if (!TextUtils.equals(qName, mTagStack.peek().tag)) { |
| + throw new SAXException("Tried closing " + mTagStack.peek().tag + " with " + qName); |
| + } |
| + mTagStack.pop(); |
| + } |
| + |
| + private void resetParsedData() { |
| + mDaystartSeconds = null; |
| + mMarketVersion = null; |
| + mUrl = null; |
| + mUpdateStatus = null; |
| + mAppStatus = null; |
| + |
| + mParsedInstallEvent = false; |
| + mParsedPing = false; |
| + mParsedUpdateCheck = false; |
| + } |
| + |
| + private void parseRootNode() throws RequestFailureException { |
| + for (int i = 0; i < mRootNode.children.size(); ++i) { |
| + if (TextUtils.equals(TAG_RESPONSE, mRootNode.children.get(i).tag)) { |
| + boolean success = parseResponseNode(mRootNode.children.get(i)); |
| + if (mSentInstallEvent) { |
| + success &= mParsedInstallEvent && !mParsedPing && !mParsedUpdateCheck; |
| + } else { |
| + success &= !mParsedInstallEvent && mParsedPing && mParsedUpdateCheck; |
| + } |
|
waffles
2013/11/11 17:34:27
Why require that a response that contains an insta
gone
2013/11/11 18:22:34
It's just the way our client works... We don't tr
waffles
2013/11/11 18:48:40
From the server side, it is equivalent to bundle t
|
| + |
| + if (success) return; |
| + resetParsedData(); |
| + } |
| + } |
| + |
| + throw new RequestFailureException("Failed to parse XML root node."); |
| + } |
| + |
| + private boolean parseResponseNode(Node node) { |
| + boolean success = true; |
| + success &= TextUtils.equals("prod", node.attributes.get("server")); |
| + success &= TextUtils.equals("3.0", node.attributes.get("protocol")); |
|
gone
2013/11/08 22:05:32
How long will server protocol 3.0 be handled for?
nyquist
2013/11/11 17:43:41
And also, what is the expected client behavior whe
gone
2013/11/11 18:22:34
Could we just assume that the server got the reque
waffles
2013/11/11 18:48:40
The server is obligated to respond in protocol 3.0
|
| + |
| + for (int i = 0; i < node.children.size(); ++i) { |
| + Node current = node.children.get(i); |
| + if (TextUtils.equals(TAG_DAYSTART, current.tag)) { |
| + success &= parseDaystartNode(current); |
| + } else if (TextUtils.equals(TAG_APP, current.tag)) { |
| + success &= parseAppNode(current); |
| + } else { |
| + Log.w(TAG, "Ignoring unknown child of <" + node.tag + "> : " + current.tag); |
| + } |
| + } |
| + |
| + success &= mDaystartSeconds != null; |
| + |
| + if (!success) Log.e(TAG, "Failed to parse: " + node.tag); |
| + return success; |
| + } |
| + |
| + private boolean parseDaystartNode(Node node) { |
| + try { |
| + mDaystartSeconds = Integer.parseInt(node.attributes.get("elapsed_seconds")); |
| + return true; |
| + } catch (NumberFormatException e) { |
| + Log.e(TAG, "Failed to parse: " + node.tag); |
| + return false; |
| + } |
| + } |
| + |
| + private boolean parseAppNode(Node node) { |
| + boolean success = true; |
| + success &= TextUtils.equals(mAppId, node.attributes.get("appid")); |
| + |
| + mAppStatus = node.attributes.get("status"); |
| + if (TextUtils.equals("ok", mAppStatus)) { |
| + for (int i = 0; i < node.children.size(); ++i) { |
| + Node current = node.children.get(i); |
| + if (TextUtils.equals(TAG_UPDATECHECK, current.tag)) { |
| + success &= parseUpdatecheck(current); |
| + } else if (TextUtils.equals(TAG_EVENT, current.tag)) { |
| + success &= parseEvent(current); |
| + } else if (TextUtils.equals(TAG_PING, current.tag)) { |
| + success &= parsePing(current); |
| + } else { |
| + Log.w(TAG, "Ignoring unknown child of <" + node.tag + "> : " + current.tag); |
| + } |
| + } |
| + } else if (TextUtils.equals("restricted", mAppStatus)) { |
| + // Chrome isn't allowed to run in this country. Pretend the request was fine. |
| + } else { |
| + success = false; |
| + } |
| + |
| + return success; |
| + } |
| + |
| + private boolean parseUpdatecheck(Node node) { |
| + boolean success = true; |
| + |
| + mUpdateStatus = node.attributes.get("status"); |
| + if (TextUtils.equals("ok", mUpdateStatus)) { |
| + for (int i = 0; i < node.children.size(); ++i) { |
| + Node current = node.children.get(i); |
| + if (TextUtils.equals(TAG_URLS, current.tag)) { |
| + success &= parseUrls(current); |
| + } else if (TextUtils.equals(TAG_MANIFEST, current.tag)) { |
| + success &= parseManifest(current); |
| + } else { |
| + Log.w(TAG, "Ignoring unknown child of <" + node.tag + "> : " + current.tag); |
| + } |
| + } |
| + } else if (TextUtils.equals("noupdate", mUpdateStatus)) { |
| + // No update is available. Don't bother searching for other attributes. |
| + } else { |
| + Log.w(TAG, "Ignoring unknown status for " + node.tag + ": " + mUpdateStatus); |
| + } |
| + |
| + if (success) { |
| + mParsedUpdateCheck = true; |
| + } else { |
| + Log.e(TAG, "Failed to parse: " + node.tag); |
| + } |
| + return success; |
| + } |
| + |
| + private boolean parsePing(Node node) { |
| + if (!TextUtils.equals("ok", node.attributes.get("status"))) { |
| + Log.e(TAG, "Failed to parse: " + node.tag); |
| + return false; |
| + } |
| + |
| + mParsedPing = true; |
| + return true; |
| + } |
| + |
| + private boolean parseEvent(Node node) { |
| + if (!TextUtils.equals("ok", node.attributes.get("status"))) { |
| + Log.e(TAG, "Failed to parse: " + node.tag); |
| + return false; |
| + } |
| + |
| + mParsedInstallEvent = true; |
| + return true; |
| + } |
| + |
| + private boolean parseUrls(Node node) { |
| + for (int i = 0; i < node.children.size(); ++i) { |
| + Node current = node.children.get(i); |
| + if (TextUtils.equals(TAG_URL, current.tag)) { |
| + parseUrl(current); |
| + } else { |
| + Log.w(TAG, "Ignoring unknown child of <" + node.tag + "> : " + current.tag); |
| + } |
| + } |
| + |
| + if (mUrl == null) { |
| + Log.e(TAG, "Failed to parse: " + node.tag); |
| + return false; |
| + } |
| + return true; |
| + } |
| + |
| + private void parseUrl(Node node) { |
| + String url = node.attributes.get("codebase"); |
| + |
| + // The URL gets a "/" tacked onto it by the Omaha server. Remove it. |
| + if (url.endsWith("/")) { |
| + url = url.substring(0, url.length() - 1); |
| + } |
| + |
| + if (url != null) mUrl = url; |
| + } |
| + |
| + private boolean parseManifest(Node node) { |
| + mMarketVersion = node.attributes.get("version"); |
| + return mMarketVersion != null; |
| + } |
| +} |