001/* 002 * Copyright 2015 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.runtime.plugins.admin.system; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.FileOutputStream; 021import java.io.InputStream; 022import java.io.OutputStream; 023import java.lang.management.ManagementFactory; 024import java.time.ZonedDateTime; 025import java.util.ArrayList; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Locale; 029import java.util.Map; 030import java.util.Properties; 031 032import javax.xml.transform.OutputKeys; 033import javax.xml.transform.TransformerFactory; 034import javax.xml.transform.sax.SAXTransformerFactory; 035import javax.xml.transform.sax.TransformerHandler; 036import javax.xml.transform.stream.StreamResult; 037 038import org.apache.avalon.framework.activity.Initializable; 039import org.apache.avalon.framework.component.Component; 040import org.apache.avalon.framework.configuration.Configuration; 041import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 042import org.apache.avalon.framework.context.Context; 043import org.apache.avalon.framework.context.ContextException; 044import org.apache.avalon.framework.context.Contextualizable; 045import org.apache.avalon.framework.logger.AbstractLogEnabled; 046import org.apache.avalon.framework.service.ServiceException; 047import org.apache.avalon.framework.service.ServiceManager; 048import org.apache.avalon.framework.service.Serviceable; 049import org.apache.cocoon.ProcessingException; 050import org.apache.cocoon.components.ContextHelper; 051import org.apache.cocoon.xml.XMLUtils; 052import org.apache.commons.io.FileUtils; 053import org.apache.commons.lang3.StringUtils; 054import org.apache.commons.lang3.Strings; 055import org.xml.sax.helpers.AttributesImpl; 056 057import org.ametys.core.cache.AbstractCacheManager; 058import org.ametys.core.cache.Cache; 059import org.ametys.core.ui.Callable; 060import org.ametys.core.user.UserIdentity; 061import org.ametys.core.util.DateUtils; 062import org.ametys.core.util.I18nUtils; 063import org.ametys.plugins.core.user.UserHelper; 064import org.ametys.runtime.i18n.I18nizableText; 065import org.ametys.runtime.servlet.RuntimeConfig; 066import org.ametys.runtime.servlet.RuntimeServlet; 067import org.ametys.runtime.servlet.RuntimeServlet.ForcedMainteanceInformations; 068import org.ametys.runtime.servlet.RuntimeServlet.RunMode; 069import org.ametys.runtime.util.AmetysHomeHelper; 070 071/** 072 * Helper for manipulating system announcement 073 */ 074public class SystemHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable, Initializable 075{ 076 /** The relative path to the file where system information are saved (announcement, maintenance...) */ 077 public static final String ADMINISTRATOR_SYSTEM_FILENAME = "system.xml"; 078 /** Avalon role */ 079 public static final String ROLE = SystemHelper.class.getName(); 080 081 private static final String SYSTEM_ANNOUNCEMENT_CACHE = SystemHelper.class.getName() + "$SystemAnnouncement"; 082 private static final String SYSTEM_ANNOUNCEMENT_CACHE_KEY = SystemHelper.class.getName() + "$SystemAnnouncement"; 083 084 085 private I18nUtils _i18nUtils; 086 private Context _context; 087 private AbstractCacheManager _cacheManager; 088 private UserHelper _userHelper; 089 090 @Override 091 public void service(ServiceManager serviceManager) throws ServiceException 092 { 093 _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE); 094 _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE); 095 _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE); 096 } 097 098 public void initialize() throws Exception 099 { 100 _cacheManager.createMemoryCache(SYSTEM_ANNOUNCEMENT_CACHE, 101 new I18nizableText("plugin.admin", "PLUGINS_ADMIN_CACHE_SYSTEM_ANNOUNCEMENT_LABEL"), 102 new I18nizableText("plugin.admin", "PLUGINS_ADMIN_CACHE_SYSTEM_ANNOUNCEMENT_DESCRIPTION"), 103 true, 104 null); 105 } 106 107 @Override 108 public void contextualize(Context context) throws ContextException 109 { 110 _context = context; 111 } 112 113 /** 114 * Get the current system announcement 115 * @return the announcement as JSON 116 */ 117 @Callable(rights = Callable.NO_CHECK_REQUIRED) 118 public Map<String, Object> getAnnouncement() 119 { 120 Map<String, Object> announcement = new HashMap<>(); 121 SystemAnnouncement systemAnnouncement = readValues(); 122 123 announcement.put("state", systemAnnouncement.getState()); 124 if (systemAnnouncement.getStartDate() != null) 125 { 126 announcement.put("startDate", DateUtils.zonedDateTimeToString(systemAnnouncement.getStartDate())); 127 } 128 if (systemAnnouncement.getEndDate() != null) 129 { 130 announcement.put("endDate", DateUtils.zonedDateTimeToString(systemAnnouncement.getEndDate())); 131 } 132 133 boolean isAvailable = isSystemAnnouncementAvailable(); 134 announcement.put("isAvailable", isAvailable); 135 136 if (isAvailable) 137 { 138 Locale locale = org.apache.cocoon.i18n.I18nUtils.findLocale(ContextHelper.getObjectModel(_context), "locale", null, Locale.getDefault(), true); 139 140 announcement.put("lastModification", getSystemAnnoucementLastModificationDate()); 141 announcement.put("message", getSystemAnnouncement(locale.getLanguage())); 142 } 143 Map<String, Object> maintenance = new HashMap<>(); 144 maintenance.put("active", RuntimeServlet.getRunMode() == RunMode.MAINTENANCE); 145 maintenance.put("mode", RuntimeServlet.getMaintenanceStatus().toString()); 146 147 long maintenanceTimestamp = ManagementFactory.getRuntimeMXBean().getStartTime(); 148 149 ForcedMainteanceInformations maintenanceStatusForcedInformations = RuntimeServlet.getMaintenanceStatusForcedInformations(); 150 if (maintenanceStatusForcedInformations != null) 151 { 152 maintenance.put("comment", maintenanceStatusForcedInformations.comment()); 153 154 UserIdentity initiator = maintenanceStatusForcedInformations.initiator(); 155 if (initiator != null) 156 { 157 String userFullName = StringUtils.defaultString(_userHelper.getUserFullName(initiator)); 158 maintenance.put("initiator", userFullName + " (" + UserIdentity.userIdentityToString(initiator) + ")"); 159 } 160 161 ZonedDateTime since = maintenanceStatusForcedInformations.since(); 162 if (since != null) 163 { 164 maintenanceTimestamp = since.toInstant().getEpochSecond(); 165 String sinceAsString = DateUtils.zonedDateTimeToString(since); 166 maintenance.put("since", sinceAsString); 167 } 168 } 169 170 maintenance.put("timestamp", maintenanceTimestamp); 171 172 announcement.put("maintenance", maintenance); 173 174 return announcement; 175 } 176 177 /** 178 * Get the system announces 179 * @return the list of announces 180 */ 181 @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin") 182 public List<Map<String, String>> getAnnouncements() 183 { 184 List<Map<String, String>> announcements = new ArrayList<> (); 185 186 SystemAnnouncement systemAnnouncement = readValues(); 187 188 Map<String, String> messages = systemAnnouncement.getMessages(); 189 190 for (String lang : messages.keySet()) 191 { 192 Map<String, String> message = new HashMap<> (); 193 message.put("language", lang); 194 message.put("message", messages.get(lang)); 195 196 announcements.add(message); 197 } 198 199 return announcements; 200 } 201 202 /** 203 * Enables or disable system announcement 204 * @param available true to enable system announcement 205 * @throws ProcessingException if an error occurred 206 */ 207 @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin") 208 public void setAnnouncementAvailable (boolean available) throws ProcessingException 209 { 210 SystemAnnouncement systemAnnouncement = readValues(); 211 212 _save(available ? "on" : "off", null, null, systemAnnouncement.getMessages()); 213 } 214 215 /** 216 * Schedule a system announcement 217 * @param startDateStr the planned start date. Can be null for no start date 218 * @param endDateStr the planned end date. Can be null for no end date 219 * @throws ProcessingException if an error occurred 220 */ 221 @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin") 222 public void scheduleAnnouncement(String startDateStr, String endDateStr) throws ProcessingException 223 { 224 // Parsing will throw an exception if string is not valid 225 // preventing the saving of bogus values 226 ZonedDateTime startDate = DateUtils.parseZonedDateTime(startDateStr); 227 ZonedDateTime endDate = DateUtils.parseZonedDateTime(endDateStr); 228 229 SystemAnnouncement systemAnnouncement = readValues(); 230 231 _save("scheduled", startDate, endDate, systemAnnouncement.getMessages()); 232 } 233 234 /** 235 * Add or edit a system announcement 236 * @param language the language typed in by the user or "*" if modifying the default message 237 * @param message the message to add nor edit 238 * @param override true to override the existing value if exists 239 * @return the result map 240 * @throws Exception if an exception occurs 241 */ 242 @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin") 243 public Map<String, Object> editAnnouncement(String language, String message, boolean override) throws Exception 244 { 245 Map<String, Object> result = new HashMap<> (); 246 247 SystemAnnouncement systemAnnouncement = readValues(); 248 249 Map<String, String> messages = systemAnnouncement.getMessages(); 250 if (messages.containsKey(language) && !override) 251 { 252 result.put("already-exists", true); 253 return result; 254 } 255 256 // Add or edit message 257 messages.put(language, message); 258 259 _save(systemAnnouncement.getState(), systemAnnouncement.getStartDate(), systemAnnouncement.getEndDate(), messages); 260 261 return result; 262 } 263 264 /** 265 * Delete a announcement 266 * @param language the language of the announcement to delete 267 * @throws ProcessingException if an exception occurs 268 * @return an empty map 269 */ 270 @Callable(rights = "Runtime_Rights_Admin_Access", context = "/admin") 271 public Map deleteAnnouncement(String language) throws ProcessingException 272 { 273 Map<String, Object> result = new HashMap<> (); 274 275 SystemAnnouncement systemAnnouncement = readValues(); 276 277 Map<String, String> messages = systemAnnouncement.getMessages(); 278 if (messages.containsKey(language)) 279 { 280 messages.remove(language); 281 282 _save(systemAnnouncement.getState(), systemAnnouncement.getStartDate(), systemAnnouncement.getEndDate(), messages); 283 } 284 285 return result; 286 } 287 288 private File _getSystemFile() 289 { 290 return FileUtils.getFile(RuntimeConfig.getInstance().getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, ADMINISTRATOR_SYSTEM_FILENAME); 291 } 292 293 /** 294 * Saves the system announcement's values 295 * @param state true to enable system announcement 296 * @param messages the messages 297 * @throws ProcessingException if an error ocurred 298 */ 299 private void _save (String state, ZonedDateTime startDate, ZonedDateTime endDate, Map<String, String> messages) throws ProcessingException 300 { 301 File systemFile = _getSystemFile(); 302 303 try 304 { 305 // Create file if not exists 306 if (!systemFile.exists()) 307 { 308 systemFile.getParentFile().mkdirs(); 309 systemFile.createNewFile(); 310 } 311 312 // create a transformer for saving sax into a file 313 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 314 315 // create the result where to write 316 try (OutputStream os = new FileOutputStream(systemFile)) 317 { 318 StreamResult sResult = new StreamResult(os); 319 th.setResult(sResult); 320 321 // create the format of result 322 Properties format = new Properties(); 323 format.put(OutputKeys.METHOD, "xml"); 324 format.put(OutputKeys.INDENT, "yes"); 325 format.put(OutputKeys.ENCODING, "UTF-8"); 326 th.getTransformer().setOutputProperties(format); 327 328 // Send SAX events 329 th.startDocument(); 330 331 AttributesImpl announcementsAttrs = new AttributesImpl(); 332 announcementsAttrs.addAttribute("", "state", "state", "CDATA", state); 333 if (startDate != null) 334 { 335 announcementsAttrs.addAttribute("", "start-date", "start-date", "CDATA", DateUtils.zonedDateTimeToString(startDate)); 336 } 337 if (endDate != null) 338 { 339 announcementsAttrs.addAttribute("", "end-date", "end-date", "CDATA", DateUtils.zonedDateTimeToString(endDate)); 340 } 341 342 XMLUtils.startElement(th, "announcements", announcementsAttrs); 343 344 for (String id : messages.keySet()) 345 { 346 AttributesImpl announcementAttrs = new AttributesImpl(); 347 if (!"*".equals(id)) 348 { 349 announcementAttrs.addAttribute("", "lang", "lang", "CDATA", id); 350 } 351 352 XMLUtils.createElement(th, "announcement", announcementAttrs, messages.get(id)); 353 } 354 355 XMLUtils.endElement(th, "announcements"); 356 357 th.endDocument(); 358 } 359 } 360 catch (Exception e) 361 { 362 throw new ProcessingException("Unable to save system announcement values", e); 363 } 364 finally 365 { 366 // clear the cache 367 _cacheManager.get(SYSTEM_ANNOUNCEMENT_CACHE).invalidateAll(); 368 } 369 } 370 371 /** 372 * Tests if system announcements are active. 373 * @return true if system announcements are active. 374 */ 375 @Callable (rights = Callable.NO_CHECK_REQUIRED) 376 public boolean isSystemAnnouncementAvailable() 377 { 378 SystemAnnouncement systemAnnouncement = readValues(); 379 return systemAnnouncement.isAvailable(); 380 } 381 382 /** 383 * Get the system announcement availability 384 * @return a map with the state, start date and end date 385 */ 386 public Map<String, Object> getSystemAnnouncementAvailability() 387 { 388 SystemAnnouncement announcement = readValues(); 389 // Use HashMap to be able to store null value 390 Map<String, Object> result = new HashMap<>(4); 391 result.put("state", announcement.getState()); 392 result.put("start-date", announcement.getStartDate()); 393 result.put("end-date", announcement.getEndDate()); 394 395 return result; 396 } 397 398 /** 399 * Return the date of the last modification of the annonce 400 * @return The date of the last modification or 0 if there is no announce file 401 */ 402 public long getSystemAnnoucementLastModificationDate() 403 { 404 try 405 { 406 File systemFile = _getSystemFile(); 407 if (!systemFile.exists() || !systemFile.isFile()) 408 { 409 return 0; 410 } 411 412 return systemFile.lastModified(); 413 } 414 catch (Exception e) 415 { 416 throw new RuntimeException("Unable to get system announcements", e); 417 } 418 } 419 420 /** 421 * Returns the system announcement for the given language code, or for the default language code if there is no specified announcement for the given language code.<br> 422 * Returns null if the system announcements are not activated. 423 * @param languageCode the desired language code of the system announcement 424 * @return the system announcement in the specified language code, or in the default language code, or null if announcements are not active. 425 */ 426 public String getSystemAnnouncement(String languageCode) 427 { 428 SystemAnnouncement systemAnnouncement = readValues(); 429 430 if (!systemAnnouncement.isAvailable()) 431 { 432 return null; 433 } 434 435 Map<String, String> messages = systemAnnouncement.getMessages(); 436 437 String announcement = null; 438 if (messages.containsKey(languageCode)) 439 { 440 announcement = messages.get(languageCode); 441 } 442 443 if (StringUtils.isEmpty(announcement)) 444 { 445 String defaultAnnouncement = messages.containsKey("*") ? messages.get("*") : null; 446 if (StringUtils.isEmpty(defaultAnnouncement)) 447 { 448 throw new IllegalStateException("There must be a default announcement."); 449 } 450 451 return defaultAnnouncement; 452 } 453 454 return announcement; 455 } 456 457 /** 458 * Read the system announcement's values 459 * @return The system announcement values; 460 */ 461 public SystemAnnouncement readValues () 462 { 463 Cache<String, SystemAnnouncement> cache = _cacheManager.get(SYSTEM_ANNOUNCEMENT_CACHE); 464 return cache.get(SYSTEM_ANNOUNCEMENT_CACHE_KEY, str -> _readValues()); 465 } 466 467 private SystemAnnouncement _readValues() 468 { 469 SystemAnnouncement announcement = new SystemAnnouncement(); 470 471 try 472 { 473 File systemFile = _getSystemFile(); 474 if (!systemFile.exists() || !systemFile.isFile()) 475 { 476 _setDefaultValues(); 477 } 478 479 Configuration configuration; 480 try (InputStream is = new FileInputStream(systemFile)) 481 { 482 configuration = new DefaultConfigurationBuilder().build(is); 483 } 484 485 // State 486 String state = configuration.getAttribute("state", "off"); 487 announcement.setState(state); 488 489 String startDate = configuration.getAttribute("start-date", null); 490 if (startDate != null) 491 { 492 announcement.setStartDate(DateUtils.parseZonedDateTime(startDate)); 493 } 494 495 String endDate = configuration.getAttribute("end-date", null); 496 if (endDate != null) 497 { 498 announcement.setEndDate(DateUtils.parseZonedDateTime(endDate)); 499 } 500 501 // Announcements 502 for (Configuration announcementConfiguration : configuration.getChildren("announcement")) 503 { 504 String lang = announcementConfiguration.getAttribute("lang", "*"); 505 String message = announcementConfiguration.getValue(); 506 507 announcement.addMessage(lang, message); 508 } 509 510 return announcement; 511 } 512 catch (Exception e) 513 { 514 throw new RuntimeException("Unable to get system announcements", e); 515 } 516 } 517 518 private void _setDefaultValues () throws ProcessingException 519 { 520 Map objectModel = ContextHelper.getObjectModel(_context); 521 Locale locale = org.apache.cocoon.i18n.I18nUtils.findLocale(objectModel, "locale", null, Locale.getDefault(), true); 522 String defaultMessage = _i18nUtils.translate(new I18nizableText("plugin.admin", "PLUGINS_ADMIN_SYSTEM_DEFAULTMESSAGE"), locale.getLanguage()); 523 524 Map<String, String> messages = new HashMap<> (); 525 messages.put("*", defaultMessage); 526 527 _save("off", null, null, messages); 528 } 529 530 /** 531 * Class representing the system announcement file 532 */ 533 public static class SystemAnnouncement 534 { 535 private String _state; 536 private Map<String, String> _messages; 537 private ZonedDateTime _startDate; 538 private ZonedDateTime _endDate; 539 540 /** 541 * Constructor 542 */ 543 public SystemAnnouncement() 544 { 545 _state = "off"; 546 _startDate = null; 547 _endDate = null; 548 _messages = new HashMap<>(); 549 } 550 551 /** 552 * Is the system announcement available ? 553 * @return true if the system announcement is available, false otherwise 554 */ 555 public boolean isAvailable() 556 { 557 return Strings.CS.equals(_state, "on") 558 || Strings.CS.equals(_state, "scheduled") 559 && (_startDate == null || ZonedDateTime.now().isAfter(_startDate)) 560 && (_endDate == null || ZonedDateTime.now().isBefore(_endDate)); 561 } 562 563 /** 564 * Get the messages by language 565 * @return the messages by languaga 566 */ 567 public Map<String, String> getMessages () 568 { 569 return _messages; 570 } 571 572 /** 573 * Set the state of the system announcement 574 * @param state 'on' to set the enable system announcement, 575 * 'scheduled' to enable it base on the start and end date 576 * disabled otherwise 577 */ 578 public void setState (String state) 579 { 580 _state = state; 581 } 582 583 /** 584 * Get the state 585 * @return "on", "off", "scheduled" 586 */ 587 public String getState() 588 { 589 return _state; 590 } 591 592 /** 593 * Set the start date for scheduled announcement 594 * @param startDate the start date or null 595 */ 596 public void setStartDate(ZonedDateTime startDate) 597 { 598 _startDate = startDate; 599 } 600 601 /** 602 * Get the start date for scheduled announcement 603 * @return the start date or null 604 */ 605 public ZonedDateTime getStartDate() 606 { 607 return _startDate; 608 } 609 610 /** 611 * Set the end date for scheduled announcement 612 * @param endDate the end date or null 613 */ 614 public void setEndDate(ZonedDateTime endDate) 615 { 616 _endDate = endDate; 617 } 618 619 /** 620 * Get the end date for scheduled announcement 621 * @return the end date or null 622 */ 623 public ZonedDateTime getEndDate() 624 { 625 return _endDate; 626 } 627 628 /** 629 * Add a message to the list of announcements 630 * @param lang the language of the message 631 * @param message the message itself 632 */ 633 public void addMessage (String lang, String message) 634 { 635 _messages.put(lang, message); 636 } 637 638 } 639}