/**
   * Computes and returns the hash of the specified <tt>capsString</tt> using the specified
   * <tt>hashAlgorithm</tt>.
   *
   * @param hashAlgorithm the name of the algorithm to be used to generate the hash
   * @param capsString the capabilities string that we'd like to compute a hash for.
   * @return the hash of <tt>capsString</tt> computed by the specified <tt>hashAlgorithm</tt> or
   *     <tt>null</tt> if generating the hash has failed
   */
  private static String capsToHash(String hashAlgorithm, String capsString) {
    try {
      MessageDigest md = MessageDigest.getInstance(hashAlgorithm);
      byte[] digest = md.digest(capsString.getBytes());

      return Base64.encodeBytes(digest);
    } catch (NoSuchAlgorithmException nsae) {
      logger.error("Unsupported XEP-0115: Entity Capabilities hash algorithm: " + hashAlgorithm);
      return null;
    }
  }
 /**
  * Checks whether the carbon is supported by the server or not.
  *
  * @return <tt>true</tt> if carbon is supported by the server and <tt>false</tt> if not.
  */
 private boolean isCarbonSupported() {
   try {
     return jabberProvider
         .getDiscoveryManager()
         .discoverInfo(jabberProvider.getAccountID().getService())
         .containsFeature(CarbonPacketExtension.NAMESPACE);
   } catch (XMPPException e) {
     logger.warn("Failed to retrieve carbon support." + e.getMessage());
   }
   return false;
 }
  /**
   * Sends enable or disable carbon packet to the server.
   *
   * @param enable if <tt>true</tt> sends enable packet otherwise sends disable packet.
   */
  private void enableDisableCarbon(final boolean enable) {
    IQ iq =
        new IQ() {

          @Override
          public String getChildElementXML() {
            return "<" + (enable ? "enable" : "disable") + " xmlns='urn:xmpp:carbons:2' />";
          }
        };

    Packet response = null;
    try {
      PacketCollector packetCollector =
          jabberProvider
              .getConnection()
              .createPacketCollector(new PacketIDFilter(iq.getPacketID()));
      iq.setFrom(jabberProvider.getOurJID());
      iq.setType(IQ.Type.SET);
      jabberProvider.getConnection().sendPacket(iq);
      response = packetCollector.nextResult(SmackConfiguration.getPacketReplyTimeout());

      packetCollector.cancel();
    } catch (Exception e) {
      logger.error("Failed to enable carbon.", e);
    }

    isCarbonEnabled = false;

    if (response == null) {
      logger.error("Failed to enable carbon. No response is received.");
    } else if (response.getError() != null) {
      logger.error("Failed to enable carbon: " + response.getError());
    } else if (!(response instanceof IQ) || !((IQ) response).getType().equals(IQ.Type.RESULT)) {
      logger.error("Failed to enable carbon. The response is not correct.");
    } else {
      isCarbonEnabled = true;
    }
  }
  /**
   * Subscribes this provider as interested in receiving notifications for new mail messages from
   * Google mail services such as Gmail or Google Apps.
   */
  private void subscribeForGmailNotifications() {
    // first check support for the notification service
    String accountIDService = jabberProvider.getAccountID().getService();
    boolean notificationsAreSupported =
        jabberProvider.isFeatureSupported(accountIDService, NewMailNotificationIQ.NAMESPACE);

    if (!notificationsAreSupported) {
      if (logger.isDebugEnabled())
        logger.debug(
            accountIDService
                + " does not seem to provide a Gmail notification "
                + " service so we won't be trying to subscribe for it");
      return;
    }

    if (logger.isDebugEnabled())
      logger.debug(
          accountIDService
              + " seems to provide a Gmail notification "
              + " service so we will try to subscribe for it");

    ProviderManager providerManager = ProviderManager.getInstance();

    providerManager.addIQProvider(
        MailboxIQ.ELEMENT_NAME, MailboxIQ.NAMESPACE, new MailboxIQProvider());
    providerManager.addIQProvider(
        NewMailNotificationIQ.ELEMENT_NAME,
        NewMailNotificationIQ.NAMESPACE,
        new NewMailNotificationProvider());

    Connection connection = jabberProvider.getConnection();

    connection.addPacketListener(new MailboxIQListener(), new PacketTypeFilter(MailboxIQ.class));
    connection.addPacketListener(
        new NewMailNotificationListener(), new PacketTypeFilter(NewMailNotificationIQ.class));

    if (opSetPersPresence.getCurrentStatusMessage().equals(JabberStatusEnum.OFFLINE)) return;

    // create a query with -1 values for newer-than-tid and
    // newer-than-time attributes
    MailboxQueryIQ mailboxQuery = new MailboxQueryIQ();

    if (logger.isTraceEnabled())
      logger.trace(
          "sending mailNotification for acc: "
              + jabberProvider.getAccountID().getAccountUniqueID());
    jabberProvider.getConnection().sendPacket(mailboxQuery);
  }
  /**
   * Creates an html description of the specified mailbox.
   *
   * @param mailboxIQ the mailboxIQ that we are to describe.
   * @return an html description of <tt>mailboxIQ</tt>
   */
  private String createMailboxDescription(MailboxIQ mailboxIQ) {
    int threadCount = mailboxIQ.getThreadCount();

    String resourceHeaderKey =
        threadCount > 1 ? "service.gui.NEW_GMAIL_MANY_HEADER" : "service.gui.NEW_GMAIL_HEADER";

    String resourceFooterKey =
        threadCount > 1 ? "service.gui.NEW_GMAIL_MANY_FOOTER" : "service.gui.NEW_GMAIL_FOOTER";

    // FIXME Escape HTML!
    String newMailHeader =
        JabberActivator.getResources()
            .getI18NString(
                resourceHeaderKey,
                new String[] {
                  jabberProvider.getAccountID().getService(), // {0} - service name
                  mailboxIQ.getUrl(), // {1} - inbox URI
                  Integer.toString(threadCount) // {2} - thread count
                });

    StringBuilder message = new StringBuilder(newMailHeader);

    // we now start an html table for the threads.
    message.append("<table width=100% cellpadding=2 cellspacing=0 ");
    message.append("border=0 bgcolor=#e8eef7>");

    Iterator<MailThreadInfo> threads = mailboxIQ.threads();

    String maxThreadsStr =
        (String)
            JabberActivator.getConfigurationService()
                .getProperty(PNAME_MAX_GMAIL_THREADS_PER_NOTIFICATION);

    int maxThreads = 5;

    try {
      if (maxThreadsStr != null) maxThreads = Integer.parseInt(maxThreadsStr);
    } catch (NumberFormatException e) {
      if (logger.isDebugEnabled())
        logger.debug("Failed to parse max threads count: " + maxThreads + ". Going for default.");
    }

    // print a maximum of MAX_THREADS
    for (int i = 0; i < maxThreads && threads.hasNext(); i++) {
      message.append(threads.next().createHtmlDescription());
    }
    message.append("</table><br/>");

    if (threadCount > maxThreads) {
      String messageFooter =
          JabberActivator.getResources()
              .getI18NString(
                  resourceFooterKey,
                  new String[] {
                    mailboxIQ.getUrl(), // {0} - inbox URI
                    Integer.toString(threadCount - maxThreads) // {1} - thread count
                  });
      message.append(messageFooter);
    }

    return message.toString();
  }
/**
 * A straightforward implementation of the basic instant messaging operation set.
 *
 * @author Damian Minkov
 * @author Matthieu Helleringer
 * @author Alain Knaebel
 * @author Emil Ivov
 * @author Hristo Terezov
 */
