/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.filesys;

import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.Locale;
import java.util.StringTokenizer;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.filesys.alfresco.AlfrescoClientInfoFactory;
import org.alfresco.filesys.alfresco.ExtendedDiskInterface;
import org.alfresco.jlan.debug.DebugConfigSection;
import org.alfresco.jlan.ftp.FTPConfigSection;
import org.alfresco.jlan.netbios.NetBIOSName;
import org.alfresco.jlan.netbios.NetBIOSNameList;
import org.alfresco.jlan.netbios.NetBIOSSession;
import org.alfresco.jlan.netbios.win32.Win32NetBIOS;
import org.alfresco.jlan.oncrpc.nfs.NFSConfigSection;
import org.alfresco.jlan.server.auth.ClientInfo;
import org.alfresco.jlan.server.config.GlobalConfigSection;
import org.alfresco.jlan.server.config.InvalidConfigurationException;
import org.alfresco.jlan.server.config.ServerConfiguration;
import org.alfresco.jlan.util.IPAddress;
import org.alfresco.jlan.util.Platform;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.extensions.config.element.GenericConfigElement;

/**
 * Alfresco File Server Configuration Bean Class
 * 
 * @author gkspencer
 */
public abstract class AbstractServerConfigurationBean extends ServerConfiguration implements
        ExtendedServerConfigurationAccessor, ApplicationListener, ApplicationContextAware
{

  // Debug logging

  protected static final Log logger = LogFactory.getLog("org.alfresco.fileserver");

  // IP address representing null
  
  public static final String BIND_TO_IGNORE = "0.0.0.0";

  // FTP server debug type strings

  protected static final String m_ftpDebugStr[] = { "STATE", "RXDATA", "TXDATA", "DUMPDATA", "SEARCH", "INFO", "FILE", "FILEIO", "ERROR", "PKTTYPE",
      "TIMING", "DATAPORT", "DIRECTORY", "SSL" };

  // Default FTP server port
  
  protected static final int DefaultFTPServerPort = 21;

  // Default FTP server session timeout
  protected static final int DefaultFTPSrvSessionTimeout = 5000;

  // Default FTP anonymous account name
  
  protected static final String DefaultFTPAnonymousAccount = "anonymous";
  
  //  NFS server debug type strings
  
  protected static final String m_nfsDebugStr[] = { "RXDATA", "TXDATA", "DUMPDATA", "SEARCH", "INFO", "FILE",
    "FILEIO", "ERROR", "TIMING", "DIRECTORY", "SESSION" };
  
  // Default thread pool size
	
  protected static final int DefaultThreadPoolInit	= 25;
  protected static final int DefaultThreadPoolMax		= 50;
	
  // Default memory pool settings
	
  protected static final int[] DefaultMemoryPoolBufSizes  = { 256, 4096, 16384, 66000 };
  protected static final int[] DefaultMemoryPoolInitAlloc = {  20,   20,     5,     5 };
  protected static final int[] DefaultMemoryPoolMaxAlloc  = { 100,   50,    50,    50 };
	
		
  // Memory pool allocation limits
	
  protected static final int MemoryPoolMinimumAllocation	= 5;
  protected static final int MemoryPoolMaximumAllocation   = 500;
	
  // Maximum session timeout
  
  public static final int MaxSessionTimeout    = 60 * 60;  // 1 hour
    
  // Disk interface to use for shared filesystems
  
  private ExtendedDiskInterface m_repoDiskInterface;
  
  // Runtime platform type
  
  private Platform.Type m_platform = Platform.Type.Unchecked;

  // flag to indicate successful initialization
  
  private boolean m_initialised;

  // Main authentication service, public API
  
  private AuthenticationService m_authenticationService;

  // Authentication component, for internal functions
  
  protected AuthenticationComponent m_authenticationComponent;
  
  // Various services
  
  private NodeService m_nodeService;
  private PersonService m_personService;
  private TransactionService m_transactionService;
  protected TenantService m_tenantService;
  private SearchService m_searchService;
  private NamespaceService m_namespaceService;
  private AuthorityService m_authorityService;
  
  // Local server name and domain/workgroup name

  private String m_localName;
  private String m_localNameFull;
  private String m_localDomain;
  
  // Disable use of native code on Windows, do not use any JNI calls
  
  protected boolean m_disableNativeCode = false;
  
  /**
   * Default constructor
   */
  public AbstractServerConfigurationBean()
  {
    super ( "");
  }
  
  /**
   * Class constructor
   * 
   * @param srvName String
   */
  public AbstractServerConfigurationBean( String srvName)
  {
      super( srvName);
  }
  
  /**
   * Set the authentication service
   * 
   * @param authenticationService AuthenticationService
   */
  public void setAuthenticationService(AuthenticationService authenticationService)
  {
      m_authenticationService = authenticationService;
  }

  /**
   * Set the filesystem driver for the node service based filesystem
   * 
   * @param diskInterface DiskInterface
   */
  public void setDiskInterface(ExtendedDiskInterface diskInterface)
  {
      m_repoDiskInterface = diskInterface;
  }

  /**
   * Set the authentication component
   * 
   * @param component AuthenticationComponent
   */
  public void setAuthenticationComponent(AuthenticationComponent component)
  {
      m_authenticationComponent = component;
  }

  /**
   * Set the node service
   * 
   * @param service NodeService
   */
  public void setNodeService(NodeService service)
  {
      m_nodeService = service;
  }

  /**
   * Set the person service
   * 
   * @param service PersonService
   */
  public void setPersonService(PersonService service)
  {
      m_personService = service;
  }

  /**
   * Set the transaction service
   * 
   * @param service TransactionService
   */
  public void setTransactionService(TransactionService service)
  {
      m_transactionService = service;
  }

  /**
   * Set the tenant service
   * 
   * @param tenantService TenantService
   */
  public void setTenantService(TenantService tenantService)
  {
	  m_tenantService = tenantService;
  }

  /**
   * Set the search service
   * 
   * @param searchService SearchService
   */
  public void setSearchService(SearchService searchService)
  {
	  m_searchService = searchService;
  }
  
  /**
   * Set the namespace service
   * 
   * @param namespaceService NamespaceService
   */
  public void setNamespaceService(NamespaceService namespaceService)
  {
	  m_namespaceService = namespaceService;
  }
  
  /**
   * Set the authority service
   * 
   * @param authService AuthorityService
   */
  public void setAuthorityService(AuthorityService authService)
  {
  	m_authorityService = authService;
  }
  
  /**
   * Check if the configuration has been initialized
   * 
   * @return Returns true if the configuration was fully initialised
   */
  public boolean isInitialised()
  {
      return m_initialised;
  }

  /**
   * Check if the FTP server is enabled
   * 
   * @return boolean
   */
  public final boolean isFTPServerEnabled()
  {
      return hasConfigSection( FTPConfigSection.SectionName);
  }

  /**
   * Check if the NFS server is enabled
   * 
   * @return boolean
   */
  public final boolean isNFSServerEnabled()
  {
      return hasConfigSection( NFSConfigSection.SectionName);
  }
  
  /**
   * Return the repository disk interface to be used to create shares
   * 
   * @return DiskInterface
   */
  public final ExtendedDiskInterface getRepoDiskInterface()
  {
      return m_repoDiskInterface;
  }
  
  /**
   * Initialize the configuration using the configuration service
   */
  public void init()
  {
      // Check that all required properties have been set
	  
      if (m_authenticationComponent == null)
      {
          throw new AlfrescoRuntimeException("Property 'authenticationComponent' not set");
      }
      else if (m_authenticationService == null)
      {
          throw new AlfrescoRuntimeException("Property 'authenticationService' not set");
      }
      else if (m_nodeService == null)
      {
          throw new AlfrescoRuntimeException("Property 'nodeService' not set");
      }
      else if (m_personService == null)
      {
          throw new AlfrescoRuntimeException("Property 'personService' not set");
      }
      else if (m_transactionService == null)
      {
          throw new AlfrescoRuntimeException("Property 'transactionService' not set");
      }
      else if (m_repoDiskInterface == null)
      {
          throw new AlfrescoRuntimeException("Property 'diskInterface' not set");
      }
      else if (m_authorityService == null)
      {
      	throw new AlfrescoRuntimeException("Property 'authorityService' not set");
      }
      
      // Set the platform type

      determinePlatformType();

      // Create the debug output configuration using a logger for all file server debug output
      
      DebugConfigSection debugConfig = new DebugConfigSection( this);
      try
      {
          debugConfig.setDebug("org.alfresco.filesys.debug.FileServerDebugInterface", new GenericConfigElement( "params"));
      }
      catch ( InvalidConfigurationException ex)
      {
      }
      
      // Create the global configuration and Alfresco configuration sections
      
      new GlobalConfigSection( this);
      new AlfrescoConfigSection( this);
      
      // Install the Alfresco client information factory
      
      ClientInfo.setFactory( new AlfrescoClientInfoFactory());
      
      // We need to check for a WINS server configuration in the CIFS server config section to initialize
      // the NetBIOS name lookups to use WINS rather broadcast lookups, which may be used to get the local
      // domain
      
      try {

    	  // Get the CIFS server config section and extract the WINS server config, if available
    	  
          processWINSServerConfig();
      }
      catch (Exception ex) {
    	  
          // Configuration error

          logger.error("File server configuration error (WINS), " + ex.getMessage(), ex);
      }
      
      // Initialize the filesystems
      
      try
      {
    	  // Process the core server configuration
    	  processCoreServerConfig();
    	  
          // Process the security configuration
          processSecurityConfig();
          
          // Process the filesystems configuration
          processFilesystemsConfig();
      }
      catch (Exception ex)
      {
          // Configuration error
          throw new AlfrescoRuntimeException("File server configuration error, " + ex.getMessage(), ex);
      }

      // Initialize the FTP server

      try
      {
          // Process the FTP server configuration
          processFTPServerConfig();
          
          // Log the successful startup
          
          logger.info("FTP server " + (isFTPServerEnabled() ? "" : "NOT ") + "started");
      }
      catch (Exception ex)
      {
          // Configuration error
        
          logger.error("FTP server configuration error, " + ex.getMessage(), ex);
      }                 
  }

  protected abstract void processCoreServerConfig() throws InvalidConfigurationException;

  protected abstract void processSecurityConfig();
  
  protected abstract void processFilesystemsConfig();

  protected abstract void processFTPServerConfig();

  protected void processWINSServerConfig() {}

  /**
   * Close the configuration bean
   */
  public final void closeConfiguration()
  {
      super.closeConfiguration();
  }
  
  /**
   * Determine the platform type
   */
  private final void determinePlatformType()
  {
    if ( m_platform == Platform.Type.Unchecked)
      m_platform = Platform.isPlatformType();
  }
  
    /**
     * Parse the platforms attribute returning the set of platform ids
     * 
     * @param platformStr String
     */
    protected final EnumSet<Platform.Type> parsePlatformString(String platformStr)
    {
        // Split the platform string and build up a set of platform types
  
        EnumSet<Platform.Type> platformTypes = EnumSet.noneOf(Platform.Type.class);
        if (platformStr == null || platformStr.length() == 0)
            return platformTypes;
  
        StringTokenizer token = new StringTokenizer(platformStr.toUpperCase(Locale.ENGLISH), ",");
        String typ = null;
  
        try
        {
            while (token.hasMoreTokens())
            {
  
                // Get the current platform type string and validate
  
                typ = token.nextToken().trim();
                Platform.Type platform = Platform.Type.valueOf(typ);
  
                if (platform != Platform.Type.Unknown)
                    platformTypes.add(platform);
                else
                    throw new AlfrescoRuntimeException("Invalid platform type, " + typ);
            }
        }
        catch (IllegalArgumentException ex)
        {
            throw new AlfrescoRuntimeException("Invalid platform type, " + typ);
        }
  
        // Return the platform types
  
        return platformTypes;
    }
    
    /**
     * Get the local server name and optionally trim the domain name
     * 
     * @param trimDomain boolean
     * @return String
     */
    public final String getLocalServerName(boolean trimDomain)
    {
        // Use cached untrimmed version if necessary
        if (!trimDomain)
        {
            return getLocalServerName();
        }
        
        // Check if the name has already been set
        if (m_localName != null)
            return m_localName;

        // Find the local server name
        String srvName = getLocalServerName();

        // Strip the domain name

        if (trimDomain && srvName != null)
        {
            int pos = srvName.indexOf(".");
            if (pos != -1)
                srvName = srvName.substring(0, pos);
        }

        // Save the local server name

        m_localName = srvName;

        // Return the local server name

        return srvName;
    }

    /**
     * Get the local server name (untrimmed)
     * 
     * @return String
     */
    private String getLocalServerName()
    {
        // Check if the name has already been set

        if (m_localNameFull != null)
            return m_localNameFull;

        // Find the local server name

        String srvName = null;

        if (getPlatformType() == Platform.Type.WINDOWS && !isNativeCodeDisabled())
        {
            // Get the local name via JNI

            srvName = Win32NetBIOS.GetLocalNetBIOSName();
        }
        else
        {
            // Get the DNS name of the local system

            try
            {
                srvName = InetAddress.getLocalHost().getHostName();
            }
            catch (UnknownHostException ex)
            {
            }
        }

        // Save the local server name

        m_localNameFull = srvName;

        // Return the local server name

        return srvName;
    }

    /**
     * Get the local domain/workgroup name
     * 
     * @return String
     */
    public final String getLocalDomainName()
    {
        // Check if the local domain has been set

        if (m_localDomain != null)
            return m_localDomain;

        // Find the local domain name

        String domainName = null;

        if (getPlatformType() == Platform.Type.WINDOWS && !isNativeCodeDisabled())
        {
            // Get the local domain/workgroup name via JNI

            domainName = Win32NetBIOS.GetLocalDomainName();

            // Debug

            if (logger.isDebugEnabled())
                logger.debug("Local domain name is " + domainName + " (via JNI)");
        }
        else
        {
            NetBIOSName nbName = null;

            try
            {
                // Try and find the browse master on the local network

                nbName = NetBIOSSession.FindName(NetBIOSName.BrowseMasterName, NetBIOSName.BrowseMasterGroup, 5000);

                // Log the browse master details

                if (logger.isDebugEnabled())
                    logger.debug("Found browse master at " + nbName.getIPAddressString(0));

                // Get the NetBIOS name list from the browse master

                NetBIOSNameList nbNameList = NetBIOSSession.FindNamesForAddress(nbName.getIPAddressString(0));
                if (nbNameList != null)
                {
                    nbName = nbNameList.findName(NetBIOSName.MasterBrowser, false);
                    // Set the domain/workgroup name
                    if (nbName != null)
                        domainName = nbName.getName();
                }
            }
            catch (IOException ex)
            {
            }
        }

        // Save the local domain name

        m_localDomain = domainName;

        // Return the local domain/workgroup name

        return domainName;
    }
    
    /**
     * Parse an adapter name string and return the matching address
     * 
     * @param adapter String
     * @return InetAddress
     * @exception InvalidConfigurationException
     */
    protected final InetAddress parseAdapterName(String adapter)
      throws InvalidConfigurationException {

      NetworkInterface ni = null;
      
      try {
        ni = NetworkInterface.getByName( adapter);
      }
      catch (SocketException ex) {
        throw new InvalidConfigurationException( "Invalid adapter name, " + adapter);
      }
      
      if ( ni == null)
        throw new InvalidConfigurationException( "Invalid network adapter name, " + adapter);
      
      // Get the IP address for the adapter

      InetAddress adapAddr = null;
      Enumeration<InetAddress> addrEnum = ni.getInetAddresses();
      
      while ( addrEnum.hasMoreElements() && adapAddr == null) {
        
        // Get the current address
        
        InetAddress addr = addrEnum.nextElement();
        if ( IPAddress.isNumericAddress( addr.getHostAddress()))
          adapAddr = addr;
      }
      
      // Check if we found the IP address to bind to
      
      if ( adapAddr == null)
        throw new InvalidConfigurationException( "Adapter " + adapter + " does not have a valid IP address");

      // Return the adapter address
      
      return adapAddr;
    }
    
    private ApplicationContext applicationContext = null;
    
    
    /* (non-Javadoc)
     * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
     */
    public void onApplicationEvent(ApplicationEvent event)
    {
        if (event instanceof ContextRefreshedEvent)
        {
            ContextRefreshedEvent refreshEvent = (ContextRefreshedEvent)event;
            ApplicationContext refreshContext = refreshEvent.getApplicationContext();
            if (refreshContext != null && refreshContext.equals(applicationContext))
            {
                // Initialize the bean
              
                init();
            }
        }
    }

    /* (non-Javadoc)
     * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
     */
    public void setApplicationContext(ApplicationContext applicationContext)
        throws BeansException
    {
        this.applicationContext = applicationContext;
    }
    
    /**
     * Return the authentication service
     * 
     * @return AuthenticationService
     */
    protected final AuthenticationService getAuthenticationService()
    {
        return m_authenticationService;
    }
    
    /**
     * Return the authentication component
     * 
     * @return AuthenticationComponent
     */
    protected final AuthenticationComponent getAuthenticationComponent()
    {
        return m_authenticationComponent;
    }
    
    /**
     * Return the node service
     * 
     * @return NodeService
     */
    protected final NodeService getNodeService()
    {
        return m_nodeService;
    }
    
    /**
     * Return the person service
     * 
     * @return PersonService
     */
    protected final PersonService getPersonService()
    {
        return m_personService;
    }
    
    /**
     * Return the transaction service
     * 
     * @return TransactionService
     */
    protected final TransactionService getTransactionService()
    {
        return m_transactionService;
    }
    
    /**
     * Return the tenant service
     * 
     * @return TenantService
     */
    protected final TenantService getTenantService()
    {
    	return m_tenantService;
    }
    
    /**
     * Return the search service
     * 
     * @return SearchService
     */
    protected final SearchService getSearchService()
    {
    	return m_searchService;
    }
    
    /**
     * Return the namespace service
     * 
     * @return NamespaceService
     */
    protected final NamespaceService getNamespaceService()
    {
    	return m_namespaceService;
    }
    
    /**
     * Check if native code calls are disabled
     * 
     * @return boolean
     */
    public final boolean isNativeCodeDisabled()
    {
    	return m_disableNativeCode;
    }
    
    /**
     * Return the named bean
     * 
     * @param beanName String
     * @return Object
     */
    public final Object getBean( String beanName)
    {
    	return applicationContext.getBean( beanName);
    }
    
    /**
     * Return the applicatin context
     * 
     * @return ApplicationContext
     */
    public final ApplicationContext getApplicationsContext()
    {
    	return applicationContext;
    }

    /**
     * Return the authority service
     * 
     * @return AuthorityService
     */
    public final AuthorityService getAuthorityService()
    {
    	return m_authorityService;
    }
}
