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