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}