/***********************************************************************
 * 
 *  Copyright (C) 2006 Novell, Inc. All Rights Reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; version 2.1
 *  of the License.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Library Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, Novell, Inc.
 * 
 *  To contact Novell about this file by physical or electronic mail, 
 *  you may find current contact information at www.novell.com.
 * 
 *  Author: Juan Carlos Luciani <jluciani@novell.com>
 *  
 ***********************************************************************/

package com.novell.casa.authtoksvc;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.ServiceUnavailableException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.Attribute;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import org.bandit.util.config.Realm;
import org.apache.log4j.Logger;

/**
 * CasaIdentityToken Class.
 * <p>
 * This class constructs Casa Identity tokens.
 * <p>
 * A Casa Identity Token is a simple XML Document
 * with information about an identity in the form
 * of:
 * <p>
 *    <?xml version="1.0" encoding="UTF-8"?>
 *    <casa_ident_tok>
 *    <id>identity id</id>
 *    <source_name>identity data source name</source_name>
 *    <source_url>identity data source url</source_url>
 *    <target_service>target service name</target_service>
 *    <target_host>target host name</target_host>
 *    <attributes>
 *    <attribute name>attribute value</attribute name>
 *    <attribute2 name>attribute2 value</attribute name>
 *    ...
 *    </attributes>
 *    </casa_ident_tok>
 * <p>
 * Attribute/values pairs. The attribute names
 * being the XML elements of the documents.
 * 
 */
public final class CasaIdentityToken implements IdentityToken
{
   private static final Logger m_log = Logger.getLogger(CasaIdentityToken.class);

   /*
   * XML Element Name Constants for the documents exchanged between the
   * Casa Client and the Casa Server.
   */
   private final static String casaIdentTokElementName = "casa_ident_tok";
   private final static String idElementName = "id";
   private final static String sourceNameElementName = "source_name";
   private final static String sourceUrlElementName = "source_url";
   private final static String targetServiceElementName = "target_service";
   private final static String targetHostElementName = "target_host";
   private final static String attributesElementName = "attributes";

   private final IdenTokenConfig m_idenTokenConfig;

   private String m_identityId = null;
   private String m_sourceName = null;
   private String m_sourceUrl = null;
   private String m_service = null;
   private String m_host = null;
   private String m_token = null;
   private javax.naming.directory.Attributes m_attributes = null;

   /**
    * Class for handling Authentication Request parsing events.
    */
   private final class SAXHandler extends org.xml.sax.helpers.DefaultHandler
   {
      private final static int AWAITING_ROOT_ELEMENT_START = 0;
      private final static int AWAITING_ROOT_ELEMENT_END = 1;
      private final static int AWAITING_ID_ELEMENT_START = 2;
      private final static int AWAITING_ID_ELEMENT_END = 3;
      private final static int AWAITING_ID_DATA = 4;
      private final static int AWAITING_SOURCE_NAME_ELEMENT_START = 5;
      private final static int AWAITING_SOURCE_NAME_ELEMENT_END = 6;
      private final static int AWAITING_SOURCE_NAME_DATA = 7;
      private final static int AWAITING_SOURCE_URL_ELEMENT_START = 8;
      private final static int AWAITING_SOURCE_URL_ELEMENT_END = 9;
      private final static int AWAITING_SOURCE_URL_DATA = 10;
      private final static int AWAITING_TARGET_SERVICE_ELEMENT_START = 11;
      private final static int AWAITING_TARGET_SERVICE_ELEMENT_END = 12;
      private final static int AWAITING_TARGET_SERVICE_DATA = 13;
      private final static int AWAITING_TARGET_HOST_ELEMENT_START = 14;
      private final static int AWAITING_TARGET_HOST_ELEMENT_END = 15;
      private final static int AWAITING_TARGET_HOST_DATA = 16;
      private final static int AWAITING_ATTRIBUTES_ELEMENT_START = 17;
      private final static int AWAITING_ATTRIBUTE_START = 18;
      private final static int AWAITING_ATTRIBUTE_END = 19;
      private final static int AWAITING_ATTRIBUTE_DATA = 20;
      private final static int AWAITING_BINARY_ATTRIBUTE_DATA = 21;
      private final static int AWAITING_BINARY_ATTRIBUTE_END = 22;
      private final static int DONE_PARSING = 23;

