/**
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2005 Sun Microsystems Inc. All Rights Reserved
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * https://opensso.dev.java.net/public/CDDLv1.0.html or
 * opensso/legal/CDDLv1.0.txt
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at opensso/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * $Id: ServiceConfigImpl.java,v 1.15 2008/10/14 04:57:20 arviranga Exp $
 *
 * Portions Copyrighted 2012-2016 ForgeRock AS.
 * Portions Copyrighted 2012 Open Source Solution Technology Corporation
 */

package com.sun.identity.sm;

import com.iplanet.services.util.AMEncryption;
import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.sun.identity.shared.debug.Debug;
import org.forgerock.openam.shared.concurrency.LockFactory;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;

/**
 * The class <code>ServiceConfigImpl</code> provides interfaces to read the
 * configuration information of a service configuration. It provides methods to
 * get configuration parameters for this service configuration.
 */
class ServiceConfigImpl implements ServiceListener {

    // Static variables
    private static ConcurrentMap<String, ServiceConfigImpl> configImpls = new ConcurrentHashMap<>();
    private static Map<String, Set<String>> userPrincipals = Collections.synchronizedMap(
            new HashMap<String, Set<String>>());
    private static final LockFactory<String> LOCK_FACTORY = new LockFactory<>();
    private static Debug debug = SMSEntry.debug;
    private ServiceConfigManagerImpl scm;
    
    // Schema manager to register for schema changes
    ServiceSchemaManagerImpl ssmi;

    private ServiceSchemaImpl ss;

    private boolean globalConfig;

    private String serviceComponentName;

    private String orgName;

    private String groupName;

    private String compName;

    private String configID;

    private int priority;

    private Map<String, Set<String>> attributes;

    private Map<String, Set<String>> attributesWithoutDefaults;

    private CachedSMSEntry smsEntry;

    private CachedSubEntries subEntries;

    private String labeledUri;
    
    private String serviceSchemaManagerListernerID;

    /**
     * Private constructor
     */
    private ServiceConfigImpl(ServiceConfigManagerImpl scm,
            ServiceSchemaImpl ss, CachedSMSEntry entry, String orgName,
            String groupName, String compName, boolean globalConfig,
            SSOToken token) throws SMSException, SSOException {
        this.scm = scm;
        this.ss = ss;
        smsEntry = entry;
        // Add listener for changes to the entry
        smsEntry.addServiceListener(this);
        // Add this object as a listener to schema changes in this service.
        ssmi = scm.getServiceSchemaManagerImpl(token);
        serviceSchemaManagerListernerID = ssmi.addListener(this);
        // Update instance variables
        this.orgName = (orgName == null) ? SMSEntry.baseDN : orgName;
        this.groupName = groupName;
        if (compName == null || compName.equals("")) {
            this.compName = "";
            serviceComponentName = "/";
        } else {
            this.compName = compName.substring(compName.lastIndexOf('/') + 1);
            serviceComponentName = compName;
        }
        this.globalConfig = globalConfig;

        // Read the attributes
        if (smsEntry.isDirty()) {
            smsEntry.refresh();
        } else {
            update();
        }
    }

    /**
     * Returns the component name
     */
    String getComponentName() {
        return (serviceComponentName);
    }

    /**
     * Returns this instance's name
     */
    String getName() {
        return compName;
    }

    /**
     * Returns the configuration's schema ID
     */
    String getSchemaID() {
        return (configID);
    }

    /**
     * Returns the group name
     */
    String getGroupName() {
        return (groupName);
    }

    /**
     * Returns the organization name
     */
    String getOrganizationName() {
        return (orgName);
    }

    /**
     * Returns the priority assigned to the service configuration.
     */
    int getPriority() {
        return (priority);
    }

    /**
     * Returns the labeled uri assigned to the service configuration.
     */
    String getLabeledUri() {
        return (labeledUri);
    }

    void setLabeledUri(String labeledUri) {
        this.labeledUri = labeledUri;
    }

    /**
     * Returns the names of all service's sub-configurations.
     */
    Set<String> getSubConfigNames(SSOToken t) throws SMSException, SSOException {
        if (subEntries == null) {
            subEntries = CachedSubEntries.getInstance(t, smsEntry.getDN());
        }
        return (subEntries.getSubEntries(t));
    }

