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