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