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 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 = _workflowHelper.getWorkflowNames(); 413 for (String workflowName : workflowNames) 414 { 415 Map<String, Object> workflowMap = new HashMap<>(); 416 workflowMap.put("value", workflowName); 417 workflowMap.put("label", _workflowHelper.getWorkflowLabel(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("ignoreRestrictions", collection.ignoreRestrictions()); 471 result.put("validateAfterImport", collection.validateAfterImport()); 472 473 result.put("workflowName", collection.getWorkflowName()); 474 result.put("initialActionId", collection.getInitialActionId()); 475 result.put("synchronizeActionId", collection.getSynchronizeActionId()); 476 result.put("validateActionId", collection.getValidateActionId()); 477 478 result.put("contentOperator", collection.getSynchronizingContentOperator()); 479 result.put("reportMails", collection.getReportMails()); 480 481 result.put("languages", collection.getLanguages()); 482 483 Map<String, Object> values = collection.getParameterValues(); 484 for (String key : values.keySet()) 485 { 486 result.put(modelId + SCC_PARAMETERS_SEPARATOR + key, values.get(key)); 487 } 488 489 return result; 490 } 491 492 /** 493 * Gets the supported user directories (i.e. user directories based on a datasource) of the population in a json map 494 * @param populationId The id of the user population 495 * @return the supported user directories (i.e. user directories based on a datasource) of the population in a json map 496 */ 497 @Callable 498 public List<Map<String, Object>> getSupportedUserDirectories(String populationId) 499 { 500 List<Map<String, Object>> result = new ArrayList<>(); 501 502 UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(populationId); 503 List<UserDirectory> userDirectories = userPopulation.getUserDirectories(); 504 for (String udId : _getDatasourceBasedUserDirectories(userDirectories)) 505 { 506 UserDirectory userDirectory = userPopulation.getUserDirectory(udId); 507 String udModelId = userDirectory.getUserDirectoryModelId(); 508 UserDirectoryModel udModel = _userDirectoryFactory.getExtension(udModelId); 509 Map<String, Object> udMap = new HashMap<>(); 510 511 udMap.put("id", udId); 512 udMap.put("modelLabel", udModel.getLabel()); 513 514 if (userDirectory instanceof JdbcUserDirectory) 515 { 516 udMap.put("type", "SQL"); 517 } 518 else if (userDirectories instanceof LdapUserDirectory) 519 { 520 udMap.put("type", "LDAP"); 521 } 522 523 result.add(udMap); 524 } 525 526 return result; 527 } 528 529 private List<String> _getDatasourceBasedUserDirectories(List<UserDirectory> userDirectories) 530 { 531 List<String> ids = new ArrayList<>(); 532 for (UserDirectory userDirectory : userDirectories) 533 { 534 if (userDirectory instanceof JdbcUserDirectory || userDirectory instanceof LdapUserDirectory) 535 { 536 ids.add(userDirectory.getId()); 537 } 538 } 539 540 return ids; 541 } 542 543 private boolean _writeFile() 544 { 545 File backup = _createBackup(); 546 boolean errorOccured = false; 547 548 // Do writing 549 try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE)) 550 { 551 TransformerHandler th = _getTransformerHandler(os); 552 553 // sax the config 554 try 555 { 556 th.startDocument(); 557 XMLUtils.startElement(th, "collections"); 558 559 _toSAX(th); 560 XMLUtils.endElement(th, "collections"); 561 th.endDocument(); 562 } 563 catch (Exception e) 564 { 565 getLogger().error("Error when saxing the collections", e); 566 errorOccured = true; 567 } 568 } 569 catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e) 570 { 571 if (getLogger().isErrorEnabled()) 572 { 573 getLogger().error("Error when trying to modify the group directories with the configuration file {}", __CONFIGURATION_FILE, e); 574 } 575 } 576 577 _restoreBackup(backup, errorOccured); 578 579 return errorOccured; 580 } 581 582 private File _createBackup() 583 { 584 File backup = new File(__CONFIGURATION_FILE.getPath() + ".tmp"); 585 586 // Create a backup file 587 try 588 { 589 Files.copy(__CONFIGURATION_FILE.toPath(), backup.toPath()); 590 } 591 catch (IOException e) 592 { 593 getLogger().error("Error when creating backup '{}' file", __CONFIGURATION_FILE.toPath(), e); 594 } 595 596 return backup; 597 } 598 599 private void _restoreBackup(File backup, boolean errorOccured) 600 { 601 // Restore the file if an error previously occured 602 try 603 { 604 if (errorOccured) 605 { 606 // An error occured, restore the original 607 Files.copy(backup.toPath(), __CONFIGURATION_FILE.toPath(), StandardCopyOption.REPLACE_EXISTING); 608 // Force to reread the file 609 _readFile(true); 610 } 611 Files.deleteIfExists(backup.toPath()); 612 } 613 catch (IOException e) 614 { 615 if (getLogger().isErrorEnabled()) 616 { 617 getLogger().error("Error when restoring backup '{}' file", __CONFIGURATION_FILE, e); 618 } 619 } 620 } 621 622 /** 623 * Add a new {@link SynchronizableContentsCollection} 624 * @param values The parameters' values 625 * @return The id of new created collection or null in case of error 626 * @throws ProcessingException if creation failed 627 */ 628 @Callable 629 public String addCollection (Map<String, Object> values) throws ProcessingException 630 { 631 getLogger().debug("Add new Collection with values '{}'", values); 632 _readFile(false); 633 634 String id = _generateUniqueId((String) values.get("label")); 635 636 try 637 { 638 _addCollection(id, values); 639 return id; 640 } 641 catch (Exception e) 642 { 643 throw new ProcessingException("Failed to add new collection'" + id + "'", e); 644 } 645 } 646 647 /** 648 * Edit a {@link SynchronizableContentsCollection} 649 * @param id The id of collection to edit 650 * @param values The parameters' values 651 * @return The id of new created collection or null in case of error 652 * @throws ProcessingException if edition failed 653 */ 654 @Callable 655 public Map<String, Object> editCollection (String id, Map<String, Object> values) throws ProcessingException 656 { 657 getLogger().debug("Edit Collection with id '{}' and values '{}'", id, values); 658 Map<String, Object> result = new LinkedHashMap<>(); 659 660 SynchronizableContentsCollection collection = _synchronizableCollections.get(id); 661 if (collection == null) 662 { 663 getLogger().error("The collection with id '{}' does not exist, it cannot be edited.", id); 664 result.put("error", "unknown"); 665 return result; 666 } 667 else 668 { 669 _synchronizableCollections.remove(id); 670 } 671 672 try 673 { 674 _addCollection(id, values); 675 result.put("id", id); 676 return result; 677 } 678 catch (Exception e) 679 { 680 throw new ProcessingException("Failed to edit collection of id '" + id + "'", e); 681 } 682 } 683 684 private boolean _isValid(SynchronizableContentsCollection collection) 685 { 686 // Check validation of a data source on its parameters 687 688 SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(collection.getSynchronizeCollectionModelId()); 689 if (model != null) 690 { 691 for (ModelItem param : model.getModelItems()) 692 { 693 if (!_validateParameter(param, collection)) 694 { 695 // At least one parameter is invalid 696 return false; 697 } 698 699 if (ModelItemTypeConstants.DATASOURCE_ELEMENT_TYPE_ID.equals(param.getType().getId())) 700 { 701 String dataSourceId = (String) collection.getParameterValues().get(param.getPath()); 702 703 if (!_checkDataSource(dataSourceId)) 704 { 705 // At least one data source is not valid 706 return false; 707 } 708 709 } 710 } 711 712 return true; 713 } 714 715 return false; // no model found 716 } 717 718 private boolean _validateParameter(ModelItem modelItem, SynchronizableContentsCollection collection) 719 { 720 if (modelItem instanceof ElementDefinition) 721 { 722 ElementDefinition param = (ElementDefinition) modelItem; 723 Validator validator = param.getValidator(); 724 if (validator != null) 725 { 726 Object value = collection.getParameterValues().get(param.getPath()); 727 728 Errors errors = new Errors(); 729 validator.validate(value, errors); 730 731 return !errors.hasErrors(); 732 } 733 } 734 735 return true; 736 } 737 738 private boolean _checkDataSource(String dataSourceId) 739 { 740 if (dataSourceId != null) 741 { 742 try 743 { 744 DataSourceDefinition def = _getSQLDataSourceManager().getDataSourceDefinition(dataSourceId); 745 746 if (def != null) 747 { 748 _getSQLDataSourceManager().checkParameters(def.getParameters()); 749 } 750 else 751 { 752 def = _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId); 753 if (def != null) 754 { 755 _getLDAPDataSourceManager().getDataSourceDefinition(dataSourceId); 756 } 757 else 758 { 759 // The data source was not found 760 return false; 761 } 762 } 763 } 764 catch (ItemCheckerTestFailureException e) 765 { 766 // Connection to the SQL data source failed 767 return false; 768 } 769 } 770 771 return true; 772 } 773 774 private boolean _addCollection(String id, Map<String, Object> values) throws FileNotFoundException, IOException, TransformerConfigurationException, SAXException 775 { 776 File backup = _createBackup(); 777 boolean success = false; 778 779 // Do writing 780 try (OutputStream os = new FileOutputStream(__CONFIGURATION_FILE)) 781 { 782 TransformerHandler th = _getTransformerHandler(os); 783 784 // sax the config 785 th.startDocument(); 786 XMLUtils.startElement(th, "collections"); 787 788 // SAX already existing collections 789 _toSAX(th); 790 791 // SAX the new collection 792 _saxCollection(th, id, values); 793 794 XMLUtils.endElement(th, "collections"); 795 th.endDocument(); 796 797 success = true; 798 } 799 800 _restoreBackup(backup, !success); 801 802 _readFile(false); 803 804 return success; 805 } 806 807 private TransformerHandler _getTransformerHandler(OutputStream os) throws TransformerConfigurationException 808 { 809 // create a transformer for saving sax into a file 810 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 811 812 StreamResult result = new StreamResult(os); 813 th.setResult(result); 814 815 // create the format of result 816 Properties format = new Properties(); 817 format.put(OutputKeys.METHOD, "xml"); 818 format.put(OutputKeys.INDENT, "yes"); 819 format.put(OutputKeys.ENCODING, "UTF-8"); 820 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 821 th.getTransformer().setOutputProperties(format); 822 823 return th; 824 } 825 826 /** 827 * Removes the given collection 828 * @param id The id of the collection to remove 829 * @return A map containing the id of the removed collection, or an error 830 */ 831 @Callable 832 public Map<String, Object> removeCollection(String id) 833 { 834 getLogger().debug("Remove Collection with id '{}'", id); 835 Map<String, Object> result = new LinkedHashMap<>(); 836 837 _readFile(false); 838 if (_synchronizableCollections.remove(id) == null) 839 { 840 getLogger().error("The synchronizable collection with id '{}' does not exist, it cannot be removed.", id); 841 result.put("error", "unknown"); 842 return result; 843 } 844 845 if (_writeFile()) 846 { 847 return null; 848 } 849 850 result.put("id", id); 851 return result; 852 } 853 854 private String _generateUniqueId(String label) 855 { 856 // Id generated from name lowercased, trimmed, and spaces and underscores replaced by dashes 857 String value = label.toLowerCase().trim().replaceAll("[\\W_]", "-").replaceAll("-+", "-").replaceAll("^-", ""); 858 int i = 2; 859 String suffixedValue = value; 860 while (_synchronizableCollections.get(suffixedValue) != null) 861 { 862 suffixedValue = value + i; 863 i++; 864 } 865 866 return suffixedValue; 867 } 868 869 private void _toSAX(TransformerHandler handler) throws SAXException 870 { 871 for (SynchronizableContentsCollection collection : _synchronizableCollections.values()) 872 { 873 _saxCollection(handler, collection); 874 } 875 } 876 877 private void _saxCollection(ContentHandler handler, String id, Map<String, Object> parameters) throws SAXException 878 { 879 AttributesImpl atts = new AttributesImpl(); 880 atts.addCDATAAttribute("id", id); 881 882 XMLUtils.startElement(handler, "collection", atts); 883 884 String label = (String) parameters.get("label"); 885 if (label != null) 886 { 887 new I18nizableText(label).toSAX(handler, "label"); 888 } 889 890 _saxNonNullValue(handler, "contentType", parameters.get("contentType")); 891 _saxNonNullValue(handler, "contentPrefix", parameters.get("contentPrefix")); 892 _saxNonNullValue(handler, "restrictedField", parameters.get("restrictedField")); 893 _saxNonNullValue(handler, "synchronizeExistingContentsOnly", parameters.get("synchronizeExistingContentsOnly")); 894 _saxNonNullValue(handler, "removalSync", parameters.get("removalSync")); 895 _saxNonNullValue(handler, "ignoreRestrictions", parameters.get("ignoreRestrictions")); 896 897 _saxNonNullValue(handler, "workflowName", parameters.get("workflowName")); 898 _saxNonNullValue(handler, "initialActionId", parameters.get("initialActionId")); 899 _saxNonNullValue(handler, "synchronizeActionId", parameters.get("synchronizeActionId")); 900 _saxNonNullValue(handler, "validateActionId", parameters.get("validateActionId")); 901 _saxNonNullValue(handler, "validateAfterImport", parameters.get("validateAfterImport")); 902 903 _saxNonNullValue(handler, "reportMails", parameters.get("reportMails")); 904 _saxNonNullValue(handler, "contentOperator", parameters.get("contentOperator")); 905 906 _saxLanguagesValue(handler, parameters.get("languages")); 907 908 parameters.remove("id"); 909 parameters.remove("label"); 910 parameters.remove("contentType"); 911 parameters.remove("synchronizeExistingContentsOnly"); 912 parameters.remove("removalSync"); 913 parameters.remove("ignoreRestrictions"); 914 parameters.remove("workflowName"); 915 parameters.remove("initialActionId"); 916 parameters.remove("synchronizeActionId"); 917 parameters.remove("validateActionId"); 918 parameters.remove("contentPrefix"); 919 parameters.remove("validateAfterImport"); 920 parameters.remove("reportMails"); 921 parameters.remove("contentOperator"); 922 parameters.remove("restrictedField"); 923 924 String modelId = (String) parameters.get("modelId"); 925 parameters.remove("modelId"); 926 927 _saxModel(handler, modelId, parameters, true); 928 929 XMLUtils.endElement(handler, "collection"); 930 } 931 932 @SuppressWarnings("unchecked") 933 private void _saxLanguagesValue(ContentHandler handler, Object languages) throws SAXException 934 { 935 if (languages != null) 936 { 937 XMLUtils.startElement(handler, "languages"); 938 for (String lang : (List<String>) languages) 939 { 940 XMLUtils.createElement(handler, "value", lang); 941 } 942 XMLUtils.endElement(handler, "languages"); 943 } 944 } 945 946 private void _saxCollection(ContentHandler handler, SynchronizableContentsCollection collection) throws SAXException 947 { 948 AttributesImpl atts = new AttributesImpl(); 949 atts.addCDATAAttribute("id", collection.getId()); 950 XMLUtils.startElement(handler, "collection", atts); 951 952 collection.getLabel().toSAX(handler, "label"); 953 954 _saxNonNullValue(handler, "contentType", collection.getContentType()); 955 _saxNonNullValue(handler, "contentPrefix", collection.getContentPrefix()); 956 _saxNonNullValue(handler, "restrictedField", collection.getRestrictedField()); 957 958 _saxNonNullValue(handler, "workflowName", collection.getWorkflowName()); 959 _saxNonNullValue(handler, "initialActionId", collection.getInitialActionId()); 960 _saxNonNullValue(handler, "synchronizeActionId", collection.getSynchronizeActionId()); 961 _saxNonNullValue(handler, "validateActionId", collection.getValidateActionId()); 962 _saxNonNullValue(handler, "validateAfterImport", collection.validateAfterImport()); 963 964 _saxNonNullValue(handler, "reportMails", collection.getReportMails()); 965 _saxNonNullValue(handler, "contentOperator", collection.getSynchronizingContentOperator()); 966 _saxNonNullValue(handler, "synchronizeExistingContentsOnly", collection.synchronizeExistingContentsOnly()); 967 _saxNonNullValue(handler, "removalSync", collection.removalSync()); 968 _saxNonNullValue(handler, "ignoreRestrictions", collection.ignoreRestrictions()); 969 970 _saxLanguagesValue(handler, collection.getLanguages()); 971 972 _saxModel(handler, collection.getSynchronizeCollectionModelId(), collection.getParameterValues(), false); 973 974 XMLUtils.endElement(handler, "collection"); 975 } 976 977 private void _saxNonNullValue (ContentHandler handler, String tagName, Object value) throws SAXException 978 { 979 if (value != null) 980 { 981 XMLUtils.createElement(handler, tagName, value.toString()); 982 } 983 } 984 985 private void _saxModel(ContentHandler handler, String modelId, Map<String, Object> paramValues, boolean withPrefix) throws SAXException 986 { 987 AttributesImpl atts = new AttributesImpl(); 988 atts.addCDATAAttribute("id", modelId); 989 XMLUtils.startElement(handler, "model", atts); 990 991 SynchronizableContentsCollectionModel model = _syncCollectionModelEP.getExtension(modelId); 992 String prefix = withPrefix ? modelId + SCC_PARAMETERS_SEPARATOR : StringUtils.EMPTY; 993 for (ModelItem parameter : model.getModelItems()) 994 { 995 String paramFieldName = prefix + parameter.getPath(); 996 Object value = paramValues.get(paramFieldName); 997 if (value != null) 998 { 999 atts.clear(); 1000 atts.addCDATAAttribute("name", parameter.getPath()); 1001 XMLUtils.createElement(handler, "param", atts, ((ElementDefinition) parameter).getType().toString(value)); 1002 } 1003 } 1004 1005 XMLUtils.endElement(handler, "model"); 1006 } 1007 1008 @Override 1009 public void dispose() 1010 { 1011 _synchronizableCollections.clear(); 1012 _lastFileReading = 0; 1013 } 1014}