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
+ * ---
+ * <html>
+ * <head>
+ * $depends.on('profile').print('
+ * ')
+ * </head>
+ * ...
+ *
+ * Output
+ * ------
+ * <html>
+ * <head>
+ * <style rel="stylesheet" type="text/css" href="css/globals.css"/>
+ * <script type="text/javascript" src="js/jquery.js"></script>
+ * <script type="text/javascript"
src="js/profile.js"></script>
+ * </head>
+ * ...
+ *
+ * Example tools.xml:
+ * <tools>
+ * <toolbox scope="request">
+ * <tool
class="org.apache.velocity.tools.view.beta.UiDependencyTool"/>
+ * </toolbox>
+ * </tools>
+ *
+ * Example ui.xml:
+ * <ui>
+ * <type name="style"><![CDATA[<link rel="stylesheet"
type="text/css" href="/styles/{file}">]]></type>
+ * <group name="globals">
+ * <file type="style">css/globals.css<file/>
+ * </group>
+ * <group name="jquery">
+ * <file type="script">js/jquery.js<file/>
+ * </group>
+ * <group name="profile">
+ * <needs>globals</needs>
+ * <needs>jquery</needs>
+ * <file type="script">js/profile.js<file/>
+ * </group>
+ * </ui>
+ * </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 = *