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