1 /* Copyright (C) 2008-2009 Marc Blank 2 * Licensed to The Android Open Source Project. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.exchange.adapter; 18 19 import com.android.exchange.CommandStatusException.CommandStatus; 20 import com.android.exchange.Eas; 21 import com.android.mail.utils.LogUtils; 22 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.util.ArrayList; 26 27 /** 28 * Parse the result of a Ping command. 29 * After {@link #parse()}, {@link #getPingStatus()} will give a valid status value. Also, when 30 * appropriate one of {@link #getSyncList()}, {@link #getMaxFolders()}, or 31 * {@link #getHeartbeatInterval()} will contain further detailed results of the parsing. 32 */ 33 public class PingParser extends Parser { 34 private static final String TAG = Eas.LOG_TAG; 35 36 /** Sentinel value, used when some property doesn't have a meaningful value. */ 37 public static final int NO_VALUE = -1; 38 39 // The following are the actual status codes from the Exchange server. 40 // See http://msdn.microsoft.com/en-us/library/gg663456(v=exchg.80).aspx for more details. 41 /** Indicates that the heartbeat interval expired before a change happened. */ 42 public static final int STATUS_EXPIRED = 1; 43 /** Indicates that one or more of the pinged folders changed. */ 44 public static final int STATUS_CHANGES_FOUND = 2; 45 /** Indicates that the ping request was missing required parameters. */ 46 public static final int STATUS_REQUEST_INCOMPLETE = 3; 47 /** Indicates that the ping request was malformed. */ 48 public static final int STATUS_REQUEST_MALFORMED = 4; 49 /** Indicates that the ping request specified a bad heartbeat (too small or too big). */ 50 public static final int STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS = 5; 51 /** Indicates that the ping requested more folders than the server will permit. */ 52 public static final int STATUS_REQUEST_TOO_MANY_FOLDERS = 6; 53 /** Indicates that the folder structure is out of sync. */ 54 public static final int STATUS_FOLDER_REFRESH_NEEDED = 7; 55 /** Indicates a server error. */ 56 public static final int STATUS_SERVER_ERROR = 8; 57 58 private int mPingStatus = NO_VALUE; 59 private final ArrayList<String> mSyncList = new ArrayList<String>(); 60 private int mMaxFolders = NO_VALUE; 61 private int mHeartbeatInterval = NO_VALUE; 62 PingParser(final InputStream in)63 public PingParser(final InputStream in) throws IOException { 64 super(in); 65 } 66 67 /** 68 * @return The status for this ping. 69 */ getPingStatus()70 public int getPingStatus() { 71 return mPingStatus; 72 } 73 74 /** 75 * If {@link #getPingStatus} indicates that there are folders to sync, this will return which 76 * folders need syncing. 77 * @return The list of folders to sync, or null if sync was not indicated in the response. 78 */ getSyncList()79 public ArrayList<String> getSyncList() { 80 if (mPingStatus != STATUS_CHANGES_FOUND) { 81 return null; 82 } 83 return mSyncList; 84 } 85 86 /** 87 * If {@link #getPingStatus} indicates that we asked for too many folders, this will return the 88 * limit. 89 * @return The maximum number of folders we may ping, or {@link #NO_VALUE} if no maximum was 90 * indicated in the response. 91 */ getMaxFolders()92 public int getMaxFolders() { 93 if (mPingStatus != STATUS_REQUEST_TOO_MANY_FOLDERS) { 94 return NO_VALUE; 95 } 96 return mMaxFolders; 97 } 98 99 /** 100 * If {@link #getPingStatus} indicates that we specified an invalid heartbeat, this will return 101 * a valid heartbeat to use. 102 * @return If our request asked for too small a heartbeat, this will return the minimum value 103 * permissible. If the request was too large, this will return the maximum value 104 * permissible. Otherwise, this returns {@link #NO_VALUE}. 105 */ getHeartbeatInterval()106 public int getHeartbeatInterval() { 107 if (mPingStatus != STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS) { 108 return NO_VALUE; 109 } 110 return mHeartbeatInterval; 111 } 112 113 /** 114 * Checks whether a status code implies we ought to send another ping immediately. 115 * @param pingStatus The ping status value we wish to check. 116 * @return Whether we should send another ping immediately. 117 */ shouldPingAgain(final int pingStatus)118 public static boolean shouldPingAgain(final int pingStatus) { 119 // Explanation for why we ping again for each case: 120 // - If the ping expired we should keep looping with pings. 121 // - The EAS spec says to handle incomplete and malformed request errors by pinging again 122 // with corrected request data. Since we always send a complete request, we simply 123 // repeat (and assume that some sort of network error is what caused the corruption). 124 // - Heartbeat errors are handled by pinging with a better heartbeat value. 125 // - Other server errors are considered transient and therefore we just reping for those. 126 return pingStatus == STATUS_EXPIRED 127 || pingStatus == STATUS_REQUEST_INCOMPLETE 128 || pingStatus == STATUS_REQUEST_MALFORMED 129 || pingStatus == STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS 130 || pingStatus == STATUS_SERVER_ERROR; 131 } 132 133 /** 134 * Parse the Folders element of the ping response, and store the results. 135 * @throws IOException 136 */ parsePingFolders()137 private void parsePingFolders() throws IOException { 138 while (nextTag(Tags.PING_FOLDERS) != END) { 139 if (tag == Tags.PING_FOLDER) { 140 // Here we'll keep track of which mailboxes need syncing 141 String serverId = getValue(); 142 mSyncList.add(serverId); 143 LogUtils.i(TAG, "Changes found in: %s", serverId); 144 } else { 145 skipTag(); 146 } 147 } 148 } 149 150 /** 151 * Parse an integer value from the response for a particular property, and bounds check the 152 * new value. A property cannot be set more than once. 153 * @param name The name of the property we're parsing (for logging purposes). 154 * @param currentValue The current value of the property we're parsing. 155 * @param minValue The minimum value for the property we're parsing. 156 * @param maxValue The maximum value for the property we're parsing. 157 * @return The new value of the property we're parsing. 158 159 */ getValue(final String name, final int currentValue, final int minValue, final int maxValue)160 private int getValue(final String name, final int currentValue, final int minValue, 161 final int maxValue) throws IOException { 162 if (currentValue != NO_VALUE) { 163 throw new IOException("Response has multiple values for " + name); 164 } 165 final int value = getValueInt(); 166 if (value < minValue || (maxValue > 0 && value > maxValue)) { 167 throw new IOException(name + " out of bounds: " + value); 168 } 169 return value; 170 } 171 172 /** 173 * Parse an integer value from the response for a particular property, and ensure it is 174 * positive. A value cannot be set more than once. 175 * @param name The name of the property we're parsing (for logging purposes). 176 * @param currentValue The current value of the property we're parsing. 177 * @return The new value of the property we're parsing. 178 * @throws IOException 179 */ getValue(final String name, final int currentValue)180 private int getValue(final String name, final int currentValue) throws IOException { 181 return getValue(name, currentValue, 1, -1); 182 } 183 184 /** 185 * Parse the entire response, and set our internal state accordingly. 186 * @return Whether the response was well-formed. 187 * @throws IOException 188 */ 189 @Override parse()190 public boolean parse() throws IOException { 191 if (nextTag(START_DOCUMENT) != Tags.PING_PING) { 192 throw new IOException("Ping response does not include a Ping element"); 193 } 194 while (nextTag(START_DOCUMENT) != END_DOCUMENT) { 195 if (tag == Tags.PING_STATUS) { 196 mPingStatus = getValue("Status", mPingStatus, STATUS_EXPIRED, 197 CommandStatus.STATUS_MAX); 198 } else if (tag == Tags.PING_MAX_FOLDERS) { 199 mMaxFolders = getValue("MaxFolders", mMaxFolders); 200 } else if (tag == Tags.PING_FOLDERS) { 201 if (!mSyncList.isEmpty()) { 202 throw new IOException("Response has multiple values for Folders"); 203 } 204 parsePingFolders(); 205 final int count = mSyncList.size(); 206 LogUtils.d(TAG, "Folders has %d elements", count); 207 if (count == 0) { 208 throw new IOException("Folders was empty"); 209 } 210 } else if (tag == Tags.PING_HEARTBEAT_INTERVAL) { 211 mHeartbeatInterval = getValue("HeartbeatInterval", mHeartbeatInterval); 212 } else { 213 // TODO: Error? 214 skipTag(); 215 } 216 } 217 218 // Check the parse results for status values that don't match the other output. 219 220 switch (mPingStatus) { 221 case NO_VALUE: 222 throw new IOException("No status set in ping response"); 223 case STATUS_CHANGES_FOUND: 224 if (mSyncList.isEmpty()) { 225 throw new IOException("No changes found in ping response"); 226 } 227 break; 228 case STATUS_REQUEST_HEARTBEAT_OUT_OF_BOUNDS: 229 if (mHeartbeatInterval == NO_VALUE) { 230 throw new IOException("No value specified for heartbeat out of bounds"); 231 } 232 break; 233 case STATUS_REQUEST_TOO_MANY_FOLDERS: 234 if (mMaxFolders == NO_VALUE) { 235 throw new IOException("No value specified for too many folders"); 236 } 237 break; 238 } 239 return true; 240 } 241 } 242