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