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
17package com.android.exchange.adapter;
18
19import com.android.exchange.CommandStatusException.CommandStatus;
20import com.android.exchange.Eas;
21import com.android.mail.utils.LogUtils;
22
23import java.io.IOException;
24import java.io.InputStream;
25import 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 */
33public 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
63    public PingParser(final InputStream in) throws IOException {
64        super(in);
65    }
66
67    /**
68     * @return The status for this ping.
69     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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