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