001/*
002 *  Copyright 2024 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.content.repair;
017
018import javax.jcr.ItemNotFoundException;
019import javax.jcr.Node;
020import javax.jcr.Repository;
021import javax.jcr.RepositoryException;
022import javax.jcr.Session;
023
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.apache.cocoon.components.ContextHelper;
027import org.apache.cocoon.environment.Request;
028import org.apache.commons.lang3.ArrayUtils;
029import org.apache.solr.client.solrj.SolrClient;
030import org.quartz.JobExecutionContext;
031
032import org.ametys.cms.CmsConstants;
033import org.ametys.cms.content.indexing.solr.SolrIndexer;
034import org.ametys.cms.repository.Content;
035import org.ametys.cms.repository.ContentQueryHelper;
036import org.ametys.cms.repository.DefaultContent;
037import org.ametys.cms.search.solr.SolrClientProvider;
038import org.ametys.core.schedule.Schedulable;
039import org.ametys.core.schedule.progression.ContainerProgressionTracker;
040import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable;
041import org.ametys.plugins.repository.AmetysObjectIterable;
042import org.ametys.plugins.repository.AmetysObjectResolver;
043import org.ametys.plugins.repository.AmetysRepositoryException;
044import org.ametys.plugins.repository.RepositoryConstants;
045import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.web.WebConstants;
048import org.ametys.web.synchronization.SynchronizeComponent;
049
050/**
051 * A {@link Schedulable} job for detecting contents with "Live" label but which are not in workspace live or not indexed and synchronizing/indexing them.
052 */
053public class SynchronizeMissingLiveContentsSchedulable  extends AbstractStaticSchedulable
054{
055    /** The ametys object resolver. */
056    protected AmetysObjectResolver _resolver;
057    /** The repository */
058    protected Repository _repository;
059    /** The synchronize component*/
060    protected SynchronizeComponent _synchronizeComponent;
061    /** The Solr client provider */
062    protected SolrClientProvider _solrClientProvider;
063    /** The Solr indexer */
064    protected SolrIndexer _solrIndexer;
065    
066    @Override
067    public void service(ServiceManager manager) throws ServiceException
068    {
069        super.service(manager);
070        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
071        _repository = (Repository) manager.lookup(Repository.class.getName());
072        _synchronizeComponent = (SynchronizeComponent) manager.lookup(SynchronizeComponent.ROLE);
073        _solrClientProvider = (SolrClientProvider) manager.lookup(SolrClientProvider.ROLE);
074        _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE);
075    }
076    
077    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
078    {
079        Request request = ContextHelper.getRequest(_context);
080        
081        // Retrieve the current workspace.
082        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
083        Session liveSession = null;
084        
085        int errors = 0;
086        int nbOfSynchronized = 0;
087        int nbOfIndexed = 0;
088        try
089        {
090            // Force the workspace.
091            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
092            
093            // Prepare live session to synchronize missing contents
094            liveSession = _repository.login(WebConstants.LIVE_WORKSPACE);
095            
096            // Prepare Solr elements to index missing contents
097            String collectionName = _solrClientProvider.getCollectionName(WebConstants.LIVE_WORKSPACE);
098            SolrClient readClient = _solrClientProvider.getReadClient();
099            
100            // Get contents
101            String query = ContentQueryHelper.getContentXPathQuery(null);
102            AmetysObjectIterable<Content> contents = _resolver.query(query);
103            
104            // Initialize the progression tracker size
105            progressionTracker.setSize(contents.getSize(), new I18nizableText("plugin.web", "PLUGINS_WEB_SCHEDULER_SYNCHRONIZE_MISSING_LIVE_CONTENTS_UNIQUE_STEP_LABEL"));
106            
107            // Iterate on contents
108            for (Content content : contents)
109            {
110                try
111                {
112                    // Check if the content has a "Live" label
113                    if (content instanceof DefaultContent defaultContent)
114                    {
115                        String[] allLabels = defaultContent.getAllLabels();
116                        
117                        if (ArrayUtils.contains(allLabels, CmsConstants.LIVE_LABEL))
118                        {
119                            // Synchronize the content in the "live" workspace if it is missing
120                            if (_trySynchronizingMissingLiveContent(defaultContent, liveSession))
121                            {
122                                nbOfSynchronized++;
123                            }
124                            
125                            // Index the content in the "live" core if it is missing
126                            if (_tryIndexingMissingLiveContent(defaultContent, collectionName, readClient))
127                            {
128                                nbOfIndexed++;
129                            }
130                        }
131                    }
132                }
133                catch (AmetysRepositoryException e)
134                {
135                    getLogger().error("Failed to check content '{}' ({})", content.getTitle(), content.getId(), e);
136                }
137                catch (Exception e)
138                {
139                    getLogger().error("Failed to synchronize or index content '{}' ({})", content.getTitle(), content.getId(), e);
140                    errors++;
141                }
142                
143                progressionTracker.increment();
144            }
145        }
146        finally
147        {
148            if (liveSession != null)
149            {
150                liveSession.logout();
151            }
152            
153            // Restore context
154            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
155            
156            getLogger().info("Number of contents synchronized: {}.", nbOfSynchronized);
157            getLogger().info("Number of contents indexed: {}", nbOfIndexed);
158            
159            if (errors > 0)
160            {
161                throw new Exception(errors + " contents could not be checked or were not synchronized or indexed");
162            }
163        }
164    }
165    
166    private boolean _trySynchronizingMissingLiveContent(DefaultContent defaultContent, Session liveSession) throws RepositoryException
167    {
168        try
169        {
170            Node node = defaultContent.getNode();
171            liveSession.getNodeByIdentifier(node.getIdentifier());
172        }
173        catch (ItemNotFoundException  e)
174        {
175            // If there is a Live label but no corresponding content in live workspace, repair the content by synchronizing it
176            _synchronizeComponent.synchronizeContent(defaultContent, liveSession);
177            liveSession.save();
178            
179            if (getLogger().isDebugEnabled())
180            {
181                getLogger().debug("The content '{}' ({}) was synchronized", defaultContent.getTitle(), defaultContent.getId());
182            }
183            
184            return true;
185        }
186        
187        return false;
188    }
189    
190    private boolean _tryIndexingMissingLiveContent(DefaultContent defaultContent, String collectionName, SolrClient readClient) throws Exception
191    {
192        if (readClient.getById(collectionName, defaultContent.getId()) == null)
193        {
194            // If there is a Live label but no corresponding content indexed, repair the content by indexing it
195            _solrIndexer.indexContent(defaultContent.getId(), WebConstants.LIVE_WORKSPACE, true);
196            
197            if (getLogger().isDebugEnabled())
198            {
199                getLogger().debug("The content '{}' ({}) was synchronized", defaultContent.getTitle(), defaultContent.getId());
200            }
201            
202            return true;
203        }
204        
205        return false;
206    }
207}