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; 033 034import javax.xml.transform.OutputKeys; 035import javax.xml.transform.TransformerConfigurationException; 036import javax.xml.transform.TransformerFactory; 037import javax.xml.transform.TransformerFactoryConfigurationError; 038import javax.xml.transform.sax.SAXTransformerFactory; 039import javax.xml.transform.sax.TransformerHandler; 040import javax.xml.transform.stream.StreamResult; 041 042import org.apache.avalon.framework.activity.Disposable; 043import org.apache.avalon.framework.activity.Initializable; 044import org.apache.avalon.framework.component.Component; 045import org.apache.avalon.framework.configuration.Configuration; 046import org.apache.avalon.framework.configuration.ConfigurationException; 047import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 048import org.apache.avalon.framework.context.Context; 049import org.apache.avalon.framework.context.ContextException; 050import org.apache.avalon.framework.context.Contextualizable; 051import org.apache.avalon.framework.service.ServiceException; 052import org.apache.avalon.framework.service.ServiceManager; 053import org.apache.avalon.framework.service.Serviceable; 054import org.apache.cocoon.ProcessingException; 055import org.apache.cocoon.components.LifecycleHelper; 056import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 057import org.apache.cocoon.xml.AttributesImpl; 058import org.apache.cocoon.xml.XMLUtils; 059import org.apache.commons.lang3.StringUtils; 060import org.apache.xml.serializer.OutputPropertiesFactory; 061import org.slf4j.Logger; 062import org.slf4j.LoggerFactory; 063import org.xml.sax.ContentHandler; 064import org.xml.sax.SAXException; 065 066import org.ametys.cms.contenttype.ContentType; 067import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 068import org.ametys.cms.languages.Language; 069import org.ametys.cms.languages.LanguagesManager; 070import org.ametys.core.datasource.AbstractDataSourceManager.DataSourceDefinition; 071import org.ametys.core.datasource.LDAPDataSourceManager; 072import org.ametys.core.datasource.SQLDataSourceManager; 073import org.ametys.core.ui.Callable; 074import org.ametys.core.user.directory.UserDirectory; 075import org.ametys.core.user.directory.UserDirectoryFactory; 076import org.ametys.core.user.directory.UserDirectoryModel; 077import org.ametys.core.user.population.UserPopulation; 078import org.ametys.core.user.population.UserPopulationDAO; 079import org.ametys.plugins.contentio.synchronize.impl.DefaultSynchronizingContentOperator; 080import org.ametys.plugins.core.impl.user.directory.JdbcUserDirectory; 081import org.ametys.plugins.core.impl.user.directory.LdapUserDirectory; 082import org.ametys.plugins.workflow.support.WorkflowHelper; 083import org.ametys.runtime.i18n.I18nizableText; 084import org.ametys.runtime.model.DefinitionContext; 085import org.ametys.runtime.model.ElementDefinition; 086import org.ametys.runtime.model.ModelItem; 087import org.ametys.runtime.model.checker.ItemCheckerTestFailureException; 088import org.ametys.runtime.model.type.ModelItemTypeConstants; 089import org.ametys.runtime.parameter.Validator; 090import org.ametys.runtime.plugin.component.AbstractLogEnabled; 091import org.ametys.runtime.plugin.component.LogEnabled; 092import org.ametys.runtime.util.AmetysHomeHelper; 093 094/** 095 * DAO for accessing {@link SynchronizableContentsCollection} 096 */ 097public class SynchronizableContentsCollectionDAO extends AbstractLogEnabled implements Component, Serviceable, Initializable, Contextualizable, Disposable 098{ 099 /** Avalon Role */ 100 public static final String ROLE = SynchronizableContentsCollectionDAO.class.getName(); 101 102 /** Separator for parameters with model id as prefix */ 103 public static final String SCC_PARAMETERS_SEPARATOR = "$"; 104 105 private static File __CONFIGURATION_FILE; 106 107 private Map<String, SynchronizableContentsCollection> _synchronizableCollections; 108 private long _lastFileReading; 109 110 private SynchronizeContentsCollectionModelExtensionPoint _syncCollectionModelEP; 111 private ContentTypeExtensionPoint _contentTypeEP; 112 private UserPopulationDAO _userPopulationDAO; 113 private UserDirectoryFactory _userDirectoryFactory; 114 private WorkflowHelper _workflowHelper; 115 private SynchronizingContentOperatorExtensionPoint _synchronizingContentOperatorEP; 116 private LanguagesManager _languagesManager; 117 118 private ServiceManager _smanager; 119 private Context _context; 120 121 private SQLDataSourceManager _sqlDataSourceManager; 122 private LDAPDataSourceManager _ldapDataSourceManager; 123 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 List<Object> contentTypes = new ArrayList<>(); 384 for (String contentTypeId : _contentTypeEP.getExtensionsIds()) 385 { 386 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 387 if (_isValidContentType(contentType)) 388 { 389 Map<String, Object> contentTypeMap = new HashMap<>(); 390 contentTypeMap.put("value", contentType.getId()); 391 contentTypeMap.put("label", contentType.getLabel()); 392 393 contentTypes.add(contentTypeMap); 394 } 395 } 396 result.put("contentTypes", contentTypes); 397 398 // LANGUAGES 399 List<Object> languages = new ArrayList<>(); 400 Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages(); 401 for (String lang : availableLanguages.keySet()) 402 { 403 Language language = availableLanguages.get(lang); 404 Map<String, Object> languageMap = new HashMap<>(); 405 languageMap.put("value", lang); 406 languageMap.put("label", language.getLabel()); 407 languages.add(languageMap); 408 } 409 result.put("languages", languages); 410 411 // WORKFLOWS 412 List<Object> workflows = new ArrayList<>(); 413 String[] workflowNames = _workflowHelper.getWorkflowNames(); 414 for (String workflowName : workflowNames) 415 { 416 Map<String, Object> workflowMap = new HashMap<>(); 417 workflowMap.put("value", workflowName); 418 workflowMap.put("label", _workflowHelper.getWorkflowLabel(workflowName)); 419 workflows.add(workflowMap); 420 } 421 result.put("workflows", workflows); 422 423 // SYNCHRONIZING CONTENT OPERATORS 424 List<Object> operators = new ArrayList<>(); 425 for (String operatorId : _synchronizingContentOperatorEP.getExtensionsIds()) 426 { 427 Map<String, Object> operatorMap = new HashMap<>(); 428 operatorMap.put("value", operatorId); 429 operatorMap.put("label", _synchronizingContentOperatorEP.getExtension(operatorId).getLabel()); 430 operators.add(operatorMap); 431 } 432 result.put("contentOperators", operators); 433 result.put("defaultContentOperator", DefaultSynchronizingContentOperator.class.getName()); 434 435 return result; 436 } 437 438 private boolean _isValidContentType (ContentType cType) 439 { 440 return !cType.isReferenceTable() && !cType.isAbstract() && !cType.isMixin(); 441 } 442 443 /** 444 * Gets the values of the parameters of the given collection 445 * @param collectionId The id of the collection 446 * @return The values of the parameters 447 */ 448 @Callable 449 public Map<String, Object> getCollectionParameterValues(String collectionId) 450 { 451 Map<String, Object> result = new LinkedHashMap<>(); 452 453 SynchronizableContentsCollection collection = getSynchronizableContentsCollection(collectionId); 454 if (collection == null) 455 { 456 getLogger().error("The collection of id '{}' does not exist.", collectionId); 457 result.put("error", "unknown"); 458 return result; 459 } 460 461 result.put("id", collectionId); 462 result.put("label", collection.getLabel()); 463 String modelId = collection.getSynchronizeCollectionModelId(); 464 result.put("modelId", modelId); 465 466 result.put("contentType", collection.getContentType()); 467 result.put("contentPrefix", collection.getContentPrefix()); 468 result.put("restrictedField", collection.getRestrictedField()); 469 result.put("synchronizeExistingContentsOnly", collection.synchronizeExistingContentsOnly()); 470 result.put("removalSync", collection.removalSync()); 471 result.put("ignoreRestrictions", collection.ignoreRestrictions()); 472 result.put("validateAfterImport", collection.validateAfterImport()); 473 474 result.put("workflowName", collection.getWorkflowName()); 475 result.put("initialActionId", collection.getInitialActionId()); 476 result.put("synchronizeActionId", collection.getSynchronizeActionId()); 477 result.put("validateActionId", collection.getValidateActionId()); 478 479 result.put("contentOperator", collection.getSynchronizingContentOperator()); 480 result.put("reportMails", collection.getReportMails()); 481 482 result.put("languages", collection.getLanguages()); 483 484 Map<String, Object> values = collection.getParameterValues(); 485 for (String key : values.keySet()) 486 { 487 result.put(modelId + SCC_PARAMETERS_SEPARATOR + key, values.get(key)); 488 } 489 490 return result; 491 } 492 493 /** 494 * Gets the supported user directories (i.e. user directories based on a datasource) of the population in a json map 495 * @param populationId The id of the user population 496 * @return the supported user directories (i.e. user directories based on a datasource) of the population in a json map 497 */ 498 @Callable 499 public List<Map<String, Object>> getSupportedUserDirectories(String populationId) 500 { 501 List<Map<String, Object>> result = new ArrayList<>(); 502 503 UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(populationId); 504 List<UserDirectory> userDirectories = userPopulation.getUserDirectories(); 505 for (String udId : _getDatasourceBasedUserDirectories(userDirectories)) 506 { 507 UserDirectory userDirectory = userPopulation.getUserDirectory(udId); 508 String udModelId = userDirectory.getUserDirectoryModelId(); 509 UserDirectoryModel udModel = _userDirectoryFactory.getExtension(udModelId); 510 Map<String, Object> udMap = new HashMap<>(); 511 512 udMap.put("id", udId); 513 udMap.put("modelLabel", udModel.getLabel()); 514 515 String label = userDirectory.getLabel(); 516 if (StringUtils.isNotBlank(label)) 517 { 518 udMap.put("label", label); 519 } 520 521 if (userDirectory instanceof JdbcUserDirectory) 522 { 523 udMap.put("type", "SQL"); 524 } 525 else if (userDirectories instanceof LdapUserDirectory) 526 { 527 udMap.put("type", "LDAP"); 528 } 529 530 result.add(udMap); 531 } 532 533 return result; 534 } 535 536 private List<String> _getDatasourceBasedUserDirectories(List<UserDirectory> userDirectories) 537 { 538 List<String> ids = new ArrayList<>(); 539 for (UserDirectory userDirectory : userDirectories) 540 { 541 if (userDirectory instanceof JdbcUserDirectory || userDirectory instanceof LdapUserDirectory) 542 { 543 ids.add(userDirectory.getId()); 544 } 545 } 546 547 return ids; 548 } 549 550 private boolean _writeFile() 551 { 552 File backup = _createBackup(); 553 boolean errorOccured = false; 554 555 // Do writing 556 try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE)) 557 { 558 TransformerHandler th = _getTransformerHandler(os); 559 560 // sax the config 561 try 562 { 563 th.startDocument(); 564 XMLUtils.startElement(th, "collections"); 565 566 _toSAX(th); 567 XMLUtils.endElement(th, "collections"); 568 th.endDocument(); 569 } 570 catch (Exception e) 571 { 572 getLogger().error("Error when saxing the collections", e); 573 errorOccured = true; 574 } 575 } 576 catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e) 577 { 578 if (getLogger().isErrorEnabled()) 579 { 580 getLogger().error("Error when trying to modify the group directories with the configuration file {}", __CONFIGURATION_FILE, e); 581 } 582 } 583 584 _restoreBackup(backup, errorOccured); 585 586 return errorOccured; 587 } 588 589 private File _createBackup() 590 { 591 File backup = new File(__CONFIGURATION_FILE.getPath() + ".tmp"); 592 593 // Create a backup file 594 try 595 { 596 Files.copy(__CONFIGURATION_FILE.toPath(), backup.toPath()); 597 } 598 catch (IOException e) 599 { 600 getLogger().error("Error when creating backup '{}' file", __CONFIGURATION_FILE.toPath(), e); 601 } 602 603 return backup; 604 } 605 606 private void _restoreBackup(File backup, boolean errorOccured) 607 { 608 // Restore the file if an error previously occured 609 try 610 { 611 if (errorOccured) 612 { 613 // An error occured, restore the original 614 Files.copy(backup.toPath(), __CONFIGURATION_FILE.toPath(), StandardCopyOption.REPLACE_EXISTING); 615 // Force to reread the file 616 _readFile(true); 617 } 618 Files.deleteIfExists(backup.toPath()); 619 } 620 catch (IOException e) 621 { 622 if (getLogger().isErrorEnabled()) 623 { 624 getLogger().error("Error when restoring backup '{}' file", __CONFIGURATION_FILE, e); 625 } 626 } 627 } 628 629 /** 630 * Add a new {@link SynchronizableContentsCollection} 631 * @param values The parameters' values 632 * @return The id of new created collection or null in case of error 633 * @throws ProcessingException if creation failed 634 */ 635 @Callable 636 public String addCollection (Map<String, Object> values) throws ProcessingException 637 { 638 getLogger().debug("Add new Collection with values '{}'", values); 639 _readFile(false); 640 641 String id = _generateUniqueId((String) values.get("label")); 642 643 try 644 { 645 _addCollection(id, values); 646 return id; 647 } 648 catch (Exception e) 649 { 650 throw new ProcessingException("Failed to add new collection'" + id + "'", e); 651 } 652 } 653 654 /** 655 * Edit a {@link SynchronizableContentsCollection} 656 * @param id The id of collection to edit 657 * @param values The parameters' values 658 * @return The id of new created collection or null in case of error 659 * @throws ProcessingException if edition failed 660 */ 661 @Callable 662 public Map<String, Object> editCollection (String id, Map<String, Object> values) throws ProcessingException 663 { 664 getLogger().debug("Edit Collection with id '{}' and values '{}'", id, values); 665 Map<String, Object> result = new LinkedHashMap<>(); 666 667 SynchronizableContentsCollection collection = _synchronizableCollections.get(id); 668 if (collection == null) 669 { 670 getLogger().error("The collection with id '{}' does not exist, it cannot be edited.", id); 671 result.put("error", "unknown"); 672 return result; 673 } 674 else 675 { 676 _synchronizableCollections.remove(id); 677 } 678 679 try 680 { 681 _addCollection(id, values); 682 result.put("id", id); 683 return result; 684 } 685 catch (Exception e) 686 { 687 throw new ProcessingException("Failed to edit collection of id '" + id + "'", e); 688 } 689 } 690 691 private boolean _isValid(SynchronizableContentsCollection collection) 692 { 693 // Check validation of a data source on its parameters 694 695 SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(collection.getSynchronizeCollectionModelId()); 696 if (model != null) 697 { 698 for (ModelItem param : model.getModelItems()) 699 { 700 if (!_validateParameter(param, collection)) 701 { 702 // At least one parameter is invalid 703 return false; 704 } 705 706 if (ModelItemTypeConstants.DATASOURCE_ELEMENT_TYPE_ID.equals(param.getType().getId())) 707 { 708 String dataSourceId = (String) collection.getParameterValues().get(param.getPath()); 709 710 if (!_checkDataSource(dataSourceId)) 711 { 712 // At least one data source is not valid 713 return false; 714 } 715 716 } 717 } 718 719 return true; 720 } 721 722 return false; // no model found 723 } 724 725 private boolean _validateParameter(ModelItem modelItem, SynchronizableContentsCollection collection) 726 { 727 if (modelItem instanceof ElementDefinition) 728 { 729 ElementDefinition param = (ElementDefinition) modelItem; 730 Validator validator = param.getValidator(); 731 if (validator != null) 732 { 733 Object value = collection.getParameterValues().get(param.getPath()); 734 735 return !validator.validate(value).hasErrors(); 736 } 737 } 738 739 return true; 740 } 741 742 private boolean _checkDataSource(String dataSourceId) 743 { 744 if (dataSourceId != null) 745 { 746 try 747 { 748 DataSourceDefinition def = _getSQLDataSourceManager().getDataSourceDefinition(dataSourceId); 749 750 if (def != null) 751 { 752 _getSQLDataSourceManager().checkParameters(def.getParameters()); 753 } 754 else 755 { 756 def = _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId); 757 if (def != null) 758 { 759 _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId); 760 } 761 else 762 { 763 // The data source was not found 764 return false; 765 } 766 } 767 } 768 catch (ItemCheckerTestFailureException e) 769 { 770 // Connection to the SQL data source failed 771 return false; 772 } 773 } 774 775 return true; 776 } 777 778 private boolean _addCollection(String id, Map<String, Object> values) throws FileNotFoundException, IOException, TransformerConfigurationException, SAXException 779 { 780 File backup = _createBackup(); 781 boolean success = false; 782 783 // Do writing 784 try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE)) 785 { 786 TransformerHandler th = _getTransformerHandler(os); 787 788 // sax the config 789 th.startDocument(); 790 XMLUtils.startElement(th, "collections"); 791 792 // SAX already existing collections 793 _toSAX(th); 794 795 // SAX the new collection 796 _saxCollection(th, id, values); 797 798 XMLUtils.endElement(th, "collections"); 799 th.endDocument(); 800 801 success = true; 802 } 803 804 _restoreBackup(backup, !success); 805 806 _readFile(false); 807 808 return success; 809 } 810 811 private TransformerHandler _getTransformerHandler(OutputStream os) throws TransformerConfigurationException 812 { 813 // create a transformer for saving sax into a file 814 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 815 816 StreamResult result = new StreamResult(os); 817 th.setResult(result); 818 819 // create the format of result 820 Properties format = new Properties(); 821 format.put(OutputKeys.METHOD, "xml"); 822 format.put(OutputKeys.INDENT, "yes"); 823 format.put(OutputKeys.ENCODING, "UTF-8"); 824 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 825 th.getTransformer().setOutputProperties(format); 826 827 return th; 828 } 829 830 /** 831 * Removes the given collection 832 * @param id The id of the collection to remove 833 * @return A map containing the id of the removed collection, or an error 834 */ 835 @Callable 836 public Map<String, Object> removeCollection(String id) 837 { 838 getLogger().debug("Remove Collection with id '{}'", id); 839 Map<String, Object> result = new LinkedHashMap<>(); 840 841 _readFile(false); 842 if (_synchronizableCollections.remove(id) == null) 843 { 844 getLogger().error("The synchronizable collection with id '{}' does not exist, it cannot be removed.", id); 845 result.put("error", "unknown"); 846 return result; 847 } 848 849 if (_writeFile()) 850 { 851 return null; 852 } 853 854 result.put("id", id); 855 return result; 856 } 857 858 private String _generateUniqueId(String label) 859 { 860 // Id generated from name lowercased, trimmed, and spaces and underscores replaced by dashes 861 String value = label.toLowerCase().trim().replaceAll("[\\W_]", "-").replaceAll("-+", "-").replaceAll("^-", ""); 862 int i = 2; 863 String suffixedValue = value; 864 while (_synchronizableCollections.get(suffixedValue) != null) 865 { 866 suffixedValue = value + i; 867 i++; 868 } 869 870 return suffixedValue; 871 } 872 873 private void _toSAX(TransformerHandler handler) throws SAXException 874 { 875 for (SynchronizableContentsCollection collection : _synchronizableCollections.values()) 876 { 877 _saxCollection(handler, collection); 878 } 879 } 880 881 private void _saxCollection(ContentHandler handler, String id, Map<String, Object> parameters) throws SAXException 882 { 883 AttributesImpl atts = new AttributesImpl(); 884 atts.addCDATAAttribute("id", id); 885 886 XMLUtils.startElement(handler, "collection", atts); 887 888 String label = (String) parameters.get("label"); 889 if (label != null) 890 { 891 new I18nizableText(label).toSAX(handler, "label"); 892 } 893 894 _saxNonNullValue(handler, "contentType", parameters.get("contentType")); 895 _saxNonNullValue(handler, "contentPrefix", parameters.get("contentPrefix")); 896 _saxNonNullValue(handler, "restrictedField", parameters.get("restrictedField")); 897 _saxNonNullValue(handler, "synchronizeExistingContentsOnly", parameters.get("synchronizeExistingContentsOnly")); 898 _saxNonNullValue(handler, "removalSync", parameters.get("removalSync")); 899 _saxNonNullValue(handler, "ignoreRestrictions", parameters.get("ignoreRestrictions")); 900 901 _saxNonNullValue(handler, "workflowName", parameters.get("workflowName")); 902 _saxNonNullValue(handler, "initialActionId", parameters.get("initialActionId")); 903 _saxNonNullValue(handler, "synchronizeActionId", parameters.get("synchronizeActionId")); 904 _saxNonNullValue(handler, "validateActionId", parameters.get("validateActionId")); 905 _saxNonNullValue(handler, "validateAfterImport", parameters.get("validateAfterImport")); 906 907 _saxNonNullValue(handler, "reportMails", parameters.get("reportMails")); 908 _saxNonNullValue(handler, "contentOperator", parameters.get("contentOperator")); 909 910 _saxLanguagesValue(handler, parameters.get("languages")); 911 912 parameters.remove("id"); 913 parameters.remove("label"); 914 parameters.remove("contentType"); 915 parameters.remove("synchronizeExistingContentsOnly"); 916 parameters.remove("removalSync"); 917 parameters.remove("ignoreRestrictions"); 918 parameters.remove("workflowName"); 919 parameters.remove("initialActionId"); 920 parameters.remove("synchronizeActionId"); 921 parameters.remove("validateActionId"); 922 parameters.remove("contentPrefix"); 923 parameters.remove("validateAfterImport"); 924 parameters.remove("reportMails"); 925 parameters.remove("contentOperator"); 926 parameters.remove("restrictedField"); 927 928 String modelId = (String) parameters.get("modelId"); 929 parameters.remove("modelId"); 930 931 _saxModel(handler, modelId, parameters, true); 932 933 XMLUtils.endElement(handler, "collection"); 934 } 935 936 @SuppressWarnings("unchecked") 937 private void _saxLanguagesValue(ContentHandler handler, Object languages) throws SAXException 938 { 939 if (languages != null) 940 { 941 XMLUtils.startElement(handler, "languages"); 942 for (String lang : (List<String>) languages) 943 { 944 XMLUtils.createElement(handler, "value", lang); 945 } 946 XMLUtils.endElement(handler, "languages"); 947 } 948 } 949 950 private void _saxCollection(ContentHandler handler, SynchronizableContentsCollection collection) throws SAXException 951 { 952 AttributesImpl atts = new AttributesImpl(); 953 atts.addCDATAAttribute("id", collection.getId()); 954 XMLUtils.startElement(handler, "collection", atts); 955 956 collection.getLabel().toSAX(handler, "label"); 957 958 _saxNonNullValue(handler, "contentType", collection.getContentType()); 959 _saxNonNullValue(handler, "contentPrefix", collection.getContentPrefix()); 960 _saxNonNullValue(handler, "restrictedField", collection.getRestrictedField()); 961 962 _saxNonNullValue(handler, "workflowName", collection.getWorkflowName()); 963 _saxNonNullValue(handler, "initialActionId", collection.getInitialActionId()); 964 _saxNonNullValue(handler, "synchronizeActionId", collection.getSynchronizeActionId()); 965 _saxNonNullValue(handler, "validateActionId", collection.getValidateActionId()); 966 _saxNonNullValue(handler, "validateAfterImport", collection.validateAfterImport()); 967 968 _saxNonNullValue(handler, "reportMails", collection.getReportMails()); 969 _saxNonNullValue(handler, "contentOperator", collection.getSynchronizingContentOperator()); 970 _saxNonNullValue(handler, "synchronizeExistingContentsOnly", collection.synchronizeExistingContentsOnly()); 971 _saxNonNullValue(handler, "removalSync", collection.removalSync()); 972 _saxNonNullValue(handler, "ignoreRestrictions", collection.ignoreRestrictions()); 973 974 _saxLanguagesValue(handler, collection.getLanguages()); 975 976 _saxModel(handler, collection.getSynchronizeCollectionModelId(), collection.getParameterValues(), false); 977 978 XMLUtils.endElement(handler, "collection"); 979 } 980 981 private void _saxNonNullValue (ContentHandler handler, String tagName, Object value) throws SAXException 982 { 983 if (value != null) 984 { 985 XMLUtils.createElement(handler, tagName, value.toString()); 986 } 987 } 988 989 private void _saxModel(ContentHandler handler, String modelId, Map<String, Object> paramValues, boolean withPrefix) throws SAXException 990 { 991 AttributesImpl atts = new AttributesImpl(); 992 atts.addCDATAAttribute("id", modelId); 993 XMLUtils.startElement(handler, "model", atts); 994 995 SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId); 996 String prefix = withPrefix ? modelId + SCC_PARAMETERS_SEPARATOR : StringUtils.EMPTY; 997 for (ModelItem parameter : model.getModelItems()) 998 { 999 String paramFieldName = prefix + parameter.getPath(); 1000 Object value = paramValues.get(paramFieldName); 1001 if (value != null) 1002 { 1003 atts.clear(); 1004 atts.addCDATAAttribute("name", parameter.getPath()); 1005 XMLUtils.createElement(handler, "param", atts, ((ElementDefinition) parameter).getType().toString(value)); 1006 } 1007 } 1008 1009 XMLUtils.endElement(handler, "model"); 1010 } 1011 1012 @Override 1013 public void dispose() 1014 { 1015 _synchronizableCollections.clear(); 1016 _lastFileReading = 0; 1017 } 1018}