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 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. /*
  2. @VaadinApache2LicenseForJavaFiles@
  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.net.JarURLConnection;
  9. import java.net.MalformedURLException;
  10. import java.net.URL;
  11. import java.net.URLConnection;
  12. import java.util.ArrayList;
  13. import java.util.HashMap;
  14. import java.util.Iterator;
  15. import java.util.LinkedHashMap;
  16. import java.util.List;
  17. import java.util.Map;
  18. import java.util.Set;
  19. import java.util.jar.Attributes;
  20. import java.util.jar.JarFile;
  21. import java.util.jar.Manifest;
  22. import java.util.logging.Level;
  23. import java.util.logging.Logger;
  24. /**
  25. * Utility class to collect widgetset related information from classpath.
  26. * Utility will seek all directories from classpaths, and jar files having
  27. * "Vaadin-Widgetsets" key in their manifest file.
  28. * <p>
  29. * Used by WidgetMapGenerator and ide tools to implement some monkey coding for
  30. * you.
  31. * <p>
  32. * Developer notice: If you end up reading this comment, I guess you have faced
  33. * a sluggish performance of widget compilation or unreliable detection of
  34. * components in your classpaths. The thing you might be able to do is to use
  35. * annotation processing tool like apt to generate the needed information. Then
  36. * either use that information in {@link WidgetMapGenerator} or create the
  37. * appropriate monkey code for gwt directly in annotation processor and get rid
  38. * of {@link WidgetMapGenerator}. Using annotation processor might be a good
  39. * idea when dropping Java 1.5 support (integrated to javac in 6).
  40. *
  41. */
  42. public class ClassPathExplorer {
  43. private static final String VAADIN_ADDON_VERSION_ATTRIBUTE = "Vaadin-Package-Version";
  44. /**
  45. * File filter that only accepts directories.
  46. */
  47. private final static FileFilter DIRECTORIES_ONLY = new FileFilter() {
  48. @Override
  49. public boolean accept(File f) {
  50. if (f.exists() && f.isDirectory()) {
  51. return true;
  52. } else {
  53. return false;
  54. }
  55. }
  56. };
  57. /**
  58. * Raw class path entries as given in the java class path string. Only
  59. * entries that could include widgets/widgetsets are listed (primarily
  60. * directories, Vaadin JARs and add-on JARs).
  61. */
  62. private static List<String> rawClasspathEntries = getRawClasspathEntries();
  63. /**
  64. * Map from identifiers (either a package name preceded by the path and a
  65. * slash, or a URL for a JAR file) to the corresponding URLs. This is
  66. * constructed from the class path.
  67. */
  68. private static Map<String, URL> classpathLocations = getClasspathLocations(rawClasspathEntries);
  69. /**
  70. * No instantiation from outside, callable methods are static.
  71. */
  72. private ClassPathExplorer() {
  73. }
  74. /**
  75. * Finds the names and locations of widgetsets available on the class path.
  76. *
  77. * @return map from widgetset classname to widgetset location URL
  78. */
  79. public static Map<String, URL> getAvailableWidgetSets() {
  80. long start = System.currentTimeMillis();
  81. Map<String, URL> widgetsets = new HashMap<String, URL>();
  82. Set<String> keySet = classpathLocations.keySet();
  83. for (String location : keySet) {
  84. searchForWidgetSets(location, widgetsets);
  85. }
  86. long end = System.currentTimeMillis();
  87. StringBuilder sb = new StringBuilder();
  88. sb.append("Widgetsets found from classpath:\n");
  89. for (String ws : widgetsets.keySet()) {
  90. sb.append("\t");
  91. sb.append(ws);
  92. sb.append(" in ");
  93. sb.append(widgetsets.get(ws));
  94. sb.append("\n");
  95. }
  96. final Logger logger = getLogger();
  97. logger.info(sb.toString());
  98. logger.info("Search took " + (end - start) + "ms");
  99. return widgetsets;
  100. }
  101. /**
  102. * Finds all GWT modules / Vaadin widgetsets in a valid location.
  103. *
  104. * If the location is a directory, all GWT modules (files with the
  105. * ".gwt.xml" extension) are added to widgetsets.
  106. *
  107. * If the location is a JAR file, the comma-separated values of the
  108. * "Vaadin-Widgetsets" attribute in its manifest are added to widgetsets.
  109. *
  110. * @param locationString
  111. * an entry in {@link #classpathLocations}
  112. * @param widgetsets
  113. * a map from widgetset name (including package, with dots as
  114. * separators) to a URL (see {@link #classpathLocations}) - new
  115. * entries are added to this map
  116. */
  117. private static void searchForWidgetSets(String locationString,
  118. Map<String, URL> widgetsets) {
  119. URL location = classpathLocations.get(locationString);
  120. File directory = new File(location.getFile());
  121. if (directory.exists() && !directory.isHidden()) {
  122. // Get the list of the files contained in the directory
  123. String[] files = directory.list();
  124. for (int i = 0; i < files.length; i++) {
  125. // we are only interested in .gwt.xml files
  126. if (!files[i].endsWith(".gwt.xml")) {
  127. continue;
  128. }
  129. // remove the .gwt.xml extension
  130. String classname = files[i].substring(0, files[i].length() - 8);
  131. String packageName = locationString.substring(locationString
  132. .lastIndexOf("/") + 1);
  133. classname = packageName + "." + classname;
  134. if (!WidgetSetBuilder.isWidgetset(classname)) {
  135. // Only return widgetsets and not GWT modules to avoid
  136. // comparing modules and widgetsets
  137. continue;
  138. }
  139. if (!widgetsets.containsKey(classname)) {
  140. String packagePath = packageName.replaceAll("\\.", "/");
  141. String basePath = location.getFile().replaceAll(
  142. "/" + packagePath + "$", "");
  143. try {
  144. URL url = new URL(location.getProtocol(),
  145. location.getHost(), location.getPort(),
  146. basePath);
  147. widgetsets.put(classname, url);
  148. } catch (MalformedURLException e) {
  149. // should never happen as based on an existing URL,
  150. // only changing end of file name/path part
  151. getLogger().log(Level.SEVERE,
  152. "Error locating the widgetset " + classname, e);
  153. }
  154. }
  155. }
  156. } else {
  157. try {
  158. // check files in jar file, entries will list all directories
  159. // and files in jar
  160. URLConnection openConnection = location.openConnection();
  161. if (openConnection instanceof JarURLConnection) {
  162. JarURLConnection conn = (JarURLConnection) openConnection;
  163. JarFile jarFile = conn.getJarFile();
  164. Manifest manifest = jarFile.getManifest();
  165. if (manifest == null) {
  166. // No manifest so this is not a Vaadin Add-on
  167. return;
  168. }
  169. String value = manifest.getMainAttributes().getValue(
  170. "Vaadin-Widgetsets");
  171. if (value != null) {
  172. String[] widgetsetNames = value.split(",");
  173. for (int i = 0; i < widgetsetNames.length; i++) {
  174. String widgetsetname = widgetsetNames[i].trim()
  175. .intern();
  176. if (!widgetsetname.equals("")) {
  177. widgetsets.put(widgetsetname, location);
  178. }
  179. }
  180. }
  181. }
  182. } catch (IOException e) {
  183. getLogger().log(Level.WARNING, "Error parsing jar file", e);
  184. }
  185. }
  186. }
  187. /**
  188. * Splits the current class path into entries, and filters them accepting
  189. * directories, Vaadin add-on JARs with widgetsets and Vaadin JARs.
  190. *
  191. * Some other non-JAR entries may also be included in the result.
  192. *
  193. * @return filtered list of class path entries
  194. */
  195. private final static List<String> getRawClasspathEntries() {
  196. // try to keep the order of the classpath
  197. List<String> locations = new ArrayList<String>();
  198. String pathSep = System.getProperty("path.separator");
  199. String classpath = System.getProperty("java.class.path");
  200. if (classpath.startsWith("\"")) {
  201. classpath = classpath.substring(1);
  202. }
  203. if (classpath.endsWith("\"")) {
  204. classpath = classpath.substring(0, classpath.length() - 1);
  205. }
  206. getLogger().fine("Classpath: " + classpath);
  207. String[] split = classpath.split(pathSep);
  208. for (int i = 0; i < split.length; i++) {
  209. String classpathEntry = split[i];
  210. if (acceptClassPathEntry(classpathEntry)) {
  211. locations.add(classpathEntry);
  212. }
  213. }
  214. return locations;
  215. }
  216. /**
  217. * Determine every URL location defined by the current classpath, and it's
  218. * associated package name.
  219. *
  220. * See {@link #classpathLocations} for information on output format.
  221. *
  222. * @param rawClasspathEntries
  223. * raw class path entries as split from the Java class path
  224. * string
  225. * @return map of classpath locations, see {@link #classpathLocations}
  226. */
  227. private final static Map<String, URL> getClasspathLocations(
  228. List<String> rawClasspathEntries) {
  229. long start = System.currentTimeMillis();
  230. // try to keep the order of the classpath
  231. Map<String, URL> locations = new LinkedHashMap<String, URL>();
  232. for (String classpathEntry : rawClasspathEntries) {
  233. File file = new File(classpathEntry);
  234. include(null, file, locations);
  235. }
  236. long end = System.currentTimeMillis();
  237. Logger logger = getLogger();
  238. if (logger.isLoggable(Level.FINE)) {
  239. logger.fine("getClassPathLocations took " + (end - start) + "ms");
  240. }
  241. return locations;
  242. }
  243. /**
  244. * Checks a class path entry to see whether it can contain widgets and
  245. * widgetsets.
  246. *
  247. * All directories are automatically accepted. JARs are accepted if they
  248. * have the "Vaadin-Widgetsets" attribute in their manifest or the JAR file
  249. * name contains "vaadin-" or ".vaadin.".
  250. *
  251. * Also other non-JAR entries may be accepted, the caller should be prepared
  252. * to handle them.
  253. *
  254. * @param classpathEntry
  255. * class path entry string as given in the Java class path
  256. * @return true if the entry should be considered when looking for widgets
  257. * or widgetsets
  258. */
  259. private static boolean acceptClassPathEntry(String classpathEntry) {
  260. if (!classpathEntry.endsWith(".jar")) {
  261. // accept all non jars (practically directories)
  262. return true;
  263. } else {
  264. // accepts jars that comply with vaadin-component packaging
  265. // convention (.vaadin. or vaadin- as distribution packages),
  266. if (classpathEntry.contains("vaadin-")
  267. || classpathEntry.contains(".vaadin.")) {
  268. return true;
  269. } else {
  270. URL url;
  271. try {
  272. url = new URL("file:"
  273. + new File(classpathEntry).getCanonicalPath());
  274. url = new URL("jar:" + url.toExternalForm() + "!/");
  275. JarURLConnection conn = (JarURLConnection) url
  276. .openConnection();
  277. getLogger().fine(url.toString());
  278. JarFile jarFile = conn.getJarFile();
  279. Manifest manifest = jarFile.getManifest();
  280. if (manifest != null) {
  281. Attributes mainAttributes = manifest
  282. .getMainAttributes();
  283. if (mainAttributes.getValue("Vaadin-Widgetsets") != null) {
  284. return true;
  285. }
  286. }
  287. } catch (MalformedURLException e) {
  288. getLogger().log(Level.FINEST, "Failed to inspect JAR file",
  289. e);
  290. } catch (IOException e) {
  291. getLogger().log(Level.FINEST, "Failed to inspect JAR file",
  292. e);
  293. }
  294. return false;
  295. }
  296. }
  297. }
  298. /**
  299. * Recursively add subdirectories and jar files to locations - see
  300. * {@link #classpathLocations}.
  301. *
  302. * @param name
  303. * @param file
  304. * @param locations
  305. */
  306. private final static void include(String name, File file,
  307. Map<String, URL> locations) {
  308. if (!file.exists()) {
  309. return;
  310. }
  311. if (!file.isDirectory()) {
  312. // could be a JAR file
  313. includeJar(file, locations);
  314. return;
  315. }
  316. if (file.isHidden() || file.getPath().contains(File.separator + ".")) {
  317. return;
  318. }
  319. if (name == null) {
  320. name = "";
  321. } else {
  322. name += ".";
  323. }
  324. // add all directories recursively
  325. File[] dirs = file.listFiles(DIRECTORIES_ONLY);
  326. for (int i = 0; i < dirs.length; i++) {
  327. try {
  328. // add the present directory
  329. if (!dirs[i].isHidden()
  330. && !dirs[i].getPath().contains(File.separator + ".")) {
  331. String key = dirs[i].getCanonicalPath() + "/" + name
  332. + dirs[i].getName();
  333. locations.put(key,
  334. new URL("file://" + dirs[i].getCanonicalPath()));
  335. }
  336. } catch (Exception ioe) {
  337. return;
  338. }
  339. include(name + dirs[i].getName(), dirs[i], locations);
  340. }
  341. }
  342. /**
  343. * Add a jar file to locations - see {@link #classpathLocations}.
  344. *
  345. * @param name
  346. * @param locations
  347. */
  348. private static void includeJar(File file, Map<String, URL> locations) {
  349. try {
  350. URL url = new URL("file:" + file.getCanonicalPath());
  351. url = new URL("jar:" + url.toExternalForm() + "!/");
  352. JarURLConnection conn = (JarURLConnection) url.openConnection();
  353. JarFile jarFile = conn.getJarFile();
  354. if (jarFile != null) {
  355. // the key does not matter here as long as it is unique
  356. locations.put(url.toString(), url);
  357. }
  358. } catch (Exception e) {
  359. // e.printStackTrace();
  360. return;
  361. }
  362. }
  363. /**
  364. * Find and return the default source directory where to create new
  365. * widgetsets.
  366. *
  367. * Return the first directory (not a JAR file etc.) on the classpath by
  368. * default.
  369. *
  370. * TODO this could be done better...
  371. *
  372. * @return URL
  373. */
  374. public static URL getDefaultSourceDirectory() {
  375. final Logger logger = getLogger();
  376. if (logger.isLoggable(Level.FINE)) {
  377. logger.fine("classpathLocations values:");
  378. ArrayList<String> locations = new ArrayList<String>(
  379. classpathLocations.keySet());
  380. for (String location : locations) {
  381. logger.fine(String.valueOf(classpathLocations.get(location)));
  382. }
  383. }
  384. Iterator<String> it = rawClasspathEntries.iterator();
  385. while (it.hasNext()) {
  386. String entry = it.next();
  387. File directory = new File(entry);
  388. if (directory.exists() && !directory.isHidden()
  389. && directory.isDirectory()) {
  390. try {
  391. return new URL("file://" + directory.getCanonicalPath());
  392. } catch (MalformedURLException e) {
  393. logger.log(Level.FINEST, "Ignoring exception", e);
  394. // ignore: continue to the next classpath entry
  395. } catch (IOException e) {
  396. logger.log(Level.FINEST, "Ignoring exception", e);
  397. // ignore: continue to the next classpath entry
  398. }
  399. }
  400. }
  401. return null;
  402. }
  403. /**
  404. * Test method for helper tool
  405. */
  406. public static void main(String[] args) {
  407. getLogger().info("Searching available widgetsets...");
  408. Map<String, URL> availableWidgetSets = ClassPathExplorer
  409. .getAvailableWidgetSets();
  410. for (String string : availableWidgetSets.keySet()) {
  411. getLogger().info(string + " in " + availableWidgetSets.get(string));
  412. }
  413. }
  414. private static final Logger getLogger() {
  415. return Logger.getLogger(ClassPathExplorer.class.getName());
  416. }
  417. }