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