/*
 * 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.ext.registry;

import static javax.jcr.ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW;

import org.exoplatform.commons.utils.SecurityHelper;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.container.xml.PropertiesParam;
import org.exoplatform.container.xml.ValueParam;
import org.exoplatform.container.xml.ValuesParam;
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.core.ManageableRepository;
import org.exoplatform.services.jcr.core.nodetype.ExtendedNodeTypeManager;
import org.exoplatform.services.jcr.core.nodetype.NodeTypeDataManager;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.picocontainer.Startable;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jcr.ItemExistsException;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

/**
 * Created by The eXo Platform SAS <br>
 * 
 * Centralized collector for JCR based entities (services, apps, users) It contains info about the
 * whole system, i.e. for all repositories used by system. All operations performed in context of
 * "current" repository, i.e. RepositoryService.getCurrentRepository() Each repository has own
 * Registry storage which is placed in workspace configured in "locations" entry like:
 * {@code
 * <properties-param>
 *   <name>locations</name>
 *   <description>registry locations</description>
 *   <property name="repository1" value="workspace1"/>
 *   <property name="repository2" value="workspace2"/>
 * </properties-param>
 * }
 * The implementation hides storage details from end user
 * 
 * 
 * @author Gennady Azarenkov
 * @version $Id: RegistryService.java 34445 2009-07-24 07:51:18Z dkatayev $
 * @LevelAPI Unsupported
 */
public class RegistryService extends Registry implements Startable
{

   private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.ext.RegistryService");

   protected final static String EXO_REGISTRY_NT = "exo:registry";

   protected final static String EXO_REGISTRYENTRY_NT = "exo:registryEntry";

   protected final static String EXO_REGISTRYGROUP_NT = "exo:registryGroup";

   protected final static String NT_FILE = "registry-nodetypes.xml";

   protected final static String EXO_REGISTRY = "exo:registry";

   protected final static String EXO_REGISTRYENTRY = "exo:registryEntry";

   public final static String EXO_SERVICES = "exo:services";

   public final static String EXO_APPLICATIONS = "exo:applications";

   public final static String EXO_USERS = "exo:users";

   public final static String EXO_GROUPS = "exo:groups";

   protected final Map<String, String> regWorkspaces;

   private HashMap<String, String> appConfigurations = new HashMap<String, String>();

   private String entryLocation;
   
   private final PropertiesParam props;

   protected final RepositoryService repositoryService;

   protected final List<String> mixinNames;

   protected boolean started = false;

