org.kohsuke.stapler.jelly.groovy.JellyBuilder Maven / Gradle / Ivy
Show all versions of stapler-groovy Show documentation
/*
* Copyright (c) 2004-2010, Kohsuke Kawaguchi
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.kohsuke.stapler.jelly.groovy;
import groovy.lang.Closure;
import groovy.lang.GString;
import groovy.lang.GroovyObjectSupport;
import groovy.lang.MissingMethodException;
import groovy.lang.MissingPropertyException;
import groovy.xml.QName;
import org.apache.commons.beanutils.ConvertingWrapDynaBean;
import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.DynaProperty;
import org.apache.commons.jelly.DynaTag;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyException;
import org.apache.commons.jelly.JellyTagException;
import org.apache.commons.jelly.Script;
import org.apache.commons.jelly.Tag;
import org.apache.commons.jelly.TagLibrary;
import org.apache.commons.jelly.XMLOutput;
import org.apache.commons.jelly.expression.ConstantExpression;
import org.apache.commons.jelly.expression.Expression;
import org.apache.commons.jelly.impl.TagScript;
import org.apache.commons.jelly.impl.TextScript;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXContentHandler;
import org.kohsuke.stapler.MetaClassLoader;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.framework.adjunct.AdjunctManager;
import org.kohsuke.stapler.framework.adjunct.AdjunctsInPage;
import org.kohsuke.stapler.framework.adjunct.NoSuchAdjunctException;
import org.kohsuke.stapler.jelly.CustomTagLibrary;
import org.kohsuke.stapler.jelly.JellyClassLoaderTearOff;
import org.kohsuke.stapler.jelly.JellyClassTearOff;
import org.kohsuke.stapler.lang.Klass;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import javax.servlet.ServletContext;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* Drive Jelly scripts from Groovy markup.
*
* @author Kohsuke Kawaguchi
*/
public final class JellyBuilder extends GroovyObjectSupport {
/**
* Current {@link XMLOutput}.
*/
private XMLOutput output;
private JellyContext context;
private final Map taglibs = new HashMap();
private final StaplerRequest request;
private StaplerResponse response;
private String rootURL;
private final AdjunctManager adjunctManager;
/**
* Cached {@Link AttributesImpl} instance.
*/
private final AttributesImpl attributes = new AttributesImpl();
public JellyBuilder(JellyContext context,XMLOutput output) {
this.context = context;
this.output = output;
this.request = Stapler.getCurrentRequest();
this.adjunctManager = AdjunctManager.get(request.getServletContext());
}
/**
* This is used to allow QName to be used for the invocation.
*/
public Namespace namespace(String nsUri, String prefix) {
return new Namespace(this,nsUri,prefix);
}
public Namespace namespace(String nsUri) {
return namespace(nsUri,null);
}
public T namespace(Class type) {
TagLibraryUri a = type.getAnnotation(TagLibraryUri.class);
if (a==null) throw new IllegalArgumentException(type+" doesn't have @TagLibraryUri annotation");
return namespace(a.value(),null).createInvoker(type);
}
public XMLOutput getOutput() {
return output;
}
public JellyContext getContext() {
return context;
}
/**
* Includes another view.
*/
public void include(Object it, String view) throws IOException, JellyException {
_include(it,request.getWebApp().getKlass(it),view);
}
/**
* Includes another view.
*/
public void include(Class clazz, String view) throws IOException, JellyException {
_include(null,Klass.java(clazz),view);
}
private void _include(Object it, Klass clazz, String view) throws IOException, JellyException {
JellyClassTearOff t = request.getWebApp().getMetaClass(clazz).loadTearOff(JellyClassTearOff.class);
Script s = t.findScript(view);
if(s==null)
throw new IllegalArgumentException("No such view: "+view+" for "+clazz);
JellyContext context = new JellyContext(getContext());
if(it!=null)
context.setVariable("it",it);
context.setVariable("from", it);
ClassLoader old = Thread.currentThread().getContextClassLoader();
if (clazz.clazz instanceof Class)
Thread.currentThread().setContextClassLoader(((Class)clazz.clazz).getClassLoader());
try {
s.run(context,output);
} finally {
Thread.currentThread().setContextClassLoader(old);
}
}
public Object methodMissing(String name, Object args) {
doInvokeMethod(new QName("",name), args);
return null;
}
@SuppressWarnings({"ChainOfInstanceofChecks"})
protected void doInvokeMethod(QName name, Object args) {
List list = InvokerHelper.asList(args);
Map attributes = Collections.EMPTY_MAP;
Closure closure = null;
String innerText = null;
// figure out what parameters are what
switch (list.size()) {
case 0:
break;
case 1: {
Object object = list.get(0);
if (object instanceof Map) {
attributes = (Map) object;
} else if (object instanceof Closure) {
closure = (Closure) object;
break;
} else {
if (object!=null)
innerText = object.toString();
}
break;
}
case 2: {
Object object1 = list.get(0);
Object object2 = list.get(1);
if (object1 instanceof Map) {
attributes = (Map) object1;
if (object2 instanceof Closure) {
closure = (Closure) object2;
} else
if(object2!=null) {
innerText = object2.toString();
}
} else {
innerText = object1.toString();
if (object2 instanceof Closure) {
closure = (Closure) object2;
} else if (object2 instanceof Map) {
attributes = (Map) object2;
} else {
throw new MissingMethodException(name.toString(), getClass(), list.toArray());
}
}
break;
}
case 3: {
Object arg0 = list.get(0);
Object arg1 = list.get(1);
Object arg2 = list.get(2);
if (arg0 instanceof Map && arg2 instanceof Closure) {
closure = (Closure) arg2;
attributes = (Map) arg0;
innerText = arg1.toString();
} else if (arg1 instanceof Map && arg2 instanceof Closure) {
closure = (Closure) arg2;
attributes = (Map) arg1;
innerText = arg0.toString();
} else {
throw new MissingMethodException(name.toString(), getClass(), list.toArray());
}
break;
}
default:
throw new MissingMethodException(name.toString(), getClass(), list.toArray());
}
if (isTag(name)) {// bridge to other Jelly tags
try {
TagScript tagScript = createTagScript(name, attributes);
if (tagScript!=null) {
Script body = NULL_SCRIPT;
if(closure!=null) {
final Closure theClosure = closure;
body = new Script() {
public Script compile() throws JellyException {
return this;
}
public void run(JellyContext context, XMLOutput output) throws JellyTagException {
JellyContext oldc = setContext(context);
XMLOutput oldo = setOutput(output);
try {
theClosure.setDelegate(JellyBuilder.this);
theClosure.call();
} finally {
setContext(oldc);
setOutput(oldo);
}
}
};
} else
if(innerText!=null)
body = new TextScript(innerText);
tagScript.setTagBody(body);
tagScript.run(context,output);
return;
}
} catch(JellyException e) {
throw new RuntimeException(e);
}
}
// static tag
this.attributes.clear();
for (Entry e : ((Map,?>)attributes).entrySet()) {
Object v = e.getValue();
if(v==null) continue;
String attName = e.getKey().toString();
this.attributes.addAttribute("",attName,attName,"CDATA", v.toString());
}
try {
output.startElement(name.getNamespaceURI(),name.getLocalPart(),name.getQualifiedName(),this.attributes);
if(closure!=null) {
closure.setDelegate(this);
closure.call();
}
if(innerText!=null)
text(innerText);
output.endElement(name.getNamespaceURI(),name.getLocalPart(),name.getQualifiedName());
} catch (SAXException e) {
throw new RuntimeException(e); // what's the proper way to handle exceptions in Groovy?
}
}
/**
* Is this a static XML tag that we just generate, or
* a jelly tag that needs evaluation?
*/
private boolean isTag(QName name) {
return name.getNamespaceURI().length()>0;
}
/**
* Create a tag script if the given QName is a taglib invocation, or return null
* to handle it like a literal static tag.
*/
private TagScript createTagScript(QName n, Map, ?> attributes) throws JellyException {
TagLibrary lib = context.getTagLibrary(n.getNamespaceURI());
if(lib!=null) {
String localName = n.getLocalPart();
TagScript tagScript = lib.createTagScript(localName, null/*this parameter appears to be unused.*/);
if (tagScript==null) tagScript = lib.createTagScript(localName.replace('_','-'), null);
if (tagScript!=null) {
if (attributes != null) {
for (Entry e : attributes.entrySet()) {
Object v = e.getValue();
if (v != null) {
if (v instanceof GString) {
v = v.toString();
}
tagScript.addAttribute(e.getKey().toString(), new ConstantExpression(v));
}
}
}
return tagScript;
}
}
// otherwise treat it as a literal.
return null;
}
/*
* Copyright 2002,2004 The Apache Software Foundation.
*
* Licensed 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.
*/
private void configureTag(Tag tag, Map attributes) throws JellyException {
if ( tag instanceof DynaTag ) {
DynaTag dynaTag = (DynaTag) tag;
for (Object o : attributes.entrySet()) {
Entry entry = (Entry) o;
String name = (String) entry.getKey();
if(name.equals("xmlns")) continue; // we'll process this by ourselves
Object value = getValue(entry, dynaTag.getAttributeType(name));
dynaTag.setAttribute(name, value);
}
} else {
// treat the tag as a bean
DynaBean dynaBean = new ConvertingWrapDynaBean( tag );
for (Object o : attributes.entrySet()) {
Entry entry = (Entry) o;
String name = (String) entry.getKey();
if(name.equals("xmlns")) continue; // we'll process this by ourselves
DynaProperty property = dynaBean.getDynaClass().getDynaProperty(name);
if (property == null) {
throw new JellyException("This tag does not understand the '" + name + "' attribute");
}
dynaBean.set(name, getValue(entry,property.getType()));
}
}
}
/**
* Obtains the value from the map entry in the right type.
*/
private Object getValue(Entry entry, Class type) {
Object value = entry.getValue();
if (type== Expression.class)
value = new ConstantExpression(entry.getValue());
return value;
}
private Attributes toAttributes(Map attributes) {
AttributesImpl atts = new AttributesImpl();
for (Object o : attributes.entrySet()) {
Entry e = (Entry) o;
if(e.getKey().toString().equals("xmlns")) continue; // we'll process them outside attributes
atts.addAttribute("", e.getKey().toString(), e.getKey().toString(), null, e.getValue().toString());
}
return atts;
}
JellyContext setContext(JellyContext newValue) {
JellyContext old = context;
context = newValue;
return old;
}
public XMLOutput setOutput(XMLOutput newValue) {
XMLOutput old = output;
output = newValue;
return old;
}
/**
* Executes the closure with the specified {@link XMLOutput}.
*/
public Object with(XMLOutput out, Closure c) {
XMLOutput old = setOutput(out);
try {
c.setDelegate(this);
return c.call();
} finally {
setOutput(old);
}
}
/**
* Captures the XML fragment generated by the given closure into dom4j DOM tree
* and return the root element.
*
* @return null
* if nothing was generated.
*/
public Element redirectToDom(Closure c) {
SAXContentHandler sc = new SAXContentHandler();
with(new XMLOutput(sc),c);
return sc.getDocument().getRootElement();
}
/**
* Allows values from {@link JellyContext} to be read
* like global variables. These includes 'request', 'response', etc.
*
* @see JellyClassTearOff
*/
public Object getProperty(String property) {
try {
return super.getProperty(property);
} catch (MissingPropertyException e) {
Object r = context.getVariableWithDefaultValue(property,MISSING);
if (r==MISSING) throw e;
return r;
}
}
/**
* Sets the value to {@link JellyContext} (typically as a pre-cursor to calling into Jelly tags.)
*/
public void set(String var, Object value) {
context.setVariable(var,value);
}
/**
* Gets the "it" object.
*
* In Groovy "it" is reserved word with a specific meaning,
* so instead use "my" as the word.
*/
public Object getMy() {
return context.getVariable("it");
}
/**
* Writes PCDATA.
*
*
* Any HTML unsafe characters in the string representation of the given object is
* properly escaped.
*
* @see #raw(Object)
*/
public void text(Object o) throws SAXException {
if (o!=null)
output.write(escape(o.toString()));
}
/**
* Generates HTML fragment from string.
*
*
* The string representation of the object is assumed to produce proper HTML.
* No further escaping is performed.
*
* @see #text(Object)
*/
public void raw(Object o) throws SAXException {
if (o!=null)
output.write(o.toString());
}
private String escape(String v) {
StringBuffer buf = new StringBuffer(v.length()+64);
for( int i=0; i')
buf.append(">");
else
if(ch=='&')
buf.append("&");
else
buf.append(ch);
}
return buf.toString();
}
/**
* {@link Script} that does nothing.
*/
private static final Script NULL_SCRIPT = new Script() {
public Script compile() {
return this;
}
public void run(JellyContext context, XMLOutput output) {
}
};
/**
* Loads a Groovy tag lib instance.
*
*
* A groovy tag library is really just a script class with bunch of method definitions,
* without any explicit class definition. Such a class is loaded as a subtype of
* {@link GroovyClosureScript} so that it can use this builder as the delegation target.
*
*
* This method instantiates the class (if not done so already for this request),
* and return it.
*/
public Object taglib(Class type) throws IllegalAccessException, InstantiationException, IOException, SAXException {
GroovyClosureScript o = taglibs.get(type);
if(o==null) {
o = (GroovyClosureScript) type.newInstance();
o.setDelegate(this);
taglibs.put(type,o);
adjunct(type.getName());
}
return o;
}
/**
* Includes the specified adjunct.
*
* This method is useful for including adjunct dynamically on demand.
*/
public void adjunct(String name) throws IOException, SAXException {
try {
AdjunctsInPage aip = AdjunctsInPage.get();
aip.generate(output,name);
} catch (NoSuchAdjunctException e) {
// that's OK.
}
}
/**
* Loads a jelly tag library.
*
* @param t
* If this is a subtype of {@link TagLibrary}, then that tag library is loaded and bound to the
* {@link Namespace} object, which you can later use to call tags.
* Otherwise, t has to have 'taglib' file in the resource and sibling "*.jelly" files will be treated
* as tag files.
*/
public Namespace jelly(Class t) {
String n = t.getName();
if(TagLibrary.class.isAssignableFrom(t)) {
// if the given class is a tag library itself, just record it
context.registerTagLibrary(n,n);
} else {
String path = n.replace('.', '/');
URL res = t.getClassLoader().getResource(path+"/taglib");
if(res!=null) {
// this class itself is not a tag library, but it contains Jelly side files that are tag files.
// (some of them might be views, but some of them are tag files, anyway.
JellyContext parseContext = MetaClassLoader.get(t.getClassLoader()).loadTearOff(JellyClassLoaderTearOff.class).createContext();
context.registerTagLibrary(n,
new CustomTagLibrary(parseContext, t.getClassLoader(), n, path));
} else {
throw new IllegalArgumentException("Cannot find taglib from "+t);
}
}
return new Namespace(this,n,"-"); // doesn't matter what the prefix is, since they are known to be taglibs
}
public ServletContext getServletContext() {
return getRequest().getServletContext();
}
public StaplerRequest getRequest() {
return request;
}
public StaplerResponse getResponse() {
if(response==null)
response = Stapler.getCurrentResponse();
return response;
}
public JellyBuilder getBuilder() {
return this;
}
/**
* Gets the absolute URL to the top of the webapp.
*
* @see StaplerRequest#getContextPath()
*/
public String getRootURL() {
if(rootURL==null)
rootURL = getRequest().getContextPath();
return rootURL;
}
/**
* Generates an {@code } tag to the resource.
*/
public void img(Object base, String localName) throws SAXException {
output.write(
"");
}
/**
* Yields a URL to the given resource.
*
* @param base
* The base class/object for which the 'localName' parameter
* is resolved from. If this is class, 'localName' is assumed
* to be a resource of this class. If it's other objects,
* 'localName' is assumed to be a resource of the class of this object.
*/
public String res(Object base, String localName) {
Class c;
if (base instanceof Class)
c = (Class) base;
else
c = base.getClass();
return adjunctManager.rootURL+'/'+c.getName().replace('.','/')+'/'+localName;
}
private static final Object MISSING = new Object();
}