Java tutorial
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; } } }