/*
 * 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.ExoContainerContext;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.container.component.ThreadContext;
import org.exoplatform.container.component.ThreadContextHolder;
import org.exoplatform.container.configuration.ConfigurationManager;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.config.RepositoryEntry;
import org.exoplatform.services.jcr.config.RepositoryServiceConfiguration;
import org.exoplatform.services.jcr.config.WorkspaceEntry;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.core.nodetype.ExtendedNodeTypeManager;
import org.exoplatform.services.jcr.core.security.JCRRuntimePermissions;
import org.exoplatform.services.jcr.dataflow.persistent.ItemsPersistenceListener;
import org.exoplatform.services.jcr.impl.core.RepositoryImpl;
import org.exoplatform.services.jcr.impl.core.SessionRegistry;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.picocontainer.Startable;

import java.io.InputStream;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;

import javax.jcr.RepositoryException;

/**
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:geaz@users.sourceforge.net">Gennady Azarenkov </a>
 * @version $Id: RepositoryServiceImpl.java 13986 2008-05-08 10:48:43Z pnedonosko $
 */

public class RepositoryServiceImpl implements RepositoryService, Startable, ThreadContextHolder
{

   protected static Log log = ExoLogger.getLogger("exo.jcr.component.core.RepositoryServiceImpl");

   private final RepositoryServiceConfiguration config;

   private final ThreadLocal<String> currentRepositoryName = new ThreadLocal<String>();

   private final ConcurrentHashMap<String, RepositoryContainer> repositoryContainers =
      new ConcurrentHashMap<String, RepositoryContainer>();

   private final List<ComponentPlugin> addNodeTypePlugins;

   private final List<ComponentPlugin> addNamespacesPlugins;

   private final ManagerStartChanges managerStartChanges;

   private final ExoContainerContext containerContext;

   private ExoContainer parentContainer;

   public RepositoryServiceImpl(RepositoryServiceConfiguration configuration)
   {
      this(configuration, null);
   }

   public RepositoryServiceImpl(RepositoryServiceConfiguration configuration, ExoContainerContext context)
   {
      this(configuration, context, null);
   }

   /**
    * @param synchronizer This component is used to synchronize the creation of the repositories between
    * all the cluster nodes. If this component has been defined in the configuration, it has to
    * be started before the {@link RepositoryServiceImpl} so we have to enforce the dependency
    * with this component by adding it to the constructor
    */
   public RepositoryServiceImpl(RepositoryServiceConfiguration configuration, ExoContainerContext context,
            RepositoryCreationSynchronizer synchronizer)
   {
      this.config = configuration;
      addNodeTypePlugins = new ArrayList<ComponentPlugin>();
      addNamespacesPlugins = new ArrayList<ComponentPlugin>();
      containerContext = context;
      currentRepositoryName.set(config.getDefaultRepositoryName());
      managerStartChanges = new ManagerStartChanges();
   }

   public void addPlugin(ComponentPlugin plugin)
   {
      if (plugin instanceof AddNodeTypePlugin)
         addNodeTypePlugins.add(plugin);
      else if (plugin instanceof AddNamespacesPlugin)
         addNamespacesPlugins.add(plugin);
      else if (plugin instanceof RepositoryChangesListenerRegisterPlugin)
      {
         managerStartChanges.addPlugin((RepositoryChangesListenerRegisterPlugin)plugin);
      }
   }

   /**
    * Create repository. <br>
    * Init worksapces for initial start or them load from persistence. <br>
    * Add namespaces and nodetypes from service plugins.
    * 
    */
   public void createRepository(RepositoryEntry rEntry) throws RepositoryConfigurationException,
      RepositoryException
   {
      // Need privileges to manage repository.
      SecurityManager security = System.getSecurityManager();
      if (security != null)
      {
         security.checkPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);
      }

      if (repositoryContainers.containsKey(rEntry.getName()))
      {
         throw new RepositoryConfigurationException("Repository container " + rEntry.getName() + " already started");
      }

      final RepositoryContainer repositoryContainer = new RepositoryContainer(parentContainer, rEntry, addNamespacesPlugins);