    /**
     * Returns the names of service's sub-configurations that match the given
     * pattern.
     */
    Set<String> getSubConfigNames(SSOToken t, String pattern) throws SMSException,
            SSOException {
        if (subEntries == null) {
            subEntries = CachedSubEntries.getInstance(t, smsEntry.getDN());
        }
        if (pattern.equals("*")) {
            return getSubConfigNames(t);
        }
        return subEntries.getSubEntries(t, pattern);
    }

    /**
     * Returns the names of service's sub-configurations that match the given
     * pattern.
     */
    Set getSubConfigNames(SSOToken t, String pattern, String serviceidPattern)
            throws SMSException, SSOException {
        if (subEntries == null) {
            subEntries = CachedSubEntries.getInstance(t, smsEntry.getDN());
        }
        return (subEntries.getSchemaSubEntries(t, pattern, serviceidPattern));
    }

    /**
     * Returns the service's sub-configuration given the service's
     * sub-configuration name.
     */
    ServiceConfigImpl getSubConfig(SSOToken token, String subConfigName)
            throws SSOException, SMSException {
        // Construct subconfig DN
        subConfigName = SMSSchema.unescapeName(subConfigName);
        String sdn = "ou=" + subConfigName + "," + smsEntry.getDN();

        // Construct ServiceConfigImpl
        return (ServiceConfigImpl.getInstance(token, scm, null, sdn, orgName,
            groupName, (serviceComponentName + "/" + SMSSchema
            .escapeSpecialCharacters(subConfigName)), globalConfig, ss));
    }

    /**
     * Returns the service configuration parameters. The keys in the
     * <code>Map</code> contains the attribute names and their corresponding
     * values in the <code>Map</code> is a <code>Set</code> that contains
     * the values for the attribute.
     */
    Map<String, Set<String>> getAttributes() {
        if (!SMSEntry.cacheSMSEntries) {
            // Read the entry, since it should not be cached
            smsEntry.refresh();
        }
        return (SMSUtils.copyAttributes(attributes));
    }

    /**
     * Returns the service configuration parameters for read only.
     * The keys in the <code>Map</code> contains the attribute names and
     * their corresponding values in the <code>Map</code> is a
     * <code>Set</code> that contains the values for the attribute.
     */
    Map<String, Set<String>> getAttributesForRead() {
        if (!SMSEntry.cacheSMSEntries) {
            // Read the entry, since it should not be cached
            smsEntry.refresh();
        }
        return (attributes);
    }

    /**
     * Returns the service configuration parameters. The keys in the
     * <code>Map</code> contains the attribute names and their corresponding
     * values in the <code>Map</code> is a <code>Set</code> that contains
     * the values for the attribute. attributes returned by this method are the
     * ones which do not include default values from the service schema
     */
    Map getAttributesWithoutDefaults() {
        if (!SMSEntry.cacheSMSEntries) {
            // Read the entry, since it should not be cached
            smsEntry.refresh();
        }
        return (SMSUtils.copyAttributes(attributesWithoutDefaults));
    }

    /**
     * Returns the service configuration parameters for read only.
     * The keys in the
     * <code>Map</code> contains the attribute names and their
     * corresponding values in the <code>Map</code> is a
     * <code>Set</code> that contains the values for the attribute.
     * attributes returned by this method are the ones which do not
     * include default values from the service schema
     */
    Map<String, Set<String>> getAttributesWithoutDefaultsForRead() {
        if (!SMSEntry.cacheSMSEntries) {
            // Read the entry, since it should not be cached
            smsEntry.refresh();
        }
        return (attributesWithoutDefaults);
    }

    /**
     * Returns the DN associated with this entry
     */
    String getDN() {
        return (smsEntry.getDN());
    }

    /**
     * Returns the SMSEntry associated with this object
     */
    SMSEntry getSMSEntry() {
        return (smsEntry.getClonedSMSEntry());
    }

    /**
     * Updates the SMSEntry with the new changes
     */
    void refresh(SMSEntry e) throws SMSException {
        smsEntry.refresh(e);
    }

    /**
     * Returns the ServiceSchemaImpl assicated with this object
     */
    ServiceSchemaImpl getServiceSchemaImpl() {
        return (ss);
    }

    /**
     * Checks if the entry exists in the directory
     */
    boolean isNewEntry() {
        return (smsEntry.isNewEntry());
    }
    
