# ClientPrincipal.java

    /*
    ** Module   : ClientPrincipal.java
    ** Abstract : Implementation of the CLIENT-PRINCIPAL resource.
    **
    ** Copyright (c) 2018-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 CA  20181128 Created initial version.
    ** 002 IAS 20190507 Added more attributes and methods.
    ** 003 IAS 20190524 Added support for CLIENT-PRINCIPAL:(LOGIN-STATE, LOGIN-EXPIRATION-TIMESTAMP,
    **                  SEAL-TIMESTAMP, PRIMARY-PASSPHRASE, QUALIFIED-USER-ID)
    ** 004 IAS 20190625 Added support for SECURITY-POLICY:SET-CLIENT
    ** 005 IAS 20190703 Changed support for SET-DB-CLIENT
    ** 006 CA  20191119 Import must set the properties in the attributes#PROPS.  An attempt to fix
    **                  validateSeal (by computing the MAC and comparing it with the existing one).
    ** 007 IAS 20200112 EXPORT/IMPORT fixed, added DOMAIN-ACCESS-CODE validation with MAC,
    **                  fixed MAC calculation, fixed VALIDATE-SEAL.
    ** 008 IAS 20200914 Re-work (de)serialization
    **     OM  20201030 Invalid attribute API support for getters/setters.
    ** 009 CA  20211005 CLIENT-PRINCIPAL can't be added to unnamed pools.
    **     CA  20211222 Fixed memory leak: when a CLIENT-PRINCIPAL is deleted, it must process the super.delete().
    **     CA  20220318 Prevent a NPE in validateSeal, in case MAC is not loaded by importPrincipal.
    **     IAS 20220415 Misc. fixes for error reporting.
    **     IAS 20220520 Fixed DAC validation for value which is empty string.
    **     IAS 20220714 Fixed DAC validation for imported sealed CLIENT-PRINCIPAL.
    ** 010 GBB 20230719 Fix for SESSION-ID, moved to a separate interface.
    ** 011 CA  20230724 Further reduce context-local usage.
    ** 012 LS  20250127 In readtimeStamp(), create the ts using the epoch and the offset.
    ** 013 OM  20250301 Performance optimisations. Code maintenance.
    ** 014 ICP 20250312 Added stubs for TENANT-NAME and TENANT-ID functions.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program 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 Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;

    import static com.goldencode.util.NativeTypeSerializer.*;
    import java.io.*;
    import java.nio.*;
    import java.nio.charset.*;
    import java.security.*;
    import java.time.*;
    import java.time.format.*;
    import java.util.*;
    import java.util.function.*;
    import javax.crypto.*;
    import javax.crypto.spec.*;
    import org.apache.commons.io.input.*;
    import org.apache.commons.lang3.*;

    /**
     * Implements the CLIENT-PRINCIPAL resource and all associated APIs.
     */
    public class ClientPrincipal
    extends HandleResource
    implements ClientPrincipalResource,
               CommonSessionId,
               Cloneable,
               Deletable,
               Externalizable
    {
       private static final Set<AttrInfo> MANDATORY = Collections.unmodifiableSet(EnumSet.of(
             AttrInfo.USER_ID,
             AttrInfo.DOMAIN_NAME,
             AttrInfo.SESSION_ID
       ));

       private static final Set<AttrInfo> NOT_FOR_MAC = Collections.unmodifiableSet(EnumSet.of(
             AttrInfo.MAC,
             AttrInfo.PRIMARY_PASSPHRASE,
             AttrInfo.SEAL_TIMESTAMP,
             AttrInfo.SEAL_TIMESTAMP_TZ
       ));

       /** A token used for non repetitive logging of missing tenantid implementation.
        *  It is to be removed from the class attributes after the feature is implemented. */
       private static volatile Object tenantidContextToken = null;

       /** A token used for non repetitive logging of missing tenantname implementation.
        *  It is to be removed from the class attributes after the feature is implemented. */
       private static volatile Object tenantnameContextToken = null;

       /** Attributes. */
       private final Map<AttrInfo, Optional<Object>> attributes = new LinkedHashMap<>();

       /** Flag indicating if this resource is was was deleted. */
       private boolean deleted = false;

       /** Generated SESSION-ID. */
       private final String generatedSessionId = generateSessionID();

       /** The set properties in this CLIENT-PRINCIPAL. */
       private final Map<String, String> properties = new LinkedHashMap<>();

       /** Flag indicating this object has been {@link #seal sealed()}. */
       private Boolean sealed = null;

       /** The domain access code used to {@link #seal seal} this object. */
       private String domainAccessCode = null;

       /**  A character value that represents the current state of the client-principal object.*/
       private LoginState loginState = LoginState.INITIAL;

       /** Flag indicated that CP was exported at least once. */
       private boolean exported = false;

       /** Flag indicated that attributed values should not be exposed (see #4801). */
       private boolean hideValues = false;

       /** SEAL failure reason. */
       private SealFailure sealFailure = null;

       /** Flag indicated that CP was sealed on import */
       private boolean sealedOnImport = false;

       /** The value of the 0x00d0001c undocumented hidden attribute */
       private long magic = 2;

       /** default constructor. */
       public ClientPrincipal()
       {
          addHiddenAttrs();
       }

       /**
        * Create a new resource and assign it to the specified handle.
        *
        * @param    h
        *           The handle where to save the resource.  Must be not-null.
        */
       public static void create(handle h)
       {
          create(h, (character) null);
       }

       /**
        * Create a new resource and assign it to the specified handle.
        *
        * @param    h
        *           The handle where to save the resource.  Must be not-null.
        * @param    widgetPool
        *           The named widget pool where to save the widget.  If <code>null</code>, use the
        *           closest unnamed pool.
        */
       public static void create(handle h, String widgetPool)
       {
          create(h, new character(widgetPool));
       }

       /**
        * Create a new resource and assign it to the specified handle.
        *
        * @param    h
        *           The handle where to save the resource.  Must be not-null.
        * @param    widgetPool
        *           The named widget pool where to save the widget.  If <code>null</code>, use the
        *           closest unnamed pool.
        */
       public static void create(handle h, character widgetPool)
       {
          // validate widget pool before anything else!!!
          if (!WidgetPool.validWidgetPool(widgetPool))
          {
             return;
          }

          ClientPrincipal client = new ClientPrincipal();
          h.assign(client);

          if (widgetPool != null)
          {
             // unnamed pools are not allowed
             client.addToPool(widgetPool);
          }
       }

       /**
        * Reports if this object is valid for use.
        *
        * @return   <code>true</code> if we are valid (can be used).
        */
       @Override
       public boolean valid()
       {
          return !deleted;
       }

       /**
        * Perform actual delete of an resource. At the time of this call, it is assumed the resource
        * is valid for deletion (the handle and the resource are both valid).
        */
       @Override
       public void delete()
       {
          super.delete();

          // just set it as invalid and clear some state
          deleted = true;
          attributes.clear();
          properties .clear();
          domainAccessCode = null;
       }

       /**
        * Get the USER-ID attribute.
        *
        * @return   See above.
        */
       @Override
       public character getUserId()
       {
          return getCharAttrValue(AttrInfo.USER_ID);
       }

       /**
        * Set the USER-ID Attribute.
        *
        * @param    userId
        *           The value of the USER-ID attribute.
        */
       @Override
       public void setUserId(character userId)
       {
          setCharAttrValue(AttrInfo.USER_ID, userId, true);
       }

       /**
        * Set the USER-ID Attribute.
        *
        * @param    userId
        *           The value of the USER-ID attribute.
        */
       @Override
       public void setUserId(String userId)
       {
          setUserId(new character(userId));
       }

       /**
        * Get the DOMAIN-TYPE attribute.
        *
        * @return   See above.
        */
       @Override
       public character getDomainType()
       {
          return getCharAttrValue(AttrInfo.DOMAIN_TYPE);
       }

       /**
        * Set the DOMAIN-TYPE Attribute.
        *
        * @param    domType
        *           The value of the DOMAIN-TYPE attribute.
        */
       @Override
       public void setDomainType(character domType)
       {
          setCharAttrValue(AttrInfo.DOMAIN_TYPE, domType);
       }

       /**
        * Set the DOMAIN-TYPE Attribute.
        *
        * @param    domainType
        *           The value of the DOMAIN-TYPE attribute.
        */
       @Override
       public void setDomainType(String domainType)
       {
          setDomainType(new character(domainType));
       }

       /**
        * Get the DOMAIN-NAME attribute.
        *
        * @return   See above.
        */
       @Override
       public character getDomainName()
       {
          return getCharAttrValue(AttrInfo.DOMAIN_NAME);
       }

       /**
        * Set the DOMAIN-NAME Attribute.
        *
        * @param    domainName
        *           The value of the DOMAIN-NAME attribute.
        */
       @Override
       public void setDomainName(character domainName)
       {
          setCharAttrValue(AttrInfo.DOMAIN_NAME, domainName, true);
       }

       /**
        * Set the DOMAIN-NAME Attribute.
        *
        * @param    domainName
        *           The value of the DOMAIN-NAME attribute.
        */
       @Override
       public void setDomainName(String domainName)
       {
          setDomainName(new character(domainName));
       }

       /**
        * Get the SESSION-ID attribute.
        *
        * @return   See above.
        */
       @Override
       public character getSessionId()
       {
          return getCharAttrValue(AttrInfo.SESSION_ID);
       }

       /**
        * Set the SESSION-ID Attribute.
        *
        * @param    sessionId
        *           The value of the SESSION-ID attribute.
        */
       @Override
       public void setSessionId(character sessionId)
       {
          if (sessionId != null && !sessionId.isUnknown() &&
                   StringUtils.isEmpty(sessionId.toStringMessage()))
          {
             attributeEmptyError("SESSION-ID");
             return;
          }
          setCharAttrValue(AttrInfo.SESSION_ID, sessionId, true);
       }

       /**
        * Set the SESSION-ID Attribute.
        *
        * @param    sessionId
        *           The value of the SESSION-ID attribute.
        */
       @Override
       public void setSessionId(String sessionId)
       {
          setSessionId(new character(sessionId));
       }

       /**
        * Get the DOMAIN-DESCRIPTION attribute.
        *
        * @return   See above.
        */
       @Override
       public character getDomainDescription()
       {
          return getCharAttrValue(AttrInfo.DOMAIN_DESCRIPTION);
       }

       /**
        * Set the DOMAIN-DESCRIPTION Attribute.
        *
        * @param    desc
        *           The value of the DOMAIN-DESCRIPTION attribute.
        */
       @Override
       public void setDomainDescription(String desc)
       {
          setDomainDescription(new character(desc));
       }

       /**
        * Set the DOMAIN-DESCRIPTION Attribute.
        *
        * @param    desc
        *           The value of the DOMAIN-DESCRIPTION attribute.
        */
       @Override
       public void setDomainDescription(character desc)
       {
          setCharAttrValue(AttrInfo.DOMAIN_DESCRIPTION, desc);
       }

       /**
        * Get the CLIENT-TTY attribute.
        *
        * @return   See above.
        */
       @Override
       public character getClientTty()
       {
          return getCharAttrValue(AttrInfo.CLIENT_TTY);
       }

       /**
        * Set the CLIENT-TTY Attribute.
        *
        * @param    tty
        *           The value of the CLIENT-TTY attribute.
        */
       @Override
       public void setClientTty(String tty)
       {
          setClientTty(new character(tty));
       }

       /**
        * Set the CLIENT-TTY Attribute.
        *
        * @param    tty
        *           The value of the CLIENT-TTY attribute.
        */
       @Override
       public void setClientTty(character tty)
       {
          setCharAttrValue(AttrInfo.CLIENT_TTY, tty);
       }

       /**
        * Get the CLIENT-WORKSTATION attribute.
        *
        * @return   See above.
        */
       @Override
       public character getClientWks()
       {
          return getCharAttrValue(AttrInfo.CLIENT_WORKSTATION);
       }

       /**
        * Set the CLIENT-WORKSTATION Attribute.
        *
        * @param    wks
        *           The value of the CLIENT-WORKSTATION attribute.
        */
       @Override
       public void setClientWks(String wks)
       {
          setClientWks(new character(wks));
       }

       /**
        * Set the CLIENT-WORKSTATION Attribute.
        *
        * @param    wks
        *           The value of the CLIENT-WORKSTATION attribute.
        */
       @Override
       public void setClientWks(character wks)
       {
          setCharAttrValue(AttrInfo.CLIENT_WORKSTATION, wks);
       }

       /**
        * Get the AUDIT-EVENT-CONTEXT attribute.
        *
        * @return   See above.
        */
       @Override
       public character getAuditEventContext()
       {
          return getCharAttrValue(AttrInfo.AUDIT_EVENT_CONTEXT);
       }

       /**
        * Set the AUDIT-EVENT-CONTEXT Attribute.
        *
        * @param    ctx
        *           The value of the AUDIT-EVENT-CONTEXT attribute.
        */
       @Override
       public void setAuditEventContext(String ctx)
       {
          setAuditEventContext(new character(ctx));
       }

       /**
        * Set the AUDIT-EVENT-CONTEXT Attribute.
        *
        * @param    ctx
        *           The value of the AUDIT-EVENT-CONTEXT attribute.
        */
       @Override
       public void setAuditEventContext(character ctx)
       {
          setCharAttrValue(AttrInfo.AUDIT_EVENT_CONTEXT, ctx);
       }

       /**
        * Get the LOGIN-HOST attribute.
        *
        * @return   See above.
        */
       @Override
       public character getLoginHost()
       {
          return getCharAttrValue(AttrInfo.LOGIN_HOST);
       }

       /**
        * Set the LOGIN-HOST Attribute.
        *
        * @param    host
        *           The value of the LOGIN-HOST attribute.
        */
       @Override
       public void setLoginHost(String host)
       {
          setLoginHost(new character(host));
       }

       /**
        * Set the LOGIN-HOST Attribute.
        *
        * @param    host
        *           The value of the LOGIN-HOST attribute.
        */
       @Override
       public void setLoginHost(character host)
       {
          setCharAttrValue(AttrInfo.LOGIN_HOST, host);
       }

       /**
        * Get the ROLES attribute.
        *
        * @return   See above.
        */
       @Override
       public character getRoles()
       {
          return getCharAttrValue(AttrInfo.ROLES);
       }

       /**
        * Set the ROLES Attribute.
        *
        * @param    roles
        *           The value of the ROLES attribute.
        */
       @Override
       public void setRoles(String roles)
       {
          setRoles(new character(roles));
       }

       /**
        * Set the ROLES Attribute.
        *
        * @param    rs
        *           The value of the ROLES attribute.
        */
       @Override
       public void setRoles(character rs)
       {
          setCharAttrValue(AttrInfo.ROLES, rs);
       }

       /**
        * Get the LOGIN-STATE attribute.
        *
        * @return   A character value that represents the current state of the
        *           client-principal object.
        */
       @Override
       public character getLoginState()
       {
          return new character(loginState.state());
       }

       /**
        * Set the LOGIN-STATE attribute. For internal use.
        *
        * @param state
        *        A character value that represents the current state of the
        *        client-principal object.
        */
       public void setLoginState(LoginState state)
       {
          this.loginState = state;
       }

       /**
        * Get the LOGIN-EXPIRATION-TIMESTAMP attribute.
        *
        * @return   The time stamp specifying when the client-principal object will expire
        *           client-principal object.
        */
       @Override
       public datetimetz getLoginExpirationTimestamp()
       {
          return (datetimetz)attributes.getOrDefault(AttrInfo.LOGIN_EXPIRATION_TIMESTAMP,
                Optional.of(new datetimetz())).orElse(new datetimetz());
       }

       /**
        * Get the LOGIN-EXPIRATION-TIMESTAMP attribute.
        *
        * @param    ts
        *           The time stamp specifying when the client-principal object will expire
        *           client-principal object.
        */
       @Override
       public void setLoginExpirationTimestamp(datetimetz ts)
       {
          // TODO: 4GL seems to include multiple versions of this attribute to serialized form
          if (wasSealed())
          {
             sealedError(AttrInfo.LOGIN_EXPIRATION_TIMESTAMP.aname());
             return;
          }
          if (ts == null || ts.isUnknown())
          {
             attributes.put(AttrInfo.LOGIN_EXPIRATION_TIMESTAMP, Optional.of(ts));
             return;
          }
          // expiration can have a minimum value of 1/1/1970 00:00:01.000-00:00
          if (ts.getYear() < 1970 || (ts.getYear() == 1970 && ts.getTime() < 1000))
          {
             ts = new datetimetz();
             return;
          }
          // expiration can have no greater precision than seconds
          ts.setTime(1000 * Math.round(ts.getTime() / 1000.0));
          attributes.put(AttrInfo.LOGIN_EXPIRATION_TIMESTAMP, Optional.of(ts));
       }

       /**
        * Get the SEAL-TIMESTAMP attribute.
        *
        * @return   The time stamp (as a DATETIME-TZ value) for when the client-principal
        *           object was sealed() in the LOGIN state
        */
       @Override
       public datetimetz getSealTimestamp()
       {
          return (datetimetz)attributes.getOrDefault(AttrInfo.SEAL_TIMESTAMP,
                Optional.of(new datetimetz())).orElse(new datetimetz());
       }

       /**
        * Get the PRIMARY-PASSPHRASE attribute.
        * Always returns error
        *
        * @return <code>null</code>
        */
       @Override
       public character getPrimaryPassphrase()
       {
          ErrorManager.recordOrShowError(16617, "PRIMARY-PASSPHRASE", "queryable", type());
          // <attribute> is not a <settable/queryable> attribute for <handle type>.
          return null;
       }

       /**
        * Get the PRIMARY-PASSPHRASE attribute value (for internal use).
        *
        * @return PRIMARY-PASSPHRASE attribute value.
        */
       @Override
       public character primaryPassphrase()
       {
          return new character((String)attributes.getOrDefault(AttrInfo.PRIMARY_PASSPHRASE, Optional.empty()).orElse(null));
       }

       /**
        * Set the PRIMARY-PASSPHRASE attribute.
        */
       @Override
       public void setPrimaryPassphrase(String passphrase)
       {
          setPrimaryPassphrase(new character(passphrase));
       }

       /**
        * Set the PRIMARY-PASSPHRASE attribute.
        */
       @Override
       public void setPrimaryPassphrase(character passphrase)
       {
          setCharAttrValue(AttrInfo.PRIMARY_PASSPHRASE, passphrase, true);
       }

       /**
        * Get the QUALIFIED-USER-ID attribute.
        */
       @Override
       public character getQualifiedUid()
       {
          String userId = getUserId().value;
          String domainName = getDomainName().value;
          String s;
          if (StringUtils.isEmpty(userId) )
          {
             s = StringUtils.isEmpty(domainName) ? "" : "@" + domainName;
          }
          else
          {
             s = StringUtils.isEmpty(domainName) ?  userId : userId + "@" + domainName;
          }
          return new character(s);
       }

       /**
        * Set the QUALIFIED-USER-ID attribute.
        */
       @Override
       public void setQualifiedUid(String uid)
       {
          setQualifiedUid(new character(uid));
       }

       /**
        * Set the QUALIFIED-USER-ID attribute.
        */
       @Override
       public void setQualifiedUid(character uid)
       {
          if (uid == null || uid.isUnknown())
          {
             attributeUnknownError("QUALIFIED-USER-ID");
             return;
          }
          if (wasSealed())
          {
             sealedError("QUALIFIED-USER-ID");
             return;
          }
          setQualifiedIserId(uid.toStringMessage());
       }

       /**
        * Set the STATE-DETAIL attribute.
        */
       @Override
       public character getStateDetail()
       {
          String state;
          if (sealed == null)
          {
             state = "The CLIENT-PRINCIPAL object has never been sealed";
          }
          else
          {
             switch(this.loginState)
             {
             case LOGIN:
                state = "The CLIENT-PRINCIPAL object is currently logged in";
                break;
             case FAILED:
                Optional<Object> reason = attributes.get(AttrInfo.FAIL_REASON);
                if (reason != null && reason.isPresent())
                {
                   state = (String)reason.get();
                }
                else
                {
                   state = "The CLIENT-PRINCIPAL object authentication failed";
                }
                break;
             case EXPIRED:
                state = "The CLIENT-PRINCIPAL object has expired";
                break;
             case LOGOUT:
                state = "The CLIENT-PRINCIPAL object is currently logged out and unsealed";
                break;
             default: // TODO: the exact 4GL message is not known yet. This one is a placeholder
                state = "The CLIENT-PRINCIPAL object is in the " + this.loginState.state() + " state.";
             }
          }
          return new character(state);
       }

       /**
        * Get a comma-separated list of logical database names that is stored in the
        * client-principal object.
        *
        * @return a comma-separated list of logical database names.
        */
       @LegacyAttribute(name = "DB-LIST")
       public character getDbList()
       {
          // TODO: implement
          return new character();
       }

       /**
        * Set this property in the CLIENT-PRINCIPAL object.
        *
        * @param    prop
        *           The property's name.
        * @param    val
        *           The property's value.
        *
        * @return   <code>true</code> if the property could be set.
        */
       @Override
       public logical setProperty(character prop, character val)
       {
          return setProperty(prop.getValue(), val.getValue());
       }

       /**
        * Set this property in the CLIENT-PRINCIPAL object.
        *
        * @param    prop
        *           The property's name.
        * @param    val
        *           The property's value.
        *
        * @return   <code>true</code> if the property could be set.
        */
       @Override
       public logical setProperty(character prop, String val)
       {
          return setProperty(prop.getValue(), val);
       }

       /**
        * Set this property in the CLIENT-PRINCIPAL object.
        *
        * @param    prop
        *           The property's name.
        * @param    val
        *           The property's value.
        *
        * @return   <code>true</code> if the property could be set.
        */
       @Override
       public logical setProperty(String prop, character val)
       {
          return setProperty(prop, val.getValue());
       }

       /**
        * Set this property in the CLIENT-PRINCIPAL object.
        *
        * @param    prop
        *           The property's name.
        * @param    val
        *           The property's value.
        *
        * @return   <code>true</code> if the property could be set.
        */
       @Override
       public logical setProperty(String prop, String val)
       {
          if (prop == null || val == null)
          {
             ErrorManager.recordOrShowError(4065,
                                            "**The SET-PROPERTY attribute on the CLIENT-PRINCIPAL " +
                                            "widget has invalid arguments",
                                            true, false, false, false);
             return logical.FALSE;
          }
          if (wasSealed())
          {
             ErrorManager.recordOrShowError(14548,
                                           "Cannot set attributes or properties for a sealed " +
                                           "CLIENT-PRINCIPAL",
                                           true, false, false, false);

             return logical.FALSE;
          }

          if (this.exported)
          {
             return logical.FALSE;
          }

          properties.put(prop, val);

          return logical.TRUE;
       }

       /**
        * Get the value of the specified property, from the CLIENT-PRINCIPAL object.
        *
        * @param    prop
        *           The property's name.
        *
        * @return   The property's value.
        */
       @Override
       public character getProperty(character prop)
       {
          return getProperty(prop.getValue());
       }

       /**
        * Get the value of the specified property, from the CLIENT-PRINCIPAL object.
        *
        * @param    prop
        *           The property's name.
        *
        * @return   The property's value.
        */
       @Override
       public character getProperty(String prop)
       {
          if (prop == null)
          {
             ErrorManager.recordOrShowError(4065,
                                            "**The GET-PROPERTY attribute on the CLIENT-PRINCIPAL " +
                                            "widget has invalid arguments",
                                            true, false, false, false);
             return new character("");
          }

          return new character(properties.get(prop));
       }

       /**
        * Get a list of all application-defined properties stored in the client-principal object.
        *
        * @return   Returns a comma-separated list of all application-defined properties stored in
        *           the client-principal object
        */
       @Override
       public character listPropertyNames()
       {
          return new character(String.join(",", properties.keySet()));
       }

       /**
        * Export the state of this CLIENT-PRINCIPAL object to a byte representation.
        *
        * @return   The byte representation as a {@link raw} instance.
        */
       @Override
       public raw exportPrincipal()
       {
          this.exported = true;
          return new raw(serializePrincipal());
       }

       /**
        * Import the state of this CLIENT-PRINCIPAL object from the specified byte representation.
        *
        * @param    data
        *           The byte representation.
        */
       @Override
       public logical importPrincipal(raw data)
       {
          if (data == null || data.isUnknown() )
          {
             ErrorManager.recordOrShowError(4065,
                   "The IMPORT-PRINCIPAL attribute on the CLIENT-PRINCIPAL widget has invalid " +
                   "arguments",
                   false, true, false, false);
             return logical.FALSE;
          }
          byte[] b = data.getByteArray();

          return logical.of(importPrincipal(b));
       }

       /**
        * Get the tenant ID associated with the client-principal object.
        *
        * @return   The tenant ID as an integer.
        */
       @Override
       public integer tenantId()
       {
          tenantidContextToken =
             UnimplementedFeature.missing("CLIENTPRINCIPAL:TENANT-ID method is not implemented.",
                                          tenantidContextToken);

          return new integer(0);
       }

       /**
        * Retrieves the tenant ID associated with the user identity sealed in the client-principal
        * and assigned to a connection to the specified multi-tenant database.
        *
        * @param    dbExp
        *           The database expression specifying the multi-tenant database connection.
        *
        * @return   The tenant ID as an integer.
        */
       @Override
       public integer tenantId(character dbExp)
       {
          tenantidContextToken =
             UnimplementedFeature.missing("CLIENTPRINCIPAL:TENANT-ID method is not implemented.",
                                          tenantidContextToken);
          return new integer(0);
       }

       /**
        * Get the tenant name associated with the client-principal object.
        *
        * @return   The time stamp specifying when the client-principal object will expire
        *           client-principal object.
        */
       @Override
       public character tenantName()
       {
          tenantnameContextToken =
             UnimplementedFeature.missing("CLIENTPRINCIPAL:TENANT-NAME method is not implemented.",
                                          tenantnameContextToken);
          return new character("");
       }

       /**
        * Retrieves the tenant name associated with the user identity sealed in the client-principal
        * and assigned to a connection to the specified multi-tenant database.
        *
        * @param    dbExp
        *           The database expression specifying the multi-tenant database connection.
        *
        * @return   The tenant name.
        */
       @Override
       public character tenantName(character dbExp)
       {
          tenantnameContextToken =
             UnimplementedFeature.missing("CLIENTPRINCIPAL:TENANT-NAME method is not implemented.",
                                          tenantnameContextToken);
          return new character("");
       }

       /**
        * Seal this CLIENT-PRINCIPAL, using the given domain access code.
        *
        * @param    domainAccessCode
        *           The access code.
        */
       @Override
       public logical seal(character domainAccessCode)
       {
          return seal(domainAccessCode.getValue());
       }

       /**
        * Seal this CLIENT-PRINCIPAL, using the given domain access code.
        *
        * @param    domainAccessCode
        *           The access code.
        */
       @Override
       public logical seal(String domainAccessCode)
       {
          if (domainAccessCode == null)
          {
             ErrorManager.recordOrShowError(4065,
                                            "**The SEAL attribute on the CLIENT-PRINCIPAL " +
                                            "widget has invalid arguments",
                                            false, false, false, false);
             return logical.FALSE;
          }
          return seal(domainAccessCode, SealCallee.APP);
       }

       /**
        * Seal this CLIENT-PRINCIPAL, using the given domain access code, for internal use.
        *
        * @param    domainAccessCode
        *           The access code.
        * @param    callee
        *           the method callee.

        * @return   <code>true</code> id successful.
        */
       @Override
       public logical seal(String domainAccessCode, SealCallee callee)
       {
          boolean expired = false;
          datetimetz now = datetimetz.now();
          datetimetz loginExpirationTimestamp = getLoginExpirationTimestamp();
          if (loginExpirationTimestamp != null && !loginExpirationTimestamp.isUnknown() &&
              loginExpirationTimestamp.compareTo(now) <= 0)
          {
             expired = true;
             this.sealed = Boolean.FALSE;
             this.loginState = LoginState.EXPIRED;
             this.sealFailure = SealFailure.EXPIRED;
          }
          if (StringUtils.isEmpty(domainAccessCode))
          {
             // current MAC calculation algorithm doesn't accept empty strings
             domainAccessCode = " ";
          }
          if (hideValues || !canBeSealed())
          {
             if (sealFailure == null)
             {
                sealFailure = SealFailure.MISSING_ATTRIBUTES;
             }
             if (callee != SealCallee.SET_DB_CLIENT)
             {
                sealFailed("not all required attributes have been set");
             }
             else {
             }
             return logical.FALSE;
          }

          if (loginState != LoginState.INITIAL)
          {
             if (!getSealTimestamp().isUnknown() ||
                 loginState == LoginState.LOGIN ||
                 loginState == LoginState.FAILED)
             {
                sealFailure = SealFailure.INVALID_STATE;
                sealFailed("object is in an improper login state");
                return logical.FALSE;
             }
             else if (loginState == LoginState.EXPIRED)
             {
                if (callee == SealCallee.APP)
                {
                   sealFailed("object expired");
                }
             }
             else
             {
                 finalizeSeal(domainAccessCode);
                 sealed = Boolean.TRUE;
                 return logical.TRUE;
             }
          }

          // no validation for the (domain-name, access-code) pair is done here - this is validated
          // when the auth is attempted via SET-DB-CLIENT
          this.domainAccessCode = domainAccessCode;

          if (expired)
          {
             return logical.FALSE;

          }
          this.loginState = LoginState.LOGIN;
          finalizeSeal(domainAccessCode);

          this.sealed = Boolean.TRUE;
          return logical.TRUE;
       }

       /**
        * Get the SEAL operation failure reason.
        *
        * @return SEAL operation failure reason.
        */
       public SealFailure sealFailure()
       {
          return sealFailure;
       }

       /**
        * Indicates that the identity asserted in the unsealed client-principal object cannot be authenticated.
        *
        * @return   <code>true</code> id successful
        */
       @Override
       public logical authenticationFailed()
       {
          return authenticationFailed((character)null);
       }

       /**
        * Indicates that the identity asserted in the unsealed client-principal object cannot be authenticated.
        *
        * @param    reason
        *           An optional character expression that specifies the reason for the authentication
        *           failure.
        *
        * @return   <code>true</code> id successful
        */
       @Override
       public logical authenticationFailed(character reason)
       {
          return authenticationFailed(reason, false);
       }
       /**
        * Indicates that the identity asserted in the unsealed client-principal object cannot be authenticated.
        *
        * @param    reason
        *           An optional character expression that specifies the reason for the authentication
        *           failure.
        * @param    fromSetClient
        *           flag indicating that called from SET-CLIENT.
        *
        * @return   <code>true</code> id successful.
        */
       @Override
       public logical authenticationFailed(character reason, boolean fromSetClient)
       {
          if (wasSealed() && !fromSetClient)
          {
             ErrorManager.recordOrShowError(new int[] {14549},
                   new String[] {
                         "CLIENT-PRINCIPAL:AUTHENTICATION-FAILED error because object is sealed"
                   },
                   false, false, false, false, false);
             return logical.FALSE;
          }
          if (reason != null && !reason.isUnknown())
          {
             attributes.put(AttrInfo.FAIL_REASON, Optional.of(reason.toStringMessage()));
          }
          this.sealed = Boolean.FALSE;
          this.loginState = LoginState.FAILED;
          // It is unclear what is the MAC key in this case.
          finalizeSeal(" ");
          return logical.TRUE;
       }

       /**
        * Indicates that the identity asserted in the unsealed client-principal object cannot be authenticated.
        *
        * @param    reason
        *           An optional character expression that specifies the reason for the authentication
        *           failure.
        * @param    fromSetClient
        *           flag indicating that called from SET-CLIENT.
        *
        * @return   <code>true</code> id successful
        */
       @Override
       public logical authenticationFailed(String reason, boolean fromSetClient)
       {
          return authenticationFailed(new character(reason), fromSetClient);
       }

       /**
        * Indicates that the identity asserted in the unsealed client-principal object cannot be authenticated.
        *
        * @param    reason
        *           An optional character expression that specifies the reason for the authentication
        *           failure.
        *
        * @return   <code>true</code> id successful.
        */
       @Override
       public logical authenticationFailed(String reason)
       {
          return authenticationFailed(new character(reason));
       }

       /**
        * Validates the message authentication code (MAC) generated by the SEAL( ) method to seal a
        * client-principal object.
        *
        */
       @Override
       public logical validateSeal()
       {
          return validateSeal(new character());
       }

       /**
        * Validates the message authentication code (MAC) generated by the SEAL( ) method to seal a
        * client-principal object.
        *
        * @param    domainAccessCode
        *           The access code.
        */
       @Override
       public logical validateSeal(String domainAccessCode)
       {
          return validateSeal(new character(domainAccessCode));
       }

       /**
        * Validates the message authentication code (MAC) generated by the SEAL( ) method to seal a
        * client-principal object.
        *
        * @param    domainAccessCode
        *           The access code.
        */
       @Override
       public logical validateSeal(character domainAccessCode)
       {
          switch(loginState)
          {
             case LOGIN:
                // temporarily remove the MAC, so that we can re-compute the MAC with the new password
                Optional<Object> mac = attributes.remove(AttrInfo.MAC);
                try
                {
                   if (mac != null && mac.isPresent())
                   {
                      String dac;
                      if (domainAccessCode == null || domainAccessCode.isUnknown())
                      {
                         character domain = getCharAttrValue(AttrInfo.DOMAIN_NAME);
                         Optional<String> odac = SecurityPolicyManager.getDomainAccessCode(
                               domain.getValue(),
                               err ->
                                  ErrorManager.recordOrShowError(new int[] {err.code},
                                     new String[] {
                                           "CLIENT-PRINCIPAL:VALIDATE-SEAL failed because " + err.message
                                     },
                                     false, false, false, false, false)
                               );
                         if (!odac.isPresent())
                         {
                            return logical.FALSE;
                         }
                         dac = odac.get();
                      }
                      else
                      {
                         dac = domainAccessCode.getValue();
                      }
                      byte[] mac1 = mac(dac);
                      byte[] mac2 = (byte[]) mac.get();
                      if (Arrays.equals(mac1, mac2))
                      {
                         return logical.TRUE;
                      }
                   }
                }
                finally
                {
                   attributes.put(AttrInfo.MAC, mac);
                }
                ErrorManager.recordOrShowError(new int[] {14541},
                      new String[] {
                            "CLIENT-PRINCIPAL:VALIDATE-SEAL failed because keys do not match"
                      },
                      false, false, false, false, false);
                break;

             case FAILED:
                ErrorManager.recordOrShowError(new int[] {16369},
                      new String[] {
                        "CLIENT-PRINCIPAL:VALIDATE-SEAL failed because - Failed account authentication"
                      },
                      false, false, false, false, false);
                break;

             default:
                ErrorManager.recordOrShowError(new int[] {14541},
                      new String[] {
                         "CLIENT-PRINCIPAL:VALIDATE-SEAL failed because object is in an improper state"
                      },
                      false, false, false, false, false);
                break;
          }
          return logical.FALSE;
       }

       /**
        * Indicates that the user represented by the sealed() client-principal object (in the LOGIN
        * state) has logged out of their current user login session.
        */
       @Override
       public logical logout()
       {
          if (isSealed())
          {
             if (loginState == LoginState.LOGIN || loginState == LoginState.SSO)
             {
                loginState = LoginState.LOGOUT;
                attributes.put(AttrInfo.MAC, Optional.of(mac(domainAccessCode)));
                return logical.TRUE;
             }
             ErrorManager.recordOrShowError(new int[] {14552},
                   new String[] {
                         "CLIENT-PRINCIPAL:LOGOUT failed because object is in wrong login state"
                   },
                   false, false, false, false, false);
             return logical.FALSE;
          }

          ErrorManager.recordOrShowError(new int[] {14551},
                new String[] {
                      "CLIENT-PRINCIPAL:LOGOUT failed because object is not sealed"
                },
                false, false, false, false, false);
          return logical.FALSE;
       }

       /**
        * Initializes the current object by setting the passed parameters after first validating them.
        *
        * @param   qualifiedUid
        *          A character expression that evaluates to a fully qualified user ID (user name and
        *          domain name delimited by the '@' character)
        *
        * @return  <code>true</code> on success
        */
       @Override
       public logical initialize(character qualifiedUid)
       {
          return init(qualifiedUid, new character(), new datetimetz(), null);
       }

       /**
        * Initializes the current object by setting the passed parameters after first validating them.
        *
        * @param   qualifiedUid
        *          A character expression that evaluates to a fully qualified user ID (user name and
        *          domain name delimited by the '@' character)
        * @param   sessionId
        *          An optional character expression that evaluates to the user's application login session ID.
        *
        * @return  <code>true</code> on success
        */
       @Override
       public logical initialize(character qualifiedUid, character sessionId)
       {
          return init(qualifiedUid, sessionId, new datetimetz(), null);
       }

       /**
        * Initializes the current object by setting the passed parameters after first validating them.
        *
        * @param   qualifiedUid
        *          A character expression that evaluates to a fully qualified user ID (user name and
        *          domain name delimited by the '@' character)
        * @param   sessionId
        *          An optional character expression that evaluates to the user's application login
        *          session ID.
        * @param   expiration
        *          An optional DATETIME-TZ expression that evaluates to a date and time value that
        *          specifies the expiration of thee client-principal user credentials
        *
        * @return  <code>true</code> on success
        */
       @Override
       public logical initialize(character qualifiedUid, character sessionId, BaseDataType expiration)
       {
          return init(qualifiedUid, sessionId, expiration, null);
       }

       /**
        * Initializes the current object by setting the passed parameters after first validating them.
        *
        * @param   qualifiedUid
        *          A character expression that evaluates to a fully qualified user ID (user name and
        *          domain name delimited by the '@' character)
        * @param   sessionId
        *          An optional character expression that evaluates to the user's application login
        *          session ID.
        * @param   expiration
        *          An optional DATETIME-TZ expression that evaluates to a date and time value that
        *          specifies the expiration of thee client-principal user credentials
        * @param   primaryPassphrase
        *          An optional character expression that evaluates to the cleartext or encrypted value
        *          of the user's account password.
        *
        * @return  <code>true</code> on success
        */
       @Override
       public logical initialize(character qualifiedUid, character sessionId,
                                 BaseDataType expiration, character primaryPassphrase)
       {
          if (primaryPassphrase == null || primaryPassphrase.isUnknown())
          {
             ErrorManager.recordOrThrowError(9178,
                   "Invalid primary-passphrase used in INITIALIZE method",false, false);
             return logical.FALSE;
          }
          return init(qualifiedUid, sessionId, expiration, primaryPassphrase.toStringMessage());
       }

       /**
        * Initializes the current object by setting the passed parameters after first validating them.
        *
        * @param   qualifiedUid
        *          A character expression that evaluates to a fully qualified user ID (user name and
        *          domain name delimited by the '@' character)
        * @param   sessionId
        *          An optional character expression that evaluates to the user's application login
        *          session ID.
        * @param   expiration
        *          An optional DATETIME-TZ expression that evaluates to a date and time value that
        *          specifies the expiration of thee client-principal user credentials
        * @param   primaryPassphrase
        *          An optional character expression that evaluates to the cleartext or encrypted value
        *          of the user's account password.
        *
        * @return  <code>true</code> on success
        */
       private logical init(character qualifiedUid,
                            character sessionId,
                            BaseDataType expiration,
                            String primaryPassphrase)
       {
          if (qualifiedUid == null || qualifiedUid.isUnknown())
          {
             ErrorManager.recordOrThrowError(15918,
                   "Required parameter for INITIALIZE was passed the Unknown value",
                   false, false);
             return logical.FALSE;
          }
          if ((expiration != null) && !expiration.isUnknown() && !(expiration instanceof datetimetz))
          {
             ErrorManager.recordOrThrowError(9178,
                   "Invalid expiration used in INITIALIZE method",
                   false, false);
             return logical.FALSE;
          }
          if (sessionId != null && !sessionId.isUnknown() &&
              StringUtils.isEmpty(sessionId.toStringMessage()))
          {
             ErrorManager.recordOrThrowError(new int[] {14553, 9178},
                      new String[] {
                          "Required attribute can not be set to the empty string",
                          "Invalid session-id used in INITIALIZE method.",
                      },
                      false, false, false);
                return logical.FALSE;
          }

          this.loginState = LoginState.INITIAL;
          reset();

          setQualifiedIserId(qualifiedUid.toStringMessage());
          setSessionId(sessionId == null || sessionId.isUnknown() ?
                                           generatedSessionId :
                                           sessionId.toStringMessage());
          this.magic = 0xf;
          if (expiration != null && !expiration.isUnknown())
          {
             this.magic |= 0x24000;
             setLoginExpirationTimestamp((datetimetz)expiration);
          }

          if (primaryPassphrase != null)
          {
             this.magic |= 0x80;
             setPrimaryPassphrase(primaryPassphrase);
          }
          addHiddenAttrs();
          return logical.TRUE;
       }

       /** Reset state. */
       private void reset()
       {
          this.sealed = null;
          this.exported = false;
          this.sealedOnImport = false;
          this.hideValues = false;
          this.magic = 2;
          attributes.clear();
          properties.clear();
       }

       /**
        * Create a copy of this CLIENT-PRINCIPAL instance.
        *
        * <p>Relies on {@link #exportPrincipal} and {@link #importPrincipal} to transfer the state
        * from this instance to the copy.
        *
        * @return   A copy of this instance.
        */
       @Override
       public ClientPrincipal clone()
       {
          raw bytes = exportPrincipal();

          ClientPrincipal copy = new ClientPrincipal();
          copy.importPrincipal(bytes);

          return copy;
       }

       /**
        * Validate CLIENT-PRINCIPAL against domain password.
        *
        * @param where
        *        subsystem name
        * @param dac
        *        domain access code
        * @param password
        *        domain password
        * @param callee
        *        where the method is called from
        *
        * @return <code>true</code> if successful
        */
       @Override
       public logical validatePassword(String where, String dac, String password, SealCallee callee)
       {
          character pwd = primaryPassphrase();
          if (password == null)
          {
             if (callee != SealCallee.APP && pwd.isUnknown())
             {
                authenticationFailed(16373, where, "Cannot access user account information");
                authenticationFailed("Cannot access the account authentication information");
             }
             else
             {
                authenticationFailed(16374, where, "User account does not exist");
                authenticationFailed("The user account does not exist");
             }
             return logical.FALSE;
          }
          logical rc = trySeal(where, dac, callee);
          if (rc.booleanValue())
          {
             if (!SecurityOps.encode(pwd).toStringMessage().equals(password))
             {
                authenticationFailed(16369, where, "Failed account authentication");
                authenticationFailed("The user failed account authentication", true);
                return logical.FALSE;
             }
          }
          return rc;
       }

       /**
        * Validate CLIENT-PRINCIPAL against domain access code.
        *
        * @param where
        *        subsystem name
        * @param dac
        *        domain access code
        * @param callee
        *        where the method is called from
        *
        * @return <code>true</code> if successful
        */
       @Override
       public logical validateDomainAccessCode(String where, String dac, SealCallee callee)
       {
          logical rc = trySeal(where, dac, callee);
          if (rc.booleanValue())
          {
             if (StringUtils.isEmpty(dac))
             {
                // current MAC calculation algorithm doesn't accept empty strings
                dac = " ";
             }
             if ((getDomainAccessCode() != null && !dac.equals(getDomainAccessCode())) ||
                 // imported CP
                 (getDomainAccessCode() == null && !validateDomainAccessCode(dac)))
             {
                // TODO: check the error message
                if (callee != SealCallee.SET_CLIENT)
                {
                   authenticationFailed(16369, where, "Failed account authentication");
                   authenticationFailed("The user failed account authentication", true);
                }
                return logical.FALSE;
             }
          }
          return rc;
       }

       /**
        * Validate domain access code for a sealed principal
        *
        * @param dac
        *        domain access code
        *
        * @return <code>true</code> if successful
        */
       public boolean validateDomainAccessCode(String dac)
       {
          Optional<Object> omac = attributes.get(AttrInfo.MAC);
          if (omac == null || !omac.isPresent())
          {
             throw new IllegalStateException("MC attribute is set for a sealed principal");
          }
          byte[] mac = (byte[])omac.get();
          return Arrays.equals(mac, mac(dac));
       }
       /**
        * Check if this CLIENT-PRINCIPIAL has been sealed.
        *
        * @return   The sealed state.
        */
       @Override
       public boolean isSealed()
       {
          return Boolean.TRUE.equals(sealed);
       }

       /**
        * Import the state of this CLIENT-PRINCIPAL object from the specified byte representation.
        *
        * @param    data
        *           The byte representation.
        */
       private boolean importPrincipal(byte[] data)
       {
          if (wasSealed())
          {
             ErrorManager.recordOrShowError(new int[] {14543},
                   new String[] {
                     "CLIENT-PRINCIPAL:IMPORT failed because object already sealed"
                   },
                   false, false, false, false, false);
             return false;
          }
          reset();
          InputStream bis = new ByteArrayInputStream(data);
          CountingInputStream counter = new CountingInputStream(bis);
          int p;
          try(DataInputStream dis = new DataInputStream(counter))
          {
             p = counter.getCount();
             if (dis.readShort() != 0x0030)
             {
                throw new RuntimeException("Invalid stream at " + p);
             }
             p = counter.getCount();
             if (dis.readShort() != AttrInfo.LOGIN_STATE.type().code())
             {
                throw new RuntimeException("Invalid stream at " + p);
             }
             p = counter.getCount();
             if (dis.readShort() != AttrInfo.LOGIN_STATE.id())
             {
                throw new RuntimeException("Invalid stream at " + p);
             }
             p = counter.getCount();
             if (dis.readShort() != 2)
             {
                throw new RuntimeException("Invalid stream at " + p);
             }
             this.loginState = LoginState.valueOf(dis.readShort());

             p = counter.getCount();
             for (; p < data.length; p = counter.getCount())
             {
                AttrType atype = AttrType.valueOf(dis.readShort());
                AttrInfo ai = AttrInfo.valueOf(atype, dis.readShort());
                int alen = dis.readInt();
                byte[] bb;
                switch (atype)
                {
                   case CHAR:
                      bb = new byte[alen];
                      dis.read(bb);
                      setCharAttrValue(ai, new character(new String(bb, 0, alen -1, StandardCharsets.UTF_8)));
                      break;

                   case LONG:
                      if (alen != 8)
                      {
                         throw new RuntimeException("Invalid LONG attribute length: " + ai.aname());
                      }
                      long value = dis.readLong();
                      attributes.put(ai, Optional.ofNullable(value));
                      if (ai == AttrInfo.ATTR_001C)
                      {
                         magic = value;
                      }
                      break;

                   case BYTES:
                      bb = new byte[alen];
                      dis.read(bb);
                      attributes.put(ai, Optional.ofNullable(bb));
                      break;

                   case DATETIME_TZ:
                      readTimestamp(ai, dis);
                      break;

                   case LIST:
                      if (ai == AttrInfo.PROPS)
                      {
                         bb = new byte[alen];
                         dis.read(bb);
                         readProperties(bb);
                         // add a placeholder to the 'attributes' map (for a correct position in the export).
                         // The actual access to the properties
                         // is implemented using 'properties' map.
                         attributes.put(AttrInfo.PROPS, Optional.of(new ArrayList<String>()));
                      }
                      else
                      {
                         // TODO: implement DB-LIST deserialization
                         attributes.put(AttrInfo.DB_LIST, Optional.of(new ArrayList<String>()));
                      }
                      break;

                   default:
                      throw new RuntimeException("Unexpected attribute: " + ai.aname());
                }
             }
          }
          catch (Exception e)
          {
             ErrorManager.recordOrShowError(new int[] {16366},
                   new String[] {
                     "CLIENT-PRINCIPAL:IMPORT failed because - Internal Authentication subsystem error [28]"
                   },
                   false, false, false, false, false);
             return false;
          }

          LoginState prevState = this.loginState;
          this.hideValues = (!canBeSealed() || getDomainAccessCode() == null ||
                             prevState != LoginState.INITIAL) && (magic & 0xff) != 0x8f;
          datetimetz now = datetimetz.now();
          datetimetz loginExpirationTimestamp = getLoginExpirationTimestamp();
          if ( this.loginState == LoginState.INITIAL &&
                loginExpirationTimestamp != null && !loginExpirationTimestamp.isUnknown() &&
                loginExpirationTimestamp.compareTo(now) <= 0)
          {
             this.loginState = LoginState.EXPIRED;
             this.sealed = Boolean.FALSE;
             this.sealedOnImport = true;
             if (canBeSealed() && (magic & 0xff) == 0x8fL)
             {
                finalizeSeal(" ");
             }
          }

          switch (this.loginState)
          {
             case LOGIN:
             case SSO:
                this.sealed = Boolean.TRUE;
                break;
             case EXPIRED:
             case LOGOUT:
             case FAILED:
                this.sealed = Boolean.FALSE;
                break;
             default:
                break;
          }
          this.hideValues &= (sealed == null) || properties.isEmpty(); //|= prevState == this.loginState;
          return true;
       }

       /**
        * Check if object was ever sealed
        *
        * @return <code>true</code> if object was ever sealed
        */
       private boolean wasSealed()
       {
          return sealed != null;
       }

       /**
        * Get this object's domain access code (valid only if it was {@link #seal sealed()}.
        *
        * @return   The {@link #domainAccessCode domain access code}.
        */
       String getDomainAccessCode()
       {
          return domainAccessCode;
       }

       /**
        * Report CLIENT-PRINCIPAL validation error
        *
        * @param code
        *        error code
        * @param where
        *        subsystem name
        * @param reason
        *        reason
        */
       public static void validationFailed(int code, String where, String reason)
       {
          ErrorManager.recordOrShowError(new int[] {code},
                new String[] {
                      String.format("CLIENT-PRINCIPAL validation failed in %s because - %s",
                            where, reason)
                },
                false, false, false, false, false);
       }

       /**
        * Report CLIENT-PRINCIPAL authentication error
        *
        * @param code
        *        error code.
        * @param where
        *        subsystem name.
        * @param reason
        *        reason.
        */
       public static void authenticationFailed(int code, String where, String reason)
       {
          ErrorManager.recordOrShowError(new int[] {code, 13691},
                new String[] {
                      String.format("CLIENT-PRINCIPAL authentication failed in %s because - %s",
                            where, reason),
                      String.format("Failed to set %s user id.", where)
                },
                false, false, false, false, false);
       }

       /**
        * Raise or log an error for the specified attribute, as it can't be set to unknown value.
        */
       private static void argumentUnknownError()
       {
          String[] texts =
          {
             "Required parameter for INITIALIZE was passed the Unknown value",
          };
          int[] nums = { 15918 };
          ErrorManager.recordOrThrowError(nums, texts, false, true);
       }

       /**
        * Raise or log an error for the specified attribute, as it can't be set to unknown value.
        *
        * @param    attr
        *           The attribute being accessed.
        */
       private static void attributeUnknownError(String attr)
       {
          String[] texts =
          {
             "Required attribute can not be set to the Unknown value",
             "Unable to set attribute " + attr + " in widget  of type CLIENT-PRINCIPAL."
          };
          int[] nums = { 14553, 3131 };
          ErrorManager.recordOrThrowError(nums, texts, false, false, false);
       }

       /**
        * Raise or log an error for the specified attribute, as it can't be set to unknown value.
        *
        * @param    attr
        *           The attribute being accessed.
        */
       private static void attributeEmptyError(String attr)
       {
          String[] texts =
          {
             "Required attribute can not be set to the empty string",
             "Unable to set attribute " + attr + " in widget  of type CLIENT-PRINCIPAL."
          };
          int[] nums = { 14553, 3131 };
          ErrorManager.recordOrThrowError(nums, texts, false, false, false);
       }

       /**
        * Raise or log an error for the specified attribute, as the object is sealed().
        *
        * @param    attr
        *           The attribute being accessed.
        */
       private void sealedError(String attr)
       {
          String[] texts =
          {
             isSealed() ? "Cannot set attributes or properties for a sealed CLIENT-PRINCIPAL"
                      : String.format(
                            "Cannot set attributes or properties in LOGIN-STATE: %s", loginState
                         ),
             "Unable to set attribute " + attr + " in widget  of type CLIENT-PRINCIPAL."
          };
          int[] nums = { isSealed() ? 14548 : 14547, 3131 };
          ErrorManager.recordOrThrowError(nums, texts, false, false, false);
       }

       /**
        * Report SEAL failure.
        *
        * @param reason
        *        fail reason.
        */
       private void sealFailed(String reason)
       {
          ErrorManager.recordOrShowError(new int[] {14540},
                new String[] {
                      "CLIENT-PRINCIPAL:SEAL failed because " + reason
                },
                false, false, false, false, false);

       }

       /** Add SEAL-TIMESTAMP and calculate MAC. */
       private void finalizeSeal(String macPwd)
       {
          attributes.put(AttrInfo.SEAL_TIMESTAMP, Optional.of(datetimetz.now()));
          attributes.put(AttrInfo.MAC, Optional.of(mac(macPwd)));
       }

       /**
        * Try to seal CLIENT-PRINCIPAL if unsealed
        *
        * @param where
        *        subsystem name
        * @param dac
        *        domain access code
        * @param callee
        *        where the method is called from
        *
        * @return <code>true</code> if successful
        */
       private logical trySeal(String where, String dac,  SealCallee callee)
       {
          logical rc = new logical(true);
          if (this.loginState == LoginState.INITIAL)
          {
             // It is unclear which access-code (MAC key) is used in this case
             rc = seal(dac, callee);
             if (this.loginState == LoginState.EXPIRED)
             {
                authenticationFailed(16376, where, "User authentication expired");
                return rc;
             }
             if (this.loginState == LoginState.FAILED)
             {
                return rc;
             }
          }
          return rc;
       }

       /**
        * Calculate Message Authentication Code (MAC) of the CP.
        *
        * @param macPwd
        *        MAC password.
        *
        * @return MAC value.
        */
       private byte[] mac(String macPwd)
       {
          String algorithm =  "HmacMD5";
          if (StringUtils.isEmpty(macPwd))
          {
             macPwd = " ";
          }
          byte[] keyData = macPwd.getBytes(StandardCharsets.UTF_8);
          SecretKeySpec key = new SecretKeySpec(keyData, algorithm);
          Mac mac;
          try
          {
             mac = Mac.getInstance(algorithm);
             mac.init(key);
          }
          catch (NoSuchAlgorithmException | InvalidKeyException e) // should never happen
          {
             throw new RuntimeException(e);
          }

          Consumer<byte[]> consumer =  b -> {
             try
             {
                mac.update(b);
             }
             catch(IllegalStateException e) // should never happen
             {
                throw new RuntimeException(e);
             }
          };

          processPrincipal(ai -> !NOT_FOR_MAC.contains(ai), consumer);
          return mac.doFinal();
       }

       /**
        * Generate SESSION-ID.
        *
        * @return random session ID (22 chars base64 encoded random UUID)
       */
       private static String generateSessionID()
       {
          UUID uuid = UUID.randomUUID();
          byte[] b = ByteBuffer.allocate(16)
                .putLong(uuid.getMostSignificantBits())
                .putLong(uuid.getLeastSignificantBits())
                .array();
          return Base64.getEncoder().encodeToString(b).substring(0, 22);
       }

       /**
        * Split QUALIFIED-USER-ID attribute value and set USER-ID and DOMAIN-NAME.
        *
        * @param quid
        *        QUALIFIED-USER-ID attribute value
        */
       private void setQualifiedIserId(String quid)
       {
          int p = quid.indexOf('@');
          if (p < 0)
          {
             setUserId(quid);
             setDomainName("");
          }
          else
          {
             setUserId(quid.substring(0, p));
             setDomainName(quid.substring(p + 1));
          }
       }

       /**
        * Return value of the CHARACTER attribute.
        *
        * @param ai
        *        attribute descriptor.
        *
        * @return value of the CHARACTER attribute.
        */
       private character getCharAttrValue(AttrInfo ai)
       {
          return hideValues ? new character("") :
                new character((String)attributes.getOrDefault(ai, Optional.of("")).orElse(null));
       }

       /**
        * Set a new value of the optional CHARACTER attribute.
        *
        * @param ai
        *        attribute descriptor.
        * @param value
        *        a new value of the attribute.
        *
        */
       private  void setCharAttrValue(AttrInfo ai, character value)
       {
          setCharAttrValue(ai, value, false);
       }

       /**
        * Set a new value of the optional CHARACTER attribute.
        *
        * @param ai
        *        attribute descriptor.
        * @param value
        *        a new value of the attribute.
        * @param mandatory
        *        boolean flag indicating that the attribute value cannot by null or UNKNOWN.
        */
       private  void setCharAttrValue(AttrInfo ai, character value, boolean mandatory)
       {
          if (mandatory && (value == null || value.isUnknown()))
          {
             attributeUnknownError(ai.aname());
             return;
          }
          if (wasSealed())
          {
             sealedError(ai.aname());
             return;
          }

          if (!mandatory && (value == null || value.isUnknown()))
          {
             Optional<?> avalue = attributes.get(ai);
             if (avalue == null)
             {
                attributes.put(ai, Optional.empty());
             }
             else
             {
                // ignore
             }
          }
          else // add new value to the end of the list
          {
             if (attributes.remove(ai) != null && MANDATORY.contains(ai) )
             {
                this.hideValues = this.exported;
             }
             attributes.put(ai, Optional.of(value.toStringMessage()));
          }
       }

       /**
        * Serialize CP.
        *
        * @return byte array containing serialized data.
        */
       private byte[] serializePrincipal()
       {
          ByteArrayOutputStream bos = new ByteArrayOutputStream();
          DataOutputStream dos = new DataOutputStream(bos);
          Consumer<byte[]> consumer =  b ->
          {
             try
             {
                dos.write(b);
             }
             catch(IOException e) // should never happen
             {
                throw new RuntimeException(e);
             }
          };
          processPrincipal(ai -> true, consumer);
          try
          {
             dos.flush();
             dos.close();
             return bos.toByteArray();
          }
          catch (IOException e) // should never happen
          {
             throw new RuntimeException(e);
          }
       }

       /**
        * Serialize CP attributes (including "hidden") one by one
        * and submit results to a consumer.
        *
        * @param consumer
        *        <code>Consumer</code> instance to process serialized attributes.
        * @param filter
        *        <code>Predicate</code> instance to filter attributes
        */
       private void processPrincipal(
             Predicate<AttrInfo> filter,
             Consumer<byte[]> consumer)
       {
          consumer.accept(new byte[] {0x00, 0x30});
          processAttribute(AttrInfo.LOGIN_STATE, loginState.code, consumer);
          attributes.entrySet().stream().filter(e -> filter.test(e.getKey()))
             .forEach(e -> processAttribute(e.getKey(), e.getValue().orElse(null), consumer));
       }

       /**
        * Serialize CP attributes
        * and submit result to a consumer.
        *
        * @param ai
        *        attribute descriptor.
        * @param value
        *        attribute value.
        * @param dest
        *        <code>Consumer</code> instance to process serialized attributes.
        */
       private void processAttribute(AttrInfo ai, Object value, Consumer<byte[]> dest)
       {
          if (value == null)
          {
             return;
          }
          byte[] data;
          dest.accept(toBytes(ai.type().code()));
          dest.accept(toBytes(ai.id()));
          switch(ai.type())
          {
          case CHAR:
             data = ((String)value).getBytes(StandardCharsets.UTF_8);
             dest.accept(toBytes(data.length + 1));
             if (ai.hideOnExport  &&
                   (isSealed() || this.loginState == LoginState.FAILED ||
                   (sealedOnImport && (magic & 0xff) != 2)))
             {
                Arrays.fill(data, (byte)'*');
                dest.accept(data);
                dest.accept(new byte[] {(byte)'*'});
             }
             else
             {
                dest.accept(data);
                dest.accept(new byte[1]);
             }
             break;
          case DATETIME_TZ:
             datetimetz ts = (datetimetz)value;
             AttrInfo tz = ai == AttrInfo.SEAL_TIMESTAMP ?
                   AttrInfo.SEAL_TIMESTAMP_TZ :
                   AttrInfo.LOGIN_EXPIRATION_TIMESTAMP_TZ;
             dest.accept(toBytes(8));
             if (!ts.isUnknown())
             {
                // Need ms since epoch, but getAbsoluteTimeOffset() returns something different
                // Is there more straightforward way?
                OffsetDateTime offsetDateTime = OffsetDateTime.parse(
                      ts.getIsoDate(),
                      DateTimeFormatter.ISO_OFFSET_DATE_TIME
                );
                long epoch = Date.from(Instant.from(offsetDateTime)).getTime() / 1000;
                dest.accept(toBytes(epoch));
                processAttribute(tz, ts.getTimeZoneOffset().longValue(), dest);
             }
             else
             {
                dest.accept(toBytes(0L));
                processAttribute(tz, 0L, dest);
             }
             break;
          case TYPE_000C:
             dest.accept(toBytes((short)2));
             dest.accept(toBytes((short)value));
             break;
          case BYTES:
             data = (byte[])value;
             dest.accept(toBytes(data.length));
             dest.accept(data);
             break;
          case LONG:
             dest.accept(toBytes(8));
             dest.accept(toBytes((long)value));
             break;
          case LIST:
             if (ai == AttrInfo.PROPS)
             {
                data = serializeProps();
                dest.accept(toBytes(data.length));
                dest.accept(data);
             }
             else
             {
                // TODO: implement
                dest.accept(toBytes(0));
             }
             break;
          }
       }

       /**
        * Serialize CP properties.
        *
        * @return byte array containing serialized data.
        */
       private byte[] serializeProps()
       {
          ByteArrayOutputStream bos = new ByteArrayOutputStream();
          DataOutputStream dos = new DataOutputStream(bos);

          try
          {
             for (Map.Entry<String, String> p: properties.entrySet())
             {
                dos.writeInt(0x000a00014);
                byte[] b1 = p.getKey().getBytes(StandardCharsets.UTF_8);
                byte[] b2 = p.getValue().getBytes(StandardCharsets.UTF_8);
                dos.writeInt(b1.length + b2.length + 2);
                dos.write(b1);
                dos.writeByte(0);
                dos.write(b2);
                dos.writeByte(0);
             }
             dos.flush();
             dos.close();
          }
          catch (IOException e) // should never happen
          {
             throw new RuntimeException(e);
          }
          return bos.toByteArray();
       }

       /**
        * Convert short to byte[].
        *
        * @param v
        *        value to be converted.
        *
        * @return
        *        converted value, MSB first.
        */
       private static byte[] toBytes(short v)
       {
          return ByteBuffer.allocate(2).putShort(v).array();
       }

       /**
        * Convert int to byte[].
        *
        * @param v
        *        value to be converted.
        *
        * @return
        *        converted value, MSB first.
        */
       private static byte[] toBytes(int v)
       {
          return ByteBuffer.allocate(4).putInt(v).array();
       }

       /**
        * Convert long to byte[].
        *
        * @param v
        *        value to be converted.
        *
        * @return
        *        converted value, MSB first.
        */
       private static byte[] toBytes(long v)
       {
          return ByteBuffer.allocate(8).putLong(v).array();
       }

       /**
        * Add standard "hidden" attributes.
        */
       private void addHiddenAttrs()
       {
          attributes.put(AttrInfo.PROPS, Optional.of(properties));
          attributes.put(AttrInfo.ATTR_001C, Optional.of(magic));
          attributes.put(AttrInfo.DB_LIST, Optional.of(new ArrayList<String>()));
       }

       /**
        * Read properties.
        *
        * @param   data
        *          serialized properties data.
        *
        * @throws  java.io.IOException
        *          If any issue is encountered while accessing the input stream.
        */
       private int readProperties(byte[] data)
       throws IOException
       {
          int nprops = 0;
          InputStream bis = new ByteArrayInputStream(data);
          CountingInputStream counter = new CountingInputStream(bis);
          try (DataInputStream dis = new DataInputStream(counter))
          {
             while (counter.getByteCount() < data.length)
             {
                AttrType atype = AttrType.valueOf(dis.readShort());
                AttrInfo ai = AttrInfo.valueOf(atype, dis.readShort());
                if (ai != AttrInfo.PROPERTY)
                {
                   throw new RuntimeException("Invalid property tag: " + ai.aname());
                }
                int len = dis.readInt();
                byte[] pdata = new byte[len];
                dis.read(pdata);
                int p = 0;
                for (; p < len && pdata[p] != 0; p++)
                {
                   // skip
                }
                if (p == 0 || p >= len)
                {
                   throw new RuntimeException("Invalid property data: " + p);
                }
                String key = new String(pdata, 0, p, StandardCharsets.UTF_8);
                String val = new String(pdata, p + 1, len - p - 2, StandardCharsets.UTF_8);
                setProperty(key, val);
                nprops++;
             }
             return nprops;
          }
       }

       /**
        * Read DATETIME-TZ attribute.
        *
        * @param   ai
        *          attribute descriptor.
        * @param   dis
        *          input stream.
        *
        * @throws  java.io.IOException
        *          If any issue is encountered while accessing the input stream.
        */
       private void readTimestamp(AttrInfo ai, DataInputStream dis)
       throws IOException
       {
          long epoch = dis.readLong();

          AttrType tztype = AttrType.valueOf(dis.readShort());
          AttrInfo tzinfo = AttrInfo.valueOf(tztype, dis.readShort());
          if ((ai == AttrInfo.SEAL_TIMESTAMP && tzinfo != AttrInfo.SEAL_TIMESTAMP_TZ) ||
                (ai == AttrInfo.LOGIN_EXPIRATION_TIMESTAMP &&
                tzinfo != AttrInfo.LOGIN_EXPIRATION_TIMESTAMP_TZ))
          {
             throw new RuntimeException("Invalid timezone attribute");
          }
          if (dis.readInt() != 8)
          {
             throw new RuntimeException("Invalid timezone attribute length");
          }
          long offset = dis.readLong();

          datetimetz ts = new datetimetz(new Date(epoch * 1000), (int) offset);

          if (epoch > 0)
          {
             attributes.put(ai, Optional.of(ts));
          }
       }

       /**
        * Check if all mandatory attributes are set.
        *
        * @return <code>true</code> if all mandatory attributes are set.
        */
       private boolean canBeSealed()
       {
          return //!hideValues &&
                   attributes.get(AttrInfo.USER_ID) != null &&
                   attributes.get(AttrInfo.DOMAIN_NAME) != null &&
                   attributes.get(AttrInfo.SESSION_ID) != null;
       }
       /** attributes' types. */
       private static enum AttrType
       {
          CHAR(0x00a0),
          DATETIME_TZ(0x0090),
          // data types with not yet known semantics
          TYPE_000C(0x000C), // used for LOGIN-STATE only
          BYTES(0x000B), // ?
          LIST(0x00c0),  // ?
          LONG(0x00d0);

          /**
           * Get AttrType by code.
           *
           * @param code
           *        attribute code.
           *
           * @return AttrType with such code.
           */
          public static AttrType valueOf(short code)
          {
             return Arrays.stream(values()).filter(at -> at.code == code)
                   .findFirst()
                   .orElseThrow(() -> new IllegalArgumentException("Unknown attr. type: " + code));
          }

          /** type code. */
          private final short code;

          /**
           * Constructor.
           *
           * @param code
           *        type code.
           */
          private AttrType(int code)
          {
             this.code = (short)code;
          }

          /**
           * Return type code.
           *
           * @return type code.
           */
          public short code()
          {
             return code;
          }
       }

       /** CP login states. */
       public static enum LoginState
       {
          INITIAL(0x00),
          LOGIN(0x0b),
          SSO,
          LOGOUT(0x0a),
          EXPIRED(0x05),
          FAILED(0x02),
          NO_LOGIN,
          NO_ACCESS,
          REVOKED,
          DISABLED,
          LOCKED;

          /**
           * Get LoginState by code.
           *
           * @param code
           *        LoginState code.
           *
           * @return LoginState with such code.
           */
          public static LoginState valueOf(short code)
          {
             return Arrays.stream(values()).filter(s -> s.code == code)
                   .findFirst()
                   .orElseThrow(() -> new IllegalArgumentException("Unknown LOGIN-STATE: " + code));
          }

          /** state code. */
          private final short code;

          /**
           * No-arg constructor.
           */
          private LoginState()
          {
             this((short)-1);
          }

          /**
           * Constructor.
           *
           * @param code
           *        state code.
           */
          private LoginState(int code)
          {
             this.code = (short)code;
          }

          /**
           * Return state code.
           *
           * @return state code.
           */
          public short code()
          {
             return code;
          }

          /**
           * Return state name.
           *
           * @return state name.
           */
          public String state()
          {
             return name().replaceAll("_", "-");
          }

       }

       /** attributes' descriptor. */
       private static enum AttrInfo
       {
          LOGIN_STATE(AttrType.TYPE_000C, 0, "LOGIN-STATE"),
          DOMAIN_NAME(AttrType.CHAR, 1, "DOMAIN-NAME"),
          USER_ID(AttrType.CHAR, 2, "USER-ID"),
          CLIENT_TTY(AttrType.CHAR, 3, "CLIENT-TTY"),
          LOGIN_HOST(AttrType.CHAR, 4, "LOGIN-HOST"),
          PRIMARY_PASSPHRASE(AttrType.CHAR, 6, "PRIMARY-PASSPHRASE", true),
          AUDIT_EVENT_CONTEXT(AttrType.CHAR, 0xa, "AUDIT-EVENT-CONTEXT"),
          SEAL_TIMESTAMP(AttrType.DATETIME_TZ, 0xb, "SEAL-TIMESTAMP"),
          SEAL_TIMESTAMP_TZ(AttrType.LONG, 0x17, "SEAL-TIMESTAMP-TZ"),
          ROLES(AttrType.CHAR, 0xd, "ROLES"),
          DOMAIN_TYPE(AttrType.CHAR, 0xe, "DOMAIN_TYPE"),
          SESSION_ID(AttrType.CHAR, 0xf, "SESSION-ID"),
          LOGIN_EXPIRATION_TIMESTAMP(AttrType.DATETIME_TZ, 0x10, "LOGIN-EXPIRATION-TIMESTAMP"),
          LOGIN_EXPIRATION_TIMESTAMP_TZ(AttrType.LONG, 0x1b, "LOGIN-EXPIRATION-TIMESTAMP-TZ"),
          CLIENT_WORKSTATION(AttrType.CHAR, 0x18, "CLIENT-WORKSTATION"),
          DOMAIN_DESCRIPTION(AttrType.CHAR, 0x19, "DOMAIN-DESCRIPTION"),
          // "hidden" attributes with not yet known/undocumented semantics
          PROPS(AttrType.LIST, 0x14, "PROPERTIES"),
          PROPERTY(AttrType.CHAR, 0x14, "PROPERTY"),
          MAC(AttrType.BYTES, 0x15, "MAC"),             // 16 bytes (MAC ?)
          DB_LIST(AttrType.LIST, 0x16, "DB-LIST"),          // (?)
          FAIL_REASON(AttrType.CHAR, 0x1a, "FAIL-REASON"),  // fail reason (?)
          ATTR_001C(AttrType.LONG, 0x1c, "ATTR_001C"); // 8 bytes (initialization method ?)

          /**
           * Get AttrInfo by type and id.
           *
           * @param atype
           *        attribute code.
           * @param id
           *        attribute id.
           * @return AttrInfo with such type and id.
           */
          public static AttrInfo valueOf(AttrType atype, short id)
          {
             return Arrays.stream(values()).filter(ai -> ai.type == atype && ai.id == id)
                   .findFirst()
                   .orElseThrow(() -> new IllegalArgumentException(
                         "Unknown attr. data: " + atype.code() + ":" + id
                    ));
          }

          /** attribute name. */
          private final String aname;
          /** attribute type. */
          private final AttrType type;
          /** attribute id. */
          private final short id;
          /** hide attribute value on export if CP was sealed. */
          private final boolean hideOnExport;

          /**
           * Constructor.
           *
           * @param type
           *        attribute type.
           * @param id
           *        attribute id.
           * @param aname
           *        attribute name.
           */
          private AttrInfo(AttrType type, int id, String aname)
          {
             this(type, id, aname, false);
          }

          /**
           * Constructor.
           *
           * @param type
           *        attribute type.
           * @param id
           *        attribute id.
           * @param aname
           *        attribute name.
           * @param hideOnExport
           *        hide attribute value on export if CP was sealed.
           */
          private AttrInfo(AttrType type, int id, String aname, boolean hideOnExport)
          {
             this.type = type;
             this.id = (short)id;
             this.aname = aname;
             this.hideOnExport = hideOnExport;
          }

          /**
           * Return attribute type.
           *
           * @return attribute type.
           */
          public AttrType type()
          {
             return type;
          }

          /**
           * Return attribute id.
           *
           * @return attribute id.
           */
          public short id()
          {
             return id;
          }

          /**
           * Return attribute name.
           *
           * @return attribute name.
           */
          public String aname()
          {
             return aname;
          }
       }

       /**
        * Replacement for the default object writing method.
        *
        * @param    out
        *           The output destination to which fields will be saved.
        *
        * @throws   IOException
        *           In case of I/O errors.
        */
       @Override
       public void writeExternal(ObjectOutput out) throws IOException
       {
          writeByteArray(out, serializePrincipal());
       }

       /**
        * Replacement for the default object reading method.
        *
        * @param    in
        *           Input source from which fields will be restored.
        *
        * @throws   IOException
        *           In case of I/O errors.
        * @throws   ClassNotFoundException
        *           If payload can't be instantiated.
        */
       @Override
       public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
       {
          importPrincipal(readByteArray(in));
       }
    }