      // Storing and starting the repository container under
      // key=repository_name
      try
      {
         if (repositoryContainers.putIfAbsent(rEntry.getName(), repositoryContainer) == null)
         {
            SecurityHelper.doPrivilegedAction(new PrivilegedAction<Void>()
            {
               public Void run()
               {
                  managerStartChanges.registerListeners(repositoryContainer);
                  repositoryContainer.start();
                  return null;
               }
            });
         }
         else
         {
            throw new RepositoryConfigurationException("Repository container " + rEntry.getName() + " already started");
         }
      }
      catch (Throwable t)
      {
         repositoryContainers.remove(rEntry.getName());

         throw new RepositoryConfigurationException("Repository container " + rEntry.getName() + " was not started.",
            t);
      }

      if (!config.getRepositoryConfigurations().contains(rEntry))
      {
         config.getRepositoryConfigurations().add(rEntry);
      }

      registerNodeTypes(rEntry.getName());

      // turn on Repository ONLINE
      ManageableRepository mr =
         (ManageableRepository)repositoryContainer.getComponentInstanceOfType(ManageableRepository.class);
      mr.setState(ManageableRepository.ONLINE);
   }

   public RepositoryServiceConfiguration getConfig()
   {
      // Need privileges to manage repository.
      SecurityManager security = System.getSecurityManager();
      if (security != null)
      {
         security.checkPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);
      }

      return config;
   }

   /**
    * {@inheritDoc} 
    */
   public ThreadContext getThreadContext()
   {
      return new ThreadContext(currentRepositoryName);
   }

   /**
    * @return Name of current repository if exists or null in other case
    */
   public String getCurrentRepositoryName()
   {
      return currentRepositoryName.get();
   }

   public ManageableRepository getCurrentRepository() throws RepositoryException
   {
      if (currentRepositoryName.get() == null)
         return getDefaultRepository();
      return getRepository(currentRepositoryName.get());
   }

   public ManageableRepository getDefaultRepository() throws RepositoryException
   {
      return getRepository(config.getDefaultRepositoryName());
   }

   /**
    * @deprecated use getDefaultRepository() instead
    */
   @Deprecated
   public ManageableRepository getRepository() throws RepositoryException
   {
      return getDefaultRepository();
   }

   // ------------------- Startable ----------------------------

   public ManageableRepository getRepository(String name) throws RepositoryException
   {
      RepositoryContainer repositoryContainer = repositoryContainers.get(name);
      log.debug("RepositoryServiceimpl() getRepository " + name);
      if (repositoryContainer == null)
         throw new RepositoryException("Repository '" + name + "' not found.");

      return (ManageableRepository)repositoryContainer.getComponentInstanceOfType(ManageableRepository.class);
   }
   
   public RepositoryContainer getRepositoryContainer(String name)
   {
      return repositoryContainers.get(name);
   }

   public void setCurrentRepositoryName(String repositoryName) throws RepositoryConfigurationException
   {
      // Need privileges to manage repository.
      SecurityManager security = System.getSecurityManager();
      if (security != null)
      {
         security.checkPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);
      }

      if (!repositoryContainers.containsKey(repositoryName))
         throw new RepositoryConfigurationException("Repository is not configured. Name " + repositoryName);
      currentRepositoryName.set(repositoryName);
   }

   public void start()
   {
      // Need privileges to manage repository.
      SecurityManager security = System.getSecurityManager();
      if (security != null)
      {
         security.checkPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);
      }

      try
      {
         ExoContainer container = null;
         if (containerContext == null)
         {
            container = PortalContainer.getInstance();
         }
         else
         {
            container = containerContext.getContainer();
         }

         init(container);

      }
      catch (RepositoryException e)
      {
         log.error("Error start repository service", e);
      }
      catch (RepositoryConfigurationException e)
      {
         log.error("Error start repository service", e);
      }
      catch (Throwable e)
      {
         log.error("Error start repository service", e);
         throw new RuntimeException(e);
      }
   }

   public void stop()
   {
      // Need privileges to manage repository.
      SecurityManager security = System.getSecurityManager();
      if (security != null)
      {
         security.checkPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);
      }

      for (Entry<String, RepositoryContainer> entry : repositoryContainers.entrySet())
      {
         entry.getValue().stop();
      }
      repositoryContainers.clear();
      addNamespacesPlugins.clear();
      addNodeTypePlugins.clear();
      managerStartChanges.cleanup();
   }

   private void init(ExoContainer container) throws RepositoryConfigurationException, RepositoryException
   {
      this.parentContainer = container;
      List<RepositoryEntry> rEntries = config.getRepositoryConfigurations();
      for (int i = 0; i < rEntries.size(); i++)
      {
         RepositoryEntry rEntry = rEntries.get(i);
         // Making new repository container as portal's subcontainer
         createRepository(rEntry);
      }
   }

   private void registerNodeTypes(String repositoryName) throws RepositoryException
   {
      ConfigurationManager configService =
         (ConfigurationManager)parentContainer.getComponentInstanceOfType(ConfigurationManager.class);

      ExtendedNodeTypeManager ntManager = getRepository(repositoryName).getNodeTypeManager();
      //
      for (int j = 0; j < addNodeTypePlugins.size(); j++)
      {
         AddNodeTypePlugin plugin = (AddNodeTypePlugin)addNodeTypePlugins.get(j);
         List<String> autoNodeTypesFiles = plugin.getNodeTypesFiles(AddNodeTypePlugin.AUTO_CREATED);
         if (autoNodeTypesFiles != null && autoNodeTypesFiles.size() > 0)
         {
            for (String nodeTypeFilesName : autoNodeTypesFiles)
            {
               InputStream inXml;
               try
               {
                  inXml = configService.getInputStream(nodeTypeFilesName);
               }
               catch (Exception e)
               {
                  throw new RepositoryException(e);
               }

               if (log.isDebugEnabled())
               {
                  log.debug("Trying register node types from xml-file " + nodeTypeFilesName);
               }
               ntManager.registerNodeTypes(inXml, ExtendedNodeTypeManager.IGNORE_IF_EXISTS);
               if (log.isDebugEnabled())
               {
                  log.debug("Node types is registered from xml-file " + nodeTypeFilesName);
               }
            }

            List<String> defaultNodeTypesFiles = plugin.getNodeTypesFiles(repositoryName);
            if (defaultNodeTypesFiles != null && defaultNodeTypesFiles.size() > 0)
            {
               for (String nodeTypeFilesName : defaultNodeTypesFiles)
               {
                  InputStream inXml;
                  try
                  {
                     inXml = configService.getInputStream(nodeTypeFilesName);
                  }
                  catch (Exception e)
                  {
                     throw new RepositoryException(e);
                  }

                  log.info("Trying register node types (" + repositoryName + ") from xml-file " + nodeTypeFilesName);
                  ntManager.registerNodeTypes(inXml, ExtendedNodeTypeManager.IGNORE_IF_EXISTS);
                  log.info("Node types is registered (" + repositoryName + ") from xml-file " + nodeTypeFilesName);
               }
            }
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void removeRepository(String name, boolean forceRemove) throws RepositoryException
   {
      // Need privileges to manage repository.
      SecurityManager security = System.getSecurityManager();
      if (security != null)
      {
         security.checkPermission(JCRRuntimePermissions.MANAGE_REPOSITORY_PERMISSION);
      }

      if (!forceRemove && !canRemoveRepository(name))
         throw new RepositoryException("Repository " + name + " in use. If you want to "
            + " remove repository close all open sessions");

      try
      {
         RepositoryEntry repconfig = config.getRepositoryConfiguration(name);
         RepositoryImpl repo = (RepositoryImpl)getRepository(name);
         repo.setState(ManageableRepository.OFFLINE);
         
         for (WorkspaceEntry wsEntry : repconfig.getWorkspaceEntries())
         {
            repo.internalRemoveWorkspace(wsEntry.getName());
         }
         repconfig.getWorkspaceEntries().clear();
         final RepositoryContainer repositoryContainer = repositoryContainers.get(name);
         SecurityHelper.doPrivilegedAction(new PrivilegedAction<Void>()
         {
            public Void run()
            {
               repositoryContainer.stop();
               return null;
            }
         });         
         repositoryContainers.remove(name);
         config.getRepositoryConfigurations().remove(repconfig);
         SecurityHelper.doPrivilegedAction(new PrivilegedAction<Void>()
         {
            public Void run()
            {
               parentContainer.unregisterComponent(repositoryContainer.getName());
               return null;
            }
         });
      }
      catch (RepositoryConfigurationException e)
      {
         throw new RepositoryException(e);
      }
      catch (Exception e)
      {
         throw new RepositoryException(e);
      }
   }
   
   /**
    * {@inheritDoc}
    */
   public void removeRepository(String name) throws RepositoryException
   {
      this.removeRepository(name, false);
   }
   
   /**
    * {@inheritDoc}
    */
   public boolean canRemoveRepository(String name) throws RepositoryException
   {
      RepositoryImpl repo = (RepositoryImpl)getRepository(name);
      try
      {
         RepositoryEntry repconfig = config.getRepositoryConfiguration(name);

         for (WorkspaceEntry wsEntry : repconfig.getWorkspaceEntries())
         {
            // Check non system workspaces
            if (!repo.getSystemWorkspaceName().equals(wsEntry.getName()) && !repo.canRemoveWorkspace(wsEntry.getName()))
               return false;
         }
         // check system workspace
         RepositoryContainer repositoryContainer = repositoryContainers.get(name);
         SessionRegistry sessionRegistry =
            (SessionRegistry)repositoryContainer.getComponentInstance(SessionRegistry.class);
         if (sessionRegistry == null || sessionRegistry.isInUse(repo.getSystemWorkspaceName()))
            return false;

      }
      catch (RepositoryConfigurationException e)
      {
         throw new RepositoryException(e);
      }

      return true;
   }

   /**
    * Manager start changes plugins.
    */
   public class ManagerStartChanges
   {

      private Map<StorageKey, ItemsPersistenceListener> startChangesListeners =
         new HashMap<StorageKey, ItemsPersistenceListener>();

      /**
       * Add new StartChangesPlugin to manager.
       * 
       * @param plugin
       *          The StartChangesPlugin
       */
      public void addPlugin(RepositoryChangesListenerRegisterPlugin plugin)
      {
         String repositoryName = plugin.getRepositoryName();
         String workspaces = plugin.getWorkspaces();
         String listenerClassName = plugin.getListenerClassName();

         if (repositoryName != null && workspaces != null && listenerClassName != null)
         {
            StringTokenizer listTokenizer = new StringTokenizer(workspaces, ",");
            while (listTokenizer.hasMoreTokens())
            {
               String wsName = listTokenizer.nextToken();
               startChangesListeners.put(new StorageKey(repositoryName, wsName, listenerClassName), null);
            }
         }
      }

      /**
       * Register listeners.
       * 
       * @param repositoryContainer
       * @throws ClassNotFoundException
       */
      public void registerListeners(RepositoryContainer repositoryContainer)
      {
         Iterator<StorageKey> storageKeys = startChangesListeners.keySet().iterator();
         while (storageKeys.hasNext())
         {
            StorageKey sk = storageKeys.next();
            if (sk.getRepositoryName().equals(repositoryContainer.getName()))
            {
               WorkspaceContainer wc = repositoryContainer.getWorkspaceContainer(sk.getWorkspaceName());

               try
               {
                  Class<?> listenerType = Class.forName(sk.getListenerClassName());
                  wc.registerComponentImplementation(listenerType);
                  ItemsPersistenceListener listener =
                     (ItemsPersistenceListener)wc.getComponentInstanceOfType(listenerType);
                  startChangesListeners.put(sk, listener);
               }
               catch (ClassNotFoundException e)
               {
                  log.error("Can not register listener " + e.getMessage());
               }
            }
         }
      }

      /**
       * Cleanup changes.
       * 
       */
      public void cleanup()
      {
         startChangesListeners.clear();
      }

      /**
       * Will be used as key for startChangesListeners.
       * 
       */
      private class StorageKey
      {
         private final String repositoryName;

         private final String workspaceName;

         private final String listenerClassName;

         /**
          * StorageKey constructor.
          * 
          * @param repositoryName
          *          The repository name
          * @param workspaceName
          *          The workspace name
          */
         public StorageKey(String repositoryName, String workspaceName, String listenerClassName)
         {
            this.repositoryName = repositoryName;
            this.workspaceName = workspaceName;
            this.listenerClassName = listenerClassName;
         }

         /**
          * {@inheritDoc}
          */
         @Override
         public boolean equals(Object o)
         {
            StorageKey k = (StorageKey)o;

            return repositoryName.equals(k.repositoryName) && workspaceName.equals(k.workspaceName)
               && listenerClassName.equals(k.listenerClassName);
         }

         /**
          * {@inheritDoc}
          */
         @Override
         public int hashCode()
         {
            return repositoryName.hashCode() ^ workspaceName.hashCode() ^ listenerClassName.hashCode();
         }

         /**
          * Return repository name.
          * 
          * @return The repository name
          */
         public String getRepositoryName()
         {
            return repositoryName;
         }

         /**
          * Return workspace name.
          * 
          * @return The workspace name
          */
         public String getWorkspaceName()
         {
            return workspaceName;
         }

         /**
          * Return listener class name.
          * 
          * @return The listener class name
          */
         public String getListenerClassName()
         {
            return listenerClassName;
         }
      }
   }
}
