content: /**
   * Copyright 2014 55 Minutes (http://www.55minutes.com)
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *    http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package fiftyfive.wicket.js.locator;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.regex.Pattern;
  
  import fiftyfive.wicket.js.JavaScriptDependencySettings;
  
  import org.apache.wicket.Application;
  import org.apache.wicket.WicketRuntimeException;
  
  import org.apache.wicket.request.resource.PackageResourceReference;
  import org.apache.wicket.request.resource.ResourceReference;
  
  import org.apache.wicket.util.lang.Args;
  import org.apache.wicket.util.lang.Classes;
  import org.apache.wicket.util.lang.Packages;
  import org.apache.wicket.util.resource.IResourceStream;
  import org.apache.wicket.util.resource.locator.IResourceStreamLocator;
  import org.apache.wicket.util.time.Duration;
  
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * Default implementation of JavaScriptDependencyLocator. Uses the Wicket
   * application's {@link IResourceStreamLocator} to load JavaScript files,
   * and our {@link SprocketsDependencyCollector} to parse them for dependencies.
   * A {@link ConcurrentHashMap} is used as a simple in-memory cache for the
   * dependency trees that are discovered.
   * 
   * @since 2.0
   */
  public class DefaultJavaScriptDependencyLocator
      implements JavaScriptDependencyLocator
  {
      static final Pattern JQUERYUI_PATT = Pattern.compile("jquery(\\.|-|_)?ui");
      
      private static final Logger LOGGER = LoggerFactory.getLogger(
          DefaultJavaScriptDependencyLocator.class
      );
      
      
      private Map<ResourceReference,CacheEntry> cache;
      
      public DefaultJavaScriptDependencyLocator()
      {
          super();
          this.cache = new ConcurrentHashMap<ResourceReference,CacheEntry>();
      }
      
      public void findLibraryScripts(String libraryName,
                                     DependencyCollection scripts)
      {
          List<ResourceReference> refs = new ArrayList<ResourceReference>();
          if("jquery".equalsIgnoreCase(libraryName))
          {
              scripts.add(settings().getJQueryResource());
          }
          else if(JQUERYUI_PATT.matcher(libraryName.toLowerCase()).matches())
          {
              scripts.add(settings().getJQueryResource());
              scripts.add(settings().getJQueryUIResource());
              scripts.setCss(getJQueryUITheme());
          }
          else
          {
              collectResourceAndDependencies(
                  searchForRequiredLibrary(libraryName),
                  scripts
              );
          }
      }
      
      public void findResourceScripts(Class<?> cls, String fileName,
                                      DependencyCollection scripts)
      {
          collectResourceAndDependencies(
              newResourceReference(cls, fileName), scripts
          );
      }
      
      public void findAssociatedScripts(final Class<?> cls,
                                        final DependencyCollection scripts)
      {
          Args.notNull(cls, "cls");
          Args.notNull(scripts, "scripts");
          
          // Traverse up the class hierarchy until we find
          // a valid JavaScript resource or we run out of super classes.
          Class<?> scope = cls;
          while(scope != null)
          {
              ResourceReference reference = newResourceReference(
                  scope,
                  Classes.simpleName(scope)
              );
              LOGGER.debug("Searching for: {}", reference);
              if(load(reference) != null)
              {
                  LOGGER.debug("Found: {}", reference);
                  collectResourceAndDependencies(reference, scripts);
                  break;
              }
              scope = scope.getSuperclass();
          }
      }
      
      /**
       * Returns a reference to the CSS file that should be used to style
       * jQuery UI widgets. The default implementation simply delegates to
       * {@link JavaScriptDependencySettings#getJQueryUICSSResource JavaScriptDependencySettings.getJQueryUICSSResource()}.
       * This means that one style is used for the entire application.
       * If you want something more advanced, for example to choose a theme
       * based on user preferences or a session value, override this method for
       * your custom logic.
       */
      protected ResourceReference getJQueryUITheme()
      {
          return settings().getJQueryUICSSResource();
      }
      
      /**
       * Adds the resource to the DependencyCollection and recursively traverses
       * all of the sprocket dependencies of that resource (and its dependencies
       * and so forth), until the entire dependency tree has been added to
       * the collection. The cache will first be consulted to avoid the
       * recursion, if possible. Otherwise the result of the recursion is cached
       * for future use.
       */
      private void collectResourceAndDependencies(ResourceReference ref,
                                                  DependencyCollection scripts)
      {
          if(scripts.isEmpty() && populateFromCache(ref, scripts)) return;
          if(!scripts.add(ref)) return;
          
          SprocketsParser parser = settings().getSprocketsParser();
          if(parser != null)
          {
              SprocketsDependencyCollector coll = new SprocketsDependencyCollector(this, parser);
              scripts.descend();
              IResourceStream stream = load(ref);
              if(null == stream)
              {
                  throw new WicketRuntimeException(
                      "JavaScript file does not exist: " + ref
                  );
              }
              coll.collectDependencies(ref, stream, scripts);
              scripts.ascend();
          }
          
          putIntoCache(ref, scripts);
      }
  
      /**
       * Modifies the given DependencyCollection so that it is identical to the
       * cached copy based on a previous call to putIntoCache() and returns
       * {@code true}.
       * If the cache is disabled or there is no existing cache for the given
       * resource, returns {@code false}.
       */
      private boolean populateFromCache(ResourceReference ref,
                                        DependencyCollection scripts)
      {
          if(null == ref) return false;
          
          CacheEntry ce = this.cache.get(ref);
          if(ce != null && ce.isActive())
          {
              ce.populate(scripts);
              return true;
          }
          return false;
      }
      
      /**
       * Stores the dependencies of the given resource in the cache. If the
       * cache is disabled (i.e. duration of zero), this has no effect.
       */
      private void putIntoCache(ResourceReference ref,
                                DependencyCollection scripts)
      {
          if(null == ref) return;
          
          Duration duration = settings().getTraversalCacheDuration();
          if(duration.getMilliseconds() > 0)
          {
              this.cache.put(ref, new CacheEntry(scripts, duration));
          }
      }
      
      /**
       * Loops through all the library search paths as configured in
       * JavaScriptDependencySettings and looks for the JavaScript library
       * with the specified name, returning a ResourceReference for the first
       * match. If none could be found, throws a WicketRuntimeException.
       */
      private ResourceReference searchForRequiredLibrary(final String name)
      {
          ResourceReference ref = null;
          
          for(SearchLocation loc : settings().getLibraryPaths())
          {
              String path = loc.getPath();
              String absolutePath = String.format(
                  "%s%s", path.isEmpty() ? "" : path + "/", name
              );
              ResourceReference testRef = newResourceReference(
                  loc.getScope(), absolutePath
              );
              if(load(testRef) != null)
              {
                  ref = testRef;
                  break;
              }
          }
          
          if(null == ref)
          {
              throw new WicketRuntimeException(
                  "Could not find JavaScript library named: " + name
              );
          }
          
          return ref;
      }
      
      /**
       * Loads the given ResourceReference as an IResourceStream or returns
       * {@code null} if the resource could not be found.
       */
      private IResourceStream load(ResourceReference ref)
      {
          IResourceStreamLocator locator =
              Application.get().getResourceSettings().getResourceStreamLocator();
          
          Class<?> scope = ref.getScope();
          String path = Packages.absolutePath(scope, ref.getName());
          
          return locator.locate(scope, path);
      }
      
      /**
       * Constructs a new ResourceReference, automatically adding the ".js"
       * extension if needed.
       */
      private ResourceReference newResourceReference(Class<?> scope, String name)
      {
          // TODO: test whether file exists first, just in case it has no
          // extension or a non-js extension.
          
          if(!name.toLowerCase().endsWith(".js"))
          {
              name = name + ".js";
          }
          return new PackageResourceReference(scope, name);
      }
      
      /**
       * Returns the JavaScriptDependencySettings associated with the current
       * Application.
       */
      private JavaScriptDependencySettings settings()
      {
          return JavaScriptDependencySettings.get();
      }
      
      /**
       * A cache entry holds an immutable DependencyCollection and expires
       * after a certain duration.
       */
      private static class CacheEntry
      {
          private long start;
          private long timeToLive;
          private DependencyCollection scripts;
          
          private CacheEntry(DependencyCollection orig, Duration duration)
          {
              super();
              // Make a private copy so that the cached copy is never mutated
              this.scripts = new DependencyCollection();
              orig.copyTo(this.scripts);
              this.scripts.freeze();
              this.start = System.currentTimeMillis();
              this.timeToLive = duration.getMilliseconds();
          }
          
          private void populate(DependencyCollection other)
          {
              this.scripts.copyTo(other);
          }
          
          private boolean isActive()
          {
              return System.currentTimeMillis() - this.start < this.timeToLive;
          }
      }
  }
  