diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java index ef8867dee5..460a56537a 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/FileCollector.java @@ -48,6 +48,7 @@ import net.sourceforge.pmd.util.log.MessageReporter; */ @SuppressWarnings("PMD.CloseResource") public final class FileCollector implements AutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(FileCollector.class); private final List allFilesToProcess = new ArrayList<>(); @@ -167,8 +168,11 @@ public final class FileCollector implements AutoCloseable { reporter.error("Not a regular file {}", file); return false; } - NioTextFile nioTextFile = new NioTextFile(file, charset, discoverer.getDefaultLanguageVersion(language), getDisplayName(file)); - addFileImpl(nioTextFile); + LanguageVersion lv = discoverer.getDefaultLanguageVersion(language); + Objects.requireNonNull(lv); + addFileImpl(TextFile.builderForPath(file, charset, lv) + .withDisplayName(getDisplayName(file)) + .build()); return true; } @@ -371,7 +375,7 @@ public final class FileCollector implements AutoCloseable { */ public void exclude(FileCollector excludeCollector) { Set toExclude = new HashSet<>(excludeCollector.allFilesToProcess); - for (Iterator iterator = allFilesToProcess.iterator(); iterator.hasNext();) { + for (Iterator iterator = allFilesToProcess.iterator(); iterator.hasNext(); ) { TextFile file = iterator.next(); if (toExclude.contains(file)) { LOG.trace("Excluding file {}", file.getPathId()); @@ -385,7 +389,7 @@ public final class FileCollector implements AutoCloseable { * collection. */ public void filterLanguages(Set languages) { - for (Iterator iterator = allFilesToProcess.iterator(); iterator.hasNext();) { + for (Iterator iterator = allFilesToProcess.iterator(); iterator.hasNext(); ) { TextFile file = iterator.next(); Language lang = file.getLanguageVersion().getLanguage(); if (!languages.contains(lang)) { diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java index 21f96da6d7..f362a231cc 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/NioTextFile.java @@ -27,13 +27,19 @@ class NioTextFile extends BaseCloseable implements TextFile { private final Charset charset; private final LanguageVersion languageVersion; private final @Nullable String displayName; + private boolean readOnly; - NioTextFile(Path path, Charset charset, LanguageVersion languageVersion, @Nullable String displayName) { + NioTextFile(Path path, + Charset charset, + LanguageVersion languageVersion, + @Nullable String displayName, + boolean readOnly) { AssertionUtil.requireParamNotNull("path", path); AssertionUtil.requireParamNotNull("charset", charset); AssertionUtil.requireParamNotNull("language version", languageVersion); this.displayName = displayName; + this.readOnly = readOnly; this.path = path; this.charset = charset; this.languageVersion = languageVersion; @@ -56,12 +62,15 @@ class NioTextFile extends BaseCloseable implements TextFile { @Override public boolean isReadOnly() { - return !Files.isWritable(path); + return readOnly || !Files.isWritable(path); } @Override public void writeContents(TextFileContent content) throws IOException { ensureOpen(); + if (isReadOnly()) { + throw new ReadOnlyFileException(); + } try (BufferedWriter bw = Files.newBufferedWriter(path, charset)) { if (content.getLineTerminator().equals(TextFileContent.NORMALIZED_LINE_TERM)) { content.getNormalizedText().writeFully(bw); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileBuilder.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileBuilder.java index 2c1e066f10..f240b51b81 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileBuilder.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/document/TextFileBuilder.java @@ -26,10 +26,41 @@ public abstract class TextFileBuilder { this.languageVersion = AssertionUtil.requireParamNotNull("language version", languageVersion); } + /** + * Specify that the built file is read only. Some text files are + * always read-only. + * + * @return This builder + */ + public TextFileBuilder asReadOnly() { + // default is appropriate if the file type is always read-only + return this; + } + + + /** + * Sets a custom display name for the new file. If null, or this is + * never called, the display name defaults to the path ID. + * + * @param displayName A display name + * + * @return This builder + */ + public TextFileBuilder withDisplayName(@Nullable String displayName) { + this.displayName = displayName; + return this; + } + + /** + * Creates and returns the new text file. + */ + public abstract TextFile build(); + static class ForNio extends TextFileBuilder { private final Path path; private final Charset charset; + private boolean readOnly = false; ForNio(LanguageVersion languageVersion, Path path, Charset charset) { super(languageVersion); @@ -37,9 +68,15 @@ public abstract class TextFileBuilder { this.charset = AssertionUtil.requireParamNotNull("charset", charset); } + @Override + public TextFileBuilder asReadOnly() { + readOnly = true; + return this; + } + @Override public TextFile build() { - return new NioTextFile(path, charset, languageVersion, displayName); + return new NioTextFile(path, charset, languageVersion, displayName, readOnly); } } @@ -77,22 +114,4 @@ public abstract class TextFileBuilder { } } - - /** - * Sets a custom display name for the new file. If null, or this is - * never called, the display name defaults to the path ID. - * - * @param displayName A display name - * - * @return This builder - */ - public TextFileBuilder withDisplayName(@Nullable String displayName) { - this.displayName = displayName; - return this; - } - - /** - * Creates and returns the new text file. - */ - public abstract TextFile build(); } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/TextFilesTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/TextFilesTest.java index c165582980..a53ec06ef8 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/TextFilesTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/lang/document/TextFilesTest.java @@ -4,6 +4,11 @@ package net.sourceforge.pmd.lang.document; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.Charset; @@ -13,7 +18,6 @@ import java.nio.file.Path; import org.apache.commons.io.IOUtils; import org.checkerframework.checker.nullness.qual.NonNull; -import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -32,10 +36,47 @@ public class TextFilesTest { public void testNioFile() throws IOException { Path file = makeTmpFile(StandardCharsets.UTF_8, "some content"); try (TextFile tf = TextFile.forPath(file, StandardCharsets.UTF_8, dummyVersion)) { - Assert.assertEquals(file.toAbsolutePath().toString(), tf.getPathId()); - Assert.assertEquals(file.toString(), tf.getDisplayName()); - Assert.assertEquals(dummyVersion, tf.getLanguageVersion()); - Assert.assertEquals(Chars.wrap("some content"), tf.readContents().getNormalizedText()); + assertEquals(file.toAbsolutePath().toString(), tf.getPathId()); + assertEquals(file.toString(), tf.getDisplayName()); + assertEquals(dummyVersion, tf.getLanguageVersion()); + assertEquals(Chars.wrap("some content"), tf.readContents().getNormalizedText()); + } + } + + @Test + public void testNioFileWrite() throws IOException { + Path file = makeTmpFile(StandardCharsets.UTF_8, "some content"); + try (TextFile tf = TextFile.forPath(file, StandardCharsets.UTF_8, dummyVersion)) { + assertEquals(Chars.wrap("some content"), tf.readContents().getNormalizedText()); + assertFalse("readonly", tf.isReadOnly()); + + tf.writeContents( + TextFileContent.fromCharSeq("new content") + ); + + assertEquals(Chars.wrap("new content"), tf.readContents().getNormalizedText()); + } + } + + @Test + public void testNioFileExplicitReadOnly() throws IOException { + Path file = makeTmpFile(StandardCharsets.UTF_8, "some content"); + try (TextFile tf = TextFile.builderForPath(file, StandardCharsets.UTF_8, dummyVersion) + .asReadOnly().build()) { + assertTrue("readonly", tf.isReadOnly()); + + assertThrows(ReadOnlyFileException.class, () -> tf.writeContents( + TextFileContent.fromCharSeq("new content") + )); + } + } + + @Test + public void testNioFileCanBeReadMultipleTimes() throws IOException { + Path file = makeTmpFile(StandardCharsets.UTF_8, "some content"); + try (TextFile tf = TextFile.forPath(file, StandardCharsets.UTF_8, dummyVersion)) { + assertEquals(Chars.wrap("some content"), tf.readContents().getNormalizedText()); + assertEquals(Chars.wrap("some content"), tf.readContents().getNormalizedText()); } } @@ -45,10 +86,10 @@ public class TextFilesTest { try (TextFile tf = TextFile.builderForPath(file, StandardCharsets.UTF_8, dummyVersion) .withDisplayName("aname") .build()) { - Assert.assertEquals(file.toAbsolutePath().toString(), tf.getPathId()); - Assert.assertEquals("aname", tf.getDisplayName()); - Assert.assertEquals(dummyVersion, tf.getLanguageVersion()); - Assert.assertEquals(Chars.wrap("some content"), tf.readContents().getNormalizedText()); + assertEquals(file.toAbsolutePath().toString(), tf.getPathId()); + assertEquals("aname", tf.getDisplayName()); + assertEquals(dummyVersion, tf.getLanguageVersion()); + assertEquals(Chars.wrap("some content"), tf.readContents().getNormalizedText()); } } @@ -56,7 +97,7 @@ public class TextFilesTest { public void testNioFileEscape() throws IOException { Path file = makeTmpFile(StandardCharsets.UTF_8, "some\r\ncontent"); try (TextFile tf = TextFile.forPath(file, StandardCharsets.UTF_8, dummyVersion)) { - Assert.assertEquals(Chars.wrap("some\ncontent"), tf.readContents().getNormalizedText()); + assertEquals(Chars.wrap("some\ncontent"), tf.readContents().getNormalizedText()); } } @@ -64,22 +105,42 @@ public class TextFilesTest { public void testReaderFileEscape() throws IOException { Path file = makeTmpFile(StandardCharsets.UTF_8, "some\r\ncontent"); try (TextFile tf = TextFile.forReader(Files.newBufferedReader(file, StandardCharsets.UTF_8), "filename", dummyVersion)) { - Assert.assertEquals("filename", tf.getPathId()); - Assert.assertEquals("filename", tf.getDisplayName()); - Assert.assertEquals(dummyVersion, tf.getLanguageVersion()); - Assert.assertEquals(Chars.wrap("some\ncontent"), tf.readContents().getNormalizedText()); - Assert.assertThrows(ReadOnlyFileException.class, () -> tf.writeContents( + assertEquals("filename", tf.getPathId()); + assertEquals("filename", tf.getDisplayName()); + assertEquals(dummyVersion, tf.getLanguageVersion()); + assertEquals(Chars.wrap("some\ncontent"), tf.readContents().getNormalizedText()); + assertThrows(ReadOnlyFileException.class, () -> tf.writeContents( TextFileContent.fromCharSeq("new content") )); } - }@Test + } + + @Test public void testStringFileEscape() throws IOException { try (TextFile tf = TextFile.forCharSeq("cont\r\nents", "filename", dummyVersion)) { - Assert.assertEquals("filename", tf.getPathId()); - Assert.assertEquals("filename", tf.getDisplayName()); - Assert.assertEquals(dummyVersion, tf.getLanguageVersion()); - Assert.assertEquals(Chars.wrap("cont\r\nent"), tf.readContents().getNormalizedText()); - Assert.assertThrows(ReadOnlyFileException.class, () -> tf.writeContents( + assertEquals("filename", tf.getPathId()); + assertEquals("filename", tf.getDisplayName()); + assertEquals(dummyVersion, tf.getLanguageVersion()); + assertEquals(Chars.wrap("cont\nents"), tf.readContents().getNormalizedText()); + assertThrows(ReadOnlyFileException.class, () -> tf.writeContents( + TextFileContent.fromCharSeq("new content") + )); + } + } + + @Test + public void testStringFileCanBeReadMultipleTimes() throws IOException { + try (TextFile tf = TextFile.forCharSeq("contents", "filename", dummyVersion)) { + assertEquals(Chars.wrap("contents"), tf.readContents().getNormalizedText()); + assertEquals(Chars.wrap("contents"), tf.readContents().getNormalizedText()); + assertEquals(Chars.wrap("contents"), tf.readContents().getNormalizedText()); + } + } + + @Test + public void testStringFileIsReadonly() throws IOException { + try (TextFile tf = TextFile.forCharSeq("contents", "filename", dummyVersion)) { + assertThrows(ReadOnlyFileException.class, () -> tf.writeContents( TextFileContent.fromCharSeq("new content") )); }