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 String childrenAttributeName = getChildrenAttributeName(); 646 if (childrenAttributeName != null) 647 { 648 List<ModifiableContent> children = handleChildren(idValue, content, create, logger); 649 additionalValues.put(childrenAttributeName, children.toArray(new ModifiableContent[children.size()])); 650 } 651 652 return additionalValues; 653 } 654 655 /** 656 * Retrieves the attribute to synchronize for the given parent (as a {@link Pair} of name and value) 657 * @param parent the parent content 658 * @return the parent attribute 659 */ 660 protected Pair<String, Object> getParentAttribute(ModifiableContent parent) 661 { 662 return null; 663 } 664 665 /** 666 * Import and synchronize children of the given content 667 * 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 668 * @param idValue The current content synchronization code 669 * @param content The current content 670 * @param create <code>true</code> if the content has been newly created 671 * @param logger The logger 672 * @return The handled children 673 */ 674 protected List<ModifiableContent> handleChildren(String idValue, Content content, boolean create, Logger logger) 675 { 676 return importOrSynchronizeChildren(idValue, content, getChildrenSCCModelId(), getChildrenAttributeName(), create, logger); 677 } 678 679 @Override 680 protected Set<String> getNotSynchronizedRelatedContentIds(Content content, Map<String, Object> contentValues, Map<String, Object> additionalParameters, String lang, Logger logger) 681 { 682 Set<String> contentIds = super.getNotSynchronizedRelatedContentIds(content, contentValues, additionalParameters, lang, logger); 683 684 getParentIdFromAdditionalParameters(additionalParameters) 685 .ifPresent(contentIds::add); 686 687 return contentIds; 688 } 689 690 /** 691 * Retrieves the parent id, extracted from additional parameters 692 * @param additionalParameters the additional parameters 693 * @return the parent id 694 */ 695 protected Optional<ModifiableContent> getParentFromAdditionalParameters(Map<String, Object> additionalParameters) 696 { 697 return getParentIdFromAdditionalParameters(additionalParameters) 698 .map(_resolver::resolveById); 699 } 700 701 /** 702 * Retrieves the parent id, extracted from additional parameters 703 * @param additionalParameters the additional parameters 704 * @return the parent id 705 */ 706 protected Optional<String> getParentIdFromAdditionalParameters(Map<String, Object> additionalParameters) 707 { 708 return Optional.ofNullable(additionalParameters) 709 .filter(params -> params.containsKey("parentId")) 710 .map(params -> params.get("parentId")) 711 .filter(String.class::isInstance) 712 .map(String.class::cast); 713 } 714 715 /** 716 * Get the children SCC model id. Can be null if no implementation is defined. 717 * @return the children SCC model id 718 */ 719 protected String getChildrenSCCModelId() 720 { 721 // Default implementation 722 return null; 723 } 724 725 /** 726 * Get the attribute name to get children 727 * @return the attribute name to get children 728 */ 729 protected abstract String getChildrenAttributeName(); 730 731 /** 732 * Transform the given {@link List} of {@link Object} to a {@link String} representing the ordered fields for SQL. 733 * @param sortList The sort list object to transform to the list of ordered fields compatible with SQL. 734 * @return A string representing the list of ordered fields 735 */ 736 @SuppressWarnings("unchecked") 737 protected String _getSort(List<Object> sortList) 738 { 739 if (sortList != null) 740 { 741 StringBuilder sort = new StringBuilder(); 742 743 for (Object sortValueObj : sortList) 744 { 745 Map<String, Object> sortValue = (Map<String, Object>) sortValueObj; 746 747 sort.append(sortValue.get("property")); 748 if (sortValue.containsKey("direction")) 749 { 750 sort.append(" "); 751 sort.append(sortValue.get("direction")); 752 sort.append(","); 753 } 754 else 755 { 756 sort.append(" ASC,"); 757 } 758 } 759 760 sort.deleteCharAt(sort.length() - 1); 761 762 return sort.toString(); 763 } 764 765 return null; 766 } 767 768 @Override 769 public int getTotalCount(Map<String, Object> searchParameters, Logger logger) 770 { 771 // Remove empty parameters 772 Map<String, Object> searchParams = new HashMap<>(); 773 for (String parameterName : searchParameters.keySet()) 774 { 775 Object parameterValue = searchParameters.get(parameterName); 776 if (parameterValue != null && !parameterValue.toString().isEmpty()) 777 { 778 searchParams.put(parameterName, parameterValue); 779 } 780 } 781 782 searchParams.put("__count", true); 783 784 List<Map<String, Object>> results = _search(searchParams, logger); 785 if (results != null && !results.isEmpty()) 786 { 787 return Integer.valueOf(results.get(0).get("COUNT(*)").toString()).intValue(); 788 } 789 790 return 0; 791 } 792 793 @Override 794 protected ModifiableContent _importContent(String idValue, Map<String, Object> additionalParameters, String lang, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 795 { 796 ModifiableContent content = super._importContent(idValue, additionalParameters, lang, _transformOrgUnitAttribute(remoteValues, logger), logger); 797 if (content != null) 798 { 799 _apogeeSCCHelper.addToHandleContents(content.getId()); 800 } 801 return content; 802 } 803 804 @Override 805 protected ModifiableContent _synchronizeContent(ModifiableContent content, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 806 { 807 if (!_apogeeSCCHelper.addToHandleContents(content.getId())) 808 { 809 return content; 810 } 811 return super._synchronizeContent(content, _transformOrgUnitAttribute(remoteValues, logger), logger); 812 } 813 814 /** 815 * Import and synchronize children of the given content 816 * 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 817 * @param idValue The parent content synchronization code 818 * @param content The parent content 819 * @param sccModelId SCC model ID 820 * @param attributeName The name of the attribute containing children 821 * @param create <code>true</code> if the content has been newly created 822 * @param logger The logger 823 * @return The imported or synchronized children 824 */ 825 protected List<ModifiableContent> importOrSynchronizeChildren(String idValue, Content content, String sccModelId, String attributeName, boolean create, Logger logger) 826 { 827 // Get the SCC for children 828 SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(sccModelId); 829 830 return create 831 // Import mode 832 ? _importChildren(idValue, scc, logger) 833 // Synchronization mode 834 : _synchronizeChildren(content, scc, attributeName, logger); 835 } 836 837 /** 838 * Import children 839 * @param idValue The parent content synchronization code 840 * @param scc The SCC 841 * @param logger The logger 842 * @return The imported children 843 */ 844 protected List<ModifiableContent> _importChildren(String idValue, SynchronizableContentsCollection scc, Logger logger) 845 { 846 // If SCC exists, search for children 847 if (scc != null && scc instanceof ApogeeSynchronizableContentsCollection) 848 { 849 // Synchronize or import children content 850 return ((ApogeeSynchronizableContentsCollection) scc).importOrSynchronizeContents(_getChildrenSearchParametersWithParent(idValue), logger); 851 } 852 853 return List.of(); 854 } 855 856 /** 857 * Synchronize children 858 * @param content Parent content 859 * @param scc The SCC 860 * @param attributeName The name of the attribute containing children 861 * @param logger The logger 862 * @return <code>true</code> if there are changes 863 */ 864 protected List<ModifiableContent> _synchronizeChildren(Content content, SynchronizableContentsCollection scc, String attributeName, Logger logger) 865 { 866 // First we get the existing children in Ametys 867 List<ModifiableContent> ametysChildren = Stream.of(content.getValue(attributeName, false, new ContentValue[0])) 868 .map(ContentValue::getContentIfExists) 869 .filter(Optional::isPresent) 870 .map(Optional::get) 871 .collect(Collectors.toList()); 872 873 // Get the children remote sync codes if needed 874 // These remote sync codes are needed if we want to add contents in Ametys from Apogee or delete obsolete contents 875 Set<String> childrenRemoteSyncCode = (removalSync() || addUnexistingChildren() || addExistingChildren()) 876 ? _getChildrenRemoteSyncCode(content, scc, logger) 877 : null; 878 879 // Can be null if we do not want to remove obsolete contents, add existing or unexisting children 880 // Or if the current content is not from Apogee 881 if (childrenRemoteSyncCode != null) 882 { 883 // Remove obsolete children if removalSync is active 884 if (removalSync()) 885 { 886 ametysChildren = ametysChildren.stream() 887 .filter(c -> !_isChildWillBeRemoved(c, scc, childrenRemoteSyncCode, logger)) 888 .collect(Collectors.toList()); 889 } 890 891 if (addExistingChildren() || addUnexistingChildren()) 892 { 893 // Then we add missing children if needed 894 for (String code : childrenRemoteSyncCode) 895 { 896 ModifiableContent child = scc.getContent(_odfLang, code); 897 898 // If the child with the given sync code does not exist, import it if the parameter to 899 // add unexisting children is checked 900 if (child == null) 901 { 902 if (addUnexistingChildren()) 903 { 904 ametysChildren.addAll(_importUnexistingChildren(scc, code, null, logger)); 905 } 906 } 907 // If the parameter to link existing children not already in the list is checked, 908 // it adds the content to the children list if it is not already in it. 909 else if (addExistingChildren() && !ametysChildren.contains(child)) 910 { 911 ametysChildren.add(child); 912 } 913 } 914 } 915 } 916 917 // Then we synchronize children in Ametys 918 // (it won't be synchronized twice because it reads the request parameters with handled contents) 919 for (ModifiableContent childContent : ametysChildren) 920 { 921 _apogeeSCCHelper.synchronizeContent(childContent, logger); 922 } 923 924 return ametysChildren; 925 } 926 927 /** 928 * Get the remote sync codes 929 * @param content Parent content 930 * @param scc the scc 931 * @param logger The logger 932 * @return the remote sync codes or null if the scc is not from Apogee 933 */ 934 protected Set<String> _getChildrenRemoteSyncCode(Content content, SynchronizableContentsCollection scc, Logger logger) 935 { 936 if (scc != null && scc instanceof AbstractApogeeSynchronizableContentsCollection) 937 { 938 String syncCode = content.getValue(getIdField()); 939 return ((AbstractApogeeSynchronizableContentsCollection) scc) 940 .search(_getChildrenSearchParametersWithParent(syncCode), 0, Integer.MAX_VALUE, null, logger) 941 .keySet(); 942 } 943 944 return null; 945 } 946 947 /** 948 * Get the children search parameters. 949 * @param parentSyncCode The parent synchronization code 950 * @return a {@link Map} of search parameters 951 */ 952 protected Map<String, Object> _getChildrenSearchParametersWithParent(String parentSyncCode) 953 { 954 Map<String, Object> searchParameters = new HashMap<>(); 955 searchParameters.put("parentCode", parentSyncCode); 956 return searchParameters; 957 } 958 959 /** 960 * Import an unexisting child in Ametys 961 * @param scc the scc to import the content 962 * @param syncCode the sync code of the content 963 * @param additionalParameters the additional params 964 * @param logger the logger 965 * @return the list of created children 966 */ 967 @SuppressWarnings("unchecked") 968 protected List<ModifiableContent> _importUnexistingChildren(SynchronizableContentsCollection scc, String syncCode, Map<String, List<Object>> additionalParameters, Logger logger) 969 { 970 try 971 { 972 return scc.importContent(syncCode, (Map<String, Object>) (Object) additionalParameters, logger); 973 } 974 catch (Exception e) 975 { 976 logger.error("An error occured while importing a new children content with syncCode '{}' from SCC '{}'", syncCode, scc.getId(), e); 977 } 978 979 return Collections.emptyList(); 980 } 981 982 /** 983 * True if the content will be removed from the structure 984 * @param content the content 985 * @param scc the scc 986 * @param childrenRemoteSyncCode the remote sync codes 987 * @param logger the logger 988 * @return <code>true</code> if the content will be removed from the structure 989 */ 990 protected boolean _isChildWillBeRemoved(ModifiableContent content, SynchronizableContentsCollection scc, Set<String> childrenRemoteSyncCode, Logger logger) 991 { 992 String syncCode = content.getValue(scc.getIdField()); 993 return !childrenRemoteSyncCode.contains(syncCode); 994 } 995 996 @Override 997 public List<ModifiableContent> importOrSynchronizeContents(Map<String, Object> searchParams, Logger logger) 998 { 999 return _importOrSynchronizeContents(searchParams, true, logger); 1000 } 1001 1002 @SuppressWarnings("unchecked") 1003 private Map<String, List<Object>> _transformOrgUnitAttribute(Map<String, List<Object>> remoteValues, Logger logger) 1004 { 1005 // Transform orgUnit values and import content if necessary (useful for Course and SubProgram) 1006 SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(OrgUnitSynchronizableContentsCollection.MODEL_ID); 1007 1008 List<Object> orgUnitCodes = remoteValues.get("orgUnit"); 1009 if (orgUnitCodes != null && !orgUnitCodes.isEmpty()) 1010 { 1011 List<?> orgUnitContents = null; 1012 String orgUnitCode = orgUnitCodes.get(0).toString(); 1013 1014 if (scc != null) 1015 { 1016 try 1017 { 1018 ModifiableContent orgUnitContent = scc.getContent(_odfLang, orgUnitCode); 1019 if (orgUnitContent == null) 1020 { 1021 orgUnitContents = scc.importContent(orgUnitCode, null, logger); 1022 } 1023 else 1024 { 1025 orgUnitContents = List.of(orgUnitContent); 1026 } 1027 } 1028 catch (Exception e) 1029 { 1030 logger.error("An error occured during the import of the OrgUnit identified by the synchronization code '{}'", orgUnitCode, e); 1031 } 1032 } 1033 1034 if (orgUnitContents == null) 1035 { 1036 // Impossible link to orgUnit 1037 remoteValues.remove("orgUnit"); 1038 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); 1039 } 1040 else 1041 { 1042 remoteValues.put("orgUnit", (List<Object>) orgUnitContents); 1043 } 1044 } 1045 1046 return remoteValues; 1047 } 1048 1049 @Override 1050 public boolean handleRightAssignmentContext() 1051 { 1052 // Rights on ODF contents are handled by ODFRightAssignmentContext 1053 return false; 1054 } 1055}