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