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.core.group; 017 018import java.io.File; 019import java.io.FileOutputStream; 020import java.io.IOException; 021import java.io.OutputStream; 022import java.nio.file.Files; 023import java.nio.file.StandardCopyOption; 024import java.time.Instant; 025import java.time.temporal.ChronoUnit; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.HashMap; 029import java.util.LinkedHashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Properties; 033import java.util.Set; 034import java.util.regex.Pattern; 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.service.ServiceException; 051import org.apache.avalon.framework.service.ServiceManager; 052import org.apache.avalon.framework.service.Serviceable; 053import org.apache.cocoon.components.LifecycleHelper; 054import org.apache.cocoon.xml.AttributesImpl; 055import org.apache.cocoon.xml.XMLUtils; 056import org.apache.xml.serializer.OutputPropertiesFactory; 057import org.xml.sax.SAXException; 058 059import org.ametys.core.group.directory.GroupDirectory; 060import org.ametys.core.group.directory.GroupDirectoryFactory; 061import org.ametys.core.group.directory.GroupDirectoryModel; 062import org.ametys.core.ui.Callable; 063import org.ametys.runtime.i18n.I18nizableText; 064import org.ametys.runtime.model.DefinitionContext; 065import org.ametys.runtime.model.ElementDefinition; 066import org.ametys.runtime.model.type.DataContext; 067import org.ametys.runtime.model.type.ElementType; 068import org.ametys.runtime.plugin.PluginsManager; 069import org.ametys.runtime.plugin.PluginsManager.Status; 070import org.ametys.runtime.plugin.component.AbstractLogEnabled; 071import org.ametys.runtime.util.AmetysHomeHelper; 072 073/** 074 * DAO for accessing {@link GroupDirectory} 075 */ 076public class GroupDirectoryDAO extends AbstractLogEnabled implements Component, Initializable, Serviceable, Disposable 077{ 078 /** Avalon Role */ 079 public static final String ROLE = GroupDirectoryDAO.class.getName(); 080 081 /** The path of the XML file containing the group directories */ 082 private static File __GROUP_DIRECTORIES_FILE; 083 084 /** The regular expression for an id of a group directory */ 085 private static final String __ID_REGEX = "^[a-z][a-z0-9_-]*"; 086 087 /** The date (as a long) of the last time the {@link #__GROUP_DIRECTORIES_FILE GroupDirectories file} was read (last update) */ 088 private long _lastFileReading; 089 090 /** The whole group directories of the application */ 091 private Map<String, GroupDirectory> _groupDirectories; 092 093 /** The factory for group directories */ 094 private GroupDirectoryFactory _groupDirectoryFactory; 095 096 @Override 097 public void initialize() 098 { 099 __GROUP_DIRECTORIES_FILE = new File(AmetysHomeHelper.getAmetysHome(), "config" + File.separator + "group-directories.xml"); 100 _groupDirectories = new LinkedHashMap<>(); 101 _lastFileReading = 0; 102 } 103 104 @Override 105 public void service(ServiceManager manager) throws ServiceException 106 { 107 _groupDirectoryFactory = (GroupDirectoryFactory) manager.lookup(GroupDirectoryFactory.ROLE); 108 } 109 110 /** 111 * Gets all the group directories to JSON format 112 * @return A list of object representing the {@link GroupDirectory GroupDirectories} 113 */ 114 public List<Object> getGroupDirectories2Json() 115 { 116 List<Object> result = new ArrayList<>(); 117 for (GroupDirectory groupDirectory : getGroupDirectories()) 118 { 119 result.add(getGroupDirectory2Json(groupDirectory)); 120 } 121 return result; 122 } 123 124 /** 125 * gets a group directory to JSON format 126 * @param groupDirectory The group directory to get 127 * @return An object representing a {@link GroupDirectory} 128 */ 129 public Map<String, Object> getGroupDirectory2Json(GroupDirectory groupDirectory) 130 { 131 Map<String, Object> result = new LinkedHashMap<>(); 132 result.put("id", groupDirectory.getId()); 133 result.put("label", groupDirectory.getLabel()); 134 String modelId = groupDirectory.getGroupDirectoryModelId(); 135 GroupDirectoryModel model = _groupDirectoryFactory.getExtension(modelId); 136 result.put("modelLabel", model.getLabel()); 137 return result; 138 } 139 140 /** 141 * Gets all the group directories of this application 142 * @return A list of {@link GroupDirectory GroupDirectories} 143 */ 144 public List<GroupDirectory> getGroupDirectories() 145 { 146 // Don't read in safe mode, we know that only the admin population is needed in this case and we want to prevent some warnings in the logs for non-safe features not found 147 if (Status.OK.equals(PluginsManager.getInstance().getStatus())) 148 { 149 _read(false); 150 return new ArrayList<>(_groupDirectories.values()); 151 } 152 else 153 { 154 return Collections.EMPTY_LIST; 155 } 156 } 157 158 /** 159 * Gets a group directory by its id. 160 * @param id The id of the group directory 161 * @return The {@link GroupDirectory}, or null if not found 162 */ 163 public GroupDirectory getGroupDirectory(String id) 164 { 165 _read(false); 166 return _groupDirectories.get(id); 167 } 168 169 /** 170 * Gets the list of the ids of all the group directories of the application 171 * @return The list of the ids of all the group directories 172 */ 173 @Callable 174 public Set<String> getGroupDirectoriesIds() 175 { 176 _read(false); 177 return _groupDirectories.keySet(); 178 } 179 180 /** 181 * Gets the configuration for creating/editing a group directory. 182 * @return A map containing information about what is needed to create/edit a group directory 183 * @throws Exception If an error occurs. 184 */ 185 @Callable 186 public Map<String, Object> getEditionConfiguration() throws Exception 187 { 188 Map<String, Object> result = new LinkedHashMap<>(); 189 190 List<Object> groupDirectoryModels = new ArrayList<>(); 191 for (String extensionId : _groupDirectoryFactory.getExtensionsIds()) 192 { 193 GroupDirectoryModel model = _groupDirectoryFactory.getExtension(extensionId); 194 Map<String, Object> gdMap = new LinkedHashMap<>(); 195 gdMap.put("id", extensionId); 196 gdMap.put("label", model.getLabel()); 197 gdMap.put("description", model.getDescription()); 198 199 Map<String, Object> params = new LinkedHashMap<>(); 200 for (String paramId : model.getParameters().keySet()) 201 { 202 // prefix in case of two parameters from two different models have the same id which can lead to some errorsin client-side 203 params.put(extensionId + "$" + paramId, model.getParameters().get(paramId).toJSON(DefinitionContext.newInstance().withEdition(true))); 204 } 205 gdMap.put("parameters", params); 206 207 groupDirectoryModels.add(gdMap); 208 } 209 result.put("groupDirectoryModels", groupDirectoryModels); 210 211 return result; 212 } 213 214 /** 215 * Gets the values of the parameters of the given group directory 216 * @param id The id of the group directory 217 * @return The values of the parameters 218 */ 219 @Callable 220 public Map<String, Object> getGroupDirectoryParameterValues(String id) 221 { 222 Map<String, Object> result = new LinkedHashMap<>(); 223 224 _read(false); 225 GroupDirectory gd = _groupDirectories.get(id); 226 227 if (gd == null) 228 { 229 getLogger().error("The GroupDirectory of id '{}' does not exist.", id); 230 result.put("error", "unknown"); 231 return result; 232 } 233 234 result.put("label", gd.getLabel()); 235 result.put("id", gd.getId()); 236 String modelId = gd.getGroupDirectoryModelId(); 237 result.put("modelId", modelId); 238 Map<String, Object> params = new HashMap<>(); 239 for (String key : gd.getParameterValues().keySet()) 240 { 241 params.put(modelId + "$" + key, gd.getParameterValues().get(key)); 242 } 243 result.put("params", params); 244 245 return result; 246 } 247 248 /** 249 * Adds a new group directory 250 * @param id The unique id of the group directory 251 * @param label The label of the group directory 252 * @param modelId The id of the group directory model 253 * @param params The parameters of the group directory 254 * @return A map containing the id of the created group directory, or the kind of error that occured 255 */ 256 @Callable 257 public Map<String, Object> add(String id, String label, String modelId, Map<String, String> params) 258 { 259 _read(false); 260 261 Map<String, Object> result = new LinkedHashMap<>(); 262 263 if (!_isCorrectId(id)) 264 { 265 return null; 266 } 267 268 GroupDirectory gd = _createGroupDirectory(id, label, modelId, params); 269 if (gd == null) 270 { 271 getLogger().error("An error occured when creating the GroupDirectory with id '{}'. See previous logs for more information.", id); 272 result.put("error", "server"); 273 return result; 274 } 275 276 _groupDirectories.put(id, gd); 277 if (_write()) 278 { 279 getLogger().error("An error occured when writing the configuration file which contains the group directories.", id); 280 result.put("error", "server"); 281 return result; 282 } 283 284 result.put("id", id); 285 return result; 286 } 287 288 private boolean _isCorrectId(String id) 289 { 290 if (_groupDirectories.get(id) != null) 291 { 292 getLogger().error("The id '{}' is already used for a group directory.", id); 293 return false; 294 } 295 296 if (!Pattern.matches(__ID_REGEX, id)) 297 { 298 getLogger().error("The id '{}' is not a correct id for a group directory.", id); 299 return false; 300 } 301 302 return true; 303 } 304 305 /** 306 * Edits the given group directory 307 * @param id The id of the group directory to edit 308 * @param label The label of the group directory 309 * @param modelId The id of the group directory model 310 * @param params The parameters of the group directory 311 * @return A map containing the id of the edited group directory, or the kind of error that occured 312 */ 313 @Callable 314 public Map<String, Object> edit(String id, String label, String modelId, Map<String, String> params) 315 { 316 _read(false); 317 318 Map<String, Object> result = new LinkedHashMap<>(); 319 320 GroupDirectory gd = _groupDirectories.get(id); 321 if (gd == null) 322 { 323 getLogger().error("The GroupDirectory with id '{}' does not exist, it cannot be edited.", id); 324 result.put("error", "unknown"); 325 return result; 326 } 327 else 328 { 329 GroupDirectory removedGroupDirectory = _groupDirectories.remove(id); 330 LifecycleHelper.dispose(removedGroupDirectory); 331 } 332 333 GroupDirectory newGd = _createGroupDirectory(id, label, modelId, params); 334 if (newGd == null) 335 { 336 getLogger().error("An error occured when editing the GroupDirectory with id '{}'. See previous logs for more information.", id); 337 result.put("error", "server"); 338 return result; 339 } 340 341 _groupDirectories.put(id, newGd); 342 if (_write()) 343 { 344 getLogger().error("An error occured when writing the configuration file which contains the group directories.", id); 345 result.put("error", "server"); 346 return result; 347 } 348 349 result.put("id", id); 350 return result; 351 } 352 353 private GroupDirectory _createGroupDirectory(String id, String label, String modelId, Map<String, String> params) 354 { 355 Map<String, Object> typedParams = _getTypedParams(params, modelId); 356 return _groupDirectoryFactory.createGroupDirectory(id, new I18nizableText(label), modelId, typedParams); 357 } 358 359 private Map<String, Object> _getTypedParams(Map<String, String> params, String modelId) 360 { 361 Map<String, Object> resultParameters = new LinkedHashMap<>(); 362 363 Map<String, ? extends ElementDefinition> declaredParameters = _groupDirectoryFactory.getExtension(modelId).getParameters(); 364 for (String paramNameWithPrefix : params.keySet()) 365 { 366 String[] splitStr = paramNameWithPrefix.split("\\$", 2); 367 String prefix = splitStr[0]; 368 String paramName = splitStr[1]; 369 if (prefix.equals(modelId) && declaredParameters.containsKey(paramName)) 370 { 371 String originalValue = params.get(paramNameWithPrefix); 372 373 ElementDefinition parameter = declaredParameters.get(paramName); 374 ElementType type = parameter.getType(); 375 376 Object typedValue = type.castValue(originalValue); 377 resultParameters.put(paramName, typedValue); 378 } 379 else if (prefix.equals(modelId)) 380 { 381 getLogger().warn("The parameter {} is not declared in extension {}. It will be ignored", paramName, modelId); 382 } 383 } 384 385 return resultParameters; 386 } 387 388 /** 389 * Removes the given group directory 390 * @param id The id of the group directory to remove 391 * @return A map containing the id of the removed group directory 392 */ 393 @Callable 394 public Map<String, Object> remove(String id) 395 { 396 Map<String, Object> result = new LinkedHashMap<>(); 397 398 _read(false); 399 GroupDirectory removedGroupDirectory = _groupDirectories.remove(id); 400 if (removedGroupDirectory == null) 401 { 402 getLogger().error("The GroupDirectory with id '{}' does not exist, it cannot be removed.", id); 403 result.put("error", "unknown"); 404 return result; 405 } 406 407 LifecycleHelper.dispose(removedGroupDirectory); 408 409 if (_write()) 410 { 411 return null; 412 } 413 414 result.put("id", id); 415 return result; 416 } 417 418 /** 419 * If needed, reads the config file representing the group directories and then 420 * reinitializes and updates the internal representation of the group directories. 421 * @param forceRead True to avoid the use of the cache and force the reading of the file 422 */ 423 private synchronized void _read(boolean forceRead) 424 { 425 try 426 { 427 if (!__GROUP_DIRECTORIES_FILE.exists()) 428 { 429 _createDirectoriesFile(__GROUP_DIRECTORIES_FILE); 430 } 431 432 // In Linux file systems, the precision of java.io.File.lastModified() is the second, so we need here to always have 433 // this (bad!) precision by doing the truncation to second precision (/1000 * 1000) on the millis time value. 434 // Therefore, the boolean outdated is computed with '>=' operator, and not '>', which will lead to sometimes (but rarely) unnecessarily re-read the file. 435 long fileLastModified = (__GROUP_DIRECTORIES_FILE.lastModified() / 1000) * 1000; 436 if (forceRead || fileLastModified >= _lastFileReading) 437 { 438 long lastFileReading = Instant.now().truncatedTo(ChronoUnit.SECONDS).toEpochMilli(); 439 Map<String, GroupDirectory> groupDirectories = new LinkedHashMap<>(); 440 441 Configuration cfg = new DefaultConfigurationBuilder().buildFromFile(__GROUP_DIRECTORIES_FILE); 442 for (Configuration childCfg : cfg.getChildren("groupDirectory")) 443 { 444 try 445 { 446 GroupDirectory groupDirectory = _configureGroupDirectory(childCfg); 447 if (groupDirectory != null) 448 { 449 groupDirectories.put(groupDirectory.getId(), groupDirectory); 450 } 451 } 452 catch (ConfigurationException e) 453 { 454 getLogger().error("Error configuring the group directory '" + childCfg.getAttribute("id", "") + "'. The group directory will be ignored.", e); 455 } 456 } 457 458 // Release previous components 459 this.dispose(); 460 461 _lastFileReading = lastFileReading; 462 _groupDirectories = groupDirectories; 463 } 464 } 465 catch (IOException | TransformerConfigurationException | ConfigurationException | SAXException e) 466 { 467 if (getLogger().isErrorEnabled()) 468 { 469 getLogger().error("Error retrieving group directories with the configuration file " + __GROUP_DIRECTORIES_FILE, e); 470 } 471 } 472 } 473 474 private void _createDirectoriesFile(File file) throws IOException, TransformerConfigurationException, SAXException 475 { 476 file.createNewFile(); 477 try (OutputStream os = new FileOutputStream(file)) 478 { 479 // create a transformer for saving sax into a file 480 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 481 482 StreamResult result = new StreamResult(os); 483 th.setResult(result); 484 485 // create the format of result 486 Properties format = new Properties(); 487 format.put(OutputKeys.METHOD, "xml"); 488 format.put(OutputKeys.INDENT, "yes"); 489 format.put(OutputKeys.ENCODING, "UTF-8"); 490 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 491 th.getTransformer().setOutputProperties(format); 492 th.startDocument(); 493 XMLUtils.createElement(th, "groupDirectories"); 494 th.endDocument(); 495 } 496 } 497 498 private GroupDirectory _configureGroupDirectory(Configuration configuration) throws ConfigurationException 499 { 500 String id = configuration.getAttribute("id"); 501 String modelId = configuration.getAttribute("modelId"); 502 I18nizableText label = new I18nizableText(configuration.getChild("label").getValue()); 503 Map<String, Object> paramValues = _getParametersFromConfiguration(configuration.getChild("params"), modelId); 504 if (paramValues != null) 505 { 506 GroupDirectory gd = _groupDirectoryFactory.createGroupDirectory(id, label, modelId, paramValues); 507 if (gd != null) 508 { 509 return gd; 510 } 511 } 512 513 return null; 514 } 515 516 private Map<String, Object> _getParametersFromConfiguration(Configuration conf, String modelId) 517 { 518 Map<String, Object> parameters = new LinkedHashMap<>(); 519 520 if (!_groupDirectoryFactory.hasExtension(modelId)) 521 { 522 getLogger().warn("The model id '{}' is referenced in the file containing the group directories but seems to not exist.", modelId); 523 return null; 524 } 525 526 Map<String, ? extends ElementDefinition> declaredParameters = _groupDirectoryFactory.getExtension(modelId).getParameters(); 527 for (Configuration paramConf : conf.getChildren()) 528 { 529 String paramName = paramConf.getName(); 530 if (declaredParameters.containsKey(paramName)) 531 { 532 String valueAsString = paramConf.getValue(""); 533 534 ElementDefinition parameter = declaredParameters.get(paramName); 535 ElementType type = parameter.getType(); 536 537 Object typedValue = type.castValue(valueAsString); 538 parameters.put(paramName, typedValue); 539 } 540 else 541 { 542 getLogger().warn("The parameter '{}' is not declared in extension '{}'. It will be ignored", paramName, modelId); 543 } 544 } 545 546 return parameters; 547 } 548 549 /** 550 * Erases the config file representing the group directories and rebuild it 551 * from the internal representation of the group directories. 552 * @return True if an error occured 553 */ 554 private boolean _write() 555 { 556 File backup = new File(__GROUP_DIRECTORIES_FILE.getPath() + ".tmp"); 557 boolean errorOccured = false; 558 559 // Create a backup file 560 try 561 { 562 Files.copy(__GROUP_DIRECTORIES_FILE.toPath(), backup.toPath()); 563 } 564 catch (IOException e) 565 { 566 if (getLogger().isErrorEnabled()) 567 { 568 getLogger().error("Error when creating backup '" + __GROUP_DIRECTORIES_FILE + "' file", e); 569 } 570 } 571 572 // Do writing 573 try (OutputStream os = new FileOutputStream(__GROUP_DIRECTORIES_FILE)) 574 { 575 // create a transformer for saving sax into a file 576 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 577 578 StreamResult result = new StreamResult(os); 579 th.setResult(result); 580 581 // create the format of result 582 Properties format = new Properties(); 583 format.put(OutputKeys.METHOD, "xml"); 584 format.put(OutputKeys.INDENT, "yes"); 585 format.put(OutputKeys.ENCODING, "UTF-8"); 586 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 587 th.getTransformer().setOutputProperties(format); 588 589 // sax the config 590 try 591 { 592 _toSAX(th); 593 } 594 catch (Exception e) 595 { 596 if (getLogger().isErrorEnabled()) 597 { 598 getLogger().error("Error when saxing the groupDirectories", e); 599 } 600 errorOccured = true; 601 } 602 } 603 catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e) 604 { 605 if (getLogger().isErrorEnabled()) 606 { 607 getLogger().error("Error when trying to modify the group directories with the configuration file " + __GROUP_DIRECTORIES_FILE, e); 608 } 609 } 610 611 // Restore the file if an error previously occured 612 try 613 { 614 if (errorOccured) 615 { 616 // An error occured, restore the original 617 Files.copy(backup.toPath(), __GROUP_DIRECTORIES_FILE.toPath(), StandardCopyOption.REPLACE_EXISTING); 618 // Force to reread the file 619 _read(true); 620 } 621 Files.deleteIfExists(backup.toPath()); 622 } 623 catch (IOException e) 624 { 625 if (getLogger().isErrorEnabled()) 626 { 627 getLogger().error("Error when restoring backup '" + __GROUP_DIRECTORIES_FILE + "' file", e); 628 } 629 } 630 631 return errorOccured; 632 } 633 634 private void _toSAX(TransformerHandler handler) throws SAXException, IOException 635 { 636 handler.startDocument(); 637 XMLUtils.startElement(handler, "groupDirectories"); 638 for (GroupDirectory gd : _groupDirectories.values()) 639 { 640 _saxGroupDirectory(gd, handler); 641 } 642 643 XMLUtils.endElement(handler, "groupDirectories"); 644 handler.endDocument(); 645 } 646 647 private void _saxGroupDirectory(GroupDirectory groupDirectory, TransformerHandler handler) throws SAXException, IOException 648 { 649 AttributesImpl atts = new AttributesImpl(); 650 atts.addCDATAAttribute("id", groupDirectory.getId()); 651 atts.addCDATAAttribute("modelId", groupDirectory.getGroupDirectoryModelId()); 652 XMLUtils.startElement(handler, "groupDirectory", atts); 653 654 groupDirectory.getLabel().toSAX(handler, "label"); 655 656 XMLUtils.startElement(handler, "params"); 657 Map<String, Object> paramValues = groupDirectory.getParameterValues(); 658 GroupDirectoryModel groupDirectoryModel = _groupDirectoryFactory.getExtension(groupDirectory.getGroupDirectoryModelId()); 659 Map<String, ? extends ElementDefinition> definitions = groupDirectoryModel.getParameters(); 660 for (String paramName : paramValues.keySet()) 661 { 662 Object value = paramValues.get(paramName); 663 ElementDefinition definition = definitions.get(paramName); 664 if (definition != null) 665 { 666 definition.getType().valueToSAX(handler, paramName, value, DataContext.newInstance()); 667 } 668 } 669 XMLUtils.endElement(handler, "params"); 670 671 XMLUtils.endElement(handler, "groupDirectory"); 672 } 673 674 @Override 675 public void dispose() 676 { 677 for (GroupDirectory gd : _groupDirectories.values()) 678 { 679 LifecycleHelper.dispose(gd); 680 } 681 _lastFileReading = 0; 682 } 683}