VIVO-120 improve the error messages from OperationUtils.cloneBean()

Add unit tests for the method and restructure the logic so a failure to create the clone is treated as seriously as a failure to load it with values. Previously  a failure to create resulted in a null return (with a log message) while a failure to load resulted in an exception (with a log message).
This commit is contained in:
j2blake 2013-06-05 15:37:05 -04:00
parent 9ec06f224e
commit a19227188b
2 changed files with 574 additions and 137 deletions

View file

@ -1,7 +1,7 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */ /* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vedit.util; package edu.cornell.mannlib.vedit.util;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@ -9,139 +9,145 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vedit.beans.EditProcessObject; import edu.cornell.mannlib.vedit.beans.EditProcessObject;
public class OperationUtils {
public class OperationUtils{
private static final Log log = LogFactory.getLog(OperationUtils.class
private static final Log log = LogFactory.getLog(OperationUtils.class.getName()); .getName());
public static void beanSetAndValidate(Object newObj, String field, String value, EditProcessObject epo){ public static void beanSetAndValidate(Object newObj, String field,
Class cls = (epo.getBeanClass() != null) ? epo.getBeanClass() : newObj.getClass(); String value, EditProcessObject epo) {
Class[] paramList = new Class[1]; Class<?> cls = (epo.getBeanClass() != null) ? epo.getBeanClass() : newObj
paramList[0] = String.class; .getClass();
boolean isInt = false; Class<?>[] paramList = new Class[1];
boolean isBoolean = false; paramList[0] = String.class;
Method setterMethod = null; boolean isInt = false;
try { boolean isBoolean = false;
setterMethod = cls.getMethod("set"+field,paramList); Method setterMethod = null;
} catch (NoSuchMethodException e) { try {
//let's try int setterMethod = cls.getMethod("set" + field, paramList);
paramList[0] = int.class; } catch (NoSuchMethodException e) {
try { // let's try int
setterMethod = cls.getMethod("set"+field,paramList); paramList[0] = int.class;
isInt = true; try {
} catch (NoSuchMethodException f) { setterMethod = cls.getMethod("set" + field, paramList);
//let's try boolean isInt = true;
paramList[0]=boolean.class; } catch (NoSuchMethodException f) {
try { // let's try boolean
setterMethod = cls.getMethod("set"+field,paramList); paramList[0] = boolean.class;
isBoolean = true; try {
log.debug("found boolean field "+field); setterMethod = cls.getMethod("set" + field, paramList);
} catch (NoSuchMethodException g) { isBoolean = true;
log.error("beanSet could not find an appropriate String, int, or boolean setter method for "+field); log.debug("found boolean field " + field);
} } catch (NoSuchMethodException g) {
log.error("beanSet could not find an appropriate String, int, or boolean setter method for "
} + field);
} }
Object[] arglist = new Object[1];
if (isInt) }
arglist[0] = Integer.decode(value); }
else if (isBoolean) Object[] arglist = new Object[1];
arglist[0] = (value.equalsIgnoreCase("TRUE")); if (isInt)
else arglist[0] = Integer.decode(value);
arglist[0] = value; else if (isBoolean)
try { arglist[0] = (value.equalsIgnoreCase("TRUE"));
setterMethod.invoke(newObj,arglist); else
} catch (Exception e) { arglist[0] = value;
log.error("Couldn't invoke method"); try {
log.error(e.getMessage()); setterMethod.invoke(newObj, arglist);
log.error(field+" "+arglist[0]); } catch (Exception e) {
} log.error("Couldn't invoke method");
} log.error(e.getMessage());
log.error(field + " " + arglist[0]);
/** }
* Takes a bean and clones it using reflection. }
* Any fields without standard getter/setter methods will not be copied.
* @param bean /**
* @return * Takes a bean and clones it using reflection. Any fields without standard
*/ * getter/setter methods will not be copied.
public static Object cloneBean (Object bean) { */
return cloneBean(bean, bean.getClass(), bean.getClass()); public static Object cloneBean(Object bean) {
} if (bean == null) {
throw new NullPointerException("bean may not be null.");
/** }
* Takes a bean and clones it using reflection. return cloneBean(bean, bean.getClass(), bean.getClass());
* Any fields without standard getter/setter methods will not be copied. }
* @param bean
* @return /**
*/ * Takes a bean and clones it using reflection. Any fields without standard
public static Object cloneBean (Object bean, Class beanClass, Class iface){ * getter/setter methods will not be copied.
Object newBean = null; */
try { public static Object cloneBean(final Object bean, final Class<?> beanClass,
newBean = beanClass.newInstance(); final Class<?> iface) {
Method[] beanMeths = iface.getMethods(); if (bean == null) {
for (int i=0; i<beanMeths.length ; ++i) { throw new NullPointerException("bean may not be null.");
Method beanMeth = beanMeths[i]; }
String methName = beanMeth.getName(); if (beanClass == null) {
if (methName.startsWith("get") throw new NullPointerException("beanClass may not be null.");
&& beanMeth.getParameterTypes().length == 0 ) { }
String fieldName = methName.substring(3,methName.length()); if (iface == null) {
Class returnType = beanMeth.getReturnType(); throw new NullPointerException("iface may not be null.");
try { }
Class[] args = new Class[1];
args[0] = returnType; class CloneBeanException extends RuntimeException {
Method setterMethod = iface.getMethod("set"+fieldName,args); public CloneBeanException(String message, Throwable cause) {
try { super(message + " <" + cause.getClass().getSimpleName()
Object fieldVal = beanMeth.invoke(bean,(Object[])null); + ">: bean=" + bean + ", beanClass="
try { + beanClass.getName() + ", iface=" + iface.getName(),
Object[] setArgs = new Object[1]; cause);
setArgs[0] = fieldVal; }
setterMethod.invoke(newBean,setArgs); }
} catch (IllegalAccessException iae) {
log.error(OperationUtils.class.getName() + Object newBean;
".cloneBean() " + try {
" encountered IllegalAccessException " + newBean = beanClass.getConstructor().newInstance();
" invoking " + } catch (NoSuchMethodException e) {
setterMethod.getName(), iae); throw new CloneBeanException("bean has no 'nullary' constructor.", e);
throw new RuntimeException(iae); } catch (InstantiationException e) {
} catch (InvocationTargetException ite) { throw new CloneBeanException("tried to create instance of an abstract class.", e);
log.error(OperationUtils.class.getName() + } catch (IllegalAccessException e) {
".cloneBean() " + throw new CloneBeanException("bean constructor is not accessible.", e);
" encountered InvocationTargetException" } catch (InvocationTargetException e) {
+ " invoking " throw new CloneBeanException("bean constructor threw an exception.", e);
+ setterMethod.getName(), ite); } catch (Exception e) {
throw new RuntimeException(ite); throw new CloneBeanException("failed to instantiate a new bean.", e);
} }
} catch (IllegalAccessException iae) {
log.error(OperationUtils.class.getName() + for (Method beanMeth : iface.getMethods()) {
".cloneBean() encountered " + String methName = beanMeth.getName();
" IllegalAccessException invoking " + if (!methName.startsWith("get")) {
beanMeths[i].getName(), iae); continue;
throw new RuntimeException(iae); }
} catch (InvocationTargetException ite) { if (beanMeth.getParameterTypes().length != 0) {
log.error(OperationUtils.class.getName() + continue;
".cloneBean() encountered " + }
" InvocationTargetException invoking " + String fieldName = methName.substring(3, methName.length());
beanMeths[i].getName(), ite); Class<?> returnType = beanMeth.getReturnType();
throw new RuntimeException(ite);
} catch (IllegalArgumentException iae) { Method setterMethod;
log.error(OperationUtils.class.getName() + try {
".cloneBean() encountered " + setterMethod = iface.getMethod("set" + fieldName, returnType);
" IllegalArgumentException invoking " + } catch (NoSuchMethodException nsme) {
beanMeths[i].getName(), iae); continue;
throw new RuntimeException(iae); }
}
} catch (NoSuchMethodException nsme){ Object fieldVal;
// ignore this field because there is no setter method try {
} fieldVal = beanMeth.invoke(bean, (Object[]) null);
} } catch (Exception e) {
} throw new CloneBeanException("failed to invoke " + beanMeth, e);
} catch (InstantiationException ie){ }
log.error("edu.cornell.mannlib.vitro.edit.utils.OperationUtils.cloneBean("+bean.getClass().toString()+") could not instantiate new instance of bean.", ie);
} catch (IllegalAccessException iae){ try {
log.error("edu.cornell.mannlib.vitro.edit.utils.OperationUtils.cloneBean("+bean.getClass().toString()+") encountered illegal access exception instantiating new bean.", iae); Object[] setArgs = new Object[1];
} setArgs[0] = fieldVal;
return newBean; setterMethod.invoke(newBean, setArgs);
} } catch (Exception e) {
throw new CloneBeanException(
"failed to invoke " + setterMethod, e);
}
}
return newBean;
}
} }