      private final CasaIdentityToken  m_casaIdentToken;
      private int       m_state;
      private String    m_currAttribute;
      private boolean   m_encryptedAttrs;

      /**
       * Constructor.
       *
       * @param casaIdentityToken   Casa Identity Token object.
       */
      public SAXHandler (CasaIdentityToken casaIdentityToken)
      {
         super();

         // Initialize our members
         m_casaIdentToken = casaIdentityToken;
         m_state = AWAITING_ROOT_ELEMENT_START;
      }

      /**
       * endDocument() implementation.
       *
       * @throws SAXException
       */
      public final void endDocument () throws SAXException
      {
         // Verify that we obtained all of the required elements
         if (m_state != DONE_PARSING)
         {
            m_log.error("SAXHandler.endDocument()- Missing element");
            throw new SAXException("Missing element");
         }
      }

      /**
       * startElement() implementation.
       *
       * @param uri     Uri.
       * @param name    Local name.
       * @param qName   Qualified name.
       * @param atts    Attributes.
       * @throws SAXException
       */
      public final void startElement (String uri, String name, String qName, org.xml.sax.Attributes atts) throws SAXException
      {
         // Proceed based on our state
         switch (m_state)
         {

            case AWAITING_ROOT_ELEMENT_START:
               // Verify that we are processing the expected tag
               if (casaIdentTokElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_ID_ELEMENT_START;
               }
               else
               {
                  m_log.error("SAXHandler.startElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_ID_ELEMENT_START:
               // Verify that we are processing the expected tag
               if (idElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_ID_DATA;
               }
               else
               {
                  m_log.error("SAXHandler.startElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_SOURCE_NAME_ELEMENT_START:
               // Verify that we are processing the expected tag
               if (sourceNameElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_SOURCE_NAME_DATA;
               }
               else
               {
                  m_log.error("SAXHandler.startElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }

               break;

            case AWAITING_SOURCE_URL_ELEMENT_START:
               // Verify that we are processing the expected tag
               if (sourceUrlElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_SOURCE_URL_DATA;
               }
               else
               {
                  m_log.error("SAXHandler.startElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_TARGET_SERVICE_ELEMENT_START:
               // Verify that we are processing the expected tag
               if (targetServiceElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_TARGET_SERVICE_DATA;
               }
               else
               {
                  m_log.error("SAXHandler.startElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_TARGET_HOST_ELEMENT_START:
               // Verify that we are processing the expected tag
               if (targetHostElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_TARGET_HOST_DATA;
               }
               else
               {
                  m_log.error("SAXHandler.startElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_ATTRIBUTES_ELEMENT_START:
               // Verify that we are processing the expected tag
               if (attributesElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_ATTRIBUTE_START;
               }
               else
               {
                  m_log.error("SAXHandler.startElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_ATTRIBUTE_START:
               // Save the element name as the current attribute
               m_currAttribute = qName;

               // Advance to the next state based on the attribute type
               String attrType = atts.getValue("type");
               if (attrType != null && attrType.equalsIgnoreCase("binary"))
               {
                  // We are dealing with a binary attribute. We are going to
                  // assume that binary attributes are always base64 encoded.
                  m_state = AWAITING_BINARY_ATTRIBUTE_DATA;
               }
               else
               {
                  // Assume we are dealing with an attribute of type string
                  m_state = AWAITING_ATTRIBUTE_DATA;
               }
               break;

            default:
               m_log.error("SAXHandler.startElement()- State error");
               throw new SAXException("State error");
         }
      }

      /**
       * endElement() implementation.
       *
       * @param uri     Uri.
       * @param name    Local name.
       * @param qName   Qualified name.
       * @throws SAXException
       */
      public final void endElement (String uri, String name, String qName) throws SAXException
      {
         // Proceed based on our state
         switch (m_state)
         {

            case AWAITING_ROOT_ELEMENT_END:
               // Verify that we are processing the expected tag
               if (casaIdentTokElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = DONE_PARSING;
               }
               else
               {
                  m_log.error("SAXHandler.endElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_ID_ELEMENT_END:
               // Verify that we are processing the expected tag
               if (idElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_SOURCE_NAME_ELEMENT_START;
               }
               else
               {
                  m_log.error("SAXHandler.endElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_SOURCE_NAME_ELEMENT_END:
               // Verify that we are processing the expected tag
               if (sourceNameElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_SOURCE_URL_ELEMENT_START;
               }
               else
               {
                  m_log.error("SAXHandler.endElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_SOURCE_URL_ELEMENT_END:
               // Verify that we are processing the expected tag
               if (sourceUrlElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_TARGET_SERVICE_ELEMENT_START;
               }
               else
               {
                  m_log.error("SAXHandler.endElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_TARGET_SERVICE_ELEMENT_END:
               // Verify that we are processing the expected tag
               if (targetServiceElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_TARGET_HOST_ELEMENT_START;
               }
               else
               {
                  m_log.error("SAXHandler.endElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_TARGET_HOST_ELEMENT_END:
               // Verify that we are processing the expected tag
               if (targetHostElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_ATTRIBUTES_ELEMENT_START;
               }
               else
               {
                  m_log.error("SAXHandler.endElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            case AWAITING_ATTRIBUTE_END:
               // Advance to the next state
               m_state = AWAITING_ATTRIBUTE_START;
               break;

            case AWAITING_BINARY_ATTRIBUTE_END:
               // Decode the attribute data since it is base64 encoded
               Attribute attrib = m_casaIdentToken.m_attributes.remove(m_currAttribute);
               if (attrib != null)
               {
                  try
                  {
                     char[] attribData = (char[]) attrib.get();
                     if (attribData != null)
                     {
                        m_casaIdentToken.m_attributes.put(m_currAttribute, Base64Coder.decode(attribData));
                     }
                     else
                     {
                        m_log.error("SAXHandler.endElement()- Attribute data not found");
                        throw new SAXException("Attribute data not found");
                     }
                  }
                  catch (NamingException e)
                  {
                     m_log.error("SAXHandler.endElement()- Attribute data not found");
                     throw new SAXException("Attribute data not found", e);
                  }
               }
               else
               {
                  m_log.error("SAXHandler.endElement()- Attribute not found");
                  throw new SAXException("Attribute not found");
               }

               // Advance to the next state
               m_state = AWAITING_ATTRIBUTE_START;
               break;

            case AWAITING_ATTRIBUTE_START:
               // Verify that we are processing the expected tag
               if (attributesElementName.equalsIgnoreCase(qName))
               {
                  // Advance to the next state
                  m_state = AWAITING_ROOT_ELEMENT_END;
               }
               else
               {
                  m_log.error("SAXHandler.endElement()- Un-expected element");
                  throw new SAXException("Un-expected element");
               }
               break;

            default:
               m_log.error("SAXHandler.endElement()- State error");
               throw new SAXException("State error");
         }
      }

      /**
       * character() implementation.
       *
       * @param ch      Characters with element data.
       * @param start   Start position in the character array.
       * @param length  Number of characters to use in the array.
       * @throws SAXException
       */
      public final void characters (char ch[], int start, int length) throws SAXException
      {
         // Proceed based on our state
         switch (m_state)
         {
            case AWAITING_ID_DATA:
               // Consume the data
               m_casaIdentToken.m_identityId = new String(ch, start, length);

               // Advance to the next state
               m_state = AWAITING_ID_ELEMENT_END;
               break;

            case AWAITING_ID_ELEMENT_END:
               // Consume the data
               m_casaIdentToken.m_identityId = m_casaIdentToken.m_identityId.concat(new String(ch, start, length));
               break;

            case AWAITING_SOURCE_NAME_DATA:
               // Consume the data
               m_casaIdentToken.m_sourceName = new String(ch, start, length);

               // Advance to the next state
               m_state = AWAITING_SOURCE_NAME_ELEMENT_END;
               break;

            case AWAITING_SOURCE_NAME_ELEMENT_END:
               // Consume the data
               m_casaIdentToken.m_sourceName = m_casaIdentToken.m_sourceName.concat(new String(ch, start, length));
               break;

            case AWAITING_SOURCE_URL_DATA:
               // Consume the data
               m_casaIdentToken.m_sourceUrl = new String(ch, start, length);

               // Advance to the next state
               m_state = AWAITING_SOURCE_URL_ELEMENT_END;
               break;

            case AWAITING_SOURCE_URL_ELEMENT_END:
               // Consume the data
               m_casaIdentToken.m_sourceUrl = m_casaIdentToken.m_sourceUrl.concat(new String(ch, start, length));
               break;

            case AWAITING_TARGET_SERVICE_DATA:
               // Consume the data
               m_casaIdentToken.m_service = new String(ch, start, length);

               // Advance to the next state
               m_state = AWAITING_TARGET_SERVICE_ELEMENT_END;
               break;

            case AWAITING_TARGET_SERVICE_ELEMENT_END:
               // Consume the data
               m_casaIdentToken.m_service = m_casaIdentToken.m_service.concat(new String(ch, start, length));
               break;

            case AWAITING_TARGET_HOST_DATA:
               // Consume the data
               m_casaIdentToken.m_host = new String(ch, start, length);

               // At this point we now have the target service and host names,
               // check if our configuration says that the attributes have been
               // encrypted.
               // tbd - Need to come up with a solution for obtaining configuration
               // information when instanstiated using a stream. May be the token should
               // carry an indication that the attributes are encrypted.
               m_encryptedAttrs = false;

               // Advance to the next state
               m_state = AWAITING_TARGET_HOST_ELEMENT_END;
               break;

            case AWAITING_TARGET_HOST_ELEMENT_END:
               // Consume the data
               m_casaIdentToken.m_host = m_casaIdentToken.m_host.concat(new String(ch, start, length));
               break;

            case AWAITING_ATTRIBUTE_DATA:
               // Consume the data
               //
               // Decrypt the attribute data if necessary
               if (m_encryptedAttrs)
               {
                  // tbd - Decrypt the attribute key and value with the private key of the service
                  // using the configured mechanism.
               }
               else
               {
                  m_casaIdentToken.m_attributes.put(m_currAttribute, new String(ch, start, length));
               }

               // Advance to the next state
               m_state = AWAITING_ATTRIBUTE_END;
               break;

            case AWAITING_ATTRIBUTE_END:
               // Consume the data
               //
               // Decrypt the attribute data if necessary
               if (m_encryptedAttrs)
               {
                  // tbd - Decrypt the attribute key and value with the private key of the service
                  // using the configured mechanism.
               }
               else
               {
                  // Append the data to what we have already consumed
                  Attribute attrib = m_casaIdentToken.m_attributes.remove(m_currAttribute);
                  if (attrib != null)
                  {
                     try
                     {
                        String attribData = (String) attrib.get();
                        if (attribData != null)
                        {
                           m_casaIdentToken.m_attributes.put(m_currAttribute,
                                                             attribData.concat(new String(ch, start, length)));
                        }
                        else
                        {
                           m_log.error("SAXHandler.characters()- Attribute data not found");
                           throw new SAXException("Attribute data not found");
                        }
                     }
                     catch (NamingException e)
                     {
                        m_log.error("SAXHandler.characters()- Attribute data not found");
                        throw new SAXException("Attribute data not found", e);
                     }
                  }
                  else
                  {
                     m_log.error("SAXHandler.characters()- Attribute not found");
                     throw new SAXException("Attribute not found");
                  }
               }
               break;

            case AWAITING_BINARY_ATTRIBUTE_DATA:
               // Consume the data
               //
               // Decrypt the attribute data if necessary
               if (m_encryptedAttrs)
               {
                  // tbd - Decrypt the attribute key and value with the private key of the service
                  // using the configured mechanism.
               }
               else
               {
                  // We are dealing with base64 encoded data
                  char[] encodedChars = new char[length];
                  System.arraycopy(ch, start, encodedChars, 0, length);
                  m_casaIdentToken.m_attributes.put(m_currAttribute, encodedChars);
               }

               // Advance to the next state
               m_state = AWAITING_BINARY_ATTRIBUTE_END;
               break;

            case AWAITING_BINARY_ATTRIBUTE_END:
               // Consume the data
               //
               // Decrypt the attribute data if necessary
               if (m_encryptedAttrs)
               {
                  // tbd - Decrypt the attribute key and value with the private key of the service
                  // using the configured mechanism.
               }
               else
               {
                  // Append the data to what we have already consumed
                  Attribute attrib = m_casaIdentToken.m_attributes.remove(m_currAttribute);
                  if (attrib != null)
                  {
                     try
                     {
                        // We are dealing with base64 encoded data
                        char[] consumedData = (char[]) attrib.get();
                        if (consumedData != null)
                        {
                           char[] encodedChars = new char[consumedData.length + length];
                           System.arraycopy(consumedData, 0, encodedChars, 0, consumedData.length);
                           System.arraycopy(ch, start, encodedChars, consumedData.length, length);
                           m_casaIdentToken.m_attributes.put(m_currAttribute, encodedChars);
                        }
                        else
                        {
                           m_log.error("SAXHandler.characters()- Attribute data not found");
                           throw new SAXException("Attribute data not found");
                        }
                     }
                     catch (NamingException e)
                     {
                        m_log.error("SAXHandler.characters()- Attribute data not found");
                        throw new SAXException("Attribute data not found", e);
                     }
                  }
                  else
                  {
                     m_log.error("SAXHandler.characters()- Attribute not found");
                     throw new SAXException("Attribute not found");
                  }
               }

               // Advance to the next state
               m_state = AWAITING_BINARY_ATTRIBUTE_END;
               break;

            default:
               // Do nothing
               break;
         }
      }
   }

   /**
    * Constructor.
    *
    * @param idenTokenConfig  Identity token configuration object.
    */
   public CasaIdentityToken (IdenTokenConfig idenTokenConfig)
   {
      // Initialize our members
      m_token = null;
      m_attributes = new javax.naming.directory.BasicAttributes();
      m_idenTokenConfig = idenTokenConfig;
   }

   /**
    * Constructor.
    */
   public CasaIdentityToken ()
   {
      // Initialize our members
      m_token = null;
      m_attributes = new javax.naming.directory.BasicAttributes();
      m_idenTokenConfig = null;
   }

   /**
    * Initialize with parameters.
    *
    * @param identityId    Identity identifier.
    * @param sourceName    Name of the realm where the identityId is valid.
    * @param sourceUrl     URL to the realm (identity data source).
    * @param targetService Name of service being targeted.
    * @param targetHost    Host where the targeted service resides.
    * @param svcConfig     Service configuration object.
    * @throws Exception
    */
   public final void initialize (String identityId,
                                 String sourceName,
                                 String sourceUrl,
                                 String targetService,
                                 String targetHost,
                                 SvcConfig svcConfig) throws Exception
   {
      // Save input parameters
      m_identityId = identityId;
      m_sourceName = sourceName;
      m_sourceUrl = sourceUrl;
      m_service = targetService;
      m_host = targetHost;

      try
      {
         // Open a directory context and use it to read the identity attributes.
         Hashtable env = new Hashtable();
         env.put(Context.INITIAL_CONTEXT_FACTORY, "org.bandit.ia.IAInitialCtxFactory");
         env.put(Realm.REALM_CONFIG_LOCATION, svcConfig.getSetting(SvcConfig.IdentityAbstractionConfigFile));
         env.put(Realm.REALM_SELECTOR, sourceName);
         if (svcConfig.m_realmsInfo.proxyCredentialsInRealmFile(sourceName) == false)
         {
            env.put(Context.SECURITY_PRINCIPAL, svcConfig.m_realmsInfo.proxyUsernameCredential(sourceName));
            env.put(Context.SECURITY_CREDENTIALS, svcConfig.m_realmsInfo.proxyPasswordCredential(sourceName));
         }

         int retries = 3;
         while (retries != 0)
         {
            // Instantiate DirContext watching for an exception since it
            // would be an indication that we should not retry the
            // operation.
            DirContext ctx;
            try
            {
               ctx = new InitialDirContext(env);
            }
            catch (Exception e)
            {
               m_log.warn("initialize(): Exception caught instantiating DirContext, msg = " + e.getMessage());

               // Stop retrying
               break;
            }

            // Setup a string buffer for building the IdentityToken, notice for now
            // we are not going to wrap the identity token.
            StringBuffer sb = new StringBuffer();
            sb.append(ProtoDefs.xmlDeclaration); sb.append("\r\n");
            sb.append("<"); sb.append(casaIdentTokElementName); sb.append(">"); sb.append("\r\n");
            sb.append("<"); sb.append(idElementName); sb.append(">"); sb.append(escapeXml(identityId)); sb.append("</"); sb.append(idElementName); sb.append(">\r\n");
            sb.append("<"); sb.append(sourceNameElementName); sb.append(">"); sb.append(sourceName); sb.append("</"); sb.append(sourceNameElementName); sb.append(">\r\n");
            sb.append("<"); sb.append(sourceUrlElementName); sb.append(">"); sb.append(m_sourceUrl); sb.append("</"); sb.append(sourceUrlElementName); sb.append(">\r\n");
            sb.append("<"); sb.append(targetServiceElementName); sb.append(">"); sb.append(m_service); sb.append("</"); sb.append(targetServiceElementName); sb.append(">\r\n");
            sb.append("<"); sb.append(targetHostElementName); sb.append(">"); sb.append(m_host); sb.append("</"); sb.append(targetHostElementName); sb.append(">\r\n");
            sb.append("<"); sb.append(attributesElementName); sb.append(">\r\n");

            // Get the necessary attributes of the specified services in the identity token
            // watching for a service unavailable exception as an indication that we should
            // retry the operation.
            try
            {
               String[] attributesNeeded  = m_idenTokenConfig.getAttributes();
               boolean encryptAttributes = "true".equalsIgnoreCase(m_idenTokenConfig.getSetting(IdenTokenConfig.EncryptAttributes));
               Attributes attrs = ctx.getAttributes(identityId, attributesNeeded);

               // Now append the attributes to the token
               for (NamingEnumeration ae = attrs.getAll(); ae.hasMore();)
               {
                  javax.naming.directory.Attribute attr = (javax.naming.directory.Attribute) ae.next();

                  NamingEnumeration<?> enumeration = attr.getAll();
                  while (enumeration.hasMore())
                  {
                     Object attrValue = enumeration.next();
                     m_attributes.put(attr.getID(), attrValue);
                     m_log.debug("initialize()- Including attribute " + attr.getID());

                     // Encrypt the attribute if necessary
                     if (encryptAttributes)
                     {
                        // tbd - Encrypt the attributes using the services public key, let the mechanism
                        // be configurable. The service's certificate should be Base64 encoded as a setting
                        // of the identoken.settings file.
                     }
                     else
                     {
                        // Proceed based on the attribute value type
                        if (attrValue instanceof byte[])
                        {
                           // The attribute value is of type byte[], we need to encode it.
                           sb.append("<"); sb.append(attr.getID()); sb.append(" type=\"binary\" encoding=\"base64\">"); sb.append(new String(Base64Coder.encode((byte[]) attrValue))); sb.append("</"); sb.append(attr.getID()); sb.append(">\r\n");
                           m_log.debug("initialize()- Attribute " + attr.getID() + "included as " + new String(Base64Coder.encode((byte[]) attrValue)));
                        }
                        else
                        {
                           // Assume the attribute value is of type String
                           sb.append("<"); sb.append(attr.getID()); sb.append(">"); sb.append(escapeXml((String) attrValue)); sb.append("</"); sb.append(attr.getID()); sb.append(">\r\n");
                        }
                     }
                  }
               }
               sb.append("</" + attributesElementName + ">" + "\r\n");
               sb.append("</" + casaIdentTokElementName + ">" + "\r\n");

               m_token = sb.toString();
            }
            catch (ServiceUnavailableException e)
            {
               m_log.warn("initialize()- ServiceUnavailable exception caught looking up attributes, msg = " + e.getMessage());

               // Retry the operation
               retries --;
               continue;
            }

            // No need to retry
            break;
         }
      }
      catch (NamingException e)
      {
         m_log.error("initialize()- Exception: " + e.getExplanation());
         throw new Exception("Error obtaining identity data for token", e);
      }
      catch (Exception e)
      {
         m_log.error("initialize()- Exception: " + e.toString());
         throw new Exception("Error obtaining identity data for token", e);
      }
   }

   /**
    * Initialize the token object with an encoded token string.
    *
    * @param encodedToken  Encoded identity token string.
    * @throws Exception
    */
   public final void initialize (String encodedToken) throws Exception
   {
      // Save copy of the token
      m_token = Base64Coder.decodeUTF8(encodedToken);

      // Now parse the token into its elements
      Reader tokenReader = null;
      try
      {
         // Parse the AuthReqMsg
         XMLReader xr = XMLReaderFactory.createXMLReader();
         SAXHandler handler = new SAXHandler(this);
         xr.setContentHandler(handler);
         xr.setErrorHandler(handler);

         tokenReader = new StringReader(m_token);
         InputSource source = new InputSource(tokenReader);
         xr.parse(source);
      }
      catch (SAXException e)
      {
         m_log.error("initialize()- Parse exception: " + e.toString());
         throw new Exception("Token error", e);
      }
      finally
      {
         if (tokenReader != null)
         {
            try
            {
               tokenReader.close();
            }
            catch (IOException e)
            {
               // Do nothing
            }
         }
      }
   }

   /**
    * Returns encoded token string.
    * <p>
    * IMPORTANT: The token string can not contain the substring "]]>"
    * within it.
    *
    * @return  Encoded identity token string.
    * @throws Exception
    */
   public final String getEncodedToken () throws Exception
   {
      if (m_token != null)
      {
         return Base64Coder.encodeUTF8(m_token);
      }
      else
      {
         m_log.error("getEncodedToken()- Not initialized");
         throw new Exception("Not initialized");
      }
   }

   /**
    * Returns a string containing our type of identity token provider.
    *
    * @return  Identity token provider type.
    * @throws Exception
    */
   public final String getProviderType () throws Exception
   {
      return "CasaIdentityToken";
   }

   /**
    * Returns a string containing the identity id.
    *
    * @return  Identity identifier.
    * @throws Exception
    */
   public final String getIdentityId () throws Exception
   {
      if (m_identityId != null)
         return m_identityId;
      else
      {
         m_log.error("getIdentityId()- Not initialized");
         throw new Exception("Not initialized");
      }
   }

   /**
    * Returns a string containing the name associated with the
    * identity source.
    *
    * @return  Name of the authentication realm.
    * @throws Exception
    */
   public final String getSourceName () throws Exception
   {
      if (m_sourceName != null)
         return m_sourceName;
      else
      {
         m_log.error("getSourceName()- Not initialized");
         throw new Exception("Not initialized");
      }
   }

   /**
    * Returns a string containing the url associated with the
    * identity source.
    *
    * @return  Url of the identity data source.
    * @throws Exception
    */
   public final String getSourceUrl () throws Exception
   {
      if (m_sourceUrl != null)
         return m_sourceUrl;
      else
      {
         m_log.error("getSourceUrl()- Not initialized");
         throw new Exception("Not initialized");
      }
   }

   /**
    * Returns a string containing the name of the targeted service.
    *
    * @return  Name of the targeted service.
    * @throws Exception
    */
   public final String getTargetService () throws Exception
   {
      if (m_service != null)
         return m_service;
      else
      {
         m_log.error("getTargetService()- Not initialized");
         throw new Exception("Not initialized");
      }
   }

   /**
    * Returns a string containig the name of the host where the
    * targeted service resides.
    *
    * @return  Name of the host where the targeted service resides.
    * @throws Exception
    */
   public final String getTargetHost () throws Exception
   {
      if (m_host != null)
         return m_host;
      else
      {
         m_log.error("getTargetHost()- Not initialized");
         throw new Exception("Not initialized");
      }
   }

   /**
    * Returns the attributes of the identity.
    *
    * @return  Identity attributes.
    * @throws Exception
    */
   public final javax.naming.directory.Attributes getAttributes () throws Exception
   {
      if (m_attributes != null)
         return m_attributes;
      else
      {
         m_log.error("getIdentityAttributes()- Not initialized");
         throw new Exception("Not initialized");
      }
   }

   /**
    * Escapes Xml string.
    *
    * @param xml  XML String to be escaped.
    * @return Escaped XML string.
    */
   private String escapeXml(String xml)
   {
      char[] xmlChars = new char[xml.length()];
      xml.getChars(0, xml.length(), xmlChars, 0);
      StringBuffer escapedXml = new StringBuffer();
      for (int i = 0; i < xmlChars.length; i++)
      {
         switch (xmlChars[i])
         {
            case '&':
               escapedXml.append("&amp;");
               break;
            case '<':
               escapedXml.append("&lt;");
               break;
            case '>':
               escapedXml.append("&gt;");
               break;
            case '\'':
               escapedXml.append("&apos;");
               break;
            case '"':
               escapedXml.append("&quot;");
               break;
            default:
               escapedXml.append(xmlChars[i]);
               break;
         }
      }

      return escapedXml.toString();
   }
}
