/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.services.jcr.impl;

import org.exoplatform.commons.utils.SecurityHelper;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.container.jmx.MX4JComponentAdapterFactory;
import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.management.jmx.annotations.NameTemplate;
import org.exoplatform.management.jmx.annotations.NamingContext;
import org.exoplatform.management.jmx.annotations.Property;
import org.exoplatform.services.jcr.access.AccessControlPolicy;
import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.config.RepositoryEntry;
import org.exoplatform.services.jcr.config.WorkspaceEntry;
import org.exoplatform.services.jcr.core.nodetype.ExtendedNodeTypeManager;
import org.exoplatform.services.jcr.core.nodetype.NodeTypeDataManager;
import org.exoplatform.services.jcr.core.security.JCRRuntimePermissions;
import org.exoplatform.services.jcr.impl.core.AddNamespacePluginHolder;
import org.exoplatform.services.jcr.impl.core.LocationFactory;
import org.exoplatform.services.jcr.impl.core.NamespaceDataPersister;
import org.exoplatform.services.jcr.impl.core.NamespaceRegistryImpl;
import org.exoplatform.services.jcr.impl.core.RepositoryImpl;
import org.exoplatform.services.jcr.impl.core.ScratchWorkspaceInitializer;
import org.exoplatform.services.jcr.impl.core.SessionFactory;
import org.exoplatform.services.jcr.impl.core.SessionRegistry;
import org.exoplatform.services.jcr.impl.core.WorkspaceInitializer;
import org.exoplatform.services.jcr.impl.core.access.DefaultAccessManagerImpl;
import org.exoplatform.services.jcr.impl.core.lock.LockManagerImpl;
import org.exoplatform.services.jcr.impl.core.lock.LockRemoverHolder;
import org.exoplatform.services.jcr.impl.core.nodetype.NodeTypeDataManagerImpl;
import org.exoplatform.services.jcr.impl.core.nodetype.NodeTypeManagerImpl;
import org.exoplatform.services.jcr.impl.core.nodetype.registration.JCRNodeTypeDataPersister;
import org.exoplatform.services.jcr.impl.core.observation.ObservationManagerRegistry;
import org.exoplatform.services.jcr.impl.core.query.QueryManagerFactory;
import org.exoplatform.services.jcr.impl.core.query.RepositoryIndexSearcherHolder;
import org.exoplatform.services.jcr.impl.core.query.SearchManager;
import org.exoplatform.services.jcr.impl.core.query.SystemSearchManager;
import org.exoplatform.services.jcr.impl.core.query.SystemSearchManagerHolder;
import org.exoplatform.services.jcr.impl.core.value.ValueFactoryImpl;
import org.exoplatform.services.jcr.impl.dataflow.persistent.CacheableWorkspaceDataManager;
import org.exoplatform.services.jcr.impl.dataflow.persistent.LinkedWorkspaceStorageCacheImpl;
import org.exoplatform.services.jcr.impl.dataflow.persistent.LocalWorkspaceDataManagerStub;
import org.exoplatform.services.jcr.impl.dataflow.session.TransactionableResourceManager;
import org.exoplatform.services.jcr.impl.storage.SystemDataContainerHolder;
import org.exoplatform.services.jcr.impl.storage.value.StandaloneStoragePluginProvider;
import org.exoplatform.services.jcr.impl.util.io.FileCleanerHolder;
import org.exoplatform.services.jcr.storage.WorkspaceDataContainer;
import org.exoplatform.services.jcr.util.IdGenerator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.List;

import javax.jcr.NamespaceRegistry;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.query.QueryManager;
import javax.naming.NameNotFoundException;

/**
 * Created by The eXo Platform SAS.
 * 
 * @author Gennady Azarenkov
 * @version $Id: RepositoryContainer.java 13986 2008-05-08 10:48:43Z pnedonosko $
 */
@Managed
@NameTemplate(@Property(key = "repository", value = "{Name}"))
@NamingContext(@Property(key = "repository", value = "{Name}"))
public class RepositoryContainer extends ExoContainer
{

