View Javadoc

1   /*
2    * EL4J, the Extension Library for the J2EE, adds incremental enhancements to
3    * the spring framework, http://el4j.sf.net
4    * Copyright (C) 2005 by ELCA Informatique SA, Av. de la Harpe 22-24,
5    * 1000 Lausanne, Switzerland, http://www.elca.ch
6    *
7    * EL4J is published under the GNU Lesser General Public License (LGPL)
8    * Version 2.1. See http://www.gnu.org/licenses/
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13   * GNU Lesser General Public License for more details.
14   *
15   * For alternative licensing, please contact info@elca.ch
16   */
17  
18  package ch.elca.el4j.services.monitoring.jmx;
19  
20  import java.beans.IntrospectionException;
21  import java.beans.Introspector;
22  import java.beans.PropertyDescriptor;
23  import java.lang.reflect.Method;
24  import java.util.Iterator;
25  
26  import javax.management.InstanceAlreadyExistsException;
27  import javax.management.MBeanRegistrationException;
28  import javax.management.MBeanServer;
29  import javax.management.MalformedObjectNameException;
30  import javax.management.NotCompliantMBeanException;
31  import javax.management.ObjectName;
32  
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  import org.springframework.aop.framework.Advised;
36  import org.springframework.beans.MutablePropertyValues;
37  import org.springframework.beans.PropertyValue;
38  import org.springframework.beans.factory.BeanFactory;
39  import org.springframework.beans.factory.config.BeanDefinition;
40  import org.springframework.beans.factory.support.DefaultListableBeanFactory;
41  import org.springframework.context.ApplicationContext;
42  
43  import ch.elca.el4j.core.exceptions.BaseException;
44  import ch.elca.el4j.core.exceptions.BaseRTException;
45  import ch.elca.el4j.services.monitoring.jmx.display.DisplayManager;
46  import ch.elca.el4j.services.monitoring.jmx.display.HtmlDisplayManager;
47  import ch.elca.el4j.services.monitoring.jmx.display.HtmlTabulator;
48  import ch.elca.el4j.services.monitoring.jmx.display.Section;
49  import ch.elca.el4j.services.monitoring.jmx.util.PropertyReflector;
50  import ch.elca.el4j.services.monitoring.notification.CoreNotificationHelper;
51  import ch.elca.el4j.util.codingsupport.annotations.FindBugsSuppressWarnings;
52  
53  /**
54   * The proxy class for a bean loaded in the ApplicationContext.
55   *
56   * @svnLink $Revision: 4010 $;$Date: 2009-12-01 10:59:54 +0100 (Di, 01. Dez 2009) $;$Author: jonasha $;$URL: https://el4j.svn.sourceforge.net/svnroot/el4j/branches/el4j_3_1/el4j/framework/modules/jmx/src/main/java/ch/elca/el4j/services/monitoring/jmx/SpringBeanMB.java $
57   *
58   * @author Raphael Boog (RBO)
59   * @author Rashid Waraich (RWA)
60   */
61  public class SpringBeanMB implements SpringBeanMBMBean {
62  
63  	/**
64  	 * The domain of the Spring Bean proxy as it will be registered at the MBean
65  	 * Server.
66  	 */
67  	public static final String SPRING_BEAN_DOMAIN = "SpringBean";
68  
69  	/**
70  	 * The name of the domain of the ObjectName.
71  	 */
72  	public static final String MBEAN_DOMAIN = "MBean";
73  
74  	/**
75  	 * The name of the key of the ObjectName.
76  	 */
77  	public static final String MBEAN_KEY = "name";
78  
79  	/**
80  	 * Private logger of this class.
81  	 */
82  	private static Logger s_logger = LoggerFactory.getLogger(SpringBeanMB.class);
83  
84  	/**
85  	 * The Application Context where this Spring Bean is registered at.
86  	 */
87  	protected ApplicationContext m_applicationContext;
88  
89  	/**
90  	 * The Application Context proxy for the real Application Context.
91  	 */
92  	protected ApplicationContextMB m_applicationContextMB;
93  
94  	/**
95  	 * The class name of this class.
96  	 */
97  	protected Class m_class;
98  
99  	/**
100 	 * The Bean Factory belonging to the referenced Application Context.
101 	 */
102 	protected BeanFactory m_beanFactory;
103 
104 	/**
105 	 * The MBean Server where this Spring Bean is registered at.
106 	 */
107 	private MBeanServer m_mBeanServer;
108 
109 	/**
110 	 * The bean definition name of this Spring Bean.
111 	 */
112 	private String m_name;
113 
114 	/**
115 	 * The object name of this Spring Bean proxy.
116 	 */
117 	private ObjectName m_objectName;
118 
119 	/**
120 	 * Constructor.
121 	 *
122 	 * @param name
123 	 *            The bean definition name of this Spring Bean
124 	 * @param acMB
125 	 *            The Application Context proxy
126 	 * @param ac
127 	 *            The real Application Context
128 	 * @param beanFactory
129 	 *            The Bean Factory belonging to this Application Context
130 	 * @param mBeanServer
131 	 *            The MBean Server where this Spring Bean is registered at
132 	 */
133 	public SpringBeanMB(String name, ApplicationContextMB acMB,
134 		ApplicationContext ac, BeanFactory beanFactory,
135 		MBeanServer mBeanServer) {
136 
137 		this.m_name = name;
138 		this.m_applicationContextMB = acMB;
139 		this.m_applicationContext = ac;
140 		this.m_mBeanServer = mBeanServer;
141 		this.m_beanFactory = beanFactory;
142 		try {
143 			this.m_class = ac.getType(name);
144 		} catch (NullPointerException e) {
145 			// Spring throws a NPE here on some proxied beans.
146 			// May be a spring bug - in any case, need to catch it.
147 			this.m_class = null;
148 		}
149 
150 	}
151 
152 	/**
153 	 * Init method. Sets up this ApplicationContextMB.
154 	 *
155 	 * @throws BaseException
156 	 *             in case the initialization failed
157 	 */
158 	public void init() throws BaseException {
159 
160 		// Set the object name of this object.
161 		setObjectName();
162 
163 		// Register the Spring Bean at the MBean Server.
164 		registerSpringBean();
165 
166 	}
167 
168 	/**
169 	 * Sets the objectName to of this object.
170 	 */
171 	public void setObjectName() {
172 
173 		String name = SPRING_BEAN_DOMAIN
174 			+ m_applicationContextMB.getInstanceCounter() + ":name="
175 			+ getName();
176 
177 		try {
178 			m_objectName = new ObjectName(name);
179 		} catch (MalformedObjectNameException e) {
180 			CoreNotificationHelper.notifyMisconfiguration(
181 					"The string passed as a parameter does not have"
182 					+ " the right format.");
183 		}
184 	}
185 
186 	/**
187 	 * Getter method for the objectName member variable.
188 	 *
189 	 * @return The objectName
190 	 */
191 	public ObjectName getObjectName() {
192 		return m_objectName;
193 	}
194 
195 	/**
196 	 * Register this SpringBean at the MBean Server.
197 	 *
198 	 * @throws BaseException
199 	 *             in case the registration at the MBean Server failed
200 	 */
201 	protected void registerSpringBean() throws BaseException {
202 
203 		if (getObjectName() == null) {
204 			String message = "The object name of the SpringBeanMB '"
205 				+ this.toString() + "' should not be null.";
206 			s_logger.error(message);
207 			throw new BaseRTException(message, (Object[]) null);
208 		} else {
209 			try {
210 				m_mBeanServer.registerMBean(this, getObjectName());
211 			} catch (InstanceAlreadyExistsException e) {
212 				String message = "The MBean is already under the "
213 					+ "control of the MBean server.";
214 				s_logger.error(message);
215 				throw new BaseException(message, e);
216 			} catch (MBeanRegistrationException e) {
217 				String message = "The MBean will not be registered.";
218 				s_logger.error(message);
219 				throw new BaseException(message, e);
220 			} catch (NotCompliantMBeanException e) {
221 				String message = "This object is not a JMX compliant"
222 					+ " MBean.";
223 				s_logger.error(message);
224 				throw new BaseException(message, e);
225 			}
226 		}
227 	}
228 
229 	/**
230 	 * Getter method for the mBeanServer member variable.
231 	 *
232 	 * @return The MBean Server
233 	 */
234 	public MBeanServer getMBeanServer() {
235 		return m_mBeanServer;
236 	}
237 
238 	/**
239 	 * {@inheritDoc}
240 	 */
241 	public String getName() {
242 		return m_name;
243 	}
244 
245 	/**
246 	 * @return The bean definition for this bean.
247 	 */
248 	private BeanDefinition getDefinition() {
249 		DefaultListableBeanFactory dlbf
250 			= (DefaultListableBeanFactory) m_beanFactory;
251 		return dlbf.getBeanDefinition(getName());
252 	}
253 	
254 	/**
255 	 * {@inheritDoc}
256 	 */
257 	public String[] getConfiguration() {
258 		
259 		// Extract the property values from the BeanDefinition object.
260 		MutablePropertyValues mpv = getDefinition().getPropertyValues();
261 
262 		PropertyValue[] pv = mpv.getPropertyValues();
263 		String[] result = new String[pv.length];
264 
265 		for (int i = 0; i < pv.length; i++) {
266 			result[i] = pv[i].getName() + " = " + pv[i].getValue().toString();
267 		}
268 
269 		return result;
270 	}
271 
272 	/**
273 	 * {@inheritDoc}
274 	 */
275 	public ObjectName getApplicationContextMB() {
276 		return m_applicationContextMB.getObjectName();
277 	}
278 
279 	/**
280 	 * {@inheritDoc}
281 	 */
282 	public ObjectName[] getRegisteredMBean() {
283 
284 		Loader loader = new Loader();
285 
286 		return loader.getObjectNames(getMBeanServer(), MBEAN_DOMAIN, MBEAN_KEY,
287 			getName());
288 	}
289 
290 	/**
291 	 * {@inheritDoc}
292 	 */
293 	public String getClassName() {
294 		return m_class.toString();
295 	}
296 
297 	/**
298 	 * {@inheritDoc}
299 	 */
300 	public boolean getIsSingleton() {
301 		return m_applicationContext.isSingleton(getName());
302 	}
303 	
304 	/**
305 	 * {@inheritDoc}
306 	 */
307 	public String[] getInterceptors() {
308 		String[] interceptorNames = null;
309 		
310 		Object target =  m_applicationContext.getBean(m_name);
311 		if (target instanceof Advised) {
312 			Advised advised = (Advised) target;
313 			interceptorNames = new String[advised.getAdvisors().length];
314 			for (int i = 0; i < advised.getAdvisors().length; i++) {
315 				interceptorNames[i]
316 					= advised.getAdvisors()[i].getAdvice().getClass().getName();
317 			}
318 		}
319 		
320 		return interceptorNames;
321 	}
322 
323 	/**
324 	 * {@inheritDoc}
325 	 */
326 	public boolean getIsProxied() {
327 		Object target =  m_applicationContext.getBean(m_name);
328 		if (target instanceof Advised) {
329 			return true;
330 		}
331 		return false;
332 	}
333 
334 	/** {@inheritDoc} */
335 	public String getResourceDescription() {
336 		return getDefinition().getResourceDescription();
337 	}
338 	
339 	/**
340 	 * {@inheritDoc}
341 	 */
342 	public String[] getMethods() {
343 		MethodReflector r = new MethodReflector(m_class);
344 		String[] result = new String[r.countMethods()];
345 		while (r.hasNext()) {
346 			r.next();
347 			result[r.getPosition()] = r.getCurrentAsString();
348 		}
349 		return result;
350 	}
351 
352 	/** {@inheritDoc} */
353 	@FindBugsSuppressWarnings(value = "REC_CATCH_EXCEPTION",
354 							justification = "A runtime exception is thrown in any case.")			
355 	public String[] getProperties() {
356 		Class<?> target = m_class;
357 		
358 		// Check if proxied, if so use target class.
359 		if (getIsProxied()) {
360 			try {
361 				Method getTarget = m_class.getMethod("getTargetClass",
362 					new Class[0]);
363 				Object bean = m_beanFactory.getBean(m_name);
364 				target = (Class<?>) getTarget.invoke(bean, (Object[]) null);
365 			} catch (Exception e) {
366 				throw new RuntimeException("Exception looking up target.");
367 			}
368 		}
369 		
370 		try {
371 			PropertyDescriptor[] pd = Introspector.getBeanInfo(target)
372 				.getPropertyDescriptors();
373 			String[] properties = new String[pd.length];
374 			for (int i = 0; i < pd.length; i++) {
375 				PropertyReflector r = new PropertyReflector(pd[i]);
376 				properties[i] = MethodReflector.className(r.getType())
377 					+ " " + r.getName() + " (" + r.getRW() + ")";
378 			}
379 			return properties;
380 		} catch (IntrospectionException e) {
381 			throw new RuntimeException("Introspection Exception");
382 		}
383 	}
384 	
385 	/** {@inheritDoc} */
386 	public String readProperty(String property) {
387 		final String err = "The property could not be read, because ";
388 		String result = "";
389 		
390 		// Uppercase the first letter, as the method will expect this.
391 		String propName = property.substring(0, 1).toUpperCase()
392 			+ property.substring(1);
393 		
394 		try {
395 			Object bean = m_beanFactory.getBean(m_name);
396 			Method m = null;
397 			try {
398 				m = bean.getClass()
399 					.getMethod("get" + propName, new Class<?>[0]);
400 			} catch (NoSuchMethodException e) {
401 				// Not "get-".
402 				try {
403 					m = bean.getClass().getMethod("is" + propName,
404 						new Class<?>[0]);
405 				} catch (NoSuchMethodException e2) {
406 					// This time, we are stuck.
407 					return err + "no get/is method was found.";
408 				}
409 			}
410 			// String returnType = MethodReflector
411 			//    .className(m.getReturnType().toString());
412 			Object o = m.invoke(bean, new Object[0]);
413 			result = MethodReflector.className(o.getClass().toString())
414 				+ " " + property + " = " + o.toString();
415 		} catch (Exception e) {
416 			result = err + "an excpetion occurred: "
417 				+ e.toString();
418 		}
419 		return result;
420 	}
421 	
422 	/**
423 	 * @param pd An array of descriptors.
424 	 * @param makeLinks Whether to make links to get*() methods.
425 	 * @return A table view of the properties.
426 	 */
427 	private String propertyTable(PropertyDescriptor[] pd, boolean makeLinks) {
428 		String result = "";
429 		
430 		HtmlTabulator table = new HtmlTabulator(
431 			"Name", "Type", "RW", "readMethod", "writeMethod");
432 
433 		for (PropertyDescriptor current : pd) {
434 			PropertyReflector r = new PropertyReflector(current);
435 
436 			String readLink = "";
437 			if (r.isReadable()) {
438 				// TODO : What is with the %2B ? A '+' ?
439 				String readUrl = "/InvokeAction//"
440 					+ m_objectName.getCanonicalName() + "/action=readProperty"
441 					+ "?action=readProperty&p1%2Bjava.lang.String="
442 					+ r.getName();
443 					/*
444 					+ (r.getReadMethod().startsWith("get")
445 						? r.getReadMethod().substring(3)
446 						: r.getReadMethod().substring(2));
447 					*/
448 				readLink = makeLinks
449 					? "<a href=\"" + readUrl + "\">"
450 						+ r.getReadMethod() + "</a>"
451 					: r.getReadMethod();
452 			}
453 			
454 			table.addRow(r.getName(),
455 						MethodReflector.className(r.getType()),
456 						"<code>" + r.getRW() + "</code>",
457 						readLink,
458 						r.isWritable() ? r.getWriteMethod() : "");
459 		}
460 
461 		result += table.tabulate();
462 		return result;
463 	}
464 	
465 	/**
466 	 * Display a bean's properties.
467 	 * @param manager The {@link DisplayManager} to use.
468 	 */
469 	public void displayProperties(DisplayManager manager) {
470 		
471 		Section section = new Section("Properties");
472 		
473 		PropertyDescriptor[] pd;
474 		Class<?> target = m_class;
475 		
476 		// Check if proxied, if so use target class.
477 		if (getIsProxied()) {
478 			section.addLine("This bean is proxied");
479 			try {
480 				Method getTarget = m_class.getMethod("getTargetClass",
481 					new Class[0]);
482 				Object bean = m_beanFactory.getBean(m_name);
483 				target = (Class<?>) getTarget.invoke(bean, (Object[]) null);
484 			} catch (Exception e) {
485 				section.addWarning("Could not find target class, "
486 					+ e.toString());
487 			}
488 		}
489 		
490 		try {
491 			pd = Introspector.getBeanInfo(target).getPropertyDescriptors();
492 			// Trun off links if proxied.
493 			// TODO : Can we invoke proxied methods?
494 			section.add(propertyTable(pd, !getIsProxied()));
495 			
496 		} catch (IntrospectionException e) {
497 			section.addWarning("Introspection Exception.");
498 		}
499 		manager.addSection(section);
500 		
501 		if (getIsProxied()) {
502 			// Show the proxy's properties too.
503 			Section proxySection = new Section("Proxy Properties");
504 			try {
505 				proxySection.add(propertyTable(Introspector.getBeanInfo(m_class)
506 						.getPropertyDescriptors(), true));
507 			} catch (IntrospectionException e) {
508 				proxySection.addWarning(
509 					"Exception looking up proxy properties.");
510 			}
511 			manager.addSection(proxySection);
512 		}
513 	}
514 	
515 	/**
516 	 * Displays the properties this bean was loaded with.
517 	 * @param manager The {@link DisplayManager} to add to.
518 	 */
519 	public void displayConfiguration(DisplayManager manager) {
520 		Section section = new Section("Loading Properties");
521 		
522 		HtmlTabulator table = new HtmlTabulator("Name", "Value");
523 		for (String current : getConfiguration()) {
524 			// Invariant : getConfiguration returns a string of form
525 			// "key = value" .
526 			int position = current.indexOf("=");
527 			// Split at " = "
528 			String key = current.substring(0, position - 1);
529 			String value = current.substring(position + 1);
530 			table.addRow(key, value);
531 		}
532 		section.add(table.tabulate());
533 		manager.addSection(section);
534 	}
535 	
536 	/**
537 	 * @return Results of bean introspection on this bean as HTML.
538 	 */
539 	public String introspect() {
540 		
541 		DisplayManager page = new HtmlDisplayManager();
542 
543 		page.setTitle("Results of introspection on bean "
544 			+ MethodReflector.className(m_class.toString()));
545 		
546 		Section infoSection = new Section("Bean");
547 		
548 		HtmlTabulator beanInfo = new HtmlTabulator("Item", "Value");
549 		beanInfo.addRow("Name", getName());
550 		beanInfo.addRow("Proxied", Boolean.toString(getIsProxied()));
551 		beanInfo.addRow("Singleton", Boolean.toString(getIsSingleton()));
552 		beanInfo.addRow("Defined in", (getResourceDescription() != null)
553 			? getResourceDescription() : "not available");
554 		
555 		infoSection.add(beanInfo.tabulate());
556 		infoSection.addLine("");
557 		
558 		// Interceptors.
559 		HtmlTabulator iTable = new HtmlTabulator("Interceptors");
560 		String[] interceptors = getInterceptors();
561 		if (interceptors == null || interceptors.length == 0) {
562 			iTable.addRow("none defined");
563 		} else {
564 			for (String i : interceptors) {
565 				iTable.addRow(i);
566 			}
567 		}
568 		infoSection.add(iTable.tabulate());
569 		
570 		page.addSection(infoSection);
571 		
572 		displayProperties(page);
573 		
574 		displayMethods(page);
575 		
576 		displayConfiguration(page);
577 		
578 		return page.getPage();
579 	}
580 	
581 	/**
582 	 * HTML listing of all methods in this class.
583 	 * @param manager The DisplayManager to use.
584 	 */
585 	public void displayMethods(DisplayManager manager) {
586 		Section section = new Section("Methods");
587 		HtmlTabulator table = new HtmlTabulator(
588 			"Return", "Name", "Parameters", "Throws");
589 		
590 		MethodReflector r = new MethodReflector(m_class);
591 		while (r.hasNext()) {
592 			r.next();
593 			table.addRow(r.getReturns(), r.getName(),
594 				r.getParameters(), r.getThrows());
595 		}
596 		section.add(table.tabulate());
597 		manager.addSection(section);
598 	}
599 
600 	/**
601 	 * Helper class for investigating a bean's methods.
602 	 */
603 	static class MethodReflector implements Iterator<Method> {
604 		
605 		/**
606 		 * The class to reflect on.
607 		 */
608 		private Class<?> m_target;
609 		
610 		/**
611 		 * Helper variable for iterator functionality.
612 		 */
613 		private int m_current = -1;
614 		
615 		/**
616 		 * The current method of this iterator.
617 		 * INVARIANT : m is a valid method once next()
618 		 * has been called at least once.
619 		 */
620 		private Method m_method = null;
621 		
622 		/**
623 		 * Creates a MethodReflector from a bean class.
624 		 * @param target The target class to reflect upon.
625 		 */
626 		public MethodReflector(Class<?> target) {
627 			this.m_target = target;
628 		}
629 		
630 		/**
631 		 * @return The number of methods in this class.
632 		 */
633 		public int countMethods() {
634 			return m_target.getMethods().length;
635 		}
636 		
637 		/**
638 		 * @return The current iterator position.
639 		 */
640 		public int getPosition() {
641 			return m_current;
642 		}
643 
644 		/** {@inheritDoc} */
645 		public boolean hasNext() {
646 			return (m_current < countMethods() - 1);
647 		}
648 
649 		/** {@inheritDoc} */
650 		public Method next() {
651 			if (!hasNext()) {
652 				throw new IndexOutOfBoundsException();
653 			}
654 			m_current++;
655 			m_method = m_target.getMethods()[m_current];
656 			return m_method;
657 		}
658 
659 		/**
660 		 * DO NOT USE!
661 		 * {@inheritDoc}
662 		 */
663 		public void remove() {
664 			throw new RuntimeException("Cannot remove methods.");
665 		}
666 
667 		/**
668 		 * @return The return type of the current method.
669 		 */
670 		public String getReturns() {
671 			return className(m_method.getReturnType().toString());
672 		}
673 		
674 		/**
675 		 * @return The name of the current method.
676 		 */
677 		public String getName() {
678 			return m_method.getName();
679 		}
680 		
681 		/**
682 		 * Strips "class" or "interface" from a string.
683 		 * @param name A class/interface name.
684 		 * @return The name with its prefix stripped.
685 		 */
686 		public static String className(String name) {
687 			final String[] prefixes = new String[] {"class", "interface"};
688 			String result = name;
689 			
690 			for (String current : prefixes) {
691 				if (result.startsWith(current)) {
692 					result = result.substring(current.length() + 1);
693 				}
694 			}
695 			return result;
696 		}
697 		
698 		/**
699 		 * @param classes An array of {@link Class} objects.
700 		 * @return A comma-separated list of their names.
701 		 */
702 		public static String getClassNames(Class<?>[] classes) {
703 			String result = "";
704 			for (Class<?> p : classes) {
705 				String param = className(p.toString());
706 				
707 				result += param + ", ";
708 			}
709 			if (result.endsWith(", ")) {
710 				result = result.substring(0, result.length() - 2);
711 			}
712 			return result;
713 		}
714 		
715 		/**
716 		 * @return The parameters of the current method.
717 		 */
718 		public String getParameters() {
719 			return getClassNames(m_method.getParameterTypes());
720 		}
721 		
722 		/**
723 		 * @return The declared exceptions of the current method.
724 		 */
725 		public String getThrows() {
726 			return getClassNames(m_method.getExceptionTypes());
727 		}
728 		
729 		/**
730 		 * @return A string describing the curent method.
731 		 */
732 		public String getCurrentAsString() {
733 			
734 			return getReturns() + " " + getName()
735 				+ "(" + getParameters() + ")"
736 				+ ((getThrows().equals(""))
737 					? ("") : (" throws " + getThrows()));
738 		}
739 		
740 	}
741 }