001/*
002 *  Copyright 2010 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.web.live;
017
018import java.util.concurrent.locks.Lock;
019
020import javax.jcr.Node;
021import javax.jcr.PathNotFoundException;
022import javax.jcr.Repository;
023import javax.jcr.RepositoryException;
024import javax.jcr.Session;
025import javax.jcr.query.Query;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.collections.PredicateUtils;
032import org.apache.jackrabbit.util.ISO9075;
033
034import org.ametys.cms.repository.Content;
035import org.ametys.cms.support.AmetysPredicateUtils;
036import org.ametys.core.schedule.progression.ContainerProgressionTracker;
037import org.ametys.core.schedule.progression.ProgressionTrackerFactory;
038import org.ametys.core.schedule.progression.SimpleProgressionTracker;
039import org.ametys.plugins.repository.AmetysObjectIterable;
040import org.ametys.plugins.repository.RepositoryConstants;
041import org.ametys.runtime.i18n.I18nizableText;
042import org.ametys.runtime.plugin.component.AbstractLogEnabled;
043import org.ametys.web.WebConstants;
044import org.ametys.web.repository.site.Site;
045import org.ametys.web.repository.sitemap.Sitemap;
046import org.ametys.web.skin.Skin;
047import org.ametys.web.synchronization.SynchronizeComponent;
048
049/**
050 * Component for populating the live workspace site by site.
051 */
052public class SitePopulator extends AbstractLogEnabled implements Serviceable, Component
053{
054    /** Avalon role. */
055    public static final String ROLE = SitePopulator.class.getName();
056    
057    private Repository _repository;
058    private LiveAccessManager _liveAccessManager;
059
060    private SynchronizeComponent _synchronizeComponent;
061    
062    @Override
063    public void service(ServiceManager manager) throws ServiceException
064    {
065        _repository = (Repository) manager.lookup(Repository.class.getName());
066        _liveAccessManager = (LiveAccessManager) manager.lookup(LiveAccessManager.ROLE);
067        _synchronizeComponent = (SynchronizeComponent) manager.lookup(SynchronizeComponent.ROLE);
068    }
069
070    /**
071     * Populates the live workspace.
072     * @param site the site to populate.
073     * @param skin the site skin model
074     * @throws Exception if an error occurs.
075     */
076    public void populate(Site site, Skin skin) throws Exception
077    {
078        populate(site, skin, ProgressionTrackerFactory.createContainerProgressionTracker(new I18nizableText("Populate site '" + site.getName() + "'"), getLogger()));
079    }
080    
081    /**
082     * Populates the live workspace.
083     * @param site the site to populate.
084     * @param skin the site skin model
085     * @param progressionTracker The progression tracker
086     * @throws Exception if an error occurs.
087     */
088    public void populate(Site site, Skin skin, ContainerProgressionTracker progressionTracker) throws Exception
089    {
090        Lock lock = _liveAccessManager.getLiveWriteLock();
091        
092        // Ensure write access to the live workspace is exclusive
093        lock.lock();
094
095        try
096        {
097            if (getLogger().isDebugEnabled())
098            {
099                getLogger().debug("Live write lock has been acquired: " + lock);
100            }
101            
102            Session liveSession = null; 
103                
104            try
105            {
106                // Open a session to the workspace live
107                liveSession = _repository.login(WebConstants.LIVE_WORKSPACE);
108                
109                _populate(liveSession, site, skin, progressionTracker);
110                
111                liveSession.save();
112            }
113            finally
114            {
115                if (liveSession != null)
116                {
117                    liveSession.logout();
118                }
119            }
120        }
121        finally
122        {
123            lock.unlock();
124        }
125    }
126
127    private void _createProgressionTrackerStepsForPopulate(Skin skin, ContainerProgressionTracker progressionTracker)
128    {
129        progressionTracker.addSimpleStep("contents", new I18nizableText("plugin.web", "PLUGINS_WEB_SCHEDULABLE_REBUILD_LIVE_CONTENTS_SYNC_STEP_LABEL"));
130
131        // Clone sitemaps
132        if (skin != null)
133        {
134            progressionTracker.addSimpleStep("sitemaps", new I18nizableText("plugin.web", "PLUGINS_WEB_SCHEDULABLE_REBUILD_LIVE_CLONE_SITEMAPS_STEP_LABEL"));
135        }
136    }
137    
138    /**
139     * Populates a site
140     * @param liveSession the live workspace session.
141     * @param site the site to populate.
142     * @param skin the site skin model
143     * @param progressionTracker The progression tracker
144     * @throws RepositoryException if an error occurs.
145     */
146    protected void _populate(Session liveSession, Site site, Skin skin, ContainerProgressionTracker progressionTracker) throws RepositoryException
147    {
148        _createProgressionTrackerStepsForPopulate(skin, progressionTracker);
149        
150        Node siteNode = site.getNode();
151        String siteNodePath = siteNode.getPath().substring(1);
152        Node liveRootNode = liveSession.getRootNode();
153        
154        Node clonedSiteNode = _cloneSiteNode(liveSession, site, siteNode, siteNodePath, liveRootNode);
155        
156        // Clone only properties
157        _cloneProperties(siteNode, clonedSiteNode);
158
159        // contents
160        _cloneContents(liveSession, site, siteNode, clonedSiteNode, progressionTracker.getStep("contents"));
161
162        // Clone sitemaps
163        if (skin != null)
164        {
165            // The skin can be null if site was created but not yet configured
166            _cloneSitemaps(liveSession, site, skin, siteNode, clonedSiteNode, progressionTracker.getStep("sitemaps"));
167        }
168    }
169
170    /**
171     * Clone a site node in the live workspace.
172     * @param liveSession the live session.
173     * @param site the source site.
174     * @param siteNode the site node to clone.
175     * @param siteNodePath the site node path.
176     * @param liveRootNode the live root node.
177     * @return the cloned site node.
178     * @throws RepositoryException if an error occurs.
179     */
180    private Node _cloneSiteNode(Session liveSession, Site site, Node siteNode, String siteNodePath, Node liveRootNode) throws RepositoryException
181    {
182        Node clonedSiteNode = null;
183        if (liveRootNode.hasNode(siteNodePath))
184        {
185            clonedSiteNode = liveRootNode.getNode(siteNodePath);
186        }
187        else
188        {
189            getLogger().debug("Creating site '{}' in live worspace", site.getName());
190            
191            Node sitesNode = _synchronizeComponent.cloneAncestorsAndPreserveUUID(siteNode, liveSession);
192            clonedSiteNode = _synchronizeComponent.addNodeWithUUID(siteNode, sitesNode, siteNode.getName());
193        }
194        
195        // Remove existing resources // or auto-created node with wrong UUID
196        if (clonedSiteNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources"))
197        {
198            getLogger().debug("Removing resources' node for site '{}'", site.getName());
199            clonedSiteNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources").remove();
200        }
201        
202        // Remove plugins // or auto-created node with wrong UUID
203        if (clonedSiteNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins"))
204        {
205            getLogger().debug("Removing plugins' node for site '{}'", site.getName());
206            clonedSiteNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins").remove();
207        }
208        
209        // Remove existing sitemaps // or auto-created node with wrong UUID
210        if (clonedSiteNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sitemaps"))
211        {
212            getLogger().debug("Removing sitemaps' node for site '{}'", site.getName());
213            clonedSiteNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sitemaps").remove();
214        }
215        
216        // Remove existing contents // or auto-created node with wrong UUID
217        if (clonedSiteNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":contents"))
218        {
219            getLogger().debug("Removing contents' node for site '{}'", site.getName());
220            clonedSiteNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":contents").remove();
221        }
222        
223        return clonedSiteNode;
224    }
225
226    /**
227     * Copy all properties from a site node to another.
228     * @param siteNode the site JCR node
229     * @param clonedSiteNode the JCR node of site to copy
230     * @throws RepositoryException if an error occurred during copy
231     */
232    private void _cloneProperties(Node siteNode, Node clonedSiteNode) throws RepositoryException
233    {
234        // Remove then clone all properties and others nodes prefixed by 'ametys:'
235        _synchronizeComponent.cloneNodeAndPreserveUUID(siteNode, clonedSiteNode, PredicateUtils.truePredicate(), AmetysPredicateUtils.ametysAttributePredicate());
236
237        // resources
238        Node resourcesNode = siteNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources");
239        Node clonedResourcesNode = _synchronizeComponent.addNodeWithUUID(resourcesNode, clonedSiteNode, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources");
240        _synchronizeComponent.cloneNodeAndPreserveUUID(resourcesNode, clonedResourcesNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate());
241
242        // plugins
243        Node pluginsNode = siteNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins");
244        Node clonedPluginsNode = null;
245        if (!clonedSiteNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins"))
246        {
247            clonedPluginsNode = _synchronizeComponent.addNodeWithUUID(pluginsNode, clonedSiteNode, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins");
248        }
249        else
250        {
251            clonedPluginsNode = clonedSiteNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins");
252        }
253        _synchronizeComponent.cloneNodeAndPreserveUUID(pluginsNode, clonedPluginsNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate());
254    }
255    
256    private void _cloneContents(Session liveSession, Site site, Node siteNode, Node clonedSiteNode, SimpleProgressionTracker progressionTracker) throws RepositoryException
257    {
258        // The contents root has to be cloned even if it doesn't contain any content.
259        Node contentsNode = siteNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":contents");
260        if (!clonedSiteNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":contents"))
261        {
262            _synchronizeComponent.addNodeWithUUID(contentsNode, clonedSiteNode, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":contents");
263            progressionTracker.setSize(0);
264        }
265
266        _synchronizeComponent.synchronizeACL(contentsNode, liveSession);
267        
268        // Clone the contents.
269        try (AmetysObjectIterable<Content> contents = site.getContents())
270        {
271            if (getLogger().isInfoEnabled())
272            {
273                getLogger().info("Synchronizing " + contents.getSize() + " contents");
274            }
275            
276            _synchronizeComponent.synchronizeContents(contents, liveSession, progressionTracker);
277        }
278    }
279    
280    @SuppressWarnings("deprecation")
281    private void _cloneSitemaps(Session liveSession, Site site, Skin skin, Node siteNode, Node clonedSiteNode, SimpleProgressionTracker progressionTracker) throws PathNotFoundException, RepositoryException
282    {
283        // Count pages
284        long size = siteNode.getSession().getWorkspace().getQueryManager().createQuery("/jcr:root" + ISO9075.encodePath(siteNode.getPath()) + "/ametys-internal:sitemaps//element(*, ametys:page)", Query.XPATH).execute().getNodes().getSize();
285        progressionTracker.setSize(size);
286        
287        Node sitemapsNode = siteNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sitemaps");
288        Node clonedSitemapsNode = null;
289        if (!clonedSiteNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sitemaps"))
290        {
291            clonedSitemapsNode = _synchronizeComponent.addNodeWithUUID(sitemapsNode, clonedSiteNode, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sitemaps");
292        }
293        else
294        {
295            clonedSitemapsNode = clonedSiteNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sitemaps");
296        }
297        
298        for (Sitemap sitemap : site.getSitemaps())
299        {
300            _populate(liveSession, clonedSitemapsNode, sitemap, skin, progressionTracker);
301        }
302    }
303
304    /**
305     * Populates a sitemap.
306     * @param liveSession The live session
307     * @param clonedSitemapsNode the cloned sitemaps node.
308     * @param sitemap the sitemap to populate.
309     * @param skin the site skin model
310     * @param progressionTracker The tracker for pages
311     * @throws RepositoryException if an error occurs.
312     */
313    protected void _populate(Session liveSession, Node clonedSitemapsNode, Sitemap sitemap, Skin skin, SimpleProgressionTracker progressionTracker) throws RepositoryException
314    {
315        if (getLogger().isDebugEnabled())
316        {
317            getLogger().debug("Creating sitemap: " + sitemap);
318        }
319
320        Node sitemapNode = sitemap.getNode();
321        Node clonedSitemapNode = _synchronizeComponent.addNodeWithUUID(sitemapNode, clonedSitemapsNode, sitemapNode.getName());
322        
323        _synchronizeComponent.synchronizeACL(sitemapNode, liveSession);
324            
325        // Clone only properties
326        _synchronizeComponent.cloneNodeAndPreserveUUID(sitemapNode, clonedSitemapNode, PredicateUtils.truePredicate(), PredicateUtils.falsePredicate());
327        
328        // Clone zones
329        _synchronizeComponent.cloneZones(sitemapNode, sitemap, clonedSitemapNode);
330        
331        // Clone eligible pages 
332        _synchronizeComponent.cloneEligibleChildrenPages(sitemap, skin, clonedSitemapsNode.getSession(), progressionTracker);
333    }
334}