   /**
    * Repository config.
    */
   private final RepositoryEntry config;

   /**
    * Repository name.
    */
   private final String name;

   /**
    * System workspace DataManager.
    */
   private LocalWorkspaceDataManagerStub systemDataManager = null;

   /**
    * Logger.
    */
   private final Log log = ExoLogger.getLogger("exo.jcr.component.core.RepositoryContainer");

   /**
    * List of AddNamespacePlugin.
    */
   private List<ComponentPlugin> addNamespacePlugins;

   /**
    * RepositoryContainer constructor.
    * 
    * @param parent
    *          container
    * @param config
    *          Repository configuration
    * @param addNamespacePlugins
    *          list of addNamespacePlugin
    * @throws RepositoryException
    *           container initialization error
    * @throws RepositoryConfigurationException
    *           configuration error
    */
   public RepositoryContainer(final ExoContainer parent, RepositoryEntry config, List<ComponentPlugin> addNamespacePlugins)
      throws RepositoryException, RepositoryConfigurationException
   {

      super(new MX4JComponentAdapterFactory(), parent);

      // Defaults:
      if (config.getAccessControl() == null)
         config.setAccessControl(AccessControlPolicy.OPTIONAL);

      this.config = config;
      this.addNamespacePlugins = addNamespacePlugins;
      this.name = config.getName();
      
      SecurityHelper.doPrivilegedAction(new PrivilegedAction<Void>()
      {
         public Void run()
         {
            context.setName(parent.getContext().getName() + "-" + name);
            parent.registerComponentInstance(name, RepositoryContainer.this);
            return null;
         }
      });
      registerComponents();
   }

   /**
    * RepositoryContainer constructor.
    * 
    * @param parent
    *          container
    * @param config
    *          Repository configuration
    * @throws RepositoryException
    *           container initialization error
    * @throws RepositoryConfigurationException
    *           configuration error
    */
   public RepositoryContainer(final ExoContainer parent, RepositoryEntry config) throws RepositoryException,
      RepositoryConfigurationException
   {

      super(new MX4JComponentAdapterFactory(), parent);

      // Defaults:
      if (config.getAccessControl() == null)
         config.setAccessControl(AccessControlPolicy.OPTIONAL);

      this.config = config;
      this.name = config.getName();
      SecurityHelper.doPrivilegedAction(new PrivilegedAction<Void>()
      {
         public Void run()
         {
            context.setName(parent.getContext().getName() + "-" + name);
            parent.registerComponentInstance(name, RepositoryContainer.this);
            return null;
         }
      });

      registerComponents();
   }

   public LocationFactory getLocationFactory()
   {
      return (LocationFactory)getComponentInstanceOfType(LocationFactory.class);
   }

   /**
    * @return Returns the name.
    */
   @Managed
   @ManagedDescription("The repository container name")
   public String getName()
   {
      return name;
   }

   public NamespaceRegistry getNamespaceRegistry()
   {
      return (NamespaceRegistry)getComponentInstanceOfType(NamespaceRegistry.class);
   }

   public ExtendedNodeTypeManager getNodeTypeManager()
   {
      return (ExtendedNodeTypeManager)getComponentInstanceOfType(NodeTypeManager.class);
   }

   /**
    * Get workspace Container by name.
    * 
    * @param workspaceName
    *          name
    * @return WorkspaceContainer
    */
   public WorkspaceContainer getWorkspaceContainer(String workspaceName)
   {
      Object comp = getComponentInstance(workspaceName);
      return comp != null && comp instanceof WorkspaceContainer ? (WorkspaceContainer)comp : null;
   }

   /**
    * Get workspace configuration entry by name.
    * 
    * @param wsName
    *          workspace name
    * @return WorkspaceEntry
    */
   public WorkspaceEntry getWorkspaceEntry(String wsName)
   {
      for (WorkspaceEntry entry : config.getWorkspaceEntries())
      {
         if (entry.getName().equals(wsName))
            return entry;
      }
      return null;
   }

