Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi source reader for parsing graphql Documents #1411

Merged
merged 3 commits into from
Feb 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 18 additions & 31 deletions src/main/java/graphql/parser/ExtendedBailStrategy.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
package graphql.parser;

import graphql.language.SourceLocation;
import graphql.parser.MultiSourceReader.SourceAndLine;
import org.antlr.v4.runtime.BailErrorStrategy;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.misc.ParseCancellationException;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

public class ExtendedBailStrategy extends BailErrorStrategy {
private final String input;
private final String sourceName;
private final MultiSourceReader multiSourceReader;

public ExtendedBailStrategy(String input, String sourceName) {
this.input = input;
this.sourceName = sourceName;
public ExtendedBailStrategy(MultiSourceReader multiSourceReader) {
this.multiSourceReader = multiSourceReader;
}

@Override
Expand All @@ -41,7 +36,11 @@ public Token recoverInline(Parser recognizer) throws RecognitionException {
}

InvalidSyntaxException mkMoreTokensException(Token token) {
SourceLocation sourceLocation = new SourceLocation(token.getLine(), token.getCharPositionInLine());
SourceAndLine sourceAndLine = multiSourceReader.getSourceAndLineFromOverallLine(token.getLine());
int column = token.getCharPositionInLine();

// graphql spec says line numbers start at 1
SourceLocation sourceLocation = new SourceLocation(sourceAndLine.getLine()+1, column, sourceAndLine.getSourceName());
String sourcePreview = mkPreview(token.getLine());
return new InvalidSyntaxException(sourceLocation,
"There are more tokens in the query that have not been consumed",
Expand All @@ -55,41 +54,29 @@ private InvalidSyntaxException mkException(Parser recognizer, RecognitionExcepti
SourceLocation sourceLocation = null;
Token currentToken = recognizer.getCurrentToken();
if (currentToken != null) {
int line = currentToken.getLine();
int tokenLine = currentToken.getLine();
int column = currentToken.getCharPositionInLine();
SourceAndLine sourceAndLine = multiSourceReader.getSourceAndLineFromOverallLine(tokenLine);
offendingToken = currentToken.getText();
sourcePreview = mkPreview(line);
sourceLocation = new SourceLocation(line, column, sourceName);
sourcePreview = mkPreview(tokenLine);
// graphql spec says line numbers start at 1
sourceLocation = new SourceLocation(sourceAndLine.getLine()+1, column, sourceAndLine.getSourceName());
}
return new InvalidSyntaxException(sourceLocation, null, sourcePreview, offendingToken, cause);
}

/* grabs 3 lines before and after the syntax error */
private String mkPreview(int line) {
StringBuilder sb = new StringBuilder();
BufferedReader reader = new BufferedReader(new StringReader(input));
int startLine = line - 3;
int endLine = line + 3;
try {
List<String> lines = readAllLines(reader);
for (int i = 0; i < lines.size(); i++) {
if (i >= startLine && i <= endLine) {
sb.append(lines.get(i)).append('\n');
}
List<String> lines = multiSourceReader.getData();
for (int i = 0; i < lines.size(); i++) {
if (i >= startLine && i <= endLine) {
sb.append(lines.get(i)).append('\n');
}
} catch (IOException ignored) {
// this cant happen - its in memory
}
return sb.toString();
}

private List<String> readAllLines(BufferedReader reader) throws IOException {
List<String> lines = new ArrayList<>();
String ln;
while ((ln = reader.readLine()) != null) {
lines.add(ln);
}
reader.close();
return lines;
}
}
24 changes: 13 additions & 11 deletions src/main/java/graphql/parser/GraphqlAntlrToLanguage.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
import graphql.parser.antlr.GraphqlLexer;
import graphql.parser.antlr.GraphqlParser;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.IntStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;

Expand All @@ -81,10 +80,12 @@ public class GraphqlAntlrToLanguage {
private static final int CHANNEL_COMMENTS = 2;
private static final int CHANNEL_IGNORED_CHARS = 3;
private final CommonTokenStream tokens;
private final MultiSourceReader multiSourceReader;


public GraphqlAntlrToLanguage(CommonTokenStream tokens) {
public GraphqlAntlrToLanguage(CommonTokenStream tokens, MultiSourceReader multiSourceReader) {
this.tokens = tokens;
this.multiSourceReader = multiSourceReader;
}

//MARKER START: Here GraphqlOperation.g4 specific methods begin
Expand Down Expand Up @@ -809,14 +810,11 @@ protected Description newDescription(GraphqlParser.DescriptionContext descriptio
}

protected SourceLocation getSourceLocation(Token token) {
String sourceName = token.getTokenSource().getSourceName();
if (IntStream.UNKNOWN_SOURCE_NAME.equals(sourceName)) {
// UNKNOWN_SOURCE_NAME is Antrl's way of indicating that no source name was given during parsing --
// which is the case when queries and other operations are parsed. We don't want this hardcoded
// '<unknown>' sourceName to leak to clients when the response is serialized as JSON, so we null it.
sourceName = null;
}
return new SourceLocation(token.getLine(), token.getCharPositionInLine() + 1, sourceName);
MultiSourceReader.SourceAndLine sourceAndLine = multiSourceReader.getSourceAndLineFromOverallLine(token.getLine());
int column = token.getCharPositionInLine() + 1;
// graphql spec says line numbers start at 1
int line = sourceAndLine.getLine() + 1;
return new SourceLocation(line, column, sourceAndLine.getSourceName());
}

protected SourceLocation getSourceLocation(ParserRuleContext parserRuleContext) {
Expand Down Expand Up @@ -847,7 +845,11 @@ protected List<Comment> getCommentOnChannel(List<Token> refChannel) {
continue;
}
text = text.replaceFirst("^#", "");
comments.add(new Comment(text, new SourceLocation(refTok.getLine(), refTok.getCharPositionInLine())));
MultiSourceReader.SourceAndLine sourceAndLine = multiSourceReader.getSourceAndLineFromOverallLine(refTok.getLine());
int column = refTok.getCharPositionInLine();
// graphql spec says line numbers start at 1
int line = sourceAndLine.getLine() + 1;
comments.add(new Comment(text, new SourceLocation(line, column, sourceAndLine.getSourceName())));
}
return comments;
}
Expand Down
244 changes: 244 additions & 0 deletions src/main/java/graphql/parser/MultiSourceReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package graphql.parser;

import graphql.Assert;
import graphql.PublicApi;

import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;

/**
* This reader allows you to read N number readers and combine them as one logical reader
* however you can then map back to the underlying readers in terms of their source name
* and the relative lines numbers.
*
* It can also track all data in memory if you want to have all of the previous read data in
* place at some point in time.
*/
@PublicApi
public class MultiSourceReader extends Reader {

private final List<SourcePart> sourceParts;
private final StringBuilder data = new StringBuilder();
private int currentIndex = 0;
private int overallLineNumber = 0;
private final boolean trackData;


private MultiSourceReader(Builder builder) {
this.sourceParts = builder.sourceParts;
this.trackData = builder.trackData;
}

@Override
public int read(char[] cbuf, int off, int len) throws IOException {
while (true) {
synchronized (this) {
if (currentIndex >= sourceParts.size()) {
return -1;
}
SourcePart sourcePart = sourceParts.get(currentIndex);
int read = sourcePart.lineReader.read(cbuf, off, len);
overallLineNumber = calcLineNumber();
if (read == -1) {
currentIndex++;
} else {
trackData(cbuf, off, read);
return read;
}
}
}
}

private void trackData(char[] cbuf, int off, int len) {
if (trackData) {
data.append(cbuf, off, len);
}
}

private int calcLineNumber() {
int linenumber = 0;
for (SourcePart sourcePart : sourceParts) {
linenumber += sourcePart.lineReader.getLineNumber();
}
return linenumber;
}

public static class SourceAndLine {
private String sourceName = null;
private int line = 0;

public String getSourceName() {
return sourceName;
}

public int getLine() {
return line;
}
}

/**
* This returns the source name and line number given an overall line number
*
* This is zeroes based like {@link java.io.LineNumberReader#getLineNumber()}
*
* @param overallLineNumber the over all line number
*
* @return the source name and relative line number to that source
*/
public SourceAndLine getSourceAndLineFromOverallLine(int overallLineNumber) {
SourceAndLine sourceAndLine = new SourceAndLine();
if (sourceParts.isEmpty()) {
return sourceAndLine;
}
SourcePart currentPart;
if (currentIndex >= sourceParts.size()) {
currentPart = sourceParts.get(sourceParts.size() - 1);
} else {
currentPart = sourceParts.get(currentIndex);
}
int page = 0;
int previousPage;
for (SourcePart sourcePart : sourceParts) {
sourceAndLine.sourceName = sourcePart.sourceName;
if (sourcePart == currentPart) {
// we cant go any further
int offset = currentPart.lineReader.getLineNumber();
previousPage = page;
page += offset;
if (page > overallLineNumber) {
sourceAndLine.line = overallLineNumber - previousPage;
} else {
sourceAndLine.line = page;
}
return sourceAndLine;
} else {
previousPage = page;
page += sourcePart.lineReader.getLineNumber();
if (page > overallLineNumber) {
sourceAndLine.line = overallLineNumber - previousPage;
return sourceAndLine;
}
}
}
sourceAndLine.line = overallLineNumber - page;
return sourceAndLine;
}

/**
* @return the line number of the current source. This is zeroes based like {@link java.io.LineNumberReader#getLineNumber()}
*/
public int getLineNumber() {
synchronized (this) {
if (sourceParts.isEmpty()) {
return 0;
}
if (currentIndex >= sourceParts.size()) {
return sourceParts.get(sourceParts.size() - 1).lineReader.getLineNumber();
}
return sourceParts.get(currentIndex).lineReader.getLineNumber();
}
}

/**
* @return The name of the current source
*/
public String getSourceName() {
synchronized (this) {
if (sourceParts.isEmpty()) {
return null;
}
if (currentIndex >= sourceParts.size()) {
return sourceParts.get(sourceParts.size() - 1).sourceName;
}
return sourceParts.get(currentIndex).sourceName;
}
}

/**
* @return the overall line number of the all the sources. This is zeroes based like {@link java.io.LineNumberReader#getLineNumber()}
*/
public int getOverallLineNumber() {
return overallLineNumber;
}

public List<String> getData() {
LineNumberReader reader = new LineNumberReader(new StringReader(data.toString()));
List<String> lines = new ArrayList<>();
while (true) {
try {
String line = reader.readLine();
if (line == null) {
return lines;
}
lines.add(line);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

@Override
public void close() throws IOException {
synchronized (this) {
for (SourcePart sourcePart : sourceParts) {
if (!sourcePart.closed) {
sourcePart.lineReader.close();
sourcePart.closed = true;
}
}
}
}

private static class SourcePart {
String sourceName;
LineNumberReader lineReader;
boolean closed;
}


public static Builder newMultiSourceReader() {
return new Builder();
}

public static class Builder {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you want a private constructor here maybe

List<SourcePart> sourceParts = new ArrayList<>();
boolean trackData = true;

private Builder() {
}

public Builder reader(Reader reader, String sourceName) {
SourcePart sourcePart = new SourcePart();
sourcePart.lineReader = new LineNumberReader(Assert.assertNotNull(reader));
sourcePart.sourceName = sourceName;
sourcePart.closed = false;
sourceParts.add(sourcePart);
return this;
}

public Builder string(String input, String sourceName) {
SourcePart sourcePart = new SourcePart();
sourcePart.lineReader = new LineNumberReader(new StringReader(input));
sourcePart.sourceName = sourceName;
sourcePart.closed = false;
sourceParts.add(sourcePart);
return this;
}

public Builder trackData(boolean trackData) {
this.trackData = trackData;
return this;

}

public MultiSourceReader build() {
return new MultiSourceReader(this);
}
}

}