package org.apache.velocity.tools.generic; | |
/* | |
* 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.Serializable; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import org.apache.velocity.tools.config.DefaultKey; | |
/** | |
* <b>NOTE: This tools is considered "alpha" quality due to lack of testing | |
* and a generally unpolished API. Feel free to use but expect changes. | |
* Also, this is not automatically provided via the default tools.xml file. | |
* </b> | |
* | |
* <p> | |
* A tool to make it easy to generate XML or HTML on the fly. It uses a CSS-type | |
* syntax with a vaguely jQuery-ish API to help you generate the markup you need.</p> | |
* <pre> | |
* Example uses in a template: | |
* #set( $foospan = $markup.span.id($foo.id).body($foo) ) | |
* $markup.tag('table tr.bar td').body("This is $foospan") | |
* | |
* Output: | |
* <table> | |
* <tr class="bar"> | |
* <td>This is <span id="foo1">my first foo.</span></td> | |
* </tr> | |
* </table> | |
* | |
* | |
* Example tools.xml config: | |
* <tools> | |
* <toolbox scope="application"> | |
* <tool class="org.apache.velocity.tools.generic.alpha.MarkupTool"/> | |
* </toolbox> | |
* </tools> | |
* </pre> | |
* | |
* @author Nathan Bubna | |
* @since VelocityTools 2.0 | |
* @version $Id$ | |
*/ | |
@DefaultKey("mark") | |
public class MarkupTool extends SafeConfig implements Serializable | |
{ | |
private static final long serialVersionUID = -777597069616274442L; | |
public static final String DEFAULT_TAB = " "; | |
public static final String DEFAULT_DELIMITER = " "; | |
private String tab = DEFAULT_TAB; | |
private String delim = DEFAULT_DELIMITER; | |
/** | |
* Configuration | |
* @param tab tab string | |
*/ | |
public void setTab(String tab) | |
{ | |
if (isConfigLocked()) | |
{ | |
getLog().error("setTab() failure: configuration is locked"); | |
} | |
else | |
{ | |
this.tab = tab; | |
} | |
} | |
public String getTab() | |
{ | |
return this.tab; | |
} | |
public Tag get(String tag) | |
{ | |
return tag(tag); | |
} | |
public Tag tag(String definition) | |
{ | |
String[] tags = split(definition); | |
Tag last = null; | |
for (int i=0; i < tags.length; i++) | |
{ | |
Tag tag = parse(tags[i]); | |
if (last != null) | |
{ | |
last.append(tag); | |
} | |
last = tag; | |
} | |
return last; | |
} | |
protected String[] split(String me) | |
{ | |
//TODO: fix escaped delimiters | |
return me.split(delim); | |
} | |
private static enum Mode { NAME, ID, CLASS, ATTR } | |
protected Tag parse(String definition) | |
{ | |
StringBuilder store = new StringBuilder(); | |
Tag tag = new Tag(this); | |
Mode mode = Mode.NAME; | |
for (int i=0; i < definition.length(); i++) | |
{ | |
char c = definition.charAt(i); | |
if (c == '#') | |
{ | |
store = clear(mode, tag, store, true); | |
mode = Mode.ID; | |
} | |
else if (c == '.') | |
{ | |
store = clear(mode, tag, store, true); | |
mode = Mode.CLASS; | |
} | |
else if (c == '[') | |
{ | |
store = clear(mode, tag, store, true); | |
mode = Mode.ATTR; | |
} | |
else if (c == ']') | |
{ | |
store = clear(mode, tag, store, true); | |
mode = Mode.NAME; | |
} | |
else | |
{ | |
store.append(c); | |
} | |
} | |
clear(mode, tag, store, false); | |
return tag; | |
} | |
private StringBuilder clear(Mode mode, Tag tag, StringBuilder val, boolean emptyStore) | |
{ | |
if (val.length() > 0) | |
{ | |
String s = val.toString(); | |
switch (mode) | |
{ | |
case NAME: | |
tag.name(s); | |
break; | |
case ID: | |
tag.id(s); | |
break; | |
case CLASS: | |
tag.addClass(s); | |
break; | |
case ATTR: | |
if (s.indexOf('=') > 0) | |
{ | |
String[] kv = s.split("="); | |
tag.attr(kv[0], kv[1]); | |
} | |
else | |
{ | |
tag.attr(s, null); | |
} | |
break; | |
} | |
if (emptyStore) | |
{ | |
return new StringBuilder(); | |
} | |
return val; | |
} | |
else | |
{ | |
// already is clean | |
return val; | |
} | |
} | |
public static class Tag | |
{ | |
private MarkupTool tool; | |
private Tag parent; | |
private Object name; | |
private Object id; | |
private List<Object> classes; | |
private Map<Object,Object> attributes; | |
private List<Object> children; | |
public Tag(MarkupTool tool) | |
{ | |
this.tool = tool; | |
} | |
public Tag name(Object name) | |
{ | |
this.name = name; | |
return this; | |
} | |
public Tag id(Object id) | |
{ | |
this.id = id; | |
return this; | |
} | |
public Tag addClass(Object c) | |
{ | |
if (c == null) | |
{ | |
return null; | |
} | |
if (classes == null) | |
{ | |
classes = new ArrayList<Object>(); | |
} | |
classes.add(c); | |
return this; | |
} | |
public Tag attr(Object k, Object v) | |
{ | |
if (k == null) | |
{ | |
return null; | |
} | |
if (attributes == null) | |
{ | |
attributes = new HashMap<Object,Object>(); | |
} | |
attributes.put(k, v); | |
return this; | |
} | |
public Tag body(Object o) | |
{ | |
if (children == null) | |
{ | |
children = new ArrayList<Object>(); | |
} | |
else | |
{ | |
children.clear(); | |
} | |
children.add(o); | |
return this; | |
} | |
public Tag append(Object o) | |
{ | |
if (children == null) | |
{ | |
children = new ArrayList<Object>(); | |
} | |
children.add(o); | |
if (o instanceof Tag) | |
{ | |
((Tag)o).parent(this); | |
} | |
return this; | |
} | |
public Tag prepend(Object o) | |
{ | |
if (children == null) | |
{ | |
children = new ArrayList<Object>(); | |
children.add(o); | |
} | |
else | |
{ | |
children.add(0, o); | |
} | |
if (o instanceof Tag) | |
{ | |
((Tag)o).parent(this); | |
} | |
return this; | |
} | |
public Tag wrap(String tag) | |
{ | |
// make new tag | |
Tag prnt = tool.tag(tag); | |
// give root of new tag our parent | |
prnt.root().parent(parent()); | |
// make new tag our parent | |
parent(prnt); | |
return this; | |
} | |
public Tag orphan() | |
{ | |
return parent(null); | |
} | |
public Tag parent(Tag parent) | |
{ | |
this.parent = parent; | |
return this; | |
} | |
public Tag parent() | |
{ | |
return this.parent; | |
} | |
public Tag root() | |
{ | |
if (isOrphan()) | |
{ | |
return this; | |
} | |
return this.parent.root(); | |
} | |
public List<Object> children() | |
{ | |
return children; | |
} | |
public boolean isOrphan() | |
{ | |
return (parent == null); | |
} | |
public boolean isEmpty() | |
{ | |
return (children == null || children().isEmpty()); | |
} | |
public boolean matches(Tag tag) | |
{ | |
if (missed(name, tag.name) || | |
missed(id, tag.id) || | |
missed(classes, tag.classes)) | |
{ | |
return false; | |
} | |
//TODO: match attributes | |
return true; | |
} | |
protected boolean missed(Object target, Object arrow) | |
{ | |
// no arrow, no miss | |
if (arrow == null) | |
{ | |
return false; | |
} | |
// otherwise, the arrow must hit the target | |
return !arrow.equals(target); | |
} | |
protected boolean missed(List<Object> targets, List<Object> arrows) | |
{ | |
// no arrows, no miss | |
if (arrows == null) | |
{ | |
return false; | |
} | |
// no targets, always miss | |
if (targets == null) | |
{ | |
return true; | |
} | |
for (Object o : arrows) | |
{ | |
if (!targets.contains(o)) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
/************* rendering methods **************/ | |
/** | |
* Render tag | |
* @param indent indentation string | |
* @param s string builder | |
*/ | |
protected void render(String indent, StringBuilder s) | |
{ | |
if (render_start(indent, s)) | |
{ | |
render_body(indent, s); | |
render_end(indent, s); | |
} | |
} | |
/** | |
* Start tag rendering | |
* @param indent indentation string | |
* @param s string builder | |
* @return whether a closing tag is needed | |
*/ | |
protected boolean render_start(String indent, StringBuilder s) | |
{ | |
if (indent != null) | |
{ | |
s.append(indent); | |
} | |
s.append('<'); | |
render_name(s); | |
render_id(s); | |
render_classes(s); | |
render_attributes(s); | |
if (isEmpty()) | |
{ | |
s.append("/>"); | |
return false; | |
} | |
else | |
{ | |
s.append('>'); | |
return true; | |
} | |
} | |
/** | |
* Render tag name | |
* @param s string builder | |
*/ | |
protected void render_name(StringBuilder s) | |
{ | |
s.append(name == null ? "div" : name); | |
} | |
/** | |
* render tag id | |
* @param s string builder | |
*/ | |
protected void render_id(StringBuilder s) | |
{ | |
if (id != null) | |
{ | |
s.append(" id=\"").append(id).append('"'); | |
} | |
} | |
/** | |
* render tag classes | |
* @param s string builder | |
*/ | |
protected void render_classes(StringBuilder s) | |
{ | |
if (classes != null) | |
{ | |
s.append(" class=\""); | |
for (int i=0; i < classes.size(); i++) | |
{ | |
s.append(classes.get(i)); | |
if (i + 1 != classes.size()) | |
{ | |
s.append(' '); | |
} | |
} | |
s.append('"'); | |
} | |
} | |
/** | |
* Render tag attributes | |
* @param s string builder | |
*/ | |
protected void render_attributes(StringBuilder s) | |
{ | |
if (attributes != null) | |
{ | |
for (Map.Entry<Object,Object> entry : attributes.entrySet()) | |
{ | |
s.append(' ').append(entry.getKey()).append("=\""); | |
if (entry.getValue() != null) | |
{ | |
s.append(entry.getValue()); | |
} | |
s.append('"'); | |
} | |
} | |
} | |
/** | |
* Render tag body | |
* @param indent indentation string | |
* @param s string builder | |
*/ | |
protected void render_body(String indent, StringBuilder s) | |
{ | |
String kidIndent = indent + tool.getTab(); | |
for (Object o : children) | |
{ | |
if (o instanceof Tag) | |
{ | |
((Tag)o).render(kidIndent, s); | |
} | |
else | |
{ | |
s.append(kidIndent); | |
s.append(o); | |
} | |
} | |
} | |
/** | |
* Render tag end | |
* @param indent indentation string | |
* @param s string builder | |
*/ | |
protected void render_end(String indent, StringBuilder s) | |
{ | |
if (indent != null) | |
{ | |
s.append(indent); | |
} | |
s.append("</").append(name).append('>'); | |
} | |
/** | |
* @return string representation | |
*/ | |
public String toString() | |
{ | |
StringBuilder s = new StringBuilder(); | |
root().render("\n", s); | |
return s.toString(); | |
} | |
} | |
} |