   /**
    * @param params
    *          accepts <i>locations</i> properties param and <i>mixin-names</i> values param.
    * @param repositoryService the repository service
    * @throws RepositoryConfigurationException if no {@link InitParams} are provided
    */
   public RegistryService(InitParams params, RepositoryService repositoryService)
      throws RepositoryConfigurationException
   {

      this.repositoryService = repositoryService;
      this.regWorkspaces = new HashMap<String, String>();
      if (params == null)
      {
         throw new RepositoryConfigurationException("Init parameters expected");
      }

      this.props = params.getPropertiesParam("locations");
      if (props == null)
      {
         throw new RepositoryConfigurationException("Property parameters 'locations' expected");
      }

      ValuesParam mixinValues = params.getValuesParam("mixin-names");
      if (mixinValues != null)
      {
         this.mixinNames = params.getValuesParam("mixin-names").getValues();
      }
      else
      {
         this.mixinNames = new ArrayList<String>();
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public RegistryEntry getEntry(final SessionProvider sessionProvider, final String entryPath)
      throws PathNotFoundException, RepositoryException
   {

      final String fullPath = "/" + EXO_REGISTRY + "/" + entryPath;
      Session session = session(sessionProvider, repositoryService.getCurrentRepository());
      try
      {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         session.exportDocumentView(fullPath, out, true, false);
         return RegistryEntry.parse(out.toByteArray());
      }
      catch (IOException e)
      {
         throw new RepositoryException("Can't export node " + fullPath + " to XML representation " + e);
      }
      catch (ParserConfigurationException e)
      {
         throw new RepositoryException("Can't export node " + fullPath + " to XML representation " + e);
      }
      catch (SAXException e)
      {
         throw new RepositoryException("Can't export node " + fullPath + " to XML representation " + e);
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void createEntry(final SessionProvider sessionProvider, final String groupPath, final RegistryEntry entry)
      throws RepositoryException
   {

      final String fullPath = "/" + EXO_REGISTRY + "/" + groupPath;
      try
      {
         checkGroup(sessionProvider, groupPath);
         session(sessionProvider, repositoryService.getCurrentRepository()).getWorkspace().importXML(fullPath,
            entry.getAsInputStream(), IMPORT_UUID_CREATE_NEW);
      }
      catch (IOException ioe)
      {
         throw new RepositoryException("Item " + fullPath + "can't be created " + ioe);
      }
      catch (ItemExistsException iee)
      {
         throw new RepositoryException("Item " + fullPath + "alredy exists " + iee);
      }
      catch (TransformerException te)
      {
         throw new RepositoryException("Can't get XML representation from stream " + te);
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void removeEntry(final SessionProvider sessionProvider, final String entryPath) throws RepositoryException
   {

      Node root = session(sessionProvider, repositoryService.getCurrentRepository()).getRootNode();
      Node node = root.getNode(EXO_REGISTRY + "/" + entryPath);
      Node parent = node.getParent();
      node.remove();
      parent.save();
   }

   /**
    * Re-creates an entry in the group.
    *
    * @param sessionProvider the session provider
    * @throws RepositoryException
    */
   @Override
   public void recreateEntry(final SessionProvider sessionProvider, final String groupPath, final RegistryEntry entry)
      throws RepositoryException
   {

      final String entryRelPath = EXO_REGISTRY + "/" + groupPath + "/" + entry.getName();
      final String parentFullPath = "/" + EXO_REGISTRY + "/" + groupPath;

      try
      {
         Session session = session(sessionProvider, repositoryService.getCurrentRepository());

         // Don't care about concurrency, Session should be dedicated to the Thread, see JCR-765
         // synchronized (session) {
         Node node = session.getRootNode().getNode(entryRelPath);

         // delete existing entry...
         node.remove();

         // create same entry,
         // [PN] no check we need here, as we have deleted this node before
         // checkGroup(sessionProvider, fullParentPath);
         session.importXML(parentFullPath, entry.getAsInputStream(), IMPORT_UUID_CREATE_NEW);

         // save recreated changes
         session.save();
         // }
      }
      catch (IOException ioe)
      {
         throw new RepositoryException("Item " + parentFullPath + "can't be created " + ioe);
      }
      catch (TransformerException te)
      {
         throw new RepositoryException("Can't get XML representation from stream " + te);
      }
   }

   /**
    * Update an entry in the group.
    *
    * @param sessionProvider the session provider
    * @throws RepositoryException
    */
   public void updateEntry(final SessionProvider sessionProvider, final String groupPath, final RegistryEntry entry)
      throws RepositoryException
   {

      final String entryRelPath = EXO_REGISTRY + "/" + groupPath + "/" + entry.getName();
      final String parentFullPath = "/" + EXO_REGISTRY + "/" + groupPath;

      try
      {
         Session session = session(sessionProvider, repositoryService.getCurrentRepository());

         // Don't care about concurrency, Session should be dedicated to the Thread, see JCR-765
         // synchronized (session) {
         try { 
            Node node = session.getRootNode().getNode(entryRelPath);
         // delete existing entry...
            node.remove();
         // create same entry,
            // [PN] no check we need here, as we have deleted this node before
            // checkGroup(sessionProvider, fullParentPath);
            session.importXML(parentFullPath, entry.getAsInputStream(), IMPORT_UUID_CREATE_NEW);
         } catch (PathNotFoundException e) {
            createEntry(sessionProvider, groupPath, entry);
         }
         // save recreated changes
         session.save();
         // }
      }
      catch (IOException ioe)
      {
         throw new RepositoryException("Item " + parentFullPath + "can't be created " + ioe);
      }
      catch (TransformerException te)
      {
         throw new RepositoryException("Can't get XML representation from stream " + te);
      }
   }


   /**
    * {@inheritDoc}
    */
   @Override
   public RegistryNode getRegistry(final SessionProvider sessionProvider) throws RepositoryException
   {

      return new RegistryNode(session(sessionProvider, repositoryService.getCurrentRepository()).getRootNode().getNode(
         EXO_REGISTRY));
   }

   /**
    * @param sessionProvider
    * @param repo
    * @return session
    * @throws RepositoryException
    */
   private Session session(final SessionProvider sessionProvider, final ManageableRepository repo)
      throws RepositoryException
   {

      return sessionProvider.getSession(regWorkspaces.get(repo.getConfiguration().getName()), repo);
   }

   /**
    * Indicates whether the service is started or not
    */
   public boolean isStarted()
   {
      return started;
   }

   /**
    * {@inheritDoc}
    */
   public void start()
   {
      if (!started)
         try
         {
            for (RepositoryEntry repConfiguration : repConfigurations())
            {
               String repName = repConfiguration.getName();
               String wsName = null;
               if (props != null)
               {
                  wsName = props.getProperty(repName);
                  if (wsName == null)
                     wsName = repConfiguration.getDefaultWorkspaceName();
               }
               else
               {
                  wsName = repConfiguration.getDefaultWorkspaceName();
               }
               addRegistryLocation(repName, wsName);
               InputStream xml = SecurityHelper.doPrivilegedAction(new PrivilegedAction<InputStream>()
               {
                  public InputStream run()
                  {
                     return getClass().getResourceAsStream(NT_FILE);
                  }
               });

               try
               {
                  repositoryService.getRepository(repName).getNodeTypeManager()
                     .registerNodeTypes(xml, ExtendedNodeTypeManager.IGNORE_IF_EXISTS, NodeTypeDataManager.TEXT_XML);
               }
               finally
               {
                  try
                  {
                     xml.close();
                  }
                  catch (Exception e)
                  {
                     if (LOG.isTraceEnabled())
                     {
                        LOG.trace("An exception occurred: " + e.getMessage());
                     }
                  }                  
               }
            }
            initStorage(false);

            started = true;
         }
         catch (RepositoryConfigurationException e)
         {
            LOG.error(e.getLocalizedMessage(), e);
         }
         catch (RepositoryException e)
         {
            LOG.error(e.getLocalizedMessage(), e);
         }
      else if (LOG.isDebugEnabled())
         LOG.warn("Registry service already started");
   }

   /**
    * {@inheritDoc}
    */
   public void stop()
   {
   }

   /**
    * Initializes the registry storage
    * @param replace <code>true</code> if we want to re-create the exo:registry node, <code>false</code> otherwise
    * @throws RepositoryConfigurationException if a configuration issue occurs
    * @throws RepositoryException if any error occurs
    */
   public void initStorage(boolean replace) throws RepositoryConfigurationException, RepositoryException
   {
      for (RepositoryEntry repConfiguration : repConfigurations())
      {
         String repName = repConfiguration.getName();
         ManageableRepository rep = repositoryService.getRepository(repName);
         final Session sysSession = rep.getSystemSession(regWorkspaces.get(repName));

         try
         {
            if (sysSession.getRootNode().hasNode(EXO_REGISTRY) && replace)
            {
               sysSession.getRootNode().getNode(EXO_REGISTRY).remove();
            }

            Node rootNode;
            Node servicesNode;
            Node applicationsNode;
            Node usersNode;
            Node groupsNode;

            if (!sysSession.getRootNode().hasNode(EXO_REGISTRY))
            {
               rootNode = sysSession.getRootNode().addNode(EXO_REGISTRY, EXO_REGISTRY_NT);
               servicesNode = rootNode.addNode(EXO_SERVICES, EXO_REGISTRYGROUP_NT);
               applicationsNode = rootNode.addNode(EXO_APPLICATIONS, EXO_REGISTRYGROUP_NT);
               usersNode = rootNode.addNode(EXO_USERS, EXO_REGISTRYGROUP_NT);
               groupsNode = rootNode.addNode(EXO_GROUPS, EXO_REGISTRYGROUP_NT);

               Set<String> appNames = appConfigurations.keySet();
               final String fullPath = "/" + EXO_REGISTRY + "/" + entryLocation;
               for (String appName : appNames)
               {
                  final String xml = appConfigurations.get(appName);
                  try
                  {
                     SecurityHelper.doPrivilegedExceptionAction(new PrivilegedExceptionAction<Void>()
                     {
                        public Void run() throws Exception
                        {
                           DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
                           ByteArrayInputStream stream = new ByteArrayInputStream(xml.getBytes());
                           Document document = builder.parse(stream);
                           RegistryEntry entry = new RegistryEntry(document);
                           sysSession.importXML(fullPath, entry.getAsInputStream(), IMPORT_UUID_CREATE_NEW);
                           return null;
                        }
                     });
                  }
                  catch (PrivilegedActionException pae)
                  {
                     Throwable cause = pae.getCause();
                     if (cause instanceof ParserConfigurationException)
                     {
                        LOG.error(cause.getLocalizedMessage(), cause);
                     }
                     else if (cause instanceof IOException)
                     {
                        LOG.error(cause.getLocalizedMessage(), cause);
                     }
                     else if (cause instanceof SAXException)
                     {
                        LOG.error(cause.getLocalizedMessage(), cause);
                     }
                     else if (cause instanceof TransformerException)
                     {
                        LOG.error(cause.getLocalizedMessage(), cause);
                     }
                     else if (cause instanceof RuntimeException)
                     {
                        throw (RuntimeException)cause;
                     }
                     else
                     {
                        throw new RuntimeException(cause);
                     }
                  }
               }
               sysSession.save();
            }
            else
            {
               rootNode = sysSession.getRootNode().getNode(EXO_REGISTRY);
               servicesNode = rootNode.getNode(EXO_SERVICES);
               applicationsNode = rootNode.getNode(EXO_APPLICATIONS);
               usersNode = rootNode.getNode(EXO_USERS);
               groupsNode = rootNode.getNode(EXO_GROUPS);
            }

            for (String mixin : mixinNames)
            {
               if (rootNode.canAddMixin(mixin))
               {
                  rootNode.addMixin(mixin);
               }

               if (servicesNode.canAddMixin(mixin))
               {
                  servicesNode.addMixin(mixin);
               }

               if (applicationsNode.canAddMixin(mixin))
               {
                  applicationsNode.addMixin(mixin);
               }

               if (usersNode.canAddMixin(mixin))
               {
                  usersNode.addMixin(mixin);
               }

               if (groupsNode.canAddMixin(mixin))
               {
                  groupsNode.addMixin(mixin);
               }
            }

            if (sysSession.hasPendingChanges())
            {
               sysSession.save();
            }
         }
         finally
         {
            sysSession.logout();
         }
      }
   }

   /**
    * Adds a new registry entry
    * @param repositoryName the repository name
    * @param workspaceName the workspace name
    */
   public void addRegistryLocation(String repositoryName, String workspaceName)
   {
      regWorkspaces.put(repositoryName, workspaceName);
   }

   /**
    * @param repositoryName the repository name
    */
   public void removeRegistryLocation(String repositoryName)
   {
      regWorkspaces.remove(repositoryName);
   }

   /**
    * Initializes the registry entry
    * @param groupName the group entry name
    * @param entryName the entry name
    * @throws RepositoryConfigurationException if a configuration issue occurs
    * @throws RepositoryException if any error occurs
    */
   public void initRegistryEntry(String groupName, String entryName) throws RepositoryException,
      RepositoryConfigurationException
   {

      String relPath = EXO_REGISTRY + "/" + groupName + "/" + entryName;
      for (RepositoryEntry repConfiguration : repConfigurations())
      {
         String repName = repConfiguration.getName();
         SessionProvider sysProvider = SessionProvider.createSystemProvider();
         Node root = session(sysProvider, repositoryService.getRepository(repName)).getRootNode();
         if (!root.hasNode(relPath))
         {
            root.addNode(relPath, EXO_REGISTRYENTRY_NT);
            root.save();
         }
         else
         {
            LOG.info("The RegistryEntry " + relPath + "is already initialized on repository " + repName);
         }
         sysProvider.close();
      }
   }

   /**
    * @return repository service
    */
   public RepositoryService getRepositoryService()
   {
      return repositoryService;
   }

   /**
    * Get value of force-xml-configuration param.
    * 
    * @param initParams
    *          The InitParams
    * @return force-xml-configuration value if present and false in other case
    */
   public boolean getForceXMLConfigurationValue(InitParams initParams)
   {
      ValueParam valueParam = initParams.getValueParam("force-xml-configuration");
      return (valueParam != null ? Boolean.valueOf(valueParam.getValue()) : false);
   }

   /**
    * @return repository entries
    */
   private List<RepositoryEntry> repConfigurations()
   {
      return repositoryService.getConfig().getRepositoryConfigurations();
   }

   /**
    * check if group exists and creates one if necessary
    * 
    * @param sessionProvider
    * @param groupPath
    * @throws RepositoryException
    */
   private void checkGroup(final SessionProvider sessionProvider, final String groupPath) throws RepositoryException
   {
      String[] groupNames = groupPath.split("/");
      String prefix = "/" + EXO_REGISTRY;
      Session session = session(sessionProvider, repositoryService.getCurrentRepository());
      for (String name : groupNames)
      {
         String path = prefix + "/" + name;
         try
         {
            Node group = (Node)session.getItem(path);
            if (!group.isNodeType(EXO_REGISTRYGROUP_NT))
               throw new RepositoryException("Node at " + path + " should be  " + EXO_REGISTRYGROUP_NT + " type");
         }
         catch (PathNotFoundException e)
         {
            Node parent = (Node)session.getItem(prefix);
            parent.addNode(name, EXO_REGISTRYGROUP_NT);
            parent.save();
         }
         prefix = path;
      }
   }

   /**
    * Defines the configuration of the service thanks to the provided plugin if and
    * only if the plugin is of type {@link RegistryInitializationEntryPlugin}
    * @param plugin the plugin from which we extract the configuration that is expected
    * to be of type {@link RegistryInitializationEntryPlugin}
    */
   public void addPlugin(ComponentPlugin plugin)
   {
      if (RegistryInitializationEntryPlugin.class.isAssignableFrom(plugin.getClass()))
      {
         RegistryInitializationEntryPlugin registryPlugin = (RegistryInitializationEntryPlugin)plugin;
         appConfigurations = registryPlugin.getAppConfiguration();
         entryLocation = registryPlugin.getLocation();
         if (entryLocation == null)
            entryLocation = EXO_APPLICATIONS;
      }
   }

}
