You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ClassPathExplorer.java 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. /*
  2. @ITMillApache2LicenseForJavaFiles@
  3. */
  4. package com.vaadin.terminal.gwt.widgetsetutils;
  5. import java.io.File;
  6. import java.io.FileFilter;
  7. import java.io.IOException;
  8. import java.io.OutputStream;
  9. import java.io.PrintStream;
  10. import java.net.JarURLConnection;
  11. import java.net.MalformedURLException;
  12. import java.net.URL;
  13. import java.net.URLConnection;
  14. import java.util.ArrayList;
  15. import java.util.Collection;
  16. import java.util.Enumeration;
  17. import java.util.HashMap;
  18. import java.util.HashSet;
  19. import java.util.Iterator;
  20. import java.util.LinkedHashMap;
  21. import java.util.List;
  22. import java.util.Map;
  23. import java.util.Set;
  24. import java.util.jar.Attributes;
  25. import java.util.jar.JarEntry;
  26. import java.util.jar.JarFile;
  27. import java.util.jar.Manifest;
  28. import java.util.logging.Level;
  29. import java.util.logging.Logger;
  30. import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
  31. import com.vaadin.event.dd.acceptcriteria.ClientCriterion;
  32. import com.vaadin.terminal.Paintable;
  33. import com.vaadin.ui.ClientWidget;
  34. /**
  35. * Utility class to collect widgetset related information from classpath.
  36. * Utility will seek all directories from classpaths, and jar files having
  37. * "Vaadin-Widgetsets" key in their manifest file.
  38. * <p>
  39. * Used by WidgetMapGenerator and ide tools to implement some monkey coding for
  40. * you.
  41. * <p>
  42. * Developer notice: If you end up reading this comment, I guess you have faced
  43. * a sluggish performance of widget compilation or unreliable detection of
  44. * components in your classpaths. The thing you might be able to do is to use
  45. * annotation processing tool like apt to generate the needed information. Then
  46. * either use that information in {@link WidgetMapGenerator} or create the
  47. * appropriate monkey code for gwt directly in annotation processor and get rid
  48. * of {@link WidgetMapGenerator}. Using annotation processor might be a good
  49. * idea when dropping Java 1.5 support (integrated to javac in 6).
  50. *
  51. */
  52. public class ClassPathExplorer {
  53. private static Logger logger = Logger
  54. .getLogger("com.vaadin.terminal.gwt.widgetsetutils");
  55. /**
  56. * File filter that only accepts directories.
  57. */
  58. private final static FileFilter DIRECTORIES_ONLY = new FileFilter() {
  59. public boolean accept(File f) {
  60. if (f.exists() && f.isDirectory()) {
  61. return true;
  62. } else {
  63. return false;
  64. }
  65. }
  66. };
  67. /**
  68. * Raw class path entries as given in the java class path string. Only
  69. * entries that could include widgets/widgetsets are listed (primarily
  70. * directories, Vaadin JARs and add-on JARs).
  71. */
  72. private static List<String> rawClasspathEntries = getRawClasspathEntries();
  73. /**
  74. * Map from identifiers (either a package name preceded by the path and a
  75. * slash, or a URL for a JAR file) to the corresponding URLs. This is
  76. * constructed from the class path.
  77. */
  78. private static Map<String, URL> classpathLocations = getClasspathLocations(rawClasspathEntries);
  79. /**
  80. * No instantiation from outside, callable methods are static.
  81. */
  82. private ClassPathExplorer() {
  83. }
  84. /**
  85. * Finds server side widgets with {@link ClientWidget} annotation on the
  86. * class path (entries that can contain widgets/widgetsets - see
  87. * {@link #getRawClasspathEntries()}).
  88. *
  89. * As a side effect, also accept criteria are searched under the same class
  90. * path entries and added into the acceptCriterion collection.
  91. *
  92. * @return a collection of {@link Paintable} classes
  93. */
  94. public static Collection<Class<? extends Paintable>> getPaintablesHavingWidgetAnnotation() {
  95. Collection<Class<? extends Paintable>> paintables = new HashSet<Class<? extends Paintable>>();
  96. Set<String> keySet = classpathLocations.keySet();
  97. for (String url : keySet) {
  98. searchForPaintables(classpathLocations.get(url), url, paintables);
  99. }
  100. return paintables;
  101. }
  102. /**
  103. * Finds all accept criteria having client side counterparts (classes with
  104. * the {@link ClientCriterion} annotation).
  105. *
  106. * @return Collection of AcceptCriterion classes
  107. */
  108. public static Collection<Class<? extends AcceptCriterion>> getCriterion() {
  109. if (acceptCriterion.isEmpty()) {
  110. // accept criterion are searched as a side effect, normally after
  111. // paintable detection
  112. getPaintablesHavingWidgetAnnotation();
  113. }
  114. return acceptCriterion;
  115. }
  116. /**
  117. * Finds the names and locations of widgetsets available on the class path.
  118. *
  119. * @return map from widgetset classname to widgetset location URL
  120. */
  121. public static Map<String, URL> getAvailableWidgetSets() {
  122. Map<String, URL> widgetsets = new HashMap<String, URL>();
  123. Set<String> keySet = classpathLocations.keySet();
  124. for (String location : keySet) {
  125. searchForWidgetSets(location, widgetsets);
  126. }
  127. StringBuilder sb = new StringBuilder();
  128. sb.append("Widgetsets found from classpath:\n");
  129. for (String ws : widgetsets.keySet()) {
  130. sb.append("\t");
  131. sb.append(ws);
  132. sb.append(" in ");
  133. sb.append(widgetsets.get(ws));
  134. sb.append("\n");
  135. }
  136. logger.info(sb.toString());
  137. return widgetsets;
  138. }
  139. /**
  140. * Finds all GWT modules / Vaadin widgetsets in a valid location.
  141. *
  142. * If the location is a directory, all GWT modules (files with the
  143. * ".gwt.xml" extension) are added to widgetsets.
  144. *
  145. * If the location is a JAR file, the comma-separated values of the
  146. * "Vaadin-Widgetsets" attribute in its manifest are added to widgetsets.
  147. *
  148. * @param locationString
  149. * an entry in {@link #classpathLocations}
  150. * @param widgetsets
  151. * a map from widgetset name (including package, with dots as
  152. * separators) to a URL (see {@link #classpathLocations}) - new
  153. * entries are added to this map
  154. */
  155. private static void searchForWidgetSets(String locationString,
  156. Map<String, URL> widgetsets) {
  157. URL location = classpathLocations.get(locationString);
  158. File directory = new File(location.getFile());
  159. if (directory.exists() && !directory.isHidden()) {
  160. // Get the list of the files contained in the directory
  161. String[] files = directory.list();
  162. for (int i = 0; i < files.length; i++) {
  163. // we are only interested in .gwt.xml files
  164. if (files[i].endsWith(".gwt.xml")) {
  165. // remove the extension
  166. String classname = files[i].substring(0,
  167. files[i].length() - 8);
  168. String packageName = locationString
  169. .substring(locationString.lastIndexOf("/") + 1);
  170. classname = packageName + "." + classname;
  171. if (!widgetsets.containsKey(classname)) {
  172. String packagePath = packageName.replaceAll("\\.", "/");
  173. String basePath = location.getFile().replaceAll(
  174. "/" + packagePath + "$", "");
  175. try {
  176. URL url = new URL(location.getProtocol(),
  177. location.getHost(), location.getPort(),
  178. basePath);
  179. widgetsets.put(classname, url);
  180. } catch (MalformedURLException e) {
  181. // should never happen as based on an existing URL,
  182. // only changing end of file name/path part
  183. e.printStackTrace();
  184. }
  185. }
  186. }
  187. }
  188. } else {
  189. try {
  190. // check files in jar file, entries will list all directories
  191. // and files in jar
  192. URLConnection openConnection = location.openConnection();
  193. if (openConnection instanceof JarURLConnection) {
  194. JarURLConnection conn = (JarURLConnection) openConnection;
  195. JarFile jarFile = conn.getJarFile();
  196. Manifest manifest = jarFile.getManifest();
  197. if (manifest == null) {
  198. // No manifest so this is not a Vaadin Add-on
  199. return;
  200. }
  201. String value = manifest.getMainAttributes().getValue(
  202. "Vaadin-Widgetsets");
  203. if (value != null) {
  204. String[] widgetsetNames = value.split(",");
  205. for (int i = 0; i < widgetsetNames.length; i++) {
  206. String widgetsetname = widgetsetNames[i].trim()
  207. .intern();
  208. if (!widgetsetname.equals("")) {
  209. widgetsets.put(widgetsetname, location);
  210. }
  211. }
  212. }
  213. }
  214. } catch (IOException e) {
  215. logger.log(Level.WARNING, "Error parsing jar file", e);
  216. }
  217. }
  218. }
  219. /**
  220. * Splits the current class path into entries, and filters them accepting
  221. * directories, Vaadin add-on JARs with widgetsets and Vaadin JARs.
  222. *
  223. * Some other non-JAR entries may also be included in the result.
  224. *
  225. * @return filtered list of class path entries
  226. */
  227. private final static List<String> getRawClasspathEntries() {
  228. // try to keep the order of the classpath
  229. List<String> locations = new ArrayList<String>();
  230. String pathSep = System.getProperty("path.separator");
  231. String classpath = System.getProperty("java.class.path");
  232. if (classpath.startsWith("\"")) {
  233. classpath = classpath.substring(1);
  234. }
  235. if (classpath.endsWith("\"")) {
  236. classpath = classpath.substring(0, classpath.length() - 1);
  237. }
  238. logger.fine("Classpath: " + classpath);
  239. String[] split = classpath.split(pathSep);
  240. for (int i = 0; i < split.length; i++) {
  241. String classpathEntry = split[i];
  242. if (acceptClassPathEntry(classpathEntry)) {
  243. locations.add(classpathEntry);
  244. }
  245. }
  246. return locations;
  247. }
  248. /**
  249. * Determine every URL location defined by the current classpath, and it's
  250. * associated package name.
  251. *
  252. * See {@link #classpathLocations} for information on output format.
  253. *
  254. * @param rawClasspathEntries
  255. * raw class path entries as split from the Java class path
  256. * string
  257. * @return map of classpath locations, see {@link #classpathLocations}
  258. */
  259. private final static Map<String, URL> getClasspathLocations(
  260. List<String> rawClasspathEntries) {
  261. // try to keep the order of the classpath
  262. Map<String, URL> locations = new LinkedHashMap<String, URL>();
  263. for (String classpathEntry : rawClasspathEntries) {
  264. File file = new File(classpathEntry);
  265. include(null, file, locations);
  266. }
  267. return locations;
  268. }
  269. /**
  270. * Checks a class path entry to see whether it can contain widgets and
  271. * widgetsets.
  272. *
  273. * All directories are automatically accepted. JARs are accepted if they
  274. * have the "Vaadin-Widgetsets" attribute in their manifest or the JAR file
  275. * name contains "vaadin-" or ".vaadin.".
  276. *
  277. * Also other non-JAR entries may be accepted, the caller should be prepared
  278. * to handle them.
  279. *
  280. * @param classpathEntry
  281. * class path entry string as given in the Java class path
  282. * @return true if the entry should be considered when looking for widgets
  283. * or widgetsets
  284. */
  285. private static boolean acceptClassPathEntry(String classpathEntry) {
  286. if (!classpathEntry.endsWith(".jar")) {
  287. // accept all non jars (practically directories)
  288. return true;
  289. } else {
  290. // accepts jars that comply with vaadin-component packaging
  291. // convention (.vaadin. or vaadin- as distribution packages),
  292. if (classpathEntry.contains("vaadin-")
  293. || classpathEntry.contains(".vaadin.")) {
  294. return true;
  295. } else {
  296. URL url;
  297. try {
  298. url = new URL("file:"
  299. + new File(classpathEntry).getCanonicalPath());
  300. url = new URL("jar:" + url.toExternalForm() + "!/");
  301. JarURLConnection conn = (JarURLConnection) url
  302. .openConnection();
  303. logger.fine(url.toString());
  304. JarFile jarFile = conn.getJarFile();
  305. Manifest manifest = jarFile.getManifest();
  306. if (manifest != null) {
  307. Attributes mainAttributes = manifest
  308. .getMainAttributes();
  309. if (mainAttributes.getValue("Vaadin-Widgetsets") != null) {
  310. return true;
  311. }
  312. }
  313. } catch (MalformedURLException e) {
  314. // TODO Auto-generated catch block
  315. e.printStackTrace();
  316. } catch (IOException e) {
  317. // TODO Auto-generated catch block
  318. e.printStackTrace();
  319. }
  320. return false;
  321. }
  322. }
  323. }
  324. /**
  325. * Recursively add subdirectories and jar files to locations - see
  326. * {@link #classpathLocations}.
  327. *
  328. * @param name
  329. * @param file
  330. * @param locations
  331. */
  332. private final static void include(String name, File file,
  333. Map<String, URL> locations) {
  334. if (!file.exists()) {
  335. return;
  336. }
  337. if (!file.isDirectory()) {
  338. // could be a JAR file
  339. includeJar(file, locations);
  340. return;
  341. }
  342. if (file.isHidden() || file.getPath().contains(File.separator + ".")) {
  343. return;
  344. }
  345. if (name == null) {
  346. name = "";
  347. } else {
  348. name += ".";
  349. }
  350. // add all directories recursively
  351. File[] dirs = file.listFiles(DIRECTORIES_ONLY);
  352. for (int i = 0; i < dirs.length; i++) {
  353. try {
  354. // add the present directory
  355. if (!dirs[i].isHidden()
  356. && !dirs[i].getPath().contains(File.separator + ".")) {
  357. String key = dirs[i].getCanonicalPath() + "/" + name
  358. + dirs[i].getName();
  359. locations.put(key,
  360. new URL("file://" + dirs[i].getCanonicalPath()));
  361. }
  362. } catch (Exception ioe) {
  363. return;
  364. }
  365. include(name + dirs[i].getName(), dirs[i], locations);
  366. }
  367. }
  368. /**
  369. * Add a jar file to locations - see {@link #classpathLocations}.
  370. *
  371. * @param name
  372. * @param locations
  373. */
  374. private static void includeJar(File file, Map<String, URL> locations) {
  375. try {
  376. URL url = new URL("file:" + file.getCanonicalPath());
  377. url = new URL("jar:" + url.toExternalForm() + "!/");
  378. JarURLConnection conn = (JarURLConnection) url.openConnection();
  379. JarFile jarFile = conn.getJarFile();
  380. if (jarFile != null) {
  381. // the key does not matter here as long as it is unique
  382. locations.put(url.toString(), url);
  383. }
  384. } catch (Exception e) {
  385. // e.printStackTrace();
  386. return;
  387. }
  388. }
  389. /**
  390. * Searches for all paintable classes and accept criteria under a location
  391. * based on {@link ClientWidget} and {@link ClientCriterion} annotations.
  392. *
  393. * Note that client criteria are updated directly to the
  394. * {@link #acceptCriterion} field, whereas paintables are added to the
  395. * paintables map given as a parameter.
  396. *
  397. * @param location
  398. * @param locationString
  399. * @param paintables
  400. */
  401. private final static void searchForPaintables(URL location,
  402. String locationString,
  403. Collection<Class<? extends Paintable>> paintables) {
  404. // Get a File object for the package
  405. File directory = new File(location.getFile());
  406. if (directory.exists() && !directory.isHidden()) {
  407. // Get the list of the files contained in the directory
  408. String[] files = directory.list();
  409. for (int i = 0; i < files.length; i++) {
  410. // we are only interested in .class files
  411. if (files[i].endsWith(".class")) {
  412. // remove the .class extension
  413. String classname = files[i].substring(0,
  414. files[i].length() - 6);
  415. String packageName = locationString
  416. .substring(locationString.lastIndexOf("/") + 1);
  417. classname = packageName + "." + classname;
  418. tryToAdd(classname, paintables);
  419. }
  420. }
  421. } else {
  422. try {
  423. // check files in jar file, entries will list all directories
  424. // and files in jar
  425. URLConnection openConnection = location.openConnection();
  426. if (openConnection instanceof JarURLConnection) {
  427. JarURLConnection conn = (JarURLConnection) openConnection;
  428. JarFile jarFile = conn.getJarFile();
  429. Enumeration<JarEntry> e = jarFile.entries();
  430. while (e.hasMoreElements()) {
  431. JarEntry entry = e.nextElement();
  432. String entryname = entry.getName();
  433. if (!entry.isDirectory()
  434. && entryname.endsWith(".class")) {
  435. String classname = entryname.substring(0,
  436. entryname.length() - 6);
  437. if (classname.startsWith("/")) {
  438. classname = classname.substring(1);
  439. }
  440. classname = classname.replace('/', '.');
  441. tryToAdd(classname, paintables);
  442. }
  443. }
  444. }
  445. } catch (IOException e) {
  446. logger.warning(e.toString());
  447. }
  448. }
  449. }
  450. /**
  451. * A print stream that ignores all output.
  452. *
  453. * This is used to hide error messages from static initializers of classes
  454. * being inspected.
  455. */
  456. private static PrintStream devnull = new PrintStream(new OutputStream() {
  457. @Override
  458. public void write(int b) throws IOException {
  459. // NOP
  460. }
  461. });
  462. /**
  463. * Collection of all {@link AcceptCriterion} classes, updated as a side
  464. * effect of {@link #searchForPaintables(URL, String, Collection)} based on
  465. * {@link ClientCriterion} annotations.
  466. */
  467. private static Set<Class<? extends AcceptCriterion>> acceptCriterion = new HashSet<Class<? extends AcceptCriterion>>();
  468. /**
  469. * Checks a class for the {@link ClientWidget} and {@link ClientCriterion}
  470. * annotations, and adds it to the appropriate collection if it has either.
  471. *
  472. * @param fullclassName
  473. * @param paintables
  474. * the collection to which to add server side classes with
  475. * {@link ClientWidget} annotation
  476. */
  477. private static void tryToAdd(final String fullclassName,
  478. Collection<Class<? extends Paintable>> paintables) {
  479. try {
  480. PrintStream out = System.out;
  481. PrintStream err = System.err;
  482. System.setErr(devnull);
  483. System.setOut(devnull);
  484. Class<?> c = Class.forName(fullclassName);
  485. System.setErr(err);
  486. System.setOut(out);
  487. if (c.getAnnotation(ClientWidget.class) != null) {
  488. paintables.add((Class<? extends Paintable>) c);
  489. // System.out.println("Found paintable " + fullclassName);
  490. } else if (c.getAnnotation(ClientCriterion.class) != null) {
  491. acceptCriterion.add((Class<? extends AcceptCriterion>) c);
  492. }
  493. } catch (ClassNotFoundException e) {
  494. // e.printStackTrace();
  495. } catch (LinkageError e) {
  496. // NOP
  497. } catch (Exception e) {
  498. e.printStackTrace();
  499. }
  500. }
  501. /**
  502. * Find and return the default source directory where to create new
  503. * widgetsets.
  504. *
  505. * Return the first directory (not a JAR file etc.) on the classpath by
  506. * default.
  507. *
  508. * TODO this could be done better...
  509. *
  510. * @return URL
  511. */
  512. public static URL getDefaultSourceDirectory() {
  513. if (logger.isLoggable(Level.FINE)) {
  514. logger.fine("classpathLocations values:");
  515. ArrayList<String> locations = new ArrayList<String>(
  516. classpathLocations.keySet());
  517. for (String location : locations) {
  518. logger.fine(String.valueOf(classpathLocations.get(location)));
  519. }
  520. }
  521. Iterator<String> it = rawClasspathEntries.iterator();
  522. while (it.hasNext()) {
  523. String entry = it.next();
  524. File directory = new File(entry);
  525. if (directory.exists() && !directory.isHidden()
  526. && directory.isDirectory()) {
  527. try {
  528. return new URL("file://" + directory.getCanonicalPath());
  529. } catch (MalformedURLException e) {
  530. e.printStackTrace();
  531. // ignore: continue to the next classpath entry
  532. } catch (IOException e) {
  533. e.printStackTrace();
  534. // ignore: continue to the next classpath entry
  535. }
  536. }
  537. }
  538. return null;
  539. }
  540. /**
  541. * Test method for helper tool
  542. */
  543. public static void main(String[] args) {
  544. Collection<Class<? extends Paintable>> paintables = ClassPathExplorer
  545. .getPaintablesHavingWidgetAnnotation();
  546. logger.info("Found annotated paintables:");
  547. for (Class<? extends Paintable> cls : paintables) {
  548. logger.info(cls.getCanonicalName());
  549. }
  550. logger.info("");
  551. logger.info("Searching available widgetsets...");
  552. Map<String, URL> availableWidgetSets = ClassPathExplorer
  553. .getAvailableWidgetSets();
  554. for (String string : availableWidgetSets.keySet()) {
  555. logger.info(string + " in " + availableWidgetSets.get(string));
  556. }
  557. }
  558. }