001/*
002 * The MIT License
003 * Copyright (c) 2012 Microsoft Corporation
004 *
005 * Permission is hereby granted, free of charge, to any person obtaining a copy
006 * of this software and associated documentation files (the "Software"), to deal
007 * in the Software without restriction, including without limitation the rights
008 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
009 * copies of the Software, and to permit persons to whom the Software is
010 * furnished to do so, subject to the following conditions:
011 *
012 * The above copyright notice and this permission notice shall be included in
013 * all copies or substantial portions of the Software.
014 *
015 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
016 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
017 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
018 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
019 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
020 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
021 * THE SOFTWARE.
022 */
023
024package microsoft.exchange.webservices.data.core.request;
025
026import microsoft.exchange.webservices.data.core.EwsServiceMultiResponseXmlReader;
027import microsoft.exchange.webservices.data.core.EwsServiceXmlReader;
028import microsoft.exchange.webservices.data.core.ExchangeService;
029import microsoft.exchange.webservices.data.core.enumeration.misc.HangingRequestDisconnectReason;
030import microsoft.exchange.webservices.data.core.enumeration.misc.TraceFlags;
031import microsoft.exchange.webservices.data.core.exception.http.EWSHttpException;
032import microsoft.exchange.webservices.data.core.exception.misc.ArgumentException;
033import microsoft.exchange.webservices.data.core.exception.service.local.ServiceVersionException;
034import microsoft.exchange.webservices.data.core.exception.service.local.ServiceXmlDeserializationException;
035import microsoft.exchange.webservices.data.core.exception.service.remote.ServiceRequestException;
036import microsoft.exchange.webservices.data.core.exception.xml.XmlException;
037import microsoft.exchange.webservices.data.misc.HangingTraceStream;
038import microsoft.exchange.webservices.data.security.XmlNodeType;
039import org.apache.commons.io.IOUtils;
040import org.apache.commons.logging.Log;
041import org.apache.commons.logging.LogFactory;
042
043import javax.xml.stream.XMLStreamException;
044
045import java.io.ByteArrayOutputStream;
046import java.io.IOException;
047import java.io.InputStream;
048import java.io.ObjectStreamException;
049import java.net.SocketTimeoutException;
050import java.net.UnknownServiceException;
051import java.util.ArrayList;
052import java.util.List;
053import java.util.concurrent.ArrayBlockingQueue;
054import java.util.concurrent.ThreadPoolExecutor;
055import java.util.concurrent.TimeUnit;
056
057
058/**
059 * Represents an abstract, hanging service request.
060 */
061public abstract class HangingServiceRequestBase<T> extends ServiceRequestBase<T> {
062
063  private static final Log LOG = LogFactory.getLog(HangingServiceRequestBase.class);
064
065
066  public interface IHandleResponseObject {
067
068    /**
069     * Callback delegate to handle asynchronous response.
070     *
071     * @param response Response received from the server
072     * @throws ArgumentException
073     */
074    void handleResponseObject(Object response) throws ArgumentException;
075  }
076
077
078  public static final int BUFFER_SIZE = 4096;
079
080  /**
081   * Test switch to log all bytes that come across the wire.
082   * Helpful when parsing fails before certain bytes hit the trace logs.
083   */
084  private static volatile boolean logAllWireBytes = false;
085
086  /**
087   * Callback delegate to handle response objects
088   */
089  private IHandleResponseObject responseHandler;
090
091  /**
092   * Response from the server.
093   */
094  private HttpWebRequest response;
095
096  /**
097   * Expected minimum frequency in response, in milliseconds.
098   */
099  protected int heartbeatFrequencyMilliseconds;
100
101
102  public interface IHangingRequestDisconnectHandler {
103
104    /**
105     * Delegate method to handle a hanging request disconnection.
106     *
107     * @param sender the object invoking the delegate
108     * @param args event data
109     */
110    void hangingRequestDisconnectHandler(Object sender,
111        HangingRequestDisconnectEventArgs args);
112
113  }
114
115
116  public static boolean isLogAllWireBytes() {
117    return logAllWireBytes;
118  }
119
120  public static void setLogAllWireBytes(final boolean logAllWireBytes) {
121    HangingServiceRequestBase.logAllWireBytes = logAllWireBytes;
122  }
123
124  /**
125   * Disconnect events Occur when the hanging request is disconnected.
126   */
127  private List<IHangingRequestDisconnectHandler> onDisconnectList =
128      new ArrayList<IHangingRequestDisconnectHandler>();
129
130  /**
131   * Set event to happen when property disconnect.
132   *
133   * @param disconnect disconnect event
134   */
135  public void addOnDisconnectEvent(IHangingRequestDisconnectHandler disconnect) {
136    onDisconnectList.add(disconnect);
137  }
138
139  /**
140   * Remove the event from happening when property disconnect.
141   *
142   * @param disconnect disconnect event
143   */
144  protected void removeDisconnectEvent(
145      IHangingRequestDisconnectHandler disconnect) {
146    onDisconnectList.remove(disconnect);
147  }
148
149  /**
150   * Clears disconnect events list.
151   */
152  protected void clearDisconnectEvents() {
153    onDisconnectList.clear();
154  }
155
156  /**
157   * Initializes a new instance of the HangingServiceRequestBase class.
158   *
159   * @param service            The service.
160   * @param handler            Callback delegate to handle response objects
161   * @param heartbeatFrequency Frequency at which we expect heartbeats, in milliseconds.
162   */
163  protected HangingServiceRequestBase(ExchangeService service,
164      IHandleResponseObject handler, int heartbeatFrequency)
165      throws ServiceVersionException {
166    super(service);
167    this.responseHandler = handler;
168    this.heartbeatFrequencyMilliseconds = heartbeatFrequency;
169  }
170
171  /**
172   * Exectures the request.
173   */
174  public void internalExecute() throws Exception {
175    synchronized (this) {
176      this.response = this.validateAndEmitRequest();
177      this.internalOnConnect();
178    }
179  }
180
181  /**
182   * Parses the response.
183   *
184   */
185  private void parseResponses() {
186    HangingTraceStream tracingStream = null;
187    ByteArrayOutputStream responseCopy = null;
188
189
190    try {
191      boolean traceEWSResponse = this.getService().isTraceEnabledFor(TraceFlags.EwsResponse);
192      InputStream responseStream = this.response.getInputStream();
193      tracingStream = new HangingTraceStream(responseStream,
194          this.getService());
195      //EWSServiceMultiResponseXmlReader. Create causes a read.
196
197      if (traceEWSResponse) {
198        responseCopy = new ByteArrayOutputStream();
199        tracingStream.setResponseCopy(responseCopy);
200      }
201
202      while (this.isConnected()) {
203        T responseObject;
204        if (traceEWSResponse) {
205          EwsServiceMultiResponseXmlReader ewsXmlReader =
206              EwsServiceMultiResponseXmlReader.create(tracingStream, getService());
207          responseObject = this.readResponse(ewsXmlReader);
208          this.responseHandler.handleResponseObject(responseObject);
209
210          // reset the stream collector.
211          responseCopy.close();
212          responseCopy = new ByteArrayOutputStream();
213          tracingStream.setResponseCopy(responseCopy);
214
215        } else {
216          EwsServiceMultiResponseXmlReader ewsXmlReader =
217              EwsServiceMultiResponseXmlReader.create(tracingStream, getService());
218          responseObject = this.readResponse(ewsXmlReader);
219          this.responseHandler.handleResponseObject(responseObject);
220        }
221      }
222    } catch (SocketTimeoutException ex) {
223      // The connection timed out.
224      this.disconnect(HangingRequestDisconnectReason.Timeout, ex);
225    } catch (UnknownServiceException ex) {
226      // Stream is closed, so disconnect.
227      this.disconnect(HangingRequestDisconnectReason.Exception, ex);
228    } catch (ObjectStreamException ex) {
229      // Stream is closed, so disconnect.
230      this.disconnect(HangingRequestDisconnectReason.Exception, ex);
231    } catch (IOException ex) {
232      // Stream is closed, so disconnect.
233      this.disconnect(HangingRequestDisconnectReason.Exception, ex);
234    } catch (UnsupportedOperationException ex) {
235      LOG.error(ex);
236      // This is thrown if we close the stream during a
237      //read operation due to a user method call.
238      // Trying to delay closing until the read finishes
239      //simply results in a long-running connection.
240      this.disconnect(HangingRequestDisconnectReason.UserInitiated, null);
241    } catch (Exception ex) {
242      // Stream is closed, so disconnect.
243      this.disconnect(HangingRequestDisconnectReason.Exception, ex);
244    } finally {
245      IOUtils.closeQuietly(responseCopy);
246    }
247  }
248
249  private boolean isConnected;
250
251  /**
252   * Gets a value indicating whether this instance is connected.
253   *
254   * @return true, if this instance is connected; otherwise, false
255   */
256  public boolean isConnected() {
257    return this.isConnected;
258  }
259
260  private void setIsConnected(boolean value) {
261    this.isConnected = value;
262  }
263
264  /**
265   * Disconnects the request.
266   */
267  public void disconnect() {
268    synchronized (this) {
269      IOUtils.closeQuietly(this.response);
270      this.disconnect(HangingRequestDisconnectReason.UserInitiated, null);
271    }
272  }
273
274  /**
275   * Disconnects the request with the specified reason and exception.
276   *
277   * @param reason    The reason.
278   * @param exception The exception.
279   */
280  public void disconnect(HangingRequestDisconnectReason reason, Exception exception) {
281    if (this.isConnected()) {
282      IOUtils.closeQuietly(this.response);
283      this.internalOnDisconnect(reason, exception);
284    }
285  }
286
287  /**
288   * Perform any bookkeeping needed when we connect
289   * @throws XMLStreamException the XML stream exception
290   */
291  private void internalOnConnect() throws XMLStreamException,
292      IOException, EWSHttpException {
293    if (!this.isConnected()) {
294      this.isConnected = true;
295
296      if (this.getService().isTraceEnabledFor(TraceFlags.EwsResponseHttpHeaders)) {
297        // Trace Http headers
298        this.getService().processHttpResponseHeaders(
299            TraceFlags.EwsResponseHttpHeaders,
300            this.response);
301      }
302      int poolSize = 1;
303
304      int maxPoolSize = 1;
305
306      long keepAliveTime = 10;
307
308      final ArrayBlockingQueue<Runnable> queue =
309          new ArrayBlockingQueue<Runnable>(
310              1);
311      ThreadPoolExecutor threadPool = new ThreadPoolExecutor(poolSize,
312          maxPoolSize,
313          keepAliveTime, TimeUnit.SECONDS, queue);
314      threadPool.execute(new Runnable() {
315        public void run() {
316          parseResponses();
317        }
318      });
319      threadPool.shutdown();
320    }
321  }
322
323  /**
324   * Perform any bookkeeping needed when we disconnect (cleanly or forcefully)
325   *
326   * @param reason    The reason.
327   * @param exception The exception.
328   */
329  private void internalOnDisconnect(HangingRequestDisconnectReason reason,
330      Exception exception) {
331    if (this.isConnected()) {
332      this.isConnected = false;
333      for (IHangingRequestDisconnectHandler disconnect : onDisconnectList) {
334        disconnect.hangingRequestDisconnectHandler(this,
335            new HangingRequestDisconnectEventArgs(reason, exception));
336      }
337    }
338  }
339
340  /**
341   * Reads any preamble data not part of the core response.
342   *
343   * @param ewsXmlReader The EwsServiceXmlReader.
344   * @throws Exception
345   */
346  @Override
347  protected void readPreamble(EwsServiceXmlReader ewsXmlReader)
348      throws Exception {
349    // Do nothing.
350    try {
351      ewsXmlReader.read(new XmlNodeType(XmlNodeType.START_DOCUMENT));
352    } catch (XmlException ex) {
353      throw new ServiceRequestException("The response received from the service didn't contain valid XML.", ex);
354    } catch (ServiceXmlDeserializationException ex) {
355      throw new ServiceRequestException("The response received from the service didn't contain valid XML.", ex);
356    }
357  }
358}