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