    /**
     * Checks if the entry is still valid
     */
    boolean isValid() {
        // check if ServiceSchemaImpl is stale
        if (!ss.isValid()) {
            return false;
        }

        if (smsEntry.isValid() && smsEntry.isDirty()) {
            smsEntry.refresh();
        }
        return (smsEntry.isValid());
    }
    
    /**
     * Clears the instance variables and deregisters notifications
     */
    void clear() {
        // Deregister from CachedSMSEntry
        smsEntry.removeServiceListener(this);
        // Invalidate CachedSMSEntry
        if (smsEntry.isValid()) {
            smsEntry.clear();
        }
        // Deregister from ServiceSchemaManagerImpl
        try {
            if ((ssmi != null) && (serviceSchemaManagerListernerID != null)) {
                ssmi.removeListener(serviceSchemaManagerListernerID);
                ssmi = null;
            }
        } catch (Exception ex) {
            debug.error("ServiceConfigImpl.clear Unable to remove " +
                "notification handler for dn: " + getDN(), ex);
        }
    }

    // @Override
    public int hashCode() {
        int hash = 5;
        hash = 41 * hash + (scm != null ? scm.hashCode() : 0);
        hash = 41 * hash + (globalConfig ? 1 : 0);
        hash = 41 * hash + (orgName != null ? orgName.hashCode() : 0);
        hash = 41 * hash + (groupName != null ? groupName.hashCode() : 0);
        hash = 41 * hash + (compName != null ? compName.hashCode() : 0);
        hash = 41 * hash + (configID != null ? configID.hashCode() : 0);
        return hash;
    }
    
    public boolean equals(Object scio) {
        if (scio instanceof ServiceConfigImpl) {
            ServiceConfigImpl s = (ServiceConfigImpl) scio;
            if ((s.globalConfig == globalConfig) &&
                s.orgName.equalsIgnoreCase(orgName) &&
                s.groupName.equalsIgnoreCase(groupName) &&
                s.compName.equalsIgnoreCase(compName) &&
                s.configID.equalsIgnoreCase(configID) &&
                s.scm.getName().equalsIgnoreCase(scm.getName())) {
                return true;
            }
        }
        return (false);
    }

    // Method gets called by local changes and also by changes from
    // notification thread. Hence it synchornized to avoid data being
    // corrupted
    synchronized void update() {
        // Check if entry is still valid
        if (!smsEntry.isValid()) {
            // Entry has to be removed from cache and cleared
            clear();
            return;
        }
        // Get the SMSEntry
        SMSEntry entry = smsEntry.getSMSEntry();

        // Read the attributes
        Map<String, Set<String>> origAttributes = SMSUtils.getAttrsFromEntry(entry);
        Map<String, Set<String>> origAttributesWithoutDefaults = SMSUtils.getAttrsFromEntry(entry);
        // Add default values, if attribute not present
        // and decrypt password attributes
        String validate = ss.getValidate();
        if ((validate == null) || validate.equalsIgnoreCase("yes")) {
            Set<String> asNames = ss.getAttributeSchemaNames();
            // Remove attributes that do not exist in the schema.
            Set<String> oldSet = origAttributes.keySet();
            Set<String> removeAttrs = new HashSet<>();
            Iterator it = oldSet.iterator();
            while (it.hasNext()) {
                String tName = (String) it.next();
                if (!asNames.contains(tName)) {
                    // attributes.remove(tName);
                    // attributesWithoutDefaults.remove(tName);
                    removeAttrs.add(tName);
                }
            }
            it = removeAttrs.iterator();
            while (it.hasNext()) {
                String t = (String) it.next();
                origAttributes.remove(t);
                origAttributesWithoutDefaults.remove(t);
            }
            for (String asName : asNames) {
                AttributeValidator av = ss.getAttributeValidator(asName);
                origAttributes = av.inheritDefaults(origAttributes);
                origAttributesWithoutDefaults = av.decodeEncodedAttrs(origAttributesWithoutDefaults);
            }
        } // if (validate....)

        // Read the priority
        priority = 0;
        String priorities[] = entry.getAttributeValues(SMSEntry.ATTR_PRIORITY);
        if (priorities != null) {
            try {
                priority = Integer.parseInt(priorities[0]);
            } catch (NumberFormatException nfe) {
                SMSEntry.debug.error("ServiceConfig::getPriority() " + nfe);
            }
        }

        // Read the service ID
        String[] ids = entry.getAttributeValues(SMSEntry.ATTR_SERVICE_ID);
        if (ids != null) {
            configID = ids[0];
        } else {
            configID = compName;
        }

        // Read the labeled uri
        String[] lUri = entry.getAttributeValues(SMSEntry.ATTR_LABELED_URI);
        if (lUri != null) {
            labeledUri = lUri[0];
        }

        // Replace the class instance attribute Maps
        attributes = Collections.unmodifiableMap(origAttributes);
        attributesWithoutDefaults = Collections.unmodifiableMap(
            origAttributesWithoutDefaults);
    }

