001/* 002 * Copyright 2010 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 */ 016 017package org.ametys.web.cache; 018 019import java.io.ByteArrayInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.nio.charset.StandardCharsets; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Timer; 028import java.util.TimerTask; 029 030import javax.xml.parsers.DocumentBuilder; 031import javax.xml.parsers.DocumentBuilderFactory; 032import javax.xml.parsers.ParserConfigurationException; 033import javax.xml.transform.TransformerException; 034 035import org.apache.commons.io.IOUtils; 036import org.apache.commons.lang3.StringUtils; 037import org.apache.http.Header; 038import org.apache.http.HttpResponse; 039import org.apache.http.NameValuePair; 040import org.apache.http.client.ClientProtocolException; 041import org.apache.http.client.config.RequestConfig; 042import org.apache.http.client.entity.UrlEncodedFormEntity; 043import org.apache.http.client.methods.CloseableHttpResponse; 044import org.apache.http.client.methods.HttpPost; 045import org.apache.http.impl.client.CloseableHttpClient; 046import org.apache.http.impl.client.HttpClientBuilder; 047import org.apache.xpath.XPathAPI; 048import org.slf4j.Logger; 049import org.w3c.dom.Document; 050import org.xml.sax.SAXException; 051 052import org.ametys.runtime.config.Config; 053import org.ametys.web.repository.page.Page; 054import org.ametys.web.repository.site.Site; 055import org.ametys.web.repository.sitemap.Sitemap; 056 057/** 058 * Helper for dealing with front-office cache. 059 */ 060public final class CacheHelper 061{ 062 static Integer _lockToken = 0; 063 static Map<String, Long> _lastInvalidationDate = new HashMap<>(); 064 static Map<String, String> _timerTasks = new HashMap<>(); 065 066 static Timer _timer = new Timer("timer-cache", true); 067 068 private static Map<String, Long> _cacheForPeriodOfValidity = new HashMap<>(); 069 070 private CacheHelper() 071 { 072 // empty constructor 073 } 074 075 /** 076 * Request the given URL on each configured front-office. 077 * @param url the url to be called. 078 * @param logger logger for traces. 079 * @throws Exception if an error occurred. 080 */ 081 public static void testWS(String url, Logger logger) throws Exception 082 { 083 testWS(url, null, logger); 084 } 085 086 /** 087 * Request the given URL on each configured front-office. 088 * @param url the url to be called. 089 * @param postParameters submited values 090 * @param logger logger for traces. 091 * @throws Exception if an error occurred. 092 */ 093 public static void testWS(String url, List<NameValuePair> postParameters, Logger logger) throws Exception 094 { 095 List<Map<String, Object>> responses = callWS(url, postParameters, logger); 096 for (Map<String, Object> response : responses) 097 { 098 byte[] byteArray = (byte[]) response.get("bodyResponse"); 099 HttpPost request = (HttpPost) response.get("request"); 100 HttpResponse httpresponse = (HttpResponse) response.get("response"); 101 102 if (_checkResponse(byteArray)) 103 { 104 if (logger.isDebugEnabled()) 105 { 106 logger.debug("Request for cache invalidation sent and received successfully to: " + request.getURI().getHost()); 107 } 108 } 109 else 110 { 111 logger.error("Unable to invalidate cache with request '" + url + "' to server '" + request.getURI().getHost() + "', response: " + (httpresponse != null ? httpresponse.getStatusLine().toString() : "<no response>")); 112 } 113 } 114 } 115 116 /** 117 * Request the given URL on each configured front-office. 118 * @param url the url to be called. 119 * @param logger logger for traces. 120 * @return The streams 121 * @throws Exception if an error occurred. 122 */ 123 public static List<Map<String, Object>> callWS(String url, Logger logger) throws Exception 124 { 125 return callWS(url, null, logger); 126 } 127 128 /** 129 * Get front office applications urls 130 * @return The non null list of url configured. Can be empty. 131 */ 132 public static String[] getFrontURLS() 133 { 134 String frontConfig = Config.getInstance().getValue("org.ametys.web.front.url"); 135 String[] frontURLs = StringUtils.split(frontConfig, ","); 136 return frontURLs; 137 } 138 139 /** 140 * Request the given URL on each configured front-office. 141 * @param url the url to be called. 142 * @param postParameters submitted values 143 * @param logger logger for traces. 144 * @return The streams 145 * @throws Exception if an error occurred. 146 */ 147 public static List<Map<String, Object>> callWS(String url, List<NameValuePair> postParameters, Logger logger) throws Exception 148 { 149 String[] frontURLs = getFrontURLS(); 150 151 List<Map<String, Object>> responses = new ArrayList<>(); 152 for (String rawFrontURL : frontURLs) 153 { 154 String frontURL = rawFrontURL.trim(); 155 156 RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(2000).setSocketTimeout(2000).build(); 157 158 String wsURL = frontURL + url; 159 160 try (CloseableHttpClient httpclient = HttpClientBuilder.create().useSystemProperties().setDefaultRequestConfig(requestConfig).build()) 161 { 162 163 // Prepare a request object 164 HttpPost request = new HttpPost(wsURL); 165 request.addHeader("X-Ametys-BO", "true"); 166 167 if (postParameters != null) 168 { 169 request.setEntity(new UrlEncodedFormEntity(postParameters, StandardCharsets.UTF_8)); 170 } 171 172 Map<String, Object> responseObject = new HashMap<>(); 173 174 // Execute the request 175 try (CloseableHttpResponse response = httpclient.execute(request)) 176 { 177 responseObject.put("bodyResponse", _getResponse(response)); 178 responseObject.put("response", response); 179 } 180 181 responses.add(responseObject); 182 responseObject.put("request", request); 183 } 184 catch (Exception e) 185 { 186 if (e instanceof IOException || e instanceof ClientProtocolException) 187 { 188 logger.error("Unable to send request: " + wsURL, e); 189 } 190 else 191 { 192 throw e; 193 } 194 } 195 } 196 197 return responses; 198 } 199 200 private static boolean _checkResponse(byte[] bodyResponse) throws ParserConfigurationException, IllegalStateException, IOException, SAXException, TransformerException 201 { 202 if (bodyResponse == null) 203 { 204 return false; 205 } 206 207 try (InputStream is = new ByteArrayInputStream(bodyResponse)) 208 { 209 DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 210 Document document = docBuilder.parse(is); 211 return XPathAPI.eval(document, "count(/ActionResult)").toString().equals("1"); 212 } 213 } 214 215 private static byte[] _getResponse(HttpResponse response) throws IllegalStateException, IOException 216 { 217 if (response.getStatusLine().getStatusCode() != 200) 218 { 219 return null; 220 } 221 222 if (response.getFirstHeader("X-Ametys-SafeMode") != null) 223 { 224 // Site application is in safe mode 225 return null; 226 } 227 228 String cType = _getContentType (response); 229 if (cType == null || !cType.startsWith("text/xml")) 230 { 231 return null; 232 } 233 234 try (InputStream is = response.getEntity().getContent()) 235 { 236 return IOUtils.toByteArray(is); 237 } 238 } 239 240 private static String _getContentType (HttpResponse invalidateResponse) 241 { 242 Header header = invalidateResponse.getFirstHeader("Content-Type"); 243 if (header != null) 244 { 245 return header.getValue(); 246 } 247 return null; 248 } 249 250 /** 251 * Invalidates the front-office from the event observed. 252 * @param site the site. 253 * @param logger logger for traces. 254 * @throws Exception if an error occurs. 255 */ 256 public static void invalidateCache(Site site, Logger logger) throws Exception 257 { 258 String siteName = site.getName(); 259 long periodOfValidity = _getPeriodOfValidity (site, siteName); 260 261 if (isCacheValidityExpired(siteName, periodOfValidity)) 262 { 263 testWS("/_invalidate-site/" + site.getName(), logger); 264 testWS("/_invalidate-skin/" + site.getSkinId(), logger); 265 } 266 else 267 { 268 delayCacheInvalidation(siteName, "/_invalidate-site/" + site.getName(), periodOfValidity, logger); 269 delayCacheInvalidation(siteName, "/_invalidate-skin/" + site.getName(), periodOfValidity, logger); 270 } 271 } 272 273 /** 274 * Invalidates the front-office from the event observed. 275 * @param sitemap the sitemap. 276 * @param logger logger for traces. 277 * @throws Exception if an error occurs. 278 */ 279 public static void invalidateCache(Sitemap sitemap, Logger logger) throws Exception 280 { 281 testWS("/_invalidate-page/" + sitemap.getSiteName() + "/" + sitemap.getName(), logger); 282 } 283 284 /** 285 * Invalidates the front-office from the event observed. 286 * @param page the page. 287 * @param logger logger for traces. 288 * @param recursively true to invalidate the sub-pages 289 * @throws Exception if an error occurs. 290 */ 291 public static void invalidateCache(Page page, Logger logger, boolean recursively) throws Exception 292 { 293 testWS("/_invalidate-page/" + page.getSiteName() + "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + ".html", logger); 294 295 if (recursively) 296 { 297 testWS("/_invalidate-page/" + page.getSiteName() + "/" + page.getSitemapName() + "/" + page.getPathInSitemap(), logger); 298 } 299 } 300 301 /** 302 * Invalidates the front-office from the event observed. 303 * @param page the page. 304 * @param logger logger for traces. 305 * @throws Exception if an error occurs. 306 */ 307 public static void invalidateCache(Page page, Logger logger) throws Exception 308 { 309 invalidateCache(page, logger, false); 310 } 311 312 private static long _getPeriodOfValidity (Site site, String siteName) 313 { 314 if (!_cacheForPeriodOfValidity.containsKey(siteName)) 315 { 316 _cacheForPeriodOfValidity.put(siteName, site.getValue("cache-validity", false, 0L) * 1000); 317 } 318 return _cacheForPeriodOfValidity.get(siteName); 319 } 320 321 /** 322 * Update the last invalidation cache 323 * @param siteName the site name 324 */ 325 public static void initializeTimerPeriod (String siteName) 326 { 327 synchronized (_lockToken) 328 { 329 _lastInvalidationDate.put(siteName, System.currentTimeMillis()); 330 _timerTasks.remove(siteName); 331 } 332 } 333 334 /** 335 * Determines if cache validity is expired for given site 336 * @param siteName the site name 337 * @param periodOfValidity the period of cache validity 338 * @return true if cache validity is expired 339 */ 340 public static boolean isCacheValidityExpired (String siteName, long periodOfValidity) 341 { 342 synchronized (_lockToken) 343 { 344 long currentMillisecond = System.currentTimeMillis(); 345 346 if (!_lastInvalidationDate.containsKey(siteName)) 347 { 348 _lastInvalidationDate.put(siteName, currentMillisecond); 349 return true; 350 } 351 352 long lastInvalidationDate = _lastInvalidationDate.get(siteName); 353 354 if (currentMillisecond - lastInvalidationDate >= periodOfValidity) 355 { 356 _lastInvalidationDate.put(siteName, currentMillisecond); 357 return true; 358 } 359 360 // Cache is still valid 361 return false; 362 } 363 } 364 365 /** 366 * Delay the cache invalidation 367 * @param siteName the site name 368 * @param urlWS the WS url to be called 369 * @param periodOfValidity the period of cache validity 370 * @param logger the logger 371 */ 372 public static void delayCacheInvalidation (String siteName, String urlWS, long periodOfValidity, Logger logger) 373 { 374 synchronized (_lockToken) 375 { 376 if (!_timerTasks.containsKey(siteName)) 377 { 378 _timerTasks.put(siteName, urlWS); 379 380 long currentMillisecond = System.currentTimeMillis(); 381 382 long lastInvalidationDate = _lastInvalidationDate.get(siteName); 383 384 // delay the cache invalidation 385 long delay = lastInvalidationDate + periodOfValidity - currentMillisecond; 386 387 if (logger.isDebugEnabled()) 388 { 389 logger.debug("Cache invalidation for site '" + siteName + "' has been delayed of " + delay + " milliseconds"); 390 } 391 _timer.schedule(new InvalidateCacheTimerTask (siteName, logger), delay); 392 } 393 } 394 } 395 396 /** 397 * {@link TimerTask} to invalidate site cache 398 * 399 */ 400 public static class InvalidateCacheTimerTask extends TimerTask 401 { 402 private String _siteName; 403 private Logger _logger; 404 405 /** 406 * Constructor 407 * @param siteName the site name 408 * @param logger the logger 409 */ 410 public InvalidateCacheTimerTask (String siteName, Logger logger) 411 { 412 _siteName = siteName; 413 _logger = logger; 414 } 415 416 @Override 417 public void run() 418 { 419 try 420 { 421 synchronized (_lockToken) 422 { 423 if (_logger.isDebugEnabled()) 424 { 425 _logger.debug("Invalide cache of site '" + _siteName + "' after delay."); 426 } 427 428 CacheHelper.testWS(_timerTasks.get(_siteName), _logger); 429 CacheHelper.initializeTimerPeriod(_siteName); 430 } 431 } 432 catch (Exception e) 433 { 434 _logger.error("Unable to invalidate cache for site: " + _siteName, e); 435 } 436 } 437 } 438}