001/* 002 * Copyright 2016 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.contentio.synchronize; 017 018import java.io.File; 019import java.io.FileNotFoundException; 020import java.io.FileOutputStream; 021import java.io.IOException; 022import java.io.OutputStream; 023import java.nio.file.Files; 024import java.nio.file.StandardCopyOption; 025import java.time.Instant; 026import java.time.temporal.ChronoUnit; 027import java.util.ArrayList; 028import java.util.HashMap; 029import java.util.LinkedHashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Properties; 033import java.util.function.Function; 034import java.util.stream.Stream; 035 036import javax.xml.transform.OutputKeys; 037import javax.xml.transform.TransformerConfigurationException; 038import javax.xml.transform.TransformerFactory; 039import javax.xml.transform.TransformerFactoryConfigurationError; 040import javax.xml.transform.sax.SAXTransformerFactory; 041import javax.xml.transform.sax.TransformerHandler; 042import javax.xml.transform.stream.StreamResult; 043 044import org.apache.avalon.framework.activity.Disposable; 045import org.apache.avalon.framework.activity.Initializable; 046import org.apache.avalon.framework.component.Component; 047import org.apache.avalon.framework.configuration.Configuration; 048import org.apache.avalon.framework.configuration.ConfigurationException; 049import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 050import org.apache.avalon.framework.context.Context; 051import org.apache.avalon.framework.context.ContextException; 052import org.apache.avalon.framework.context.Contextualizable; 053import org.apache.avalon.framework.service.ServiceException; 054import org.apache.avalon.framework.service.ServiceManager; 055import org.apache.avalon.framework.service.Serviceable; 056import org.apache.cocoon.ProcessingException; 057import org.apache.cocoon.components.LifecycleHelper; 058import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 059import org.apache.cocoon.xml.AttributesImpl; 060import org.apache.cocoon.xml.XMLUtils; 061import org.apache.commons.lang3.StringUtils; 062import org.apache.xml.serializer.OutputPropertiesFactory; 063import org.slf4j.Logger; 064import org.slf4j.LoggerFactory; 065import org.xml.sax.ContentHandler; 066import org.xml.sax.SAXException; 067 068import org.ametys.cms.contenttype.ContentType; 069import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 070import org.ametys.cms.languages.LanguagesManager; 071import org.ametys.core.datasource.AbstractDataSourceManager.DataSourceDefinition; 072import org.ametys.core.datasource.LDAPDataSourceManager; 073import org.ametys.core.datasource.SQLDataSourceManager; 074import org.ametys.core.ui.Callable; 075import org.ametys.core.user.directory.UserDirectory; 076import org.ametys.core.user.directory.UserDirectoryFactory; 077import org.ametys.core.user.directory.UserDirectoryModel; 078import org.ametys.core.user.population.UserPopulation; 079import org.ametys.core.user.population.UserPopulationDAO; 080import org.ametys.plugins.contentio.synchronize.impl.DefaultSynchronizingContentOperator; 081import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory; 082import org.ametys.plugins.core.impl.user.directory.LdapUserDirectory; 083import org.ametys.plugins.workflow.support.WorkflowHelper; 084import org.ametys.runtime.i18n.I18nizableText; 085import org.ametys.runtime.model.DefinitionContext; 086import org.ametys.runtime.model.ElementDefinition; 087import org.ametys.runtime.model.ModelItem; 088import org.ametys.runtime.model.checker.ItemCheckerTestFailureException; 089import org.ametys.runtime.model.type.ModelItemTypeConstants; 090import org.ametys.runtime.parameter.Validator; 091import org.ametys.runtime.plugin.component.AbstractLogEnabled; 092import org.ametys.runtime.plugin.component.LogEnabled; 093import org.ametys.runtime.util.AmetysHomeHelper; 094 095/** 096 * DAO for accessing {@link SynchronizableContentsCollection} 097 */ 098public class SynchronizableContentsCollectionDAO extends AbstractLogEnabled implements Component, Serviceable, Initializable, Contextualizable, Disposable 099{ 100 /** Avalon Role */ 101 public static final String ROLE = SynchronizableContentsCollectionDAO.class.getName(); 102 103 /** Separator for parameters with model id as prefix */ 104 public static final String SCC_PARAMETERS_SEPARATOR = "$"; 105 106 private static File __CONFIGURATION_FILE; 107 108 private Map<String, SynchronizableContentsCollection> _synchronizableCollections; 109 private long _lastFileReading; 110 111 private SynchronizeContentsCollectionModelExtensionPoint _syncCollectionModelEP; 112 private ContentTypeExtensionPoint _contentTypeEP; 113 private UserPopulationDAO _userPopulationDAO; 114 private UserDirectoryFactory _userDirectoryFactory; 115 private WorkflowHelper _workflowHelper; 116 private SynchronizingContentOperatorExtensionPoint _synchronizingContentOperatorEP; 117 private LanguagesManager _languagesManager; 118 119 private ServiceManager _smanager; 120 private Context _context; 121 122 private SQLDataSourceManager _sqlDataSourceManager; 123 private LDAPDataSourceManager _ldapDataSourceManager; 124 125 @Override 126 public void initialize() throws Exception 127 { 128 __CONFIGURATION_FILE = new File(AmetysHomeHelper.getAmetysHome(), "config" + File.separator + "synchronizable-collections.xml"); 129 _synchronizableCollections = new HashMap<>(); 130 _lastFileReading = 0; 131 } 132 133 @Override 134 public void contextualize(Context context) throws ContextException 135 { 136 _context = context; 137 } 138 139 @Override 140 public void service(ServiceManager smanager) throws ServiceException 141 { 142 _smanager = smanager; 143 _syncCollectionModelEP = (SynchronizeContentsCollectionModelExtensionPoint) smanager.lookup(SynchronizeContentsCollectionModelExtensionPoint.ROLE); 144 _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 145 _userPopulationDAO = (UserPopulationDAO) smanager.lookup(UserPopulationDAO.ROLE); 146 _userDirectoryFactory = (UserDirectoryFactory) smanager.lookup(UserDirectoryFactory.ROLE); 147 _workflowHelper = (WorkflowHelper) smanager.lookup(WorkflowHelper.ROLE); 148 _synchronizingContentOperatorEP = (SynchronizingContentOperatorExtensionPoint) smanager.lookup(SynchronizingContentOperatorExtensionPoint.ROLE); 149 _languagesManager = (LanguagesManager) smanager.lookup(LanguagesManager.ROLE); 150 } 151 152 private SQLDataSourceManager _getSQLDataSourceManager() 153 { 154 if (_sqlDataSourceManager == null) 155 { 156 try 157 { 158 _sqlDataSourceManager = (SQLDataSourceManager) _smanager.lookup(SQLDataSourceManager.ROLE); 159 } 160 catch (ServiceException e) 161 { 162 throw new RuntimeException(e); 163 } 164 } 165 166 return _sqlDataSourceManager; 167 } 168 169 private LDAPDataSourceManager _getLDAPDataSourceManager() 170 { 171 if (_ldapDataSourceManager == null) 172 { 173 try 174 { 175 _ldapDataSourceManager = (LDAPDataSourceManager) _smanager.lookup(LDAPDataSourceManager.ROLE); 176 } 177 catch (ServiceException e) 178 { 179 throw new RuntimeException(e); 180 } 181 } 182 183 return _ldapDataSourceManager; 184 } 185 186 /** 187 * Gets a synchronizable contents collection to JSON format 188 * @param collectionId The id of the synchronizable contents collection to get 189 * @return An object representing a {@link SynchronizableContentsCollection} 190 */ 191 @Callable 192 public Map<String, Object> getSynchronizableContentsCollectionAsJson(String collectionId) 193 { 194 return getSynchronizableContentsCollectionAsJson(getSynchronizableContentsCollection(collectionId)); 195 } 196 197 /** 198 * Gets a synchronizable contents collection to JSON format 199 * @param collection The synchronizable contents collection to get 200 * @return An object representing a {@link SynchronizableContentsCollection} 201 */ 202 public Map<String, Object> getSynchronizableContentsCollectionAsJson(SynchronizableContentsCollection collection) 203 { 204 Map<String, Object> result = new LinkedHashMap<>(); 205 result.put("id", collection.getId()); 206 result.put("label", collection.getLabel()); 207 208 String cTypeId = collection.getContentType(); 209 result.put("contentTypeId", cTypeId); 210 211 ContentType cType = _contentTypeEP.getExtension(cTypeId); 212 result.put("contentType", cType != null ? cType.getLabel() : cTypeId); 213 214 String modelId = collection.getSynchronizeCollectionModelId(); 215 result.put("modelId", modelId); 216 SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId); 217 result.put("model", model.getLabel()); 218 219 result.put("isValid", _isValid(collection)); 220 221 return result; 222 } 223 224 /** 225 * Get the synchronizable contents collections 226 * @return the synchronizable contents collections 227 */ 228 public List<SynchronizableContentsCollection> getSynchronizableContentsCollections() 229 { 230 getLogger().debug("Calling #getSynchronizableContentsCollections()"); 231 _readFile(false); 232 ArrayList<SynchronizableContentsCollection> cols = new ArrayList<>(_synchronizableCollections.values()); 233 getLogger().debug("#getSynchronizableContentsCollections() returns '{}'", cols); 234 return cols; 235 } 236 237 /** 238 * Get a synchronizable contents collection by its id 239 * @param collectionId The id of collection 240 * @return the synchronizable contents collection or <code>null</code> if not found 241 */ 242 public SynchronizableContentsCollection getSynchronizableContentsCollection(String collectionId) 243 { 244 getLogger().debug("Calling #getSynchronizableContentsCollection(String collectionId) with collectionId '{}'", collectionId); 245 _readFile(false); 246 SynchronizableContentsCollection col = _synchronizableCollections.get(collectionId); 247 getLogger().debug("#getSynchronizableContentsCollection(String collectionId) with collectionId '{}' returns '{}'", collectionId, col); 248 return col; 249 } 250 251 private void _readFile(boolean forceRead) 252 { 253 try 254 { 255 if (!__CONFIGURATION_FILE.exists()) 256 { 257 getLogger().debug("=> SCC file does not exist, it will be created."); 258 _createFile(__CONFIGURATION_FILE); 259 } 260 else 261 { 262 // In Linux file systems, the precision of java.io.File.lastModified() is the second, so we need here to always have 263 // this (bad!) precision by doing the truncation to second precision (/1000 * 1000) on the millis time value. 264 // Therefore, the boolean outdated is computed with '>=' operator, and not '>', which will lead to sometimes (but rarely) unnecessarily re-read the file. 265 long cfgFileLastModified = (__CONFIGURATION_FILE.lastModified() / 1000) * 1000; 266 boolean outdated = cfgFileLastModified >= _lastFileReading; 267 getLogger().debug("=> forceRead: {}", forceRead); 268 getLogger().debug("=> The configuration was last modified in (long value): {}", cfgFileLastModified); 269 getLogger().debug("=> The '_lastFileReading' fields is equal to (long value): {}", _lastFileReading); 270 if (forceRead || outdated) 271 { 272 getLogger().debug(forceRead ? "=> SCC file will be read (force)" : "=> SCC file was (most likely) updated since the last time it was read ({} >= {}). It will be re-read...", cfgFileLastModified, _lastFileReading); 273 getLogger().debug("==> '_synchronizableCollections' map before calling #_readFile(): '{}'", _synchronizableCollections); 274 _lastFileReading = Instant.now().truncatedTo(ChronoUnit.SECONDS).toEpochMilli(); 275 _synchronizableCollections = new LinkedHashMap<>(); 276 277 Configuration cfg = new DefaultConfigurationBuilder().buildFromFile(__CONFIGURATION_FILE); 278 for (Configuration collectionConfig : cfg.getChildren("collection")) 279 { 280 SynchronizableContentsCollection syncCollection = _createSynchronizableCollection(collectionConfig); 281 _synchronizableCollections.put(syncCollection.getId(), syncCollection); 282 } 283 getLogger().debug("==> '_synchronizableCollections' map after calling #_readFile(): '{}'", _synchronizableCollections); 284 } 285 else 286 { 287 getLogger().debug("=> SCC file will not be re-read, the internal representation is up-to-date."); 288 } 289 } 290 } 291 catch (Exception e) 292 { 293 getLogger().error("Failed to retrieve synchronizable contents collections from the configuration file {}", __CONFIGURATION_FILE, e); 294 } 295 } 296 297 private void _createFile(File file) throws IOException, TransformerConfigurationException, SAXException 298 { 299 file.createNewFile(); 300 try (OutputStream os = new FileOutputStream(file)) 301 { 302 TransformerHandler th = _getTransformerHandler(os); 303 304 th.startDocument(); 305 XMLUtils.createElement(th, "collections"); 306 th.endDocument(); 307 } 308 } 309 310 private SynchronizableContentsCollection _createSynchronizableCollection(Configuration collectionConfig) throws ConfigurationException 311 { 312 String modelId = collectionConfig.getChild("model").getAttribute("id"); 313 314 if (_syncCollectionModelEP.hasExtension(modelId)) 315 { 316 SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId); 317 Class<SynchronizableContentsCollection> synchronizableCollectionClass = model.getSynchronizableCollectionClass(); 318 319 SynchronizableContentsCollection synchronizableCollection = null; 320 try 321 { 322 synchronizableCollection = synchronizableCollectionClass.getDeclaredConstructor().newInstance(); 323 } 324 catch (Exception e) 325 { 326 throw new IllegalArgumentException("Cannot instanciate the class " + synchronizableCollectionClass.getCanonicalName() + ". Check that there is a public constructor with no arguments."); 327 } 328 329 Logger logger = LoggerFactory.getLogger(synchronizableCollectionClass); 330 try 331 { 332 if (synchronizableCollection instanceof LogEnabled) 333 { 334 ((LogEnabled) synchronizableCollection).setLogger(logger); 335 } 336 337 LifecycleHelper.setupComponent(synchronizableCollection, new SLF4JLoggerAdapter(logger), _context, _smanager, collectionConfig); 338 } 339 catch (Exception e) 340 { 341 throw new ConfigurationException("The model id '" + modelId + "' is not a valid", e); 342 } 343 344 return synchronizableCollection; 345 } 346 347 throw new ConfigurationException("The model id '" + modelId + "' is not a valid model for collection '" + collectionConfig.getChild("id") + "'", collectionConfig); 348 } 349 350 /** 351 * Gets the configuration for creating/editing a collection of synchronizable contents. 352 * @return A map containing information about what is needed to create/edit a collection of synchronizable contents 353 * @throws Exception If an error occurs. 354 */ 355 @Callable 356 public Map<String, Object> getEditionConfiguration() throws Exception 357 { 358 Map<String, Object> result = new HashMap<>(); 359 360 // MODELS 361 List<Object> collectionModels = new ArrayList<>(); 362 for (String modelId : _syncCollectionModelEP.getExtensionsIds()) 363 { 364 SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId); 365 Map<String, Object> modelMap = new HashMap<>(); 366 modelMap.put("id", modelId); 367 modelMap.put("label", model.getLabel()); 368 modelMap.put("description", model.getDescription()); 369 370 Map<String, Object> params = new LinkedHashMap<>(); 371 for (ModelItem param : model.getModelItems()) 372 { 373 // prefix in case of two parameters from two different models have the same id which can lead to some errors in client-side 374 params.put(modelId + SCC_PARAMETERS_SEPARATOR + param.getPath(), param.toJSON(DefinitionContext.newInstance().withEdition(true))); 375 } 376 modelMap.put("parameters", params); 377 378 collectionModels.add(modelMap); 379 } 380 result.put("models", collectionModels); 381 382 // CONTENT TYPES 383 result.put( 384 "contentTypes", 385 _transformToJSONEnumerator( 386 _contentTypeEP.getExtensionsIds().stream().map(_contentTypeEP::getExtension).filter(this::_isValidContentType), 387 ContentType::getId, 388 ContentType::getLabel 389 ) 390 ); 391 392 // LANGUAGES 393 result.put( 394 "languages", 395 _transformToJSONEnumerator( 396 _languagesManager.getAvailableLanguages().entrySet().stream(), 397 entry -> entry.getKey(), 398 entry -> entry.getValue().getLabel() 399 ) 400 ); 401 402 // WORKFLOWS 403 result.put( 404 "workflows", 405 _transformToJSONEnumerator( 406 Stream.of(_workflowHelper.getWorkflowNames()), 407 Function.identity(), 408 id -> _workflowHelper.getWorkflowLabel(id) 409 ) 410 ); 411 412 // SYNCHRONIZING CONTENT OPERATORS 413 result.put( 414 "contentOperators", 415 _transformToJSONEnumerator( 416 _synchronizingContentOperatorEP.getExtensionsIds().stream(), 417 Function.identity(), 418 id -> _synchronizingContentOperatorEP.getExtension(id).getLabel() 419 ) 420 ); 421 result.put("defaultContentOperator", DefaultSynchronizingContentOperator.class.getName()); 422 423 // EXISTING SCC 424 result.put( 425 "existingSCC", 426 _transformToJSONEnumerator( 427 getSynchronizableContentsCollections().stream(), 428 SynchronizableContentsCollection::getId, 429 SynchronizableContentsCollection::getLabel 430 ) 431 ); 432 433 return result; 434 } 435 436 private <T> List<Map<String, Object>> _transformToJSONEnumerator(Stream<T> values, Function<T, String> valueFunction, Function<T, I18nizableText> labelFunction) 437 { 438 return values.map(value -> 439 Map.of( 440 "value", valueFunction.apply(value), 441 "label", labelFunction.apply(value) 442 ) 443 ) 444 .toList(); 445 } 446 447 private boolean _isValidContentType (ContentType cType) 448 { 449 return !cType.isReferenceTable() && !cType.isAbstract() && !cType.isMixin(); 450 } 451 452 /** 453 * Gets the values of the parameters of the given collection 454 * @param collectionId The id of the collection 455 * @return The values of the parameters 456 */ 457 @Callable 458 public Map<String, Object> getCollectionParameterValues(String collectionId) 459 { 460 Map<String, Object> result = new LinkedHashMap<>(); 461 462 SynchronizableContentsCollection collection = getSynchronizableContentsCollection(collectionId); 463 if (collection == null) 464 { 465 getLogger().error("The collection of id '{}' does not exist.", collectionId); 466 result.put("error", "unknown"); 467 return result; 468 } 469 470 result.put("id", collectionId); 471 result.put("label", collection.getLabel()); 472 String modelId = collection.getSynchronizeCollectionModelId(); 473 result.put("modelId", modelId); 474 475 result.put("contentType", collection.getContentType()); 476 result.put("contentPrefix", collection.getContentPrefix()); 477 result.put("restrictedField", collection.getRestrictedField()); 478 result.put("synchronizeExistingContentsOnly", collection.synchronizeExistingContentsOnly()); 479 result.put("removalSync", collection.removalSync()); 480 result.put("ignoreRestrictions", collection.ignoreRestrictions()); 481 result.put("checkCollection", collection.checkCollection()); 482 result.put("compatibleSCC", collection.getCompatibleSCC(false)); 483 result.put("validateAfterImport", collection.validateAfterImport()); 484 485 result.put("workflowName", collection.getWorkflowName()); 486 result.put("initialActionId", collection.getInitialActionId()); 487 result.put("synchronizeActionId", collection.getSynchronizeActionId()); 488 result.put("validateActionId", collection.getValidateActionId()); 489 490 result.put("contentOperator", collection.getSynchronizingContentOperator()); 491 result.put("reportMails", collection.getReportMails()); 492 493 result.put("languages", collection.getLanguages()); 494 495 Map<String, Object> values = collection.getParameterValues(); 496 for (String key : values.keySet()) 497 { 498 result.put(modelId + SCC_PARAMETERS_SEPARATOR + key, values.get(key)); 499 } 500 501 return result; 502 } 503 504 /** 505 * Gets the supported user directories (i.e. user directories based on a datasource) of the population in a json map 506 * @param populationId The id of the user population 507 * @return the supported user directories (i.e. user directories based on a datasource) of the population in a json map 508 */ 509 @Callable 510 public List<Map<String, Object>> getSupportedUserDirectories(String populationId) 511 { 512 List<Map<String, Object>> result = new ArrayList<>(); 513 514 UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(populationId); 515 List<UserDirectory> userDirectories = userPopulation.getUserDirectories(); 516 for (String udId : _getDatasourceBasedUserDirectories(userDirectories)) 517 { 518 UserDirectory userDirectory = userPopulation.getUserDirectory(udId); 519 String udModelId = userDirectory.getUserDirectoryModelId(); 520 UserDirectoryModel udModel = _userDirectoryFactory.getExtension(udModelId); 521 Map<String, Object> udMap = new HashMap<>(); 522 523 udMap.put("id", udId); 524 udMap.put("modelLabel", udModel.getLabel()); 525 526 String label = userDirectory.getLabel(); 527 if (StringUtils.isNotBlank(label)) 528 { 529 udMap.put("label", label); 530 } 531 532 if (userDirectory instanceof JdbcUserDirectory) 533 { 534 udMap.put("type", "SQL"); 535 } 536 else if (userDirectories instanceof LdapUserDirectory) 537 { 538 udMap.put("type", "LDAP"); 539 } 540 541 result.add(udMap); 542 } 543 544 return result; 545 } 546 547 private List<String> _getDatasourceBasedUserDirectories(List<UserDirectory> userDirectories) 548 { 549 List<String> ids = new ArrayList<>(); 550 for (UserDirectory userDirectory : userDirectories) 551 { 552 if (userDirectory instanceof JdbcUserDirectory || userDirectory instanceof LdapUserDirectory) 553 { 554 ids.add(userDirectory.getId()); 555 } 556 } 557 558 return ids; 559 } 560 561 private boolean _writeFile() 562 { 563 File backup = _createBackup(); 564 boolean errorOccured = false; 565 566 // Do writing 567 try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE)) 568 { 569 TransformerHandler th = _getTransformerHandler(os); 570 571 // sax the config 572 try 573 { 574 th.startDocument(); 575 XMLUtils.startElement(th, "collections"); 576 577 _toSAX(th); 578 XMLUtils.endElement(th, "collections"); 579 th.endDocument(); 580 } 581 catch (Exception e) 582 { 583 getLogger().error("Error when saxing the collections", e); 584 errorOccured = true; 585 } 586 } 587 catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e) 588 { 589 if (getLogger().isErrorEnabled()) 590 { 591 getLogger().error("Error when trying to modify the group directories with the configuration file {}", __CONFIGURATION_FILE, e); 592 } 593 } 594 595 _restoreBackup(backup, errorOccured); 596 597 return errorOccured; 598 } 599 600 private File _createBackup() 601 { 602 File backup = new File(__CONFIGURATION_FILE.getPath() + ".tmp"); 603 604 // Create a backup file 605 try 606 { 607 Files.copy(__CONFIGURATION_FILE.toPath(), backup.toPath()); 608 } 609 catch (IOException e) 610 { 611 getLogger().error("Error when creating backup '{}' file", __CONFIGURATION_FILE.toPath(), e); 612 } 613 614 return backup; 615 } 616 617 private void _restoreBackup(File backup, boolean errorOccured) 618 { 619 // Restore the file if an error previously occured 620 try 621 { 622 if (errorOccured) 623 { 624 // An error occured, restore the original 625 Files.copy(backup.toPath(), __CONFIGURATION_FILE.toPath(), StandardCopyOption.REPLACE_EXISTING); 626 // Force to reread the file 627 _readFile(true); 628 } 629 Files.deleteIfExists(backup.toPath()); 630 } 631 catch (IOException e) 632 { 633 if (getLogger().isErrorEnabled()) 634 { 635 getLogger().error("Error when restoring backup '{}' file", __CONFIGURATION_FILE, e); 636 } 637 } 638 } 639 640 /** 641 * Add a new {@link SynchronizableContentsCollection} 642 * @param values The parameters' values 643 * @return The id of new created collection or null in case of error 644 * @throws ProcessingException if creation failed 645 */ 646 @Callable 647 public String addCollection (Map<String, Object> values) throws ProcessingException 648 { 649 getLogger().debug("Add new Collection with values '{}'", values); 650 _readFile(false); 651 652 String id = _generateUniqueId((String) values.get("label")); 653 654 try 655 { 656 _addCollection(id, values); 657 return id; 658 } 659 catch (Exception e) 660 { 661 throw new ProcessingException("Failed to add new collection'" + id + "'", e); 662 } 663 } 664 665 /** 666 * Edit a {@link SynchronizableContentsCollection} 667 * @param id The id of collection to edit 668 * @param values The parameters' values 669 * @return The id of new created collection or null in case of error 670 * @throws ProcessingException if edition failed 671 */ 672 @Callable 673 public Map<String, Object> editCollection (String id, Map<String, Object> values) throws ProcessingException 674 { 675 getLogger().debug("Edit Collection with id '{}' and values '{}'", id, values); 676 Map<String, Object> result = new LinkedHashMap<>(); 677 678 SynchronizableContentsCollection collection = _synchronizableCollections.get(id); 679 if (collection == null) 680 { 681 getLogger().error("The collection with id '{}' does not exist, it cannot be edited.", id); 682 result.put("error", "unknown"); 683 return result; 684 } 685 else 686 { 687 _synchronizableCollections.remove(id); 688 } 689 690 try 691 { 692 _addCollection(id, values); 693 result.put("id", id); 694 return result; 695 } 696 catch (Exception e) 697 { 698 throw new ProcessingException("Failed to edit collection of id '" + id + "'", e); 699 } 700 } 701 702 private boolean _isValid(SynchronizableContentsCollection collection) 703 { 704 // Check validation of a data source on its parameters 705 706 SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(collection.getSynchronizeCollectionModelId()); 707 if (model != null) 708 { 709 for (ModelItem param : model.getModelItems()) 710 { 711 if (!_validateParameter(param, collection)) 712 { 713 // At least one parameter is invalid 714 return false; 715 } 716 717 if (ModelItemTypeConstants.DATASOURCE_ELEMENT_TYPE_ID.equals(param.getType().getId())) 718 { 719 String dataSourceId = (String) collection.getParameterValues().get(param.getPath()); 720 721 if (!_checkDataSource(dataSourceId)) 722 { 723 // At least one data source is not valid 724 return false; 725 } 726 727 } 728 } 729 730 return true; 731 } 732 733 return false; // no model found 734 } 735 736 private boolean _validateParameter(ModelItem modelItem, SynchronizableContentsCollection collection) 737 { 738 if (modelItem instanceof ElementDefinition) 739 { 740 ElementDefinition param = (ElementDefinition) modelItem; 741 Validator validator = param.getValidator(); 742 if (validator != null) 743 { 744 Object value = collection.getParameterValues().get(param.getPath()); 745 746 return !validator.validate(value).hasErrors(); 747 } 748 } 749 750 return true; 751 } 752 753 private boolean _checkDataSource(String dataSourceId) 754 { 755 if (dataSourceId != null) 756 { 757 try 758 { 759 DataSourceDefinition def = _getSQLDataSourceManager().getDataSourceDefinition(dataSourceId); 760 761 if (def != null) 762 { 763 _getSQLDataSourceManager().checkParameters(def.getParameters()); 764 } 765 else 766 { 767 def = _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId); 768 if (def != null) 769 { 770 _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId); 771 } 772 else 773 { 774 // The data source was not found 775 return false; 776 } 777 } 778 } 779 catch (ItemCheckerTestFailureException e) 780 { 781 // Connection to the SQL data source failed 782 return false; 783 } 784 } 785 786 return true; 787 } 788 789 private boolean _addCollection(String id, Map<String, Object> values) throws FileNotFoundException, IOException, TransformerConfigurationException, SAXException 790 { 791 File backup = _createBackup(); 792 boolean success = false; 793 794 // Do writing 795 try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE)) 796 { 797 TransformerHandler th = _getTransformerHandler(os); 798 799 // sax the config 800 th.startDocument(); 801 XMLUtils.startElement(th, "collections"); 802 803 // SAX already existing collections 804 _toSAX(th); 805 806 // SAX the new collection 807 _saxCollection(th, id, values); 808 809 XMLUtils.endElement(th, "collections"); 810 th.endDocument(); 811 812 success = true; 813 } 814 815 _restoreBackup(backup, !success); 816 817 _readFile(false); 818 819 return success; 820 } 821 822 private TransformerHandler _getTransformerHandler(OutputStream os) throws TransformerConfigurationException 823 { 824 // create a transformer for saving sax into a file 825 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 826 827 StreamResult result = new StreamResult(os); 828 th.setResult(result); 829 830 // create the format of result 831 Properties format = new Properties(); 832 format.put(OutputKeys.METHOD, "xml"); 833 format.put(OutputKeys.INDENT, "yes"); 834 format.put(OutputKeys.ENCODING, "UTF-8"); 835 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 836 th.getTransformer().setOutputProperties(format); 837 838 return th; 839 } 840 841 /** 842 * Removes the given collection 843 * @param id The id of the collection to remove 844 * @return A map containing the id of the removed collection, or an error 845 */ 846 @Callable 847 public Map<String, Object> removeCollection(String id) 848 { 849 getLogger().debug("Remove Collection with id '{}'", id); 850 Map<String, Object> result = new LinkedHashMap<>(); 851 852 _readFile(false); 853 if (_synchronizableCollections.remove(id) == null) 854 { 855 getLogger().error("The synchronizable collection with id '{}' does not exist, it cannot be removed.", id); 856 result.put("error", "unknown"); 857 return result; 858 } 859 860 if (_writeFile()) 861 { 862 return null; 863 } 864 865 result.put("id", id); 866 return result; 867 } 868 869 private String _generateUniqueId(String label) 870 { 871 // Id generated from name lowercased, trimmed, and spaces and underscores replaced by dashes 872 String value = label.toLowerCase().trim().replaceAll("[\\W_]", "-").replaceAll("-+", "-").replaceAll("^-", ""); 873 int i = 2; 874 String suffixedValue = value; 875 while (_synchronizableCollections.get(suffixedValue) != null) 876 { 877 suffixedValue = value + i; 878 i++; 879 } 880 881 return suffixedValue; 882 } 883 884 private void _toSAX(TransformerHandler handler) throws SAXException 885 { 886 for (SynchronizableContentsCollection collection : _synchronizableCollections.values()) 887 { 888 _saxCollection(handler, collection); 889 } 890 } 891 892 private void _saxCollection(ContentHandler handler, String id, Map<String, Object> parameters) throws SAXException 893 { 894 AttributesImpl atts = new AttributesImpl(); 895 atts.addCDATAAttribute("id", id); 896 897 XMLUtils.startElement(handler, "collection", atts); 898 899 String label = (String) parameters.get("label"); 900 if (label != null) 901 { 902 new I18nizableText(label).toSAX(handler, "label"); 903 } 904 905 _saxNonNullValue(handler, "contentType", parameters.get("contentType")); 906 _saxNonNullValue(handler, "contentPrefix", parameters.get("contentPrefix")); 907 _saxNonNullValue(handler, "restrictedField", parameters.get("restrictedField")); 908 _saxNonNullValue(handler, "synchronizeExistingContentsOnly", parameters.get("synchronizeExistingContentsOnly")); 909 _saxNonNullValue(handler, "removalSync", parameters.get("removalSync")); 910 _saxNonNullValue(handler, "ignoreRestrictions", parameters.get("ignoreRestrictions")); 911 _saxNonNullValue(handler, "checkCollection", parameters.get("checkCollection")); 912 _saxMultipleValues(handler, "compatibleSCC", parameters.get("compatibleSCC")); 913 914 _saxNonNullValue(handler, "workflowName", parameters.get("workflowName")); 915 _saxNonNullValue(handler, "initialActionId", parameters.get("initialActionId")); 916 _saxNonNullValue(handler, "synchronizeActionId", parameters.get("synchronizeActionId")); 917 _saxNonNullValue(handler, "validateActionId", parameters.get("validateActionId")); 918 _saxNonNullValue(handler, "validateAfterImport", parameters.get("validateAfterImport")); 919 920 _saxNonNullValue(handler, "reportMails", parameters.get("reportMails")); 921 _saxNonNullValue(handler, "contentOperator", parameters.get("contentOperator")); 922 923 _saxMultipleValues(handler, "languages", parameters.get("languages")); 924 925 String modelId = (String) parameters.get("modelId"); 926 _saxModel(handler, modelId, parameters, true); 927 928 XMLUtils.endElement(handler, "collection"); 929 } 930 931 @SuppressWarnings("unchecked") 932 private void _saxMultipleValues(ContentHandler handler, String tagName, Object values) throws SAXException 933 { 934 if (values != null) 935 { 936 XMLUtils.startElement(handler, tagName); 937 for (String lang : (List<String>) values) 938 { 939 XMLUtils.createElement(handler, "value", lang); 940 } 941 XMLUtils.endElement(handler, tagName); 942 } 943 } 944 945 private void _saxCollection(ContentHandler handler, SynchronizableContentsCollection collection) throws SAXException 946 { 947 AttributesImpl atts = new AttributesImpl(); 948 atts.addCDATAAttribute("id", collection.getId()); 949 XMLUtils.startElement(handler, "collection", atts); 950 951 collection.getLabel().toSAX(handler, "label"); 952 953 _saxNonNullValue(handler, "contentType", collection.getContentType()); 954 _saxNonNullValue(handler, "contentPrefix", collection.getContentPrefix()); 955 _saxNonNullValue(handler, "restrictedField", collection.getRestrictedField()); 956 957 _saxNonNullValue(handler, "workflowName", collection.getWorkflowName()); 958 _saxNonNullValue(handler, "initialActionId", collection.getInitialActionId()); 959 _saxNonNullValue(handler, "synchronizeActionId", collection.getSynchronizeActionId()); 960 _saxNonNullValue(handler, "validateActionId", collection.getValidateActionId()); 961 _saxNonNullValue(handler, "validateAfterImport", collection.validateAfterImport()); 962 963 _saxNonNullValue(handler, "reportMails", collection.getReportMails()); 964 _saxNonNullValue(handler, "contentOperator", collection.getSynchronizingContentOperator()); 965 _saxNonNullValue(handler, "synchronizeExistingContentsOnly", collection.synchronizeExistingContentsOnly()); 966 _saxNonNullValue(handler, "removalSync", collection.removalSync()); 967 _saxNonNullValue(handler, "ignoreRestrictions", collection.ignoreRestrictions()); 968 _saxNonNullValue(handler, "checkCollection", collection.checkCollection()); 969 _saxMultipleValues(handler, "compatibleSCC", collection.getCompatibleSCC(false)); 970 971 _saxMultipleValues(handler, "languages", collection.getLanguages()); 972 973 _saxModel(handler, collection.getSynchronizeCollectionModelId(), collection.getParameterValues(), false); 974 975 XMLUtils.endElement(handler, "collection"); 976 } 977 978 private void _saxNonNullValue(ContentHandler handler, String tagName, Object value) throws SAXException 979 { 980 if (value != null) 981 { 982 XMLUtils.createElement(handler, tagName, value.toString()); 983 } 984 } 985 986 private void _saxModel(ContentHandler handler, String modelId, Map<String, Object> paramValues, boolean withPrefix) throws SAXException 987 { 988 AttributesImpl atts = new AttributesImpl(); 989 atts.addCDATAAttribute("id", modelId); 990 XMLUtils.startElement(handler, "model", atts); 991 992 SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId); 993 String prefix = withPrefix ? modelId + SCC_PARAMETERS_SEPARATOR : StringUtils.EMPTY; 994 for (ModelItem parameter : model.getModelItems()) 995 { 996 String paramFieldName = prefix + parameter.getPath(); 997 Object value = paramValues.get(paramFieldName); 998 if (value != null) 999 { 1000 atts.clear(); 1001 atts.addCDATAAttribute("name", parameter.getPath()); 1002 XMLUtils.createElement(handler, "param", atts, ((ElementDefinition) parameter).getType().toString(value)); 1003 } 1004 } 1005 1006 XMLUtils.endElement(handler, "model"); 1007 } 1008 1009 @Override 1010 public void dispose() 1011 { 1012 _synchronizableCollections.clear(); 1013 _lastFileReading = 0; 1014 } 1015}