/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.xd.dirt.job.dsl;
import java.util.Collections;
import java.util.List;
/**
* Class that converts an expression into a list of {@link Token tokens}.
* Furthermore, this class provides methods to process the tokens and
* keeps track of the current token being processed.
*
* @author Andy Clement
* @author Patrick Peralta
*/
public class Tokens {
/**
* Expression string to be parsed.
*/
private final String expression;
/**
* List of tokens created from {@link #expression}.
*/
private final List<Token> tokenStream;
/**
* Index of token currently being processed.
*/
private int position = 0;
/**
* Index of last token that was successfully processed.
*/
private int lastGoodPosition = 0;
/**
* Construct a {@code Tokens} object based on the provided string expression.
*
* @param expression string expression to convert into {@link Token tokens}.
*/
public Tokens(String expression) {
this.expression = expression;
this.tokenStream = Collections.unmodifiableList(new Tokenizer(expression).getTokens());
}
/**
* Return the expression string converted to tokens.
*
* @return expression string
*/
protected String getExpression() {
return expression;
}
/**
* Decrement the current token position and return the new position.
*
* @return new token position
*/
protected int decrementPosition() {
return --position;
}
/**
* Return the current token position.
*
* @return current token position
*/
protected int position() {
return position;
}
/**
* Return an immutable list of {@link Token tokens}
*
* @return list of tokens
*/
public List<Token> getTokenStream() {
return tokenStream;
}
/**
* Return {@code true} if the token in the position indicated
* by {@link #position} + {@code distance} matches
* the token indicated by {@code desiredTokenKind}.
*
* @param distance number of token positions past the current position
* @param desiredTokenKind the token to check for
* @return true if the token at the indicated position matches
* {@code desiredTokenKind}
*/
protected boolean lookAhead(int distance, TokenKind desiredTokenKind) {
if ((position + distance) >= tokenStream.size()) {
return false;
}
Token t = tokenStream.get(position + distance);
return t.kind == desiredTokenKind;
}
/**
* Return {@code true} if there are more tokens to process.
*
* @return {@code true} if there are more tokens to process
*/
protected boolean hasNext() {
return position < tokenStream.size();
}
/**
* Return the token at the current position. If there are no more tokens,
* return {@code null}.
*
* @return token at current position or {@code null} if there are no more tokens
*/
protected Token peek() {
return hasNext() ? tokenStream.get(position) : null;
}
/**
* Return the token at the specified offset from the current position. If there
* are no more tokens, return {@code null}.
*
* @return token at specified offset from current position or {@code null} if there are no more tokens
*/
protected Token peek(int offset) {
if ((position + offset) >= tokenStream.size()) {
return null;
}
return tokenStream.get(position + offset);
}
/**
* Peek at the token at an offset (relative to the current position) - if the token
* matches the indicated kind, return true, otherwise return false.
*
* @param offset the offset from the current position to peek at
* @param tokenKind the tokenKind to check against
* @return true if the token at the specified offset matches the indicated kind, otherwise false
*/
protected boolean peek(int offset, TokenKind tokenKind) {
if ((position + offset) >= tokenStream.size()) {
return false;
}
Token nextToken = tokenStream.get(position + offset);
return (nextToken.getKind() == tokenKind);
}
/**
* Return {@code true} if the indicated token matches the current token
* position.
*
* @param desiredTokenKind token to match
* @return true if the current token kind matches the provided token kind
*/
protected boolean peek(TokenKind desiredTokenKind) {
return peek(desiredTokenKind, false);
}
/**
* Return {@code true} if the indicated token matches the current token
* position.
*
* @param desiredTokenKind token to match
* @param consumeIfMatched if {@code true}, advance the current token position
* @return true if the current token kind matches the provided token kind
*/
private boolean peek(TokenKind desiredTokenKind, boolean consumeIfMatched) {
if (!hasNext()) {
return false;
}
Token t = peek();
if (t.kind == desiredTokenKind) {
if (consumeIfMatched) {
position++;
}
return true;
}
else {
return false;
}
}
/**
* Return the next {@link Token} and advance the current token position.
*
* @return next {@code Token}
*/
protected Token next() {
if (!hasNext()) {
raiseException(expression.length(), JobDSLMessage.OOD);
}
return tokenStream.get(position++);
}
/**
* Consume the next token if it matches the indicated token kind;
* otherwise throw {@link CheckpointedJobDefinitionException}.
*
* @param expectedKind the expected token kind
* @return the next token
* @throws CheckpointedJobDefinitionException if the next token does not match
* the expected token kind
*/
protected Token eat(TokenKind expectedKind) {
Token t = next();
if (t == null) {
raiseException(expression.length(), JobDSLMessage.OOD);
}
if (t.kind != expectedKind) {
raiseException(t.startpos, JobDSLMessage.NOT_EXPECTED_TOKEN,
expectedKind.toString().toLowerCase(),
t.getKind().toString().toLowerCase() + (t.data == null ? "" : "(" + t.data + ")"));
}
return t;
}
/**
* Consume the next token if it matches the desired token kind and return true; otherwise
* return false.
*
* @param desiredKind the desired token to eat
* @return true if the token was consumed, otherwise false
*/
protected boolean maybeEat(TokenKind desiredKind) {
if (peek(desiredKind)) {
next();
return true;
}
else {
return false;
}
}
/**
* Return {@code true} if the first character of the token at the current position
* is the same as the last character of the token at the previous position.
*
* @return true if the first character of the current token matches the last
* character of the previous token
*/
protected boolean isNextAdjacent() {
if (!hasNext()) {
return false;
}
Token last = tokenStream.get(position - 1);
Token next = tokenStream.get(position);
return next.startpos == last.endpos;
}
/**
* Indicate that a piece of the DSL has been successfully processed.
*
* @see #lastGoodPosition
*/
protected void checkpoint() {
lastGoodPosition = position;
}
/**
* Throw a new {@link CheckpointedJobDefinitionException} based on the current and
* last successfully processed token position.
*
* @param position position where parse error occurred
* @param message parse exception message
* @param inserts variables that may be inserted in the error message
*/
protected void raiseException(int position, JobDSLMessage message, Object... inserts) {
throw new CheckpointedJobDefinitionException(expression, position, this.position,
lastGoodPosition, tokenStream, message, inserts);
}
@Override
public String toString() {
StringBuilder s = new StringBuilder();
s.append(tokenStream).append("\n");
s.append(expression).append("\n");
Token t = tokenStream.get(position);
int i = 0;
for (; i < t.startpos; i++) {
s.append(" ");
}
for (; i < t.endpos; i++) {
s.append("^");
}
s.append("\n");
return s.toString();
}
}