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