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.lang3.StringUtils; 036import org.apache.http.Header; 037import org.apache.http.HttpResponse; 038import org.apache.http.NameValuePair; 039import org.apache.http.client.ClientProtocolException; 040import org.apache.http.client.config.RequestConfig; 041import org.apache.http.client.entity.UrlEncodedFormEntity; 042import org.apache.http.client.methods.CloseableHttpResponse; 043import org.apache.http.client.methods.HttpPost; 044import org.apache.http.impl.client.CloseableHttpClient; 045import org.apache.http.impl.client.HttpClientBuilder; 046import org.apache.tika.io.IOUtils; 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 * Request the given URL on each configured front-office. 130 * @param url the url to be called. 131 * @param postParameters submitted values 132 * @param logger logger for traces. 133 * @return The streams 134 * @throws Exception if an error occurred. 135 */ 136 public static List<Map<String, Object>> callWS(String url, List<NameValuePair> postParameters, Logger logger) throws Exception 137 { 138 String frontConfig = Config.getInstance().getValueAsString("org.ametys.web.front.url"); 139 String[] frontURLs = StringUtils.split(frontConfig, ","); 140 141 List<Map<String, Object>> responses = new ArrayList<>(); 142 for (String rawFrontURL : frontURLs) 143 { 144 String frontURL = rawFrontURL.trim(); 145 146 RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(2000).setSocketTimeout(2000).build(); 147 148 String wsURL = frontURL + url; 149 150 try (CloseableHttpClient httpclient = HttpClientBuilder.create().useSystemProperties().setDefaultRequestConfig(requestConfig).build()) 151 { 152 153 // Prepare a request object 154 HttpPost request = new HttpPost(wsURL); 155 request.addHeader("X-Ametys-BO", "true"); 156 157 if (postParameters != null) 158 { 159 request.setEntity(new UrlEncodedFormEntity(postParameters, StandardCharsets.UTF_8)); 160 } 161 162 Map<String, Object> responseObject = new HashMap<>(); 163 responses.add(responseObject); 164 responseObject.put("request", request); 165 166 // Execute the request 167 try (CloseableHttpResponse response = httpclient.execute(request)) 168 { 169 responseObject.put("bodyResponse", _getResponse(response)); 170 responseObject.put("response", response); 171 } 172 } 173 catch (Exception e) 174 { 175 if (e instanceof IOException || e instanceof ClientProtocolException) 176 { 177 logger.error("Unable to send request: " + wsURL, e); 178 } 179 else 180 { 181 throw e; 182 } 183 } 184 } 185 186 return responses; 187 } 188 189 private static boolean _checkResponse(byte[] bodyResponse) throws ParserConfigurationException, IllegalStateException, IOException, SAXException, TransformerException 190 { 191 if (bodyResponse == null) 192 { 193 return false; 194 } 195 196 try (InputStream is = new ByteArrayInputStream(bodyResponse)) 197 { 198 DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 199 Document document = docBuilder.parse(is); 200 return XPathAPI.eval(document, "count(/ActionResult)").toString().equals("1"); 201 } 202 } 203 204 private static byte[] _getResponse(HttpResponse response) throws IllegalStateException, IOException 205 { 206 if (response.getStatusLine().getStatusCode() != 200) 207 { 208 return null; 209 } 210 211 if (response.getFirstHeader("X-Ametys-SafeMode") != null) 212 { 213 // Site application is in safe mode 214 return null; 215 } 216 217 String cType = _getContentType (response); 218 if (cType == null || !cType.startsWith("text/xml")) 219 { 220 return null; 221 } 222 223 try (InputStream is = response.getEntity().getContent()) 224 { 225 return IOUtils.toByteArray(is); 226 } 227 } 228 229 private static String _getContentType (HttpResponse invalidateResponse) 230 { 231 Header header = invalidateResponse.getFirstHeader("Content-Type"); 232 if (header != null) 233 { 234 return header.getValue(); 235 } 236 return null; 237 } 238 239 /** 240 * Invalidates the front-office from the event observed. 241 * @param site the site. 242 * @param logger logger for traces. 243 * @throws Exception if an error occurs. 244 */ 245 public static void invalidateCache(Site site, Logger logger) throws Exception 246 { 247 String siteName = site.getName(); 248 long periodOfValidity = _getPeriodOfValidity (site, siteName); 249 250 if (isCacheValidityExpired(siteName, periodOfValidity)) 251 { 252 testWS("/_invalidate-site/" + site.getName(), logger); 253 testWS("/_invalidate-skin/" + site.getSkinId(), logger); 254 } 255 else 256 { 257 delayCacheInvalidation(siteName, "/_invalidate-site/" + site.getName(), periodOfValidity, logger); 258 delayCacheInvalidation(siteName, "/_invalidate-skin/" + site.getName(), periodOfValidity, logger); 259 } 260 } 261 262 /** 263 * Invalidates the front-office from the event observed. 264 * @param sitemap the sitemap. 265 * @param logger logger for traces. 266 * @throws Exception if an error occurs. 267 */ 268 public static void invalidateCache(Sitemap sitemap, Logger logger) throws Exception 269 { 270 testWS("/_invalidate-page/" + sitemap.getSiteName() + "/" + sitemap.getName(), logger); 271 } 272 273 /** 274 * Invalidates the front-office from the event observed. 275 * @param page the page. 276 * @param logger logger for traces. 277 * @param recursively true to invalidate the sub-pages 278 * @throws Exception if an error occurs. 279 */ 280 public static void invalidateCache(Page page, Logger logger, boolean recursively) throws Exception 281 { 282 testWS("/_invalidate-page/" + page.getSiteName() + "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + ".html", logger); 283 284 if (recursively) 285 { 286 testWS("/_invalidate-page/" + page.getSiteName() + "/" + page.getSitemapName() + "/" + page.getPathInSitemap(), logger); 287 } 288 } 289 290 /** 291 * Invalidates the front-office from the event observed. 292 * @param page the page. 293 * @param logger logger for traces. 294 * @throws Exception if an error occurs. 295 */ 296 public static void invalidateCache(Page page, Logger logger) throws Exception 297 { 298 invalidateCache(page, logger, false); 299 } 300 301 private static long _getPeriodOfValidity (Site site, String siteName) 302 { 303 if (!_cacheForPeriodOfValidity.containsKey(siteName)) 304 { 305 _cacheForPeriodOfValidity.put(siteName, site.getMetadataHolder().getLong("cache-validity", 0) * 1000); 306 } 307 return _cacheForPeriodOfValidity.get(siteName); 308 } 309 310 /** 311 * Update the last invalidation cache 312 * @param siteName the site name 313 */ 314 public static void initializeTimerPeriod (String siteName) 315 { 316 synchronized (_lockToken) 317 { 318 _lastInvalidationDate.put(siteName, System.currentTimeMillis()); 319 _timerTasks.remove(siteName); 320 } 321 } 322 323 /** 324 * Determines if cache validity is expired for given site 325 * @param siteName the site name 326 * @param periodOfValidity the period of cache validity 327 * @return true if cache validity is expired 328 */ 329 public static boolean isCacheValidityExpired (String siteName, long periodOfValidity) 330 { 331 synchronized (_lockToken) 332 { 333 long currentMillisecond = System.currentTimeMillis(); 334 335 if (!_lastInvalidationDate.containsKey(siteName)) 336 { 337 _lastInvalidationDate.put(siteName, currentMillisecond); 338 return true; 339 } 340 341 long lastInvalidationDate = _lastInvalidationDate.get(siteName); 342 343 if (currentMillisecond - lastInvalidationDate >= periodOfValidity) 344 { 345 _lastInvalidationDate.put(siteName, currentMillisecond); 346 return true; 347 } 348 349 // Cache is still valid 350 return false; 351 } 352 } 353 354 /** 355 * Delay the cache invalidation 356 * @param siteName the site name 357 * @param urlWS the WS url to be called 358 * @param periodOfValidity the period of cache validity 359 * @param logger the logger 360 */ 361 public static void delayCacheInvalidation (String siteName, String urlWS, long periodOfValidity, Logger logger) 362 { 363 synchronized (_lockToken) 364 { 365 if (!_timerTasks.containsKey(siteName)) 366 { 367 _timerTasks.put(siteName, urlWS); 368 369 long currentMillisecond = System.currentTimeMillis(); 370 371 long lastInvalidationDate = _lastInvalidationDate.get(siteName); 372 373 // delay the cache invalidation 374 long delay = lastInvalidationDate + periodOfValidity - currentMillisecond; 375 376 if (logger.isDebugEnabled()) 377 { 378 logger.debug("Cache invalidation for site '" + siteName + "' has been delayed of " + delay + " milliseconds"); 379 } 380 _timer.schedule(new InvalidateCacheTimerTask (siteName, logger), delay); 381 } 382 } 383 } 384 385 /** 386 * {@link TimerTask} to invalidate site cache 387 * 388 */ 389 public static class InvalidateCacheTimerTask extends TimerTask 390 { 391 private String _siteName; 392 private Logger _logger; 393 394 /** 395 * Constructor 396 * @param siteName the site name 397 * @param logger the logger 398 */ 399 public InvalidateCacheTimerTask (String siteName, Logger logger) 400 { 401 _siteName = siteName; 402 _logger = logger; 403 } 404 405 @Override 406 public void run() 407 { 408 try 409 { 410 synchronized (_lockToken) 411 { 412 if (_logger.isDebugEnabled()) 413 { 414 _logger.debug("Invalide cache of site '" + _siteName + "' after delay."); 415 } 416 417 CacheHelper.testWS(_timerTasks.get(_siteName), _logger); 418 CacheHelper.initializeTimerPeriod(_siteName); 419 } 420 } 421 catch (Exception e) 422 { 423 _logger.error("Unable to invalidate cache for site: " + _siteName, e); 424 } 425 } 426 } 427}