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

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