
org.kohsuke.stapler.RequestImpl Maven / Gradle / Ivy
Show all versions of stapler-core 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;
import net.sf.json.JSON;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.jvnet.tiger_types.Lister;
import org.kohsuke.stapler.bind.BoundObjectTable;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
/**
* {@link StaplerRequest} implementation.
*
* @author Kohsuke Kawaguchi
*/
public class RequestImpl extends HttpServletRequestWrapper implements StaplerRequest {
/**
* Tokenized URLs and consumed tokens.
* This object is modified by {@link Stapler} as we parse through the URL.
*/
public final TokenList tokens;
/**
* Ancesotr nodes traversed so far.
* This object is modified by {@link Stapler} as we parse through the URL.
*/
public final List ancestors;
private final List ancestorsView;
public final Stapler stapler;
private final String originalRequestURI;
/**
* Cached result of {@link #getSubmittedForm()}
*/
private JSONObject structuredForm;
/**
* If the request is "multipart/form-data", parsed result goes here.
*
* @see #parseMultipartFormData()
*/
private Map parsedFormData;
private BindInterceptor bindInterceptor = BindInterceptor.NOOP;
public RequestImpl(Stapler stapler, HttpServletRequest request, List ancestors, TokenList tokens) {
super(request);
this.stapler = stapler;
this.ancestors = ancestors;
this.ancestorsView = Collections.unmodifiableList(ancestors);
this.tokens = tokens;
this.originalRequestURI = request.getRequestURI();
}
public boolean isJavaScriptProxyCall() {
String ct = getContentType();
return ct!=null && ct.startsWith("application/x-stapler-method-invocation");
}
public BoundObjectTable getBoundObjectTable() {
return stapler.getWebApp().boundObjectTable;
}
public String createJavaScriptProxy(Object toBeExported) {
return getBoundObjectTable().bind(toBeExported).getProxyScript();
}
public Stapler getStapler() {
return stapler;
}
public WebApp getWebApp() {
return stapler.getWebApp();
}
public String getRestOfPath() {
return tokens.assembleRestOfPath();
}
public String getOriginalRestOfPath() {
return tokens.assembleOriginalRestOfPath();
}
public ServletContext getServletContext() {
return stapler.getServletContext();
}
public RequestDispatcher getView(Object it,String viewName) throws IOException {
return getView(it.getClass(),it,viewName);
}
public RequestDispatcher getView(Class clazz, String viewName) throws IOException {
return getView(clazz,null,viewName);
}
public RequestDispatcher getView(Class clazz, Object it, String viewName) throws IOException {
for( Facet f : stapler.getWebApp().facets ) {
RequestDispatcher rd = f.createRequestDispatcher(this,clazz,it,viewName);
if(rd!=null)
return rd;
}
return null;
}
public String getRootPath() {
StringBuffer buf = super.getRequestURL();
int idx = 0;
for( int i=0; i<3; i++ )
idx += buf.substring(idx).indexOf("/")+1;
buf.setLength(idx-1);
buf.append(super.getContextPath());
return buf.toString();
}
public String getReferer() {
return getHeader("Referer");
}
public List getAncestors() {
return ancestorsView;
}
public Ancestor findAncestor(Class type) {
for( int i = ancestors.size()-1; i>=0; i-- ) {
AncestorImpl a = ancestors.get(i);
Object o = a.getObject();
if (type.isInstance(o))
return a;
}
return null;
}
public T findAncestorObject(Class type) {
Ancestor a = findAncestor(type);
if(a==null) return null;
return type.cast(a.getObject());
}
public Ancestor findAncestor(Object anc) {
for( int i = ancestors.size()-1; i>=0; i-- ) {
AncestorImpl a = ancestors.get(i);
Object o = a.getObject();
if (o==anc)
return a;
}
return null;
}
public boolean hasParameter(String name) {
return getParameter(name)!=null;
}
public String getOriginalRequestURI() {
return originalRequestURI;
}
public boolean checkIfModified(long lastModified, StaplerResponse rsp) {
return checkIfModified(lastModified,rsp,0);
}
public boolean checkIfModified(long lastModified, StaplerResponse rsp, long expiration) {
if(lastModified<=0)
return false;
// send out Last-Modified, or check If-Modified-Since
String since = getHeader("If-Modified-Since");
SimpleDateFormat format = Stapler.HTTP_DATE_FORMAT.get();
if(since!=null) {
try {
long ims = format.parse(since).getTime();
if(lastModified
List bindParametersToList(Class type, String prefix) {
List r = new ArrayList();
int len = Integer.MAX_VALUE;
Enumeration e = getParameterNames();
while(e.hasMoreElements()) {
String name = (String)e.nextElement();
if(name.startsWith(prefix))
len = Math.min(len,getParameterValues(name).length);
}
if(len==Integer.MAX_VALUE)
return r; // nothing
try {
loadConstructorParamNames(type);
// use the designated constructor for databinding
for( int i=0; i T bindParameters(Class type, String prefix) {
return bindParameters(type,prefix,0);
}
public T bindParameters(Class type, String prefix, int index) {
String[] names = loadConstructorParamNames(type);
// the actual arguments to invoke the constructor with.
Object[] args = new Object[names.length];
// constructor
Constructor c = findConstructor(type, names.length);
Class[] types = c.getParameterTypes();
// convert parameters
for( int i=0; i T bindJSON(Class type, JSONObject src) {
return type.cast(bindJSON(type, type, src));
}
public Object bindJSON(Type type, Class erasure, Object json) {
return new TypePair(type,erasure).convertJSON(json);
}
public void bindJSON(Object bean, JSONObject src) {
try {
for( String key : (Set)src.keySet() ) {
TypePair type = getPropertyType(bean, key);
if(type==null)
continue;
fill(bean,key, type.convertJSON(src.get(key)));
}
} catch (IllegalAccessException e) {
IllegalAccessError x = new IllegalAccessError(e.getMessage());
x.initCause(e);
throw x;
} catch (InvocationTargetException x) {
Throwable e = x.getTargetException();
if(e instanceof RuntimeException)
throw (RuntimeException)e;
if(e instanceof Error)
throw (Error)e;
throw new RuntimeException(x);
}
}
public List bindJSONToList(Class type, Object src) {
ArrayList r = new ArrayList();
if (src instanceof JSONObject) {
JSONObject j = (JSONObject) src;
r.add(bindJSON(type,j));
}
if (src instanceof JSONArray) {
JSONArray a = (JSONArray) src;
for (Object o : a) {
if (o instanceof JSONObject) {
JSONObject j = (JSONObject) o;
r.add(bindJSON(type,j));
}
}
}
return r;
}
private T invokeConstructor(Constructor c, Object[] args) {
try {
return c.newInstance(args);
} catch (InstantiationException e) {
InstantiationError x = new InstantiationError(e.getMessage());
x.initCause(e);
throw x;
} catch (IllegalAccessException e) {
IllegalAccessError x = new IllegalAccessError(e.getMessage());
x.initCause(e);
throw x;
} catch (InvocationTargetException e) {
Throwable x = e.getTargetException();
if(x instanceof Error)
throw (Error)x;
if(x instanceof RuntimeException)
throw (RuntimeException)x;
throw new IllegalArgumentException(x);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Failed to invoke "+c+" with "+ Arrays.asList(args),e);
}
}
private Constructor findConstructor(Class type, int length) {
Constructor>[] ctrs = type.getConstructors();
// one with DataBoundConstructor is the most reliable
for (Constructor c : ctrs) {
if(c.getAnnotation(DataBoundConstructor.class)!=null) {
if(c.getParameterTypes().length!=length)
throw new IllegalArgumentException(c+" has @DataBoundConstructor but it doesn't match with your .stapler file. Try clean rebuild");
return c;
}
}
// if not, maybe this was from @stapler-constructor,
// so look for the constructor with the expected argument length.
// this is not very reliable.
for (Constructor c : ctrs) {
if(c.getParameterTypes().length==length)
return c;
}
throw new IllegalArgumentException(type+" does not have a constructor with "+length+" arguments");
}
/**
* Determines the constructor parameter names.
*
*
* First, try to load names from the debug information. Otherwise
* if there's the .stapler file, load it as a property file and determines the constructor parameter names.
* Otherwise, look for {@link CapturedParameterNames} annotation.
*/
private String[] loadConstructorParamNames(Class> type) {
Constructor>[] ctrs = type.getConstructors();
// which constructor was data bound?
Constructor> dbc = null;
for (Constructor> c : ctrs) {
if (c.getAnnotation(DataBoundConstructor.class) != null) {
dbc = c;
break;
}
}
if (dbc==null)
throw new NoStaplerConstructorException("There's no @DataBoundConstructor on any constructor of " + type);
String[] names = ClassDescriptor.loadParameterNames(dbc);
if (names.length==dbc.getParameterTypes().length)
return names;
String resourceName = type.getName().replace('.', '/').replace('$','/') + ".stapler";
ClassLoader cl = type.getClassLoader();
if(cl==null)
throw new NoStaplerConstructorException(type+" is a built-in type");
InputStream s = cl.getResourceAsStream(resourceName);
if (s != null) {// load the property file and figure out parameter names
try {
Properties p = new Properties();
p.load(s);
s.close();
String v = p.getProperty("constructor");
if (v.length() == 0) return new String[0];
return v.split(",");
} catch (IOException e) {
throw new IllegalArgumentException("Unable to load " + resourceName, e);
}
}
// no debug info and no stapler file
throw new NoStaplerConstructorException(
"Unable to find " + resourceName + ". " +
"Run 'mvn clean compile' once to run the annotation processor.");
}
private static void fill(Object bean, String key, Object value) {
StringTokenizer tokens = new StringTokenizer(key);
while(tokens.hasMoreTokens()) {
String token = tokens.nextToken();
boolean last = !tokens.hasMoreTokens(); // is this the last token?
try {
if(last) {
copyProperty(bean,token,value);
} else {
bean = BeanUtils.getProperty(bean,token);
}
} catch (IllegalAccessException x) {
throw new IllegalAccessError(x.getMessage());
} catch (InvocationTargetException x) {
Throwable e = x.getTargetException();
if(e instanceof RuntimeException)
throw (RuntimeException)e;
if(e instanceof Error)
throw (Error)e;
throw new RuntimeException(x);
} catch (NoSuchMethodException e) {
// ignore if there's no such property
}
}
}
/**
* Information about the type.
*/
private final class TypePair {
final Type genericType;
/**
* Erasure of {@link #genericType}
*/
final Class type;
TypePair(Type genericType, Class type) {
this.genericType = genericType;
this.type = type;
}
TypePair(Field f) {
this(f.getGenericType(),f.getType());
}
/**
* Converts the given JSON object (either {@link JSONObject}, {@link JSONArray}, or other primitive types
* in JSON, to the type represented by the 'this' object.
*/
public Object convertJSON(Object o) {
Object r = bindInterceptor.onConvert(genericType, type, o);
if (r!= BindInterceptor.DEFAULT) return r; // taken over by the listener
if(o==null) {
// this method returns null if the type is not primitive, which works.
return ReflectionUtils.getVmDefaultValueFor(type);
}
if (type==JSONArray.class) {
if (o instanceof JSONArray) return o;
JSONArray a = new JSONArray();
a.add(o);
return a;
}
Lister l = Lister.create(type,genericType);
if (o instanceof JSONObject) {
JSONObject j = (JSONObject) o;
if (j.isNullObject()) // another flavor of null. json-lib sucks.
return ReflectionUtils.getVmDefaultValueFor(type);
if(l==null) {// single value conversion
try {
Class actualType = type;
if(j.has("stapler-class")) {
// sub-type is specified in JSON.
// note that this can come from malicious clients, so we need to make sure we don't have security issues.
ClassLoader cl = stapler.getWebApp().getClassLoader();
String className = j.getString("stapler-class");
try {
Class> subType = cl.loadClass(className);
if(!actualType.isAssignableFrom(subType))
throw new IllegalArgumentException("Specified type "+subType+" is not assignable to the expected "+actualType);
actualType = (Class)subType; // I'm being lazy here
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Class "+className+" is specified in JSON, but no such class found in "+cl,e);
}
}
if (actualType==JSONObject.class || actualType==JSON.class) return actualType.cast(j);
String[] names = loadConstructorParamNames(actualType);
// the actual arguments to invoke the constructor with.
Object[] args = new Object[names.length];
// constructor
Constructor c = findConstructor(actualType, names.length);
Class[] types = c.getParameterTypes();
Type[] genTypes = c.getGenericParameterTypes();
// convert parameters
for( int i=0; i e : (Set>)j.entrySet()) {
Object v = e.getValue();
String className = e.getKey().replace('-','.'); // decode JSON-safe class name escaping
try {
Class> itemType = cl.loadClass(className);
if (v instanceof JSONObject) {
l.add(bindJSON(itemType, (JSONObject) v));
}
if (v instanceof JSONArray) {
for(Object i : bindJSONToList(itemType, (JSONArray) v))
l.add(i);
}
} catch (ClassNotFoundException e1) {
// ignore unrecognized element
}
}
} else if (Enum.class.isAssignableFrom(l.itemType)) {
// this is a hash of element names as enum constant names
for (Map.Entry e : (Set>)j.entrySet()) {
Object v = e.getValue();
if (v==null || (v instanceof Boolean && !(Boolean)v))
continue; // skip if the value is null or false
l.add(Enum.valueOf(l.itemType,e.getKey()));
}
} else {
// only one value given to the collection
l.add(new TypePair(l.itemGenericType,l.itemType).convertJSON(j));
}
return l.toCollection();
}
}
if (o instanceof JSONArray) {
JSONArray a = (JSONArray) o;
TypePair itemType = new TypePair(l.itemGenericType,l.itemType);
for (Object item : a)
l.add(itemType.convertJSON(item));
return l.toCollection();
}
if(Enum.class.isAssignableFrom(type))
return Enum.valueOf(type,o.toString());
if (l==null) {// single value conversion
Converter converter = Stapler.lookupConverter(type);
if (converter==null)
throw new IllegalArgumentException("Unable to convert to "+type);
return converter.convert(type,o);
} else {// single value in a collection
Converter converter = Stapler.lookupConverter(l.itemType);
if (converter==null)
throw new IllegalArgumentException("Unable to convert to "+type);
l.add(converter.convert(type,o));
return l.toCollection();
}
}
}
/**
* Gets the type of the field/property designate by the given name.
*/
private TypePair getPropertyType(Object bean, String name) throws IllegalAccessException, InvocationTargetException {
try {
PropertyDescriptor propDescriptor = PropertyUtils.getPropertyDescriptor(bean, name);
if(propDescriptor!=null) {
Method m = propDescriptor.getWriteMethod();
if(m!=null)
return new TypePair(m.getGenericParameterTypes()[0], m.getParameterTypes()[0]);
}
} catch (NoSuchMethodException e) {
// no such property
}
// try a field
try {
return new TypePair(bean.getClass().getField(name));
} catch (NoSuchFieldException e) {
// no such field
}
return null;
}
/**
* Sets the property/field value of the given name, by performing a value type conversion if necessary.
*/
private static void copyProperty(Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException {
PropertyDescriptor propDescriptor;
try {
propDescriptor =
PropertyUtils.getPropertyDescriptor(bean, name);
} catch (NoSuchMethodException e) {
propDescriptor = null;
}
if ((propDescriptor != null) &&
(propDescriptor.getWriteMethod() == null)) {
propDescriptor = null;
}
if (propDescriptor != null) {
Converter converter = Stapler.lookupConverter(propDescriptor.getPropertyType());
if (converter != null)
value = converter.convert(propDescriptor.getPropertyType(), value);
try {
PropertyUtils.setSimpleProperty(bean, name, value);
} catch (NoSuchMethodException e) {
throw new NoSuchMethodError(e.getMessage());
}
return;
}
// try a field
try {
Field field = bean.getClass().getField(name);
Converter converter = ConvertUtils.lookup(field.getType());
if (converter != null)
value = converter.convert(field.getType(), value);
field.set(bean,value);
} catch (NoSuchFieldException e) {
// no such field
}
}
private void parseMultipartFormData() throws ServletException {
if(parsedFormData!=null) return;
parsedFormData = new HashMap();
ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory());
try {
for( FileItem fi : (List)upload.parseRequest(this) )
parsedFormData.put(fi.getFieldName(),fi);
} catch (FileUploadException e) {
throw new ServletException(e);
}
}
public JSONObject getSubmittedForm() throws ServletException {
if(structuredForm==null) {
String ct = getContentType();
String p = null;
boolean isSubmission; // for error diagnosis, if something is submitted, set to true
if(ct!=null && ct.startsWith("multipart/")) {
isSubmission=true;
parseMultipartFormData();
FileItem item = parsedFormData.get("json");
if(item!=null)
p = item.getString();
} else {
p = getParameter("json");
isSubmission = !getParameterMap().isEmpty();
}
if(p==null) {
// no data submitted
try {
StaplerResponse rsp = Stapler.getCurrentResponse();
if(isSubmission)
rsp.sendError(SC_BAD_REQUEST,"This page expects a form submission");
else
rsp.sendError(SC_BAD_REQUEST,"Nothing is submitted");
rsp.getWriter().close();
throw new Error("This page expects a form submission");
} catch (IOException e) {
throw new Error(e);
}
}
structuredForm = JSONObject.fromObject(p);
}
return structuredForm;
}
public FileItem getFileItem(String name) throws ServletException, IOException {
parseMultipartFormData();
if(parsedFormData==null) return null;
FileItem item = parsedFormData.get(name);
if(item==null || item.isFormField()) return null;
return item;
}
}