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