    // ------------------------------------------------------------------
    // Static Protected method to get an instance of ServiceConfigImpl
    // ------------------------------------------------------------------
    // Method called by ServiceConfigManagerImpl to Global or Organization
    // service configuration
    static ServiceConfigImpl getInstance(SSOToken token,
            ServiceConfigManagerImpl scm, ServiceSchemaImpl ss, String dn,
            String oName, String groupName, String compName,
            boolean globalConfig) throws SSOException, SMSException {
        return getInstance(token, scm, ss, dn, oName, groupName, compName,
                globalConfig, null);
    }

    // Method called by ServiceConfig to get sub-service configuration
    static ServiceConfigImpl getInstance(SSOToken token,
        ServiceConfigManagerImpl scm, ServiceSchemaImpl ss, String dn,
        String oName, String groupName, String compName,
        boolean globalConfig, ServiceSchemaImpl parentSS)
        throws SSOException, SMSException {
        if (debug.messageEnabled()) {
            debug.message("ServiceConfigImpl::getInstance: called: " + dn);
        }

        // Get required parameters
        String orgName = DNMapper.orgNameToDN(oName);
        String cacheName = getCacheName(scm.getName(), scm.getVersion(),
            orgName, groupName, compName, globalConfig);

        // Check cache for the object, if present return
        // If present, Cache checks if the object is valid and if dirty
        // refreshes the object
        ServiceConfigImpl answer = getFromCache(cacheName, token);
        if (answer != null ) {
            // Check if the entry has to be updated
            if (!SMSEntry.cacheSMSEntries) {
                // Read the entry, since it should not be cached
                answer.smsEntry.refresh();
            }
            return (answer);
        }

        // Since entry not in cache, first check if the orgName exists
        if (!SMSEntry.checkIfEntryExists(orgName, token)) {
            debug.error("ServiceConfigImpl::getInstance called with " +
                    "non existant organization name: " + orgName);
            // To maintain backward compatibilty return null if orgName
            // is not found. Throwing exception is commented out.
            // Object [] args = { orgName };
            // throw (new SMSException(IUMSConstants.UMS_BUNDLE_NAME,
            // "sms-invalid-org-name", args));
            return (null);
        }

        // Not in cache and org exists, construct SMSEntry object
        CachedSMSEntry entry = checkAndUpdatePermission(cacheName, dn, token);
        if (!entry.isValid()) {
            entry = checkAndUpdatePermission(cacheName, dn, token);
        }
        
        // Since entry not in cache, check if service schema exists
        if (ss == null) {
            // Need to get the sub-schema name
            String subConfigId = null;
            SMSEntry sentry = entry.getSMSEntry();
            String[] ids = sentry.getAttributeValues(
                SMSEntry.ATTR_SERVICE_ID);
            if (ids != null) {
                subConfigId = ids[0];
            } else {
                // Get configId from sub config name
                int index = compName.lastIndexOf('/');
                subConfigId = compName.substring(index + 1);
            }
            // Get the schema from the parent
            if (parentSS != null) {
                ss = parentSS.getSubSchema(subConfigId);
            }
            // Return null if schema is not defined
            if (ss == null) {
                // SMSEntry shouldn't exist in this case, but if it does, it is most 
                // likely to be incorrect, hence we remove it from the cache.
                entry.clear();
                configImpls.remove(cacheName);

                if (debug.messageEnabled()) {
                    debug.message("ServiceConfigImpl::getInstance: couldn't find subschema : " + dn);
                }
                return (null);
            }
        }

        answer = getFromCache(cacheName, null);
        if (answer == null) {
            final Lock lock = LOCK_FACTORY.acquireLock(cacheName);
            try {
                lock.lock();
                answer = getFromCache(cacheName, null);
                if (answer == null) {
                    answer = new ServiceConfigImpl(scm, ss, entry, orgName, groupName, compName, globalConfig, token);
                    configImpls.put(cacheName, answer);
                }
            } finally {
                lock.unlock();
            }
        }

        if (debug.messageEnabled()) {
            debug.message("ServiceConfigImpl::getInstance: return: " + dn);
        }
        return answer;
    }

    // Method called by ServiceConfig to delete sub-service configuration
    // key from the cache when removeSubConfig is called.
    static void deleteInstance(SSOToken token,
        ServiceConfigManagerImpl scm, ServiceSchemaImpl ss, String dn,
        String oName, String groupName, String compName,
        boolean globalConfig, ServiceSchemaImpl parentSS)
        throws SSOException, SMSException {

        if (debug.messageEnabled()) {
            debug.message("ServiceConfigImpl::deleteInstance: called: dn: " 
                + dn + " Org Name: " + oName + " Group Name: " + groupName
                + " Component Name: "+ compName);
        }

        // Get required parameters
        String orgName = DNMapper.orgNameToDN(oName);
        String cacheName = getCacheName(scm.getName(), scm.getVersion(),
            orgName, groupName, compName, globalConfig);

        if (debug.messageEnabled()) {
            debug.message("ServiceConfigImpl::deleteInstance: cacheName: " + 
                    cacheName);
        }
        // Delete from cache if it exists.
        configImpls.remove(cacheName);
        
        if (debug.messageEnabled()) {
            debug.message("ServiceConfigImpl::deleteInstance: deleted: " + dn);
        }
    }

    // This function is executed after obtaining "configMutex" lock
    static ServiceConfigImpl getFromCache(String cn, SSOToken t)
        throws SMSException, SSOException {
        ServiceConfigImpl answer = configImpls.get(cn);
        if (answer != null) {
            if (!answer.isValid()) {
                configImpls.remove(cn, answer);
                answer.clear();
                answer = null;
            } else if (answer.smsEntry.isNewEntry()) {
                answer = null;
            }
        }
        if ((answer != null) && (t != null)) {
            // Check if the user has permissions
            Set<String> principals = userPrincipals.get(cn);
            if ((principals == null) ||
                !principals.contains(t.getTokenID().toString())) {
                // Principal name not in cache, need to check perm
                checkAndUpdatePermission(cn, answer.getDN(), t);
            }
        }
        return (answer);        
    }

    static CachedSMSEntry checkAndUpdatePermission(
        String cacheName, String dn, SSOToken t)
        throws SMSException, SSOException {
        CachedSMSEntry answer = CachedSMSEntry.getInstance(t, dn);
        Set<String> sudoPrincipals = userPrincipals.get(cacheName);
        if (sudoPrincipals == null) {
            sudoPrincipals = Collections.synchronizedSet(new HashSet(2));
            userPrincipals.put(cacheName, sudoPrincipals);
        }
        sudoPrincipals.add(t.getTokenID().toString());
        return (answer);
    }

    // Clears the cache
    static void clearCache() {
        Map<String, ServiceConfigImpl> cache = new HashMap<>(configImpls);
        for (Map.Entry<String, ServiceConfigImpl> entry : cache.entrySet()) {
            final String cacheName = entry.getKey();
            final ServiceConfigImpl serviceConfig = entry.getValue();
            serviceConfig.clear();
            configImpls.remove(cacheName, serviceConfig);
        }
        userPrincipals.clear();
    }

    static String getCacheName(String sName, String version, String oName,
            String gName, String cName, boolean global) {
        StringBuilder sb = new StringBuilder(100);
        sb.append(sName).append(version).append(oName).append(gName).append(
                cName).append(global);
        return (sb.toString().toLowerCase());
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.sun.identity.sm.ServiceListener#globalConfigChanged(
     *      java.lang.String,
     *      java.lang.String, java.lang.String, java.lang.String, int)
     */
    public void globalConfigChanged(String serviceName, String version,
            String groupName, String serviceComponent, int type) {
        // do nothing, the update method for this is called by the internal
        // notification mechanism in CachedSMSEntry
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.sun.identity.sm.ServiceListener#organizationConfigChanged(
     *      java.lang.String,
     *      java.lang.String, java.lang.String, java.lang.String,
     *      java.lang.String, int)
     */
    public void organizationConfigChanged(String serviceName, String version,
        String orgName, String groupName, String serviceComponent, int type) {
        // do nothing. Same as "globalConfigChanged"
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.sun.identity.sm.ServiceListener#schemaChanged(java.lang.String,
     *      java.lang.String)
     */
    public void schemaChanged(String serviceName, String version) {
        if (serviceName.equalsIgnoreCase(scm.getName())) {
            update();
        }
    }
    
    public String toXML(
        SSOToken token, 
        String nodeName, 
        AMEncryption encryptObj
    ) throws SMSException, SSOException {
        return toXML(token, nodeName, encryptObj, null);
    }
    
    public String toXML(
        SSOToken token, 
        String nodeName,
        AMEncryption encryptObj,
        String organizationName
    ) throws SMSException, SSOException {
        Set serviceConfigNames = getSubConfigNames(token);
        Map orgAttributes = null;

        if (nodeName.equals(SMSUtils.ORG_CONFIG)) {
            OrganizationConfigManager ocm = new OrganizationConfigManager(
                token, organizationName);
            orgAttributes = ocm.getAttributes(scm.getName());
        }

        if (((attributesWithoutDefaults == null) ||
            attributesWithoutDefaults.isEmpty()) &&
            ((serviceConfigNames == null) ||
                serviceConfigNames.isEmpty()) &&
            ((orgAttributes == null) || orgAttributes.isEmpty()) &&
            ((nodeName == null) || !nodeName.equals(SMSUtils.SUB_CONFIG))
        ) {
            return "";
        }

        StringBuilder buff = new StringBuilder();
        buff.append("<").append(nodeName);
        
        if (nodeName.equals(SMSUtils.GLOBAL_CONFIG) ||
            nodeName.equals(SMSUtils.ORG_CONFIG) ||
            nodeName.equals(SMSUtils.INSTANCE)
        ) {
            if ((groupName != null) && (groupName.length() > 0)) {
                buff.append(" ").append(SMSUtils.GROUP).append("=\"")
                    .append(groupName).append("\"");
            }
        }
        
        if (nodeName.equals(SMSUtils.ORG_CONFIG) ||
            nodeName.equals(SMSUtils.INSTANCE)
        ) {
            if ((organizationName != null) && 
                (organizationName.length() > 0)
            ) {
                String oName = (organizationName.indexOf('/') == -1) ?
                    DNMapper.orgNameToRealmName(organizationName) :
                    organizationName;
                buff.append(" ").append(SMSUtils.NAME).append("=\"")
                    .append(oName).append("\"");
            }
        }
        
        if (nodeName.equals(SMSUtils.SUB_CONFIG)) {
            if ((compName != null) && (compName.length() > 0)) {
                buff.append(" ").append(SMSUtils.NAME).append("=\"")
                    .append(compName).append("\"");
            }
        
            if ((configID != null) && (configID.length() > 0)) {
                buff.append(" ").append(SMSUtils.SERVICE_ID).append("=\"")
                    .append(configID).append("\"");
            }
            if (priority > 0) {
                buff.append(" ").append(SMSUtils.PRIORITY).append("=\"")
                    .append(Integer.toString(priority)).append("\"");
            }
        }
        
        buff.append(">");
        
        Map deepCopied = SMSUtils.copyAttributes(attributesWithoutDefaults);
        Set asNames = ss.getAttributeSchemaNames();
        for (Iterator i = asNames.iterator(); i.hasNext(); ) {
             AttributeValidator av = ss.getAttributeValidator((String)i.next());
             av.encodedAttrs(deepCopied, encryptObj);
        }
        
        buff.append(SMSUtils.toAttributeValuePairXML(deepCopied));
        
        for (Iterator i = serviceConfigNames.iterator(); i.hasNext(); ) {
            String scName = (String)i.next();
            ServiceConfigImpl sci = this.getSubConfig(token, scName);
            if (null != sci) {
                buff.append(sci.toXML(token, SMSUtils.SUB_CONFIG, encryptObj));
            }
        }
        
        if ((orgAttributes != null) && !orgAttributes.isEmpty()) {
            buff.append("<").append(SMSUtils.ORG_ATTRIBUTE_VALUE_PAIR)
                .append(">\n");
            buff.append(SMSUtils.toAttributeValuePairXML(orgAttributes));
            buff.append("</").append(SMSUtils.ORG_ATTRIBUTE_VALUE_PAIR)
                .append(">\n");
        }

        buff.append("</").append(nodeName).append(">");
        return buff.toString();
    }
}