public class OperationSetBasicInstantMessagingJabberImpl
    extends AbstractOperationSetBasicInstantMessaging implements OperationSetMessageCorrection {
  /** Our class logger */
  private static final Logger logger =
      Logger.getLogger(OperationSetBasicInstantMessagingJabberImpl.class);

  /** The maximum number of unread threads that we'd be notifying the user of. */
  private static final String PNAME_MAX_GMAIL_THREADS_PER_NOTIFICATION =
      "net.java.sip.communicator.impl.protocol.jabber." + "MAX_GMAIL_THREADS_PER_NOTIFICATION";

  /**
   * A table mapping contact addresses to full jids that can be used to target a specific resource
   * (rather than sending a message to all logged instances of a user).
   */
  private Map<String, StoredThreadID> jids = new Hashtable<String, StoredThreadID>();

  /** The most recent full JID used for the contact address. */
  private Map<String, String> recentJIDForAddress = new Hashtable<String, String>();
  /**
   * The smackMessageListener instance listens for incoming messages. Keep a reference of it so if
   * anything goes wrong we don't add two different instances.
   */
  private SmackMessageListener smackMessageListener = null;

  /**
   * Contains the complete jid of a specific user and the time that it was last used so that we
   * could remove it after a certain point.
   */
  public static class StoredThreadID {
    /** The time that we last sent or received a message from this jid */
    long lastUpdatedTime;

    /** The last chat used, this way we will reuse the thread-id */
    String threadID;
  }

  /** A prefix helps to make sure that thread ID's are unique across mutliple instances. */
  private static String prefix = StringUtils.randomString(5);

  /**
   * Keeps track of the current increment, which is appended to the prefix to forum a unique thread
   * ID.
   */
  private static long id = 0;

  /**
   * The number of milliseconds that we preserve threads with no traffic before considering them
   * dead.
   */
  private static final long JID_INACTIVITY_TIMEOUT = 10 * 60 * 1000; // 10 min.

  /**
   * Indicates the time of the last Mailbox report that we received from Google (if this is a Google
   * server we are talking to). Should be included in all following mailbox queries
   */
  private long lastReceivedMailboxResultTime = -1;

  /** The provider that created us. */
  private final ProtocolProviderServiceJabberImpl jabberProvider;

  /**
   * A reference to the persistent presence operation set that we use to match incoming messages to
   * <tt>Contact</tt>s and vice versa.
   */
  private OperationSetPersistentPresenceJabberImpl opSetPersPresence = null;

  /** The opening BODY HTML TAG: &ltbody&gt */
  private static final String OPEN_BODY_TAG = "<body>";

  /** The closing BODY HTML TAG: &ltbody&gt */
  private static final String CLOSE_BODY_TAG = "</body>";

  /** The html namespace used as feature XHTMLManager.namespace */
  private static final String HTML_NAMESPACE = "http://jabber.org/protocol/xhtml-im";

  /** List of filters to be used to filter which messages to handle current Operation Set. */
  private List<PacketFilter> packetFilters = new ArrayList<PacketFilter>();

  /** Whether carbon is enabled or not. */
  private boolean isCarbonEnabled = false;

  /**
   * Creates an instance of this operation set.
   *
   * @param provider a reference to the <tt>ProtocolProviderServiceImpl</tt> that created us and
   *     that we'll use for retrieving the underlying aim connection.
   */
  OperationSetBasicInstantMessagingJabberImpl(ProtocolProviderServiceJabberImpl provider) {
    this.jabberProvider = provider;

    packetFilters.add(new GroupMessagePacketFilter());
    packetFilters.add(new PacketTypeFilter(org.jivesoftware.smack.packet.Message.class));

    provider.addRegistrationStateChangeListener(new RegistrationStateListener());

    ProviderManager man = ProviderManager.getInstance();
    MessageCorrectionExtensionProvider extProvider = new MessageCorrectionExtensionProvider();
    man.addExtensionProvider(
        MessageCorrectionExtension.ELEMENT_NAME, MessageCorrectionExtension.NAMESPACE, extProvider);
  }

  /**
   * Create a Message instance with the specified UID, content type and a default encoding. This
   * method can be useful when message correction is required. One can construct the corrected
   * message to have the same UID as the message before correction.
   *
   * @param messageText the string content of the message.
   * @param contentType the MIME-type for <tt>content</tt>
   * @param messageUID the unique identifier of this message.
   * @return Message the newly created message
   */
  public Message createMessageWithUID(String messageText, String contentType, String messageUID) {
    return new MessageJabberImpl(messageText, contentType, DEFAULT_MIME_ENCODING, null, messageUID);
  }

  /**
   * Create a Message instance for sending arbitrary MIME-encoding content.
   *
   * @param content content value
   * @param contentType the MIME-type for <tt>content</tt>
   * @return the newly created message.
   */
  public Message createMessage(String content, String contentType) {
    return createMessage(content, contentType, DEFAULT_MIME_ENCODING, null);
  }

  /**
   * Create a Message instance for sending arbitrary MIME-encoding content.
   *
   * @param content content value
   * @param contentType the MIME-type for <tt>content</tt>
   * @param subject the Subject of the message that we'd like to create.
   * @param encoding the enconding of the message that we will be sending.
   * @return the newly created message.
   */
  @Override
  public Message createMessage(
      String content, String contentType, String encoding, String subject) {
    return new MessageJabberImpl(content, contentType, encoding, subject);
  }

  Message createMessage(String content, String contentType, String messageUID) {
    return new MessageJabberImpl(content, contentType, DEFAULT_MIME_ENCODING, null, messageUID);
  }

  /**
   * Determines wheter the protocol provider (or the protocol itself) support sending and receiving
   * offline messages. Most often this method would return true for protocols that support offline
   * messages and false for those that don't. It is however possible for a protocol to support these
   * messages and yet have a particular account that does not (i.e. feature not enabled on the
   * protocol server). In cases like this it is possible for this method to return true even when
   * offline messaging is not supported, and then have the sendMessage method throw an
   * OperationFailedException with code - OFFLINE_MESSAGES_NOT_SUPPORTED.
   *
   * @return <tt>true</tt> if the protocol supports offline messages and <tt>false</tt> otherwise.
   */
  public boolean isOfflineMessagingSupported() {
    return true;
  }

  /**
   * Determines wheter the protocol supports the supplied content type
   *
   * @param contentType the type we want to check
   * @return <tt>true</tt> if the protocol supports it and <tt>false</tt> otherwise.
   */
  public boolean isContentTypeSupported(String contentType) {
    return (contentType.equals(DEFAULT_MIME_TYPE) || contentType.equals(HTML_MIME_TYPE));
  }

  /**
   * Determines whether the protocol supports the supplied content type for the given contact.
   *
   * @param contentType the type we want to check
   * @param contact contact which is checked for supported contentType
   * @return <tt>true</tt> if the contact supports it and <tt>false</tt> otherwise.
   */
  @Override
  public boolean isContentTypeSupported(String contentType, Contact contact) {
    // by default we support default mime type, for other mimetypes
    // method must be overriden
    if (contentType.equals(DEFAULT_MIME_TYPE)) return true;
    else if (contentType.equals(HTML_MIME_TYPE)) {
      String toJID = recentJIDForAddress.get(contact.getAddress());

      if (toJID == null) toJID = contact.getAddress();

      return jabberProvider.isFeatureListSupported(toJID, HTML_NAMESPACE);
    }

    return false;
  }

  /**
   * Remove from our <tt>jids</tt> map all entries that have not seen any activity (i.e. neither
   * outgoing nor incoming messags) for more than JID_INACTIVITY_TIMEOUT. Note that this method is
   * not synchronous and that it is only meant for use by the {@link #getThreadIDForAddress(String)}
   * and {@link #putJidForAddress(String, String)}
   */
  private void purgeOldJids() {
    long currentTime = System.currentTimeMillis();

    Iterator<Map.Entry<String, StoredThreadID>> entries = jids.entrySet().iterator();

    while (entries.hasNext()) {
      Map.Entry<String, StoredThreadID> entry = entries.next();
      StoredThreadID target = entry.getValue();

      if (currentTime - target.lastUpdatedTime > JID_INACTIVITY_TIMEOUT) entries.remove();
    }
  }

  /**
   * Returns the last jid that the party with the specified <tt>address</tt> contacted us from or
   * <tt>null</tt>(or bare jid) if we don't have a jid for the specified <tt>address</tt> yet. The
   * method would also purge all entries that haven't seen any activity (i.e. no one has tried to
   * get or remap it) for a delay longer than <tt>JID_INACTIVITY_TIMEOUT</tt>.
   *
   * @param jid the <tt>jid</tt> that we'd like to obtain a threadID for.
   * @return the last jid that the party with the specified <tt>address</tt> contacted us from or
   *     <tt>null</tt> if we don't have a jid for the specified <tt>address</tt> yet.
   */
  String getThreadIDForAddress(String jid) {
    synchronized (jids) {
      purgeOldJids();
      StoredThreadID ta = jids.get(jid);

      if (ta == null) return null;

      ta.lastUpdatedTime = System.currentTimeMillis();

      return ta.threadID;
    }
  }

  /**
   * Maps the specified <tt>address</tt> to <tt>jid</tt>. The point of this method is to allow us to
   * send all messages destined to the contact with the specified <tt>address</tt> to the
   * <tt>jid</tt> that they last contacted us from.
   *
   * @param threadID the threadID of conversation.
   * @param jid the jid (i.e. address/resource) that the contact with the specified <tt>address</tt>
   *     last contacted us from.
   */
  private void putJidForAddress(String jid, String threadID) {
    synchronized (jids) {
      purgeOldJids();

      StoredThreadID ta = jids.get(jid);

      if (ta == null) {
        ta = new StoredThreadID();
        jids.put(jid, ta);
      }

      recentJIDForAddress.put(StringUtils.parseBareAddress(jid), jid);

      ta.lastUpdatedTime = System.currentTimeMillis();
      ta.threadID = threadID;
    }
  }

  /**
   * Helper function used to send a message to a contact, with the given extensions attached.
   *
   * @param to The contact to send the message to.
   * @param toResource The resource to send the message to or null if no resource has been specified
   * @param message The message to send.
   * @param extensions The XMPP extensions that should be attached to the message before sending.
   * @return The MessageDeliveryEvent that resulted after attempting to send this message, so the
   *     calling function can modify it if needed.
   */
  private MessageDeliveredEvent sendMessage(
      Contact to, ContactResource toResource, Message message, PacketExtension[] extensions) {
    if (!(to instanceof ContactJabberImpl))
      throw new IllegalArgumentException("The specified contact is not a Jabber contact." + to);

    assertConnected();

    org.jivesoftware.smack.packet.Message msg = new org.jivesoftware.smack.packet.Message();

    String toJID = null;

    if (toResource != null) {
      if (toResource.equals(ContactResource.BASE_RESOURCE)) {
        toJID = to.getAddress();
      } else toJID = ((ContactResourceJabberImpl) toResource).getFullJid();
    }

    if (toJID == null) {
      toJID = to.getAddress();
    }

    msg.setPacketID(message.getMessageUID());
    msg.setTo(toJID);

    for (PacketExtension ext : extensions) {
      msg.addExtension(ext);
    }

    if (logger.isTraceEnabled())
      logger.trace("Will send a message to:" + toJID + " chat.jid=" + toJID);

    MessageDeliveredEvent msgDeliveryPendingEvt =
        new MessageDeliveredEvent(message, to, toResource);

    MessageDeliveredEvent[] transformedEvents =
        messageDeliveryPendingTransform(msgDeliveryPendingEvt);

    if (transformedEvents == null || transformedEvents.length == 0) return null;

    for (MessageDeliveredEvent event : transformedEvents) {
      String content = event.getSourceMessage().getContent();

      if (message.getContentType().equals(HTML_MIME_TYPE)) {
        msg.setBody(Html2Text.extractText(content));

        // Check if the other user supports XHTML messages
        // make sure we use our discovery manager as it caches calls
        if (jabberProvider.isFeatureListSupported(toJID, HTML_NAMESPACE)) {
          // Add the XHTML text to the message
          XHTMLManager.addBody(msg, OPEN_BODY_TAG + content + CLOSE_BODY_TAG);
        }
      } else {
        // this is plain text so keep it as it is.
        msg.setBody(content);
      }

      // msg.addExtension(new Version());

      if (event.isMessageEncrypted() && isCarbonEnabled) {
        msg.addExtension(new CarbonPacketExtension.PrivateExtension());
      }

      MessageEventManager.addNotificationsRequests(msg, true, false, false, true);

      String threadID = getThreadIDForAddress(toJID);
      if (threadID == null) threadID = nextThreadID();

      msg.setThread(threadID);
      msg.setType(org.jivesoftware.smack.packet.Message.Type.chat);
      msg.setFrom(jabberProvider.getConnection().getUser());

      jabberProvider.getConnection().sendPacket(msg);

      putJidForAddress(toJID, threadID);
    }

    return new MessageDeliveredEvent(message, to, toResource);
  }

  /**
   * Sends the <tt>message</tt> to the destination indicated by the <tt>to</tt> contact.
   *
   * @param to the <tt>Contact</tt> to send <tt>message</tt> to
   * @param message the <tt>Message</tt> to send.
   * @throws java.lang.IllegalStateException if the underlying stack is not registered and
   *     initialized.
   * @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an instance of ContactImpl.
   */
  public void sendInstantMessage(Contact to, Message message)
      throws IllegalStateException, IllegalArgumentException {
    sendInstantMessage(to, null, message);
  }

  /**
   * Sends the <tt>message</tt> to the destination indicated by the <tt>to</tt>. Provides a default
   * implementation of this method.
   *
   * @param to the <tt>Contact</tt> to send <tt>message</tt> to
   * @param toResource the resource to which the message should be send
   * @param message the <tt>Message</tt> to send.
   * @throws java.lang.IllegalStateException if the underlying ICQ stack is not registered and
   *     initialized.
   * @throws java.lang.IllegalArgumentException if <tt>to</tt> is not an instance belonging to the
   *     underlying implementation.
   */
  @Override
  public void sendInstantMessage(Contact to, ContactResource toResource, Message message)
      throws IllegalStateException, IllegalArgumentException {
    MessageDeliveredEvent msgDelivered =
        sendMessage(to, toResource, message, new PacketExtension[0]);

    fireMessageEvent(msgDelivered);
  }

  /**
   * Replaces the message with ID <tt>correctedMessageUID</tt> sent to the contact <tt>to</tt> with
   * the message <tt>message</tt>
   *
   * @param to The contact to send the message to.
   * @param message The new message.
   * @param correctedMessageUID The ID of the message being replaced.
   */
  public void correctMessage(
      Contact to, ContactResource resource, Message message, String correctedMessageUID) {
    PacketExtension[] exts = new PacketExtension[1];
    exts[0] = new MessageCorrectionExtension(correctedMessageUID);
    MessageDeliveredEvent msgDelivered = sendMessage(to, resource, message, exts);
    msgDelivered.setCorrectedMessageUID(correctedMessageUID);
    fireMessageEvent(msgDelivered);
  }

  /**
   * Utility method throwing an exception if the stack is not properly initialized.
   *
   * @throws java.lang.IllegalStateException if the underlying stack is not registered and
   *     initialized.
   */
  private void assertConnected() throws IllegalStateException {
    if (opSetPersPresence == null) {
      throw new IllegalStateException(
          "The provider must be signed on the service before" + " being able to communicate.");
    } else opSetPersPresence.assertConnected();
  }

  /** Our listener that will tell us when we're registered to */
  private class RegistrationStateListener implements RegistrationStateChangeListener {
    /**
     * The method is called by a ProtocolProvider implementation whenever a change in the
     * registration state of the corresponding provider had occurred.
     *
     * @param evt ProviderStatusChangeEvent the event describing the status change.
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt) {
      if (logger.isDebugEnabled())
        logger.debug(
            "The provider changed state from: " + evt.getOldState() + " to: " + evt.getNewState());

      if (evt.getNewState() == RegistrationState.REGISTERING) {
        opSetPersPresence =
            (OperationSetPersistentPresenceJabberImpl)
                jabberProvider.getOperationSet(OperationSetPersistentPresence.class);

        if (smackMessageListener == null) {
          smackMessageListener = new SmackMessageListener();
        } else {
          // make sure this listener is not already installed in this
          // connection
          jabberProvider.getConnection().removePacketListener(smackMessageListener);
        }

        jabberProvider
            .getConnection()
            .addPacketListener(
                smackMessageListener,
                new AndFilter(packetFilters.toArray(new PacketFilter[packetFilters.size()])));
      } else if (evt.getNewState() == RegistrationState.REGISTERED) {
        new Thread(
                new Runnable() {
                  @Override
                  public void run() {
                    initAdditionalServices();
                  }
                })
            .start();
      } else if (evt.getNewState() == RegistrationState.UNREGISTERED
          || evt.getNewState() == RegistrationState.CONNECTION_FAILED
          || evt.getNewState() == RegistrationState.AUTHENTICATION_FAILED) {
        if (jabberProvider.getConnection() != null) {
          if (smackMessageListener != null)
            jabberProvider.getConnection().removePacketListener(smackMessageListener);
        }

        smackMessageListener = null;
      }
    }
  }

  /** Initialize additional services, like gmail notifications and message carbons. */
  private void initAdditionalServices() {
    // subscribe for Google (Gmail or Google Apps) notifications
    // for new mail messages.
    boolean enableGmailNotifications =
        jabberProvider
            .getAccountID()
            .getAccountPropertyBoolean("GMAIL_NOTIFICATIONS_ENABLED", false);

    if (enableGmailNotifications) subscribeForGmailNotifications();

    boolean enableCarbon =
        isCarbonSupported()
            && !jabberProvider
                .getAccountID()
                .getAccountPropertyBoolean(ProtocolProviderFactory.IS_CARBON_DISABLED, false);
    if (enableCarbon) {
      enableDisableCarbon(true);
    } else {
      isCarbonEnabled = false;
    }
  }

  /**
   * Sends enable or disable carbon packet to the server.
   *
   * @param enable if <tt>true</tt> sends enable packet otherwise sends disable packet.
   */
  private void enableDisableCarbon(final boolean enable) {
    IQ iq =
        new IQ() {

          @Override
          public String getChildElementXML() {
            return "<" + (enable ? "enable" : "disable") + " xmlns='urn:xmpp:carbons:2' />";
          }
        };

    Packet response = null;
    try {
      PacketCollector packetCollector =
          jabberProvider
              .getConnection()
              .createPacketCollector(new PacketIDFilter(iq.getPacketID()));
      iq.setFrom(jabberProvider.getOurJID());
      iq.setType(IQ.Type.SET);
      jabberProvider.getConnection().sendPacket(iq);
      response = packetCollector.nextResult(SmackConfiguration.getPacketReplyTimeout());

      packetCollector.cancel();
    } catch (Exception e) {
      logger.error("Failed to enable carbon.", e);
    }

    isCarbonEnabled = false;

    if (response == null) {
      logger.error("Failed to enable carbon. No response is received.");
    } else if (response.getError() != null) {
      logger.error("Failed to enable carbon: " + response.getError());
    } else if (!(response instanceof IQ) || !((IQ) response).getType().equals(IQ.Type.RESULT)) {
      logger.error("Failed to enable carbon. The response is not correct.");
    } else {
      isCarbonEnabled = true;
    }
  }

  /**
   * Checks whether the carbon is supported by the server or not.
   *
   * @return <tt>true</tt> if carbon is supported by the server and <tt>false</tt> if not.
   */
  private boolean isCarbonSupported() {
    try {
      return jabberProvider
          .getDiscoveryManager()
          .discoverInfo(jabberProvider.getAccountID().getService())
          .containsFeature(CarbonPacketExtension.NAMESPACE);
    } catch (XMPPException e) {
      logger.warn("Failed to retrieve carbon support." + e.getMessage());
    }
    return false;
  }

  /** The listener that we use in order to handle incoming messages. */
  @SuppressWarnings("unchecked")
  private class SmackMessageListener implements PacketListener {
    /**
     * Handles incoming messages and dispatches whatever events are necessary.
     *
     * @param packet the packet that we need to handle (if it is a message).
     */
    public void processPacket(Packet packet) {
      if (!(packet instanceof org.jivesoftware.smack.packet.Message)) return;

      org.jivesoftware.smack.packet.Message msg = (org.jivesoftware.smack.packet.Message) packet;

      boolean isForwardedSentMessage = false;
      if (msg.getBody() == null) {

        CarbonPacketExtension carbonExt =
            (CarbonPacketExtension) msg.getExtension(CarbonPacketExtension.NAMESPACE);
        if (carbonExt == null) return;

        isForwardedSentMessage =
            (carbonExt.getElementName() == CarbonPacketExtension.SENT_ELEMENT_NAME);
        List<ForwardedPacketExtension> extensions =
            carbonExt.getChildExtensionsOfType(ForwardedPacketExtension.class);
        if (extensions.isEmpty()) return;
        ForwardedPacketExtension forwardedExt = extensions.get(0);
        msg = forwardedExt.getMessage();
        if (msg == null || msg.getBody() == null) return;
      }

      Object multiChatExtension = msg.getExtension("x", "http://jabber.org/protocol/muc#user");

      // its not for us
      if (multiChatExtension != null) return;

      String userFullId = isForwardedSentMessage ? msg.getTo() : msg.getFrom();

      String userBareID = StringUtils.parseBareAddress(userFullId);

      boolean isPrivateMessaging = false;
      ChatRoom privateContactRoom = null;
      OperationSetMultiUserChatJabberImpl mucOpSet =
          (OperationSetMultiUserChatJabberImpl)
              jabberProvider.getOperationSet(OperationSetMultiUserChat.class);
      if (mucOpSet != null) privateContactRoom = mucOpSet.getChatRoom(userBareID);

      if (privateContactRoom != null) {
        isPrivateMessaging = true;
      }

      if (logger.isDebugEnabled()) {
        if (logger.isDebugEnabled())
          logger.debug("Received from " + userBareID + " the message " + msg.toXML());
      }

      Message newMessage = createMessage(msg.getBody(), DEFAULT_MIME_TYPE, msg.getPacketID());

      // check if the message is available in xhtml
      PacketExtension ext = msg.getExtension("http://jabber.org/protocol/xhtml-im");

      if (ext != null) {
        XHTMLExtension xhtmlExt = (XHTMLExtension) ext;

        // parse all bodies
        Iterator<String> bodies = xhtmlExt.getBodies();
        StringBuffer messageBuff = new StringBuffer();
        while (bodies.hasNext()) {
          String body = bodies.next();
          messageBuff.append(body);
        }

        if (messageBuff.length() > 0) {
          // we remove body tags around message cause their
          // end body tag is breaking
          // the visualization as html in the UI
          String receivedMessage =
              messageBuff
                  .toString()
                  // removes body start tag
                  .replaceAll("\\<[bB][oO][dD][yY].*?>", "")
                  // removes body end tag
                  .replaceAll("\\</[bB][oO][dD][yY].*?>", "");

          // for some reason &apos; is not rendered correctly
          // from our ui, lets use its equivalent. Other
          // similar chars(< > & ") seem ok.
          receivedMessage = receivedMessage.replaceAll("&apos;", "&#39;");

          newMessage = createMessage(receivedMessage, HTML_MIME_TYPE, msg.getPacketID());
        }
      }

      PacketExtension correctionExtension = msg.getExtension(MessageCorrectionExtension.NAMESPACE);
      String correctedMessageUID = null;
      if (correctionExtension != null) {
        correctedMessageUID =
            ((MessageCorrectionExtension) correctionExtension).getCorrectedMessageUID();
      }

      Contact sourceContact =
          opSetPersPresence.findContactByID((isPrivateMessaging ? userFullId : userBareID));
      if (msg.getType() == org.jivesoftware.smack.packet.Message.Type.error) {
        // error which is multichat and we don't know about the contact
        // is a muc message error which is missing muc extension
        // and is coming from the room, when we try to send message to
        // room which was deleted or offline on the server
        if (isPrivateMessaging && sourceContact == null) {
          if (privateContactRoom != null) {
            XMPPError error = packet.getError();
            int errorResultCode = ChatRoomMessageDeliveryFailedEvent.UNKNOWN_ERROR;

            if (error != null && error.getCode() == 403) {
              errorResultCode = ChatRoomMessageDeliveryFailedEvent.FORBIDDEN;
            }

            String errorReason = error.getMessage();

            ChatRoomMessageDeliveryFailedEvent evt =
                new ChatRoomMessageDeliveryFailedEvent(
                    privateContactRoom, null, errorResultCode, errorReason, new Date(), newMessage);
            ((ChatRoomJabberImpl) privateContactRoom).fireMessageEvent(evt);
          }

          return;
        }

        if (logger.isInfoEnabled()) logger.info("Message error received from " + userBareID);

        int errorResultCode = MessageDeliveryFailedEvent.UNKNOWN_ERROR;
        if (packet.getError() != null) {
          int errorCode = packet.getError().getCode();

          if (errorCode == 503) {
            org.jivesoftware.smackx.packet.MessageEvent msgEvent =
                (org.jivesoftware.smackx.packet.MessageEvent)
                    packet.getExtension("x", "jabber:x:event");
            if (msgEvent != null && msgEvent.isOffline()) {
              errorResultCode = MessageDeliveryFailedEvent.OFFLINE_MESSAGES_NOT_SUPPORTED;
            }
          }
        }

        if (sourceContact == null) {
          sourceContact = opSetPersPresence.createVolatileContact(userFullId, isPrivateMessaging);
        }

        MessageDeliveryFailedEvent ev =
            new MessageDeliveryFailedEvent(
                newMessage, sourceContact, correctedMessageUID, errorResultCode);

        // ev = messageDeliveryFailedTransform(ev);

        if (ev != null) fireMessageEvent(ev);
        return;
      }
      putJidForAddress(userFullId, msg.getThread());

      // In the second condition we filter all group chat messages,
      // because they are managed by the multi user chat operation set.
      if (sourceContact == null) {
        if (logger.isDebugEnabled())
          logger.debug("received a message from an unknown contact: " + userBareID);
        // create the volatile contact
        sourceContact = opSetPersPresence.createVolatileContact(userFullId, isPrivateMessaging);
      }

      Date timestamp = new Date();
      // Check for XEP-0091 timestamp (deprecated)
      PacketExtension delay = msg.getExtension("x", "jabber:x:delay");
      if (delay != null && delay instanceof DelayInformation) {
        timestamp = ((DelayInformation) delay).getStamp();
      }
      // check for XEP-0203 timestamp
      delay = msg.getExtension("delay", "urn:xmpp:delay");
      if (delay != null && delay instanceof DelayInfo) {
        timestamp = ((DelayInfo) delay).getStamp();
      }

      ContactResource resource = ((ContactJabberImpl) sourceContact).getResourceFromJid(userFullId);

      EventObject msgEvt = null;
      if (!isForwardedSentMessage)
        msgEvt =
            new MessageReceivedEvent(
                newMessage,
                sourceContact,
                resource,
                timestamp,
                correctedMessageUID,
                isPrivateMessaging,
                privateContactRoom);
      else msgEvt = new MessageDeliveredEvent(newMessage, sourceContact, timestamp);
      // msgReceivedEvt = messageReceivedTransform(msgReceivedEvt);
      if (msgEvt != null) fireMessageEvent(msgEvt);
    }
  }

  /** A filter that prevents this operation set from handling multi user chat messages. */
  private static class GroupMessagePacketFilter implements PacketFilter {
    /**
     * Returns <tt>true</tt> if <tt>packet</tt> is a <tt>Message</tt> and false otherwise.
     *
     * @param packet the packet that we need to check.
     * @return <tt>true</tt> if <tt>packet</tt> is a <tt>Message</tt> and false otherwise.
     */
    public boolean accept(Packet packet) {
      if (!(packet instanceof org.jivesoftware.smack.packet.Message)) return false;

      org.jivesoftware.smack.packet.Message msg = (org.jivesoftware.smack.packet.Message) packet;

      return !msg.getType().equals(org.jivesoftware.smack.packet.Message.Type.groupchat);
    }
  }

  /**
   * Subscribes this provider as interested in receiving notifications for new mail messages from
   * Google mail services such as Gmail or Google Apps.
   */
  private void subscribeForGmailNotifications() {
    // first check support for the notification service
    String accountIDService = jabberProvider.getAccountID().getService();
    boolean notificationsAreSupported =
        jabberProvider.isFeatureSupported(accountIDService, NewMailNotificationIQ.NAMESPACE);

    if (!notificationsAreSupported) {
      if (logger.isDebugEnabled())
        logger.debug(
            accountIDService
                + " does not seem to provide a Gmail notification "
                + " service so we won't be trying to subscribe for it");
      return;
    }

    if (logger.isDebugEnabled())
      logger.debug(
          accountIDService
              + " seems to provide a Gmail notification "
              + " service so we will try to subscribe for it");

    ProviderManager providerManager = ProviderManager.getInstance();

    providerManager.addIQProvider(
        MailboxIQ.ELEMENT_NAME, MailboxIQ.NAMESPACE, new MailboxIQProvider());
    providerManager.addIQProvider(
        NewMailNotificationIQ.ELEMENT_NAME,
        NewMailNotificationIQ.NAMESPACE,
        new NewMailNotificationProvider());

    Connection connection = jabberProvider.getConnection();

    connection.addPacketListener(new MailboxIQListener(), new PacketTypeFilter(MailboxIQ.class));
    connection.addPacketListener(
        new NewMailNotificationListener(), new PacketTypeFilter(NewMailNotificationIQ.class));

    if (opSetPersPresence.getCurrentStatusMessage().equals(JabberStatusEnum.OFFLINE)) return;

    // create a query with -1 values for newer-than-tid and
    // newer-than-time attributes
    MailboxQueryIQ mailboxQuery = new MailboxQueryIQ();

    if (logger.isTraceEnabled())
      logger.trace(
          "sending mailNotification for acc: "
              + jabberProvider.getAccountID().getAccountUniqueID());
    jabberProvider.getConnection().sendPacket(mailboxQuery);
  }

  /**
   * Creates an html description of the specified mailbox.
   *
   * @param mailboxIQ the mailboxIQ that we are to describe.
   * @return an html description of <tt>mailboxIQ</tt>
   */
  private String createMailboxDescription(MailboxIQ mailboxIQ) {
    int threadCount = mailboxIQ.getThreadCount();

    String resourceHeaderKey =
        threadCount > 1 ? "service.gui.NEW_GMAIL_MANY_HEADER" : "service.gui.NEW_GMAIL_HEADER";

    String resourceFooterKey =
        threadCount > 1 ? "service.gui.NEW_GMAIL_MANY_FOOTER" : "service.gui.NEW_GMAIL_FOOTER";

    // FIXME Escape HTML!
    String newMailHeader =
        JabberActivator.getResources()
            .getI18NString(
                resourceHeaderKey,
                new String[] {
                  jabberProvider.getAccountID().getService(), // {0} - service name
                  mailboxIQ.getUrl(), // {1} - inbox URI
                  Integer.toString(threadCount) // {2} - thread count
                });

    StringBuilder message = new StringBuilder(newMailHeader);

    // we now start an html table for the threads.
    message.append("<table width=100% cellpadding=2 cellspacing=0 ");
    message.append("border=0 bgcolor=#e8eef7>");

    Iterator<MailThreadInfo> threads = mailboxIQ.threads();

    String maxThreadsStr =
        (String)
            JabberActivator.getConfigurationService()
                .getProperty(PNAME_MAX_GMAIL_THREADS_PER_NOTIFICATION);

    int maxThreads = 5;

    try {
      if (maxThreadsStr != null) maxThreads = Integer.parseInt(maxThreadsStr);
    } catch (NumberFormatException e) {
      if (logger.isDebugEnabled())
        logger.debug("Failed to parse max threads count: " + maxThreads + ". Going for default.");
    }

    // print a maximum of MAX_THREADS
    for (int i = 0; i < maxThreads && threads.hasNext(); i++) {
      message.append(threads.next().createHtmlDescription());
    }
    message.append("</table><br/>");

    if (threadCount > maxThreads) {
      String messageFooter =
          JabberActivator.getResources()
              .getI18NString(
                  resourceFooterKey,
                  new String[] {
                    mailboxIQ.getUrl(), // {0} - inbox URI
                    Integer.toString(threadCount - maxThreads) // {1} - thread count
                  });
      message.append(messageFooter);
    }

    return message.toString();
  }

  public String getRecentJIDForAddress(String address) {
    return recentJIDForAddress.get(address);
  }

  /** Receives incoming MailNotification Packets */
  private class MailboxIQListener implements PacketListener {
    /**
     * Handles incoming <tt>MailboxIQ</tt> packets.
     *
     * @param packet the IQ that we need to handle in case it is a <tt>MailboxIQ</tt>.
     */
    public void processPacket(Packet packet) {
      if (packet != null && !(packet instanceof MailboxIQ)) return;

      MailboxIQ mailboxIQ = (MailboxIQ) packet;

      if (mailboxIQ.getTotalMatched() < 1) return;

      // Get a reference to a dummy volatile contact
      Contact sourceContact =
          opSetPersPresence.findContactByID(jabberProvider.getAccountID().getService());

      if (sourceContact == null)
        sourceContact =
            opSetPersPresence.createVolatileContact(jabberProvider.getAccountID().getService());

      lastReceivedMailboxResultTime = mailboxIQ.getResultTime();

      String newMail = createMailboxDescription(mailboxIQ);

      Message newMailMessage =
          new MessageJabberImpl(newMail, HTML_MIME_TYPE, DEFAULT_MIME_ENCODING, null);

      MessageReceivedEvent msgReceivedEvt =
          new MessageReceivedEvent(
              newMailMessage,
              sourceContact,
              new Date(),
              MessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED);

      fireMessageEvent(msgReceivedEvt);
    }
  }

  /** Receives incoming NewMailNotification Packets. */
  private class NewMailNotificationListener implements PacketListener {
    /**
     * Handles incoming <tt>NewMailNotificationIQ</tt> packets.
     *
     * @param packet the IQ that we need to handle in case it is a <tt>NewMailNotificationIQ</tt>.
     */
    public void processPacket(Packet packet) {
      if (packet != null && !(packet instanceof NewMailNotificationIQ)) return;

      // check whether we are still enabled.
      boolean enableGmailNotifications =
          jabberProvider
              .getAccountID()
              .getAccountPropertyBoolean("GMAIL_NOTIFICATIONS_ENABLED", false);

      if (!enableGmailNotifications) return;

      if (opSetPersPresence.getCurrentStatusMessage().equals(JabberStatusEnum.OFFLINE)) return;

      MailboxQueryIQ mailboxQueryIQ = new MailboxQueryIQ();

      if (lastReceivedMailboxResultTime != -1)
        mailboxQueryIQ.setNewerThanTime(lastReceivedMailboxResultTime);

      if (logger.isTraceEnabled())
        logger.trace(
            "send mailNotification for acc: " + jabberProvider.getAccountID().getAccountUniqueID());

      jabberProvider.getConnection().sendPacket(mailboxQueryIQ);
    }
  }

  /**
   * Returns the inactivity timeout in milliseconds.
   *
   * @return The inactivity timeout in milliseconds. Or -1 if undefined
   */
  public long getInactivityTimeout() {
    return JID_INACTIVITY_TIMEOUT;
  }

  /**
   * Adds additional filters for incoming messages. To be able to skip some messages.
   *
   * @param filter to add
   */
  public void addMessageFilters(PacketFilter filter) {
    this.packetFilters.add(filter);
  }

  /**
   * Returns the next unique thread id. Each thread id made up of a short alphanumeric prefix along
   * with a unique numeric value.
   *
   * @return the next thread id.
   */
  public static synchronized String nextThreadID() {
    return prefix + Long.toString(id++);
  }
}
  /**
   * Helper function used to send a message to a contact, with the given extensions attached.
   *
   * @param to The contact to send the message to.
   * @param toResource The resource to send the message to or null if no resource has been specified
   * @param message The message to send.
   * @param extensions The XMPP extensions that should be attached to the message before sending.
   * @return The MessageDeliveryEvent that resulted after attempting to send this message, so the
   *     calling function can modify it if needed.
   */
  private MessageDeliveredEvent sendMessage(
      Contact to, ContactResource toResource, Message message, PacketExtension[] extensions) {
    if (!(to instanceof ContactJabberImpl))
      throw new IllegalArgumentException("The specified contact is not a Jabber contact." + to);

    assertConnected();

    org.jivesoftware.smack.packet.Message msg = new org.jivesoftware.smack.packet.Message();

    String toJID = null;

    if (toResource != null) {
      if (toResource.equals(ContactResource.BASE_RESOURCE)) {
        toJID = to.getAddress();
      } else toJID = ((ContactResourceJabberImpl) toResource).getFullJid();
    }

    if (toJID == null) {
      toJID = to.getAddress();
    }

    msg.setPacketID(message.getMessageUID());
    msg.setTo(toJID);

    for (PacketExtension ext : extensions) {
      msg.addExtension(ext);
    }

    if (logger.isTraceEnabled())
      logger.trace("Will send a message to:" + toJID + " chat.jid=" + toJID);

    MessageDeliveredEvent msgDeliveryPendingEvt =
        new MessageDeliveredEvent(message, to, toResource);

    MessageDeliveredEvent[] transformedEvents =
        messageDeliveryPendingTransform(msgDeliveryPendingEvt);

    if (transformedEvents == null || transformedEvents.length == 0) return null;

    for (MessageDeliveredEvent event : transformedEvents) {
      String content = event.getSourceMessage().getContent();

      if (message.getContentType().equals(HTML_MIME_TYPE)) {
        msg.setBody(Html2Text.extractText(content));

        // Check if the other user supports XHTML messages
        // make sure we use our discovery manager as it caches calls
        if (jabberProvider.isFeatureListSupported(toJID, HTML_NAMESPACE)) {
          // Add the XHTML text to the message
          XHTMLManager.addBody(msg, OPEN_BODY_TAG + content + CLOSE_BODY_TAG);
        }
      } else {
        // this is plain text so keep it as it is.
        msg.setBody(content);
      }

      // msg.addExtension(new Version());

      if (event.isMessageEncrypted() && isCarbonEnabled) {
        msg.addExtension(new CarbonPacketExtension.PrivateExtension());
      }

      MessageEventManager.addNotificationsRequests(msg, true, false, false, true);

      String threadID = getThreadIDForAddress(toJID);
      if (threadID == null) threadID = nextThreadID();

      msg.setThread(threadID);
      msg.setType(org.jivesoftware.smack.packet.Message.Type.chat);
      msg.setFrom(jabberProvider.getConnection().getUser());

      jabberProvider.getConnection().sendPacket(msg);

      putJidForAddress(toJID, threadID);
    }

    return new MessageDeliveredEvent(message, to, toResource);
  }
/**
 * Keeps track of entity capabilities.
 *
 * <p>This work is based on Jonas Adahl's smack fork.
 *
 * @author Emil Ivov
 * @author Lyubomir Marinov
 */
public class EntityCapsManager {
  /**
   * The <tt>Logger</tt> used by the <tt>EntityCapsManager</tt> class and its instances for logging
   * output.
   */
  private static final Logger logger = Logger.getLogger(EntityCapsManager.class);

  /** Static OSGi bundle context used by this class. */
  private static BundleContext bundleContext;

  /** Configuration service instance used by this class. */
  private static ConfigurationService configService;

  /**
   * The prefix of the <tt>ConfigurationService</tt> properties which persist {@link
   * #caps2discoverInfo}.
   */
  private static final String CAPS_PROPERTY_NAME_PREFIX =
      "net.java.sip.communicator.impl.protocol.jabber.extensions.caps." + "EntityCapsManager.CAPS.";

  /**
   * An empty array of <tt>UserCapsNodeListener</tt> elements explicitly defined in order to reduce
   * unnecessary allocations.
   */
  private static final UserCapsNodeListener[] NO_USER_CAPS_NODE_LISTENERS =
      new UserCapsNodeListener[0];

  /** The node value to advertise. */
  private static String entityNode =
      OSUtils.IS_ANDROID ? "http://android.jitsi.org" : "http://jitsi.org";

  /**
   * The <tt>Map</tt> of <tt>Caps</tt> to <tt>DiscoverInfo</tt> which associates a node#ver with the
   * entity capabilities so that they don't have to be retrieved every time their necessary. Because
   * ver is constructed from the entity capabilities using a specific hash method, the hash method
   * is also associated with the entity capabilities along with the node and the ver in order to
   * disambiguate cases of equal ver values for different entity capabilities constructed using
   * different hash methods.
   */
  private static final Map<Caps, DiscoverInfo> caps2discoverInfo =
      new ConcurrentHashMap<Caps, DiscoverInfo>();

  /**
   * Map of Full JID -&gt; DiscoverInfo/null. In case of c2s connection the key is formed as
   * user@server/resource (resource is required) In case of link-local connection the key is formed
   * as user@host (no resource)
   */
  private final Map<String, Caps> userCaps = new ConcurrentHashMap<String, Caps>();

  /** CapsVerListeners gets notified when the version string is changed. */
  private final Set<CapsVerListener> capsVerListeners = new CopyOnWriteArraySet<CapsVerListener>();

  /** The current hash of our version and supported features. */
  private String currentCapsVersion = null;

  /**
   * The list of <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the
   * list of user caps nodes of this <tt>EntityCapsManager</tt>.
   */
  private final List<UserCapsNodeListener> userCapsNodeListeners =
      new LinkedList<UserCapsNodeListener>();

  static {
    ProviderManager.getInstance()
        .addExtensionProvider(
            CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE, new CapsProvider());
  }

  /**
   * Add {@link DiscoverInfo} to our caps database.
   *
   * <p><b>Warning</b>: The specified <tt>DiscoverInfo</tt> is trusted to be valid with respect to
   * the specified <tt>Caps</tt> for performance reasons because the <tt>DiscoverInfo</tt> should
   * have already been validated in order to be used elsewhere anyway.
   *
   * @param caps the <tt>Caps<tt/> i.e. the node, the hash and the ver for which a
   *     <tt>DiscoverInfo</tt> is to be added to our caps database.
   * @param info {@link DiscoverInfo} for the specified <tt>Caps</tt>.
   */
  public static void addDiscoverInfoByCaps(Caps caps, DiscoverInfo info) {
    cleanupDiscoverInfo(info);
    /*
     * DiscoverInfo carries the node we're now associating it with a
     * specific node so we'd better keep them in sync.
     */
    info.setNode(caps.getNodeVer());

    synchronized (caps2discoverInfo) {
      DiscoverInfo oldInfo = caps2discoverInfo.put(caps, info);

      /*
       * If the specified info is a new association for the specified
       * node, remember it across application instances in order to not
       * query for it over the network.
       */
      if ((oldInfo == null) || !oldInfo.equals(info)) {
        String xml = info.getChildElementXML();

        if ((xml != null) && (xml.length() != 0)) {
          getConfigService().setProperty(getCapsPropertyName(caps), xml);
        }
      }
    }
  }

  /**
   * Gets the name of the property in the <tt>ConfigurationService</tt> which is or is to be
   * associated with a specific <tt>Caps</tt> value.
   *
   * @param caps the <tt>Caps</tt> value for which the associated <tt>ConfigurationService</tt>
   *     property name is to be returned
   * @return the name of the property in the <tt>ConfigurationService</tt> which is or is to be
   *     associated with a specific <tt>Caps</tt> value
   */
  private static String getCapsPropertyName(Caps caps) {
    return CAPS_PROPERTY_NAME_PREFIX + caps.node + '#' + caps.hash + '#' + caps.ver;
  }

  /** Returns cached instance of {@link ConfigurationService}. */
  private static ConfigurationService getConfigService() {
    if (configService == null) {
      configService = ServiceUtils.getService(bundleContext, ConfigurationService.class);
    }
    return configService;
  }

  /**
   * Sets OSGi bundle context instance that will be used by this class.
   *
   * @param bundleContext the <tt>BundleContext</tt> instance to be used by this class or
   *     <tt>null</tt> to clear the reference.
   */
  public static void setBundleContext(BundleContext bundleContext) {
    if (bundleContext == null) {
      configService = null;
    }
    EntityCapsManager.bundleContext = bundleContext;
  }

  /**
   * Add a record telling what entity caps node a user has.
   *
   * @param user the user (Full JID)
   * @param node the node (of the caps packet extension)
   * @param hash the hashing algorithm used to calculate <tt>ver</tt>
   * @param ver the version (of the caps packet extension)
   * @param ext the ext (of the caps packet extension)
   * @param online indicates if the user is online
   */
  private void addUserCapsNode(
      String user, String node, String hash, String ver, String ext, boolean online) {
    if ((user != null) && (node != null) && (hash != null) && (ver != null)) {
      Caps caps = userCaps.get(user);

      if ((caps == null)
          || !caps.node.equals(node)
          || !caps.hash.equals(hash)
          || !caps.ver.equals(ver)) {
        caps = new Caps(node, hash, ver, ext);

        userCaps.put(user, caps);
      } else return;

      // Fire userCapsNodeAdded.
      UserCapsNodeListener[] listeners;

      synchronized (userCapsNodeListeners) {
        listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS);
      }
      if (listeners.length != 0) {
        String nodeVer = caps.getNodeVer();

        for (UserCapsNodeListener listener : listeners)
          listener.userCapsNodeAdded(user, nodeVer, online);
      }
    }
  }

  /**
   * Adds a specific <tt>UserCapsNodeListener</tt> to the list of <tt>UserCapsNodeListener</tt>s
   * interested in events notifying about changes in the list of user caps nodes of this
   * <tt>EntityCapsManager</tt>.
   *
   * @param listener the <tt>UserCapsNodeListener</tt> which is interested in events notifying about
   *     changes in the list of user caps nodes of this <tt>EntityCapsManager</tt>
   */
  public void addUserCapsNodeListener(UserCapsNodeListener listener) {
    if (listener == null) throw new NullPointerException("listener");
    synchronized (userCapsNodeListeners) {
      if (!userCapsNodeListeners.contains(listener)) userCapsNodeListeners.add(listener);
    }
  }

  /**
   * Remove records telling what entity caps node a contact has.
   *
   * @param contact the contact
   */
  public void removeContactCapsNode(Contact contact) {
    Caps caps = null;
    String lastRemovedJid = null;

    Iterator<String> iter = userCaps.keySet().iterator();
    while (iter.hasNext()) {
      String jid = iter.next();

      if (StringUtils.parseBareAddress(jid).equals(contact.getAddress())) {
        caps = userCaps.get(jid);
        lastRemovedJid = jid;
        iter.remove();
      }
    }

    // fire only for the last one, at the end the event out
    // of the protocol will be one and for the contact
    if (caps != null) {
      UserCapsNodeListener[] listeners;
      synchronized (userCapsNodeListeners) {
        listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS);
      }
      if (listeners.length != 0) {
        String nodeVer = caps.getNodeVer();

        for (UserCapsNodeListener listener : listeners)
          listener.userCapsNodeRemoved(lastRemovedJid, nodeVer, false);
      }
    }
  }

  /**
   * Remove a record telling what entity caps node a user has.
   *
   * @param user the user (Full JID)
   */
  public void removeUserCapsNode(String user) {
    Caps caps = userCaps.remove(user);

    // Fire userCapsNodeRemoved.
    if (caps != null) {
      UserCapsNodeListener[] listeners;

      synchronized (userCapsNodeListeners) {
        listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS);
      }
      if (listeners.length != 0) {
        String nodeVer = caps.getNodeVer();

        for (UserCapsNodeListener listener : listeners)
          listener.userCapsNodeRemoved(user, nodeVer, false);
      }
    }
  }

  /**
   * Removes a specific <tt>UserCapsNodeListener</tt> from the list of
   * <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the list of user
   * caps nodes of this <tt>EntityCapsManager</tt>.
   *
   * @param listener the <tt>UserCapsNodeListener</tt> which is no longer interested in events
   *     notifying about changes in the list of user caps nodes of this <tt>EntityCapsManager</tt>
   */
  public void removeUserCapsNodeListener(UserCapsNodeListener listener) {
    if (listener != null) {
      synchronized (userCapsNodeListeners) {
        userCapsNodeListeners.remove(listener);
      }
    }
  }

  /**
   * Gets the <tt>Caps</tt> i.e. the node, the hash and the ver of a user.
   *
   * @param user the user (Full JID)
   * @return the <tt>Caps</tt> i.e. the node, the hash and the ver of <tt>user</tt>
   */
  public Caps getCapsByUser(String user) {
    return userCaps.get(user);
  }

  /**
   * Get the discover info given a user name. The discover info is returned if the user has a
   * node#ver associated with it and the node#ver has a discover info associated with it.
   *
   * @param user user name (Full JID)
   * @return the discovered info
   */
  public DiscoverInfo getDiscoverInfoByUser(String user) {
    Caps caps = userCaps.get(user);

    return (caps == null) ? null : getDiscoverInfoByCaps(caps);
  }

  /**
   * Get our own caps version.
   *
   * @return our own caps version
   */
  public String getCapsVersion() {
    return currentCapsVersion;
  }

  /**
   * Get our own entity node.
   *
   * @return our own entity node.
   */
  public String getNode() {
    return entityNode;
  }

  /**
   * Set our own entity node.
   *
   * @param node the new node
   */
  public void setNode(String node) {
    entityNode = node;
  }

  /**
   * Retrieve DiscoverInfo for a specific node.
   *
   * @param caps the <tt>Caps</tt> i.e. the node, the hash and the ver
   * @return The corresponding DiscoverInfo or null if none is known.
   */
  public static DiscoverInfo getDiscoverInfoByCaps(Caps caps) {
    synchronized (caps2discoverInfo) {
      DiscoverInfo discoverInfo = caps2discoverInfo.get(caps);

      /*
       * If we don't have the discoverInfo in the runtime cache yet, we
       * may have it remembered in a previous application instance.
       */
      if (discoverInfo == null) {
        ConfigurationService configurationService = getConfigService();
        String capsPropertyName = getCapsPropertyName(caps);
        String xml = configurationService.getString(capsPropertyName);

        if ((xml != null) && (xml.length() != 0)) {
          IQProvider discoverInfoProvider =
              (IQProvider)
                  ProviderManager.getInstance()
                      .getIQProvider("query", "http://jabber.org/protocol/disco#info");

          if (discoverInfoProvider != null) {
            XmlPullParser parser = new MXParser();

            try {
              parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
              parser.setInput(new StringReader(xml));
              // Start the parser.
              parser.next();
            } catch (XmlPullParserException xppex) {
              parser = null;
            } catch (IOException ioex) {
              parser = null;
            }

            if (parser != null) {
              try {
                discoverInfo = (DiscoverInfo) discoverInfoProvider.parseIQ(parser);
              } catch (Exception ex) {
              }

              if (discoverInfo != null) {
                if (caps.isValid(discoverInfo)) caps2discoverInfo.put(caps, discoverInfo);
                else {
                  logger.error(
                      "Invalid DiscoverInfo for " + caps.getNodeVer() + ": " + discoverInfo);
                  /*
                   * The discoverInfo doesn't seem valid
                   * according to the caps which means that we
                   * must have stored invalid information.
                   * Delete the invalid information in order
                   * to not try to validate it again.
                   */
                  configurationService.removeProperty(capsPropertyName);
                }
              }
            }
          }
        }
      }
      return discoverInfo;
    }
  }

  /**
   * Removes from, to and packet-id from <tt>info</tt>.
   *
   * @param info the {@link DiscoverInfo} that we'd like to cleanup.
   */
  private static void cleanupDiscoverInfo(DiscoverInfo info) {
    info.setFrom(null);
    info.setTo(null);
    info.setPacketID(null);
  }

  /**
   * Gets the features of a specific <tt>DiscoverInfo</tt> in the form of a read-only
   * <tt>Feature</tt> <tt>Iterator<tt/> by calling the internal method {@link
   * DiscoverInfo#getFeatures()}.
   *
   * @param discoverInfo the <tt>DiscoverInfo</tt> the features of which are to be retrieved
   * @return a read-only <tt>Feature</tt> <tt>Iterator</tt> which lists the features of the
   *     specified <tt>discoverInfo</tt>
   */
  @SuppressWarnings("unchecked")
  private static Iterator<DiscoverInfo.Feature> getDiscoverInfoFeatures(DiscoverInfo discoverInfo) {
    Method getFeaturesMethod;

    try {
      getFeaturesMethod = DiscoverInfo.class.getDeclaredMethod("getFeatures");
    } catch (NoSuchMethodException nsmex) {
      throw new UndeclaredThrowableException(nsmex);
    }
    getFeaturesMethod.setAccessible(true);
    try {
      return (Iterator<DiscoverInfo.Feature>) getFeaturesMethod.invoke(discoverInfo);
    } catch (IllegalAccessException iaex) {
      throw new UndeclaredThrowableException(iaex);
    } catch (InvocationTargetException itex) {
      throw new UndeclaredThrowableException(itex);
    }
  }

  /**
   * Registers this Manager's listener with <tt>connection</tt>.
   *
   * @param connection the connection that we'd like this manager to register with.
   */
  public void addPacketListener(XMPPConnection connection) {
    PacketFilter filter =
        new AndFilter(
            new PacketTypeFilter(Presence.class),
            new PacketExtensionFilter(
                CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE));

    connection.addPacketListener(new CapsPacketListener(), filter);
  }

  /**
   * Adds <tt>listener</tt> to the list of {@link CapsVerListener}s that we notify when new features
   * occur and the version hash needs to be regenerated. The method would also notify
   * <tt>listener</tt> if our current caps version has been generated and is different than
   * <tt>null</tt>.
   *
   * @param listener the {@link CapsVerListener} we'd like to register.
   */
  public void addCapsVerListener(CapsVerListener listener) {
    synchronized (capsVerListeners) {
      if (capsVerListeners.contains(listener)) return;

      capsVerListeners.add(listener);

      if (currentCapsVersion != null) listener.capsVerUpdated(currentCapsVersion);
    }
  }

  /**
   * Removes <tt>listener</tt> from the list of currently registered {@link CapsVerListener}s.
   *
   * @param listener the {@link CapsVerListener} we'd like to unregister.
   */
  public void removeCapsVerListener(CapsVerListener listener) {
    synchronized (capsVerListeners) {
      capsVerListeners.remove(listener);
    }
  }

  /**
   * Notifies all currently registered {@link CapsVerListener}s that the version hash has changed.
   */
  private void fireCapsVerChanged() {
    List<CapsVerListener> listenersCopy = null;

    synchronized (capsVerListeners) {
      listenersCopy = new ArrayList<CapsVerListener>(capsVerListeners);
    }

    for (CapsVerListener listener : listenersCopy) listener.capsVerUpdated(currentCapsVersion);
  }

  /**
   * Computes and returns the hash of the specified <tt>capsString</tt> using the specified
   * <tt>hashAlgorithm</tt>.
   *
   * @param hashAlgorithm the name of the algorithm to be used to generate the hash
   * @param capsString the capabilities string that we'd like to compute a hash for.
   * @return the hash of <tt>capsString</tt> computed by the specified <tt>hashAlgorithm</tt> or
   *     <tt>null</tt> if generating the hash has failed
   */
  private static String capsToHash(String hashAlgorithm, String capsString) {
    try {
      MessageDigest md = MessageDigest.getInstance(hashAlgorithm);
      byte[] digest = md.digest(capsString.getBytes());

      return Base64.encodeBytes(digest);
    } catch (NoSuchAlgorithmException nsae) {
      logger.error("Unsupported XEP-0115: Entity Capabilities hash algorithm: " + hashAlgorithm);
      return null;
    }
  }

  /**
   * Converts the form field values in the <tt>ffValuesIter</tt> into a caps string.
   *
   * @param ffValuesIter the {@link Iterator} containing the form field values.
   * @param capsBldr a <tt>StringBuilder</tt> to which the caps string representing the form field
   *     values is to be appended
   */
  private static void formFieldValuesToCaps(Iterator<String> ffValuesIter, StringBuilder capsBldr) {
    SortedSet<String> fvs = new TreeSet<String>();

    while (ffValuesIter.hasNext()) fvs.add(ffValuesIter.next());

    for (String fv : fvs) capsBldr.append(fv).append('<');
  }

  /**
   * Calculates the <tt>String</tt> for a specific <tt>DiscoverInfo</tt> which is to be hashed in
   * order to compute the ver string for that <tt>DiscoverInfo</tt>.
   *
   * @param discoverInfo the <tt>DiscoverInfo</tt> for which the <tt>String</tt> to be hashed in
   *     order to compute its ver string is to be calculated
   * @return the <tt>String</tt> for <tt>discoverInfo</tt> which is to be hashed in order to compute
   *     its ver string
   */
  private static String calculateEntityCapsString(DiscoverInfo discoverInfo) {
    StringBuilder bldr = new StringBuilder();

    // Add identities
    {
      Iterator<DiscoverInfo.Identity> identities = discoverInfo.getIdentities();
      SortedSet<DiscoverInfo.Identity> is =
          new TreeSet<DiscoverInfo.Identity>(
              new Comparator<DiscoverInfo.Identity>() {
                public int compare(DiscoverInfo.Identity i1, DiscoverInfo.Identity i2) {
                  int category = i1.getCategory().compareTo(i2.getCategory());

                  if (category != 0) return category;

                  int type = i1.getType().compareTo(i2.getType());

                  if (type != 0) return type;

                  /*
                   * TODO Sort by xml:lang.
                   *
                   * Since sort by xml:lang is currently missing,
                   * use the last supported sort criterion i.e.
                   * type.
                   */
                  return type;
                }
              });

      if (identities != null) while (identities.hasNext()) is.add(identities.next());

      for (DiscoverInfo.Identity i : is) {
        bldr.append(i.getCategory())
            .append('/')
            .append(i.getType())
            .append("//")
            .append(i.getName())
            .append('<');
      }
    }

    // Add features
    {
      Iterator<DiscoverInfo.Feature> features = getDiscoverInfoFeatures(discoverInfo);
      SortedSet<String> fs = new TreeSet<String>();

      if (features != null) while (features.hasNext()) fs.add(features.next().getVar());

      for (String f : fs) bldr.append(f).append('<');
    }

    DataForm extendedInfo = (DataForm) discoverInfo.getExtension("x", "jabber:x:data");

    if (extendedInfo != null) {
      synchronized (extendedInfo) {
        SortedSet<FormField> fs =
            new TreeSet<FormField>(
                new Comparator<FormField>() {
                  public int compare(FormField f1, FormField f2) {
                    return f1.getVariable().compareTo(f2.getVariable());
                  }
                });

        FormField formType = null;

        for (Iterator<FormField> fieldsIter = extendedInfo.getFields(); fieldsIter.hasNext(); ) {
          FormField f = fieldsIter.next();
          if (!f.getVariable().equals("FORM_TYPE")) fs.add(f);
          else formType = f;
        }

        // Add FORM_TYPE values
        if (formType != null) formFieldValuesToCaps(formType.getValues(), bldr);

        // Add the other values
        for (FormField f : fs) {
          bldr.append(f.getVariable()).append('<');
          formFieldValuesToCaps(f.getValues(), bldr);
        }
      }
    }

    return bldr.toString();
  }

  /**
   * Calculates the ver string for the specified <tt>discoverInfo</tt>, identity type, name
   * features, and extendedInfo.
   *
   * @param discoverInfo the {@link DiscoverInfo} we'd be creating a ver <tt>String</tt> for
   */
  public void calculateEntityCapsVersion(DiscoverInfo discoverInfo) {
    setCurrentCapsVersion(
        discoverInfo,
        capsToHash(CapsPacketExtension.HASH_METHOD, calculateEntityCapsString(discoverInfo)));
  }

  /**
   * Set our own caps version.
   *
   * @param discoverInfo the {@link DiscoverInfo} that we'd like to map to the <tt>capsVersion</tt>.
   * @param capsVersion the new caps version
   */
  public void setCurrentCapsVersion(DiscoverInfo discoverInfo, String capsVersion) {
    Caps caps = new Caps(getNode(), CapsPacketExtension.HASH_METHOD, capsVersion, null);

    /*
     * DiscoverInfo carries the node and the ver and we're now setting a new
     * ver so we should update the DiscoveryInfo.
     */
    discoverInfo.setNode(caps.getNodeVer());

    if (!caps.isValid(discoverInfo)) {
      throw new IllegalArgumentException(
          "The specified discoverInfo must be valid with respect"
              + " to the specified capsVersion");
    }

    currentCapsVersion = capsVersion;
    addDiscoverInfoByCaps(caps, discoverInfo);
    fireCapsVerChanged();
  }

  /** The {@link PacketListener} that will be registering incoming caps. */
  private class CapsPacketListener implements PacketListener {
    /**
     * Handles incoming presence packets and maps jids to node#ver strings.
     *
     * @param packet the incoming presence <tt>Packet</tt> to be handled
     * @see PacketListener#processPacket(Packet)
     */
    public void processPacket(Packet packet) {
      CapsPacketExtension ext =
          (CapsPacketExtension)
              packet.getExtension(CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE);

      /*
       * Before Version 1.4 of XEP-0115: Entity Capabilities, the 'ver'
       * attribute was generated differently and the 'hash' attribute was
       * absent. The 'ver' attribute in Version 1.3 represents the
       * specific version of the client and thus does not provide a way to
       * validate the DiscoverInfo sent by the client. If
       * EntityCapsManager receives no 'hash' attribute, it will assume
       * the legacy format and will not cache it because the DiscoverInfo
       * to be received from the client later on will not be trustworthy.
       */
      String hash = ext.getHash();

      /* Google Talk web does not set hash but we need it to be cached */
      if (hash == null) hash = "";

      if (hash != null) {
        // Check it the packet indicates  that the user is online. We
        // will use this information to decide if we're going to send
        // the discover info request.
        boolean online = (packet instanceof Presence) && ((Presence) packet).isAvailable();

        if (online) {
          addUserCapsNode(
              packet.getFrom(), ext.getNode(), hash, ext.getVersion(), ext.getExtensions(), online);
        } else {
          removeUserCapsNode(packet.getFrom());
        }
      }
    }
  }

  /**
   * Implements an immutable value which stands for a specific node, a specific hash (algorithm) and
   * a specific ver.
   *
   * @author Lyubomir Marinov
   */
  public static class Caps {
    /** The hash (algorithm) of this <tt>Caps</tt> value. */
    public final String hash;

    /** The node of this <tt>Caps</tt> value. */
    public final String node;

    /** The ext info of this <tt>Caps</tt> value. */
    public String ext;

    /**
     * The String which is the concatenation of {@link #node} and the {@link #ver} separated by the
     * character '#'. Cached for the sake of efficiency.
     */
    private final String nodeVer;

    /** The ver of this <tt>Caps</tt> value. */
    public final String ver;

    /**
     * Initializes a new <tt>Caps</tt> instance which is to represent a specific node, a specific
     * hash (algorithm) and a specific ver.
     *
     * @param node the node to be represented by the new instance
     * @param hash the hash (algorithm) to be represented by the new instance
     * @param ver the ver to be represented by the new instance
     * @param ext the ext to be represented by the new instance
     */
    public Caps(String node, String hash, String ver, String ext) {
      if (node == null) throw new NullPointerException("node");
      if (hash == null) throw new NullPointerException("hash");
      if (ver == null) throw new NullPointerException("ver");

      this.node = node;
      this.hash = hash;
      this.ver = ver;
      this.ext = ext;

      this.nodeVer = this.node + '#' + this.ver;
    }

    /**
     * Gets a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of
     * this instance, the character '#' and the <tt>ver</tt> property of this instance.
     *
     * @return a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of
     *     this instance, the character '#' and the <tt>ver</tt> property of this instance
     */
    public final String getNodeVer() {
      return nodeVer;
    }

    /**
     * Determines whether a specific <tt>DiscoverInfo</tt> is valid according to this <tt>Caps</tt>
     * i.e. whether the <tt>discoverInfo</tt> has the node and the ver of this <tt>Caps</tt> and the
     * ver calculated from the <tt>discoverInfo</tt> using the hash (algorithm) of this
     * <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>.
     *
     * @param discoverInfo the <tt>DiscoverInfo</tt> to be validated by this <tt>Caps</tt>
     * @return <tt>true</tt> if the specified <tt>DiscoverInfo</tt> has the node and the ver of this
     *     <tt>Caps</tt> and the ver calculated from the <tt>discoverInfo</tt> using the hash
     *     (algorithm) of this <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>; otherwise,
     *     <tt>false</tt>
     */
    public boolean isValid(DiscoverInfo discoverInfo) {
      if (discoverInfo != null) {
        // The "node" attribute is not necessary in the query element.
        // For example, Swift does not send back the "node" attribute in
        // the Disco#info response. Thus, if the node of the IQ response
        // is null, then we set it to the request one.
        if (discoverInfo.getNode() == null) {
          discoverInfo.setNode(getNodeVer());
        }

        if (getNodeVer().equals(discoverInfo.getNode())
            && !hash.equals("")
            && ver.equals(capsToHash(hash, calculateEntityCapsString(discoverInfo)))) {
          return true;
        }
      }
      return false;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      Caps caps = (Caps) o;

      if (!hash.equals(caps.hash)) return false;
      if (!node.equals(caps.node)) return false;
      if (!ver.equals(caps.ver)) return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result = hash.hashCode();
      result = 31 * result + node.hashCode();
      result = 31 * result + ver.hashCode();
      return result;
    }
  }
}
  /**
   * Retrieve DiscoverInfo for a specific node.
   *
   * @param caps the <tt>Caps</tt> i.e. the node, the hash and the ver
   * @return The corresponding DiscoverInfo or null if none is known.
   */
  public static DiscoverInfo getDiscoverInfoByCaps(Caps caps) {
    synchronized (caps2discoverInfo) {
      DiscoverInfo discoverInfo = caps2discoverInfo.get(caps);

      /*
       * If we don't have the discoverInfo in the runtime cache yet, we
       * may have it remembered in a previous application instance.
       */
      if (discoverInfo == null) {
        ConfigurationService configurationService = getConfigService();
        String capsPropertyName = getCapsPropertyName(caps);
        String xml = configurationService.getString(capsPropertyName);

        if ((xml != null) && (xml.length() != 0)) {
          IQProvider discoverInfoProvider =
              (IQProvider)
                  ProviderManager.getInstance()
                      .getIQProvider("query", "http://jabber.org/protocol/disco#info");

          if (discoverInfoProvider != null) {
            XmlPullParser parser = new MXParser();

            try {
              parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
              parser.setInput(new StringReader(xml));
              // Start the parser.
              parser.next();
            } catch (XmlPullParserException xppex) {
              parser = null;
            } catch (IOException ioex) {
              parser = null;
            }

            if (parser != null) {
              try {
                discoverInfo = (DiscoverInfo) discoverInfoProvider.parseIQ(parser);
              } catch (Exception ex) {
              }

              if (discoverInfo != null) {
                if (caps.isValid(discoverInfo)) caps2discoverInfo.put(caps, discoverInfo);
                else {
                  logger.error(
                      "Invalid DiscoverInfo for " + caps.getNodeVer() + ": " + discoverInfo);
                  /*
                   * The discoverInfo doesn't seem valid
                   * according to the caps which means that we
                   * must have stored invalid information.
                   * Delete the invalid information in order
                   * to not try to validate it again.
                   */
                  configurationService.removeProperty(capsPropertyName);
                }
              }
            }
          }
        }
      }
      return discoverInfo;
    }
  }