View file

@ -0,0 +1,431 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vedit.util;
import static org.junit.Assert.assertEquals;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import edu.cornell.mannlib.vitro.testing.AbstractTestClass;
/**
* All of these tests are for OperationUtils.cloneBean()
*/
public class OperationUtils_CloneBeanTest extends AbstractTestClass {
// ----------------------------------------------------------------------
// Allow the tests to expect a RuntimeException with a particular cause.
// ----------------------------------------------------------------------
@Rule
public ExpectedException thrown = ExpectedException.none();
Matcher<?> causedBy(Class<? extends Throwable> causeClass) {
return new CausedByMatcher(causeClass);
}
class CausedByMatcher extends BaseMatcher<Throwable> {
private final Class<? extends Throwable> causeClass;
public CausedByMatcher(Class<? extends Throwable> causeClass) {
this.causeClass = causeClass;
}
@Override
public boolean matches(Object actualThrowable) {
if (!(actualThrowable instanceof RuntimeException)) {
return false;
}
Throwable cause = ((RuntimeException) actualThrowable).getCause();
return causeClass.isInstance(cause);
}
@Override
public void describeTo(Description d) {
d.appendText("RuntimeException caused by " + causeClass.getName());
}
}
// ----------------------------------------------------------------------
// Test for invalid classes
// ----------------------------------------------------------------------
@Test(expected = NullPointerException.class)
public void nullArgument() {
OperationUtils.cloneBean(null);
}
@Test(expected = NullPointerException.class)
public void nullBean() {
OperationUtils
.cloneBean(null, SimpleSuccess.class, SimpleSuccess.class);
}
@Test(expected = NullPointerException.class)
public void nullBeanClass() {
OperationUtils
.cloneBean(new SimpleSuccess(), null, SimpleSuccess.class);
}
@Test(expected = NullPointerException.class)
public void nullInterfaceClass() {
OperationUtils
.cloneBean(new SimpleSuccess(), SimpleSuccess.class, null);
}
@Test(expected = IllegalAccessException.class)
@Ignore("Why doesn't this throw an exception?")
public void privateClass() {
OperationUtils.cloneBean(new PrivateClass());
}
@Test
public void privateConstructor() {
thrown.expect(causedBy(NoSuchMethodException.class));
OperationUtils.cloneBean(new PrivateConstructor());
}
@Test
public void abstractClass() {
thrown.expect(causedBy(InstantiationException.class));
OperationUtils.cloneBean(new ConcreteOfAbstractClass(),
AbstractClass.class, AbstractClass.class);
}
@Test
public void interfaceClass() {
thrown.expect(causedBy(InstantiationException.class));
OperationUtils.cloneBean(new ConcreteOfInterfaceClass(),
InterfaceClass.class, InterfaceClass.class);
}
@Test
public void arrayClass() {
thrown.expect(causedBy(NoSuchMethodException.class));
OperationUtils.cloneBean(new String[0]);
}
@Test
public void primitiveTypeClass() {
thrown.expect(causedBy(NoSuchMethodException.class));
OperationUtils.cloneBean(1, Integer.TYPE, Integer.TYPE);
}
@Test
public void voidClass() {
thrown.expect(causedBy(NoSuchMethodException.class));
OperationUtils.cloneBean(new Object(), Void.TYPE, Void.TYPE);
}
@Test
public void noNullaryConstructor() {
thrown.expect(causedBy(NoSuchMethodException.class));
OperationUtils.cloneBean(new NoNullaryConstructor(1));
}
@Test(expected = ExceptionInInitializerError.class)
public void classThrowsExceptionWhenLoaded() {
OperationUtils.cloneBean(new ThrowsExceptionWhenLoaded());
}
@Test
public void initializerThrowsException() {
thrown.expect(causedBy(InvocationTargetException.class));
OperationUtils.cloneBean("random object",
InitializerThrowsException.class,
InitializerThrowsException.class);
}
@Test
public void wrongInterfaceClass() {
thrown.expect(causedBy(IllegalArgumentException.class));
OperationUtils.cloneBean(new WrongConcreteClass(),
WrongConcreteClass.class, WrongInterface.class);
}
@Test
public void getThrowsException() {
thrown.expect(causedBy(InvocationTargetException.class));
OperationUtils.cloneBean(new GetMethodThrowsException());
}
@Test
public void setThrowsException() {
thrown.expect(causedBy(InvocationTargetException.class));
OperationUtils.cloneBean(new SetMethodThrowsException());
}
private static class PrivateClass {
public PrivateClass() {
}
}
private static class PrivateConstructor {
private PrivateConstructor() {
}
}
public abstract static class AbstractClass {
public AbstractClass() {
}
}
public static class ConcreteOfAbstractClass extends AbstractClass {
public ConcreteOfAbstractClass() {
}
}
public abstract static class InterfaceClass {
public InterfaceClass() {
}
}
public static class ConcreteOfInterfaceClass extends InterfaceClass {
public ConcreteOfInterfaceClass() {
}
}
public static class NoNullaryConstructor {
@SuppressWarnings("unused")
public NoNullaryConstructor(int i) {
// nothing to do
}
}
public static class ThrowsExceptionWhenLoaded {
static {
if (true)
throw new IllegalArgumentException();
}
}
public static class InitializerThrowsException {
{
if (true)
throw new IllegalStateException("Initializer throws exception");
}
}
public static class WrongConcreteClass {
private String junk = "junk";
public String getJunk() {
return this.junk;
}
@SuppressWarnings("unused")
private void setJunk(String junk) {
this.junk = junk;
}
}
public static interface WrongInterface {
String getJunk();
void setJunk(String junk);
}
public static class GetMethodThrowsException {
@SuppressWarnings("unused")
private String junk = "junk";
public String getJunk() {
throw new UnsupportedOperationException();
}
public void setJunk(String junk) {
this.junk = junk;
}
}
public static class SetMethodThrowsException {
private String junk = "junk";
public String getJunk() {
return this.junk;
}
@SuppressWarnings("unused")
public void setJunk(String junk) {
throw new UnsupportedOperationException();
}
}
// ----------------------------------------------------------------------
// Test simple success and innocuous variations
// ----------------------------------------------------------------------
@Test
public void simpleSuccess() {
expectSuccess(new SimpleSuccess().insertField("label", "a prize"));
}
@Test
public void getButNoSet() {
expectSuccess(new GetButNoSet().insertField("label", "shouldBeEqual"));
}
@Test
public void getTakesParameters() {
expectSuccess(new GetTakesParameters().insertField("label", "fine"));
}
@Test
public void getReturnsVoid() {
expectSuccess(new GetReturnsVoid().insertField("label", "fine"));
}
@Test
public void getAndSetDontMatch() {
expectSuccess(new GetAndSetDontMatch().insertField("label", "fine"));
}
@Test
public void getIsStatic() {
expectSuccess(new GetIsStatic().insertField("label", "fine")
.insertField("instanceJunk", "the junk"));
}
@Test
public void getMethodIsPrivate() {
expectSuccess(new GetMethodIsPrivate().insertField("label", "fine"));
}
@Test
public void setMethodIsPrivate() {
expectSuccess(new SetMethodIsPrivate().insertField("label", "fine"));
}
private void expectSuccess(BeanBase original) {
BeanBase cloned = (BeanBase) OperationUtils.cloneBean(original);
assertEquals("Simple success", original, cloned);
}
public static abstract class BeanBase {
protected final Map<String, Object> fields = new HashMap<>();
public BeanBase insertField(String key, Object value) {
if (value != null) {
fields.put(key, value);
}
return this;
}
@Override
public int hashCode() {
return fields.hashCode();
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null) {
return false;
}
if (!this.getClass().equals(o.getClass())) {
return false;
}
return this.fields.equals(((BeanBase) o).fields);
}
@Override
public String toString() {
return this.getClass().getSimpleName() + fields;
}
}
public static class SimpleSuccess extends BeanBase {
public String getLabel() {
return (String) fields.get("label");
}
public void setLabel(String label) {
insertField("label", label);
}
}
public static class GetButNoSet extends SimpleSuccess {
public String getJunk() {
throw new UnsupportedOperationException();
}
}
public static class GetTakesParameters extends SimpleSuccess {
@SuppressWarnings("unused")
public String getJunk(String why) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unused")
public void setJunk(String junk) {
throw new UnsupportedOperationException();
}
}
public static class GetReturnsVoid extends SimpleSuccess {
public void getJunk() {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unused")
public void setJunk(String junk) {
throw new UnsupportedOperationException();
}
}
public static class GetAndSetDontMatch extends SimpleSuccess {
public String getJunk() {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unused")
public void setJunk(Integer junk) {
throw new UnsupportedOperationException();
}
}
public static class GetIsStatic extends SimpleSuccess {
public static String getJunk() {
return ("the junk");
}
public void setJunk(String junk) {
insertField("instanceJunk", junk);
}
}
public static class GetMethodIsPrivate extends SimpleSuccess {
@SuppressWarnings("unused")
private String getJunk() {
return ("the junk");
}
public void setJunk(String junk) {
insertField("instanceJunk", junk);
}
}
public static class SetMethodIsPrivate extends SimpleSuccess {
public String getJunk() {
return ("the junk");
}
@SuppressWarnings("unused")
private void setJunk(String junk) {
insertField("instanceJunk", junk);
}
}
}