org.apache.velocity.tools.view.UiDependencyTool Maven / Gradle / Ivy
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.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 jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.velocity.tools.generic.SafeConfig;
import org.apache.velocity.tools.generic.ValueParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.apache.commons.digester3.Digester;
import org.apache.commons.digester3.Rule;
import org.apache.velocity.tools.ClassUtils;
import org.apache.velocity.tools.Scope;
import org.apache.velocity.tools.config.DefaultKey;
import org.apache.velocity.tools.config.ValidScope;
/**
* NOTE: This tool is considered "beta" quality due to lack of public testing
* and is not automatically provided via the default tools.xml file.
*
*
* 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.
*
* Example of use:
*
* 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>
*
*
* @author Nathan Bubna
* @version $Revision: 16660 $
*/
@DefaultKey("depends")
@ValidScope(Scope.REQUEST)
public class UiDependencyTool extends SafeConfig
{
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 DEFAULT_TYPES;
static {
List types = new ArrayList();
// start out with these two types
types.add(new Type("style", ""));
types.add(new Type("script", ""));
DEFAULT_TYPES = Collections.unmodifiableList(types);
}
private Map groups = null;
private List types = DEFAULT_TYPES;
private Map> dependencies;
private static Logger LOG = LoggerFactory.getLogger(UiDependencyTool.class);
private String context = "";
protected void configure(ValueParser params) {
ServletContext app = (ServletContext)params.get(ViewContext.SERVLET_CONTEXT_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 {
getLog().debug("UiDependencyTool: Loading file: {}", file);
}
synchronized (app) {
// first, see if we've already read this file
groups = (Map)app.getAttribute(GROUPS_KEY_SPACE+file);
if (groups == null) {
groups = new LinkedHashMap();
// 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 alt = (List)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.
* @param name group name
* @return this or null
*/
public UiDependencyTool on(String name) {
Map> 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.
* @param type file type
* @param file dependency file
* @return this or null
*/
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.
* @return all dependencies
*/
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.
* @param typeOrDelim type asked for, or delimiter
* @return all dependencies
* @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.
* @param type file type
* @param delim lines delimiter
* @return list of dependencies for thie type, formatted using delimiter
*/
public String print(String type, String delim) {
List 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.
* @param delim delimiter
* @return list of dependencies
*/
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 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.
* @param path context path
* @return this
*/
public UiDependencyTool context(String path)
{
this.context = path;
return this;
}
/**
* Retrieves the configured format string for the specified file type.
* @param type file type
* @return configured format
*/
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.
* @param type file type
* @param format format string
*/
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();
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.
* @return map of all dependencies
*/
public Map> getDependencies() {
return dependencies;
}
/**
* Returns the {@link List} of files for the specified file type, if any.
* @param type file type
* @return all dependencies for this type
*/
public List 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.
* @param name group name
* @return map of all dependencies for this group
*/
public Map> 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)}.
* @return empty 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.
* @param file file
* @param required whether this file is required
*/
protected void read(String file, boolean required) {
getLog().debug("UiDependencyTool: Reading file from {}", file);
URL url = toURL(file);
if (url == null) {
String msg = "UiDependencyTool: Could not read file from '"+file+"'";
if (required) {
getLog().error(msg);
throw new IllegalArgumentException(msg);
} else {
getLog().debug(msg);
}
} else {
Digester digester = createDigester();
try
{
digester.parse(url.openStream());
}
catch (SAXException saxe)
{
getLog().error("UiDependencyTool: Failed to parse '{}'", file, saxe);
throw new RuntimeException("While parsing the InputStream", saxe);
}
catch (IOException ioe)
{
getLog().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.
* @return new digester
*/
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.
* @param format format string
* @param value dependency file
* @return formatted string
*/
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.
* @param name file name
* @return group this file belongs to, or null
*/
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.
* @param name group name
* @return new group
*/
protected Group makeGroup(String name) {
getLog().trace("UiDependencyTool: Creating group '{}'", name);
Group group = new Group(name);
groups.put(name, group);
return group;
}
/**
* Adds the specified files organized by type to this instance's
* current dependencies.
* @param fbt dependencies map
*/
protected void addDependencies(Map> fbt) {
if (this.dependencies == null) {
dependencies = new LinkedHashMap>(fbt.size());
}
for (Map.Entry> entry : fbt.entrySet()) {
String type = entry.getKey();
if (getType(type) == null) {
getLog().error("UiDependencyTool: Type '{}' is unknown and will not be printed unless defined.", type);
}
List existing = dependencies.get(type);
if (existing == null) {
existing = new ArrayList(entry.getValue().size());
dependencies.put(type, existing);
}
for (String file : entry.getValue()) {
if (!existing.contains(file)) {
getLog().trace("UiDependencyTool: Adding {}: {}", type, file);
existing.add(file);
}
}
}
}
/**
* Adds a file to this instance's dependencies under the specified type.
* @param type file type
* @param file file name
*/
protected void addFile(String type, String file) {
List files = null;
if (dependencies == null) {
dependencies = new LinkedHashMap>(types.size());
} else {
files = dependencies.get(type);
}
if (files == null) {
files = new ArrayList();
dependencies.put(type, files);
}
if (!files.contains(file)) {
getLog().trace("UiDependencyTool: Adding {}: {}", type, file);
files.add(file);
}
}
/**
* For internal use only. Use/override get/setFormat instead.
* @param type file type
* @return {@link Type} object
*/
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 class Group {
private volatile boolean resolved = true;
private String name;
private Map typeCounts = new LinkedHashMap();
private Map> dependencies = new LinkedHashMap>();
private List groups;
public Group(String name) {
this.name = name;
}
public void addFile(String type, String value) {
List files = dependencies.get(type);
if (files == null) {
files = new ArrayList();
dependencies.put(type, files);
}
if (!files.contains(value)) {
getLog().trace("Group {}: Adding {}: {}", name, type, value);
files.add(value);
}
}
public void addGroup(String group) {
if (this.groups == null) {
this.resolved = false;
this.groups = new ArrayList();
}
if (!this.groups.contains(group)) {
getLog().trace("Group {}: Adding group {}", name, group);
this.groups.add(group);
}
}
public Map> 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;
getLog().trace("Group {}: resolving...", name);
for (String name : groups) {
Group group = parent.getGroup(name);
if (group == null) {
throw new NullPointerException("No group named '"+name+"'");
}
Map> dependencies = group.getDependencies(parent);
for (Map.Entry> type : dependencies.entrySet()) {
for (String value : type.getValue()) {
addFileFromGroup(type.getKey(), value);
}
}
}
getLog().trace("Group {}: is resolved.", name);
}
}
private void addFileFromGroup(String type, String value) {
List files = dependencies.get(type);
if (files == null) {
files = new ArrayList();
files.add(value);
getLog().trace("Group {}: adding {} '{}' first", name, 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);
getLog().trace("Group {}: adding {} '{}' at {}", name, 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)getDigester().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)) {
getDigester().push(attributes.getValue(i));
}
}
}
public void body(String ns, String el, String typeFormat) throws Exception {
String typeName = (String)getDigester().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)getDigester().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)) {
getDigester().push(parent.makeGroup(attributes.getValue(i)));
}
}
}
public void end(String ns, String el) throws Exception {
getDigester().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)) {
getDigester().push(attributes.getValue(i));
}
}
}
public void body(String ns, String el, String value) throws Exception {
String type = (String)getDigester().pop();
Group group = (Group)getDigester().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)getDigester().peek();
group.addGroup(otherGroup);
}
}
private static final class Type {
protected String name;
protected String format;
Type(String n, String f) {
name = n;
format = f;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy