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