fiftyfive.wicket.js.locator.DefaultJavaScriptDependencyLocator.java Source code

Java tutorial

Introduction

Here is the source code for fiftyfive.wicket.js.locator.DefaultJavaScriptDependencyLocator.java

Source

/**
 * Copyright 2013 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;
        }
    }
}