I recognize this might be an XY Problem, so I'll explain the context in case there's a better solution further up the chain than what I'm trying to do.
I am trying to add the ability to access a class's static methods from within EL expressions in a JSP.
My solution so far is to instantiate the specified class and put it on the page (or specified) scope. Since you can access static methods from an instance, it seemed like the simplest and most straightforward solution. I have written a tag class for this purpose, StaticMethodAccessTag
, and added it to one of our tld's as constants:staticMethodAccess
.
The problem with this solution is that the non-static methods can also be called, with all the potential for problems and abuse that that means.
Is there a way to use reflection to instantiate an instance of a class, while preventing (eg by throwing an exception) any of the non-static methods from being called?
Or better yet, is there a way to "call" any arbitrarily-named and -parametered method through EL and have one java method handle all those calls? Something like ${MyStaticUtil.foo(bar1, bar2)}
and ${MyStaticUtil.baz(bar1)}
where both would come into StaticMethodAccessTag.processArbitraryMethod(String methodName, Object... args)
which could then use reflection to call the appropriate static method on MyStaticUtil
.
Or better yet, is there a way to call static methods in EL without needing an instance of a class to call it through?
StaticMethodAccessTag.java:
import java.lang.reflect.Constructor;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import org.apache.log4j.Logger;
public class StaticMethodAccessTag extends SimpleTagSupport {
private static Logger log = Logger.getLogger(StaticMethodAccessTag.class);
private String varName;
private String className;
private String scope;
private Object[] args;
@Override
public void doTag() throws JspException {
try {
if (scope == null || "".equals(scope)) {
scope = "page";
}
int requestedScope = TagUtils.getScope(scope);
Class<?> declaringClass = Class.forName(this.className);
Object instance = null;
Class<?>[] argTypes = null;
if (args == null || args.length == 0) {
// no args; use default constructor
instance = declaringClass.newInstance();
} else {
// args provided, find the matching constructor and use it
argTypes = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] == null) {
// unknown type, assume Object. This can lead to ambiguity when picking a constructor.
argTypes[i] = Object.class;
} else {
argTypes[i] = args[i].getClass();
}
}
// check each constructor
Constructor<?> matchedConstructor = null;
nextConstructor: for (Constructor<?> constructor : declaringClass.getConstructors()) {
Class<?>[] expectedTypes = constructor.getParameterTypes();
// check the provided arguments against the current constructor
if (expectedTypes == null || expectedTypes.length != args.length) {
// mismatch; move on to the next constructor
continue;
}
for (int i = 0; i < argTypes.length; i++) {
if (!argTypes[i].isAssignableFrom(expectedTypes[i])) {
// mismatch; move on to the next constructor
continue nextConstructor;
}
}
// if another match was already found, that means we have ambiguous arguments.
if (matchedConstructor != null) {
outputArgs(argTypes);
throw new Exception("Given the provided arguments, there are multiple eligible constructors for " + declaringClass.getName());
}
matchedConstructor = constructor;
}
if (matchedConstructor != null) {
instance = matchedConstructor.newInstance(args);
}
}
if (instance == null) {
outputArgs(argTypes);
throw new NullPointerException("Failed to find a matching constructor for the provided args for " + this.className + ".");
}
getJspContext().setAttribute(varName, instance, requestedScope);
} catch (Exception e) {
// TODO output jsp name, other helpful information, if possible.
throw new JspException("Exception setting up static method access for " + this.className, e);
}
}
private void outputArgs(Class<?>[] argTypes) {
log.debug("Provided " + args.length + " arguments: ");
for (int i = 0; i < argTypes.length; i++) {
String argStr = "null";
if (args[i] != null) {
argStr = args[i].toString();
}
log.debug("[" + i + "] expected type: " + argTypes[i].getName() + " toString: " + argStr);
}
}
public String getVar() {
return varName;
}
public void setVar(String varName) {
this.varName = varName;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object... args) {
this.args = args;
}
}
Tag definition in constants.tld:
<tag>
<name>staticMethodAccess</name>
<tagclass>foopackage.presentation.resource.tag.StaticMethodAccessTag</tagclass>
<bodycontent>empty</bodycontent>
<attribute>
<name>className</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<name>var</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>scope</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<name>args</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
Example usage in a JSP:
<%@ taglib prefix="constants" uri="/constants" %>
<%-- debug, delete this block --%>
<constants:staticMethodAccess var="MyJSPUtils" className="foopackage.presentation.resource.tag.MyJSPUtils"/>
<c:forEach items="${MyJSPUtils.args('one', 'two', 'three')}" var="v">
${v};
</c:forEach>
<constants:staticMethodAccess var="type1" className="foopackage.integration.value.Type" args="${jspUtils.args(DBConstants.STATUS_TYPE_ID_ACTIVE)}"/>
<constants:staticMethodAccess var="type2" className="foopackage.integration.value.Type" args="${jspUtils.args(DBConstants.STATUS_TYPE_ID_ACTIVE, 'Active')}"/>
<constants:staticMethodAccess var="type3" className="foopackage.integration.value.Type" args="${jspUtils.args(type1)}"/>
<constants:staticMethodAccess var="type4" className="foopackage.integration.value.Type" args="${jspUtils.args(DBConstants.STATUS_TYPE_ID_ACTIVE, 'Active', 'desc')}"/>
<constants:staticMethodAccess var="type5" className="foopackage.integration.value.Type" args="${jspUtils.args(DBConstants.STATUS_TYPE_ID_ACTIVE, null, 'desc')}"/>
<constants:staticMethodAccess var="type3" className="foopackage.integration.value.Type" args="${jspUtils.args(null)}"/><%-- this line will throw an error because it matches 2 possible constructors --%>
<br/>${type1.id} ${type1.name} ${type1.description}
<br/>${type2.id} ${type2.name} ${type2.description}
<br/>${type3.id} ${type3.name} ${type3.description}
<br/>${type4.id} ${type4.name} ${type4.description}
<br/>${type5.id} ${type5.name} ${type5.description}
<br/>
<%-- end debug block --%>