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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. /*
  2. * Copyright 2000-2016 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.server.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. /**
  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 final String VAADIN_ADDON_VERSION_ATTRIBUTE = "Vaadin-Package-Version";
  54. /**
  55. * File filter that only accepts directories.
  56. */
  57. private final static FileFilter DIRECTORIES_ONLY = (File f) -> {
  58. if (f.exists() && f.isDirectory()) {
  59. return true;
  60. } else {
  61. return false;
  62. }
  63. };
  64. /**
  65. * Contains information about widgetsets and themes found on the classpath
  66. *
  67. * @since 7.1
  68. */
  69. public static class LocationInfo {
  70. private final Map<String, URL> widgetsets;
  71. private final Map<String, URL> addonStyles;
  72. public LocationInfo(Map<String, URL> widgetsets,
  73. Map<String, URL> themes) {
  74. this.widgetsets = widgetsets;
  75. addonStyles = themes;
  76. }
  77. public Map<String, URL> getWidgetsets() {
  78. return widgetsets;
  79. }
  80. public Map<String, URL> getAddonStyles() {
  81. return addonStyles;
  82. }
  83. }
  84. /**
  85. * Raw class path entries as given in the java class path string. Only
  86. * entries that could include widgets/widgetsets are listed (primarily
  87. * directories, Vaadin JARs and add-on JARs).
  88. */
  89. private static List<String> rawClasspathEntries = getRawClasspathEntries();
  90. /**
  91. * Map from identifiers (either a package name preceded by the path and a
  92. * slash, or a URL for a JAR file) to the corresponding URLs. This is
  93. * constructed from the class path.
  94. */
  95. private static Map<String, URL> classpathLocations = getClasspathLocations(
  96. rawClasspathEntries);
  97. private static boolean debug = false;
  98. static {
  99. String debugProperty = System.getProperty("debug");
  100. if (debugProperty != null && !debugProperty.isEmpty()) {
  101. debug = true;
  102. }
  103. }
  104. /**
  105. * No instantiation from outside, callable methods are static.
  106. */
  107. private ClassPathExplorer() {
  108. }
  109. /**
  110. * Finds the names and locations of widgetsets available on the class path.
  111. *
  112. * @return map from widgetset classname to widgetset location URL
  113. * @deprecated Use {@link #getAvailableWidgetSetsAndStylesheets()} instead
  114. */
  115. @Deprecated
  116. public static Map<String, URL> getAvailableWidgetSets() {
  117. return getAvailableWidgetSetsAndStylesheets().getWidgetsets();
  118. }
  119. /**
  120. * Finds the names and locations of widgetsets and themes available on the
  121. * class path.
  122. *
  123. * @return
  124. */
  125. public static LocationInfo getAvailableWidgetSetsAndStylesheets() {
  126. long start = System.currentTimeMillis();
  127. Map<String, URL> widgetsets = new HashMap<>();
  128. Map<String, URL> themes = new HashMap<>();
  129. Set<String> keySet = classpathLocations.keySet();
  130. for (String location : keySet) {
  131. searchForWidgetSetsAndAddonStyles(location, widgetsets, themes);
  132. }
  133. long end = System.currentTimeMillis();
  134. StringBuilder sb = new StringBuilder();
  135. sb.append("Widgetsets found from classpath:\n");
  136. for (String ws : widgetsets.keySet()) {
  137. sb.append("\t");
  138. sb.append(ws);
  139. sb.append(" in ");
  140. sb.append(widgetsets.get(ws));
  141. sb.append("\n");
  142. }
  143. sb.append("Addon styles found from classpath:\n");
  144. for (String theme : themes.keySet()) {
  145. sb.append("\t");
  146. sb.append(theme);
  147. sb.append(" in ");
  148. sb.append(themes.get(theme));
  149. sb.append("\n");
  150. }
  151. log(sb.toString());
  152. log("Search took " + (end - start) + "ms");
  153. return new LocationInfo(widgetsets, themes);
  154. }
  155. /**
  156. * Finds all GWT modules / Vaadin widgetsets and Addon styles in a valid
  157. * location.
  158. *
  159. * If the location is a directory, all GWT modules (files with the
  160. * ".gwt.xml" extension) are added to widgetsets.
  161. *
  162. * If the location is a JAR file, the comma-separated values of the
  163. * "Vaadin-Widgetsets" attribute in its manifest are added to widgetsets.
  164. *
  165. * @param locationString
  166. * an entry in {@link #classpathLocations}
  167. * @param widgetsets
  168. * a map from widgetset name (including package, with dots as
  169. * separators) to a URL (see {@link #classpathLocations}) - new
  170. * entries are added to this map
  171. */
  172. private static void searchForWidgetSetsAndAddonStyles(String locationString,
  173. Map<String, URL> widgetsets, Map<String, URL> addonStyles) {
  174. URL location = classpathLocations.get(locationString);
  175. File directory = new File(location.getFile());
  176. if (directory.exists() && !directory.isHidden()) {
  177. // Get the list of the files contained in the directory
  178. String[] files = directory.list();
  179. for (int i = 0; i < files.length; i++) {
  180. // we are only interested in .gwt.xml files
  181. if (!files[i].endsWith(".gwt.xml")) {
  182. continue;
  183. }
  184. // remove the .gwt.xml extension
  185. String classname = files[i].substring(0, files[i].length() - 8);
  186. String packageName = locationString
  187. .substring(locationString.lastIndexOf('/') + 1);
  188. classname = packageName + "." + classname;
  189. if (!WidgetSetBuilder.isWidgetset(classname)) {
  190. // Only return widgetsets and not GWT modules to avoid
  191. // comparing modules and widgetsets
  192. continue;
  193. }
  194. if (!widgetsets.containsKey(classname)) {
  195. String packagePath = packageName.replaceAll("\\.", "/");
  196. String basePath = location.getFile();
  197. if (basePath.endsWith("/" + packagePath)) {
  198. basePath = basePath.replaceAll("/" + packagePath + "$",
  199. "");
  200. } else if (basePath.endsWith("/" + packagePath + "/")) {
  201. basePath = basePath.replaceAll("/" + packagePath + "/$",
  202. "");
  203. } else {
  204. throw new IllegalStateException(
  205. "Error trying to find base path, location ("
  206. + location.getFile()
  207. + ") does not end in expected '/"
  208. + packagePath + "'");
  209. }
  210. try {
  211. URL url = new URL(location.getProtocol(),
  212. location.getHost(), location.getPort(),
  213. basePath);
  214. widgetsets.put(classname, url);
  215. } catch (MalformedURLException e) {
  216. // should never happen as based on an existing URL,
  217. // only changing end of file name/path part
  218. error("Error locating the widgetset " + classname, e);
  219. }
  220. }
  221. }
  222. } else {
  223. try {
  224. // check files in jar file, entries will list all directories
  225. // and files in jar
  226. URLConnection openConnection = location.openConnection();
  227. if (openConnection instanceof JarURLConnection) {
  228. JarURLConnection conn = (JarURLConnection) openConnection;
  229. JarFile jarFile = conn.getJarFile();
  230. Manifest manifest = jarFile.getManifest();
  231. if (manifest == null) {
  232. // No manifest so this is not a Vaadin Add-on
  233. return;
  234. }
  235. // Check for widgetset attribute
  236. String value = manifest.getMainAttributes()
  237. .getValue("Vaadin-Widgetsets");
  238. if (value != null) {
  239. String[] widgetsetNames = value.split(",");
  240. for (String widgetsetName : widgetsetNames) {
  241. String widgetsetname = widgetsetName.trim();
  242. if (!widgetsetname.isEmpty()) {
  243. widgetsets.put(widgetsetname, location);
  244. }
  245. }
  246. }
  247. // Check for theme attribute
  248. value = manifest.getMainAttributes()
  249. .getValue("Vaadin-Stylesheets");
  250. if (value != null) {
  251. String[] stylesheets = value.split(",");
  252. for (String stylesheet1 : stylesheets) {
  253. String stylesheet = stylesheet1.trim();
  254. if (!stylesheet.isEmpty()) {
  255. addonStyles.put(stylesheet, location);
  256. }
  257. }
  258. }
  259. }
  260. } catch (IOException e) {
  261. error("Error parsing jar file", e);
  262. }
  263. }
  264. }
  265. /**
  266. * Splits the current class path into entries, and filters them accepting
  267. * directories, Vaadin add-on JARs with widgetsets and Vaadin JARs.
  268. *
  269. * Some other non-JAR entries may also be included in the result.
  270. *
  271. * @return filtered list of class path entries
  272. */
  273. private final static List<String> getRawClasspathEntries() {
  274. // try to keep the order of the classpath
  275. List<String> locations = new ArrayList<>();
  276. String pathSep = System.getProperty("path.separator");
  277. String classpath = System.getProperty("java.class.path");
  278. if (classpath.startsWith("\"")) {
  279. classpath = classpath.substring(1);
  280. }
  281. if (classpath.endsWith("\"")) {
  282. classpath = classpath.substring(0, classpath.length() - 1);
  283. }
  284. debug("Classpath: " + classpath);
  285. String[] split = classpath.split(pathSep);
  286. for (int i = 0; i < split.length; i++) {
  287. String classpathEntry = split[i];
  288. if (acceptClassPathEntry(classpathEntry)) {
  289. locations.add(classpathEntry);
  290. }
  291. }
  292. return locations;
  293. }
  294. /**
  295. * Determine every URL location defined by the current classpath, and it's
  296. * associated package name.
  297. *
  298. * See {@link #classpathLocations} for information on output format.
  299. *
  300. * @param rawClasspathEntries
  301. * raw class path entries as split from the Java class path
  302. * string
  303. * @return map of classpath locations, see {@link #classpathLocations}
  304. */
  305. private final static Map<String, URL> getClasspathLocations(
  306. List<String> rawClasspathEntries) {
  307. long start = System.currentTimeMillis();
  308. // try to keep the order of the classpath
  309. Map<String, URL> locations = new LinkedHashMap<>();
  310. for (String classpathEntry : rawClasspathEntries) {
  311. File file = new File(classpathEntry);
  312. include(null, file, locations);
  313. }
  314. long end = System.currentTimeMillis();
  315. if (debug) {
  316. debug("getClassPathLocations took " + (end - start) + "ms");
  317. }
  318. return locations;
  319. }
  320. /**
  321. * Checks a class path entry to see whether it can contain widgets and
  322. * widgetsets.
  323. *
  324. * All directories are automatically accepted. JARs are accepted if they
  325. * have the "Vaadin-Widgetsets" attribute in their manifest or the JAR file
  326. * name contains "vaadin-" or ".vaadin.".
  327. *
  328. * Also other non-JAR entries may be accepted, the caller should be prepared
  329. * to handle them.
  330. *
  331. * @param classpathEntry
  332. * class path entry string as given in the Java class path
  333. * @return true if the entry should be considered when looking for widgets
  334. * or widgetsets
  335. */
  336. private static boolean acceptClassPathEntry(String classpathEntry) {
  337. if (!classpathEntry.endsWith(".jar")) {
  338. // accept all non jars (practically directories)
  339. return true;
  340. } else {
  341. // accepts jars that comply with vaadin-component packaging
  342. // convention (.vaadin. or vaadin- as distribution packages),
  343. if (classpathEntry.contains("vaadin-")
  344. || classpathEntry.contains(".vaadin.")) {
  345. return true;
  346. } else {
  347. URL url;
  348. try {
  349. url = new URL("file:"
  350. + new File(classpathEntry).getCanonicalPath());
  351. url = new URL("jar:" + url.toExternalForm() + "!/");
  352. JarURLConnection conn = (JarURLConnection) url
  353. .openConnection();
  354. debug(url.toString());
  355. JarFile jarFile = conn.getJarFile();
  356. Manifest manifest = jarFile.getManifest();
  357. if (manifest != null) {
  358. Attributes mainAttributes = manifest
  359. .getMainAttributes();
  360. if (mainAttributes
  361. .getValue("Vaadin-Widgetsets") != null) {
  362. return true;
  363. }
  364. if (mainAttributes
  365. .getValue("Vaadin-Stylesheets") != null) {
  366. return true;
  367. }
  368. }
  369. } catch (MalformedURLException e) {
  370. if (debug) {
  371. error("Failed to inspect JAR file", e);
  372. }
  373. } catch (IOException e) {
  374. if (debug) {
  375. error("Failed to inspect JAR file", e);
  376. }
  377. }
  378. return false;
  379. }
  380. }
  381. }
  382. /**
  383. * Recursively add subdirectories and jar files to locations - see
  384. * {@link #classpathLocations}.
  385. *
  386. * @param name
  387. * @param file
  388. * @param locations
  389. */
  390. private final static void include(String name, File file,
  391. Map<String, URL> locations) {
  392. if (!file.exists()) {
  393. return;
  394. }
  395. if (!file.isDirectory()) {
  396. // could be a JAR file
  397. includeJar(file, locations);
  398. return;
  399. }
  400. if (file.isHidden() || file.getPath().contains(File.separator + ".")) {
  401. return;
  402. }
  403. if (name == null) {
  404. name = "";
  405. } else {
  406. name += ".";
  407. }
  408. // add all directories recursively
  409. File[] dirs = file.listFiles(DIRECTORIES_ONLY);
  410. for (int i = 0; i < dirs.length; i++) {
  411. try {
  412. // add the present directory
  413. if (!dirs[i].isHidden()
  414. && !dirs[i].getPath().contains(File.separator + ".")) {
  415. String key = dirs[i].getCanonicalPath() + "/" + name
  416. + dirs[i].getName();
  417. URL url = dirs[i].getCanonicalFile().toURI().toURL();
  418. locations.put(key, url);
  419. }
  420. } catch (Exception ioe) {
  421. return;
  422. }
  423. include(name + dirs[i].getName(), dirs[i], locations);
  424. }
  425. }
  426. /**
  427. * Add a jar file to locations - see {@link #classpathLocations}.
  428. *
  429. * @param name
  430. * @param locations
  431. */
  432. private static void includeJar(File file, Map<String, URL> locations) {
  433. try {
  434. URL url = new URL("file:" + file.getCanonicalPath());
  435. url = new URL("jar:" + url.toExternalForm() + "!/");
  436. JarURLConnection conn = (JarURLConnection) url.openConnection();
  437. JarFile jarFile = conn.getJarFile();
  438. if (jarFile != null) {
  439. // the key does not matter here as long as it is unique
  440. locations.put(url.toString(), url);
  441. }
  442. } catch (Exception e) {
  443. // e.printStackTrace();
  444. return;
  445. }
  446. }
  447. /**
  448. * Find and return the default source directory where to create new
  449. * widgetsets.
  450. *
  451. * Return the first directory (not a JAR file etc.) on the classpath by
  452. * default.
  453. *
  454. * TODO this could be done better...
  455. *
  456. * @return URL
  457. */
  458. public static URL getDefaultSourceDirectory() {
  459. return getWidgetsetSourceDirectory(null);
  460. }
  461. /**
  462. * Find and return the source directory which contains the given widgetset
  463. * file.
  464. *
  465. * If not applicable or widgetsetFileName is null, return the first
  466. * directory (not a JAR file etc.) on the classpath.
  467. *
  468. * TODO this could be done better...
  469. *
  470. * @since 7.6.5
  471. *
  472. * @param widgetsetFileName
  473. * relative path for the widgetset
  474. *
  475. * @return URL
  476. */
  477. public static URL getWidgetsetSourceDirectory(String widgetsetFileName) {
  478. if (debug) {
  479. debug("classpathLocations values:");
  480. ArrayList<String> locations = new ArrayList<>(
  481. classpathLocations.keySet());
  482. for (String location : locations) {
  483. debug(String.valueOf(classpathLocations.get(location)));
  484. }
  485. }
  486. URL firstDirectory = null;
  487. Iterator<String> it = rawClasspathEntries.iterator();
  488. while (it.hasNext()) {
  489. String entry = it.next();
  490. File directory = new File(entry);
  491. if (directory.exists() && !directory.isHidden()
  492. && directory.isDirectory()) {
  493. try {
  494. URL directoryUrl = directory.toURI().toURL();
  495. // Store the first directory encountered.
  496. if (firstDirectory == null) {
  497. firstDirectory = directoryUrl;
  498. }
  499. if (widgetsetFileName == null
  500. || new File(directory, widgetsetFileName)
  501. .exists()) {
  502. return directoryUrl;
  503. }
  504. } catch (MalformedURLException e) {
  505. // ignore: continue to the next classpath entry
  506. if (debug) {
  507. e.printStackTrace();
  508. }
  509. }
  510. }
  511. }
  512. return firstDirectory;
  513. }
  514. /**
  515. * Test method for helper tool
  516. */
  517. public static void main(String[] args) {
  518. log("Searching for available widgetsets and stylesheets...");
  519. ClassPathExplorer.getAvailableWidgetSetsAndStylesheets();
  520. }
  521. private static void log(String message) {
  522. System.out.println(message);
  523. }
  524. private static void error(String message, Exception e) {
  525. System.err.println(message);
  526. e.printStackTrace();
  527. }
  528. private static void debug(String message) {
  529. if (debug) {
  530. System.out.println(message);
  531. }
  532. }
  533. }