/*
 *   Licensed to the Apache Software Foundation (ASF) under one
 *   or more contributor license agreements.  See the NOTICE file
 *   distributed with this work for additional information
 *   regarding copyright ownership.  The ASF licenses this file
 *   to you under the Apache License, Version 2.0 (the
 *   "License"); you may not use this file except in compliance
 *   with the License.  You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing,
 *   software distributed under the License is distributed on an
 *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *   KIND, either express or implied.  See the License for the
 *   specific language governing permissions and limitations
 *   under the License.
 *
 */
package org.apache.directory.fortress.core.impl;


import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.apache.commons.lang3.StringUtils;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.entry.*;
import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapNoSuchObjectException;
import org.apache.directory.fortress.core.CreateException;
import org.apache.directory.fortress.core.FinderException;
import org.apache.directory.fortress.core.GlobalErrIds;
import org.apache.directory.fortress.core.GlobalIds;
import org.apache.directory.fortress.core.RemoveException;
import org.apache.directory.fortress.core.UpdateException;
import org.apache.directory.fortress.core.ldap.LdapDataProvider;
import org.apache.directory.fortress.core.model.Configuration;
import org.apache.directory.fortress.core.util.PropUtil;
import org.apache.directory.fortress.core.util.Config;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * This class provides data access for the standard ldap object device that has been extended to support name/value pairs.
 * Fortress uses this data structure to store its remote cfg parameters.
 * <p>
 * The Fortress Config node is a combination of:
 * <p>
 * 'device' Structural Object Class is used to store basic attributes like cn which will be used for config node name.
 * <ul>
 * <li>  ------------------------------------------
 * <li> <code>objectclass ( 2.5.6.14 NAME 'device'</code>
 * <li> <code>DESC 'RFC2256: a device''</code>
 * <li> <code>SUP top STRUCTURAL</code>
 * <li> <code>MUST cn</code>
 * <li> <code>MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $ description ) )</code>
 * <li>  ------------------------------------------
 * </ul>
 * <p>
 * 'ftProperties' AUXILIARY Object Class is used to store name/value pairs on target node.<br>
 * <code>This aux object class can be used to store custom attributes.</code><br>
 * <code>The properties collections consist of name/value pairs and are not constrainted by Fortress.</code><br>
 * <ul>
 * <li>  ------------------------------------------
 * <li> <code>objectclass ( 1.3.6.1.4.1.38088.3.2</code>
 * <li> <code>NAME 'ftProperties'</code>
 * <li> <code>DESC 'Fortress Properties AUX Object Class'</code>
 * <li> <code>AUXILIARY</code>
 * <li> <code>MAY ( ftProps ) ) </code>
 * <li>  ------------------------------------------
 * </ul>
 * <p>
 * This class is thread safe.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
final class ConfigDAO extends LdapDataProvider
{
    private static final String CLS_NM = ConfigDAO.class.getName();
    private static final Logger LOG = LoggerFactory.getLogger( CLS_NM );
    private String CONFIG_ROOT_DN;
    public static final String GID_NUMBER_SEQUENCE = "ftGidNumber";
    public static final String UID_NUMBER_SEQUENCE = "ftUidNumber";

    private final String[] CONFIG_OBJ_CLASS =
    {
        SchemaConstants.DEVICE_OC, GlobalIds.PROPS_AUX_OBJECT_CLASS_NAME, GlobalIds.FT_CONFIG_AUX_OBJECT_CLASS_NAME
    };
    private final String[] POSIX_IDS =
    {
        GID_NUMBER_SEQUENCE, UID_NUMBER_SEQUENCE
    };
    private final String[] CONFIG_ATRS =
    {
        SchemaConstants.CN_AT, GlobalIds.PROPS, GID_NUMBER_SEQUENCE, UID_NUMBER_SEQUENCE
    };

    /**
     * Package private default constructor.
     */
    ConfigDAO()
    {
        super();
        CONFIG_ROOT_DN = Config.getInstance().getProperty( GlobalIds.CONFIG_ROOT_PARAM );
    }

