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.plugins.odfsync.apogee.scc;
017
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Optional;
022import java.util.Set;
023
024import org.apache.avalon.framework.activity.Initializable;
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.context.Context;
027import org.apache.avalon.framework.context.ContextException;
028import org.apache.avalon.framework.context.Contextualizable;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.cocoon.components.ContextHelper;
033import org.apache.cocoon.environment.Request;
034import org.apache.commons.lang3.StringUtils;
035import org.slf4j.Logger;
036
037import org.ametys.cms.repository.Content;
038import org.ametys.cms.repository.ContentDAO;
039import org.ametys.cms.repository.ModifiableContent;
040import org.ametys.odf.ODFHelper;
041import org.ametys.odf.ProgramItem;
042import org.ametys.odf.catalog.CatalogsManager;
043import org.ametys.odf.course.Course;
044import org.ametys.odf.coursepart.CoursePart;
045import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionDAO;
046import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionHelper;
047import org.ametys.runtime.config.Config;
048
049/**
050 * Helper for Apogee Synchronizable Contents Collections.
051 */
052public class ApogeeSynchronizableContentsCollectionHelper implements Serviceable, Component, Contextualizable, Initializable
053{
054    /** The Avalon Role */
055    public static final String ROLE = ApogeeSynchronizableContentsCollectionHelper.class.getName();
056 
057    /** Request attribute name to store handled contents during import or synchronization. */
058    public static final String HANDLED_CONTENTS = AbstractApogeeSynchronizableContentsCollection.class.getName() + "$handledContents";
059
060    /** Request attribute name to store catalog during import or synchronization. */
061    public static final String CATALOG = AbstractApogeeSynchronizableContentsCollection.class.getName() + "$catalog";
062    
063    /** Request attribute name to store language during import or synchronization. */
064    public static final String LANG = AbstractApogeeSynchronizableContentsCollection.class.getName() + "$lang";
065    
066    /** SCC DAO */
067    protected SynchronizableContentsCollectionHelper _sccHelper;
068    
069    /** The ODF Helper */
070    protected ODFHelper _odfHelper;
071    
072    /** SCC DAO */
073    protected SynchronizableContentsCollectionDAO _sccDAO;
074    
075    /** Content DAO */
076    protected ContentDAO _contentDAO;
077    
078    /** The catalogs manager */
079    protected CatalogsManager _catalogsManager;
080    
081    /** Context */
082    protected Context _context;
083    
084    /** Default language configured for ODF */
085    protected String _odfLang;
086    
087    
088    @Override
089    public void service(ServiceManager smanager) throws ServiceException
090    {
091        _odfHelper = (ODFHelper) smanager.lookup(ODFHelper.ROLE);
092        _sccHelper = (SynchronizableContentsCollectionHelper) smanager.lookup(SynchronizableContentsCollectionHelper.ROLE);
093        _sccDAO = (SynchronizableContentsCollectionDAO) smanager.lookup(SynchronizableContentsCollectionDAO.ROLE);
094        _contentDAO = (ContentDAO) smanager.lookup(ContentDAO.ROLE);
095        _catalogsManager = (CatalogsManager) smanager.lookup(CatalogsManager.ROLE);
096    }
097    
098    public void contextualize(Context context) throws ContextException
099    {
100        _context = context;
101    }
102    
103    public void initialize() throws Exception
104    {
105        _odfLang = Config.getInstance().getValue("odf.programs.lang");
106    }
107    
108    /**
109     * Synchronize the content or its children if the content has no Apogee SCC
110     * @param content the content
111     * @param logger the logger
112     */
113    public void synchronizeContent(ModifiableContent content, Logger logger)
114    {
115        boolean handled = false;
116        
117        AbstractApogeeSynchronizableContentsCollection scc = getContentSCC(content, logger).orElse(null);
118        // The content has a SCC, so synchronize it
119        if (scc != null)
120        {
121            try
122            {
123                Map<String, Object> searchParams = new HashMap<>();
124                String idField = scc.getIdField();
125                String syncCode = content.getValue(idField);
126                if (StringUtils.isNotBlank(syncCode))
127                {
128                    searchParams.put(idField, syncCode);
129                    if (scc.removalSync() && scc.getTotalCount(searchParams, logger) == 0)
130                    {
131                        _contentDAO.forceDeleteContentsWithLog(List.of(content), null, logger);
132                    }
133                    else
134                    {
135                        scc.synchronizeContent(content, logger);
136                    }
137                    handled = true;
138                }
139                else
140                {
141                    logger.warn("The synchronization code of the content '{}' ({}) is empty but it has the SCC '{}'. It has been skipped.", content.getTitle(), content.getId(), scc.getId());
142                }
143            }
144            catch (Exception e)
145            {
146                logger.error("An error occurred synchronized content '{}' ({}) from SCC '{}'", content.getTitle(), content.getId(), scc.getId(), e);
147            }
148        }
149        
150        
151        // The content has not been handled because of error or the content is manually created, so search in deeper children
152        if (!handled && addToHandleContents(content.getId()) && content instanceof ProgramItem programItem)
153        {
154            for (ProgramItem syncChild : _odfHelper.getChildProgramItems(programItem))
155            {
156                synchronizeContent((ModifiableContent) syncChild, logger);
157            }
158            
159            if (content instanceof Course course)
160            {
161                for (CoursePart syncChild : course.getCourseParts())
162                {
163                    synchronizeContent(syncChild, logger);
164                }
165            }
166        }
167    }
168    
169    /**
170     * Get the Apogee SCC for the content if it exists
171     * @param content The content to search on
172     * @param logger the logger
173     * @return the Apogee SCC
174     */
175    public Optional<AbstractApogeeSynchronizableContentsCollection> getContentSCC(Content content, Logger logger)
176    {
177        try
178        {
179            return _sccHelper.getSynchronizableCollectionIds(content)
180                .stream()
181                .map(_sccDAO::getSynchronizableContentsCollection)
182                .filter(AbstractApogeeSynchronizableContentsCollection.class::isInstance)
183                .map(AbstractApogeeSynchronizableContentsCollection.class::cast)
184                .findFirst();
185        }
186        catch (Exception e)
187        {
188            logger.error("An error occurred getting ametys-internal:scc property", e);
189        }
190        
191        return Optional.empty();
192    }
193    
194    /**
195     * Add the content ID to the handle contents list.
196     * @param contentId Content ID
197     * @return <code>true</code> if the content ID have been added, <code>false</code> is returned if the content ID already exists in the handle contents list.
198     */
199    public boolean addToHandleContents(String contentId)
200    {
201        return _addContentToRequestAttribute(contentId, HANDLED_CONTENTS);
202    }
203    
204    private boolean _addContentToRequestAttribute(String contentId, String attributeName)
205    {
206        Request request = ContextHelper.getRequest(_context);
207        @SuppressWarnings("unchecked")
208        Set<String> handleContents = (Set<String>) request.getAttribute(attributeName);
209        boolean added = handleContents.add(contentId);
210        request.setAttribute(attributeName, handleContents);
211        return added;
212    }
213    
214    /**
215     * Get the synchronization catalog, can be the one defined by the synchronized content
216     * or the default catalog.
217     * @return the synchronization catalog, can be null
218     */
219    public String getSynchronizationCatalog()
220    {
221        return Optional.of(ContextHelper.getRequest(_context))
222            .map(r -> (String) r.getAttribute(CATALOG))
223            .orElseGet(() -> _catalogsManager.getDefaultCatalogName());
224    }
225    
226    /**
227     * Get the synchronization language, can be the one defined by the synchronized content
228     * or the ODF language.
229     * @return the synchronization language
230     */
231    public String getSynchronizationLang()
232    {
233        return Optional.of(ContextHelper.getRequest(_context))
234                .map(r -> (String) r.getAttribute(LANG))
235                .orElse(_odfLang);
236    }
237}