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