    /**
     * Create a new configuration node and load it with properties.
     * @param cfg the new configuration node to be created in ldap.
     * @return what was just added.
     * @throws org.apache.directory.fortress.core.CreateException
     */
    Configuration create( Configuration cfg )
        throws CreateException
    {
        LdapConnection ld = null;
        String dn = getDn( cfg.getName() );
        LOG.info( "create dn [{}]", dn );
        try
        {
            Entry myEntry = new DefaultEntry( dn );
            myEntry.add( SchemaConstants.OBJECT_CLASS_AT, CONFIG_OBJ_CLASS );
            myEntry.add( SchemaConstants.CN_AT, cfg.getName() );
            loadProperties( cfg.getProperties(), myEntry, GlobalIds.PROPS );
            // These attributes hold sequence numbers:
            if (StringUtils.isNotEmpty(cfg.getUidNumber()))
            {
                myEntry.add( UID_NUMBER_SEQUENCE, cfg.getUidNumber() );
            }
            else
            {
                myEntry.add( UID_NUMBER_SEQUENCE, "0" );
            }
            if (StringUtils.isNotEmpty(cfg.getGidNumber()))
            {
                myEntry.add( GID_NUMBER_SEQUENCE, cfg.getGidNumber() );
            }
            else
            {
                myEntry.add( GID_NUMBER_SEQUENCE, "0" );
            }
            ld = getAdminConnection();
            add( ld, myEntry, cfg );
        }
        catch ( LdapEntryAlreadyExistsException e )
        {
            String warning = "create config dn [" + dn + "] caught LdapEntryAlreadyExistsException="
                + e;
            throw new CreateException( GlobalErrIds.FT_CONFIG_ALREADY_EXISTS, warning, e );
        }
        catch ( LdapException e )
        {
            String error;
            error = "create config dn [" + dn + "] caught LDAPException=" + e;
            LOG.error( error, e );
            throw new CreateException( GlobalErrIds.FT_CONFIG_CREATE_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
        return cfg;
    }


    /**
     * Update existing node with new properties.
     * @param cfg contains the name and each property name value will loaded into an attribute (ftprops) under the config node.
     * @return the updated entity.
     * @throws org.apache.directory.fortress.core.UpdateException
     */
    Configuration update( Configuration cfg )
        throws UpdateException
    {
        LdapConnection ld = null;
        String dn = getDn( cfg.getName() );
        LOG.debug( "update dn [{}]", dn );
        try
        {
            List<Modification> mods = new ArrayList<>();
            if ( PropUtil.isNotEmpty( cfg.getProperties() ) )
            {
                loadProperties( cfg.getProperties(), mods, GlobalIds.PROPS, false );
            }
            if (StringUtils.isNotEmpty(cfg.getUidNumber()))
            {
                mods.add(new DefaultModification(ModificationOperation.REPLACE_ATTRIBUTE, UID_NUMBER_SEQUENCE,
                        cfg.getUidNumber()));
            }
            if (StringUtils.isNotEmpty(cfg.getGidNumber()))
            {
                mods.add(new DefaultModification(ModificationOperation.REPLACE_ATTRIBUTE, GID_NUMBER_SEQUENCE,
                        cfg.getGidNumber()));
            }
            if ( mods.size() > 0 )
            {
                ld = getAdminConnection();
                modify( ld, dn, mods, cfg );
            }
        }
        catch ( LdapException e )
        {
            String error = "update dn [" + dn + "] caught LDAPException=" + e;
            throw new UpdateException( GlobalErrIds.FT_CONFIG_UPDATE_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
        return cfg;
    }


    /**
     * This method will update a single property with a new value.
     *
     * @param name of the config node, mostly likely 'DEFAULT'.
     * @param key used for the property.
     * @param value this is old value to be replaced with newValue.
     * @param newValue new value for the property
     * @throws UpdateException in the event the attribute can't be replaced.
     * @throws FinderException in the event the config node and/or property key:value can't be located.
     */
    void updateProperty( String name, String key, String value, String newValue ) throws UpdateException
    {
        LdapConnection ld = null;
        String dn = getDn( name );
        LOG.debug( "update dn [{}], key [{}], value [{}], newValue [{}]", dn, key, value, newValue );
        try
        {
            List<Modification> mods = new ArrayList<Modification>();
            mods.add( new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE, GlobalIds.PROPS, key + GlobalIds.PROP_SEP + value ) );
            mods.add( new DefaultModification( ModificationOperation.ADD_ATTRIBUTE, GlobalIds.PROPS, key + GlobalIds.PROP_SEP + newValue ) );
            ld = getAdminConnection();
            modify( ld, dn, mods );
        }
        catch ( LdapException e )
        {
            String error = "updateProperty dn [" + dn + "] caught LDAPException=" + e;
            throw new UpdateException( GlobalErrIds.FT_CONFIG_UPDATE_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
    }


    /**
     * Remove the configuration node from the directory.
     * @param name of the node to be removed.
     * @throws org.apache.directory.fortress.core.RemoveException
     */
    void remove( String name )
        throws RemoveException
    {
        LdapConnection ld = null;
        String dn = getDn( name );
        LOG.info( "remove dn [{}]", dn );
        try
        {
            ld = getAdminConnection();
            delete( ld, dn );
        }
        catch ( LdapException e )
        {
            String error = "remove dn [" + dn + "] LDAPException=" + e;
            throw new RemoveException( GlobalErrIds.FT_CONFIG_DELETE_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
    }


    /**
     * A set of properties from a configuration node.
     * @param name the name of config node.
     * @param props each name/value will be a ftprop attribute to remove.
     * @return
     * @throws org.apache.directory.fortress.core.UpdateException
     */
    Properties remove( String name, Properties props )
        throws UpdateException
    {
        LdapConnection ld = null;
        String dn = getDn( name );
        LOG.info( "remove props dn [{}]", dn );
        try
        {
            List<Modification> mods = new ArrayList<>();
            if ( PropUtil.isNotEmpty( props ) )
            {
                removeProperties( props, mods, GlobalIds.PROPS );
            }
            if ( mods.size() > 0 )
            {
                ld = getAdminConnection();
                modify( ld, dn, mods );
            }
        }
        catch ( LdapException e )
        {
            String error = "remove props dn [" + dn + "] caught LDAPException=" + e;
            throw new UpdateException( GlobalErrIds.FT_CONFIG_DELETE_PROPS_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
        return props;
    }


    /**
     * @param name
     * @return the existing config node.
     * @throws org.apache.directory.fortress.core.FinderException
     */
    Configuration getConfig( String name )
        throws FinderException
    {
        Configuration configuration = new Configuration();
        LdapConnection ld = null;
        String dn = getDn( name );
        LOG.info( "getConfig dn [{}]", dn );
        try
        {
            ld = getAdminConnection();
            Entry findEntry = read( ld, dn, CONFIG_ATRS );
            configuration.setName( name );
            configuration.addProperties( PropUtil.getProperties( getAttributes( findEntry, GlobalIds.PROPS ) ) );
            configuration.setUidNumber( getAttribute( findEntry, UID_NUMBER_SEQUENCE ) );
            configuration.setGidNumber( getAttribute( findEntry, GID_NUMBER_SEQUENCE ) );
        }
        catch ( LdapNoSuchObjectException e )
        {
            String warning = "getConfig COULD NOT FIND ENTRY for dn [" + dn + "]";
            throw new FinderException( GlobalErrIds.FT_CONFIG_NOT_FOUND, warning, e );
        }
        catch ( LdapException e )
        {
            String error = "getConfig dn [" + dn + "] caught LdapException=" + e;
            throw new FinderException( GlobalErrIds.FT_CONFIG_READ_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
        return configuration;
    }

    /**
     *
     * @param name
     * @return
     * @throws FinderException
     */
    Configuration getPosixIds( String name )
        throws FinderException
    {
        Configuration configuration = new Configuration();
        LdapConnection ld = null;
        String dn = getDn( name );
        LOG.debug( "getPosixIds dn [{}]", dn );
        try
        {
            ld = getAdminConnection();
            Entry findEntry = read( ld, dn, POSIX_IDS );
            configuration.setName( name );
            configuration.setUidNumber( getAttribute( findEntry, UID_NUMBER_SEQUENCE ) );
            configuration.setGidNumber( getAttribute( findEntry, GID_NUMBER_SEQUENCE ) );
        }
        catch ( LdapNoSuchObjectException e )
        {
            String warning = "getPosixIds COULD NOT FIND ENTRY for dn [" + dn + "]";
            throw new FinderException( GlobalErrIds.FT_CONFIG_NOT_FOUND, warning, e );
        }
        catch ( LdapException e )
        {
            String error = "getPosixIds dn [" + dn + "] caught LdapException=" + e;
            throw new FinderException( GlobalErrIds.FT_CONFIG_READ_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
        return configuration;
    }


    /**
     *
     * @param name
     * @return
     */
    private String getDn( String name )
    {
        return SchemaConstants.CN_AT + "=" + name + "," + Config.getInstance().getProperty( GlobalIds.CONFIG_ROOT_PARAM );
    }
}
