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