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