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