/*
 * 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.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.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.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
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:
 * <properties-param> <name>locations</name> <description>registry locations</description> <property
 * name="repository1" value="workspace1"/> <property name="repository2" value="workspace2"/> The
 * implementation hides storage details from end user
 * 
 * 
 * @author Gennady Azarenkov
 * @version $Id: RegistryService.java 34445 2009-07-24 07:51:18Z dkatayev $
 */
public class RegistryService extends Registry implements Startable
{

   private static 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;

   // TODO temporary flag to have start() run once
   protected boolean started = false;

   /**
    * @param params
    *          accepts "locations" properties param
    * @param repositoryService
    * @throws RepositoryConfigurationException
    * @throws RepositoryException
    * @throws FileNotFoundException
    */
   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");
   }

   /**
    * {@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();
   }

   /**
    * {@inheritDoc}
    */
   @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);
      }
   }
   
   
   
   /**
    * {@inheritDoc}
    */
   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);
   }

   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);
               }
               finally
               {
                  try
                  {
                     xml.close();
                  }
                  catch (Exception e)
                  {
                     //ignore me
                  }                  
               }
            }
            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()
   {
   }

   /**
    * @param replace
    * @throws RepositoryConfigurationException
    * @throws RepositoryException
    */
   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));

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

         if (!sysSession.getRootNode().hasNode(EXO_REGISTRY))
         {
            Node rootNode = sysSession.getRootNode().addNode(EXO_REGISTRY, EXO_REGISTRY_NT);
            rootNode.addNode(EXO_SERVICES, EXO_REGISTRYGROUP_NT);
            rootNode.addNode(EXO_APPLICATIONS, EXO_REGISTRYGROUP_NT);
            rootNode.addNode(EXO_USERS, EXO_REGISTRYGROUP_NT);
            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();
         }
         sysSession.logout();
      }
   }

   /**
    * @param repositoryName
    * @param workspaceName
    */
   public void addRegistryLocation(String repositoryName, String workspaceName)
   {
      regWorkspaces.put(repositoryName, workspaceName);
   }

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

   /**
    * @param sessionProvider
    * @param groupName
    * @param entryName
    * @param repositoryName
    * @return
    * @throws RepositoryException
    */
   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;
      }
   }

   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;
      }
   }

}
