1 4 package com.tc.config.schema; 5 6 import org.apache.commons.lang.StringUtils; 7 import org.apache.xmlbeans.XmlObject; 8 import org.apache.xmlbeans.impl.values.XmlObjectBase; 9 10 import com.tc.config.schema.dynamic.BooleanConfigItem; 11 import com.tc.config.schema.dynamic.ConfigItem; 12 import com.tc.config.schema.dynamic.ConfigItemListener; 13 import com.tc.config.schema.dynamic.FileConfigItem; 14 import com.tc.config.schema.dynamic.IntConfigItem; 15 import com.tc.config.schema.dynamic.ObjectArrayConfigItem; 16 import com.tc.config.schema.dynamic.StringArrayConfigItem; 17 import com.tc.config.schema.dynamic.StringConfigItem; 18 import com.tc.config.schema.dynamic.XPathBasedConfigItem; 19 import com.tc.logging.TCLogger; 20 import com.tc.logging.TCLogging; 21 import com.tc.util.Assert; 22 23 import java.io.File ; 24 import java.lang.reflect.Field ; 25 import java.lang.reflect.InvocationHandler ; 26 import java.lang.reflect.InvocationTargetException ; 27 import java.lang.reflect.Method ; 28 import java.lang.reflect.Modifier ; 29 import java.lang.reflect.Proxy ; 30 import java.util.HashMap ; 31 import java.util.Map ; 32 33 51 public class TestConfigObjectInvocationHandler implements InvocationHandler { 52 53 private static final TCLogger logger = TCLogging.getLogger(TestConfigObjectInvocationHandler.class); 54 55 60 private class OurSettableConfigItem implements SettableConfigItem, BooleanConfigItem, FileConfigItem, IntConfigItem, 61 ObjectArrayConfigItem, StringArrayConfigItem, StringConfigItem { 62 private final String xpath; 63 64 public OurSettableConfigItem(String xpath) { 65 this.xpath = xpath; 66 } 67 68 public synchronized void addListener(ConfigItemListener changeListener) { 69 throw Assert.failure("You can only set values on these items; you shouldn't actually be using them."); 70 } 71 72 public synchronized Object getObject() { 73 throw Assert.failure("You can only set values on these items; you shouldn't actually be using them."); 74 } 75 76 public synchronized void removeListener(ConfigItemListener changeListener) { 77 throw Assert.failure("You can only set values on these items; you shouldn't actually be using them."); 78 } 79 80 public synchronized void setValue(Object newValue) { 81 for (int i = 0; i < beansToSetOn.length; ++i) { 82 logger.debug("Setting value " + newValue + " on XPath '" + xpath + "' for bean " + beansToSetOn[i]); 83 setValueOnBean(newValue, beansToSetOn[i], this.xpath); 84 } 85 } 86 87 private String toMethodName(String xpathComponent) { 88 while (xpathComponent.startsWith("@")) 89 xpathComponent = xpathComponent.substring(1); 90 91 StringBuffer out = new StringBuffer (); 92 char[] in = xpathComponent.toCharArray(); 93 boolean capitalizeNext = true; 94 95 for (int i = 0; i < in.length; ++i) { 96 char ch = in[i]; 97 if (ch == '-') capitalizeNext = true; 98 else { 99 out.append(capitalizeNext ? Character.toUpperCase(ch) : ch); 100 capitalizeNext = false; 101 } 102 } 103 104 return out.toString(); 105 } 106 107 private void setValueOnBean(Object newValue, XmlObject beanToSetOn, String theXPath) { 108 synchronized (beanToSetOn) { 109 if (StringUtils.isBlank(theXPath)) { 110 logger.debug("Setting value " + newValue + " directly on bean " + beanToSetOn + "."); 111 setValueDirectly(newValue, beanToSetOn); 112 } else { 113 String remainingComponents = allButFirstComponentOf(theXPath); 114 String component = firstComponentOf(theXPath); 115 116 if (StringUtils.isBlank(remainingComponents)) { 117 if (setValueAsPropertyDirectly(beanToSetOn, component, newValue, "set")) return; 119 if (setValueAsPropertyDirectly(beanToSetOn, component, newValue, "xset")) return; 120 } 121 122 logger.debug("Looking for property '" + component + "' of bean " + beanToSetOn + "."); 123 XmlObject[] children = beanToSetOn.selectPath(component); 124 125 if (children == null || children.length == 0) { 126 logger.debug("Property '" + component + "' doesn't exist; creating it."); 128 XmlObject newChild = createAndAddNewChild(beanToSetOn, component); 129 children = new XmlObject[] { newChild }; 130 } 131 132 for (int i = 0; i < children.length; ++i) { 133 logger.debug("Setting value " + newValue + " on child " + (i + 1) + " of " + children.length + "."); 134 setValueOnBean(newValue, children[i], remainingComponents); 135 } 136 } 137 } 138 } 139 140 private XmlObject createAndAddNewChild(XmlObject beanToSetOn, String name) { 141 String asPropertyName = toMethodName(name); 142 Class beanClass = beanToSetOn.getClass(); 143 Method creatorMethod = null; 144 String creatorMethodName = "addNew" + asPropertyName; 145 146 try { 147 logger.debug("Trying to create new child for property '" + name + "' on bean " + beanToSetOn + " of " 148 + beanClass + "."); 149 creatorMethod = beanClass.getMethod(creatorMethodName, new Class [0]); 150 logger.debug("Found creator method " + creatorMethod); 151 Assert.eval(XmlObject.class.isAssignableFrom(creatorMethod.getReturnType())); 152 return (XmlObject) creatorMethod.invoke(beanToSetOn, new Object [0]); 153 } catch (NoSuchMethodException nsme) { 154 logger.debug("Didn't find creator method named '" + creatorMethodName + "'"); 156 } catch (IllegalArgumentException iae) { 157 throw Assert.failure("Can't invoke creator method " + creatorMethod, iae); 158 } catch (IllegalAccessException iae) { 159 throw Assert.failure("Can't invoke creator method " + creatorMethod, iae); 160 } catch (InvocationTargetException ite) { 161 throw Assert.failure("Can't invoke creator method " + creatorMethod, ite); 162 } 163 164 Method [] allMethods = beanClass.getMethods(); 165 Method xsetMethod = null; 166 String methodName = "xset" + asPropertyName; 167 String factoryClassName = null; 168 169 try { 170 logger.debug("Looking for method named '" + methodName + "' instead."); 171 for (int i = 0; i < allMethods.length; ++i) { 172 Method thisMethod = allMethods[i]; 173 if (thisMethod.getName().equals(methodName) && thisMethod.getReturnType().equals(Void.TYPE) 174 && thisMethod.getParameterTypes().length == 1 175 && XmlObject.class.isAssignableFrom(thisMethod.getParameterTypes()[0])) { 176 if (xsetMethod != null) { 177 throw Assert.failure("There are multiple '" + methodName + "' methods in " + beanClass + "; one is " 179 + xsetMethod + ", and another is " + thisMethod + "."); 180 } 181 xsetMethod = thisMethod; 182 } 183 } 184 185 if (xsetMethod == null) { 186 throw Assert.failure("There is no method named '" + methodName + "' in " + beanClass 188 + ". If this method does exist, but on a subclass of " + beanClass 189 + ", not on it itself, you may have specified the wrong XPath prefix when you created " 190 + "this invocation handler."); 191 } 192 logger.debug("Found xset...() method: " + xsetMethod); 193 194 Class newObjectClass = xsetMethod.getParameterTypes()[0]; 195 factoryClassName = newObjectClass.getName() + ".Factory"; 196 logger.debug("Creating a new instance of " + newObjectClass + " using factory class '" + factoryClassName 197 + "'."); 198 Class factoryClass = Class.forName(factoryClassName); 199 Method createMethod = factoryClass.getMethod("newInstance", new Class [0]); 200 Assert.eval(XmlObject.class.isAssignableFrom(createMethod.getReturnType())); 201 Assert.eval(Modifier.isStatic(createMethod.getModifiers())); 202 XmlObject out = (XmlObject) createMethod.invoke(null, null); 203 logger.debug("Created new object " + out + "."); 204 xsetMethod.invoke(beanToSetOn, new Object [] { out }); 205 logger.debug("Set new object " + out + " via xset...() method " + xsetMethod + " on bean " + beanToSetOn); 206 return out; 207 } catch (ClassNotFoundException cnfe) { 208 throw Assert.failure("No factory class '" + factoryClassName + "'?", cnfe); 209 } catch (IllegalArgumentException iae) { 210 throw Assert.failure("Can't create instance using factory " + factoryClassName + " and assign using method " 211 + xsetMethod + "?", iae); 212 } catch (IllegalAccessException iae) { 213 throw Assert.failure("Can't create instance using factory " + factoryClassName + " and assign using method " 214 + xsetMethod + "?", iae); 215 } catch (InvocationTargetException ite) { 216 throw Assert.failure("Can't create instance using factory " + factoryClassName + " and assign using method " 217 + xsetMethod + "?", ite); 218 } catch (SecurityException se) { 219 throw Assert.failure("Can't create instance using factory " + factoryClassName + " and assign using method " 220 + xsetMethod + "?", se); 221 } catch (NoSuchMethodException nsme) { 222 throw Assert.failure("Can't create instance using factory " + factoryClassName + " and assign using method " 223 + xsetMethod + "?", nsme); 224 } 225 } 226 227 private boolean assignmentCompatible(Class parameterType, Object actualObject) { 228 if (parameterType.isPrimitive()) { 229 if (parameterType.equals(Boolean.TYPE)) return actualObject instanceof Boolean ; 230 if (parameterType.equals(Character.TYPE)) return actualObject instanceof Character ; 231 if (parameterType.equals(Byte.TYPE)) return actualObject instanceof Byte ; 232 if (parameterType.equals(Short.TYPE)) return actualObject instanceof Short ; 233 if (parameterType.equals(Integer.TYPE)) return actualObject instanceof Integer ; 234 if (parameterType.equals(Long.TYPE)) return actualObject instanceof Long ; 235 if (parameterType.equals(Float.TYPE)) return actualObject instanceof Float ; 236 if (parameterType.equals(Double.TYPE)) return actualObject instanceof Double ; 237 throw Assert.failure("Unknown primitive type " + parameterType); 238 } else { 239 return actualObject == null || parameterType.isInstance(actualObject); 240 } 241 } 242 243 private boolean setValueAsPropertyDirectly(XmlObject onBean, String propertyName, Object newValue, 244 String prefixToTry) { 245 Method [] allMethods = onBean.getClass().getMethods(); 246 String methodName = prefixToTry + toMethodName(propertyName); 247 Method setterMethod = null; 248 249 logger.debug("Looking for '" + methodName + "' method on " + onBean.getClass() + "..."); 250 for (int i = 0; i < allMethods.length; ++i) { 251 if (allMethods[i].getName().equals(methodName) && allMethods[i].getParameterTypes().length == 1) { 252 if (setterMethod != null) { 253 logger.debug("There are multiple '" + methodName + "' methods in " + onBean.getClass() + "; one is " 255 + setterMethod + ", and another is " + allMethods[i] + ". Returning false."); 256 return false; 257 } 258 259 setterMethod = allMethods[i]; 260 } 261 } 262 263 if (setterMethod == null) { 264 logger.debug("Can't find '" + methodName + "' method with one argument on " + onBean.getClass() 265 + "; returning false."); 266 return false; 267 } 268 269 if (newValue == null && setterMethod.getParameterTypes()[0].isPrimitive()) { 270 logger.debug("Unable to set null as a value on XML bean " + onBean + "; it requires a(n) " 272 + setterMethod.getParameterTypes()[0] + ", which is a primitive type. Returning false."); 273 return false; 274 } 275 276 if (newValue != null && (!assignmentCompatible(setterMethod.getParameterTypes()[0], newValue))) { 277 logger.debug("Unable to set value " + newValue + " on XML bean " + onBean + " via method " + setterMethod 279 + "; it requires an object of " + setterMethod.getParameterTypes()[0] + ", and this value is of " 280 + newValue.getClass() + " instead. Returning false."); 281 return false; 282 } 283 284 try { 285 logger.debug("Setting value " + newValue + " on " + onBean + " via method " + setterMethod + "."); 286 setterMethod.invoke(onBean, new Object [] { newValue }); 287 return true; 288 } catch (IllegalArgumentException iae) { 289 throw Assert.failure("Unable to invoke " + setterMethod, iae); 290 } catch (IllegalAccessException iae) { 291 throw Assert.failure("Unable to invoke " + setterMethod, iae); 292 } catch (InvocationTargetException ite) { 293 throw Assert.failure("Unable to invoke " + setterMethod, ite); 294 } 295 } 296 297 private void setValueDirectly(Object newValue, XmlObject onBean) { 298 Method [] allMethods = onBean.getClass().getMethods(); 299 Method setterMethod = null; 300 boolean isArray = newValue != null && newValue.getClass().isArray(); 301 String searchDescrip = isArray ? "set...Array(...[])" : "set(...)"; 302 303 logger.debug("Looking for " + searchDescrip + " method on " + onBean.getClass() + "..."); 304 for (int i = 0; i < allMethods.length; ++i) { 305 if (isArray) { 306 if (allMethods[i].getName().startsWith("set") && allMethods[i].getName().endsWith("Array") 307 && allMethods[i].getParameterTypes().length == 1 && allMethods[i].getParameterTypes()[0].isArray()) { 308 if (setterMethod != null) { 309 throw Assert.failure("There are multiple '" + searchDescrip + "' methods in " + onBean.getClass() 311 + "; one is " + setterMethod + ", and another is " + allMethods[i]); 312 } 313 314 setterMethod = allMethods[i]; 315 } 316 } else { 317 Class declaringClass = allMethods[i].getDeclaringClass(); 318 if (declaringClass.equals(XmlObject.class) || declaringClass.equals(XmlObjectBase.class)) continue; 319 320 if (allMethods[i].getName().equals("set") && allMethods[i].getParameterTypes().length == 1) { 321 if (setterMethod != null) { 322 throw Assert.failure("There are multiple '" + searchDescrip + "' methods in " + onBean.getClass() 324 + "; one is " + setterMethod + ", and another is " + allMethods[i]); 325 } 326 327 setterMethod = allMethods[i]; 328 } 329 } 330 } 331 332 if (setterMethod == null) throw Assert.failure("Can't find '" + searchDescrip + "' method with one argument on " 333 + onBean.getClass() + "."); 334 335 if (newValue == null && setterMethod.getParameterTypes()[0].isPrimitive()) { 336 throw Assert.failure("You can't set null as a value on XML bean " + onBean + "; it requires a(n) " 338 + setterMethod.getParameterTypes()[0] + ", which is a primitive type."); 339 } 340 341 if (newValue != null && (!setterMethod.getParameterTypes()[0].isInstance(newValue))) { 342 throw Assert.failure("You can't set value " + newValue + " on XML bean " + onBean + " via method " 344 + setterMethod + "; it requires an object of " + setterMethod.getParameterTypes()[0] 345 + ", and your value is of " + newValue.getClass() + " instead."); 346 } 347 348 try { 349 logger.debug("Setting value " + newValue + " on " + onBean + " via method " + setterMethod + "."); 350 setterMethod.invoke(onBean, new Object [] { newValue }); 351 } catch (IllegalArgumentException iae) { 352 throw Assert.failure("Unable to invoke " + setterMethod, iae); 353 } catch (IllegalAccessException iae) { 354 throw Assert.failure("Unable to invoke " + setterMethod, iae); 355 } catch (InvocationTargetException ite) { 356 throw Assert.failure("Unable to invoke " + setterMethod, ite); 357 } 358 } 359 360 private String firstComponentOf(String theXPath) { 361 while (theXPath.startsWith("/")) 362 theXPath = theXPath.substring(1); 363 int index = theXPath.indexOf("/"); 364 if (index < 0) return theXPath; 365 Assert.eval(index > 0); 366 String out = theXPath.substring(0, index); 367 while (out.startsWith("@")) 368 out = out.substring(1); 369 return out; 370 } 371 372 private String allButFirstComponentOf(String theXPath) { 373 while (theXPath.startsWith("/")) 374 theXPath = theXPath.substring(1); 375 int index = theXPath.indexOf("/"); 376 if (index < 0) return null; 377 Assert.eval(index > 0); 378 return theXPath.substring(index + 1); 379 } 380 381 public void setValue(boolean newValue) { 382 setValue(new Boolean (newValue)); 383 } 384 385 public void setValue(int newValue) { 386 setValue(new Integer (newValue)); 387 } 388 389 public boolean getBoolean() { 390 return ((Boolean ) getObject()).booleanValue(); 391 } 392 393 public String getString() { 394 return (String ) getObject(); 395 } 396 397 public String [] getStringArray() { 398 return (String []) getObject(); 399 } 400 401 public File getFile() { 402 return (File ) getObject(); 403 } 404 405 public int getInt() { 406 return ((Integer ) getObject()).intValue(); 407 } 408 409 public Object [] getObjects() { 410 return (Object []) getObject(); 411 } 412 } 413 414 private final Class theInterface; 415 private final XmlObject[] beansToSetOn; 416 private final Object realImplementation; 417 private final String xpathPrefix; 418 419 private final Map configItems; 420 421 public TestConfigObjectInvocationHandler(Class theInterface, XmlObject[] beansToSetOn, Object realImplementation, 426 String xpathPrefix) { 427 Assert.assertNotNull(theInterface); 428 Assert.assertNoNullElements(beansToSetOn); 429 Assert.assertNotNull(realImplementation); 430 431 Assert.eval(theInterface.isInstance(realImplementation)); 432 433 this.theInterface = theInterface; 434 this.beansToSetOn = beansToSetOn; 435 this.realImplementation = realImplementation; 436 this.xpathPrefix = xpathPrefix; 437 438 this.configItems = new HashMap (); 439 } 440 441 public Object invoke(Object proxy, Method method, Object [] args) throws Throwable { 442 Assert.assertNotNull(proxy); 443 Assert.assertNotNull(method); 444 445 if (ConfigItem.class.isAssignableFrom(method.getReturnType())) { 447 String propertyName = method.getName(); 448 return itemForProperty(propertyName, method); 449 } else if (NewConfig.class.isAssignableFrom(method.getReturnType())) { 450 Object newRealObject = method.invoke(this.realImplementation, new Object [0]); 451 String subXPath = fetchSubXPathFor(method.getName()); 452 String xpath; 453 if (xpathPrefix != null && subXPath != null) xpath = xpathPrefix + "/" + subXPath; 454 else if (xpathPrefix != null) xpath = xpathPrefix; 455 else xpath = subXPath; 456 System.err.println("Creating new sub-config object of " + method.getReturnType()); 457 System.err.println("XPath: " + xpath); 458 System.err.println("New real object: " + newRealObject); 459 return Proxy.newProxyInstance(getClass().getClassLoader(), new Class [] { method.getReturnType() }, 460 new TestConfigObjectInvocationHandler(method.getReturnType(), this.beansToSetOn, 461 newRealObject, xpath)); 462 } else { 463 throw Assert.failure("This method, '" + method.getName() + "', has a return type of " 464 + method.getReturnType().getName() + ", which isn't a descendant of ConfigItem. " 465 + "We don't know how to support this for test config objects yet."); 466 } 467 } 468 469 private String fetchSubXPathFor(String methodName) { 470 String fieldName = camelToUnderscores(methodName) + "_SUB_XPATH"; 471 472 try { 473 Field theField = this.realImplementation.getClass().getField(fieldName); 474 Assert.eval(Modifier.isPublic(theField.getModifiers())); 475 Assert.eval(Modifier.isStatic(theField.getModifiers())); 476 Assert.eval(Modifier.isFinal(theField.getModifiers())); 477 Assert.eval(String .class.equals(theField.getType())); 478 return (String ) theField.get(null); 479 } catch (IllegalAccessException iae) { 480 throw Assert.failure("Can't fetch field '" + fieldName + "'?", iae); 481 } catch (NoSuchFieldException nsfe) { 482 return null; 483 } 484 } 485 486 private String camelToUnderscores(String methodName) { 487 StringBuffer out = new StringBuffer (); 488 char[] source = methodName.toCharArray(); 489 boolean lastWasLowercase = isLowercase(source[0]); 490 out.append(toUppercase(source[0])); 491 492 for (int i = 1; i < source.length; ++i) { 493 boolean thisIsLowercase = isLowercase(source[i]); 494 495 if (lastWasLowercase && (!thisIsLowercase)) { 496 out.append("_"); 498 } 499 500 lastWasLowercase = thisIsLowercase; 501 out.append(toUppercase(source[i])); 502 } 503 504 return out.toString(); 505 } 506 507 private boolean isLowercase(char ch) { 508 return Character.isLowerCase(ch); 509 } 510 511 private char toUppercase(char ch) { 512 return Character.toUpperCase(ch); 513 } 514 515 private synchronized OurSettableConfigItem itemForProperty(String propertyName, Method theMethod) { 516 OurSettableConfigItem out = (OurSettableConfigItem) this.configItems.get(propertyName); 517 if (out == null) { 518 try { 519 ConfigItem realItem = (ConfigItem) theMethod.invoke(realImplementation, new Object [0]); 520 Assert.eval("The base item we found was a " + realItem.getClass() + ", not an XPathBasedConfigItem", 521 realItem instanceof XPathBasedConfigItem); 522 523 String effectiveXPath = (this.xpathPrefix == null ? "" : this.xpathPrefix + "/") 524 + ((XPathBasedConfigItem) realItem).xpath(); 525 logger.debug("For property '" + propertyName + "' on proxied config-object implementation of interface " 526 + this.theInterface.getName() + ", returning a config item with effective XPath '" 527 + effectiveXPath + "'."); 528 529 out = new OurSettableConfigItem(effectiveXPath); 530 this.configItems.put(propertyName, out); 531 } catch (IllegalAccessException iae) { 532 throw Assert.failure("Unable to retrieve the real ConfigItem so we can get its XPath.", iae); 533 } catch (InvocationTargetException ite) { 534 throw Assert.failure("Unable to retrieve the real ConfigItem so we can get its XPath.", ite); 535 } 536 } 537 return out; 538 } 539 540 } 541 | Popular Tags |