Index: main/plugin-user-directory/plugin.xml
===================================================================
--- main/plugin-user-directory/plugin.xml	(revision 42775)
+++ main/plugin-user-directory/plugin.xml	(working copy)
@@ -928,4 +928,12 @@
                            class="org.ametys.plugins.userdirectory.cachepolicy.UserDirectoryVirtualPagesCachePolicy"/>
             </extensions>
         </feature>
+        
+        <feature name="user.directory.livepopulator">
+            <extensions>
+                <extension point="org.ametys.web.live.LivePopulatorExtensionPoint"
+                           id="org.ametys.plugins.userdirectory.live.UserDirectoryLivePopulator"
+                           class="org.ametys.plugins.userdirectory.live.UserDirectoryLivePopulator" />
+            </extensions>
+        </feature>
 </plugin>
Index: main/plugin-user-directory/src/org/ametys/plugins/userdirectory/live/UserDirectoryLivePopulator.java
===================================================================
--- main/plugin-user-directory/src/org/ametys/plugins/userdirectory/live/UserDirectoryLivePopulator.java	(revision 0)
+++ main/plugin-user-directory/src/org/ametys/plugins/userdirectory/live/UserDirectoryLivePopulator.java	(revision 0)
@@ -0,0 +1,180 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.ametys.plugins.userdirectory.live;
+
+import java.util.Arrays;
+import java.util.concurrent.locks.Lock;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.version.VersionHistory;
+
+import org.apache.avalon.framework.logger.AbstractLogEnabled;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.avalon.framework.service.Serviceable;
+import org.apache.commons.collections.PredicateUtils;
+
+import org.ametys.cms.repository.Content;
+import org.ametys.plugins.repository.AmetysObjectIterable;
+import org.ametys.plugins.repository.AmetysObjectResolver;
+import org.ametys.plugins.repository.RepositoryConstants;
+import org.ametys.plugins.repository.TraversableAmetysObject;
+import org.ametys.plugins.repository.jcr.JCRAmetysObject;
+import org.ametys.web.WebConstants;
+import org.ametys.web.live.LiveAccessManager;
+import org.ametys.web.live.LivePopulator;
+import org.ametys.web.repository.content.jcr.TaggableAmetysObjectHelper;
+import org.ametys.web.synchronization.SynchronizeComponent;
+
+/**
+ * {@link LivePopulator} for synchronizing user directory contents.
+ */
+public class UserDirectoryLivePopulator extends AbstractLogEnabled implements LivePopulator, Serviceable
+{
+    private AmetysObjectResolver _resolver;
+    private SynchronizeComponent _synchronizeCmp;
+    private LiveAccessManager _liveAccessManager;
+    
+    @Override
+    public void service(ServiceManager manager) throws ServiceException
+    {
+        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
+        _synchronizeCmp = (SynchronizeComponent) manager.lookup(SynchronizeComponent.ROLE);
+        _liveAccessManager = (LiveAccessManager) manager.lookup(LiveAccessManager.ROLE);
+    }
+    
+    @Override
+    public void populate(Session session, Session liveSession) throws Exception
+    {
+        Lock lock = _liveAccessManager.getLiveWriteLock();
+        
+        // Ensure write access to the live workspace is exclusive.
+        lock.lock();
+        
+        try
+        {
+            if (getLogger().isDebugEnabled())
+            {
+                getLogger().debug("Live write lock has been acquired: " + lock);
+            }
+            
+            populateUserDirectoryLive(liveSession);
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * Populate the live workspace with user directory data.
+     * @param liveSession a session on the live workspace.
+     * @throws RepositoryException if an error occurs.
+     */
+    public void populateUserDirectoryLive(Session liveSession) throws RepositoryException
+    {
+        if (getLogger().isInfoEnabled())
+        {
+            getLogger().info("Removing user directory root...");
+        }
+        
+        if (liveSession.getRootNode().hasNode("ametys:root/ametys:user-directory"))
+        {
+            liveSession.getRootNode().getNode("ametys:root/ametys:user-directory").remove();
+            liveSession.save();
+        }
+        
+        if (getLogger().isInfoEnabled())
+        {
+            getLogger().info("User directory root removed.");
+        }
+
+        TraversableAmetysObject root = _resolver.resolve("/");
+        TraversableAmetysObject userDirectoryRoot = root.getChild("ametys:user-directory/ametys:contents");
+        
+        AmetysObjectIterable<Content> contents = userDirectoryRoot.getChildren();
+            
+        if (getLogger().isInfoEnabled())
+        {
+            getLogger().info("Synchronizing " + contents.getSize() + " user directory contents...");
+        }
+        
+        // contents
+        for (Content content : contents)
+        {
+            if (getLogger().isDebugEnabled())
+            {
+                getLogger().debug("Synchronizing user directory content " + content.getId());
+            }
+            
+            if (content instanceof JCRAmetysObject)
+            {
+                Node contentNode = ((JCRAmetysObject) content).getNode();
+                
+                VersionHistory history = contentNode.getSession().getWorkspace().getVersionManager().getVersionHistory(contentNode.getPath());
+                
+                if (Arrays.asList(history.getVersionLabels()).contains(WebConstants.LIVE_LABEL))
+                {
+                    // content is valid, synchronize it
+                    Node clonedContentParentNode = _synchronizeCmp.cloneAncestorsAndPreserveUUID(contentNode, liveSession);
+                    
+                    Node validatedContentNode = history.getVersionByLabel(WebConstants.LIVE_LABEL).getNode("jcr:frozenNode");
+                    Node clonedNode = _synchronizeCmp.addNodeWithUUID(validatedContentNode, clonedContentParentNode, contentNode.getName());
+                 
+                    _synchronizeCmp.cloneNodeAndPreserveUUID(validatedContentNode, clonedNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate());
+                    
+                    // Clone tags
+                    String tagProperty = TaggableAmetysObjectHelper.METADATA_TAGS;
+                    
+                    if (contentNode.hasProperty(tagProperty))
+                    {
+                        clonedNode.setProperty(tagProperty, contentNode.getProperty(tagProperty).getValues());
+                    }
+                    
+                    // Clone unversioned metadata
+                    String unversionedNodeName = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":unversioned";
+                    Node unversionedNode = null;
+                    
+                    if (clonedNode.hasNode(unversionedNodeName))
+                    {
+                        unversionedNode = clonedNode.getNode(unversionedNodeName);
+                    }
+                    else
+                    {
+                        unversionedNode = clonedNode.addNode(unversionedNodeName, "ametys:compositeMetadata");
+                    }
+                    
+                    _synchronizeCmp.cloneNodeAndPreserveUUID(contentNode.getNode(unversionedNodeName), unversionedNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate());
+                }
+                
+                liveSession.save();
+            }
+            
+            if (getLogger().isDebugEnabled())
+            {
+                getLogger().debug("User directory content synchronized " + content.getId());
+            }
+        }
+        
+        if (getLogger().isInfoEnabled())
+        {
+            getLogger().info("User directory contents synchronized.");
+        }
+    }
+}
