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