Add close semantics
Remove data source adapter
This commit is contained in:
@ -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;
|
||||
}
|
@ -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);
|
||||
|
@ -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".
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
Reference in New Issue
Block a user