001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.q2; 020 021 022import org.jdom2.Content; 023import org.jdom2.Element; 024import org.jdom2.Text; 025import org.jpos.core.*; 026import org.jpos.core.annotation.Config; 027import org.jpos.q2.qbean.QConfig; 028import org.jpos.util.LogSource; 029import org.jpos.util.Logger; 030import org.jpos.util.NameRegistrar; 031 032import javax.management.*; 033import java.lang.reflect.Field; 034import java.lang.reflect.InvocationTargetException; 035import java.lang.reflect.Method; 036import java.net.MalformedURLException; 037import java.util.Collection; 038import java.util.List; 039import java.util.MissingResourceException; 040import java.util.ResourceBundle; 041import java.util.StringTokenizer; 042import java.util.concurrent.ExecutorService; 043import java.util.concurrent.Executors; 044 045/** 046 * @author <a href="mailto:taherkordy@dpi2.dpi.net.ir">Alireza Taherkordi</a> 047 * @author <a href="mailto:apr@cs.com.uy">Alejandro P. Revilla</a> 048 * @version $Revision$ $Date$ 049 */ 050@SuppressWarnings("unchecked") 051public class QFactory { 052 ObjectName loaderName; 053 Q2 q2; 054 ResourceBundle classMapping; 055 ConfigurationFactory defaultConfigurationFactory = new SimpleConfigurationFactory(); 056 057 public QFactory (ObjectName loaderName, Q2 q2) { 058 super (); 059 this.loaderName = loaderName; 060 this.q2 = q2; 061 classMapping = ResourceBundle.getBundle(this.getClass().getName()); 062 } 063 064 @SuppressWarnings("PMD.EmptyCatchBlock") 065 public Object instantiate (Q2 server, Element e) 066 throws ReflectionException, 067 MBeanException, 068 InstanceNotFoundException 069 { 070 String clazz = getAttributeValue (e, "class"); 071 if (clazz == null) { 072 try { 073 clazz = classMapping.getString (e.getName()); 074 } catch (MissingResourceException ex) { 075 // no class attribute, no mapping 076 // let MBeanServer do the yelling 077 } 078 } 079 MBeanServer mserver = server.getMBeanServer(); 080 if (!q2.isDisableDynamicClassloader()) 081 getExtraPath(server.getLoader(), e); 082 return mserver.instantiate (clazz, loaderName); 083 } 084 085 public ObjectInstance createQBean (Q2 server, Element e, Object obj) 086 throws MalformedObjectNameException, 087 InstanceAlreadyExistsException, 088 InstanceNotFoundException, 089 MBeanException, 090 NotCompliantMBeanException, 091 InvalidAttributeValueException, 092 ReflectionException, 093 ConfigurationException 094 { 095 String name = getAttributeValue (e, "name"); 096 if (name == null) 097 name = e.getName (); 098 099 ObjectName objectName = new ObjectName (Q2.QBEAN_NAME + name); 100 MBeanServer mserver = server.getMBeanServer(); 101 if(mserver.isRegistered(objectName)) { 102 throw new InstanceAlreadyExistsException (name+" has already been deployed in another file."); 103 } 104 ObjectInstance instance = mserver.registerMBean ( 105 obj, objectName 106 ); 107 try { 108 setAttribute (mserver, objectName, "Name", name); 109 String logger = getAttributeValue (e, "logger"); 110 if (logger != null) 111 setAttribute (mserver, objectName, "Logger", logger); 112 String realm = getAttributeValue (e, "realm"); 113 if (realm != null) 114 setAttribute (mserver, objectName, "Realm", realm); 115 setAttribute (mserver, objectName, "Server", server); 116 setAttribute (mserver, objectName, "Persist", e); 117 configureQBean(mserver,objectName,e); 118 setConfiguration (obj, e); // handle legacy (QSP v1) Configurables 119 120 if (obj instanceof QBean) 121 mserver.invoke (objectName, "init", null, null); 122 } 123 catch (Throwable t) { 124 mserver.unregisterMBean(objectName); 125 t.fillInStackTrace(); 126 throw t; 127 } 128 129 return instance; 130 } 131 public Q2 getQ2() { 132 return q2; 133 } 134 135 private void getExtraPath (QClassLoader loader, Element e) { 136 Element classpathElement = e.getChild ("classpath"); 137 if (classpathElement != null) { 138 try { 139 loader = loader.scan (true); // force a new classloader 140 } catch (Throwable t) { 141 getQ2().getLog().error(t); 142 } 143 for (Element u : classpathElement.getChildren("url")) { 144 try { 145 loader.addURL(u.getTextTrim()); 146 } catch (MalformedURLException ex) { 147 q2.getLog().warn(u.getTextTrim(), ex); 148 } 149 } 150 } 151 } 152 153 @SuppressWarnings("PMD.EmptyCatchBlock") 154 public void setAttribute 155 (MBeanServer server, ObjectName objectName, 156 String attribute, Object value) 157 throws InstanceNotFoundException, 158 MBeanException, 159 InvalidAttributeValueException, 160 ReflectionException 161 { 162 try { 163 server.setAttribute ( 164 objectName, new Attribute (attribute, value) 165 ); 166 } catch (AttributeNotFoundException ex) { 167 // okay to fail 168 } catch (InvalidAttributeValueException ex) { 169 // okay to fail (produced by some application servers instead of AttributeNotFoundException) 170 } 171 } 172 173 public void startQBean (Q2 server, ObjectName objectName) 174 throws InstanceNotFoundException, 175 MBeanException, 176 ReflectionException 177 { 178 MBeanServer mserver = server.getMBeanServer(); 179 mserver.invoke (objectName, "start", null, null); 180 } 181 182 public void destroyQBean (Q2 server, ObjectName objectName, Object obj) 183 throws InstanceNotFoundException, 184 MBeanException, 185 ReflectionException 186 { 187 MBeanServer mserver = server.getMBeanServer(); 188 if (obj instanceof QBean) { 189 mserver.invoke (objectName, "stop", null, null); 190 mserver.invoke (objectName, "destroy", null, null); 191 } 192 if (objectName != null) 193 mserver.unregisterMBean (objectName); 194 } 195 196 public void configureQBean(MBeanServer server, ObjectName objectName, Element e) 197 throws ConfigurationException 198 { 199 try { 200 AttributeList attributeList = getAttributeList(e); 201 for (Object anAttributeList : attributeList) 202 server.setAttribute(objectName, (Attribute) anAttributeList); 203 } catch (Exception e1) { 204 throw new ConfigurationException(e1); 205 } 206 207 } 208 public AttributeList getAttributeList(Element e) 209 throws ConfigurationException 210 { 211 AttributeList attributeList = new AttributeList(); 212 List childs = e.getChildren("attr"); 213 for (Object child : childs) { 214 Element childElement = (Element) child; 215 String name = childElement.getAttributeValue("name"); 216 name = getAttributeName(name); 217 Attribute attr = new Attribute(name, getObject(childElement)); 218 attributeList.add(attr); 219 } 220 return attributeList; 221 } 222 223 /** 224 * Creates an object from a definition element. 225 * The element may have an attribute called type indicating the type of the object 226 * to create, if this attribute is not present java.lang.String is assumed. 227 * int, long and boolean are converted to their wrappers. 228 * @return The created object. 229 * @param childElement Dom Element with the definition of the object. 230 * @throws ConfigurationException If an exception is found trying to create the object. 231 */ 232 @SuppressWarnings("unchecked") 233 protected Object getObject(Element childElement) 234 throws ConfigurationException 235 { 236 String type = childElement.getAttributeValue("type","java.lang.String"); 237 if ("int".equals (type)) 238 type = "java.lang.Integer"; 239 else if ("long".equals (type)) 240 type = "java.lang.Long"; 241 else if ("boolean".equals (type)) 242 type = "java.lang.Boolean"; 243 244 String value = childElement.getText(); 245 value = Environment.getEnvironment().getProperty(value, value); 246 try { 247 Class<?> attributeType = Class.forName(type); 248 if(Collection.class.isAssignableFrom(attributeType)) 249 return getCollection(attributeType, childElement); 250 else{ 251 Class[] parameterTypes = {"".getClass()}; 252 Object[] parameterValues = {value}; 253 return attributeType.getConstructor(parameterTypes).newInstance(parameterValues); 254 } 255 } catch (Exception e1) { 256 throw new ConfigurationException(e1); 257 } 258 259 } 260 261 262 /** Creats a collection from a definition element with the format. 263 * <PRE> 264 * <{attr|item} type="..."> 265 * <item [type="..."]>...</item> 266 * ... 267 * </attr> 268 * </PRE> 269 * @param type class type 270 * @param e the Element 271 * @throws ConfigurationException 272 * @return the object collection 273 */ 274 @SuppressWarnings("unchecked") 275 protected Collection getCollection(Class type, Element e) 276 throws ConfigurationException 277 { 278 try{ 279 Collection<Object> col = (Collection<Object>) type.newInstance(); 280 for (Element o : e.getChildren("item")) { 281 col.add(getObject(o)); 282 } 283 return col; 284 }catch(Exception e1){ 285 throw new ConfigurationException(e1); 286 } 287 } 288 /** 289 * sets the first character of the string to the upper case 290 * @param name attribute name 291 * @return attribute name 292 */ 293 public String getAttributeName(String name) 294 { 295 if (name == null) 296 throw new NullPointerException("attribute name can not be null"); 297 StringBuilder tmp = new StringBuilder(name); 298 if (tmp.length() > 0) 299 tmp.setCharAt(0,name.toUpperCase().charAt(0)) ; 300 return tmp.toString(); 301 } 302 303 public <T> T newInstance (String clazz) 304 throws ConfigurationException 305 { 306 try { 307 MBeanServer mserver = q2.getMBeanServer(); 308 return (T)mserver.instantiate (clazz, loaderName); 309 } catch (Exception e) { 310 throw new ConfigurationException (clazz, e); 311 } 312 } 313 314 public <T> T newInstance (Class<T> clazz) 315 throws ConfigurationException 316 { 317 return newInstance(clazz.getName()); 318 } 319 320 /** 321 * Creates a new instance from the values in {@code Element e}.<br/> 322 * <p> 323 * The method honors the {@code enabled} attribute in the given {@code Element e}, 324 * returning null immediately if {@code enabled} is computed to a true-equivalent.</p> 325 * <p> 326 * It also calls {@link #setLogger} to set logger and realm, and {@link #setConfiguration(Object, Element)} 327 * to trigger the standard [auto]configuration sequence from properties and XML.</p> 328 * 329 * @param e The XML config 330 * @return the new instance, or null if not enabled 331 * @throws ConfigurationException if the instance can't be created (e.g. class not found) 332 * or the configuration process itself threw the exception. 333 */ 334 public <T> T newInstance(Element e) throws ConfigurationException { 335 if (!QFactory.isEnabled(e)) 336 return null; 337 T obj = newInstance(QFactory.getAttributeValue (e, "class")); 338 setLogger (obj, e); 339 setConfiguration(obj, e); 340 return obj; 341 } 342 343 public Configuration getConfiguration (Element e) 344 throws ConfigurationException 345 { 346 String configurationFactoryClazz = getAttributeValue(e, "configuration-factory"); 347 ConfigurationFactory cf = configurationFactoryClazz != null ? 348 (ConfigurationFactory) newInstance(configurationFactoryClazz) : defaultConfigurationFactory; 349 350 Configuration cfg = cf.getConfiguration(e); 351 String merge = getAttributeValue(e, "merge-configuration"); 352 if (merge != null) { 353 StringTokenizer st = new StringTokenizer(merge, ", "); 354 while (st.hasMoreElements()) { 355 try { 356 Configuration c = QConfig.getConfiguration(st.nextToken()); 357 for (String k : c.keySet()) { 358 if (cfg.get(k, null) == null) { 359 String[] v = c.getAll(k); 360 switch (v.length) { 361 case 0: 362 break; 363 case 1: 364 cfg.put(k, v[0]); 365 break; 366 default: 367 cfg.put(k, v); 368 } 369 } 370 } 371 } catch (NameRegistrar.NotFoundException ex) { 372 throw new ConfigurationException (ex.getMessage()); 373 } 374 } 375 } 376 return cfg; 377 } 378 379 public void setLogger (Object obj, Element e) { 380 if (obj instanceof LogSource) { 381 String loggerName = getAttributeValue (e, "logger"); 382 if (loggerName != null) { 383 String realm = getAttributeValue (e, "realm"); 384 if (realm == null) 385 realm = e.getName(); 386 Logger logger = Logger.getLogger (loggerName); 387 ((LogSource)obj).setLogger (logger, realm); 388 } 389 } 390 } 391 392 public static String getAttributeValue (Element e, String name) { 393 String s = e.getAttributeValue(name); 394 return Environment.getEnvironment().getProperty(s, s); 395 } 396 public void setConfiguration (Object obj, Element e) 397 throws ConfigurationException 398 { 399 try { 400 Configuration cfg = getConfiguration (e); 401 autoconfigure(obj, cfg); 402 403 if (obj instanceof Configurable) 404 ((Configurable)obj).setConfiguration (cfg); 405 if (obj instanceof XmlConfigurable) 406 ((XmlConfigurable)obj).setConfiguration(e); 407 } catch (ConfigurationException | IllegalAccessException ex) { 408 throw new ConfigurationException (ex); 409 } 410 } 411 /** 412 * Try to invoke a method (usually a setter) on the given object 413 * silently ignoring if method does not exist 414 * @param obj the object 415 * @param m method to invoke 416 * @param p parameter 417 * @throws ConfigurationException if method happens to throw an exception 418 */ 419 public static void invoke (Object obj, String m, Object p) 420 throws ConfigurationException 421 { 422 invoke (obj, m, p, p != null ? p.getClass() : null); 423 } 424 425 /** 426 * Try to invoke a method (usually a setter) on the given object 427 * silently ignoring if method does not exist 428 * @param obj the object 429 * @param m method to invoke 430 * @param p parameter 431 * @param pc parameter class 432 * @throws ConfigurationException if method happens to throw an exception 433 */ 434 @SuppressWarnings("PMD.EmptyCatchBlock") 435 public static void invoke (Object obj, String m, Object p, Class pc) 436 throws ConfigurationException 437 { 438 try { 439 if (p!=null) { 440 Class[] paramTemplate = { pc }; 441 Method method = obj.getClass().getMethod(m, paramTemplate); 442 Object[] param = new Object[1]; 443 param[0] = p; 444 method.invoke (obj, param); 445 } else { 446 Method method = obj.getClass().getMethod(m); 447 method.invoke (obj); 448 } 449 } catch (NoSuchMethodException ignored) { 450 } catch (NullPointerException ignored) { 451 } catch (IllegalAccessException ignored) { 452 } catch (InvocationTargetException e) { 453 throw new ConfigurationException ( 454 obj.getClass().getName() + "." + m + "(" + p +")" , 455 e.getTargetException() 456 ); 457 } 458 } 459 public static boolean isEnabled(Element e) { 460 return isTrue(getEnabledAttribute(e)); 461 } 462 public static boolean isEagerStart(Element e) { 463 return isTrue(getEagerStartAttribute(e)); 464 } 465 private static boolean isTrue(String attribute) { 466 return "true".equalsIgnoreCase(attribute) || 467 "yes".equalsIgnoreCase(attribute) || 468 attribute.contains(Environment.getEnvironment().getName()); 469 } 470 471 public static String getEnabledAttribute (Element e) { 472 return getAttribute(e, "enabled", "true"); 473 } 474 public static String getEagerStartAttribute (Element e) { 475 return getAttribute(e, "eager-start", "false"); 476 } 477 private static String getAttribute (Element e, String attr, String def) { 478 return Environment.get(e.getAttributeValue(attr, def)); 479 } 480 481 @SuppressWarnings("rawtypes") 482 public static void autoconfigure (Object obj, Configuration cfg) throws IllegalAccessException { 483 Class cc = obj.getClass(); 484 do { 485 Field[] fields = cc.getDeclaredFields(); 486 for (Field field : fields) { 487 if (field.isAnnotationPresent(Config.class)) { 488 Config config = field.getAnnotation(Config.class); 489 String v = cfg.get(config.value(), null); 490 if (v != null) { 491 if (!field.isAccessible()) 492 field.setAccessible(true); 493 Class<?> c = field.getType(); 494 if (c.isAssignableFrom(String.class)) 495 field.set(obj, v); 496 else if (c.isAssignableFrom(int.class) || c.isAssignableFrom(Integer.class)) 497 field.set(obj, cfg.getInt(config.value())); 498 else if (c.isAssignableFrom(long.class) || c.isAssignableFrom(Long.class)) 499 field.set(obj, cfg.getLong(config.value())); 500 else if (c.isAssignableFrom(double.class) || c.isAssignableFrom(Double.class)) 501 field.set(obj, cfg.getDouble(config.value())); 502 else if (c.isAssignableFrom(boolean.class) || c.isAssignableFrom(Boolean.class)) 503 field.set(obj, cfg.getBoolean(config.value())); 504 else if (c.isEnum()) 505 field.set(obj, Enum.valueOf((Class<Enum>) c, v)); 506 else if (c.isArray()) { 507 Class<?> ct = c.getComponentType(); 508 if (ct.isAssignableFrom(String.class)) 509 field.set(obj, cfg.getAll(config.value())); 510 else if (ct.isAssignableFrom(int.class) || ct.isAssignableFrom(Integer.class)) 511 field.set(obj, cfg.getInts(config.value())); 512 else if (ct.isAssignableFrom(long.class) || ct.isAssignableFrom(Long.class)) 513 field.set(obj, cfg.getLongs(config.value())); 514 else if (ct.isAssignableFrom(double.class) || ct.isAssignableFrom(Double.class)) 515 field.set(obj, cfg.getDoubles(config.value())); 516 } 517 } 518 } 519 } 520 cc = cc.getSuperclass(); 521 } while (!cc.equals(Object.class)); 522 } 523 524 /** 525 * Decorates an {@link Element} by replacing its attributes, and content {@link Environment} properties references. 526 * @param e The element being decorated. 527 * @return The modified element, it is modified in place, but it is returned to ease method chaining or call composition. 528 */ 529 public static Element expandEnvProperties(Element e) { 530 expandEnvProperties(e, Environment.getEnvironment()); 531 return e; 532 } 533 534 public static ExecutorService executorService(boolean virtual) { 535 return virtual ? 536 Executors.newVirtualThreadPerTaskExecutor() : 537 Executors.newThreadPerTaskExecutor( 538 Thread.ofPlatform().inheritInheritableThreadLocals(true) 539 .factory() 540 ); 541 } 542 543 /** 544 * Recursively replaces {@link Environment} properties in an element's attributes, its content and its children. 545 * Properties are replaced in place. 546 * @param e The element in which properties are being replaced. 547 * @param env The {@link Environment}'s singleton instance. 548 */ 549 private static void expandEnvProperties(Element e, Environment env) { 550 if (Boolean.parseBoolean(e.getAttributeValue("verbatim"))) return; 551 for (org.jdom2.Attribute attr : e.getAttributes()) { 552 String value = attr.getValue(); 553 attr.setValue(env.getProperty(value, value)); 554 } 555 for (Content child : e.getContent()) { 556 if (child instanceof Element) { 557 expandEnvProperties((Element) child, env); 558 } else if (child instanceof Text text) { 559 String textValue = text.getText(); 560 text.setText(env.getProperty(textValue, textValue)); 561 } 562 } 563 } 564 565}