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