package utils;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import static java.nio.file.StandardWatchEventKinds.*;
import java.nio.file.WatchEvent;

import javax.servlet.ServletContext;

import org.webdsl.logging.Logger;

import com.google.common.hash.Hashing;
import com.google.common.io.Files;

public class CachedResourceFileNameHelper {
  private static Map<String, String> resources = new HashMap<>();
  private static String resourcePathRoot = null;
  private static boolean warningShown = false;

  public static void init(ServletContext ctx) {
    resourcePathRoot = ctx.getRealPath(File.separator);
    setCommonAndFavIcoTagSuffixes();
    invalidateOnChanges(resourcePathRoot);
  }

  // Given a name of a resource (with or without existing query param), it
  // returns the name with the file hash (md5) as query param
  // This way, the browser will never use its client-cached resource when the
  // resource changed. This method caches the hashed filenames
  // until a change is detected in the file resources (using a directory
  // watcher)
  public static String getNameWithHash(String resourceDirName, String name) {
    String hashname = resources.get(name);
    if (hashname == null) {
      if (resourcePathRoot == null) {
        if (!warningShown) {
          Logger.warn(CachedResourceFileNameHelper.class.getName()
              + " is not initialized by the servlet context or the servlet is not deployed in an exploded way (e.g. when serving directly from war-file). It won't add file-hashes to resource file names.");
          warningShown = true;
        }
        hashname = name;
      } else {
        String nameNoSuffix = name.replaceAll("(\\?.*)$", "");
        String realPath = resourcePathRoot + File.separator + resourceDirName + File.separator + nameNoSuffix;
        try {
          String hashStr = Files.asByteSource(new File(realPath)).hash(Hashing.md5()).toString();
          hashname = nameNoSuffix + "?" + hashStr;
        } catch (IOException e) {
          Logger.error(e);
          hashname = name;
        }
        resources.put(name, hashname);
      }
    }
    return hashname;
  }

  public static void invalidateCache() {
    resources.clear();
    setCommonAndFavIcoTagSuffixes();
    if (AbstractPageServlet.pageCacheEnabled) {
      AbstractPageServlet.doInvalidateAllPageCache("change in css or js resource");
    }
  }

  public static String common_css_link_tag_suffix;
  public static String fav_ico_link_tag_suffix;

  private static void setCommonAndFavIcoTagSuffixes() {
    common_css_link_tag_suffix = "/stylesheets/"
        + CachedResourceFileNameHelper.getNameWithHash("stylesheets", "common_.css")
        + "\" rel=\"stylesheet\" type=\"text/css\" />";
    fav_ico_link_tag_suffix = "/" + CachedResourceFileNameHelper.getNameWithHash("", "favicon.ico")
        + "\" rel=\"shortcut icon\" type=\"image/x-icon\" />";
  }

  /* Watch for changes */

  private static WatchService watcher;
  private static Thread dirWatcherThread;

  public static void invalidateOnChanges(String resourcePathRoot) {
    // Path rootPath = Paths.get(resourcePathRoot);
    String jsDir = resourcePathRoot + File.separator + "javascript";
    String cssDir = resourcePathRoot + File.separator + "stylesheets";
    Path jsPath = Paths.get(jsDir);
    Path cssPath = Paths.get(cssDir);
    Path[] paths = { jsPath, cssPath };

    try {
      watcher = FileSystems.getDefault().newWatchService();

      for (Path path : paths) {
        java.nio.file.Files.walkFileTree(path, new java.nio.file.SimpleFileVisitor<Path>() {
          @Override
          public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            dir.register(watcher, ENTRY_CREATE, ENTRY_MODIFY);
            return FileVisitResult.CONTINUE;
          }
        });
      }
    } catch (IOException e) {
      Logger.error(
          "Error during initialization of directory watcher for cache busting css/js resources on change. Changes won't be detected",
          e);
      return;
    }

    // Logger.info("Started watcher service for cache busting resource files
    // changes in deployed war file, watching dirs (recursively): ["
    // + jsPath + "] and [" + cssPath + "]");

    dirWatcherThread = new Thread() {
      public void run() {

        while (!this.isInterrupted()) {
          WatchKey key;
          try {
            // wait for a key to be available
            key = watcher.take();
          } catch (InterruptedException ex) {
            return;
          }

          for (WatchEvent<?> event : key.pollEvents()) {
            WatchEvent.Kind<?> kind = event.kind();
            if (kind == OVERFLOW) {
              continue;
            } else {
              @SuppressWarnings("unchecked")
              WatchEvent<Path> ev = (WatchEvent<Path>) event;
              Path changedFile = ev.context();
              Logger.info("Change to resource file '" + changedFile
                  + "' detected, going to recalculate file hashes for js/css includes.");

              invalidateCache();
            }
          }
          boolean valid = key.reset();
          if (!valid) {
            break;
          }
        }

      }
    };
    dirWatcherThread.start();
  }
  
  public static void cleanupOnServletDestroy() {
    dirWatcherThread.interrupt();
  }

}

@@ -58,16 +58,66 @@
 eption;%0A
+import java.nio.file.ClosedWatchServiceException;%0A
 import j
@@ -3426,55 +3426,183 @@
     
-// Path rootPath = Paths.get(resourcePathRoot);
+%0A    if(resourcePathRoot == null %7C%7C ! (new File(resourcePathRoot).exists()) ) %7B%0A      //don't start watcher when path root is empty or does not exists%0A      return;%0A    %7D%0A    
 %0A   
@@ -4541,201 +4541,8 @@
  %7D%0A%0A
-    // Logger.info(%22Started watcher service for cache busting resource files%0A    // changes in deployed war file, watching dirs (recursively): %5B%22%0A    // + jsPath + %22%5D and %5B%22 + cssPath + %22%5D%22);%0A%0A
     
@@ -4802,16 +4802,46 @@
 ception 
+%7C ClosedWatchServiceException 
 ex) %7B%0A  
@@ -5579,72 +5579,330 @@
 ad.s
-tart();%0A  %7D%0A  %0A  public static void cleanupOnServletDestroy() %7B%0A
+etDaemon(true);%0A    dirWatcherThread.setName(%22Watcher thread for changes in js/css resources%22);%0A    dirWatcherThread.start();%0A  %7D%0A  %0A  public static void cleanupOnServletDestroy() %7B%0A    if(watcher == null) %7B%0A      return;%0A    %7D%0A    try %7B%0A      watcher.close();%0A    %7D catch (IOException e) %7B%0A      Logger.error(e);%0A    %7D%0A//
     

Improve cleanup + don't start watcher when resource path not set