   /**
    * Register workspace from configuration.
    * 
    * @param wsConfig
    *          configuration
    * @throws RepositoryException
    *           initialization error
    * @throws RepositoryConfigurationException
    *           configuration error
    */
   public void registerWorkspace(final WorkspaceEntry wsConfig) throws RepositoryException,
      RepositoryConfigurationException
   {
      // Need privileges to manage repository.
      SecurityManager security = System.getSecurityManager();
      if (security != null)
      {
         security.checkPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);
      }

      try
      {
         SecurityHelper.doPrivilegedExceptionAction(new PrivilegedExceptionAction<Void>()
         {
            public Void run() throws RepositoryException, RepositoryConfigurationException
            {
               final boolean isSystem = config.getSystemWorkspaceName().equals(wsConfig.getName());

               if (getWorkspaceContainer(wsConfig.getName()) != null)
                  throw new RepositoryException("Workspace " + wsConfig.getName() + " already registered");

               final WorkspaceContainer workspaceContainer = new WorkspaceContainer(RepositoryContainer.this, wsConfig);
               registerComponentInstance(wsConfig.getName(), workspaceContainer);

               wsConfig.setUniqueName(getName() + "_" + wsConfig.getName());

               workspaceContainer.registerComponentInstance(wsConfig);

               workspaceContainer.registerComponentImplementation(StandaloneStoragePluginProvider.class);
               try
               {
                  final Class<?> containerType = Class.forName(wsConfig.getContainer().getType());
                  workspaceContainer.registerComponentImplementation(containerType);
                  if (isSystem)
                  {
                     registerComponentInstance(new SystemDataContainerHolder((WorkspaceDataContainer)workspaceContainer
                        .getComponentInstanceOfType(WorkspaceDataContainer.class)));
                  }
               }
               catch (ClassNotFoundException e)
               {
                  throw new RepositoryConfigurationException("Class not found for workspace data container "
                     + wsConfig.getUniqueName() + " : " + e);
               }
               // cache type
               try
               {
                  String className = wsConfig.getCache().getType();
                  if (className != null && className.length() > 0)
                  {
                     workspaceContainer.registerComponentImplementation(Class.forName(className));
                  }
                  else
                     workspaceContainer.registerComponentImplementation(LinkedWorkspaceStorageCacheImpl.class);
               }
               catch (ClassNotFoundException e)
               {
                  log.warn("Workspace cache class not found " + wsConfig.getCache().getType()
                     + ", will use default. Error : " + e);
                  workspaceContainer.registerComponentImplementation(LinkedWorkspaceStorageCacheImpl.class);
               }

               workspaceContainer.registerComponentImplementation(CacheableWorkspaceDataManager.class);
               workspaceContainer.registerComponentImplementation(LocalWorkspaceDataManagerStub.class);
               workspaceContainer.registerComponentImplementation(ObservationManagerRegistry.class);

               // Lock manager and Lock persister is a optional parameters
               if (wsConfig.getLockManager() != null && wsConfig.getLockManager().getPersister() != null)
               {
                  try
                  {
                     final Class<?> lockPersister = Class.forName(wsConfig.getLockManager().getPersister().getType());
                     workspaceContainer.registerComponentImplementation(lockPersister);
                  }
                  catch (ClassNotFoundException e)
                  {
                     throw new RepositoryConfigurationException("Class not found for workspace lock persister "
                        + wsConfig.getLockManager().getPersister().getType() + ", container " + wsConfig.getUniqueName()
                        + " : " + e);
                  }
               }

               if (wsConfig.getLockManager() != null && wsConfig.getLockManager().getType() != null)
               {
                  try
                  {
                     final Class<?> lockManagerType = Class.forName(wsConfig.getLockManager().getType());
                     workspaceContainer.registerComponentImplementation(lockManagerType);
                  }
                  catch (ClassNotFoundException e)
                  {
                     throw new RepositoryConfigurationException("Class not found for workspace lock manager "
                        + wsConfig.getLockManager().getType() + ", container " + wsConfig.getUniqueName() + " : " + e);
                  }
               }
               else
               {
                  workspaceContainer.registerComponentImplementation(LockManagerImpl.class);
               }
               // Query handler
               if (wsConfig.getQueryHandler() != null)
               {
                  workspaceContainer.registerComponentImplementation(SearchManager.class);
                  workspaceContainer.registerComponentImplementation(QueryManager.class);
                  workspaceContainer.registerComponentImplementation(QueryManagerFactory.class);
                  workspaceContainer.registerComponentInstance(wsConfig.getQueryHandler());
                  if (isSystem)
                  {
                     workspaceContainer.registerComponentImplementation(SystemSearchManager.class);
                  }
               }

               // access manager
               if (wsConfig.getAccessManager() != null && wsConfig.getAccessManager().getType() != null)
               {
                  try
                  {
                     final Class<?> am = Class.forName(wsConfig.getAccessManager().getType());
                     workspaceContainer.registerComponentImplementation(am);
                  }
                  catch (ClassNotFoundException e)
                  {
                     throw new RepositoryConfigurationException("Class not found for workspace access manager "
                        + wsConfig.getAccessManager().getType() + ", container " + wsConfig.getUniqueName() + " : " + e);
                  }
               }

               // initializer
               final Class<?> initilizerType;
               if (wsConfig.getInitializer() != null && wsConfig.getInitializer().getType() != null)
               {
                  // use user defined
                  try
                  {
                     initilizerType = Class.forName(wsConfig.getInitializer().getType());
                  }
                  catch (ClassNotFoundException e)
                  {
                     throw new RepositoryConfigurationException("Class not found for workspace initializer "
                        + wsConfig.getInitializer().getType() + ", container " + wsConfig.getUniqueName() + " : " + e);
                  }
               }
               else
               {
                  // use default
                  initilizerType = ScratchWorkspaceInitializer.class;
               }
               workspaceContainer.registerComponentImplementation(initilizerType);
               workspaceContainer.registerComponentImplementation(TransactionableResourceManager.class);
               workspaceContainer.registerComponentImplementation(SessionFactory.class);
               final LocalWorkspaceDataManagerStub wsDataManager =
                  (LocalWorkspaceDataManagerStub)workspaceContainer
                     .getComponentInstanceOfType(LocalWorkspaceDataManagerStub.class);

               if (isSystem)
               {
                  // system workspace
                  systemDataManager = wsDataManager;
                  registerComponentInstance(systemDataManager);
               }

               wsDataManager.setSystemDataManager(systemDataManager);

               if (!config.getWorkspaceEntries().contains(wsConfig))
                  config.getWorkspaceEntries().add(wsConfig);
               return null;
            }
         });

      }
      catch (PrivilegedActionException pae)
      {
         Throwable cause = pae.getCause();
         if (cause instanceof RepositoryConfigurationException)
         {
            throw (RepositoryConfigurationException)cause;
         }
         else if (cause instanceof RepositoryException)
         {
            throw (RepositoryException)cause;
         }
         else if (cause instanceof RuntimeException)
         {
            RuntimeException e = (RuntimeException)cause;
            int depth = 0;
            Throwable retval = e;
            while (retval.getCause() != null && depth < 100)
            {
               retval = retval.getCause();
               if (retval instanceof RepositoryException)
               {
                  throw new RepositoryException(retval.getMessage(), e);
               }
               else if (retval instanceof RepositoryConfigurationException)
               {
                  throw new RepositoryConfigurationException(retval.getMessage(), e);
               }
               else if (retval instanceof NameNotFoundException)
               {
                  throw new RepositoryException(retval.getMessage(), e);
               }
               depth++;
            }
            throw e;            
         }
         else
         {
            throw new RepositoryException(cause);
         }
      }      
   }

   // Components access methods -------

   /**
    * {@inheritDoc}
    */
   @Override
   public void start()
   {

      try
      {
         init();
         load();
      }
      catch (RepositoryException e)
      {
         log.error("Repository error", e);
         throw new RuntimeException(e);
      }
      catch (RepositoryConfigurationException e)
      {
         log.error("Configuration error", e);
         throw new RuntimeException(e);
      }

      super.start();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public synchronized void stop()
   {
      super.stop();
      super.unregisterAllComponents();
   }

   /**
    * Initialize worspaces (root node and jcr:system for system workspace).
    * <p>
    * Runs on container start.
    * 
    * @throws RepositoryException
    * @throws RepositoryConfigurationException
    */
   private void init() throws RepositoryException, RepositoryConfigurationException
   {
      List<WorkspaceEntry> wsEntries = config.getWorkspaceEntries();

      NodeTypeDataManager typeManager = (NodeTypeDataManager)this.getComponentInstanceOfType(NodeTypeDataManager.class);
      NamespaceRegistryImpl namespaceRegistry =
         (NamespaceRegistryImpl)this.getComponentInstanceOfType(NamespaceRegistry.class);

      for (WorkspaceEntry ws : wsEntries)
      {
         initWorkspace(ws);
         WorkspaceContainer workspaceContainer = getWorkspaceContainer(ws.getName());
         SearchManager searchManager =
            (SearchManager)workspaceContainer.getComponentInstanceOfType(SearchManager.class);
         //         if (searchManager != null)
         //         {
         //            typeManager.addQueryHandler(searchManager.getHandler());
         //            namespaceRegistry.addQueryHandler(searchManager.getHandler());
         //         }
         //         else
         //         {
         //            log.warn("Search manager not configured for " + ws.getName());
         //         }
      }

      SystemSearchManagerHolder searchManager =
         (SystemSearchManagerHolder)this.getComponentInstanceOfType(SystemSearchManagerHolder.class);
      //      if (searchManager != null)
      //      {
      //         typeManager.addQueryHandler(searchManager.get().getHandler());
      //         namespaceRegistry.addQueryHandler(searchManager.get().getHandler());
      //      }
      //      else
      //      {
      //         log.warn("System search manager not configured ");
      //      }

   }

   /**
    * Init workspace root node. If it's the system workspace init jcr:system too.
    * 
    * @param wsConfig
    * @throws RepositoryException
    */
   private void initWorkspace(WorkspaceEntry wsConfig) throws RepositoryException
   {

      WorkspaceContainer workspaceContainer = getWorkspaceContainer(wsConfig.getName());

      // touch independent components
      workspaceContainer.getComponentInstanceOfType(IdGenerator.class);

      // Init Root and jcr:system if workspace is system workspace
      WorkspaceInitializer wsInitializer =
         (WorkspaceInitializer)workspaceContainer.getComponentInstanceOfType(WorkspaceInitializer.class);
      RepositoryCreationSynchronizer synchronizer =
         (RepositoryCreationSynchronizer)getComponentInstanceOfType(RepositoryCreationSynchronizer.class);
      // The synchronizer will be used to synchronize all the cluster
      // nodes to prevent any concurrent jcr initialization i.e. EXOJCR-887
      synchronizer.waitForApproval(wsInitializer.isWorkspaceInitialized());
      wsInitializer.initWorkspace();
   }

   // ////// initialize --------------

   private void registerComponents() throws RepositoryConfigurationException, RepositoryException
   {
      SecurityHelper.doPrivilegedAction(new PrivilegedAction<Void>()
      {
         public Void run()
         {
            registerComponentInstance(config);
            // WorkspaceFileCleanerHolder - is a common holder for all workspaces. 
            // It is used to initialize FileValueStorage
            registerComponentImplementation(FileCleanerHolder.class);
            registerComponentImplementation(LockRemoverHolder.class);
            return null;
         }
      });
      registerWorkspacesComponents();
      registerRepositoryComponents();
   }

   private void registerRepositoryComponents() throws RepositoryConfigurationException, RepositoryException
   {
      try
      {
         SecurityHelper.doPrivilegedExceptionAction(new PrivilegedExceptionAction<Void>()
         {
            public Void run() throws RepositoryConfigurationException, RepositoryException
            {

               registerComponentImplementation(RepositorySuspendController.class);
               registerComponentImplementation(IdGenerator.class);

               registerComponentImplementation(RepositoryIndexSearcherHolder.class);

               registerComponentImplementation(LocationFactory.class);
               registerComponentImplementation(ValueFactoryImpl.class);

               registerComponentInstance(new AddNamespacePluginHolder(addNamespacePlugins));

               registerComponentImplementation(JCRNodeTypeDataPersister.class);
               registerComponentImplementation(NamespaceDataPersister.class);
               registerComponentImplementation(NamespaceRegistryImpl.class);

               registerComponentImplementation(NodeTypeManagerImpl.class);
               registerComponentImplementation(NodeTypeDataManagerImpl.class);

               registerComponentImplementation(DefaultAccessManagerImpl.class);

               registerComponentImplementation(SessionRegistry.class);

               String systemWsname = config.getSystemWorkspaceName();
               WorkspaceEntry systemWsEntry = getWorkspaceEntry(systemWsname);

               if (systemWsEntry != null && systemWsEntry.getQueryHandler() != null)
               {
                  SystemSearchManager systemSearchManager =
                     (SystemSearchManager)getWorkspaceContainer(systemWsname).getComponentInstanceOfType(
                        SystemSearchManager.class);
                  registerComponentInstance(new SystemSearchManagerHolder(systemSearchManager));
               }
               try
               {
                  final Class<?> authenticationPolicyClass = Class.forName(config.getAuthenticationPolicy());
                  registerComponentImplementation(authenticationPolicyClass);
               }
               catch (ClassNotFoundException e)
               {
                  throw new RepositoryConfigurationException("Class not found for repository authentication policy: " + e);
               }

               // Repository
               final RepositoryImpl repository = new RepositoryImpl(RepositoryContainer.this);
               registerComponentInstance(repository);
               return null;
            }
         });
      }
      catch (PrivilegedActionException pae)
      {
         Throwable cause = pae.getCause();
         if (cause instanceof RepositoryConfigurationException)
         {
            throw (RepositoryConfigurationException)cause;
         }
         else if (cause instanceof RepositoryException)
         {
            throw (RepositoryException)cause;
         }
         else
         {
            throw new RepositoryException(cause);
         }
      }
   }

   private void registerWorkspacesComponents() throws RepositoryException, RepositoryConfigurationException
   {
      // System workspace should be first initialized.
      for (WorkspaceEntry we : config.getWorkspaceEntries())
      {
         if (we.getName().equals(config.getSystemWorkspaceName()))
         {
            registerWorkspace(we);
         }
      }

      // Initialize other (non system) workspaces.
      for (WorkspaceEntry we : config.getWorkspaceEntries())
      {
         if (!we.getName().equals(config.getSystemWorkspaceName()))
         {
            registerWorkspace(we);
         }
      }
   }

   /**
    * Load namespaces and nodetypes from persistent repository.
    * 
    * <p>
    * Runs on container start.
    * 
    * @throws RepositoryException
    */
   private void load() throws RepositoryException
   {
      //Namespaces first
      NamespaceDataPersister namespacePersister =
         (NamespaceDataPersister)this.getComponentInstanceOfType(NamespaceDataPersister.class);
      NamespaceRegistryImpl nsRegistry = (NamespaceRegistryImpl)getNamespaceRegistry();

      namespacePersister.start();
      nsRegistry.start();

      //Node types now.
      JCRNodeTypeDataPersister nodeTypePersister =
         (JCRNodeTypeDataPersister)this.getComponentInstanceOfType(JCRNodeTypeDataPersister.class);

      NodeTypeDataManagerImpl ntManager =
         (NodeTypeDataManagerImpl)this.getComponentInstanceOfType(NodeTypeDataManagerImpl.class);

      nodeTypePersister.start();
      ntManager.start();

   }

}
