001/*
002 *  Copyright 2025 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.ArrayList;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025import java.util.stream.Collectors;
026import java.util.stream.Stream;
027
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.commons.lang3.StringUtils;
031import org.slf4j.Logger;
032
033import org.ametys.cms.repository.Content;
034import org.ametys.cms.repository.ModifiableContent;
035import org.ametys.core.schedule.progression.ContainerProgressionTracker;
036import org.ametys.odf.ProgramItem;
037import org.ametys.odf.cdmfr.CDMFRHandler;
038import org.ametys.plugins.contentio.synchronize.impl.AbstractDefaultSynchronizableContentsCollection;
039import org.ametys.plugins.odfsync.apogee.ApogeePreviousYearsFieldsDAO;
040import org.ametys.plugins.repository.query.expression.Expression;
041import org.ametys.plugins.repository.query.expression.Expression.Operator;
042import org.ametys.plugins.repository.query.expression.StringExpression;
043
044/**
045 * SCC for previous years fields.
046 */
047public abstract class AbstractPreviousYearsSynchronizableContentsCollection extends AbstractDefaultSynchronizableContentsCollection
048{
049    /** Name of parameter holding the data source id */
050    public static final String PARAM_DATASOURCE_ID = "datasourceId";
051    /** Name of parameter holding the administrative year */
052    public static final String PARAM_CURRENT_YEAR = "currentYear";
053    /** Name of parameter holding the last administrative year (N-1) */
054    public static final String PARAM_PRECEDING_YEAR = "precedingYear";
055    
056    /** The DAO for remote DB Apogee */
057    protected ApogeePreviousYearsFieldsDAO _apogeePreviousYearsFieldsDAO;
058    
059    /** The Apogée SCC helper */
060    protected ApogeeSynchronizableContentsCollectionHelper _apogeeSCCHelper;
061
062    /** The CDM-fr handler */
063    protected CDMFRHandler _cdmfrHandler;
064    
065    @Override
066    public void service(ServiceManager manager) throws ServiceException
067    {
068        super.service(manager);
069        _apogeePreviousYearsFieldsDAO = (ApogeePreviousYearsFieldsDAO) manager.lookup(ApogeePreviousYearsFieldsDAO.ROLE);
070        _apogeeSCCHelper = (ApogeeSynchronizableContentsCollectionHelper) manager.lookup(ApogeeSynchronizableContentsCollectionHelper.ROLE);
071        _cdmfrHandler = (CDMFRHandler) manager.lookup(CDMFRHandler.ROLE);
072    }
073    
074    /**
075     * Get the id of data source
076     * @return The id of data source
077     */
078    protected String getDataSourceId()
079    {
080        return (String) getParameterValues().get(PARAM_DATASOURCE_ID);
081    }
082    
083    /**
084     * Get the current administrative year
085     * @return The current administrative year
086     */
087    protected String getCurrentYear()
088    {
089        return (String) getParameterValues().get(PARAM_CURRENT_YEAR);
090    }
091    /**
092     * Get the previous administrative year (N-1)
093     * @return The previous administrative year (N-1)
094     */
095    protected String getPrecedingYear()
096    {
097        return (String) getParameterValues().get(PARAM_PRECEDING_YEAR);
098    }
099    
100    @Override
101    public boolean checkCollection()
102    {
103        // We only synchronize, so we don't care about this parameter
104        // If set to true on first launch, no field will be synchronized, so always set to false
105        return false;
106    }
107    
108    /**
109     * Get the identifier column (can be a concatened column).
110     * @return the column id
111     */
112    protected String getIdColumn()
113    {
114        return "ID_SYNC";
115    }
116    
117    @Override
118    public List<String> getLanguages()
119    {
120        return List.of(_apogeeSCCHelper.getSynchronizationLang());
121    }
122    
123    @Override
124    public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters)
125    {
126        Set<String> fields = new HashSet<>();
127        fields.add(getCurrentYearAttributeName());
128        fields.add(getPrecedingYearAttributeName());
129        return fields;
130    }
131    
132    @Override
133    public void updateSyncInformations(ModifiableContent content, String syncCode, Logger logger) throws Exception
134    {
135        throw new UnsupportedOperationException("updateSyncInformations() method is not supported for this synchronizable contents collections.");
136    }
137    
138    @Override
139    public List<ModifiableContent> populate(Logger logger, ContainerProgressionTracker progressionTracker)
140    {
141        Set<String> contentIds = null;
142        
143        try
144        {
145            _startHandleCDMFR();
146            List<ModifiableContent> contents = super.populate(logger, progressionTracker);
147            contentIds = contents.stream().map(ModifiableContent::getId).collect(Collectors.toSet());
148            return contents;
149        }
150        finally
151        {
152            _endHandleCDMFR(contentIds);
153        }
154    }
155    
156    /**
157     * Start handle CDM-fr treatments
158     */
159    protected void _startHandleCDMFR()
160    {
161        _cdmfrHandler.suspendCDMFRObserver();
162    }
163    
164    /**
165     * End handle CDM-fr treatments
166     * @param contentIds the updated contents ids
167     */
168    protected void _endHandleCDMFR(Set<String> contentIds)
169    {
170        _cdmfrHandler.unsuspendCDMFRObserver(contentIds);
171    }
172    
173    /**
174     * Get the attribute name for current year data.
175     * @return the attribute name
176     */
177    protected abstract String getCurrentYearAttributeName();
178    
179    /**
180     * Get the attribute name for preceding year data.
181     * @return the attribute name
182     */
183    
184    protected abstract String getPrecedingYearAttributeName();
185    
186    /**
187     * Retrieve the contents for which to retrieve the values
188     * @return The contents
189     */
190    protected Stream<Content> getContents()
191    {
192        String query = _getContentPathQuery(_apogeeSCCHelper.getSynchronizationLang(), null, getContentType(), false);
193        return _resolver.<Content>query(query).stream();
194    }
195
196    @Override
197    protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> initialSearchParameters, int offset, int limit, List<Object> sort, Logger logger)
198    {
199        // It is a single search if the code is defined in the search, in other cases, it is a global search
200        boolean isSingleSearch = initialSearchParameters.containsKey(getIdField());
201        
202        // Add sync code to search parameters if needed
203        Map<String, Object> commonSearchParameters = isSingleSearch
204            ? _addSyncCodeToSearchParams(initialSearchParameters)
205            : initialSearchParameters;
206        
207        // Get all results
208        List<Map<String, Object>> listOfResults = _search(commonSearchParameters);
209        
210        // Fill the map with sync codes and codes correspondance
211        Map<String, String> syncCode2Code = isSingleSearch
212            ? Map.of((String) initialSearchParameters.get(getIdField()), (String) commonSearchParameters.get("syncCode"))
213            : _getAllSyncCodes();
214        
215        // Group results by code
216        return _group(listOfResults, syncCode2Code);
217    }
218    
219    private Map<String, Object> _addSyncCodeToSearchParams(Map<String, Object> initialSearchParameters)
220    {
221        Map<String, Object> commonSearchParameters = new HashMap<>(initialSearchParameters);
222        
223        String code = (String) commonSearchParameters.remove(getIdField());
224        
225        String lang = _apogeeSCCHelper.getSynchronizationLang();
226        
227        // The "code" in parameter is the code of the content not the sync code, retrieve it and add it to search parameters
228        String syncCode = Optional.ofNullable(getContent(lang, code, false))
229            .map(this::getSyncCode)
230            // If sync code does not exists, throw an exception
231            .orElseThrow(() -> new IllegalArgumentException("The content with code '" + code + "' with language '" + lang + "' in catalog '" + _apogeeSCCHelper.getSynchronizationCatalog() + "' does not have a synchronization code for SCC '" + getId() + "'"));
232        
233        // Update search parameters
234        commonSearchParameters.put("syncCode", syncCode);
235        
236        return commonSearchParameters;
237    }
238    
239    private List<Map<String, Object>> _search(Map<String, Object> searchParameters)
240    {
241        List<Map<String, Object>> results = new ArrayList<>();
242        results.addAll(searchByYear(getCurrentYear(), getCurrentYearAttributeName(), searchParameters));
243        results.addAll(searchByYear(getPrecedingYear(), getPrecedingYearAttributeName(), searchParameters));
244        return results;
245    }
246    
247    private Map<String, Map<String, Object>> _group(List<Map<String, Object>> listOfResults, Map<String, String> syncCode2Code)
248    {
249        // Compute results
250        String idColumn = getIdColumn();
251        // Map<code, Map<column, value>>
252        Map<String, Map<String, Object>> results = new HashMap<>();
253        for (Map<String, Object> resultFromList : listOfResults)
254        {
255            String syncCode = resultFromList.get(idColumn).toString();
256            String code = syncCode2Code.get(syncCode);
257            
258            // Ignore elements that does not have corresponding code with the sync code, that means the corresponding content has not been imported yet.
259            if (StringUtils.isNotBlank(code))
260            {
261                Map<String, Object> result = results.computeIfAbsent(code, __ -> new HashMap<>());
262                result.putAll(resultFromList);
263            }
264        }
265        
266        return results;
267    }
268    
269    /**
270     * Search the values for a year
271     * @param year The year value
272     * @param yearAttributeName The year attribute name
273     * @param searchParameters The common search parameters
274     * @return The results
275     */
276    protected List<Map<String, Object>> searchByYear(String year, String yearAttributeName, Map<String, Object> searchParameters)
277    {
278        // If the year is not filled in the configuration, it may not be requested
279        if (StringUtils.isBlank(year))
280        {
281            return List.of();
282        }
283        
284        Map<String, Object> searchParametersForYear = new HashMap<>(searchParameters);
285        searchParametersForYear.put("yearValue", year);
286        searchParametersForYear.put("attributeName", yearAttributeName);
287        
288        return executeApogeeRequest(searchParametersForYear);
289    }
290    
291    /**
292     * Get the sync code for a content
293     * @param content The content
294     * @return The sync code
295     */
296    protected String getSyncCode(Content content)
297    {
298        return content.getValue(getSyncCodeItemName());
299    }
300    
301    /**
302     * Get the item name that contains the synchronization code, it can be an attribute or a property.
303     * @return the synchronization code item name
304     */
305    protected abstract String getSyncCodeItemName();
306    
307    /**
308     * Execute the corresponding apogee request to retrieve the values from Apogee
309     * @param parameters The parameters of the request
310     * @return The results of the request
311     */
312    protected abstract List<Map<String, Object>> executeApogeeRequest(Map<String, Object> parameters);
313    
314    @Override
315    protected List<Expression> _getExpressionsList(String lang, String idValue, String contentType, boolean forceStrictCheck)
316    {
317        List<Expression> expList = super._getExpressionsList(lang, idValue, contentType, forceStrictCheck);
318        
319        String catalog = _apogeeSCCHelper.getSynchronizationCatalog();
320        if (catalog != null)
321        {
322            expList.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
323        }
324        
325        return expList;
326    }
327    
328    @Override
329    protected ModifiableContent _importContent(String idValue, Map<String, Object> additionalParameters, String lang, Map<String, List<Object>> remoteValues, Logger logger) throws Exception
330    {
331        throw new UnsupportedOperationException("The method _importContent is not handled by PreviousYearsSCC. The previous years fields can only be synchronized.");
332    }
333    
334    /**
335     * Ensure title is present.
336     * @implNote This method always forces the title to the current one, and does not warn
337     */
338    @Override
339    protected void ensureTitleIsPresent(Content content, Map<String, List<Object>> remoteValues, Logger logger)
340    {
341        remoteValues.put(Content.ATTRIBUTE_TITLE, List.of(content.getTitle()));
342    }
343    
344    private Map<String, String> _getAllSyncCodes()
345    {
346        Map<String, String> syncCode2Code = new HashMap<>();
347        
348        getContents().forEach(
349            content ->
350            {
351                String syncCode = getSyncCode(content);
352                if (StringUtils.isNotEmpty(syncCode))
353                {
354                    syncCode2Code.put(syncCode, content.getValue(getIdField()));
355                }
356            }
357        );
358        
359        return syncCode2Code;
360    }
361}