velocity-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From nbu...@apache.org
Subject svn commit: r933306 - /velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/UiDependencyTool.java
Date Mon, 12 Apr 2010 16:28:45 GMT
Author: nbubna
Date: Mon Apr 12 16:28:45 2010
New Revision: 933306

URL: http://svn.apache.org/viewvc?rev=933306&view=rev
Log:
initial revision

Added:
    velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/UiDependencyTool.java
  (with props)

Added: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/UiDependencyTool.java
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/UiDependencyTool.java?rev=933306&view=auto
==============================================================================
--- velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/UiDependencyTool.java
(added)
+++ velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/UiDependencyTool.java
Mon Apr 12 16:28:45 2010
@@ -0,0 +1,730 @@
+package org.apache.velocity.tools.view;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.    
+ */
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.apache.commons.digester.Digester;
+import org.apache.commons.digester.Rule;
+import org.apache.velocity.tools.ClassUtils;
+import org.apache.velocity.tools.view.ViewContext;
+import org.apache.velocity.runtime.log.Log;
+import org.apache.velocity.tools.Scope;
+import org.apache.velocity.tools.ToolContext;
+import org.apache.velocity.tools.config.DefaultKey;
+import org.apache.velocity.tools.config.ValidScope;
+
+/**
+ * <b>NOTE: This tool is considered "beta" quality due to lack of public testing
+ * and is not automatically provided via the default tools.xml file.
+ * </b>
+ *
+ * Tool to make it easier to manage usage of client-side dependencies.
+ * This is essentially a simple dependency system for javascript and css.
+ * This could be cleaned up to use fewer maps, use more classes,
+ * and cache formatted values, but this is good enough for now.
+ *
+ * To use it, create a ui.xml file at the root of the classpath.
+ * Follow the example below.  By default, it prepends the request context path
+ * and then "css/" to every stylesheet file and the request context path
+ * and "js/" to every javascript file path.  You can
+ * alter those defaults by changing the type definition. In the example
+ * below, the file path for the style type is changed to "/styles/", leaving out
+ * the {context}.
+ *
+ * This is safe in request scope, but the group info (from ui.xml)
+ * should only be read once.  It is not re-parsed on every request.
+ * <p>
+ * Example of use:
+ * <pre>
+ *  Template
+ *  ---
+ *  &lt;html&gt;
+ *    &lt;head&gt;
+ *      $depends.on('profile').print('
+ *      ')
+ *    &lt;/head&gt;
+ *  ...
+ *
+ *  Output
+ *  ------
+ *  &lt;html&gt;
+ *    &lt;head&gt;
+ *      &lt;style rel="stylesheet" type="text/css" href="css/globals.css"/&gt;
+ *      &lt;script type="text/javascript" src="js/jquery.js"&gt;&lt;/script&gt;
+ *      &lt;script type="text/javascript" src="js/profile.js"&gt;&lt;/script&gt;
+ *    &lt;/head&gt;
+ *  ...
+ *
+ * Example tools.xml:
+ * &lt;tools&gt;
+ *   &lt;toolbox scope="request"&gt;
+ *     &lt;tool class="org.apache.velocity.tools.view.beta.UiDependencyTool"/&gt;
+ *   &lt;/toolbox&gt;
+ * &lt;/tools&gt;
+ *
+ * Example ui.xml:
+ * &lt;ui&gt;
+ *   &lt;type name="style"&gt;&lt;![CDATA[&lt;link rel="stylesheet" type="text/css"
href="/styles/{file}"&gt;]]&gt;&lt;/type&gt;
+ *   &lt;group name="globals"&gt;
+ *     &lt;file type="style"&gt;css/globals.css&lt;file/&gt;
+ *   &lt;/group&gt;
+ *   &lt;group name="jquery"&gt;
+ *     &lt;file type="script"&gt;js/jquery.js&lt;file/&gt;
+ *   &lt;/group&gt;
+ *   &lt;group name="profile"&gt;
+ *     &lt;needs&gt;globals&lt;/needs&gt;
+ *     &lt;needs&gt;jquery&lt;/needs&gt;
+ *     &lt;file type="script"&gt;js/profile.js&lt;file/&gt;
+ *   &lt;/group&gt;
+ * &lt;/ui&gt;
+ * </pre>
+ * </p>
+ *
+ * @author Nathan Bubna
+ * @version $Revision: 16660 $
+ */
+@DefaultKey("depends")
+@ValidScope(Scope.REQUEST)
+public class UiDependencyTool {
+
+    public static final String GROUPS_KEY_SPACE = UiDependencyTool.class.getName() + ":";
+    public static final String TYPES_KEY_SPACE = UiDependencyTool.class.getName() + ":types:";
+    public static final String SOURCE_FILE_KEY = "file";
+    public static final String DEFAULT_SOURCE_FILE = "ui.xml";
+    private static final List<Type> DEFAULT_TYPES;
+    static {
+        List<Type> types = new ArrayList<Type>();
+        // start out with these two types
+        types.add(new Type("style", "<link rel=\"stylesheet\" type=\"text/css\" href=\"{context}/css/{file}\"/>"));
+        types.add(new Type("script", "<script type=\"text/javascript\" src=\"{context}/js/{file}\"></script>"));
+        DEFAULT_TYPES = Collections.unmodifiableList(types);
+    }
+
+    private Map<String,Group> groups = null;
+    private List<Type> types = DEFAULT_TYPES;
+    private Map<String,List<String>> dependencies;
+    private Log LOG;
+    private String context = "";
+
+    private void debug(String msg, Object... args) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug(String.format("UiDependencyTool: "+msg, args));
+        }
+    }
+
+    protected static final void trace(Log log, String msg, Object... args) {
+        if (log.isTraceEnabled()) {
+            log.trace(String.format("UiDependencyTool: "+msg, args));
+        }
+    }
+
+    public void configure(Map params) {
+        ServletContext app = (ServletContext)params.get(ViewContext.SERVLET_CONTEXT_KEY);
+        LOG = (Log)params.get(ToolContext.LOG_KEY);
+
+        HttpServletRequest request = (HttpServletRequest)params.get(ViewContext.REQUEST);
+        context = request.getContextPath();
+
+        String file = (String)params.get(SOURCE_FILE_KEY);
+        if (file == null) {
+            file = DEFAULT_SOURCE_FILE;
+        } else {
+            debug("Loading file: %s", file);
+        }
+
+        synchronized (app) {
+            // first, see if we've already read this file
+            groups = (Map<String,Group>)app.getAttribute(GROUPS_KEY_SPACE+file);
+            if (groups == null) {
+                groups = new LinkedHashMap<String,Group>();
+                // only require file presence, if one is specified
+                read(file, (file != DEFAULT_SOURCE_FILE));
+                app.setAttribute(GROUPS_KEY_SPACE+file, groups);
+                if (types != DEFAULT_TYPES) {
+                    app.setAttribute(TYPES_KEY_SPACE+file, types);
+                }
+            } else {
+                // load any custom types too
+                List<Type> alt = (List<Type>)app.getAttribute(TYPES_KEY_SPACE+file);
+                if (alt != null) {
+                    types = alt;
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds all the files required for the specified group, then returns
+     * this instance.  If the group name is null or no such group exists,
+     * this will return null to indicate the error.
+     */
+    public UiDependencyTool on(String name) {
+        Map<String,List<String>> groupDeps = getGroupDependencies(name);
+        if (groupDeps == null) {
+            return null;
+        } else {
+            addDependencies(groupDeps);
+            return this;
+        }
+    }
+
+    /**
+     * Adds the specified file to this instance's list of dependencies
+     * of the specified type, then returns this instance.  If either the
+     * type or file are null, this will return null to indicate the error.
+     */
+    public UiDependencyTool on(String type, String file) {
+        if (type == null || file == null) {
+            return null;
+        } else {
+            addFile(type, file);
+            return this;
+        }
+    }
+
+    /**
+     * Formats and prints all the current dependencies of this tool,
+     * using a new line in between the printed/formatted files.
+     */
+    public String print() {
+        return printAll("\n");
+    }
+
+    /**
+     * If the parameter value is a known type, then this will
+     * format and print all of this instance's current dependencies of the
+     * specified type, using a new line in between the printed/formatted files.
+     * If the parameter value is NOT a known type, then this will treat it
+     * as a delimiter and print all of this instance's dependencies of all
+     * types, using the specified value as the delimiter in between the
+     * printed/formatted files.
+     * @see #print(String,String)
+     * @see #printAll(String)
+     */
+    public String print(String typeOrDelim) {
+        if (getType(typeOrDelim) == null) {
+            // then it's a delimiter
+            return printAll(typeOrDelim);
+        } else {
+            // then it's obviously a type
+            return print(typeOrDelim, "\n");
+        }
+    }
+
+    /**
+     * Formats and prints all of this instance's current dependencies of the
+     * specified type, using the specified delimiter in between the
+     * printed/formatted files.
+     */
+    public String print(String type, String delim) {
+        List<String> files = getDependencies(type);
+        if (files == null) {
+            return null;
+        }
+
+        String format = getFormat(type);
+        StringBuilder out = new StringBuilder();
+        for (String file : files) {
+            out.append(format(format, file));
+            out.append(delim);
+        }
+        return out.toString();
+    }
+
+    /**
+     * Formats and prints all the current dependencies of this tool,
+     * using the specified delimiter in between the printed/formatted files.
+     */
+    public String printAll(String delim) {
+        if (dependencies == null) {
+            return null;
+        }
+
+        StringBuilder out = new StringBuilder();
+        for (Type type : types) {
+            if (out.length() > 0) {
+                out.append(delim);
+            }
+            List<String> files = dependencies.get(type.name);
+            if (files != null) {
+                for (int i=0; i < files.size(); i++) {
+                    if (i > 0) {
+                        out.append(delim);
+                    }
+                    out.append(format(type.format, files.get(i)));
+                }
+            }
+        }
+        return out.toString();
+    }
+
+    /**
+     * Sets a custom {context} variable for the formats to use.
+     */
+    public UiDependencyTool context(String path)
+    {
+        this.context = path;
+        return this;
+    }
+
+    /**
+     * Retrieves the configured format string for the specified file type.
+     */
+    public String getFormat(String type) {
+        Type t = getType(type);
+        if (t == null) {
+            return null;
+        }
+        return t.format;
+    }
+
+    /**
+     * Sets the format string for the specified file type.
+     */
+    public void setFormat(String type, String format) {
+        if (format == null || type == null) {
+            throw new NullPointerException("Type name and format must not be null");
+        }
+        // do NOT alter the defaults, just copy them
+        if (types == DEFAULT_TYPES) {
+            types = new ArrayList<Type>();
+            for (Type t : DEFAULT_TYPES) {
+                types.add(new Type(t.name, t.format));
+            }
+        }
+        Type t = getType(type);
+        if (t == null) {
+            types.add(new Type(type, format));
+        } else {
+            t.format = format;
+        }
+    }
+
+    /**
+     * Returns the current dependencies of this instance, organized
+     * as an ordered map of file types to lists of the required files
+     * of that type.
+     */
+    public Map<String,List<String>> getDependencies() {
+        return dependencies;
+    }
+
+    /**
+     * Returns the {@link List} of files for the specified file type, if any.
+     */
+    public List<String> getDependencies(String type) {
+        if (dependencies == null) {
+            return null;
+        }
+        return dependencies.get(type);
+    }
+
+    /**
+     * Returns the dependencies of the specified group, organized
+     * as an ordered map of file types to lists of the required files
+     * of that type.
+     */
+    public Map<String,List<String>> getGroupDependencies(String name) {
+        Group group = getGroup(name);
+        if (group == null) {
+            return null;
+        }
+        return group.getDependencies(this);
+    }
+
+    /**
+     * Returns an empty String to avoid polluting the template output after a
+     * successful call to {@link #on(String)} or {@link #on(String,String)}.
+     */
+    @Override
+    public String toString() {
+        return "";
+    }
+
+
+    /**
+     * Reads group info out of the specified file and into this instance.
+     * If the file cannot be found and required is true, then this will throw
+     * an IllegalArgumentException.  Otherwise, it will simply do nothing. Any
+     * checked exceptions during the actual reading of the file are caught and
+     * wrapped as {@link RuntimeException}s.
+     */
+    protected void read(String file, boolean required) {
+        debug("UiDependencyTool: Reading file from %s", file);
+        URL url = toURL(file);
+        if (url == null) {
+            String msg = "UiDependencyTool: Could not read file from '"+file+"'";
+            if (required) {
+                LOG.error(msg);
+                throw new IllegalArgumentException(msg);
+            } else {
+                LOG.debug(msg);
+            }
+        } else {
+            Digester digester = createDigester();
+            try
+            {
+                digester.parse(url.openStream());
+            }
+            catch (SAXException saxe)
+            {
+                LOG.error("UiDependencyTool: Failed to parse '"+file+"'", saxe);
+                throw new RuntimeException("While parsing the InputStream", saxe);
+            }
+            catch (IOException ioe)
+            {
+                LOG.error("UiDependencyTool: Failed to read '"+file+"'", ioe);
+                throw new RuntimeException("While handling the InputStream", ioe);
+            }
+        }
+    }
+
+    /**
+     * Creates the {@link Digester} used by {@link #read} to create
+     * the group info for this instance out of the specified XML file.
+     */
+    protected Digester createDigester() {
+        Digester digester = new Digester();
+        digester.setValidating(false);
+        digester.setUseContextClassLoader(true);
+        digester.addRule("ui/type", new TypeRule());
+        digester.addRule("ui/group", new GroupRule());
+        digester.addRule("ui/group/file", new FileRule());
+        digester.addRule("ui/group/needs", new NeedsRule());
+        digester.push(this);
+        return digester;
+    }
+
+    /**
+     * Applies the format string to the given value.  Currently,
+     * this simply replaces '{file}' with the value.  If you
+     * want to handle more complicated formats, override this method.
+     */
+    protected String format(String format, String value) {
+        if (format == null) {
+            return value;
+        }
+        return format.replace("{file}", value).replace("{context}", this.context);
+    }
+
+    /**
+     * NOTE: This method may change or disappear w/o warning; don't depend
+     * on it unless you're willing to update your code whenever this changes.
+     */
+    protected Group getGroup(String name) {
+        if (groups == null) {
+            return null;
+        }
+        return groups.get(name);
+    }
+
+    /**
+     * NOTE: This method may change or disappear w/o warning; don't depend
+     * on it unless you're willing to update your code whenever this changes.
+     */
+    protected Group makeGroup(String name) {
+        trace(LOG, "Creating group '%s'", name);
+        Group group = new Group(name, LOG);
+        groups.put(name, group);
+        return group;
+    }
+
+    /**
+     * Adds the specified files organized by type to this instance's
+     * current dependencies.
+     */
+    protected void addDependencies(Map<String,List<String>> fbt) {
+        if (this.dependencies == null) {
+            dependencies = new LinkedHashMap<String,List<String>>(fbt.size());
+        }
+        for (Map.Entry<String,List<String>> entry : fbt.entrySet()) {
+            String type = entry.getKey();
+            if (getType(type) == null) {
+                LOG.error("UiDependencyTool: Type '"+type+"' is unknown and will not be printed
unless defined.");
+            }
+            List<String> existing = dependencies.get(type);
+            if (existing == null) {
+                existing =  new ArrayList<String>(entry.getValue().size());
+                dependencies.put(type, existing);
+            }
+            for (String file : entry.getValue()) {
+                if (!existing.contains(file)) {
+                    trace(LOG, "Adding %s: %s", type, file);
+                    existing.add(file);
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds a file to this instance's dependencies under the specified type.
+     */
+    protected void addFile(String type, String file) {
+        List<String> files = null;
+        if (dependencies == null) {
+            dependencies = new LinkedHashMap<String,List<String>>(types.size());
+        } else {
+            files = dependencies.get(type);
+        }
+        if (files == null) {
+            files = new ArrayList<String>();
+            dependencies.put(type, files);
+        }
+        if (!files.contains(file)) {
+            trace(LOG, "Adding %s: %s", type, file);
+            files.add(file);
+        }
+    }
+
+
+    /**
+     * For internal use only. Use/override get/setFormat instead.
+     */
+    private Type getType(String type) {
+        for (Type t : types) {
+            if (t.name.equals(type)) {
+                return t;
+            }
+        }
+        return null;
+    }
+
+    //TODO: replace this method with ConversionUtils.toURL(file, this)
+    //      once VelocityTools 2.0-beta3 or 2.0 final is released.
+    private URL toURL(String file) {
+        try
+        {
+            return ClassUtils.getResource(file, this);
+        }
+        catch (Exception e) {
+            return null;
+        }
+    }
+
+
+    /**
+     * NOTE: This class may change or disappear w/o warning; don't depend
+     * on it unless you're willing to update your code whenever this changes.
+     */
+    protected static class Group {
+
+        private volatile boolean resolved = true;
+        private String name;
+        private Map<String,Integer> typeCounts = new LinkedHashMap<String,Integer>();
+        private Map<String,List<String>> dependencies = new LinkedHashMap<String,List<String>>();
+        private List<String> groups;
+        private Log LOG;
+
+        public Group(String name, Log log) {
+            this.name = name;
+            this.LOG = log;
+        }
+
+        private void trace(String msg, Object... args) {
+            if (LOG.isTraceEnabled()) {
+                UiDependencyTool.trace(LOG, "Group "+name+": "+msg, args);
+            }
+        }
+
+        public void addFile(String type, String value) {
+            List<String> files = dependencies.get(type);
+            if (files == null) {
+                files = new ArrayList<String>();
+                dependencies.put(type, files);
+            }
+            if (!files.contains(value)) {
+                trace("Adding %s: %s", type, value);
+                files.add(value);
+            }
+        }
+
+        public void addGroup(String group) {
+            if (this.groups == null) {
+                this.resolved = false;
+                this.groups = new ArrayList<String>();
+            }
+            if (!this.groups.contains(group)) {
+                trace("Adding group %s", group, name);
+                this.groups.add(group);
+            }
+        }
+
+        public Map<String,List<String>> getDependencies(UiDependencyTool parent)
{
+            resolve(parent);
+            return this.dependencies;
+        }
+
+        protected void resolve(UiDependencyTool parent) {
+            if (!resolved)  {
+                // mark first to keep circular from becoming infinite
+                resolved = true;
+                trace("resolving...");
+                for (String name : groups) {
+                    Group group = parent.getGroup(name);
+                    if (group == null) {
+                        throw new NullPointerException("No group named '"+name+"'");
+                    }
+                    Map<String,List<String>> dependencies = group.getDependencies(parent);
+                    for (Map.Entry<String,List<String>> type : dependencies.entrySet())
{
+                        for (String value : type.getValue()) {
+                            addFileFromGroup(type.getKey(), value);
+                        }
+                    }
+                }
+                trace(" is resolved.");
+            }
+        }
+
+        private void addFileFromGroup(String type, String value) {
+            List<String> files = dependencies.get(type);
+            if (files == null) {
+                files = new ArrayList<String>();
+                files.add(value);
+                trace("adding %s '%s' first", type, value);
+                dependencies.put(type, files);
+                typeCounts.put(type, 1);
+            } else if (!files.contains(value)) {
+                Integer count = typeCounts.get(type);
+                if (count == null) {
+                    count = 0;
+                }
+                files.add(count, value);
+                trace("adding %s '%s' at %s", type, value, count);
+                typeCounts.put(type, ++count);
+            }
+        }
+    }
+
+    /**
+     * NOTE: This class may change or disappear w/o warning; don't depend
+     * on it unless you're willing to update your code whenever this changes.
+     */
+    protected static class TypeRule extends Rule {
+
+        private UiDependencyTool parent;
+
+        public void begin(String ns, String el, Attributes attributes) throws Exception {
+            parent = (UiDependencyTool)digester.peek();
+
+            for (int i=0; i < attributes.getLength(); i++) {
+                String name = attributes.getLocalName(i);
+                if ("".equals(name)) {
+                    name = attributes.getQName(i);
+                }
+                if ("name".equals(name)) {
+                    digester.push(attributes.getValue(i));
+                }
+            }
+        }
+
+        public void body(String ns, String el, String typeFormat) throws Exception {
+            String typeName = (String)digester.pop();
+            parent.setFormat(typeName, typeFormat);
+        }
+    }
+
+    /**
+     * NOTE: This class may change or disappear w/o warning; don't depend
+     * on it unless you're willing to update your code whenever this changes.
+     */
+    protected static class GroupRule extends Rule {
+
+        private UiDependencyTool parent;
+
+        public void begin(String ns, String el, Attributes attributes) throws Exception {
+            parent = (UiDependencyTool)digester.peek();
+
+            for (int i=0; i < attributes.getLength(); i++) {
+                String name = attributes.getLocalName(i);
+                if ("".equals(name)) {
+                    name = attributes.getQName(i);
+                }
+                if ("name".equals(name)) {
+                    digester.push(parent.makeGroup(attributes.getValue(i)));
+                }
+            }
+        }
+
+        public void end(String ns, String el) throws Exception {
+            digester.pop();
+        }
+    }
+
+    /**
+     * NOTE: This class may change or disappear w/o warning; don't depend
+     * on it unless you're willing to update your code whenever this changes.
+     */
+    protected static class FileRule extends Rule {
+
+        public void begin(String ns, String el, Attributes attributes) throws Exception {
+            for (int i=0; i < attributes.getLength(); i++) {
+                String name = attributes.getLocalName(i);
+                if ("".equals(name)) {
+                    name = attributes.getQName(i);
+                }
+                if ("type".equals(name)) {
+                    digester.push(attributes.getValue(i));
+                }
+            }
+        }
+
+        public void body(String ns, String el, String value) throws Exception {
+            String type = (String)digester.pop();
+            Group group = (Group)digester.peek();
+            group.addFile(type, value);
+        }
+    }
+
+    /**
+     * NOTE: This class may change or disappear w/o warning; don't depend
+     * on it unless you're willing to update your code whenever this changes.
+     */
+    protected static class NeedsRule extends Rule {
+
+        public void body(String ns, String el, String otherGroup) throws Exception {
+            Group group = (Group)digester.peek();
+            group.addGroup(otherGroup);
+        }
+    }
+
+
+    private static final class Type {
+
+        protected String name;
+        protected String format;
+
+        Type(String n, String f) {
+            name = n;
+            format = f;
+        }
+    }
+
+}

Propchange: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/view/UiDependencyTool.java
------------------------------------------------------------------------------
    svn:executable = *



Mime
View raw message