diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntry.java b/api/src/main/java/net/neoforged/jst/api/FileEntry.java similarity index 92% rename from cli/src/main/java/net/neoforged/jst/cli/io/SourceEntry.java rename to api/src/main/java/net/neoforged/jst/api/FileEntry.java index d8b8dfc..bc7b891 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntry.java +++ b/api/src/main/java/net/neoforged/jst/api/FileEntry.java @@ -1,10 +1,10 @@ -package net.neoforged.jst.cli.io; +package net.neoforged.jst.api; import java.io.IOException; import java.io.InputStream; import java.util.Locale; -public interface SourceEntry { +public interface FileEntry { /** * @return True for directories. */ diff --git a/api/src/main/java/net/neoforged/jst/api/FileSink.java b/api/src/main/java/net/neoforged/jst/api/FileSink.java new file mode 100644 index 0000000..3588f33 --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/FileSink.java @@ -0,0 +1,15 @@ +package net.neoforged.jst.api; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public interface FileSink extends AutoCloseable { + @Override + default void close() throws IOException { + } + + boolean isOrdered(); + + void put(FileEntry entry, byte[] content) throws IOException; +} diff --git a/api/src/main/java/net/neoforged/jst/api/FileSource.java b/api/src/main/java/net/neoforged/jst/api/FileSource.java new file mode 100644 index 0000000..a9fe38d --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/FileSource.java @@ -0,0 +1,19 @@ +package net.neoforged.jst.api; + +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; + +import java.io.IOException; +import java.util.stream.Stream; + +public interface FileSource extends AutoCloseable { + VirtualFile createSourceRoot(VirtualFileManager vfsManager); + + Stream streamEntries() throws IOException; + + boolean isOrdered(); + + @Override + default void close() throws IOException { + } +} diff --git a/api/src/main/java/net/neoforged/jst/api/IntelliJEnvironment.java b/api/src/main/java/net/neoforged/jst/api/IntelliJEnvironment.java new file mode 100644 index 0000000..01fd4c9 --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/IntelliJEnvironment.java @@ -0,0 +1,13 @@ +package net.neoforged.jst.api; + +import com.intellij.core.CoreApplicationEnvironment; +import com.intellij.core.JavaCoreProjectEnvironment; +import com.intellij.psi.PsiManager; + +public interface IntelliJEnvironment { + CoreApplicationEnvironment getAppEnv(); + + JavaCoreProjectEnvironment getProjectEnv(); + + PsiManager getPsiManager(); +} diff --git a/api/src/main/java/net/neoforged/jst/api/SourceTransformer.java b/api/src/main/java/net/neoforged/jst/api/SourceTransformer.java index d4a39d5..9f4835f 100644 --- a/api/src/main/java/net/neoforged/jst/api/SourceTransformer.java +++ b/api/src/main/java/net/neoforged/jst/api/SourceTransformer.java @@ -3,11 +3,12 @@ import com.intellij.psi.PsiFile; public interface SourceTransformer { - default void beforeRun() { + default void beforeRun(TransformContext context) { } - default void afterRun() { + default void afterRun(TransformContext context) { } void visitFile(PsiFile psiFile, Replacements replacements); } + diff --git a/api/src/main/java/net/neoforged/jst/api/TransformContext.java b/api/src/main/java/net/neoforged/jst/api/TransformContext.java new file mode 100644 index 0000000..a1963a9 --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/TransformContext.java @@ -0,0 +1,4 @@ +package net.neoforged.jst.api; + +public record TransformContext(IntelliJEnvironment environment, FileSource source, FileSink sink) { +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/IoSuppplier.java b/cli/src/main/java/net/neoforged/jst/cli/IoSuppplier.java new file mode 100644 index 0000000..4e9e8eb --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/IoSuppplier.java @@ -0,0 +1,8 @@ +package net.neoforged.jst.cli; + +import java.io.IOException; + +@FunctionalInterface +public interface IoSuppplier { + byte[] getContent() throws IOException; +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/Main.java b/cli/src/main/java/net/neoforged/jst/cli/Main.java index c1e6656..16c51b6 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/Main.java +++ b/cli/src/main/java/net/neoforged/jst/cli/Main.java @@ -2,8 +2,8 @@ import net.neoforged.jst.api.SourceTransformer; import net.neoforged.jst.api.SourceTransformerPlugin; -import net.neoforged.jst.cli.io.Sink; -import net.neoforged.jst.cli.io.Source; +import net.neoforged.jst.cli.io.FileSinks; +import net.neoforged.jst.cli.io.FileSources; import org.jetbrains.annotations.VisibleForTesting; import picocli.CommandLine; @@ -53,7 +53,7 @@ public static int innerMain(String... args) { @Override public Integer call() throws Exception { - try (var source = new Source(inputPath, inputFormat); + try (var source = FileSources.create(inputPath, inputFormat); var processor = new SourceFileProcessor()) { if (librariesList != null) { @@ -62,7 +62,7 @@ public Integer call() throws Exception { var orderedTransformers = new ArrayList<>(enabledTransformers); - try (var sink = new Sink(source, outputPath, outputFormat)) { + try (var sink = FileSinks.create(outputPath, outputFormat, source)) { processor.process(source, sink, orderedTransformers); } diff --git a/cli/src/main/java/net/neoforged/jst/cli/OrderedParallelWorkQueue.java b/cli/src/main/java/net/neoforged/jst/cli/OrderedParallelWorkQueue.java new file mode 100644 index 0000000..2d26509 --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/OrderedParallelWorkQueue.java @@ -0,0 +1,108 @@ +package net.neoforged.jst.cli; + +import net.neoforged.jst.api.FileEntry; +import net.neoforged.jst.api.FileSink; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Consumer; + +class OrderedParallelWorkQueue implements AutoCloseable { + private final Deque>> pending; + private final FileSink sink; + private final int maxQueueDepth; + + public OrderedParallelWorkQueue(FileSink sink, int maxQueueDepth) { + this.sink = sink; + this.maxQueueDepth = maxQueueDepth; + if (maxQueueDepth < 0) { + throw new IllegalArgumentException("Max queue depth must not be negative"); + } + this.pending = new ArrayDeque<>(maxQueueDepth); + } + + public void submit(Consumer producer) throws IOException { + if (pending.isEmpty()) { + // Can write directly if nothing else is pending + producer.accept(sink); + } else { + // Needs to be queued behind currently queued async work + submitAsync(producer); + } + } + + public void submitAsync(Consumer producer) { + try { + if (maxQueueDepth <= 0) { + // Forced into synchronous mode + submit(producer); + return; + } + drainTo(maxQueueDepth - 1); + pending.add(CompletableFuture.supplyAsync(() -> { + try (var parallelSink = new ParallelSink()) { + producer.accept(parallelSink); + return parallelSink.workResults; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + })); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static final class ParallelSink implements FileSink { + private final List workResults = new ArrayList<>(); + + @Override + public boolean isOrdered() { + return false; + } + + @Override + public void put(FileEntry entry, byte[] content) { + workResults.add(new WorkResult(entry, content)); + } + } + + private void drainTo(int drainTo) throws InterruptedException, IOException { + while (pending.size() > drainTo) { + List workResults; + try { + workResults = pending.removeFirst().get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof IOException ioe) { + throw ioe; + } + throw new RuntimeException(e.getCause()); + } + for (var workResult : workResults) { + sink.put(workResult.entry, workResult.content); + } + } + } + + @Override + public void close() throws IOException { + try { + drainTo(0); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + sink.close(); + } + } + + private record WorkResult(FileEntry entry, byte[] content) { + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/OrderedWorkQueue.java b/cli/src/main/java/net/neoforged/jst/cli/OrderedWorkQueue.java deleted file mode 100644 index 7f89acd..0000000 --- a/cli/src/main/java/net/neoforged/jst/cli/OrderedWorkQueue.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.neoforged.jst.cli; - -import java.io.IOException; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.function.Supplier; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -class OrderedWorkQueue implements AutoCloseable { - private final Deque> pending; - private final ZipOutputStream zout; - private final int maxQueueDepth; - - public OrderedWorkQueue(ZipOutputStream zout, int maxQueueDepth) { - this.zout = zout; - this.maxQueueDepth = maxQueueDepth; - if (maxQueueDepth < 0) { - throw new IllegalArgumentException("Max queue depth must not be negative"); - } - this.pending = new ArrayDeque<>(maxQueueDepth); - } - - public void submit(ZipEntry entry, byte[] content) throws InterruptedException, IOException { - if (pending.isEmpty()) { - // Can write directly if nothing else is pending - zout.putNextEntry(entry); - zout.write(content); - zout.closeEntry(); - } else { - // Needs to be queued behind currently queued async work - drainTo(maxQueueDepth - 1); - pending.add(CompletableFuture.completedFuture(new WorkResult(entry, content))); - } - } - - public void submitAsync(ZipEntry entry, Supplier contentSupplier) throws InterruptedException, IOException { - if (maxQueueDepth <= 0) { - // Forced into synchronous mode - submit(entry, contentSupplier.get()); - return; - } - drainTo(maxQueueDepth - 1); - pending.add(CompletableFuture.supplyAsync(() -> new WorkResult(entry, contentSupplier.get()))); - } - - private void drainTo(int drainTo) throws InterruptedException, IOException { - while (pending.size() > drainTo) { - WorkResult workResult; - try { - workResult = pending.removeFirst().get(); - } catch (ExecutionException e) { - if (e.getCause() instanceof IOException ioe) { - throw ioe; - } - throw new RuntimeException(e.getCause()); - } - zout.putNextEntry(workResult.entry); - zout.write(workResult.content); - zout.closeEntry(); - } - } - - @Override - public void close() throws IOException { - try { - drainTo(0); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } finally { - zout.close(); - } - } - - record WorkResult(ZipEntry entry, byte[] content) { - } -} diff --git a/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java b/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java index 98e2cde..1d511f6 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java +++ b/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java @@ -1,12 +1,15 @@ package net.neoforged.jst.cli; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import net.neoforged.jst.api.FileEntry; +import net.neoforged.jst.api.FileSink; +import net.neoforged.jst.api.FileSource; import net.neoforged.jst.api.Replacements; import net.neoforged.jst.api.SourceTransformer; +import net.neoforged.jst.api.TransformContext; import net.neoforged.jst.cli.intellij.ClasspathSetup; -import net.neoforged.jst.cli.intellij.IntelliJEnvironment; -import net.neoforged.jst.cli.io.Sink; -import net.neoforged.jst.cli.io.Source; +import net.neoforged.jst.cli.intellij.IntelliJEnvironmentImpl; import java.io.IOException; import java.io.UncheckedIOException; @@ -19,7 +22,7 @@ * https://github.com/JetBrains/kotlin/blob/22aa9ee65f759ad21aeaeb8ad9ac0b123b2c32fe/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt#L108 */ class SourceFileProcessor implements AutoCloseable { - private final IntelliJEnvironment ijEnv = new IntelliJEnvironment(); + private final IntelliJEnvironmentImpl ijEnv = new IntelliJEnvironmentImpl(); private int maxQueueDepth = 50; private boolean enableJavadoc = true; @@ -27,53 +30,46 @@ public SourceFileProcessor() throws IOException { ijEnv.addCurrentJdkToClassPath(); } - public void process(Source source, Sink sink, List transformers) throws IOException, InterruptedException { + public void process(FileSource source, FileSink sink, List transformers) throws IOException { - var sourceRoot = source.createSourceRoot(ijEnv.getAppEnv()); + var context = new TransformContext(ijEnv, source, sink); + + var sourceRoot = source.createSourceRoot(VirtualFileManager.getInstance()); ijEnv.addSourceRoot(sourceRoot); for (var transformer : transformers) { - transformer.beforeRun(); + transformer.beforeRun(context); } - var javaEnv = ijEnv.getProjectEnv(); if (sink.isOrdered()) { -// var asyncZout = new OrderedWorkQueue(new ZipOutputStream(fout), maxQueueDepth); - - source.streamEntries().forEach(entry -> { - - try (var in = entry.openInputStream()) { - if (entry.hasExtension("java")) { - var content = in.readAllBytes(); - content = transformSource(sourceRoot, entry.relativePath(), transformers, content); - sink.put(entry, content); - } else { - sink.put(entry, in); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + try (var stream = source.streamEntries()) { + stream.forEach(entry -> { + processEntry(entry, sourceRoot, transformers, sink); + }); + } } else { - - source.streamEntries().parallel().forEach(entry -> { - try (var in = entry.openInputStream()) { - if (entry.hasExtension("java")) { - var content = in.readAllBytes(); - content = transformSource(sourceRoot, entry.relativePath(), transformers, content); - sink.put(entry, content); - } else { - sink.put(entry, in); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - + try (var asyncOut = new OrderedParallelWorkQueue(sink, maxQueueDepth); + var stream = source.streamEntries()) { + stream.forEach(entry -> asyncOut.submitAsync(parallelSink -> { + processEntry(entry, sourceRoot, transformers, parallelSink); + })); + } } for (var transformer : transformers) { - transformer.afterRun(); + transformer.afterRun(context); + } + } + + private void processEntry(FileEntry entry, VirtualFile sourceRoot, List transformers, FileSink sink) { + try (var in = entry.openInputStream()) { + byte[] content = in.readAllBytes(); + if (entry.hasExtension("java")) { + content = transformSource(sourceRoot, entry.relativePath(), transformers, content); + } + sink.put(entry, content); + } catch (IOException e) { + throw new UncheckedIOException(e); } } diff --git a/cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java b/cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java index 2718337..4032b20 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java +++ b/cli/src/main/java/net/neoforged/jst/cli/intellij/ClasspathSetup.java @@ -61,7 +61,7 @@ public static void addJdkModules(Path jdkHome, JavaCoreProjectEnvironment javaEn System.out.println("Added " + moduleCount + " modules from " + jdkHome); } - public static void addLibraries(Path librariesPath, IntelliJEnvironment ijEnv) throws IOException { + public static void addLibraries(Path librariesPath, IntelliJEnvironmentImpl ijEnv) throws IOException { var libraryFiles = Files.readAllLines(librariesPath) .stream() .filter(l -> l.startsWith("-e=")) diff --git a/cli/src/main/java/net/neoforged/jst/cli/intellij/IntelliJEnvironment.java b/cli/src/main/java/net/neoforged/jst/cli/intellij/IntelliJEnvironmentImpl.java similarity index 96% rename from cli/src/main/java/net/neoforged/jst/cli/intellij/IntelliJEnvironment.java rename to cli/src/main/java/net/neoforged/jst/cli/intellij/IntelliJEnvironmentImpl.java index f357f1e..bccce97 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/intellij/IntelliJEnvironment.java +++ b/cli/src/main/java/net/neoforged/jst/cli/intellij/IntelliJEnvironmentImpl.java @@ -32,6 +32,7 @@ import com.intellij.psi.impl.source.tree.JavaTreeGenerator; import com.intellij.psi.impl.source.tree.TreeGenerator; import com.intellij.psi.util.JavaClassSupers; +import net.neoforged.jst.api.IntelliJEnvironment; import org.jetbrains.annotations.VisibleForTesting; import java.io.IOException; @@ -40,7 +41,7 @@ import java.nio.file.Paths; import java.util.Objects; -public class IntelliJEnvironment implements AutoCloseable { +public class IntelliJEnvironmentImpl implements IntelliJEnvironment, AutoCloseable { private final Disposable rootDisposable; private final Path tempDir; @@ -48,10 +49,10 @@ public class IntelliJEnvironment implements AutoCloseable { private final JavaCoreProjectEnvironment javaEnv; private final PsiManager psiManager; - public IntelliJEnvironment() throws IOException { + public IntelliJEnvironmentImpl() throws IOException { System.setProperty("java.awt.headless", "true"); - tempDir = Files.createTempDirectory("applyparchment"); + tempDir = Files.createTempDirectory("jst"); this.rootDisposable = Disposer.newDisposable(); System.setProperty("idea.home.path", tempDir.toAbsolutePath().toString()); @@ -78,14 +79,17 @@ protected VirtualFileSystem createJrtFileSystem() { psiManager = PsiManager.getInstance(project); } + @Override public PsiManager getPsiManager() { return psiManager; } + @Override public CoreApplicationEnvironment getAppEnv() { return javaEnv.getEnvironment(); } + @Override public JavaCoreProjectEnvironment getProjectEnv() { return javaEnv; } diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSink.java b/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSink.java new file mode 100644 index 0000000..8d7b9cd --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSink.java @@ -0,0 +1,40 @@ +package net.neoforged.jst.cli.io; + +import net.neoforged.jst.api.FileSink; +import net.neoforged.jst.api.FileEntry; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class ArchiveFileSink implements FileSink { + private final ZipOutputStream zout; + + public ArchiveFileSink(Path path) throws IOException { + this.zout = new ZipOutputStream(Files.newOutputStream(path)); + } + + @Override + public boolean isOrdered() { + return false; + } + + @Override + public void put(FileEntry entry, byte[] content) throws IOException { + var ze = new ZipEntry(entry.relativePath()); + ze.setLastModifiedTime(FileTime.from(Instant.now())); + zout.putNextEntry(ze); + zout.write(content); + zout.closeEntry(); + } + + @Override + public void close() throws IOException { + this.zout.close(); + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSource.java b/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSource.java new file mode 100644 index 0000000..8bbbe44 --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/ArchiveFileSource.java @@ -0,0 +1,42 @@ +package net.neoforged.jst.cli.io; + +import com.intellij.openapi.vfs.StandardFileSystems; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import net.neoforged.jst.api.FileSource; +import net.neoforged.jst.api.FileEntry; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.stream.Stream; +import java.util.zip.ZipFile; + +public class ArchiveFileSource implements FileSource { + private final Path path; + private final ZipFile zipFile; + + public ArchiveFileSource(Path path) throws IOException { + this.path = path; + this.zipFile = new ZipFile(path.toFile()); + } + + @Override + public VirtualFile createSourceRoot(VirtualFileManager vfsManager) { + return vfsManager.getFileSystem(StandardFileSystems.JAR_PROTOCOL).findFileByPath(path.toString() + "!/"); + } + + @Override + public Stream streamEntries() { + return zipFile.stream().map(ze -> new ZipFileEntry(zipFile, ze)); + } + + @Override + public boolean isOrdered() { + return true; + } + + @Override + public void close() throws IOException { + zipFile.close(); + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/FileSinks.java b/cli/src/main/java/net/neoforged/jst/cli/io/FileSinks.java new file mode 100644 index 0000000..755a486 --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/FileSinks.java @@ -0,0 +1,34 @@ +package net.neoforged.jst.cli.io; + +import net.neoforged.jst.api.FileSink; +import net.neoforged.jst.api.FileSource; +import net.neoforged.jst.cli.PathType; + +import java.io.IOException; +import java.nio.file.Path; + +public final class FileSinks { + private FileSinks() { + } + + public static FileSink create(Path path, PathType format, FileSource source) throws IOException { + if (format == PathType.AUTO) { + if (source instanceof SingleFileSource) { + format = PathType.SINGLE_FILE; + } else if (source instanceof ArchiveFileSource) { + format = PathType.ARCHIVE; + } else if (source instanceof FolderFileSource) { + format = PathType.FOLDER; + } else { + throw new IllegalArgumentException("Cannot auto-detect output format based on source: " + source.getClass()); + } + } + + return switch (format) { + case AUTO -> throw new IllegalArgumentException("Do not support AUTO for output when input also was AUTO!"); + case SINGLE_FILE -> new SingleFileSink(path); + case ARCHIVE -> new ArchiveFileSink(path); + case FOLDER -> new FolderFileSink(path); + }; + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/FileSources.java b/cli/src/main/java/net/neoforged/jst/cli/io/FileSources.java new file mode 100644 index 0000000..7d837c1 --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/FileSources.java @@ -0,0 +1,58 @@ +package net.neoforged.jst.cli.io; + +import net.neoforged.jst.api.FileSource; +import net.neoforged.jst.cli.PathType; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public final class FileSources { + private FileSources() { + } + + public static FileSource create(Path path, PathType format) throws IOException { + if (!Files.exists(path)) { + throw new FileNotFoundException("File does not exist: " + path); + } + + return switch (format) { + case AUTO -> { + // Directories are easy + if (Files.isDirectory(path)) { + yield new FolderFileSource(path); + } else if (Files.isRegularFile(path)) { + try { + // Try opening it as a ZIP-File first + yield new ArchiveFileSource(path); + } catch (IOException ignored) { + // Fall back to single-file + yield new SingleFileSource(path); + } + } else { + throw new IOException("Cannot detect type of " + path + " it is neither file nor folder."); + } + } + case SINGLE_FILE -> { + if (!Files.isRegularFile(path)) { + throw new IOException("Expected " + path + " to be a file."); + } + yield new SingleFileSource(path); + } + case ARCHIVE -> { + if (!Files.isRegularFile(path)) { + throw new IOException("Expected " + path + " to be a file."); + } + yield new ArchiveFileSource(path); + } + case FOLDER -> { + if (!Files.isDirectory(path)) { + throw new IOException("Expected " + path + " to be a directory."); + } + yield new FolderFileSource(path); + } + }; + } + +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java b/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java new file mode 100644 index 0000000..c7d6837 --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java @@ -0,0 +1,23 @@ +package net.neoforged.jst.cli.io; + +import net.neoforged.jst.api.FileEntry; +import net.neoforged.jst.api.FileSink; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; + +public record FolderFileSink(Path path) implements FileSink { + @Override + public void put(FileEntry entry, byte[] content) throws IOException { + var targetPath = path.resolve(entry.relativePath()); + Files.write(targetPath, content); + Files.setLastModifiedTime(path, FileTime.fromMillis(entry.lastModified())); + } + + @Override + public boolean isOrdered() { + return false; + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSource.java b/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSource.java new file mode 100644 index 0000000..a742984 --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSource.java @@ -0,0 +1,29 @@ +package net.neoforged.jst.cli.io; + +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import net.neoforged.jst.api.FileSource; +import net.neoforged.jst.api.FileEntry; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public record FolderFileSource(Path path) implements FileSource, AutoCloseable { + @Override + public VirtualFile createSourceRoot(VirtualFileManager vfsManager) { + return vfsManager.findFileByNioPath(path); + } + + @Override + public Stream streamEntries() throws IOException { + return Files.walk(path) + .map(child -> new PathEntry(path, child)); + } + + @Override + public boolean isOrdered() { + return false; // We currently do not guarantee ordering of the file-tree + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/PathSourceEntry.java b/cli/src/main/java/net/neoforged/jst/cli/io/PathEntry.java similarity index 89% rename from cli/src/main/java/net/neoforged/jst/cli/io/PathSourceEntry.java rename to cli/src/main/java/net/neoforged/jst/cli/io/PathEntry.java index 210476f..b273489 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/PathSourceEntry.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/PathEntry.java @@ -1,19 +1,21 @@ package net.neoforged.jst.cli.io; +import net.neoforged.jst.api.FileEntry; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; -final class PathSourceEntry implements SourceEntry { +final class PathEntry implements FileEntry { private final Path relativeTo; private final Path path; private final String relativePath; private final boolean directory; private final long lastModified; - public PathSourceEntry(Path relativeTo, Path path) { + public PathEntry(Path relativeTo, Path path) { this.directory = Files.isDirectory(path); this.relativeTo = relativeTo; this.path = path; diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSink.java b/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSink.java new file mode 100644 index 0000000..7d59e08 --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSink.java @@ -0,0 +1,29 @@ +package net.neoforged.jst.cli.io; + +import net.neoforged.jst.api.FileEntry; +import net.neoforged.jst.api.FileSink; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; + +public record SingleFileSink(Path path) implements FileSink { + + @Override + public void put(FileEntry entry, byte[] content) throws IOException { + Path targetPath; + if (Files.isDirectory(path)) { + targetPath = path.resolve(entry.relativePath()); + } else { + targetPath = path; + } + Files.write(targetPath, content); + Files.setLastModifiedTime(path, FileTime.fromMillis(entry.lastModified())); + } + + @Override + public boolean isOrdered() { + return false; + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSource.java b/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSource.java new file mode 100644 index 0000000..6b7b7ac --- /dev/null +++ b/cli/src/main/java/net/neoforged/jst/cli/io/SingleFileSource.java @@ -0,0 +1,26 @@ +package net.neoforged.jst.cli.io; + +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import net.neoforged.jst.api.FileSource; +import net.neoforged.jst.api.FileEntry; + +import java.nio.file.Path; +import java.util.stream.Stream; + +public record SingleFileSource(Path path) implements FileSource, AutoCloseable { + @Override + public VirtualFile createSourceRoot(VirtualFileManager vfsManager) { + return vfsManager.findFileByNioPath(path.getParent()); + } + + @Override + public Stream streamEntries() { + return Stream.of(new PathEntry(path.getParent(), path)); + } + + @Override + public boolean isOrdered() { + return false; + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/Sink.java b/cli/src/main/java/net/neoforged/jst/cli/io/Sink.java deleted file mode 100644 index b346bcb..0000000 --- a/cli/src/main/java/net/neoforged/jst/cli/io/Sink.java +++ /dev/null @@ -1,75 +0,0 @@ -package net.neoforged.jst.cli.io; - -import net.neoforged.jst.cli.PathType; -import org.jetbrains.annotations.Nullable; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.FileTime; -import java.time.Instant; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -public class Sink implements AutoCloseable { - private final Path path; - private final PathType format; - @Nullable - private final ZipOutputStream zout; - - public Sink(Source source, Path outputPath, PathType outputFormat) throws IOException { - this.format = outputFormat == PathType.AUTO ? source.getFormat() : outputFormat; - this.path = outputPath; - this.zout = this.format == PathType.ARCHIVE ? new ZipOutputStream(Files.newOutputStream(outputPath)) : null; - } - - public void put(SourceEntry entry, byte[] content) throws IOException { - put(entry, new ByteArrayInputStream(content)); - } - - public void put(SourceEntry entry, InputStream content) throws IOException { - switch (format) { - case SINGLE_FILE -> { - Path targetPath; - if (Files.isDirectory(path)) { - targetPath = path.resolve(entry.relativePath()); - } else { - targetPath = path; - } - try (var out = Files.newOutputStream(targetPath)) { - content.transferTo(out); - } - Files.setLastModifiedTime(path, FileTime.fromMillis(entry.lastModified())); - } - case ARCHIVE -> { - if (zout != null) { - var ze = new ZipEntry(entry.relativePath()); - ze.setLastModifiedTime(FileTime.from(Instant.now())); - zout.putNextEntry(ze); - content.transferTo(zout); - zout.closeEntry(); - } - } - case FOLDER -> { - try (var out = Files.newOutputStream(path.resolve(entry.relativePath()))) { - content.transferTo(out); - } - Files.setLastModifiedTime(path, FileTime.fromMillis(entry.lastModified())); - } - default -> throw new IllegalStateException("Unexpected format: " + format); - } - } - - @Override - public void close() throws Exception { - if (zout != null) { - zout.close(); - } - } - - public boolean isOrdered() { - return format == PathType.ARCHIVE; - } -} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/Source.java b/cli/src/main/java/net/neoforged/jst/cli/io/Source.java deleted file mode 100644 index cb4b07e..0000000 --- a/cli/src/main/java/net/neoforged/jst/cli/io/Source.java +++ /dev/null @@ -1,127 +0,0 @@ -package net.neoforged.jst.cli.io; - -import com.intellij.core.CoreApplicationEnvironment; -import com.intellij.openapi.vfs.VirtualFile; -import net.neoforged.jst.cli.PathType; -import org.jetbrains.annotations.Nullable; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class Source implements AutoCloseable { - private final Path path; - private final PathType format; - @Nullable - private ZipFile zf = null; - @Nullable - private Stream directoryStream; - - public Source(Path path, PathType format) throws IOException { - this.path = path; - - if (!Files.exists(path)) { - throw new FileNotFoundException("File does not exist: " + path); - } - - this.format = switch (format) { - case AUTO -> { - // Directories are easy - if (Files.isDirectory(path)) { - this.zf = null; - yield PathType.FOLDER; - } else if (Files.isRegularFile(path)) { - // Try opening it as a ZIP-File first - try { - zf = new ZipFile(path.toFile()); - } catch (IOException ignored) { - } - yield zf != null ? PathType.ARCHIVE : PathType.SINGLE_FILE; - } else { - throw new IOException("Cannot detect type of " + path + " it is neither file nor folder."); - } - } - case SINGLE_FILE -> { - if (!Files.isRegularFile(path)) { - throw new IOException("Expected " + path + " to be a file."); - } - yield PathType.SINGLE_FILE; - } - case ARCHIVE -> { - if (!Files.isRegularFile(path)) { - throw new IOException("Expected " + path + " to be a file."); - } - zf = new ZipFile(path.toFile()); - yield PathType.ARCHIVE; - } - case FOLDER -> { - if (!Files.isDirectory(path)) { - throw new IOException("Expected " + path + " to be a directory."); - } - yield PathType.FOLDER; - } - }; - } - - public Path getPath() { - return path; - } - - public PathType getFormat() { - return format; - } - - public Stream streamEntries() throws IOException { - switch (format) { - case SINGLE_FILE -> { - return Stream.of(new PathSourceEntry(path.getParent(), path)); - } - case ARCHIVE -> { - return createArchiveStream(); - } - case FOLDER -> { - return Files.walk(path) - .map(child -> new PathSourceEntry(path, child)); - } - default -> throw new IllegalStateException("Unexpected format: " + format); - } - } - - private Stream createArchiveStream() { - assert zf != null; - - Spliterator spliterator = Spliterators.spliterator(zf.entries().asIterator(), zf.size(), Spliterator.IMMUTABLE | Spliterator.ORDERED); - - return StreamSupport.stream(spliterator, false).map(ze -> new ZipFileSourceEntry(zf, ze)); - } - - public VirtualFile createSourceRoot(CoreApplicationEnvironment env) { - return switch (format) { - case SINGLE_FILE -> env.getLocalFileSystem().findFileByNioFile(path.getParent()); - case FOLDER -> env.getLocalFileSystem().findFileByNioFile(path); - case ARCHIVE -> env.getJarFileSystem().findFileByPath(path.toString() + "!/"); - default -> throw new IllegalStateException("Unexpected format: " + format); - }; - } - - @Override - public void close() throws IOException { - if (zf != null) { - zf.close(); - } - if (directoryStream != null) { - directoryStream.close(); - } - } - - public boolean isOrdered() { - return format == PathType.ARCHIVE; - } -} diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntryWithContent.java b/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntryWithContent.java index 7f48ee1..5c8d45d 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntryWithContent.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/SourceEntryWithContent.java @@ -1,6 +1,8 @@ package net.neoforged.jst.cli.io; +import net.neoforged.jst.api.FileEntry; + import java.io.InputStream; -public record SourceEntryWithContent(SourceEntry sourceEntry, InputStream contentStream) { +public record SourceEntryWithContent(FileEntry sourceEntry, InputStream contentStream) { } diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/ZipFileSourceEntry.java b/cli/src/main/java/net/neoforged/jst/cli/io/ZipFileEntry.java similarity index 83% rename from cli/src/main/java/net/neoforged/jst/cli/io/ZipFileSourceEntry.java rename to cli/src/main/java/net/neoforged/jst/cli/io/ZipFileEntry.java index 050d2c4..db771cc 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/ZipFileSourceEntry.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/ZipFileEntry.java @@ -1,15 +1,17 @@ package net.neoforged.jst.cli.io; +import net.neoforged.jst.api.FileEntry; + import java.io.IOException; import java.io.InputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -class ZipFileSourceEntry implements SourceEntry { +class ZipFileEntry implements FileEntry { private final ZipFile zipFile; private final ZipEntry zipEntry; - public ZipFileSourceEntry(ZipFile zipFile, ZipEntry zipEntry) { + public ZipFileEntry(ZipFile zipFile, ZipEntry zipEntry) { this.zipFile = zipFile; this.zipEntry = zipEntry; } diff --git a/cli/src/test/java/net/neoforged/jst/cli/PsiHelperTest.java b/cli/src/test/java/net/neoforged/jst/cli/PsiHelperTest.java index 6d1d2c2..1f17fb4 100644 --- a/cli/src/test/java/net/neoforged/jst/cli/PsiHelperTest.java +++ b/cli/src/test/java/net/neoforged/jst/cli/PsiHelperTest.java @@ -5,7 +5,7 @@ import com.intellij.psi.PsiMethod; import com.intellij.psi.util.PsiTreeUtil; import net.neoforged.jst.api.PsiHelper; -import net.neoforged.jst.cli.intellij.IntelliJEnvironment; +import net.neoforged.jst.cli.intellij.IntelliJEnvironmentImpl; import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -17,11 +17,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; class PsiHelperTest { - static IntelliJEnvironment ijEnv; + static IntelliJEnvironmentImpl ijEnv; @BeforeAll static void setUp() throws IOException { - ijEnv = new IntelliJEnvironment(); + ijEnv = new IntelliJEnvironmentImpl(); } @AfterAll diff --git a/parchment/src/main/java/net/neoforged/jst/parchment/ParchmentTransformer.java b/parchment/src/main/java/net/neoforged/jst/parchment/ParchmentTransformer.java index ba831b8..9cc8ae8 100644 --- a/parchment/src/main/java/net/neoforged/jst/parchment/ParchmentTransformer.java +++ b/parchment/src/main/java/net/neoforged/jst/parchment/ParchmentTransformer.java @@ -3,6 +3,7 @@ import com.intellij.psi.PsiFile; import net.neoforged.jst.api.Replacements; import net.neoforged.jst.api.SourceTransformer; +import net.neoforged.jst.api.TransformContext; import net.neoforged.jst.parchment.namesanddocs.NameAndDocSourceLoader; import net.neoforged.jst.parchment.namesanddocs.NamesAndDocsDatabase; import picocli.CommandLine; @@ -21,7 +22,7 @@ public class ParchmentTransformer implements SourceTransformer { private NamesAndDocsDatabase namesAndDocs; @Override - public void beforeRun() { + public void beforeRun(TransformContext context) { System.out.println("Loading mapping file " + mappingsPath); try { namesAndDocs = NameAndDocSourceLoader.load(mappingsPath);