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}