Add close semantics

Remove data source adapter
This commit is contained in:
Clément Fournier
2020-01-02 19:44:59 +01:00
parent b958b129e4
commit 9ca01cdb69
10 changed files with 183 additions and 129 deletions

View File

@ -0,0 +1,29 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.internal.util;
import java.io.Closeable;
import java.io.IOException;
public abstract class BaseCloseable implements Closeable {
protected boolean open = true;
protected void ensureOpen() throws IOException {
if (!open) {
throw new IOException("Closed " + this);
}
}
@Override
public void close() throws IOException {
if (open) {
open = false;
doClose();
}
}
protected abstract void doClose() throws IOException;
}

View File

@ -18,7 +18,8 @@ class IoBuffer {
private final TextFileBehavior backend;
private final long originalStamp;
private final StringBuilder buffer;
private final CharSequence original;
private StringBuilder buffer;
/** @throws ReadOnlyFileException If the backend is read-only */
@ -27,11 +28,16 @@ class IoBuffer {
throw new ReadOnlyFileException(backend + " is readonly");
}
this.original = sequence;
this.backend = backend;
this.buffer = new StringBuilder(sequence);
this.originalStamp = stamp;
}
void reset() {
buffer = new StringBuilder(original);
}
void replace(final TextRegion region, final String textToReplace) {
buffer.replace(region.getStartOffset(), region.getEndOffset(), textToReplace);

View File

@ -58,9 +58,8 @@ public interface TextDocument extends Closeable {
/**
* Add line information to the given region. Only the start and end
* offsets are considered, if the region is already a {@link RegionWithLines},
* that information is discarded.
* Turn a text region into a {@link RegionWithLines}. If the region
* is already a {@link RegionWithLines}, that information is discarded.
*
* @return A new region with line information
*
@ -98,12 +97,28 @@ public interface TextDocument extends Closeable {
* @return A new editor
*
* @throws IOException If an IO error occurs
* @throws IOException If this document was closed
* @throws ReadOnlyFileException If this document is read-only
* @throws ConcurrentModificationException If an editor is already open for this document
*/
TextEditor newEditor() throws IOException;
/**
* Closing a document closes the underlying {@link TextFileBehavior}.
* New editors cannot be produced after that, and the document otherwise
* remains in its current state.
*
* @throws IOException If {@link TextFileBehavior#close()} throws
* @throws IllegalStateException If an editor is currently open. In this case
* the editor is rendered ineffective before the
* exception is thrown. This indicates a programming
* mistake.
*/
@Override
void close() throws IOException;
/**
* Returns a document backed by the given text "file".
*

View File

@ -11,9 +11,10 @@ import net.sourceforge.pmd.internal.util.AssertionUtil;
import net.sourceforge.pmd.util.document.TextRegion.RegionWithLines;
import net.sourceforge.pmd.util.document.TextRegionImpl.WithLineInfo;
import net.sourceforge.pmd.util.document.io.TextFileBehavior;
import net.sourceforge.pmd.internal.util.BaseCloseable;
final class TextDocumentImpl implements TextDocument {
final class TextDocumentImpl extends BaseCloseable implements TextDocument {
private static final String OUT_OF_BOUNDS_WITH_OFFSET =
"Region [%d, +%d] is not in range of this document (length %d)";
@ -25,7 +26,7 @@ final class TextDocumentImpl implements TextDocument {
private SourceCodePositioner positioner;
private CharSequence text;
private int numOpenEditors;
private TextEditorImpl curEditor;
TextDocumentImpl(TextFileBehavior backend) throws IOException {
this.backend = backend;
@ -41,25 +42,30 @@ final class TextDocumentImpl implements TextDocument {
@Override
public TextEditor newEditor() throws IOException {
synchronized (this) {
if (numOpenEditors++ > 0) {
throw new ConcurrentModificationException("An editor is already open on this document");
}
return new TextEditorImpl(this, backend);
ensureOpen();
if (curEditor != null) {
throw new ConcurrentModificationException("An editor is already open on this document");
}
return curEditor = new TextEditorImpl(this, backend);
}
void closeEditor(CharSequence text, long stamp) {
synchronized (this) {
numOpenEditors--;
this.text = text.toString();
this.positioner = null;
this.curStamp = stamp;
}
curEditor = null;
this.text = text.toString();
this.positioner = null;
this.curStamp = stamp;
}
@Override
public void close() throws IOException {
protected void doClose() throws IOException {
if (curEditor != null) {
curEditor.sever();
curEditor = null;
throw new IllegalStateException("Unclosed editor!");
}
backend.close();
}

View File

@ -60,8 +60,8 @@ public interface TextEditor extends AutoCloseable {
/**
* Commit the document. The {@linkplain TextDocument#getText() text}
* of the associated document is updated to reflect the changes. The
* Commits the document. If there are some changes, the {@linkplain TextDocument#getText() text}
* of the associated document is updated to reflect them, and the
* {@link TextFileBehavior} is written to. This editor becomes unusable
* after being closed.
*
@ -74,6 +74,14 @@ public interface TextEditor extends AutoCloseable {
void close() throws IOException;
/**
* Drops all updates created in this editor.
*
* @throws IllegalStateException If this editor has been closed
*/
void drop();
/**
* Signals that an operation of a {@link TextEditor} modifies a text
* region that has already been modified. This means, that the text

View File

@ -13,16 +13,15 @@ import java.util.TreeMap;
import net.sourceforge.pmd.util.document.io.ReadOnlyFileException;
import net.sourceforge.pmd.util.document.io.TextFileBehavior;
import net.sourceforge.pmd.internal.util.BaseCloseable;
class TextEditorImpl implements TextEditor {
class TextEditorImpl extends BaseCloseable implements TextEditor {
private final TextDocumentImpl document;
private final IoBuffer out;
private boolean open = true;
private SortedMap<Integer, Integer> accumulatedOffsets = new TreeMap<>();
private List<TextRegion> affectedRegions = new ArrayList<>();
@ -33,22 +32,34 @@ class TextEditorImpl implements TextEditor {
this.document = document;
}
private void ensureOpen() {
if (!open) {
throw new IllegalStateException("Closed handler");
@Override
protected void ensureOpen() {
try {
super.ensureOpen();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
@Override
public void close() throws IOException {
synchronized (this) {
ensureOpen();
open = false;
protected void doClose() throws IOException {
if (!affectedRegions.isEmpty()) {
out.close(document);
}
}
void sever() {
open = false; // doClose will never be called
}
@Override
public void drop() {
ensureOpen();
out.reset();
accumulatedOffsets.clear();
affectedRegions.clear();
}
@Override
public void insert(int offset, String textToInsert) {
replace(document.createRegion(offset, 0), textToInsert);

View File

@ -11,11 +11,12 @@ import java.nio.file.Files;
import java.nio.file.Path;
import net.sourceforge.pmd.internal.util.AssertionUtil;
import net.sourceforge.pmd.internal.util.BaseCloseable;
/**
* A {@link TextFileBehavior} backed by a file in some {@link FileSystem}.
*/
class FsTextFileBehavior implements TextFileBehavior {
class FsTextFileBehavior extends BaseCloseable implements TextFileBehavior {
private final Path path;
private final Charset charset;
@ -40,24 +41,28 @@ class FsTextFileBehavior implements TextFileBehavior {
@Override
public void writeContents(CharSequence charSequence) throws IOException {
ensureOpen();
byte[] bytes = charSequence.toString().getBytes(charset);
Files.write(path, bytes);
}
@Override
public CharSequence readContents() throws IOException {
ensureOpen();
byte[] bytes = Files.readAllBytes(path);
return new String(bytes, charset);
}
@Override
public long fetchStamp() throws IOException {
ensureOpen();
return Files.getLastModifiedTime(path).hashCode();
}
@Override
public void close() throws IOException {
@Override
protected void doClose() {
// do nothing
}
@Override

View File

@ -1,68 +0,0 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.util.document.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import org.apache.commons.io.IOUtils;
import net.sourceforge.pmd.internal.util.AssertionUtil;
import net.sourceforge.pmd.util.datasource.DataSource;
/**
* Adapter for a {@link DataSource}.
*/
class ReadOnlyDataSourceBehavior implements TextFileBehavior {
private final DataSource dataSource;
private final Charset encoding;
public ReadOnlyDataSourceBehavior(DataSource source, Charset encoding) {
this.encoding = encoding;
AssertionUtil.requireParamNotNull("source text", source);
AssertionUtil.requireParamNotNull("charset", encoding);
this.dataSource = source;
}
@Override
public boolean isReadOnly() {
return true;
}
@Override
public void writeContents(CharSequence charSequence) {
throw new ReadOnlyFileException("Readonly source");
}
@Override
public CharSequence readContents() throws IOException {
try (InputStream is = dataSource.getInputStream();
Reader reader = new InputStreamReader(is, encoding)) {
return IOUtils.toString(reader);
}
}
@Override
public long fetchStamp() throws IOException {
return hashCode();
}
@Override
public void close() throws IOException {
dataSource.close();
}
@Override
public String toString() {
return "ReadOnly[" + dataSource + "]";
}
}

View File

@ -11,7 +11,6 @@ import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import net.sourceforge.pmd.util.datasource.DataSource;
import net.sourceforge.pmd.util.document.TextDocument;
/**
@ -36,6 +35,7 @@ public interface TextFileBehavior extends Closeable {
*
* @param charSequence Content to write
*
* @throws IOException If this instance is closed
* @throws IOException If an error occurs
* @throws ReadOnlyFileException If this text source is read-only
*/
@ -46,6 +46,9 @@ public interface TextFileBehavior extends Closeable {
* Reads the contents of the underlying character source.
*
* @return The most up-to-date content
*
* @throws IOException If this instance is closed
* @throws IOException If reading causes an IOException
*/
CharSequence readContents() throws IOException;
@ -57,6 +60,9 @@ public interface TextFileBehavior extends Closeable {
* should change stamps. This however doesn't mandate a pattern for
* the stamps over time, eg they don't need to increase, or really
* represent anything.
*
* @throws IOException If this instance is closed
* @throws IOException If reading causes an IOException
*/
long fetchStamp() throws IOException;
@ -75,16 +81,4 @@ public interface TextFileBehavior extends Closeable {
}
/**
* Returns a read-only instance of this interface reading from the
* given dataSource.
*
* @param dataSource Data source
* @param charset Encoding to use
*/
static TextFileBehavior forDataSource(final DataSource dataSource, final Charset charset) {
return new ReadOnlyDataSourceBehavior(dataSource, charset);
}
}

View File

@ -7,6 +7,7 @@ package net.sourceforge.pmd.util.document;
import static net.sourceforge.pmd.util.document.TextEditor.OverlappingOperationsException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.BufferedWriter;
import java.io.IOException;
@ -22,6 +23,7 @@ import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import net.sourceforge.pmd.util.document.TextRegion.RegionWithLines;
import net.sourceforge.pmd.util.document.io.ReadOnlyFileException;
import net.sourceforge.pmd.util.document.io.ReadOnlyStringBehavior;
import net.sourceforge.pmd.util.document.io.TextFileBehavior;
@ -156,6 +158,19 @@ public class TextEditorTest {
assertFinalFileIs(doc, "public static void main(String[] args){}");
}
@Test
public void testInsertTwiceInSamePlace() throws IOException {
final String code = "void main(String[] args)";
TextDocument doc = tempFile(code);
try (TextEditor editor = doc.newEditor()) {
editor.insert(0, "public ");
editor.insert(0, "static ");
}
assertFinalFileIs(doc, "public static void main(String[] args)");
}
@Test
public void removeTokenShouldSucceed() throws IOException {
final String code = "public static void main(final String[] args) {}";
@ -211,19 +226,6 @@ public class TextEditorTest {
assertFinalFileIs(doc, "void main(String[] args) {}");
}
@Test
public void replaceVariousTokensShouldSucceed() throws IOException {
final String code = "int main(String[] args) {}";
TextDocument doc = tempFile(code);
try (TextEditor editor = doc.newEditor()) {
editor.replace(doc.createRegion(0, 3), "void");
editor.replace(doc.createRegion(4, 4), "foo");
editor.replace(doc.createRegion(9, 6), "CharSequence");
}
assertFinalFileIs(doc, "void foo(CharSequence[] args) {}");
}
@Test
public void insertDeleteAndReplaceVariousTokensShouldSucceed() throws IOException {
@ -294,6 +296,52 @@ public class TextEditorTest {
}
@Test
public void closedTextDocumentShouldntProduceNewEditors() throws IOException {
final String code = "static int main(CharSequence[] args) {}";
TextDocument doc = tempFile(code);
doc.close();
expect.expect(IOException.class);
doc.newEditor();
}
@Test
public void closedTextDocumentWithOpenEditorShouldThrow() throws IOException {
final String code = "static int main(CharSequence[] args) {}";
TextDocument doc = tempFile(code);
TextEditor editor = doc.newEditor();
expect.expect(IllegalStateException.class);
doc.close();
}
@Test
public void closedTextDocumentShouldntNeutralizeExistingEditor() throws IOException {
final String code = "static int main(CharSequence[] args) {}";
TextDocument doc = tempFile(code);
TextEditor editor = doc.newEditor();
editor.insert(0, "FOO");
try {
doc.close();
fail();
} catch (IllegalStateException e) {
editor.close();
assertFinalFileIs(doc, code); // no modification
}
}
@Test
public void textReadOnlyDocumentCannotBeEdited() throws IOException {
ReadOnlyStringBehavior someFooBar = new ReadOnlyStringBehavior("someFooBar");
@ -302,7 +350,7 @@ public class TextEditorTest {
assertTrue(doc.isReadOnly());
expect.expect(UnsupportedOperationException.class);
expect.expect(ReadOnlyFileException.class);
doc.newEditor();
}