docs: public ResponseEvents sendEventGeneratingAction(EventGeneratingAction action, long timeout)
              throws IOException, EventTimeoutException, IllegalArgumentException, IllegalStateException
      {
          final ResponseEventsImpl responseEvents;
          final ResponseEventHandler responseEventHandler;
          final String internalActionId;
  
          if (action == null)
          {
              throw new IllegalArgumentException("Unable to send action: action is null.");
          }
          else if (action.getActionCompleteEventClass() == null)
          {
              throw new IllegalArgumentException(
                      "Unable to send action: actionCompleteEventClass for " + action.getClass().getName() + " is null.");
          }
          else if (!ResponseEvent.class.isAssignableFrom(action.getActionCompleteEventClass()))
          {
              throw new IllegalArgumentException(
                      "Unable to send action: actionCompleteEventClass (" + action.getActionCompleteEventClass().getName()
                              + ") for " + action.getClass().getName() + " is not a ResponseEvent.");
          }
  
          if (state != CONNECTED)
          {
              throw new IllegalStateException(
                      "Actions may only be sent when in state " + "CONNECTED but connection is in state " + state);
          }
  
          responseEvents = new ResponseEventsImpl();
          responseEventHandler = new ResponseEventHandler(responseEvents, action.getActionCompleteEventClass());
  
          internalActionId = createInternalActionId();
  
          try
          {
              // register response handler...
              synchronized (this.responseListeners)
              {
                  this.responseListeners.put(internalActionId, responseEventHandler);
              }
  
              // ...and event handler.
              synchronized (this.responseEventListeners)
              {
                  this.responseEventListeners.put(internalActionId, responseEventHandler);
              }
  
              writer.sendAction(action, internalActionId);
              // only wait if response has not yet arrived.
              if (responseEvents.getResponse() == null || !responseEvents.isComplete())
              {
                  try
                  {
                      responseEvents.await(timeout);
                  }
                  catch (InterruptedException e)
                  {
                      logger.warn("Interrupted while waiting for response events.");
                      Thread.currentThread().interrupt();
                  }
              }
  
              // still no response or not all events received and timed out?
              if (responseEvents.getResponse() == null || !responseEvents.isComplete())
              {
                  throw new EventTimeoutException(
                          "Timeout waiting for response or response events to " + action.getAction()
                                  + (action.getActionId() == null ? "" : " (actionId: " + action.getActionId() + ")"),
                          responseEvents);
              }
  
          }
          finally
          {
              // remove the event handler
              synchronized (this.responseEventListeners)
              {
                  this.responseEventListeners.remove(internalActionId);
              }
  
              // Note: The response handler should have already been removed
              // when the response was received, however we remove it here
              // just in case it was never received.
              synchronized (this.responseListeners)
              {
                  this.responseListeners.remove(internalActionId);
              }
  
          }
  
          return responseEvents;
      }