001/*
002 *  Copyright 2013 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 */
016
017package org.ametys.web.live;
018
019import java.io.PrintWriter;
020import java.io.StringWriter;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.List;
024import java.util.Set;
025
026import javax.jcr.Repository;
027import javax.jcr.Session;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.commons.lang.StringUtils;
034import org.apache.solr.client.solrj.SolrClient;
035
036import org.ametys.cms.content.indexing.solr.SolrIndexer;
037import org.ametys.cms.indexing.IndexingException;
038import org.ametys.cms.indexing.WorkspaceIndexer;
039import org.ametys.cms.repository.Content;
040import org.ametys.cms.search.solr.SolrClientProvider;
041import org.ametys.core.cache.AbstractCacheManager;
042import org.ametys.plugins.repository.AmetysObjectIterable;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.plugins.repository.ChainedAmetysObjectIterable;
045import org.ametys.plugins.repository.CollectionIterable;
046import org.ametys.plugins.repository.collection.AmetysObjectCollection;
047import org.ametys.runtime.plugin.component.AbstractLogEnabled;
048import org.ametys.web.WebConstants;
049import org.ametys.web.cache.CacheHelper;
050import org.ametys.web.indexing.SiteIndexer;
051import org.ametys.web.repository.site.Site;
052import org.ametys.web.repository.site.SiteManager;
053import org.ametys.web.skin.Skin;
054import org.ametys.web.skin.SkinsManager;
055
056/**
057 * Component for rebuild the live workspace, reindex all sitemaps and reset cache.
058 * Provide a way to rebuild the full live workspace, or only the live of a site.
059 */
060public class RebuildLiveComponent extends AbstractLogEnabled implements Component, Serviceable
061{
062    /** Avalon Role. */
063    public static final String ROLE = RebuildLiveComponent.class.getName();
064    
065    private AbstractCacheManager _cacheManager;
066    private Repository _repository;
067    private LivePopulatorExtensionPoint _livePopulatorExtensionPoint;
068    private SitePopulator _sitePopulator;
069    private SiteIndexer _siteIndexer;
070    private WorkspaceIndexer _workspaceIndexer;
071    private SiteManager _siteManager;
072    private SkinsManager _skinsManager;
073    private SolrIndexer _solrIndexer;
074    private SolrClientProvider _solrClientProvider;
075    private AmetysObjectResolver _resolver;
076    
077    @Override
078    public void service(ServiceManager manager) throws ServiceException
079    {
080        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
081        _siteIndexer = (SiteIndexer) manager.lookup(SiteIndexer.ROLE);
082        _repository = (Repository) manager.lookup(Repository.class.getName());
083        _livePopulatorExtensionPoint = (LivePopulatorExtensionPoint)  manager.lookup(LivePopulatorExtensionPoint.ROLE);
084        _sitePopulator = (SitePopulator) manager.lookup(SitePopulator.ROLE);
085        _workspaceIndexer = (WorkspaceIndexer) manager.lookup(WorkspaceIndexer.ROLE);
086        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
087        _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE);
088        _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE);
089        _solrClientProvider = (SolrClientProvider) manager.lookup(SolrClientProvider.ROLE);
090        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
091    }
092    
093    /**
094     * Rebuild live workspace, index all sitemaps and reset cache.
095     * @throws Exception if an error occurs.
096     */
097    public void rebuildLiveWorkspace() throws Exception
098    {
099        List<String> errors = new ArrayList<>();
100        
101        try
102        {
103            getLogger().info("Rebuilding live workspace started");
104
105            long t0 = System.currentTimeMillis();
106        
107            errors.addAll(_populateLiveWorkspace());
108            errors.addAll(_reindexLiveWorkspace());
109            
110            errors.addAll(_clearAllSitesCache());
111            
112            _clearApplicationCache();
113            
114            getLogger().info("Rebuilding live workspace ended in {} ms with {} error(s)", System.currentTimeMillis() - t0, errors.size());
115        }
116        catch (Exception e)
117        {
118            throw new Exception("Failed to rebuild live workspace", e);
119        }
120        
121        if (errors.size() > 0)
122        {
123            throw new Exception("Failed to rebuild live workspace with " + errors.size() + " error(s)\n\n--------------------------------------\n" + StringUtils.join(errors, "\n\n") + "\n--------------------------------------");
124        }
125    }
126    
127    private List<String> _populateLiveWorkspace()
128    {
129        return _populate(_livePopulatorExtensionPoint.getExtensionsIds());
130    }
131
132    private List<String> _populate(Set<String> populatorIds)
133    {
134        List<String> errors = new ArrayList<>();
135
136        Session session = null;
137        Session liveSession = null;
138        
139        try
140        {
141            getLogger().info("Populating live workspace started");
142            
143            long t0 = System.currentTimeMillis();
144            
145            session = _repository.login();
146            liveSession = _repository.login(WebConstants.LIVE_WORKSPACE);
147            
148            // Synchronize other data
149            for (String id : populatorIds)
150            {
151                _populate(id, session, liveSession, errors);
152            }
153            
154            getLogger().info("Populating live workspace ended in {} ms", System.currentTimeMillis() - t0);
155        }
156        catch (Throwable t)
157        {
158            StringWriter sw = new StringWriter();
159            t.printStackTrace(new PrintWriter(sw));
160            
161            errors.add("Failed to populate live workspace\n" + sw.toString());
162            getLogger().error("Failed to populate live workspace", t);
163        }
164        finally
165        {
166            if (session != null)
167            {
168                session.logout();
169            }
170            
171            if (liveSession != null)
172            {
173                liveSession.logout();
174            }
175        }
176        
177        return errors;
178    }
179
180    private void _populate(String populatorId, Session session, Session liveSession, List<String> errors)
181    {
182        getLogger().info("Populating live workspace using LivePopulator {} started", populatorId);
183          
184        try
185        {
186            LivePopulator populator = _livePopulatorExtensionPoint.getExtension(populatorId);
187            
188            errors.addAll(populator.populate(session, liveSession));
189            
190            if (liveSession.hasPendingChanges())
191            {
192                liveSession.save();
193            }
194        }
195        catch (Throwable t)
196        {
197            errors.add("Failed to populate live workspace with populator '" + populatorId + "'\n" + t.getMessage());
198            getLogger().error("Failed to populate live workspace with populator '" + populatorId + "'", t);
199        }
200        
201        getLogger().info("Populating live workspace using LivePopulator {} ended", populatorId);
202    }
203    
204    private List<String> _reindexLiveWorkspace()
205    {
206        List<String> errors = new ArrayList<>();
207        
208        try
209        {
210            getLogger().info("Indexing Live workspace started");
211            
212            long t0 = System.currentTimeMillis();
213            
214            // Index the whole live workspace and not just every site.
215            _workspaceIndexer.index(WebConstants.LIVE_WORKSPACE);
216            
217            getLogger().info("Indexing live workspace ended in {} ms", System.currentTimeMillis() - t0);
218        }
219        catch (IndexingException e)
220        {
221            errors.add("Failed to index live workspace\n" + e.getMessage());
222            getLogger().error("Failed to index live workspace", e);
223        }
224        
225        return errors;
226    }
227    
228    private List<String> _clearAllSitesCache()
229    {
230        List<String> errors = new ArrayList<>();
231        try (AmetysObjectIterable<Site> siteIterable = _siteManager.getSites())
232        {
233            for (Site site : siteIterable)
234            {
235                try
236                {
237                    // TODO Clear only the live workspace?
238                    _clearSiteCache(site);
239                }
240                catch (Throwable t)
241                {
242                    errors.add("Failed to clear cache of site '" + site.getName() + "'\n" + t.getMessage());
243                    getLogger().error("Failed to clear cache of site '" + site.getName() + "'", t);
244                }
245            }
246        }
247        return errors;
248    }
249
250    private void _clearSiteCache(Site site) throws Exception
251    {
252        String siteName = site.getName();
253        
254        getLogger().info("Clearing cache for site {} started", siteName);
255        
256        long t0 = System.currentTimeMillis();
257        
258        CacheHelper.invalidateCache(site, getLogger());
259        
260        getLogger().info("Clearing cache for site '{}' ended in {} ms", siteName, System.currentTimeMillis() - t0);
261    }
262    
263    private void _clearApplicationCache()
264    {
265        _cacheManager.resetAllMemoryCaches();
266    }
267
268    /**
269     * Rebuild the live of a site, index all sitemaps and reset cache.
270     * @param site The site to be rebuilt
271     * @throws Exception if an error occurs.
272     */
273    public void rebuildLive(Site site) throws Exception
274    {
275        String siteName = "";
276        try
277        {
278            siteName = site.getName();
279            
280            Skin skin = _skinsManager.getSkin(site.getSkinId());
281            assert skin != null;
282            
283            getLogger().info("Rebuilding site {} started", siteName);
284
285            long t0 = System.currentTimeMillis();
286            
287            _populateSite(site, skin);
288            _reindexSite(site);
289            _clearSiteCache(site);
290            _clearApplicationCache();
291            
292            getLogger().info("Rebuilding site {} ended in {} ms", siteName, System.currentTimeMillis() - t0);
293        }
294        catch (Exception e)
295        {
296            throw new Exception("Failed to rebuild live workspace for site '" + siteName + "'", e);
297        }
298    }
299    
300    private void _populateSite(Site site, Skin skin) throws Exception
301    {
302        String siteName = site.getName();
303
304        getLogger().info("Populating site {} started", siteName);
305        
306        long t0 = System.currentTimeMillis();
307        
308        _sitePopulator.populate(site, skin);
309        
310        getLogger().info("Populating site {} ended in {} ms", siteName, System.currentTimeMillis() - t0);
311    }
312    
313    private void _reindexSite(Site site) throws Exception
314    {
315        String siteName = site.getName();
316        
317        getLogger().info("Indexing site '{}' started", siteName);
318        
319        long t0 = System.currentTimeMillis();
320        
321        // Reindex the site in the live workspace.
322        _siteIndexer.indexSite(site.getName(), WebConstants.LIVE_WORKSPACE);
323        
324        getLogger().info("Indexing of site '{}' ended in {} ms", siteName, System.currentTimeMillis() - t0);
325    }
326    
327    /**
328     * Rebuild the live workspace for contents without site.
329     * 
330     * More precisely, this method will synchronize and reindex all contents located
331     * at '/ametys:root/ametys:contents' and '/ametys:root/ametys:plugins/&lt;pluginName>/ametys:contents'
332     * @throws Exception if an error occurred
333     */
334    public void rebuildContentsWithoutSiteWorkspace() throws Exception
335    {
336        List<String> errors = new ArrayList<>();
337        
338        try
339        {
340            getLogger().info("Rebuilding live workspace started");
341
342            long t0 = System.currentTimeMillis();
343        
344            // call ContentsLivePopulator and PluginsLivePopulator
345            errors.addAll(_populate(Set.of(ContentsLivePopulator.class.getName(), PluginsLivePopulator.class.getName())));
346            // reindex impacted contents
347            errors.addAll(_reindexContentWithoutSiteLiveWorkspace());
348            
349            // Clear every site's cache.
350            errors.addAll(_clearAllSitesCache());
351            
352            // Clear application cache to make sur that no outdated info is kept in cache
353            _clearApplicationCache();
354            
355            getLogger().info("Rebuilding live workspace ended in {} ms with {} error(s)", System.currentTimeMillis() - t0, errors.size());
356        }
357        catch (Exception e)
358        {
359            throw new Exception("Failed to rebuild live workspace", e);
360        }
361        
362        if (errors.size() > 0)
363        {
364            throw new Exception("Failed to rebuild live workspace with " + errors.size() + " error(s)\n\n--------------------------------------\n" + StringUtils.join(errors, "\n\n") + "\n--------------------------------------");
365        }
366    }
367
368    private Collection< ? extends String> _reindexContentWithoutSiteLiveWorkspace() throws Exception
369    {
370        List<String> errors = new ArrayList<>();
371        
372        try
373        {
374            getLogger().info("Indexing contents without site live workspace started");
375            
376            long t0 = System.currentTimeMillis();
377            
378            // Do not close the client, this is the provider responsibility
379            SolrClient solrClient = _solrClientProvider.getUpdateClient(WebConstants.LIVE_WORKSPACE, false);
380            try (AmetysObjectIterable<AmetysObjectCollection> roots = _getContentsWithoutSiteRootNodes())
381            {
382                for (AmetysObjectCollection root : roots)
383                {
384                    getLogger().info("Indexing live contents at path {}", root.getPath());
385                    try (AmetysObjectIterable<Content> contents = root.getChildren())
386                    {
387                        _solrIndexer.indexContents(contents, WebConstants.LIVE_WORKSPACE, true, solrClient);
388                        _solrIndexer.commit(WebConstants.LIVE_WORKSPACE, solrClient);
389                    }
390                    getLogger().info("Indexing live contents at path {} ended", root.getPath());
391                }
392            }
393            
394            getLogger().info("Indexing contents witout site live workspace ended in {} ms", System.currentTimeMillis() - t0);
395        }
396        catch (IndexingException e)
397        {
398            errors.add("Failed to index contents without site live workspace\n" + e.getMessage());
399            getLogger().error("Failed to index contents without site live workspace", e);
400        }
401        
402        return errors;
403    }
404
405    private AmetysObjectIterable<AmetysObjectCollection> _getContentsWithoutSiteRootNodes()
406    {
407        // contents nodes in plugins
408        AmetysObjectIterable<AmetysObjectCollection> roots = _resolver.<AmetysObjectCollection>query("/jcr:root/ametys:root/ametys:plugins/*/ametys:contents");
409        // contents node at ametys root
410        AmetysObjectIterable<AmetysObjectCollection> rootIterable = new CollectionIterable<>(List.of(_resolver.resolveByPath("/ametys:contents")));
411        
412        // Return an iterable so that the consumer is responsible for closing
413        return new ChainedAmetysObjectIterable<>(List.of(rootIterable, roots));
414    }
415}