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.Collection; 029import java.util.Date; 030import java.util.HashMap; 031import java.util.HashSet; 032import java.util.LinkedHashMap; 033import java.util.LinkedHashSet; 034import java.util.LinkedList; 035import java.util.List; 036import java.util.Map; 037import java.util.Map.Entry; 038import java.util.Objects; 039import java.util.Optional; 040import java.util.Set; 041import java.util.stream.Collectors; 042import java.util.stream.Stream; 043 044import org.apache.avalon.framework.configuration.Configuration; 045import org.apache.avalon.framework.configuration.ConfigurationException; 046import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 047import org.apache.avalon.framework.context.Context; 048import org.apache.avalon.framework.context.ContextException; 049import org.apache.avalon.framework.context.Contextualizable; 050import org.apache.avalon.framework.service.ServiceException; 051import org.apache.avalon.framework.service.ServiceManager; 052import org.apache.cocoon.Constants; 053import org.apache.cocoon.components.ContextHelper; 054import org.apache.cocoon.environment.Request; 055import org.apache.commons.collections4.ListUtils; 056import org.apache.commons.collections4.SetUtils; 057import org.apache.commons.collections4.SetUtils.SetView; 058import org.apache.commons.io.IOUtils; 059import org.apache.commons.lang.StringUtils; 060import org.slf4j.Logger; 061 062import org.ametys.cms.content.external.ExternalizableMetadataHelper; 063import org.ametys.cms.content.external.ExternalizableMetadataProvider.ExternalizableMetadataStatus; 064import org.ametys.cms.contenttype.ContentType; 065import org.ametys.cms.data.ContentValue; 066import org.ametys.cms.repository.ModifiableContent; 067import org.ametys.cms.repository.ModifiableDefaultContent; 068import org.ametys.core.util.JSONUtils; 069import org.ametys.odf.ODFHelper; 070import org.ametys.plugins.contentio.ContentImporterHelper; 071import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection; 072import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection; 073import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionDAO; 074import org.ametys.plugins.odfsync.apogee.ApogeeDAO; 075import org.ametys.plugins.odfsync.apogee.scc.impl.OrgUnitSynchronizableContentsCollection; 076import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 077import org.ametys.plugins.repository.metadata.ModifiableRichText; 078import org.ametys.runtime.config.Config; 079import org.ametys.runtime.i18n.I18nizableText; 080import org.ametys.runtime.model.ModelItem; 081import org.ametys.runtime.model.type.ModelItemTypeConstants; 082 083import com.google.common.base.CharMatcher; 084import com.google.common.collect.ImmutableList; 085import com.google.common.collect.ImmutableMap; 086 087/** 088 * Abstract class for Apogee synchronization 089 */ 090public abstract class AbstractApogeeSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection implements Contextualizable, ApogeeSynchronizableContentsCollection 091{ 092 /** Name of parameter holding the data source id */ 093 public static final String PARAM_DATASOURCE_ID = "datasourceId"; 094 /** Name of parameter holding the administrative year */ 095 public static final String PARAM_YEAR = "year"; 096 /** Name of parameter holding the adding unexisting children parameter */ 097 public static final String PARAM_ADD_UNEXISTING_CHILDREN = "add-unexisting-children"; 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 /** The ODF helper */ 157 protected ODFHelper _odfHelper; 158 159 /** The Apogee SCC helper */ 160 protected ApogeeSynchronizableContentsCollectionHelper _apogeeSCCHelper; 161 162 @Override 163 public void service(ServiceManager manager) throws ServiceException 164 { 165 super.service(manager); 166 _apogeeDAO = (ApogeeDAO) manager.lookup(ApogeeDAO.ROLE); 167 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 168 _sccDAO = (SynchronizableContentsCollectionDAO) manager.lookup(SynchronizableContentsCollectionDAO.ROLE); 169 _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE); 170 _apogeeSCCHelper = (ApogeeSynchronizableContentsCollectionHelper) manager.lookup(ApogeeSynchronizableContentsCollectionHelper.ROLE); 171 } 172 173 @Override 174 public void contextualize(Context context) throws ContextException 175 { 176 _context = context; 177 } 178 179 @Override 180 protected void configureDataSource(Configuration configuration) throws ConfigurationException 181 { 182 @SuppressWarnings("resource") 183 InputStream is = null; 184 _odfLang = Config.getInstance().getValue("odf.programs.lang"); 185 try 186 { 187 org.apache.cocoon.environment.Context ctx = (org.apache.cocoon.environment.Context) _context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 188 File apogeeMapping = new File(ctx.getRealPath("/WEB-INF/param/odf/apogee-mapping.xml")); 189 if (!apogeeMapping.isFile()) 190 { 191 is = getClass().getResourceAsStream("/org/ametys/plugins/odfsync/apogee/apogee-mapping.xml"); 192 } 193 else 194 { 195 is = new FileInputStream(apogeeMapping); 196 } 197 Configuration cfg = new DefaultConfigurationBuilder().build(is); 198 Configuration child = cfg.getChild(getMappingName()); 199 if (child != null) 200 { 201 _criteria = new LinkedHashSet<>(); 202 _columns = new LinkedHashSet<>(); 203 _idColumn = child.getChild(__PARAM_ID_COLUMN).getValue(); 204 _mapping = new HashMap<>(); 205 _extFields = new HashSet<>(); 206 _syncFields = new HashSet<>(); 207 String mappingAsString = child.getChild(__PARAM_MAPPING).getValue(); 208 _mapping.put(getIdField(), ImmutableList.of(getIdColumn())); 209 if (StringUtils.isNotEmpty(mappingAsString)) 210 { 211 List<Object> mappingAsList = _jsonUtils.convertJsonToList(mappingAsString); 212 for (Object object : mappingAsList) 213 { 214 @SuppressWarnings("unchecked") 215 Map<String, Object> field = (Map<String, Object>) object; 216 217 String metadataRef = (String) field.get(__PARAM_MAPPING_METADATA_REF); 218 219 String[] attributes = ((String) field.get(__PARAM_MAPPING_ATTRIBUTE)).split(","); 220 _mapping.put(metadataRef, Arrays.asList(attributes)); 221 222 boolean isSynchronized = field.containsKey(__PARAM_MAPPING_SYNCHRO) ? (Boolean) field.get(__PARAM_MAPPING_SYNCHRO) : false; 223 if (isSynchronized) 224 { 225 _syncFields.add(metadataRef); 226 } 227 else 228 { 229 _extFields.add(metadataRef); 230 } 231 } 232 } 233 234 Configuration[] criteria = child.getChild(__PARAM_CRITERIA).getChildren(__PARAM_CRITERIA_CRITERION); 235 for (Configuration criterion : criteria) 236 { 237 String id = criterion.getChild(__PARAM_CRITERIA_CRITERION_ID).getValue(); 238 I18nizableText label = _getCriterionLabel(criterion.getChild(__PARAM_CRITERIA_CRITERION_LABEL), id); 239 String type = criterion.getChild(__PARAM_CRITERIA_CRITERION_TYPE).getValue("STRING"); 240 241 _criteria.add(new ApogeeCriterion(id, label, type)); 242 } 243 244 Configuration[] columns = child.getChild(__PARAM_COLUMNS).getChildren(__PARAM_COLUMNS_COLUMN); 245 for (Configuration column : columns) 246 { 247 _columns.add(column.getValue()); 248 } 249 } 250 } 251 catch (Exception e) 252 { 253 throw new ConfigurationException("Error while parsing apogee-mapping.xml", e); 254 } 255 finally 256 { 257 IOUtils.closeQuietly(is); 258 } 259 } 260 261 private I18nizableText _getCriterionLabel(Configuration configuration, String defaultValue) 262 { 263 if (configuration.getAttributeAsBoolean("i18n", false)) 264 { 265 return new I18nizableText("plugin.odf-sync", configuration.getValue(defaultValue)); 266 } 267 else 268 { 269 return new I18nizableText(configuration.getValue(defaultValue)); 270 } 271 } 272 273 @Override 274 public List<ModifiableDefaultContent> populate(Logger logger) 275 { 276 boolean isRequestAttributeOwner = false; 277 278 Request request = ContextHelper.getRequest(_context); 279 if (request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS) == null) 280 { 281 request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS, new HashSet<String>()); 282 request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_PARENT_CONTENTS, new HashSet<String>()); 283 isRequestAttributeOwner = true; 284 } 285 286 List<ModifiableDefaultContent> populatedContents = super.populate(logger); 287 288 _applyChangesToParentContent(logger); 289 290 if (isRequestAttributeOwner) 291 { 292 request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS); 293 request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_PARENT_CONTENTS); 294 } 295 296 return populatedContents; 297 } 298 299 @Override 300 protected List<ModifiableDefaultContent> _internalPopulate(Logger logger) 301 { 302 return _importOrSynchronizeContents(Map.of("isGlobalSync", true), false, logger); 303 } 304 305 @Override 306 protected Map<String, Map<String, Object>> internalSearch(Map<String, Object> parameters, int offset, int limit, List<Object> sort, Logger logger) 307 { 308 Map<String, Object> searchParams = new HashMap<>(parameters); 309 if (offset > 0) 310 { 311 searchParams.put("__offset", offset); 312 } 313 if (limit < Integer.MAX_VALUE) 314 { 315 searchParams.put("__limit", offset + limit); 316 } 317 searchParams.put("__order", _getSort(sort)); 318 319 // We don't use session.selectMap which reorder data 320 List<Map<String, Object>> requestValues = _search(searchParams, logger); 321 322 // Transform CLOBs to String 323 Set<String> clobColumns = getClobColumns(); 324 if (!clobColumns.isEmpty()) 325 { 326 for (Map<String, Object> contentValues : requestValues) 327 { 328 String idValue = contentValues.get(getIdColumn()).toString(); 329 for (String clobKey : getClobColumns()) 330 { 331 // Get the old values for the CLOB 332 @SuppressWarnings("unchecked") 333 Optional<List<Object>> oldValues = Optional.of(clobKey) 334 .map(contentValues::get) 335 .map(obj -> (List<Object>) obj); 336 337 if (oldValues.isPresent()) 338 { 339 // Get the new values for the CLOB 340 List<Object> newValues = oldValues.get() 341 .stream() 342 .map(value -> _transformClobToString(value, idValue, logger)) 343 .filter(Objects::nonNull) 344 .collect(Collectors.toList()); 345 346 // Set the transformed CLOB values 347 contentValues.put(clobKey, newValues); 348 } 349 } 350 } 351 } 352 353 // Reorganize results 354 String idColumn = getIdColumn(); 355 Map<String, Map<String, Object>> results = new LinkedHashMap<>(); 356 for (Map<String, Object> contentValues : requestValues) 357 { 358 results.put(contentValues.get(idColumn).toString(), contentValues); 359 } 360 361 for (Map<String, Object> result : results.values()) 362 { 363 result.put(SCC_UNIQUE_ID, result.get(getIdColumn())); 364 } 365 366 return results; 367 } 368 369 @Override 370 protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> parameters, Logger logger) 371 { 372 Map<String, Map<String, List<Object>>> remoteValues = new HashMap<>(); 373 374 Map<String, Map<String, Object>> results = internalSearch(parameters, 0, Integer.MAX_VALUE, null, logger); 375 376 if (results != null) 377 { 378 remoteValues = _sccHelper.organizeRemoteValuesByMetadata(results, _mapping); 379 } 380 381 return remoteValues; 382 } 383 384 @Override 385 protected List<ModifiableDefaultContent> _importOrSynchronizeContent(String idValue, Map<String, List<Object>> remoteValues, boolean forceImport, Logger logger) 386 { 387 return _importOrSynchronizeContent(idValue, _odfLang, remoteValues, forceImport, logger); 388 } 389 390 @Override 391 public List<ModifiableDefaultContent> importContent(String idValue, Map<String, Object> importParams, Logger logger) throws Exception 392 { 393 boolean isRequestAttributeOwner = false; 394 395 Request request = ContextHelper.getRequest(_context); 396 if (request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS) == null) 397 { 398 request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS, new HashSet<String>()); 399 request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_PARENT_CONTENTS, new HashSet<String>()); 400 isRequestAttributeOwner = true; 401 } 402 403 List<ModifiableDefaultContent> createdContents = new ArrayList<>(); 404 405 Map<String, Object> parameters = putIdParameter(idValue); 406 Map<String, Map<String, List<Object>>> results = getTransformedRemoteValues(parameters, logger); 407 if (!results.isEmpty()) 408 { 409 try 410 { 411 createdContents.add(_importContent(idValue, importParams, _odfLang, results.get(idValue), logger)); 412 } 413 catch (Exception e) 414 { 415 _nbError++; 416 logger.error("An error occurred while importing or synchronizing content", e); 417 } 418 } 419 420 _applyChangesToParentContent(logger); 421 422 if (isRequestAttributeOwner) 423 { 424 request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS); 425 request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_PARENT_CONTENTS); 426 } 427 428 return createdContents; 429 } 430 431 @Override 432 public void synchronizeContent(ModifiableDefaultContent content, Logger logger) throws Exception 433 { 434 boolean isRequestAttributeOwner = false; 435 436 Request request = ContextHelper.getRequest(_context); 437 if (request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS) == null) 438 { 439 request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS, new HashSet<String>()); 440 request.setAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_PARENT_CONTENTS, new HashSet<String>()); 441 isRequestAttributeOwner = true; 442 } 443 444 super.synchronizeContent(content, logger); 445 446 _applyChangesToParentContent(logger); 447 448 if (isRequestAttributeOwner) 449 { 450 request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS); 451 request.removeAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_PARENT_CONTENTS); 452 } 453 } 454 455 /** 456 * Apply changes to parent contents which are impacted by the synchronization 457 * @param logger the logger 458 */ 459 protected void _applyChangesToParentContent(Logger logger) 460 { 461 Request request = ContextHelper.getRequest(_context); 462 if (request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS) != null) 463 { 464 @SuppressWarnings("unchecked") 465 Set<String> handleContents = (Set<String>) request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_CONTENTS); 466 467 @SuppressWarnings("unchecked") 468 Set<String> parentContents = (HashSet<String>) request.getAttribute(ApogeeSynchronizableContentsCollectionHelper.HANDLE_PARENT_CONTENTS); 469 470 // Apply changes on parent content only if they are not handled by the synchronization 471 SetView<String> contentsToApplyChanges = SetUtils.difference(parentContents, handleContents); 472 for (String parentContentId : contentsToApplyChanges) 473 { 474 try 475 { 476 ModifiableDefaultContent parentContent = _resolver.resolveById(parentContentId); 477 applyChanges(parentContent, 22, org.ametys.plugins.contentio.synchronize.observation.ObservationConstants.EVENT_CONTENT_SYNCHRONIZED, logger); 478 } 479 catch (Exception e) 480 { 481 logger.error("An error occurred applying changes to content '{}'", parentContentId, e); 482 } 483 } 484 } 485 } 486 487 @Override 488 protected Map<String, Object> putIdParameter(String idValue) 489 { 490 Map<String, Object> parameters = new HashMap<>(); 491 parameters.put(getIdField(), idValue); 492 return parameters; 493 } 494 495 /** 496 * Search the contents with the search parameters. Use id parameter to search an unique content. 497 * @param searchParams Search parameters 498 * @param logger The logger 499 * @return A Map of mapped metadatas extract from Apogée database ordered by content unique Apogée ID 500 */ 501 protected abstract List<Map<String, Object>> _search(Map<String, Object> searchParams, Logger logger); 502 503 /** 504 * Convert the {@link BigDecimal} values retrieved from database into long values 505 * @param searchResults The initial search results from database 506 * @return The converted search results 507 */ 508 protected List<Map<String, Object>> _convertBigDecimal(List<Map<String, Object>> searchResults) 509 { 510 List<Map<String, Object>> convertedSearchResults = new ArrayList<>(); 511 512 for (Map<String, Object> searchResult : searchResults) 513 { 514 for (String key : searchResult.keySet()) 515 { 516 searchResult.put(key, _convertBigDecimal(getContentType(), key, searchResult.get(key))); 517 } 518 519 convertedSearchResults.add(searchResult); 520 } 521 522 return convertedSearchResults; 523 } 524 525 /** 526 * Convert the object in parameter to a long if it's a {@link BigDecimal}, otherwise return the object itself. 527 * @param contentTypeId The content type of the parent content 528 * @param attributeName The metadata name 529 * @param objectToConvert The object to convert if necessary 530 * @return The converted object 531 */ 532 protected Object _convertBigDecimal(String contentTypeId, String attributeName, Object objectToConvert) 533 { 534 if (objectToConvert instanceof BigDecimal) 535 { 536 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 537 if (contentType.hasModelItem(attributeName)) 538 { 539 ModelItem definition = contentType.getModelItem(attributeName); 540 String typeId = definition.getType().getId(); 541 switch (typeId) 542 { 543 case ModelItemTypeConstants.DOUBLE_TYPE_ID: 544 return ((BigDecimal) objectToConvert).doubleValue(); 545 case ModelItemTypeConstants.LONG_TYPE_ID: 546 return ((BigDecimal) objectToConvert).longValue(); 547 default: 548 // Do nothing 549 break; 550 } 551 } 552 return ((BigDecimal) objectToConvert).toString(); 553 } 554 return objectToConvert; 555 } 556 557 /** 558 * Transform CLOB value to String value. 559 * @param value The input value 560 * @param idValue The identifier of the program 561 * @param logger The logger 562 * @return the same value, with CLOB transformed to String. 563 */ 564 protected Object _transformClobToString(Object value, String idValue, Logger logger) 565 { 566 if (value instanceof Clob) 567 { 568 Clob clob = (Clob) value; 569 try 570 { 571 String strValue = IOUtils.toString(clob.getCharacterStream()); 572 return CharMatcher.javaIsoControl().and(CharMatcher.anyOf("\r\n\t").negate()).removeFrom(strValue); 573 } 574 catch (SQLException | IOException e) 575 { 576 logger.error("Unable to get education add elements from the program '{}'.", idValue, e); 577 return null; 578 } 579 finally 580 { 581 try 582 { 583 clob.free(); 584 } 585 catch (SQLException e) 586 { 587 // Ignore the exception. 588 } 589 } 590 } 591 592 return value; 593 } 594 595 /** 596 * Get the list of CLOB column's names. 597 * @return The list of the CLOB column's names to transform to {@link String} 598 */ 599 protected Set<String> getClobColumns() 600 { 601 return Set.of(); 602 } 603 604 /** 605 * Transform a {@link List} of {@link Map} to a {@link Map} of {@link List} computed by keys (lines to columns). 606 * @param lines {@link List} to reorganize 607 * @return {@link Map} of {@link List} 608 */ 609 protected Map<String, List<Object>> _transformListOfMap2MapOfList(List<Map<String, Object>> lines) 610 { 611 return lines.stream() 612 .filter(Objects::nonNull) 613 .map(Map::entrySet) 614 .flatMap(Collection::stream) 615 .filter(e -> Objects.nonNull(e.getValue())) 616 .collect( 617 Collectors.toMap( 618 Entry::getKey, 619 e -> List.of(e.getValue()), 620 (l1, l2) -> ListUtils.union(l1, l2) 621 ) 622 ); 623 } 624 625 /** 626 * Get the name of the mapping. 627 * @return the mapping name 628 */ 629 protected abstract String getMappingName(); 630 631 /** 632 * Get the id of data source 633 * @return The id of data source 634 */ 635 protected String getDataSourceId() 636 { 637 return (String) getParameterValues().get(PARAM_DATASOURCE_ID); 638 } 639 640 /** 641 * Get the administrative year 642 * @return The administrative year 643 */ 644 protected String getYear() 645 { 646 return (String) getParameterValues().get(PARAM_YEAR); 647 } 648 649 /** 650 * Check if unexisting contents in Ametys should be added from data source 651 * @return <code>true</code> if unexisting contents in Ametys should be added from data source, default value is <code>false</code> 652 */ 653 protected boolean addUnexistingChildren() 654 { 655 return Boolean.valueOf((String) getParameterValues().getOrDefault(PARAM_ADD_UNEXISTING_CHILDREN, Boolean.TRUE.toString())); 656 } 657 658 /** 659 * Get the identifier column (can be a concatened column). 660 * @return the column id 661 */ 662 protected String getIdColumn() 663 { 664 return _idColumn; 665 } 666 667 @Override 668 public String getIdField() 669 { 670 return "apogeeSyncCode"; 671 } 672 673 @Override 674 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 675 { 676 return _syncFields; 677 } 678 679 @Override 680 public Set<String> getExternalOnlyFields(Map<String, Object> additionalParameters) 681 { 682 return _extFields; 683 } 684 685 @Override 686 protected boolean _fillContent(Map<String, List<Object>> remoteValues, ModifiableDefaultContent content, boolean create, Logger logger) 687 { 688 ModifiableCompositeMetadata holder = content.getMetadataHolder(); 689 690 boolean hasChanges = _fillRichTexts(remoteValues, content, create, logger); 691 hasChanges = super._fillContent(remoteValues, content, create, logger) || hasChanges; 692 hasChanges = _handleAdditionalMetadata(holder, create) || hasChanges; 693 694 if (!holder.hasMetadata(getIdField())) 695 { 696 holder.setMetadata(getIdField(), remoteValues.get(getIdField()).get(0).toString()); 697 hasChanges |= create; 698 } 699 700 return hasChanges; 701 } 702 703 /** 704 * Fill the richt texts of the content with the remote values. 705 * @param remoteValues The remote values 706 * @param content The content to synchronize 707 * @param create <code>true</code> if content is creating, false if it is updated 708 * @param logger The logger 709 * @return <code>true</code> if changes were made 710 */ 711 protected boolean _fillRichTexts(Map<String, List<Object>> remoteValues, ModifiableDefaultContent content, boolean create, Logger logger) 712 { 713 boolean hasChanges = false; 714 715 ModifiableCompositeMetadata holder = content.getMetadataHolder(); 716 Map<String, Object> params = Map.of("contentTypes", List.of(getContentType())); 717 718 for (String metadataName : getRichTextFields()) 719 { 720 if (remoteValues.containsKey(metadataName)) 721 { 722 boolean synchronize = getLocalAndExternalFields(params).contains(metadataName); 723 724 ModifiableRichText richText = ExternalizableMetadataHelper.getRichText(holder, metadataName, synchronize ? ExternalizableMetadataStatus.EXTERNAL : ExternalizableMetadataStatus.LOCAL, true); 725 richText.setMimeType("text/xml"); 726 richText.setLastModified(new Date()); 727 try 728 { 729 _setRichTextValue(richText, remoteValues.get(metadataName)); 730 } 731 catch (IOException e) 732 { 733 logger.error("An error occured while parsing the rich text '{}' of the content '{}'", metadataName, content.getTitle(), e); 734 } 735 736 if (synchronize && create) 737 { 738 // Force external status 739 ExternalizableMetadataHelper.updateStatus(holder, metadataName, ExternalizableMetadataStatus.EXTERNAL); 740 } 741 742 remoteValues.remove(metadataName); 743 hasChanges = true; 744 } 745 } 746 747 return hasChanges; 748 } 749 750 /** 751 * Set the value on the rich text. 752 * @param richText The rich text to update 753 * @param remoteValue The remote value of the rich text (can be composed of several texts to concatenate) 754 * @throws IOException if an exception occurs 755 */ 756 protected void _setRichTextValue(ModifiableRichText richText, List<Object> remoteValue) throws IOException 757 { 758 List<String> lines = new LinkedList<>(); 759 for (Object remoteText : remoteValue) 760 { 761 lines.add(remoteText.toString()); 762 } 763 764 String docbook = ContentImporterHelper.textToDocbook(lines.toArray(new String[lines.size()])); 765 766 try (ByteArrayInputStream is = new ByteArrayInputStream(docbook.getBytes("UTF-8"))) 767 { 768 richText.setInputStream(is); 769 } 770 } 771 772 /** 773 * Method to add additional metadata on import or synchronize. 774 * @param holder The holder of the content to update 775 * @param create If we are on creation mode 776 * @return <code>true</code> if changes has been made 777 */ 778 protected boolean _handleAdditionalMetadata(ModifiableCompositeMetadata holder, boolean create) 779 { 780 // Do nothing 781 return false; 782 } 783 784 /** 785 * Get the list of rich text fields of the imported content. 786 * @return The list of the rich text fields metadata name 787 */ 788 protected Set<String> getRichTextFields() 789 { 790 return new HashSet<>(); 791 } 792 793 @Override 794 protected void configureSearchModel() 795 { 796 for (ApogeeCriterion criterion : _criteria) 797 { 798 _searchModelConfiguration.addCriterion(criterion.getId(), criterion.getLabel(), criterion.getType()); 799 } 800 for (String columnName : _columns) 801 { 802 _searchModelConfiguration.addColumn(columnName); 803 } 804 } 805 806 @Override 807 protected boolean additionalCommonOperations(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Map<String, Object> importParams, boolean create, Logger logger) 808 { 809 boolean hasChanges = super.additionalCommonOperations(content, remoteValues, importParams, create, logger); 810 811 Object parentId = importParams != null && importParams.containsKey("parentId") ? importParams.get("parentId") : null; 812 ModifiableDefaultContent parentContent = parentId != null ? _resolver.resolveById(parentId.toString()) : null; 813 814 hasChanges = handleParent(content, parentContent, logger) || hasChanges; 815 hasChanges = handleChildren(content, create, logger) || hasChanges; 816 hasChanges = setAdditionalMetadata(content, remoteValues, create, logger) || hasChanges; 817 818 return hasChanges; 819 } 820 821 /** 822 * Set the parent metadata and invert relation. 823 * @param currentContent Current content 824 * @param parentContent Parent content to set 825 * @param logger The logger 826 * @return <code>true</code> if there are changes 827 */ 828 protected boolean handleParent(ModifiableDefaultContent currentContent, ModifiableDefaultContent parentContent, Logger logger) 829 { 830 // Nothing to do by default 831 return false; 832 } 833 834 /** 835 * Set the children metadata and invert relation, import and synchronize the children too. 836 * @param content Current content 837 * @param create true if the content has been newly created 838 * @param logger The logger 839 * @return <code>true</code> if there are changes 840 */ 841 protected boolean handleChildren(ModifiableDefaultContent content, boolean create, Logger logger) 842 { 843 return importOrSynchronizeChildren(content, getChildrenSCCModelId(), getChildrenMetadataName(), getChildrenInvertMetadataName(), create, logger); 844 } 845 846 /** 847 * Get the children SCC model id. Can be null if no implementation is defined. 848 * @return the children SCC model id 849 */ 850 protected String getChildrenSCCModelId() 851 { 852 // Default implementation 853 return null; 854 } 855 856 /** 857 * Get the metadata name to get children 858 * @return the metadata name to get children 859 */ 860 protected abstract String getChildrenMetadataName(); 861 862 /** 863 * Get the metadata name to get parent from children 864 * @return the metadata name to get parent from children 865 */ 866 protected abstract String getChildrenInvertMetadataName(); 867 868 /** 869 * Set the additional metadata. 870 * @param content Current content 871 * @param remoteValues Values of the content 872 * @param create true if the content has been newly created 873 * @param logger The logger 874 * @return <code>true</code> if there are changes 875 */ 876 protected boolean setAdditionalMetadata(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, boolean create, Logger logger) 877 { 878 // Nothing to do by default 879 return false; 880 } 881 882 /** 883 * Transform the given {@link List} of {@link Object} to a {@link String} representing the ordered fields for SQL. 884 * @param sortList The sort list object to transform to the list of ordered fields compatible with SQL. 885 * @return A string representing the list of ordered fields 886 */ 887 @SuppressWarnings("unchecked") 888 protected String _getSort(List<Object> sortList) 889 { 890 if (sortList != null) 891 { 892 StringBuilder sort = new StringBuilder(); 893 894 for (Object sortValueObj : sortList) 895 { 896 Map<String, Object> sortValue = (Map<String, Object>) sortValueObj; 897 898 sort.append(sortValue.get("property")); 899 if (sortValue.containsKey("direction")) 900 { 901 sort.append(" "); 902 sort.append(sortValue.get("direction")); 903 sort.append(","); 904 } 905 else 906 { 907 sort.append(" ASC,"); 908 } 909 } 910 911 sort.deleteCharAt(sort.length() - 1); 912 913 return sort.toString(); 914 } 915 916 return null; 917 } 918 919 @Override 920 public int getTotalCount(Map<String, Object> parameters, Logger logger) 921 { 922 // Remove empty parameters 923 Map<String, Object> searchParams = new HashMap<>(); 924 for (String parameterName : parameters.keySet()) 925 { 926 Object parameterValue = parameters.get(parameterName); 927 if (parameterValue != null && !parameterValue.toString().isEmpty()) 928 { 929 searchParams.put(parameterName, parameterValue); 930 } 931 } 932 933 searchParams.put("__count", true); 934 935 List<Map<String, Object>> results = _search(searchParams, logger); 936 if (results != null && !results.isEmpty()) 937 { 938 return Integer.valueOf(results.get(0).get("COUNT(*)").toString()).intValue(); 939 } 940 941 return 0; 942 } 943 944 @Override 945 protected ModifiableDefaultContent _importContent(String idValue, Map<String, Object> importParams, String lang, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 946 { 947 ModifiableDefaultContent content = super._importContent(idValue, importParams, lang, _transformOrgUnitMetadata(remoteValues, logger), logger); 948 if (content != null) 949 { 950 _apogeeSCCHelper.addToHandleContents(content.getId()); 951 } 952 return content; 953 } 954 955 @Override 956 protected ModifiableDefaultContent _synchronizeContent(ModifiableDefaultContent content, Map<String, List<Object>> remoteValues, Logger logger) throws Exception 957 { 958 if (!_apogeeSCCHelper.addToHandleContents(content.getId())) 959 { 960 return content; 961 } 962 return super._synchronizeContent(content, _transformOrgUnitMetadata(remoteValues, logger), logger); 963 } 964 965 /** 966 * Import and synchronize children of the given content, then edit the structure of the content and its children. 967 * 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 968 * @param content Parent content 969 * @param sccModelId SCC model ID 970 * @param metadataName Metadata name to set 971 * @param invertMetadataName Metadata name of the invert relation 972 * @param create true if the content has been newly created 973 * @param logger The logger 974 * @return <code>true</code> if there are changes 975 */ 976 protected boolean importOrSynchronizeChildren(ModifiableDefaultContent content, String sccModelId, String metadataName, String invertMetadataName, boolean create, Logger logger) 977 { 978 // Get the SCC for children 979 SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(sccModelId); 980 981 return create 982 // Import mode 983 ? _importChildren(content, scc, metadataName, invertMetadataName, logger) 984 // Synchronization mode 985 : _synchronizeChildren(content, scc, metadataName, invertMetadataName, logger); 986 } 987 988 989 990 /** 991 * Import children 992 * @param content Parent content 993 * @param scc the scc 994 * @param metadataName Metadata name to set 995 * @param invertMetadataName Metadata name of the invert relation 996 * @param logger The logger 997 * @return <code>true</code> if there are changes 998 */ 999 protected boolean _importChildren(ModifiableDefaultContent content, SynchronizableContentsCollection scc, String metadataName, String invertMetadataName, Logger logger) 1000 { 1001 // If SCC exists, search for children 1002 if (scc != null && scc instanceof ApogeeSynchronizableContentsCollection) 1003 { 1004 // Synchronize or import children content 1005 ModifiableCompositeMetadata cm = content.getMetadataHolder(); 1006 String syncCode = cm.getString(getIdField()); 1007 List<ModifiableDefaultContent> importedOrSynchronizedChildren = ((ApogeeSynchronizableContentsCollection) scc).importOrSynchronizeContents(ImmutableMap.of("parentCode", syncCode), logger); 1008 1009 return _resetChildren(content, importedOrSynchronizedChildren, metadataName, invertMetadataName, logger); 1010 } 1011 1012 return false; 1013 } 1014 1015 /** 1016 * Synchronize children 1017 * @param content Parent content 1018 * @param scc the scc 1019 * @param metadataName Metadata name to set 1020 * @param invertMetadataName Metadata name of the invert relation 1021 * @param logger The logger 1022 * @return <code>true</code> if there are changes 1023 */ 1024 protected boolean _synchronizeChildren(ModifiableDefaultContent content, SynchronizableContentsCollection scc, String metadataName, String invertMetadataName, Logger logger) 1025 { 1026 // Get the children remote sync codes if needed 1027 // These remote sync codes are needed if we want to add unexisting contents in Ametys from Apogee or delete obsolete contents 1028 Set<String> childrenRemoteSyncCode = (removalSync() || addUnexistingChildren()) 1029 ? _getChildrenRemoteSyncCode(content, scc, logger) 1030 : null; 1031 1032 // First we get the existing children in Ametys 1033 List<ModifiableContent> ametysChildren = Stream.of(content.getValue(metadataName, false, new ContentValue[0])) 1034 .map(ContentValue::getContentIfExists) 1035 .filter(Optional::isPresent) 1036 .map(Optional::get) 1037 .collect(Collectors.toList()); 1038 1039 // Remove obsolete children if removeSync is active 1040 if (childrenRemoteSyncCode != null && removalSync()) 1041 { 1042 ametysChildren = ametysChildren.stream() 1043 .filter(c -> !_isChildWillBeRemoved(c, scc, childrenRemoteSyncCode, logger)) 1044 .collect(Collectors.toList()); 1045 } 1046 1047 // Then we synchronize the existing children in Ametys 1048 for (ModifiableContent childContent : ametysChildren) 1049 { 1050 _apogeeSCCHelper.synchronizeContent(childContent, logger); 1051 } 1052 1053 // Then we add unexisting children if needed 1054 if (childrenRemoteSyncCode != null && addUnexistingChildren()) 1055 { 1056 ametysChildren.addAll(_addUnexistingChildren(scc, childrenRemoteSyncCode, logger)); 1057 } 1058 1059 // Reset children if needed 1060 if (childrenRemoteSyncCode != null && (removalSync() || addUnexistingChildren())) 1061 { 1062 return _resetChildren(content, ametysChildren, metadataName, invertMetadataName, logger); 1063 } 1064 1065 return false; 1066 } 1067 1068 /** 1069 * Get the remote sync codes 1070 * @param content Parent content 1071 * @param scc the scc 1072 * @param logger The logger 1073 * @return the remote sync codes or null if the scc is not from Apogee 1074 */ 1075 protected Set<String> _getChildrenRemoteSyncCode(ModifiableDefaultContent content, SynchronizableContentsCollection scc, Logger logger) 1076 { 1077 if (scc != null && scc instanceof AbstractApogeeSynchronizableContentsCollection) 1078 { 1079 String syncCode = content.getMetadataHolder().getString(getIdField()); 1080 return ((AbstractApogeeSynchronizableContentsCollection) scc) 1081 .search(ImmutableMap.of("parentCode", syncCode), 0, Integer.MAX_VALUE, null, logger) 1082 .keySet(); 1083 } 1084 1085 return null; 1086 } 1087 1088 /** 1089 * Add unexisting children in Ametys from Apogee 1090 * @param scc the scc to import contents 1091 * @param childrenRemoteSyncCode the remote sync codes 1092 * @param logger the logger 1093 * @return the list of unexisting contents 1094 */ 1095 protected List<ModifiableContent> _addUnexistingChildren(SynchronizableContentsCollection scc, Set<String> childrenRemoteSyncCode, Logger logger) 1096 { 1097 return childrenRemoteSyncCode 1098 .stream() 1099 .filter(code -> scc.getContent(_odfLang, code) == null) // get only remote content values which doesn't exist in Ametys 1100 .flatMap(code -> _importUnexistingChildren(scc, code, null, logger)) 1101 .collect(Collectors.toList()); 1102 } 1103 1104 /** 1105 * Import an unexisting child in Ametys 1106 * @param scc the scc to import the content 1107 * @param syncCode the sync code of the content 1108 * @param params the additional params 1109 * @param logger the logger 1110 * @return the list of created children 1111 */ 1112 @SuppressWarnings("unchecked") 1113 protected Stream<ModifiableDefaultContent> _importUnexistingChildren(SynchronizableContentsCollection scc, String syncCode, Map<String, List<Object>> params, Logger logger) 1114 { 1115 try 1116 { 1117 return scc.importContent(syncCode, (Map<String, Object>) (Object) params, logger).stream(); 1118 } 1119 catch (Exception e) 1120 { 1121 logger.error("An error occured while importing a new children content with syncCode '{}' from SCC '{}'", syncCode, scc.getId(), e); 1122 } 1123 1124 return Stream.empty(); 1125 } 1126 1127 /** 1128 * True if the content will be removed from the structure 1129 * @param content the content 1130 * @param scc the scc 1131 * @param childrenRemoteSyncCode the remote sync codes 1132 * @param logger the logger 1133 * @return <code>true</code> if the content will be removed from the structure 1134 */ 1135 protected boolean _isChildWillBeRemoved(ModifiableContent content, SynchronizableContentsCollection scc, Set<String> childrenRemoteSyncCode, Logger logger) 1136 { 1137 String syncCode = content.getMetadataHolder().getString(scc.getIdField(), null); 1138 return !childrenRemoteSyncCode.contains(syncCode); 1139 } 1140 1141 /** 1142 * Reset the children of the structure to force with the children from Apogée. 1143 * @param content Parent content 1144 * @param importedOrSynchronizedChildren the imported or synchronized children 1145 * @param metadataName Metadata name to set 1146 * @param invertMetadataName Metadata name of the invert relation 1147 * @param logger The logger 1148 * @return <code>true</code> if there are changes 1149 */ 1150 protected boolean _resetChildren(ModifiableDefaultContent content, List<? extends ModifiableContent> importedOrSynchronizedChildren , String metadataName, String invertMetadataName, Logger logger) 1151 { 1152 boolean hasChanges = false; 1153 1154 ModifiableCompositeMetadata cm = content.getMetadataHolder(); 1155 1156 // Remove all children relations 1157 ContentValue[] oldChildren = content.getValue(metadataName, false, new ContentValue[0]); 1158 if (oldChildren.length > 0) 1159 { 1160 hasChanges = ExternalizableMetadataHelper.removeMetadataIfExists(cm, metadataName) || hasChanges; 1161 1162 for (ContentValue child : oldChildren) 1163 { 1164 Optional<ModifiableContent> childContent = child.getContentIfExists(); 1165 if (childContent.isPresent()) 1166 { 1167 hasChanges = _updateRelation(childContent.get().getMetadataHolder(), invertMetadataName, content, true) || hasChanges; 1168 } 1169 } 1170 } 1171 1172 if (!importedOrSynchronizedChildren.isEmpty()) 1173 { 1174 // Update child relation to link to the parent 1175 ModifiableDefaultContent[] newChildren = importedOrSynchronizedChildren.toArray(new ModifiableDefaultContent[importedOrSynchronizedChildren.size()]); 1176 hasChanges = ExternalizableMetadataHelper.setMetadata(cm, metadataName, newChildren) || hasChanges; 1177 1178 // Update parent relation on each children 1179 for (ModifiableDefaultContent child : newChildren) 1180 { 1181 hasChanges = _updateRelation(child.getMetadataHolder(), invertMetadataName, content) || hasChanges; 1182 } 1183 } 1184 1185 return hasChanges; 1186 } 1187 1188 @Override 1189 public List<ModifiableDefaultContent> importOrSynchronizeContents(Map<String, Object> searchParams, Logger logger) 1190 { 1191 return _importOrSynchronizeContents(searchParams, true, logger); 1192 } 1193 1194 @SuppressWarnings("unchecked") 1195 private Map<String, List<Object>> _transformOrgUnitMetadata(Map<String, List<Object>> remoteValues, Logger logger) 1196 { 1197 // Transform orgUnit values and import content if necessary (useful for Course and SubProgram) 1198 SynchronizableContentsCollection scc = _sccHelper.getSCCFromModelId(OrgUnitSynchronizableContentsCollection.MODEL_ID); 1199 1200 List<Object> orgUnitCodes = remoteValues.get("orgUnit"); 1201 if (orgUnitCodes != null && !orgUnitCodes.isEmpty()) 1202 { 1203 List<?> orgUnitContents = null; 1204 String orgUnitCode = orgUnitCodes.get(0).toString(); 1205 1206 if (scc != null) 1207 { 1208 try 1209 { 1210 ModifiableDefaultContent orgUnitContent = scc.getContent(_odfLang, orgUnitCode); 1211 if (orgUnitContent == null) 1212 { 1213 orgUnitContents = scc.importContent(orgUnitCode, null, logger); 1214 } 1215 else 1216 { 1217 orgUnitContents = ImmutableList.of(orgUnitContent); 1218 } 1219 } 1220 catch (Exception e) 1221 { 1222 logger.error("An error occured during the import of the OrgUnit identified by the synchronization code '{}'", orgUnitCode, e); 1223 } 1224 } 1225 1226 if (orgUnitContents == null) 1227 { 1228 // Impossible link to orgUnit 1229 remoteValues.remove("orgUnit"); 1230 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); 1231 } 1232 else 1233 { 1234 remoteValues.put("orgUnit", (List<Object>) orgUnitContents); 1235 } 1236 } 1237 1238 return remoteValues; 1239 } 1240 1241 @Override 1242 public boolean handleRightAssignmentContext() 1243 { 1244 // Rights on ODF contents are handled by ODFRightAssignmentContext 1245 return false; 1246 } 1247}