/**
* Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* 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.mapstruct.ap.internal.writer;
import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
/**
* EXPERIMENTAL: A writer used to write processed templates which corrects line indentation based on the nesting level
* as implied by (normal and curly) braces.
* <p>
* This writer discards any leading whitespace characters following to a line break character. When the first
* non-whitespace character is written after a line break, the correct indentation characters are added, which is four
* whitespace characters per indentation level.
*
* <p>
* The state pattern is line oriented. It starts by writing text. Indentation is increased if a brace '('or
* brace '{' is encountered in the code to be generated and written out in state: IN_TEXT_START_OF_LINE. Whenever
* a line end occurs (PC or Linux style) the amount of enters is checked and at max set to 2.
*
* Whenever a string definition is encountered in the code that should be generated, increasing the indentation is
* stopped (so `{` and '(' are ignored) until the end of the string is encountered ('"'). To avoid writing a new
* indentation, the state then returns to IN_TEXT.
*
* <p>
* This is a very basic implementation which does not take into account comments, escaping etc.
*
* @author Gunnar Morling
*/
class IndentationCorrectingWriter extends Writer {
/**
* Set to true to enable output of written characters on the console.
*/
private static final boolean DEBUG = false;
private static final String LINE_SEPARATOR = System.getProperty( "line.separator" );
private static final boolean IS_WINDOWS = System.getProperty( "os.name" ).startsWith( "Windows" );
private State currentState = State.START_OF_LINE;
private final StateContext context;
IndentationCorrectingWriter(Writer out) {
super( out );
this.context = new StateContext( out );
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
context.reset( cbuf, off );
for ( int i = off; i < len; i++ ) {
char c = cbuf[i];
State newState = currentState.handleCharacter( c, context );
if ( newState != currentState ) {
currentState.onExit( context, newState );
newState.onEntry( context );
currentState = newState;
}
context.currentIndex++;
}
currentState.onBufferFinished( context );
}
@Override
public void flush() throws IOException {
context.writer.flush();
}
@Override
public void close() throws IOException {
currentState.onExit( context, null );
context.writer.close();
}
private static boolean isWindows() {
return IS_WINDOWS;
}
private static char[] getIndentation(int indentationLevel) {
char[] indentation = new char[indentationLevel * 4];
Arrays.fill( indentation, ' ' );
return indentation;
}
/**
* A state of parsing a given character buffer.
*/
private enum State {
/**
* Within any text, before encountering a String definition.
*/
START_OF_LINE {
@Override
State doHandleCharacter(char c, StateContext context) {
switch ( c ) {
case '{':
case '(':
context.incrementIndentationLevel();
return IN_TEXT;
case '}':
case ')':
context.decrementIndentationLevel();
return IN_TEXT;
case '\"':
return IN_STRING;
case '\r':
return isWindows() ? IN_LINE_BREAK : AFTER_LINE_BREAK;
case '\n':
return AFTER_LINE_BREAK;
default:
return IN_TEXT;
}
}
/**
* Writes out leading whitespace as per the current indentation level.
*/
@Override
void doOnEntry(StateContext context) throws IOException {
context.writer.write( getIndentation( context.getIndentationLevel() ) );
if ( DEBUG ) {
System.out.print( new String( getIndentation( context.getIndentationLevel() ) )
.replace( " ", "_" ) );
}
}
/**
* Writes out the current text.
*/
@Override
void onExit(StateContext context, State nextState) throws IOException {
flush( context );
}
/**
* Writes out the current text.
*/
@Override
void onBufferFinished(StateContext context) throws IOException {
flush( context );
}
},
/**
* Within any text, but after a String (" ").
*/
IN_TEXT {
@Override
State doHandleCharacter(char c, StateContext context) {
switch ( c ) {
case '{':
case '(':
context.incrementIndentationLevel();
return IN_TEXT;
case '}':
case ')':
context.decrementIndentationLevel();
return IN_TEXT;
case '\"':
return IN_STRING;
case '\r':
return isWindows() ? IN_LINE_BREAK : AFTER_LINE_BREAK;
case '\n':
return AFTER_LINE_BREAK;
default:
return IN_TEXT;
}
}
/**
* Writes out the current text.
*/
@Override
void onExit(StateContext context, State nextState) throws IOException {
flush( context );
}
/**
* Writes out the current text.
*/
@Override
void onBufferFinished(StateContext context) throws IOException {
flush( context );
}
},
/**
* In a String definition, Between un-escaped quotes " "
*/
IN_STRING {
@Override
State doHandleCharacter(char c, StateContext context) {
switch ( c ) {
case '\"':
return IN_TEXT;
case '\\':
return IN_STRING_ESCAPED_CHAR;
default:
return IN_STRING;
}
}
/**
* Writes out the current text.
*/
@Override
void onExit(StateContext context, State nextState) throws IOException {
flush( context );
}
/**
* Writes out the current text.
*/
@Override
void onBufferFinished(StateContext context) throws IOException {
flush( context );
}
},
/**
* In a String, character following an escape character '\', should be ignored, can also be '"' that
* should be ignored.
*/
IN_STRING_ESCAPED_CHAR {
@Override
State doHandleCharacter(char c, StateContext context) {
// ignore escaped character
return IN_STRING;
}
/**
* Writes out the current text.
*/
@Override
void onExit(StateContext context, State nextState) throws IOException {
flush( context );
}
/**
* Writes out the current text.
*/
@Override
void onBufferFinished(StateContext context) throws IOException {
flush( context );
}
},
/**
* Between \r and \n of a Windows line-break.
*/
IN_LINE_BREAK {
@Override
State doHandleCharacter(char c, StateContext context) {
if ( c == '\n' ) {
return AFTER_LINE_BREAK;
}
else {
throw new IllegalArgumentException( "Unexpected character: " + c );
}
}
},
/**
* Directly after a line-break, or within leading whitespace following to a line-break.
*/
AFTER_LINE_BREAK {
@Override
State doHandleCharacter(char c, StateContext context) {
switch ( c ) {
case '{':
case '(':
context.incrementIndentationLevel();
return START_OF_LINE;
case '}':
if ( context.consecutiveLineBreaks > 0 ) {
context.consecutiveLineBreaks = 0; // remove previous blank lines
}
case ')':
context.decrementIndentationLevel();
return START_OF_LINE;
case '\r':
return isWindows() ? IN_LINE_BREAK : AFTER_LINE_BREAK;
case ' ':
return AFTER_LINE_BREAK;
case '\n':
context.consecutiveLineBreaks++;
return AFTER_LINE_BREAK;
default:
return START_OF_LINE;
}
}
/**
* Writes out the current line-breaks, avoiding more than one consecutive empty line
*/
@Override
void onExit(StateContext context, State nextState) throws IOException {
context.consecutiveLineBreaks++;
if ( nextState != IN_LINE_BREAK ) {
int lineBreaks = Math.min( context.consecutiveLineBreaks, 2 );
for ( int i = 0; i < lineBreaks; i++ ) {
context.writer.append( LINE_SEPARATOR );
if ( DEBUG ) {
System.out.print( "\\n" + LINE_SEPARATOR );
}
}
context.consecutiveLineBreaks = 0;
}
}
};
final State handleCharacter(char c, StateContext context) throws IOException {
return doHandleCharacter( c, context );
}
abstract State doHandleCharacter(char c, StateContext context) throws IOException;
final void onEntry(StateContext context) throws IOException {
context.lastStateChange = context.currentIndex;
doOnEntry( context );
}
void doOnEntry(StateContext context) throws IOException {
}
void onExit(StateContext context, State nextState) throws IOException {
}
void onBufferFinished(StateContext context) throws IOException {
}
protected void flush(StateContext context) throws IOException {
if ( null != context.characters && context.currentIndex - context.lastStateChange > 0 ) {
context.writer.write(
context.characters,
context.lastStateChange,
context.currentIndex - context.lastStateChange
);
if ( DEBUG ) {
System.out.print(
new String(
java.util.Arrays.copyOfRange(
context.characters,
context.lastStateChange,
context.currentIndex
)
)
);
}
}
}
}
/**
* Keeps the current context of parsing the given character buffer.
*/
private static class StateContext {
final Writer writer;
char[] characters;
/**
* The position at which when the current state was entered.
*/
int lastStateChange;
/**
* The current position within the buffer.
*/
int currentIndex;
/**
* Keeps track of the current indentation level, as implied by brace characters.
*/
private int indentationLevel;
/**
* The number of consecutive line-breaks when within {@link State#AFTER_LINE_BREAK}.
*/
int consecutiveLineBreaks;
StateContext(Writer writer) {
this.writer = writer;
}
void reset(char[] characters, int off) {
this.characters = characters;
this.lastStateChange = off;
this.currentIndex = 0;
}
void incrementIndentationLevel() {
indentationLevel++;
}
void decrementIndentationLevel() {
// decrementing below 0 indicates misbalanced braces in the code, typically because too many closing braces
// are given in an expression; we let that code pass through, the compiler will complain eventually about
// the malformed source file
if ( indentationLevel > 0 ) {
indentationLevel--;
}
}
int getIndentationLevel() {
return indentationLevel;
}
}
}