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