1 /*
   2  * Copyright (c) 2004, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @bug 5012634
  27  * @summary Test that JMX classes use fully-qualified class names
  28  * in MBeanNotificationInfo
  29  * @author Eamonn McManus
  30  * @modules java.management.rmi
  31  * @run clean NotificationInfoTest
  32  * @run build NotificationInfoTest
  33  * @run main NotificationInfoTest
  34  */
  35 
  36 import java.io.*;
  37 import java.lang.management.*;
  38 import java.lang.reflect.*;
  39 import java.net.*;
  40 import java.nio.file.FileSystem;
  41 import java.nio.file.FileSystems;
  42 import java.nio.file.Files;
  43 import java.nio.file.Path;
  44 import java.nio.file.Paths;
  45 import java.util.*;
  46 import java.util.stream.Collectors;
  47 import javax.management.*;
  48 import javax.management.relation.*;
  49 import javax.management.remote.*;
  50 import javax.management.remote.rmi.*;
  51 
  52 /*
  53  * This test finds all classes in the same code-base as the JMX
  54  * classes that look like Standard MBeans, and checks that if they are
  55  * NotificationBroadcasters they declare existent notification types.
  56  * A class looks like a Standard MBean if both Thing and ThingMBean
  57  * classes exist.  So for example javax.management.timer.Timer looks
  58  * like a Standard MBean because javax.management.timer.TimerMBean
  59  * exists.  Timer is instanceof NotificationBroadcaster, so we expect
  60  * that ((NotificationBroadcaster) timer).getNotificationInfo() will
  61  * return an array of MBeanNotificationInfo where each entry has a
  62  * getName() that names an existent Java class that is a Notification.
  63  *
  64  * An MBean is "suspicious" if it is a NotificationBroadcaster but its
  65  * MBeanNotificationInfo[] is empty.  This is legal, but surprising.
  66  *
  67  * In order to call getNotificationInfo(), we need an instance of the
  68  * class.  We attempt to make one by calling a public no-arg
  69  * constructor.  But the "construct" method below can be extended to
  70  * construct specific MBean classes for which the no-arg constructor
  71  * doesn't exist.
  72  *
  73  * The test is obviously not exhaustive, but does catch the cases that
  74  * failed in 5012634.
  75  */
  76 public class NotificationInfoTest {
  77     // class or object names where the test failed
  78     private static final Set<String> failed = new TreeSet<String>();
  79 
  80     // class or object names where there were no MBeanNotificationInfo entries
  81     private static final Set<String> suspicious = new TreeSet<String>();
  82 
  83     public static void main(String[] args) throws Exception {
  84         System.out.println("Checking that all known MBeans that are " +
  85                            "NotificationBroadcasters have sane " +
  86                            "MBeanInfo.getNotifications()");
  87 
  88         System.out.println("Checking platform MBeans...");
  89         checkPlatformMBeans();
  90 
  91         URL codeBase;
  92         String home = System.getProperty("java.home");
  93         Path classFile = Paths.get(home, "modules", "java.management");
  94         if (Files.isDirectory(classFile)) {
  95             codeBase = classFile.toUri().toURL();
  96         } else {
  97             codeBase = URI.create("jrt:/java.management").toURL();
  98         }
  99 
 100         System.out.println();
 101         System.out.println("Looking for standard MBeans...");
 102         String[] classes = findStandardMBeans(codeBase);
 103 
 104         System.out.println("Testing standard MBeans...");
 105         for (int i = 0; i < classes.length; i++) {
 106             String name = classes[i];
 107             Class<?> c;
 108             try {
 109                 c = Class.forName(name);
 110             } catch (Throwable e) {
 111                 System.out.println(name + ": cannot load (not public?): " + e);
 112                 continue;
 113             }
 114             if (!NotificationBroadcaster.class.isAssignableFrom(c)) {
 115                 System.out.println(name + ": not a NotificationBroadcaster");
 116                 continue;
 117             }
 118             if (Modifier.isAbstract(c.getModifiers())) {
 119                 System.out.println(name + ": abstract class");
 120                 continue;
 121             }
 122 
 123             NotificationBroadcaster mbean;
 124             Constructor<?> constr;
 125             try {
 126                 constr = c.getConstructor();
 127             } catch (Exception e) {
 128                 System.out.println(name + ": no public no-arg constructor: "
 129                                    + e);
 130                 continue;
 131             }
 132             try {
 133                 mbean = (NotificationBroadcaster) constr.newInstance();
 134             } catch (Exception e) {
 135                 System.out.println(name + ": no-arg constructor failed: " + e);
 136                 continue;
 137             }
 138 
 139             check(mbean);
 140         }
 141 
 142         System.out.println();
 143         System.out.println("Testing some explicit cases...");
 144 
 145         check(new RelationService(false));
 146         /*
 147           We can't do this:
 148             check(new RequiredModelMBean());
 149           because the Model MBean spec more or less forces us to use the
 150           names GENERIC and ATTRIBUTE_CHANGE for its standard notifs.
 151         */
 152         checkRMIConnectorServer();
 153 
 154         System.out.println();
 155         if (!suspicious.isEmpty())
 156             System.out.println("SUSPICIOUS CLASSES: " + suspicious);
 157 
 158         if (failed.isEmpty())
 159             System.out.println("TEST PASSED");
 160         else {
 161             System.out.println("TEST FAILED: " + failed);
 162             System.exit(1);
 163         }
 164     }
 165 
 166     private static void check(NotificationBroadcaster mbean)
 167             throws Exception {
 168         System.out.print(mbean.getClass().getName() + ": ");
 169 
 170         check(mbean.getClass().getName(), mbean.getNotificationInfo());
 171     }
 172 
 173     private static void checkPlatformMBeans() throws Exception {
 174         MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
 175         Set<ObjectName> mbeanNames = mbs.queryNames(null, null);
 176         for (ObjectName name : mbeanNames) {
 177             if (!mbs.isInstanceOf(name,
 178                                   NotificationBroadcaster.class.getName())) {
 179                 System.out.println(name + ": not a NotificationBroadcaster");
 180             } else {
 181                 MBeanInfo mbi = mbs.getMBeanInfo(name);
 182                 check(name.toString(), mbi.getNotifications());
 183             }
 184         }
 185     }
 186 
 187     private static void checkRMIConnectorServer() throws Exception {
 188         JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://");
 189         RMIConnectorServer connector = new RMIConnectorServer(url, null);
 190         check(connector);
 191     }
 192 
 193     private static void check(String what, MBeanNotificationInfo[] mbnis) {
 194         System.out.print(what + ": checking notification info: ");
 195 
 196         if (mbnis.length == 0) {
 197             System.out.println("NONE (suspicious)");
 198             suspicious.add(what);
 199             return;
 200         }
 201 
 202         // Each MBeanNotificationInfo.getName() should be an existent
 203         // Java class that is Notification or a subclass of it
 204         for (int j = 0; j < mbnis.length; j++) {
 205             String notifClassName = mbnis[j].getName();
 206                 Class notifClass;
 207                 try {
 208                     notifClass = Class.forName(notifClassName);
 209                 } catch (Exception e) {
 210                     System.out.print("FAILED(" + notifClassName + ": " + e +
 211                                      ") ");
 212                     failed.add(what);
 213                     continue;
 214                 }
 215                 if (!Notification.class.isAssignableFrom(notifClass)) {
 216                     System.out.print("FAILED(" + notifClassName +
 217                                      ": not a Notification) ");
 218                     failed.add(what);
 219                     continue;
 220                 }
 221                 System.out.print("OK(" + notifClassName + ") ");
 222         }
 223         System.out.println();
 224     }
 225 
 226     private static String[] findStandardMBeans(URL codeBase) throws Exception {
 227         Set<String> names;
 228         if (codeBase.getProtocol().equalsIgnoreCase("jrt")) {
 229             names = findStandardMBeansFromRuntime();
 230         } else {
 231             names = findStandardMBeansFromDir(codeBase);
 232         }
 233 
 234         Set<String> standardMBeanNames = new TreeSet<String>();
 235         for (String name : names) {
 236             if (name.endsWith("MBean")) {
 237                 String prefix = name.substring(0, name.length() - 5);
 238                 if (names.contains(prefix))
 239                     standardMBeanNames.add(prefix);
 240             }
 241         }
 242         return standardMBeanNames.toArray(new String[0]);
 243     }
 244 
 245     private static Set<String> findStandardMBeansFromRuntime() throws Exception {
 246         FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
 247         Path modules = fs.getPath("/modules");
 248         return Files.walk(modules)
 249                 .filter(path -> path.toString().endsWith(".class"))
 250                 .map(path -> path.subpath(2, path.getNameCount()))
 251                 .map(Path::toString)
 252                 .map(s -> s.substring(0, s.length() - 6))  // drop .class
 253                 .filter(s -> !s.equals("module-info"))
 254                 .map(s -> s.replace('/', '.'))
 255                 .collect(Collectors.toSet());
 256     }
 257 
 258     private static Set<String> findStandardMBeansFromDir(URL codeBase)
 259             throws Exception {
 260         File dir = new File(new URI(codeBase.toString()));
 261         Set<String> names = new TreeSet<String>();
 262         scanDir(dir, "", names);
 263         return names;
 264     }
 265 
 266     private static void scanDir(File dir, String prefix, Set<String> names)
 267             throws Exception {
 268         File[] files = dir.listFiles();
 269         if (files == null)
 270             return;
 271         for (int i = 0; i < files.length; i++) {
 272             File f = files[i];
 273             String name = f.getName();
 274             String p = (prefix.equals("")) ? name : prefix + "." + name;
 275             if (f.isDirectory())
 276                 scanDir(f, p, names);
 277             else if (name.endsWith(".class")) {
 278                 p = p.substring(0, p.length() - 6);
 279                 names.add(p);
 280             }
 281         }
 282     }
 283 }