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