/**
   * Returns a pair consisting of a MarshalledObject and attributes to be bound with the stub.
   *
   * @param obj The non-null object to store.
   * @param inAttrs The possible null attributes to store with object.
   * @return A non-null Result consisting of the MarshalledObject and attributes.
   */
  private static DirStateFactory.Result jrmpObject(Object obj, Attributes inAttrs)
      throws NamingException {
    try {
      Object mobj = new MarshalledObject(obj);

      Attributes outAttrs = null;
      Attribute cname = null;
      Attribute tnames = null;
      Attribute objectClass = null;

      if (inAttrs != null) {
        // Get existing objectclass attribute
        objectClass = (Attribute) inAttrs.get("objectClass");
        if (objectClass == null && !inAttrs.isCaseIgnored()) {
          // %%% workaround
          objectClass = (Attribute) inAttrs.get("objectclass");
        }

        // No objectclasses supplied, use "top" to start
        if (objectClass == null) {
          objectClass = new BasicAttribute("objectClass", "top");
        } else {
          objectClass = (Attribute) objectClass.clone();
        }

        cname = inAttrs.get(CLASSNAME_ATTRID);
        tnames = inAttrs.get(CLASSNAMES_ATTRID);

        outAttrs = (Attributes) inAttrs.clone();
      } else {
        outAttrs = new BasicAttributes(true);
        objectClass = new BasicAttribute("objectClass", "top");
      }

      if (cname == null) {
        outAttrs.put(CLASSNAME_ATTRID, obj.getClass().getName());
      }
      if (tnames == null) {
        Attribute tAttr = LdapCtxFactory.createTypeNameAttr(obj.getClass());
        if (tAttr != null) {
          outAttrs.put(tAttr);
        }
      }

      boolean structural =
          (objectClass.size() == 0 || (objectClass.size() == 1 && objectClass.contains("top")));

      if (structural) {
        objectClass.add(STRUCTURAL_OCID);
      }
      objectClass.add(MARSHALLED_OCID);
      outAttrs.put(objectClass);

      return new DirStateFactory.Result(mobj, outAttrs);

    } catch (java.io.IOException e) {
      NamingException ne = new NamingException("Cannot create MarshallObject for " + obj);
      ne.setRootCause(e);
      throw ne;
    }
  }
    private LdapContext bind(
        String principalName, String password, SocketInfo server, Hashtable<String, String> props)
        throws NamingException {
      String ldapUrl = (FORCE_LDAPS ? "ldaps://" : "ldap://") + server + '/';
      String oldName = Thread.currentThread().getName();
      Thread.currentThread().setName("Connecting to " + ldapUrl + " : " + oldName);
      LOGGER.fine("Connecting to " + ldapUrl);
      try {
        props.put(Context.PROVIDER_URL, ldapUrl);
        props.put("java.naming.ldap.version", "3");

        customizeLdapProperties(props);

        LdapContext context = (LdapContext) LdapCtxFactory.getLdapCtxInstance(ldapUrl, props);

        if (!FORCE_LDAPS) {
          // try to upgrade to TLS if we can, but failing to do so isn't fatal
          // see http://download.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html
          try {
            StartTlsResponse rsp =
                (StartTlsResponse) context.extendedOperation(new StartTlsRequest());
            rsp.negotiate((SSLSocketFactory) TrustAllSocketFactory.getDefault());
            LOGGER.fine("Connection upgraded to TLS");
          } catch (NamingException e) {
            LOGGER.log(
                Level.FINE,
                "Failed to start TLS. Authentication will be done via plain-text LDAP",
                e);
            context.removeFromEnvironment("java.naming.ldap.factory.socket");
          } catch (IOException e) {
            LOGGER.log(
                Level.FINE,
                "Failed to start TLS. Authentication will be done via plain-text LDAP",
                e);
            context.removeFromEnvironment("java.naming.ldap.factory.socket");
          }
        }

        if (principalName == null || password == null || password.equals("")) {
          // anonymous bind. LDAP uses empty password as a signal to anonymous bind (RFC 2829 5.1),
          // which means it can never be the actual user password.
          context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "none");
          LOGGER.fine("Binding anonymously to " + ldapUrl);
        } else {
          // authenticate after upgrading to TLS, so that the credential won't go in clear text
          context.addToEnvironment(Context.SECURITY_PRINCIPAL, principalName);
          context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
          LOGGER.fine("Binding as " + principalName + " to " + ldapUrl);
        }

        // this is supposed to cause the LDAP bind operation with the server,
        // but I notice that AD may still accept this and yet fail to search later,
        // when I tried anonymous bind.
        // if I do specify a wrong credential, this seems to fail.
        context.reconnect(null);

        return context; // worked
      } finally {
        Thread.currentThread().setName(oldName);
      }
    }