001/*
002 *  Copyright 2020 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.odf.schedulable;
017
018import java.io.IOException;
019import java.util.Collections;
020import java.util.Set;
021import java.util.stream.Collectors;
022
023import org.apache.avalon.framework.service.ServiceException;
024import org.apache.avalon.framework.service.ServiceManager;
025import org.apache.cocoon.components.ContextHelper;
026import org.apache.cocoon.environment.Request;
027import org.apache.commons.lang3.StringUtils;
028import org.apache.solr.client.solrj.SolrClient;
029import org.apache.solr.client.solrj.SolrServerException;
030import org.quartz.JobDataMap;
031import org.quartz.JobExecutionContext;
032
033import org.ametys.cms.content.indexing.solr.SolrIndexer;
034import org.ametys.cms.repository.Content;
035import org.ametys.cms.search.content.ContentSearcherFactory;
036import org.ametys.cms.search.query.Query.Operator;
037import org.ametys.cms.search.query.StringQuery;
038import org.ametys.cms.search.solr.SolrClientProvider;
039import org.ametys.core.schedule.progression.ContainerProgressionTracker;
040import org.ametys.odf.ProgramItem;
041import org.ametys.odf.catalog.CatalogsManager;
042import org.ametys.odf.program.ProgramFactory;
043import org.ametys.odf.program.SubProgramFactory;
044import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable;
045import org.ametys.plugins.core.schedule.Scheduler;
046import org.ametys.plugins.repository.RepositoryConstants;
047import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
048
049/**
050 * Scheduler to compute the acquired skills on subprogram/programs from skills hold by their courses.
051 */
052public class ComputeProgramSkillsSchedulable extends AbstractStaticSchedulable
053{
054    /** The key for the catalog */
055    public static final String JOBDATAMAP_CATALOG_KEY = "catalog";
056    
057    /** The catalog manager */
058    protected CatalogsManager _catalogsManager;
059    /** The solr indexer */
060    protected SolrIndexer _solrIndexer;
061    /** The provider for solr client */
062    protected SolrClientProvider _solrClientProvider;
063    /** The content searcher */
064    protected ContentSearcherFactory _contentSearcherFactory;
065    
066    @Override
067    public void service(ServiceManager manager) throws ServiceException
068    {
069        super.service(manager);
070        _catalogsManager = (CatalogsManager) manager.lookup(CatalogsManager.ROLE);
071        _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE);
072        _solrClientProvider = (SolrClientProvider) manager.lookup(SolrClientProvider.ROLE);
073        _contentSearcherFactory = (ContentSearcherFactory) manager.lookup(ContentSearcherFactory.ROLE);
074    }
075    
076    @Override
077    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
078    {
079        doReindex(context, RepositoryConstants.DEFAULT_WORKSPACE);
080    }
081    
082    /**
083     * Find and reindex contents for a given workspace and catalog
084     * @param context The execution context
085     * @param workspaceName the workspace's name
086     */
087    protected void doReindex(JobExecutionContext context, String workspaceName)
088    {
089        long time_0 = System.currentTimeMillis();
090        
091        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
092        String catalogName = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_CATALOG_KEY);
093        
094        Set<String> contentIds = getContentIds(catalogName, workspaceName);
095        indexContents(contentIds, workspaceName);
096        
097        getLogger().debug("Successfully reindex {} programs and subprograms for workspace '{}' and catalog '{}'  in {} ms", contentIds.size(), workspaceName, catalogName, System.currentTimeMillis() - time_0);
098    }
099    
100    /**
101     * Get the id of contents to re-index
102     * @param catalogName The catalog's name. Can be empty or null to get contents of default catalog
103     * @param workspaceName the workspace's name
104     * @return the id of contents to re-index
105     */
106    protected Set<String> getContentIds(String catalogName, String workspaceName)
107    {
108        Request request = ContextHelper.getRequest(_context);
109        String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
110        
111        try
112        {
113            // Force workspace
114            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName);
115            
116            String searchCatalogName = catalogName;
117            if (StringUtils.isEmpty(searchCatalogName))
118            {
119                searchCatalogName = _catalogsManager.getDefaultCatalogName();
120            }
121            
122            Set<String> contentIds = _contentSearcherFactory
123                    .create(ProgramFactory.PROGRAM_CONTENT_TYPE, SubProgramFactory.SUBPROGRAM_CONTENT_TYPE)
124                    .setCheckRights(false)
125                    .search(new StringQuery(ProgramItem.CATALOG, Operator.EQ, searchCatalogName, null))
126                    .stream()
127                    .map(Content::getId)
128                    .collect(Collectors.toSet());
129            
130            getLogger().debug("Found {} contents to reindex for workspace '{}' and catalog '{}'", contentIds.size(), workspaceName, searchCatalogName);
131            
132            return contentIds;
133        }
134        catch (Exception e)
135        {
136            getLogger().error("Unable to get content ids to reindex for updating skills", e);
137            return Collections.EMPTY_SET;
138        }
139        finally
140        {
141            // Restore current workspace
142            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace);
143        }
144    }
145    
146    /**
147     * Index the given contents for a given workspace
148     * @param contentIds the id of contents to index
149     * @param workspaceName the workspace's name
150     */
151    protected void indexContents(Set<String> contentIds, String workspaceName)
152    {
153        SolrClient solrClient = _solrClientProvider.getUpdateClient(workspaceName, false);
154        
155        contentIds.stream().forEach(id -> 
156        {
157            try
158            {
159                _solrIndexer.indexContent(id, workspaceName, false, solrClient);
160            }
161            catch (Exception e)
162            {
163                getLogger().error("Fail to re-index content with id '{}' after computing skills", id);
164            }
165        });
166        
167        try
168        {
169            // Commit all uncommited changes
170            _solrIndexer.commit();
171        }
172        catch (IOException | SolrServerException e)
173        {
174            getLogger().error("Unable to commit changes into Solr index after computing skills", e);
175        }
176    }
177}