Home | History | Annotate | Download | only in adapter
      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 
     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