blob: 3e20c1b0fcc9bf1998c1165261480782a233934c [file] [log] [blame]
/* Copyright (C) 2008-2009 Marc Blank
* Licensed to The Android Open Source Project.
*
* 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.android.exchange.adapter;
import com.android.mail.utils.LogUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
/**
* Parse the result of a Ping command.
* After {@link #parse()}, {@link #getPingStatus()} will give a valid status value. Also, when
* appropriate one of {@link #getSyncList()}, {@link #getMaxFolders()}, or
* {@link #getHeartbeatInterval()} will contain further detailed results of the parsing.
*/
public class PingParser extends Parser {
private static final String TAG = "PingParser";
/** Sentinel value, used when some property doesn't have a meaningful value. */
public static final int NO_VALUE = -1;
// The following are the actual status codes from the Exchange server.
// See http://msdn.microsoft.com/en-us/library/gg663456(v=exchg.80).aspx for more details.
/** Indicates that the heartbeat interval expired before a change happened. */
public static final int STATUS_EXPIRED = 1;
/** Indicates that one or more of the pinged folders changed. */
public static final int STATUS_CHANGES_FOUND = 2;
/** Indicates that the ping request was missing required parameters. */
public static final int STATUS_REQUEST_INCOMPLETE = 3;
/** Indicates that the ping request was malformed. */
public static final int STATUS_REQUEST_MALFORMED = 4;
/** Indicates that the ping request specified a bad heartbeat (too small or too big). */
public static final int STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS = 5;
/** Indicates that the ping requested more folders than the server will permit. */
public static final int STATUS_REQUEST_TOO_MANY_FOLDERS = 6;
/** Indicates that the folder structure is out of sync. */
public static final int STATUS_FOLDER_REFRESH_NEEDED = 7;
/** Indicates a server error. */
public static final int STATUS_SERVER_ERROR = 8;
private int mPingStatus = NO_VALUE;
private final ArrayList<String> mSyncList = new ArrayList<String>();
private int mMaxFolders = NO_VALUE;
private int mHeartbeatInterval = NO_VALUE;
public PingParser(final InputStream in) throws IOException {
super(in);
}
/**
* @return The status for this ping.
*/
public int getPingStatus() {
return mPingStatus;
}
/**
* If {@link #getPingStatus} indicates that there are folders to sync, this will return which
* folders need syncing.
* @return The list of folders to sync, or null if sync was not indicated in the response.
*/
public ArrayList<String> getSyncList() {
if (mPingStatus != STATUS_CHANGES_FOUND) {
return null;
}
return mSyncList;
}
/**
* If {@link #getPingStatus} indicates that we asked for too many folders, this will return the
* limit.
* @return The maximum number of folders we may ping, or {@link #NO_VALUE} if no maximum was
* indicated in the response.
*/
public int getMaxFolders() {
if (mPingStatus != STATUS_REQUEST_TOO_MANY_FOLDERS) {
return NO_VALUE;
}
return mMaxFolders;
}
/**
* If {@link #getPingStatus} indicates that we specified an invalid heartbeat, this will return
* a valid heartbeat to use.
* @return If our request asked for too small a heartbeat, this will return the minimum value
* permissible. If the request was too large, this will return the maximum value
* permissible. Otherwise, this returns {@link #NO_VALUE}.
*/
public int getHeartbeatInterval() {
if (mPingStatus != STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS) {
return NO_VALUE;
}
return mHeartbeatInterval;
}
/**
* Checks whether a status code implies we ought to send another ping immediately.
* @param pingStatus The ping status value we wish to check.
* @return Whether we should send another ping immediately.
*/
public static boolean shouldPingAgain(final int pingStatus) {
// Explanation for why we ping again for each case:
// - If the ping expired we should keep looping with pings.
// - The EAS spec says to handle incomplete and malformed request errors by pinging again
// with corrected request data. Since we always send a complete request, we simply
// repeat (and assume that some sort of network error is what caused the corruption).
// - Heartbeat errors are handled by pinging with a better heartbeat value.
// - Other server errors are considered transient and therefore we just reping for those.
return pingStatus == STATUS_EXPIRED
|| pingStatus == STATUS_REQUEST_INCOMPLETE
|| pingStatus == STATUS_REQUEST_MALFORMED
|| pingStatus == STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS
|| pingStatus == STATUS_SERVER_ERROR;
}
/**
* Parse the Folders element of the ping response, and store the results.
* @throws IOException
*/
private void parsePingFolders() throws IOException {
while (nextTag(Tags.PING_FOLDERS) != END) {
if (tag == Tags.PING_FOLDER) {
// Here we'll keep track of which mailboxes need syncing
String serverId = getValue();
mSyncList.add(serverId);
LogUtils.i(TAG, "Changes found in: %s", serverId);
} else {
skipTag();
}
}
}
/**
* Parse an integer value from the response for a particular property, and bounds check the
* new value. A property cannot be set more than once.
* @param name The name of the property we're parsing (for logging purposes).
* @param currentValue The current value of the property we're parsing.
* @param minValue The minimum value for the property we're parsing.
* @param maxValue The maximum value for the property we're parsing.
* @return The new value of the property we're parsing.
*/
private int getValue(final String name, final int currentValue, final int minValue,
final int maxValue) throws IOException {
if (currentValue != NO_VALUE) {
throw new IOException("Response has multiple values for " + name);
}
final int value = getValueInt();
if (value < minValue || (maxValue > 0 && value > maxValue)) {
throw new IOException(name + " out of bounds: " + value);
}
return value;
}
/**
* Parse an integer value from the response for a particular property, and ensure it is
* positive. A value cannot be set more than once.
* @param name The name of the property we're parsing (for logging purposes).
* @param currentValue The current value of the property we're parsing.
* @return The new value of the property we're parsing.
* @throws IOException
*/
private int getValue(final String name, final int currentValue) throws IOException {
return getValue(name, currentValue, 1, -1);
}
/**
* Parse the entire response, and set our internal state accordingly.
* @return Whether the response was well-formed.
* @throws IOException
*/
@Override
public boolean parse() throws IOException {
if (nextTag(START_DOCUMENT) != Tags.PING_PING) {
throw new IOException("Ping response does not include a Ping element");
}
while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
if (tag == Tags.PING_STATUS) {
mPingStatus = getValue("Status", mPingStatus, STATUS_EXPIRED,
STATUS_SERVER_ERROR);
} else if (tag == Tags.PING_MAX_FOLDERS) {
mMaxFolders = getValue("MaxFolders", mMaxFolders);
} else if (tag == Tags.PING_FOLDERS) {
if (!mSyncList.isEmpty()) {
throw new IOException("Response has multiple values for Folders");
}
parsePingFolders();
final int count = mSyncList.size();
LogUtils.i(TAG, "Folders has %d elements", count);
if (count == 0) {
throw new IOException("Folders was empty");
}
} else if (tag == Tags.PING_HEARTBEAT_INTERVAL) {
mHeartbeatInterval = getValue("HeartbeatInterval", mHeartbeatInterval);
} else {
// TODO: Error?
skipTag();
}
}
// Check the parse results for status values that don't match the other output.
switch (mPingStatus) {
case NO_VALUE:
throw new IOException("No status set in ping response");
case STATUS_CHANGES_FOUND:
if (mSyncList.isEmpty()) {
throw new IOException("No changes found in ping response");
}
break;
case STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS:
if (mHeartbeatInterval == NO_VALUE) {
throw new IOException("No value specified for heartbeat out of bounds");
}
break;
case STATUS_REQUEST_TOO_MANY_FOLDERS:
if (mMaxFolders == NO_VALUE) {
throw new IOException("No value specified for too many folders");
}
break;
}
return true;
}
}