001/* 002 * Copyright 2017 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.io.File; 019import java.io.FileInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.math.BigDecimal; 023import java.sql.Clob; 024import java.sql.SQLException; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.Collection; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.HashSet; 031import java.util.LinkedHashMap; 032import java.util.LinkedHashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Map.Entry; 036import java.util.Objects; 037import java.util.Optional; 038import java.util.Set; 039import java.util.stream.Collectors; 040import java.util.stream.Stream; 041 042import org.apache.avalon.framework.configuration.Configuration; 043import org.apache.avalon.framework.configuration.ConfigurationException; 044import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 045import org.apache.avalon.framework.context.Context; 046import org.apache.avalon.framework.context.ContextException; 047import org.apache.avalon.framework.context.Contextualizable; 048import org.apache.avalon.framework.service.ServiceException; 049import org.apache.avalon.framework.service.ServiceManager; 050import org.apache.cocoon.Constants; 051import org.apache.cocoon.components.ContextHelper; 052import org.apache.cocoon.environment.Request; 053import org.apache.commons.collections4.ListUtils; 054import org.apache.commons.io.IOUtils; 055import org.apache.commons.lang3.StringUtils; 056import org.apache.commons.lang3.tuple.Pair; 057import org.slf4j.Logger; 058 059import org.ametys.cms.contenttype.ContentType; 060import org.ametys.cms.data.ContentValue; 061import org.ametys.cms.repository.Content; 062import org.ametys.cms.repository.ModifiableContent; 063import org.ametys.core.schedule.progression.ContainerProgressionTracker; 064import org.ametys.core.schedule.progression.ProgressionTrackerFactory; 065import org.ametys.core.util.JSONUtils; 066import org.ametys.odf.ODFHelper; 067import org.ametys.odf.cdmfr.CDMFRHandler; 068import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection; 069import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection; 070import org.ametys.plugins.odfsync.apogee.ApogeeDAO; 071import org.ametys.plugins.odfsync.apogee.scc.impl.OrgUnitSynchronizableContentsCollection; 072import org.ametys.runtime.i18n.I18nizableText; 073import org.ametys.runtime.model.ModelItem; 074import org.ametys.runtime.model.type.ModelItemTypeConstants; 075 076import com.google.common.base.CharMatcher; 077 078/** 079 * Abstract class for Apogee synchronization 080 */ 081public abstract class AbstractApogeeSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection implements Contextualizable, ApogeeSynchronizableContentsCollection 082{ 083 /** Name of parameter holding the data source id */ 084 public static final String PARAM_DATASOURCE_ID = "datasourceId"; 085 /** Name of parameter holding the administrative year */ 086 public static final String PARAM_YEAR = "year"; 087 /** Name of parameter holding the adding unexisting children parameter */ 088 public static final String PARAM_ADD_UNEXISTING_CHILDREN = "add-unexisting-children"; 089 /** Name of parameter holding the link existing children parameter */ 090 public static final String PARAM_ADD_EXISTING_CHILDREN = "add-existing-children"; 091 /** Name of parameter holding the field ID column */ 092 protected static final String __PARAM_ID_COLUMN = "idColumn"; 093 /** Name of parameter holding the fields mapping */ 094 protected static final String __PARAM_MAPPING = "mapping"; 095 /** Name of parameter into mapping holding the synchronized property */ 096 protected static final String __PARAM_MAPPING_SYNCHRO = "synchro"; 097 /** Name of parameter into mapping holding the path of metadata */ 098 protected static final String __PARAM_MAPPING_METADATA_REF = "metadata-ref"; 099 /** Name of parameter into mapping holding the remote attribute */ 100 protected static final String __PARAM_MAPPING_ATTRIBUTE = "attribute"; 101 /** Name of parameter holding the criteria */ 102 protected static final String __PARAM_CRITERIA = "criteria"; 103 /** Name of parameter into criteria holding a criterion */ 104 protected static final String __PARAM_CRITERIA_CRITERION = "criterion"; 105 /** Name of parameter into criterion holding the id */ 106 protected static final String __PARAM_CRITERIA_CRITERION_ID = "id"; 107 /** Name of parameter into criterion holding the label */ 108 protected static final String __PARAM_CRITERIA_CRITERION_LABEL = "label"; 109 /** Name of parameter into criterion holding the type */ 110 protected static final String __PARAM_CRITERIA_CRITERION_TYPE = "type"; 111 /** Name of paramter holding columns */ 112 protected static final String __PARAM_COLUMNS = "columns"; 113 /** Name of paramter into columns holding column */ 114 protected static final String __PARAM_COLUMNS_COLUMN = "column"; 115 116 /** Parameter value to add unexisting children from Ametys on the current element */ 117 protected Boolean _addUnexistingChildren; 118 119 /** Parameter value to add existing children in Ametys on the current element */ 120 protected Boolean _addExistingChildren; 121 122 /** Name of the Apogée column which contains the ID */ 123 protected String _idColumn; 124 125 /** Mapping between metadata and columns */ 126 protected Map<String, List<String>> _mapping; 127 128 /** Synchronized fields */ 129 protected Set<String> _syncFields; 130 131 /** Synchronized fields */ 132 protected Set<String> _columns; 133 134 /** Synchronized fields */ 135 protected Set<ApogeeCriterion> _criteria; 136 137 /** Context */ 138 protected Context _context; 139 140 /** The DAO for remote DB Apogee */ 141 protected ApogeeDAO _apogeeDAO; 142 143 /** The JSON utils */ 144 protected JSONUtils _jsonUtils; 145 146 /** The ODF helper */ 147 protected ODFHelper _odfHelper; 148 149 /** The Apogee SCC helper */ 150 protected ApogeeSynchronizableContentsCollectionHelper _apogeeSCCHelper; 151 152 /** The CDM-fr handler */ 153 protected CDMFRHandler _cdmfrHandler; 154 155 @Override 156 public void service(ServiceManager manager) throws ServiceException 157 { 158 super.service(manager); 159 _apogeeDAO = (ApogeeDAO) manager.lookup(ApogeeDAO.ROLE); 160 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 161 _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE); 162 _apogeeSCCHelper = (ApogeeSynchronizableContentsCollectionHelper) manager.lookup(ApogeeSynchronizableContentsCollectionHelper.ROLE); 163 _cdmfrHandler = (CDMFRHandler) manager.lookup(CDMFRHandler.ROLE); 164 } 165 166 @Override 167 public void contextualize(Context context) throws ContextException 168 { 169 _context = context; 170 } 171 172 @Override 173 protected void configureDataSource(Configuration configuration) throws ConfigurationException 174 { 175 try 176 { 177 org.apache.cocoon.environment.Context ctx = (org.apache.cocoon.environment.Context) _context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 178 File apogeeMapping = new File(ctx.getRealPath("/WEB-INF/param/odf/apogee-mapping.xml")); 179 180 // FIXME CONTENTIO-343 Update the mapping names and use the SynchronizableContentsCollectionMappingHelper 181 try (InputStream is = apogeeMapping.isFile() 182 ? new FileInputStream(apogeeMapping) 183 : getClass().getResourceAsStream("/org/ametys/plugins/odfsync/apogee/apogee-mapping.xml")) 184 { 185 Configuration cfg = new DefaultConfigurationBuilder().build(is); 186 Configuration child = cfg.getChild(getMappingName()); 187 if (child != null) 188 { 189 _criteria = new LinkedHashSet<>(); 190 _columns = new LinkedHashSet<>(); 191 _idColumn = child.getChild(__PARAM_ID_COLUMN).getValue(); 192 _mapping = new HashMap<>(); 193 _syncFields = new HashSet<>(); 194 String mappingAsString = child.getChild(__PARAM_MAPPING).getValue(); 195 _mapping.put(getIdField(), List.of(getIdColumn())); 196 if (StringUtils.isNotEmpty(mappingAsString)) 197 { 198 List<Object> mappingAsList = _jsonUtils.convertJsonToList(mappingAsString); 199 for (Object object : mappingAsList) 200 { 201 @SuppressWarnings("unchecked") 202 Map<String, Object> field = (Map<String, Object>) object; 203 204 String metadataRef = (String) field.get(__PARAM_MAPPING_METADATA_REF); 205 206 String[] attributes = ((String) field.get(__PARAM_MAPPING_ATTRIBUTE)).split(","); 207 _mapping.put(metadataRef, Arrays.asList(attributes)); 208 209 boolean isSynchronized = field.containsKey(__PARAM_MAPPING_SYNCHRO) ? (Boolean) field.get(__PARAM_MAPPING_SYNCHRO) : false; 210 if (isSynchronized) 211 { 212 _syncFields.add(metadataRef); 213 } 214 } 215 } 216 217 Configuration[] criteria = child.getChild(__PARAM_CRITERIA).getChildren(__PARAM_CRITERIA_CRITERION); 218 for (Configuration criterion : criteria) 219 { 220 String id = criterion.getChild(__PARAM_CRITERIA_CRITERION_ID).getValue(); 221 I18nizableText label = _getCriterionLabel(criterion.getChild(__PARAM_CRITERIA_CRITERION_LABEL), id); 222 String type = criterion.getChild(__PARAM_CRITERIA_CRITERION_TYPE).getValue("STRING"); 223 224 _criteria.add(new ApogeeCriterion(id, label, type)); 225 } 226 227 Configuration[] columns = child.getChild(__PARAM_COLUMNS).getChildren(__PARAM_COLUMNS_COLUMN); 228 for (Configuration column : columns) 229 { 230 _columns.add(column.getValue()); 231 } 232 } 233 } 234 } 235 catch (Exception e) 236 { 237 throw new ConfigurationException("Error while parsing apogee-mapping.xml", e); 238 } 239 } 240 241 private I18nizableText _getCriterionLabel(Configuration configuration, String defaultValue) 242 { 243 if (configuration.getAttributeAsBoolean("i18n", false)) 244 { 245 return new I18nizableText("plugin.odf-sync", configuration.getValue(defaultValue)); 246 } 247 else 248 { 249 return new I18nizableText(configuration.getValue(defaultValue)); 250 } 251 } 252 253 @Override 254 public List<ModifiableContent> populate(Logger logger, ContainerProgressionTracker progressionTracker) 255 { 256 boolean isRequestAttributeOwner = false; 257 258 Request request = ContextHelper.getRequest(_context); 259 260 try 261 { 262 if (request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLED_CONTENTS) == null) 263 { 264 _startHandleCDMFR(); 265 request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLED_CONTENTS, new HashSet<>()); 266 isRequestAttributeOwner = true; 267 } 268 269 return super.populate(logger, progressionTracker); 270 } 271 finally 272 { 273 if (isRequestAttributeOwner) 274 { 275 _endHandleCDMFR(request); 276 request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLED_CONTENTS); 277 } 278 } 279 } 280 281 @Override 282 protected List<ModifiableContent> _internalPopulate(Logger logger, ContainerProgressionTracker progressionTracker) 283 { 284 return _importOrSynchronizeContents(Map.of("isGlobalSync", true), false, logger, progressionTracker); 285 } 286 287 @Override 288 protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger) 289 { 290 Map<String, Object> searchParams = new HashMap<>(searchParameters); 291 if (offset > 0) 292 { 293 searchParams.put("__offset", offset); 294 } 295 if (limit < Integer.MAX_VALUE) 296 { 297 searchParams.put("__limit", offset + limit); 298 } 299 searchParams.put("__order", _getSort(sort)); 300 301 // We don't use session.selectMap which reorder data 302 List<Map<String, Object>> requestValues = _search(searchParams, logger); 303 304 // Transform CLOBs to String 305 Set<String> clobColumns = getClobColumns(); 306 if (!clobColumns.isEmpty()) 307 { 308 for (Map<String, Object> contentValues : requestValues) 309 { 310 String idValue = contentValues.get(getIdColumn()).toString(); 311 for (String clobKey : getClobColumns()) 312 { 313 // Get the old values for the CLOB 314 @SuppressWarnings("unchecked") 315 Optional<List<Object>> oldValues = Optional.of(clobKey) 316 .map(contentValues::get) 317 .map(obj -> (List<Object>) obj); 318 319 if (oldValues.isPresent()) 320 { 321 // Get the new values for the CLOB 322 List<Object> newValues = oldValues.get() 323 .stream() 324 .map(value -> _transformClobToString(value, idValue, logger)) 325 .filter(Objects::nonNull) 326 .collect(Collectors.toList()); 327 328 // Set the transformed CLOB values 329 contentValues.put(clobKey, newValues); 330 } 331 } 332 } 333 } 334 335 // Reorganize results 336 String idColumn = getIdColumn(); 337 Map<String, Map<String, Object>> results = new LinkedHashMap<>(); 338 for (Map<String, Object> contentValues : requestValues) 339 { 340 results.put(contentValues.get(idColumn).toString(), contentValues); 341 } 342 343 for (Map<String, Object> result : results.values()) 344 { 345 result.put(SCC_UNIQUE_ID, result.get(getIdColumn())); 346 } 347 348 return results; 349 } 350 351 @Override 352 protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> searchParameters, Logger logger) 353 { 354 Map<String, Map<String, List<Object>>> remoteValues = new HashMap<>(); 355 356 Map<String, Map<String, Object>> results = internalSearch(searchParameters, 0, Integer.MAX_VALUE, null, logger); 357 358 if (results != null) 359 { 360 remoteValues = _sccHelper.organizeRemoteValuesByAttribute(results, _mapping); 361 } 362 363 return remoteValues; 364 } 365 366 @Override 367 public List<String> getLanguages() 368 { 369 return List.of(_apogeeSCCHelper.getSynchronizationLang()); 370 } 371 372 @Override 373 public List<ModifiableContent> importContent(String idValue, Map<String, Object> additionalParameters, Logger logger) throws Exception 374 { 375 boolean isRequestAttributeOwner = false; 376 377 Request request = ContextHelper.getRequest(_context); 378 379 try 380 { 381 if (request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLED_CONTENTS) == null) 382 { 383 _startHandleCDMFR(); 384 request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLED_CONTENTS, new HashSet<>()); 385 isRequestAttributeOwner = true; 386 } 387 388 return super.importContent(idValue, additionalParameters, logger); 389 } 390 finally 391 { 392 if (isRequestAttributeOwner) 393 { 394 _endHandleCDMFR(request); 395 request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLED_CONTENTS); 396 } 397 } 398 } 399 400 @Override 401 public void synchronizeContent(ModifiableContent content, Logger logger) throws Exception 402 { 403 boolean isRequestAttributeOwner = false; 404 405 Request request = ContextHelper.getRequest(_context); 406 407 try 408 { 409 if (request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLED_CONTENTS) == null) 410 { 411 _startHandleCDMFR(); 412 request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLED_CONTENTS, new HashSet<>()); 413 _addContentAttributes(request, content); 414 isRequestAttributeOwner = true; 415 } 416 417 super.synchronizeContent(content, logger); 418 } 419 finally 420 { 421 if (isRequestAttributeOwner) 422 { 423 _endHandleCDMFR(request); 424 request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLED_CONTENTS); 425 _removeContentAttributes(request); 426 } 427 } 428 } 429 430 /** 431 * Start handle CDM-fr treatments 432 */ 433 protected void _startHandleCDMFR() 434 { 435 _cdmfrHandler.suspendCDMFRObserver(); 436 } 437 438 /** 439 * End handle CDM-fr treatments 440 * @param request the request 441 */ 442 protected void _endHandleCDMFR(Request request) 443 { 444 @SuppressWarnings("unchecked") 445 Set<String> handledContents = (Set<String>) request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLED_CONTENTS); 446 _cdmfrHandler.unsuspendCDMFRObserver(handledContents); 447 } 448 449 /** 450 * Add attributes from content in the request. 451 * @param request The request 452 * @param content The content 453 */ 454 protected void _addContentAttributes(Request request, ModifiableContent content) 455 { 456 request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.LANG, content.getLanguage()); 457 } 458 459 /** 460 * Remove attributes of the content from the request. 461 * @param request The request 462 */ 463 protected void _removeContentAttributes(Request request) 464 { 465 request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.LANG); 466 } 467 468 @Override 469 protected Map<String, Object> putIdParameter(String idValue) 470 { 471 Map<String, Object> parameters = new HashMap<>(); 472 parameters.put(getIdField(), idValue); 473 return parameters; 474 } 475 476 /** 477 * Search the contents with the search parameters. Use id parameter to search an unique content. 478 * @param searchParameters Search parameters 479 * @param logger The logger 480 * @return A Map of mapped metadatas extract from Apogée database ordered by content unique Apogée ID 481 */ 482 protected abstract List<Map<String, Object>> _search(Map<String, Object> searchParameters, Logger logger); 483 484 /** 485 * Convert the {@link BigDecimal} values retrieved from database into long values 486 * @param searchResults The initial search results from database 487 * @return The converted search results 488 */ 489 protected List<Map<String, Object>> _convertBigDecimal(List<Map<String, Object>> searchResults) 490 { 491 List<Map<String, Object>> convertedSearchResults = new ArrayList<>(); 492 493 for (Map<String, Object> searchResult : searchResults) 494 { 495 for (String key : searchResult.keySet()) 496 { 497 searchResult.put(key, _convertBigDecimal(getContentType(), key, searchResult.get(key))); 498 } 499 500 convertedSearchResults.add(searchResult); 501 } 502 503 return convertedSearchResults; 504 } 505 506 /** 507 * Convert the object in parameter to a long if it's a {@link BigDecimal}, otherwise return the object itself. 508 * @param contentTypeId The content type of the parent content 509 * @param attributeName The metadata name 510 * @param objectToConvert The object to convert if necessary 511 * @return The converted object 512 */ 513 protected Object _convertBigDecimal(String contentTypeId, String attributeName, Object objectToConvert) 514 { 515 if (objectToConvert instanceof BigDecimal) 516 { 517 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 518 if (contentType.hasModelItem(attributeName)) 519 { 520 ModelItem definition = contentType.getModelItem(attributeName); 521 String typeId = definition.getType().getId(); 522 switch (typeId) 523 { 524 case ModelItemTypeConstants.DOUBLE_TYPE_ID: 525 return ((BigDecimal) objectToConvert).doubleValue(); 526 case ModelItemTypeConstants.LONG_TYPE_ID: 527 return ((BigDecimal) objectToConvert).longValue(); 528 default: 529 // Do nothing 530 break; 531 } 532 } 533 return ((BigDecimal) objectToConvert).toString(); 534 } 535 return objectToConvert; 536 } 537 538 /** 539 * Transform CLOB value to String value. 540 * @param value The input value 541 * @param idValue The identifier of the program 542 * @param logger The logger 543 * @return the same value, with CLOB transformed to String. 544 */ 545 protected Object _transformClobToString(Object value, String idValue, Logger logger) 546 { 547 if (value instanceof Clob) 548 { 549 Clob clob = (Clob) value; 550 try 551 { 552 String strValue = IOUtils.toString(clob.getCharacterStream()); 553 return CharMatcher.javaIsoControl().and(CharMatcher.anyOf("\r\n\t").negate()).removeFrom(strValue); 554 } 555 catch (SQLException | IOException e) 556 { 557 logger.error("Unable to get education add elements from the program '{}'.", idValue, e); 558 return null; 559 } 560 finally 561 { 562 try 563 { 564 clob.free(); 565 } 566 catch (SQLException e) 567 { 568 // Ignore the exception. 569 } 570 } 571 } 572 573 return value; 574 } 575 576 /** 577 * Get the list of CLOB column's names. 578 * @return The list of the CLOB column's names to transform to {@link String} 579 */ 580 protected Set<String> getClobColumns() 581 { 582 return Set.of(); 583 } 584 585 /** 586 * Transform a {@link List} of {@link Map} to a {@link Map} of {@link List} computed by keys (lines to columns). 587 * @param lines {@link List} to reorganize 588 * @return {@link Map} of {@link List} 589 */ 590 protected Map<String, List<Object>> _transformListOfMap2MapOfList(List<Map<String, Object>> lines) 591 { 592 return lines.stream() 593 .filter(Objects::nonNull) 594 .map(Map::entrySet) 595 .flatMap(Collection::stream) 596 .filter(e -> Objects.nonNull(e.getValue())) 597 .collect( 598 Collectors.toMap( 599 Entry::getKey, 600 e -> List.of(e.getValue()), 601 (l1, l2) -> ListUtils.union(l1, l2) 602 ) 603 ); 604 } 605 606 /** 607 * Get the name of the mapping. 608 * @return the mapping name 609 */ 610 protected abstract String getMappingName(); 611 612 /** 613 * Get the id of data source 614 * @return The id of data source 615 */ 616 protected String getDataSourceId() 617 { 618 return (String) getParameterValues().get(PARAM_DATASOURCE_ID); 619 } 620 621 /** 622 * Get the administrative year 623 * @return The administrative year 624 */ 625 protected String getYear() 626 { 627 return (String) getParameterValues().get(PARAM_YEAR); 628 } 629 630 /** 631 * Check if unexisting contents in Ametys should be added from data source 632 * @return <code>true</code> if unexisting contents in Ametys should be added from data source, default value is <code>true</code> 633 */ 634 protected boolean addUnexistingChildren() 635 { 636 // This parameter is read many times so we store it 637 if (_addUnexistingChildren == null) 638 { 639 _addUnexistingChildren = Boolean.valueOf((String) getParameterValues().getOrDefault(PARAM_ADD_UNEXISTING_CHILDREN, Boolean.TRUE.toString())); 640 } 641 return _addUnexistingChildren; 642 } 643 644 /** 645 * Check if existing contents in Ametys should be added from data source 646 * @return <code>true</code> if existing contents in Ametys should be added from data source, default value is <code>false</code> 647 */ 648 protected boolean addExistingChildren() 649 { 650 // This parameter is read many times so we store it 651 if (_addExistingChildren == null) 652 { 653 _addExistingChildren = Boolean.valueOf((String) getParameterValues().getOrDefault(PARAM_ADD_EXISTING_CHILDREN, Boolean.FALSE.toString())); 654 } 655 return _addExistingChildren; 656 } 657 658 /** 659 * Get the identifier column (can be a concatened column). 660 * @return the column id 661 */ 662 protected String getIdColumn() 663 { 664 return _idColumn; 665 } 666 667 @Override 668 public String getIdField() 669 { 670 return "apogeeSyncCode"; 671 } 672 673 @Override 674 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 675 { 676 return _syncFields; 677 } 678 679 @Override 680 protected void configureSearchModel() 681 { 682 for (ApogeeCriterion criterion : _criteria) 683 { 684 _searchModelConfiguration.addCriterion(criterion.getId(), criterion.getLabel(), criterion.getType()); 685 } 686 for (String columnName : _columns) 687 { 688 _searchModelConfiguration.addColumn(columnName); 689 } 690 } 691 692 @Override 693 protected Map<String, Object> getAdditionalAttributeValues(String idValue, Content content, Map<String, Object> additionalParameters, boolean create, Logger logger) 694 { 695 Map<String, Object> additionalValues = super.getAdditionalAttributeValues(idValue, content, additionalParameters, create, logger); 696 697 // Handle parents 698 getParentFromAdditionalParameters(additionalParameters) 699 .map(this::getParentAttribute) 700 .ifPresent(attribute -> additionalValues.put(attribute.getKey(), attribute.getValue())); 701 702 // Handle children 703 String childrenAttributeName = getChildrenAttributeName(); 704 if (childrenAttributeName != null) 705 { 706 List<ModifiableContent> children = handleChildren(idValue, content, create, logger); 707 additionalValues.put(childrenAttributeName, children.toArray(new ModifiableContent[children.size()])); 708 } 709 710 return additionalValues; 711 } 712 713 /** 714 * Retrieves the attribute to synchronize for the given parent (as a {@link Pair} of name and value) 715 * @param parent the parent content 716 * @return the parent attribute 717 */ 718 protected Pair<String, Object> getParentAttribute(ModifiableContent parent) 719 { 720 return null; 721 } 722 723 /** 724 * Import and synchronize children of the given content 725 * In the usual case, if we are in import mode (create to true) or if we trust the source (removalSync to true) we just synchronize structure but we don't synchronize child contents 726 * @param idValue The current content synchronization code 727 * @param content The current content 728 * @param create <code>true</code> if the content has been newly created 729 * @param logger The logger 730 * @return The handled children 731 */ 732 protected List<ModifiableContent> handleChildren(String idValue, Content content, boolean create, Logger logger) 733 { 734 return importOrSynchronizeChildren(idValue, content, getChildrenSCCModelId(), getChildrenAttributeName(), create, logger); 735 } 736 737 @Override 738 protected Set<String> getNotSynchronizedRelatedContentIds(Content content, Map<String, Object> contentValues, Map<String, Object> additionalParameters, String lang, Logger logger) 739 { 740 Set<String> contentIds = super.getNotSynchronizedRelatedContentIds(content, contentValues, additionalParameters, lang, logger); 741 742 getParentIdFromAdditionalParameters(additionalParameters) 743 .ifPresent(contentIds::add); 744 745 return contentIds; 746 } 747 748 /** 749 * Retrieves the parent id, extracted from additional parameters 750 * @param additionalParameters the additional parameters 751 * @return the parent id 752 */ 753 protected Optional<ModifiableContent> getParentFromAdditionalParameters(Map<String, Object> additionalParameters) 754 { 755 return getParentIdFromAdditionalParameters(additionalParameters) 756 .map(_resolver::resolveById); 757 } 758 759 /** 760 * Retrieves the parent id, extracted from additional parameters 761 * @param additionalParameters the additional parameters 762 * @return the parent id 763 */ 764 protected Optional<String> getParentIdFromAdditionalParameters(Map<String, Object> additionalParameters) 765 { 766 return Optional.ofNullable(additionalParameters) 767 .filter(params -> params.containsKey("parentId")) 768 .map(params -> params.get("parentId")) 769 .filter(String.class::isInstance) 770 .map(String.class::cast); 771 } 772 773 /** 774 * Get the children SCC model id. Can be null if no implementation is defined. 775 * @return the children SCC model id 776 */ 777 protected String getChildrenSCCModelId() 778 { 779 // Default implementation 780 return null; 781 } 782 783 /** 784 * Get the attribute name to get children 785 * @return the attribute name to get children 786 */ 787 protected abstract String getChildrenAttributeName(); 788 789 /** 790 * Transform the given {@link List} of {@link Object} to a {@link String} representing the ordered fields for SQL. 791 * @param sortList The sort list object to transform to the list of ordered fields compatible with SQL. 792 * @return A string representing the list of ordered fields 793 */ 794 @SuppressWarnings("unchecked") 795 protected String _getSort(List<Object> sortList) 796 { 797 if (sortList != null) 798 { 799 StringBuilder sort = new StringBuilder(); 800 801 for (Object sortValueObj : sortList) 802 { 803 Map<String, Object> sortValue = (Map<String, Object>) sortValueObj; 804 805 sort.append(sortValue.get("property")); 806 if (sortValue.containsKey("direction")) 807 { 808 sort.append(" "); 809 sort.append(sortValue.get("direction")); 810 sort.append(","); 811 } 812 else 813 { 814 sort.append(" ASC,"); 815 } 816 } 817 818 sort.deleteCharAt(sort.length() - 1); 819 820 return sort.toString(); 821 } 822 823 return null; 824 } 825 826 @Override 827 public int getTotalCount(Map<String, Object> searchParameters, Logger logger) 828 { 829 // Remove empty parameters 830 Map<String, Object> searchParams = new HashMap<>(); 831 for (String parameterName : searchParameters.keySet()) 832 { 833 Object parameterValue = searchParameters.get(parameterName); 834 if (parameterValue != null && !parameterValue.toString().isEmpty()) 835 { 836 searchParams.put(parameterName, parameterValue); 837 } 838 } 839 840 searchParams.put("__count", true); 841 842 List<Map<String, Object>> results = _search(searchParams, logger); 843 if (results != null && !results.isEmpty()) 844 { 845 return Integer.valueOf(results.get(0).get("COUNT(*)").toString()).intValue(); 846 } 847 848 return 0; 849 } 850 851 @Override 852 protected ModifiableContent _importContent(String idValue, Map<String, Object> additionalParameters, String lang, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 853 { 854 ModifiableContent content = super._importContent(idValue, additionalParameters, lang, _transformOrgUnitAttribute(remoteValues, logger), logger); 855 if (content != null) 856 { 857 _apogeeSCCHelper.addToHandleContents(content.getId()); 858 } 859 return content; 860 } 861 862 @Override 863 protected ModifiableContent _synchronizeContent(ModifiableContent content, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 864 { 865 if (!_apogeeSCCHelper.addToHandleContents(content.getId())) 866 { 867 return content; 868 } 869 return super._synchronizeContent(content, _transformOrgUnitAttribute(remoteValues, logger), logger); 870 } 871 872 /** 873 * Import and synchronize children of the given content 874 * In the usual case, if we are in import mode (create to true) or if we trust the source (removalSync to true) we just synchronize structure but we don't synchronize child contents 875 * @param idValue The parent content synchronization code 876 * @param content The parent content 877 * @param sccModelId SCC model ID 878 * @param attributeName The name of the attribute containing children 879 * @param create <code>true</code> if the content has been newly created 880 * @param logger The logger 881 * @return The imported or synchronized children 882 */ 883 protected List<ModifiableContent> importOrSynchronizeChildren(String idValue, Content content, String sccModelId, String attributeName, boolean create, Logger logger) 884 { 885 // Get the SCC for children 886 SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(sccModelId); 887 888 return create 889 // Import mode 890 ? _importChildren(idValue, scc, logger) 891 // Synchronization mode 892 : _synchronizeChildren(content, scc, attributeName, logger); 893 } 894 895 /** 896 * Import children 897 * @param idValue The parent content synchronization code 898 * @param scc The SCC 899 * @param logger The logger 900 * @return The imported children 901 */ 902 protected List<ModifiableContent> _importChildren(String idValue, SynchronizableContentsCollection scc, Logger logger) 903 { 904 // If SCC exists, search for children 905 if (scc != null && scc instanceof ApogeeSynchronizableContentsCollection) 906 { 907 // Synchronize or import children content 908 return ((ApogeeSynchronizableContentsCollection) scc).importOrSynchronizeContents(_getChildrenSearchParametersWithParent(idValue), logger); 909 } 910 911 return List.of(); 912 } 913 914 /** 915 * Synchronize children 916 * @param content Parent content 917 * @param scc The SCC 918 * @param attributeName The name of the attribute containing children 919 * @param logger The logger 920 * @return <code>true</code> if there are changes 921 */ 922 protected List<ModifiableContent> _synchronizeChildren(Content content, SynchronizableContentsCollection scc, String attributeName, Logger logger) 923 { 924 // First we get the existing children in Ametys 925 List<ModifiableContent> ametysChildren = Stream.of(content.getValueOrDefault(attributeName, new ContentValue[0])) 926 .map(ContentValue::getContentIfExists) 927 .flatMap(Optional::stream) 928 .collect(Collectors.toList()); 929 930 // Get the children remote sync codes if needed 931 // These remote sync codes are needed if we want to add contents in Ametys from Apogee or delete obsolete contents 932 Set<String> childrenRemoteSyncCode = (removalSync() || addUnexistingChildren() || addExistingChildren()) 933 ? _getChildrenRemoteSyncCode(content, scc, logger) 934 : null; 935 936 // Can be null if we do not want to remove obsolete contents, add existing or unexisting children 937 // Or if the current content is not from Apogee 938 if (childrenRemoteSyncCode != null) 939 { 940 // Remove obsolete children if removalSync is active 941 if (removalSync()) 942 { 943 ametysChildren = ametysChildren.stream() 944 .filter(c -> !_isChildWillBeRemoved(c, scc, childrenRemoteSyncCode, logger)) 945 .collect(Collectors.toList()); 946 } 947 948 if (addExistingChildren() || addUnexistingChildren()) 949 { 950 // Then we add missing children if needed 951 for (String code : childrenRemoteSyncCode) 952 { 953 ModifiableContent child = scc.getContent(_apogeeSCCHelper.getSynchronizationLang(), code, false); 954 955 // If the child with the given sync code does not exist, import it if the parameter to 956 // add unexisting children is checked 957 if (child == null) 958 { 959 if (addUnexistingChildren()) 960 { 961 ametysChildren.addAll(_importUnexistingChildren(scc, code, null, logger)); 962 } 963 } 964 // If the parameter to link existing children not already in the list is checked, 965 // it adds the content to the children list if it is not already in it. 966 else if (addExistingChildren() && !ametysChildren.contains(child)) 967 { 968 ametysChildren.add(child); 969 } 970 } 971 } 972 } 973 974 // Then we synchronize children in Ametys 975 // (it won't be synchronized twice because it reads the request parameters with handled contents) 976 for (ModifiableContent childContent : ametysChildren) 977 { 978 _apogeeSCCHelper.synchronizeContent(childContent, logger); 979 } 980 981 return ametysChildren; 982 } 983 984 /** 985 * Get the remote sync codes 986 * @param content Parent content 987 * @param scc the scc 988 * @param logger The logger 989 * @return the remote sync codes or null if the scc is not from Apogee 990 */ 991 protected Set<String> _getChildrenRemoteSyncCode(Content content, SynchronizableContentsCollection scc, Logger logger) 992 { 993 if (scc != null && scc instanceof AbstractApogeeSynchronizableContentsCollection) 994 { 995 String syncCode = content.getValue(getIdField()); 996 return ((AbstractApogeeSynchronizableContentsCollection) scc) 997 .search(_getChildrenSearchParametersWithParent(syncCode), 0, Integer.MAX_VALUE, null, logger) 998 .keySet(); 999 } 1000 1001 return null; 1002 } 1003 1004 /** 1005 * Get the children search parameters. 1006 * @param parentSyncCode The parent synchronization code 1007 * @return a {@link Map} of search parameters 1008 */ 1009 protected Map<String, Object> _getChildrenSearchParametersWithParent(String parentSyncCode) 1010 { 1011 Map<String, Object> searchParameters = new HashMap<>(); 1012 searchParameters.put("parentCode", parentSyncCode); 1013 return searchParameters; 1014 } 1015 1016 /** 1017 * Import an unexisting child in Ametys 1018 * @param scc the scc to import the content 1019 * @param syncCode the sync code of the content 1020 * @param additionalParameters the additional params 1021 * @param logger the logger 1022 * @return the list of created children 1023 */ 1024 @SuppressWarnings("unchecked") 1025 protected List<ModifiableContent> _importUnexistingChildren(SynchronizableContentsCollection scc, String syncCode, Map<String, List<Object>> additionalParameters, Logger logger) 1026 { 1027 try 1028 { 1029 return scc.importContent(syncCode, (Map<String, Object>) (Object) additionalParameters, logger); 1030 } 1031 catch (Exception e) 1032 { 1033 logger.error("An error occured while importing a new children content with syncCode '{}' from SCC '{}'", syncCode, scc.getId(), e); 1034 } 1035 1036 return Collections.emptyList(); 1037 } 1038 1039 /** 1040 * True if the content will be removed from the structure 1041 * @param content the content 1042 * @param scc the scc 1043 * @param childrenRemoteSyncCode the remote sync codes 1044 * @param logger the logger 1045 * @return <code>true</code> if the content will be removed from the structure 1046 */ 1047 protected boolean _isChildWillBeRemoved(ModifiableContent content, SynchronizableContentsCollection scc, Set<String> childrenRemoteSyncCode, Logger logger) 1048 { 1049 String syncCode = content.getValue(scc.getIdField()); 1050 return !childrenRemoteSyncCode.contains(syncCode); 1051 } 1052 1053 @Override 1054 public List<ModifiableContent> importOrSynchronizeContents(Map<String, Object> searchParams, Logger logger) 1055 { 1056 ContainerProgressionTracker containerProgressionTracker = ProgressionTrackerFactory.createContainerProgressionTracker("Import or synchronize contents", logger); 1057 1058 return _importOrSynchronizeContents(searchParams, true, logger, containerProgressionTracker); 1059 } 1060 1061 @SuppressWarnings("unchecked") 1062 private Map<String, List<Object>> _transformOrgUnitAttribute(Map<String, List<Object>> remoteValues, Logger logger) 1063 { 1064 // Transform orgUnit values and import content if necessary (useful for Course and SubProgram) 1065 SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(OrgUnitSynchronizableContentsCollection.MODEL_ID); 1066 1067 List<Object> orgUnitCodes = remoteValues.get("orgUnit"); 1068 if (orgUnitCodes != null && !orgUnitCodes.isEmpty()) 1069 { 1070 List<?> orgUnitContents = null; 1071 String orgUnitCode = orgUnitCodes.get(0).toString(); 1072 1073 if (scc != null) 1074 { 1075 try 1076 { 1077 ModifiableContent orgUnitContent = scc.getContent(_apogeeSCCHelper.getSynchronizationLang(), orgUnitCode, false); 1078 if (orgUnitContent == null) 1079 { 1080 orgUnitContents = scc.importContent(orgUnitCode, null, logger); 1081 } 1082 else 1083 { 1084 orgUnitContents = List.of(orgUnitContent); 1085 } 1086 } 1087 catch (Exception e) 1088 { 1089 logger.error("An error occured during the import of the OrgUnit identified by the synchronization code '{}'", orgUnitCode, e); 1090 } 1091 } 1092 1093 if (orgUnitContents == null) 1094 { 1095 // Impossible link to orgUnit 1096 remoteValues.remove("orgUnit"); 1097 logger.warn("Impossible to import the OrgUnit with the synchronization code '{}', check if you set the OrgUnit SCC with the following model ID: '{}'", orgUnitCode, OrgUnitSynchronizableContentsCollection.MODEL_ID); 1098 } 1099 else 1100 { 1101 remoteValues.put("orgUnit", (List<Object>) orgUnitContents); 1102 } 1103 } 1104 1105 return remoteValues; 1106 } 1107 1108 @Override 1109 public boolean handleRightAssignmentContext() 1110 { 1111 // Rights on ODF contents are handled by ODFRightAssignmentContext 1112 return false; 1113 } 1114}