PageRenderTime 57ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/src/main/java/org/openstreetmap/josm/plugins/scripting/graalvm/esmodule/JarESModuleRepository.java

https://github.com/Gubaer/josm-scripting-plugin
Java | 276 lines | 213 code | 14 blank | 49 comment | 28 complexity | 88492bed2fae82ce217dde01a054dbf4 MD5 | raw file
  1. package org.openstreetmap.josm.plugins.scripting.graalvm.esmodule;
  2. import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
  3. import org.openstreetmap.josm.plugins.scripting.graalvm.ModuleJarURI;
  4. import javax.validation.constraints.NotNull;
  5. import javax.validation.constraints.Null;
  6. import java.io.File;
  7. import java.io.IOException;
  8. import java.net.MalformedURLException;
  9. import java.net.URI;
  10. import java.net.URISyntaxException;
  11. import java.nio.channels.SeekableByteChannel;
  12. import java.nio.file.Path;
  13. import java.text.MessageFormat;
  14. import java.util.List;
  15. import java.util.Objects;
  16. import java.util.function.Supplier;
  17. import java.util.jar.JarFile;
  18. import java.util.logging.Level;
  19. import java.util.logging.Logger;
  20. import java.util.regex.Pattern;
  21. /**
  22. * <code>JarESModuleRepository</code> is a repository of ES Modules stored in a jar file.
  23. */
  24. public class JarESModuleRepository extends AbstractESModuleRepository {
  25. private static final Logger logger = Logger.getLogger(JarESModuleRepository.class.getName());
  26. static private void logFine(Supplier<String> supplier) {
  27. if (logger.isLoggable(Level.FINE)) {
  28. logger.fine(supplier.get());
  29. }
  30. }
  31. private final JarFile jar;
  32. private final File jarFile;
  33. private final Path root;
  34. final private static Pattern LEADING_SLASHES = Pattern.compile("^/+");
  35. static private String removeLeadingSlashes(String path) {
  36. return LEADING_SLASHES.matcher(path).replaceFirst("");
  37. }
  38. private static final List<String> SUFFIXES = List.of("", ".mjs", ".js");
  39. private Path resolveZipEntryPath(@NotNull Path relativeModulePath) {
  40. if (relativeModulePath.isAbsolute()) {
  41. // paths to zip entries in a jar file don't start with
  42. // a '/'. Remove leading '/'.
  43. relativeModulePath = Path.of(removeLeadingSlashes(relativeModulePath.toString()));
  44. }
  45. // remove/resolve any './' or '..' in the path and prepend the repository
  46. // root
  47. final var path = Path.of(
  48. root.toString(),
  49. relativeModulePath.normalize().toString()
  50. );
  51. logFine(() -> MessageFormat.format("normalized relative repo path is ''{0}''", path));
  52. // try to locate a suitable zip entry
  53. return SUFFIXES.stream().map(suffix -> path + suffix)
  54. .filter(p -> {
  55. var entry = jar.getEntry(p);
  56. logFine(() -> MessageFormat.format("Tried relative repo path ''{0}'', found entry ''{1}''", p, entry));
  57. return entry != null && !entry.isDirectory();
  58. })
  59. .findFirst()
  60. .map(Path::of)
  61. .orElse(null);
  62. }
  63. /**
  64. * {@inheritDoc}
  65. */
  66. @Override
  67. public URI getBaseURI() {
  68. try {
  69. return ModuleJarURI.buildJarUri(jarFile.getAbsolutePath(), root.toString());
  70. } catch (MalformedURLException | URISyntaxException e) {
  71. // shouldn't happen
  72. throw new RuntimeException(e);
  73. }
  74. }
  75. /**
  76. * Creates a repository of ES Modules stored in a jar file.
  77. *
  78. * @param jarFile the jar file
  79. * @throws IOException if <code>jarFile</code> doesn't exist or isn't a jar file
  80. * @throws NullPointerException if <code>jarFile</code> is null
  81. */
  82. public JarESModuleRepository(@NotNull final File jarFile) throws IOException {
  83. Objects.requireNonNull(jarFile);
  84. this.jarFile = jarFile;
  85. this.jar = new JarFile(jarFile);
  86. this.root = Path.of("");
  87. }
  88. /**
  89. * Creates a repository of ES Modules stored in a jar file. ES Modules are stored
  90. * in the subtree of jar entries given by <code>rootEntry</code>.
  91. *
  92. * @param jarFile the jar file
  93. * @param rootEntry the path to the root entry for the repository
  94. * @throws IOException if <code>jarFile</code> doesn't exist or isn't a jar file
  95. * @throws IOException if <code>jarFile</code> or <code>rootEntry</code> is null
  96. * @throws IllegalArgumentException if there isn't an entry with name <code>rootEntry</code> in the <code>jarFile</code>
  97. */
  98. public JarESModuleRepository(@NotNull final File jarFile, @NotNull final String rootEntry) throws IOException {
  99. Objects.requireNonNull(jarFile);
  100. Objects.requireNonNull(rootEntry);
  101. this.jarFile = jarFile;
  102. this.jar = new JarFile(jarFile);
  103. var path = Path.of(rootEntry).normalize();
  104. if (path.isAbsolute()) {
  105. this.root = Path.of(removeLeadingSlashes(path.toString()));
  106. } else {
  107. this.root = path;
  108. }
  109. var entry = this.jar.getEntry(this.root.toString());
  110. if (entry == null) {
  111. throw new IllegalArgumentException(MessageFormat.format(
  112. "Root entry ''{0}'' not found in jar file ''{1}''",
  113. rootEntry,
  114. this.jarFile.getAbsolutePath()
  115. ));
  116. }
  117. }
  118. /**
  119. * Creates a repository of ES Modules stored in a jar file.
  120. *
  121. * @param uri a jar-URI
  122. * @throws IOException thrown if the jar-file doesn't exist or isnt' readable
  123. * @throws IOException thrown if the jar-URI includes a root entry which doesn't exist,
  124. * isn't directory entry, or isn't readable
  125. * @throws IllegalESModuleBaseUri thrown if <code>uri</code> isn't a valid jar-URI
  126. * @throws NullPointerException thrown if <code>uri</code> is null
  127. */
  128. public JarESModuleRepository(@NotNull final URI uri) throws IOException, IllegalESModuleBaseUri {
  129. Objects.requireNonNull(uri);
  130. try {
  131. final var moduleJarUri = new ModuleJarURI(uri);
  132. if (!moduleJarUri.refersToJarFile()) {
  133. throw new IllegalESModuleBaseUri(MessageFormat.format(
  134. "Jar-file doesn''t exist or isn''t readable. uri=''{0}''", uri));
  135. }
  136. if (!moduleJarUri.getJarEntryName().isEmpty() && !moduleJarUri.refersToDirectoryJarEntry()) {
  137. throw new IllegalESModuleBaseUri(MessageFormat.format(
  138. "Root entry doesn''t exist or isn''t a directory entry. uri=''{0}''", uri));
  139. }
  140. this.jarFile = moduleJarUri.getJarFile();
  141. this.jar = new JarFile(jarFile);
  142. this.root = Path.of(moduleJarUri.getJarEntryName());
  143. } catch(IllegalArgumentException e) {
  144. throw new IllegalESModuleBaseUri(MessageFormat.format(
  145. "Illegal base URI for jar-file based ES Module repository. uri=''{0}''", uri), e);
  146. }
  147. }
  148. /**
  149. * {@inheritDoc}
  150. */
  151. @Override
  152. public @Null Path resolveModulePath(@NotNull String modulePath) {
  153. Objects.requireNonNull(modulePath);
  154. return resolveModulePath(Path.of(modulePath));
  155. }
  156. /**
  157. * {@inheritDoc}
  158. */
  159. @Override
  160. public @Null Path resolveModulePath(@NotNull Path modulePath) {
  161. Objects.requireNonNull(modulePath);
  162. if (modulePath.isAbsolute()) {
  163. var normalizedModulePath= modulePath.normalize();
  164. logFine(() -> MessageFormat.format(
  165. "{0}: normalized module path is ''{1}''",
  166. modulePath,
  167. normalizedModulePath
  168. ));
  169. if (normalizedModulePath.startsWith(getUniquePathPrefix())) {
  170. if (normalizedModulePath.getNameCount() < 3) {
  171. logFine(() -> MessageFormat.format(
  172. "{0}: normalized absolute module path ''{1}'' is too short",
  173. modulePath,
  174. normalizedModulePath
  175. ));
  176. logFine(() -> MessageFormat.format("{0}: resolution FAILED", modulePath));
  177. return null;
  178. }
  179. final var relativeRepoPath = normalizedModulePath.subpath(2, normalizedModulePath.getNameCount());
  180. logFine(() -> MessageFormat.format("{0}: relative repo path is ''{1}''", modulePath, relativeRepoPath));
  181. var resolvedRelativeRepoPath = resolveZipEntryPath(relativeRepoPath);
  182. if (resolvedRelativeRepoPath == null) {
  183. logFine(() -> MessageFormat.format("{0}: resolution FAILED", modulePath));
  184. return null;
  185. }
  186. if (root.toString().isEmpty()) {
  187. return Path.of(getUniquePathPrefix().toString(), resolvedRelativeRepoPath.toString());
  188. } else {
  189. return Path.of(
  190. getUniquePathPrefix().toString(),
  191. resolvedRelativeRepoPath.subpath(
  192. root.getNameCount(),
  193. resolvedRelativeRepoPath.getNameCount()).toString()
  194. );
  195. }
  196. } else {
  197. logFine(() -> MessageFormat.format(
  198. "{0}: can''t resolve absolute module path in the file system based ES module repository with unique prefix ''{1}''",
  199. modulePath.toString(),
  200. getUniquePathPrefix().toString()
  201. ));
  202. return null;
  203. }
  204. } else {
  205. var repoPath = resolveZipEntryPath(modulePath);
  206. if (repoPath == null) {
  207. logFine(() -> MessageFormat.format(
  208. "{0}: can''t resolve relative module path in the jar file based ES Module repository ''{1}''. "
  209. +"The path doesn''t refer to a readable zip entry.",
  210. modulePath.toString(),
  211. jarFile.getAbsolutePath()
  212. ));
  213. logFine(() -> MessageFormat.format("{0}: resolution FAILED", modulePath));
  214. return null;
  215. }
  216. return Path.of(
  217. getUniquePathPrefix().toString(),
  218. modulePath.normalize().toString()
  219. );
  220. }
  221. }
  222. /**
  223. * {@inheritDoc}
  224. */
  225. @Override
  226. public @NotNull SeekableByteChannel newByteChannel(@NotNull Path absolutePath) throws IOException {
  227. if (!absolutePath.startsWith(getUniquePathPrefix())) {
  228. throw new IllegalArgumentException(MessageFormat.format(
  229. "Can''t resolve path ''{0}''. Path doesn''t match unique path prefix ''{1}'' of jar file based ES Module repository",
  230. absolutePath.toString(),
  231. getUniquePathPrefix().toString()
  232. ));
  233. }
  234. if (absolutePath.getNameCount() < 3) {
  235. throw new IllegalArgumentException(MessageFormat.format(
  236. "Can''t resolve path ''{0}''. Path is too short.",
  237. absolutePath.toString(),
  238. getUniquePathPrefix().toString()
  239. ));
  240. }
  241. final var relativeRepoPath = absolutePath.subpath(2, absolutePath.getNameCount());
  242. final var zipEntryPath = resolveZipEntryPath(relativeRepoPath);
  243. if (zipEntryPath == null) {
  244. throw new IllegalArgumentException(MessageFormat.format(
  245. "Can''t resolve path ''{0}''. Didn''t find a zip entry under this path in the repo ''{1}''",
  246. absolutePath.toString(),
  247. getUniquePathPrefix().toString()
  248. ));
  249. }
  250. final var zipEntry = jar.getEntry(zipEntryPath.toString());
  251. if (zipEntry == null) {
  252. // shouldn't happen, but just in case
  253. throw new IllegalArgumentException(MessageFormat.format(
  254. "Can''t resolve path ''{0}''. Didn''t find a zip entry under this path in the repo ''{1}''",
  255. absolutePath.toString(),
  256. getUniquePathPrefix().toString()
  257. ));
  258. }
  259. final var bytes = jar.getInputStream(zipEntry).readAllBytes();
  260. return new SeekableInMemoryByteChannel(bytes);
  261. }
  262. }