From 72049532fa289d0f5664e9b1a7fd19fd514e595f Mon Sep 17 00:00:00 2001
From: Gunnar Morling <gunnar.morling@googlemail.com>
Date: Wed, 31 Aug 2016 15:28:25 +0200
Subject: [PATCH] HV-1091 Using iterative instead of recursive approach for
 message parsing

---
 .../parser/BeginState.java                    | 12 +--
 .../messageinterpolation/parser/ELState.java  | 10 ---
 .../parser/EscapedState.java                  |  9 --
 .../parser/InterpolationTermState.java        | 12 ---
 .../parser/MessageState.java                  | 11 ---
 .../parser/ParserState.java                   |  2 -
 .../parser/TokenCollector.java                | 16 ++--
 .../validator/bugs/TooBigMessageTest.java     | 90 +++++++++++++++++++
 .../TokenIteratorTest.java                    |  8 +-
 9 files changed, 103 insertions(+), 67 deletions(-)
 create mode 100644 engine/src/test/java/org/hibernate/validator/bugs/TooBigMessageTest.java

diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/BeginState.java b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/BeginState.java
index 678f16e46..5aee73b9f 100644
--- a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/BeginState.java
+++ b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/BeginState.java
@@ -18,11 +18,7 @@
 
 	@Override
 	public void terminate(TokenCollector tokenCollector) throws MessageDescriptorFormatException {
-	}
-
-	@Override
-	public void start(TokenCollector tokenCollector) throws MessageDescriptorFormatException {
-		tokenCollector.next();
+		tokenCollector.terminateToken();
 	}
 
 	@Override
@@ -30,7 +26,6 @@ public void handleNonMetaCharacter(char character, TokenCollector tokenCollector
 			throws MessageDescriptorFormatException {
 		tokenCollector.appendToToken( character );
 		tokenCollector.transitionState( new MessageState() );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -43,7 +38,6 @@ public void handleBeginTerm(char character, TokenCollector tokenCollector) throw
 			tokenCollector.makeParameterToken();
 		}
 		tokenCollector.transitionState( new InterpolationTermState() );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -56,7 +50,6 @@ public void handleEscapeCharacter(char character, TokenCollector tokenCollector)
 			throws MessageDescriptorFormatException {
 		tokenCollector.appendToToken( character );
 		tokenCollector.transitionState( new EscapedState( this ) );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -68,9 +61,6 @@ public void handleELDesignator(char character, TokenCollector tokenCollector)
 		else {
 			ParserState state = new ELState();
 			tokenCollector.transitionState( state );
-			tokenCollector.next();
 		}
 	}
 }
-
-
diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/ELState.java b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/ELState.java
index ce7df402f..569ea1e2e 100644
--- a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/ELState.java
+++ b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/ELState.java
@@ -15,11 +15,6 @@
 public class ELState implements ParserState {
 	private static final Log log = LoggerFactory.make();
 
-	@Override
-	public void start(TokenCollector tokenCollector) {
-		throw new IllegalStateException( "Parsing of message descriptor cannot start in this state" );
-	}
-
 	@Override
 	public void terminate(TokenCollector tokenCollector) throws MessageDescriptorFormatException {
 		tokenCollector.appendToToken( TokenCollector.EL_DESIGNATOR );
@@ -33,7 +28,6 @@ public void handleNonMetaCharacter(char character, TokenCollector tokenCollector
 		tokenCollector.appendToToken( character );
 		tokenCollector.terminateToken();
 		tokenCollector.transitionState( new BeginState() );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -44,7 +38,6 @@ public void handleBeginTerm(char character, TokenCollector tokenCollector) throw
 		tokenCollector.appendToToken( character );
 		tokenCollector.makeELToken();
 		tokenCollector.transitionState( new InterpolationTermState() );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -59,7 +52,6 @@ public void handleEndTerm(char character, TokenCollector tokenCollector) throws
 	public void handleEscapeCharacter(char character, TokenCollector tokenCollector)
 			throws MessageDescriptorFormatException {
 		tokenCollector.transitionState( new EscapedState( this ) );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -68,5 +60,3 @@ public void handleELDesignator(char character, TokenCollector tokenCollector)
 		handleNonMetaCharacter( character, tokenCollector );
 	}
 }
-
-
diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/EscapedState.java b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/EscapedState.java
index 901acff4f..f44242dd1 100644
--- a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/EscapedState.java
+++ b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/EscapedState.java
@@ -16,11 +16,6 @@ public EscapedState(ParserState previousState) {
 		this.previousState = previousState;
 	}
 
-	@Override
-	public void start(TokenCollector tokenCollector) {
-		throw new IllegalStateException( "Parsing of message descriptor cannot start in this state" );
-	}
-
 	@Override
 	public void terminate(TokenCollector tokenCollector) throws MessageDescriptorFormatException {
 		tokenCollector.terminateToken();
@@ -58,9 +53,5 @@ private void handleEscapedCharacter(char character, TokenCollector tokenCollecto
 			throws MessageDescriptorFormatException {
 		tokenCollector.appendToToken( character );
 		tokenCollector.transitionState( previousState );
-		tokenCollector.next();
 	}
 }
-
-
-
diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/InterpolationTermState.java b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/InterpolationTermState.java
index f921113f9..11c974373 100644
--- a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/InterpolationTermState.java
+++ b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/InterpolationTermState.java
@@ -15,11 +15,6 @@
 public class InterpolationTermState implements ParserState {
 	private static final Log log = LoggerFactory.make();
 
-	@Override
-	public void start(TokenCollector tokenCollector) {
-		throw new IllegalStateException( "Parsing of message descriptor cannot start in this state" );
-	}
-
 	@Override
 	public void terminate(TokenCollector tokenCollector) throws MessageDescriptorFormatException {
 		throw log.getNonTerminatedParameterException(
@@ -32,7 +27,6 @@ public void terminate(TokenCollector tokenCollector) throws MessageDescriptorFor
 	public void handleNonMetaCharacter(char character, TokenCollector tokenCollector)
 			throws MessageDescriptorFormatException {
 		tokenCollector.appendToToken( character );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -46,7 +40,6 @@ public void handleEndTerm(char character, TokenCollector tokenCollector) throws
 		tokenCollector.terminateToken();
 		BeginState beginState = new BeginState();
 		tokenCollector.transitionState( beginState );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -55,16 +48,11 @@ public void handleEscapeCharacter(char character, TokenCollector tokenCollector)
 		tokenCollector.appendToToken( character );
 		ParserState state = new EscapedState( this );
 		tokenCollector.transitionState( state );
-		tokenCollector.next();
-
 	}
 
 	@Override
 	public void handleELDesignator(char character, TokenCollector tokenCollector)
 			throws MessageDescriptorFormatException {
 		tokenCollector.appendToToken( character );
-		tokenCollector.next();
 	}
 }
-
-
diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/MessageState.java b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/MessageState.java
index bac9c7e46..37cd23503 100644
--- a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/MessageState.java
+++ b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/MessageState.java
@@ -16,11 +16,6 @@
 public class MessageState implements ParserState {
 	private static final Log log = LoggerFactory.make();
 
-	@Override
-	public void start(TokenCollector tokenCollector) {
-		throw new IllegalStateException( "The parsing of the message descriptor cannot start in this state." );
-	}
-
 	@Override
 	public void terminate(TokenCollector tokenCollector) throws MessageDescriptorFormatException {
 		tokenCollector.terminateToken();
@@ -30,7 +25,6 @@ public void terminate(TokenCollector tokenCollector) throws MessageDescriptorFor
 	public void handleNonMetaCharacter(char character, TokenCollector tokenCollector)
 			throws MessageDescriptorFormatException {
 		tokenCollector.appendToToken( character );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -42,7 +36,6 @@ public void handleBeginTerm(char character, TokenCollector tokenCollector) throw
 			tokenCollector.makeParameterToken();
 		}
 		tokenCollector.transitionState( new InterpolationTermState() );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -59,7 +52,6 @@ public void handleEscapeCharacter(char character, TokenCollector tokenCollector)
 		tokenCollector.appendToToken( character );
 
 		tokenCollector.transitionState( new EscapedState( this ) );
-		tokenCollector.next();
 	}
 
 	@Override
@@ -70,9 +62,6 @@ public void handleELDesignator(char character, TokenCollector tokenCollector)
 		}
 		else {
 			tokenCollector.transitionState( new ELState() );
-			tokenCollector.next();
 		}
 	}
 }
-
-
diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/ParserState.java b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/ParserState.java
index a48a48f2b..64317de46 100644
--- a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/ParserState.java
+++ b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/ParserState.java
@@ -13,7 +13,6 @@
  * @author Hardy Ferentschik
  */
 public interface ParserState {
-	void start(TokenCollector tokenCollector) throws MessageDescriptorFormatException;
 
 	void terminate(TokenCollector tokenCollector) throws MessageDescriptorFormatException;
 
@@ -27,4 +26,3 @@
 
 	void handleELDesignator(char character, TokenCollector tokenCollector) throws MessageDescriptorFormatException;
 }
-
diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/TokenCollector.java b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/TokenCollector.java
index 1443eee7f..ffce7bb68 100644
--- a/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/TokenCollector.java
+++ b/engine/src/main/java/org/hibernate/validator/internal/engine/messageinterpolation/parser/TokenCollector.java
@@ -6,13 +6,13 @@
  */
 package org.hibernate.validator.internal.engine.messageinterpolation.parser;
 
+import static org.hibernate.validator.internal.util.CollectionHelper.newArrayList;
+
 import java.util.Collections;
 import java.util.List;
 
 import org.hibernate.validator.internal.engine.messageinterpolation.InterpolationTermType;
 
-import static org.hibernate.validator.internal.util.CollectionHelper.newArrayList;
-
 /**
  * Used to creates a list of tokens from a message descriptor.
  *
@@ -28,7 +28,7 @@
 	private final String originalMessageDescriptor;
 	private final InterpolationTermType interpolationTermType;
 
-	private List<Token> tokenList;
+	private final List<Token> tokenList;
 	private ParserState currentParserState;
 	private int currentPosition;
 	private Token currentToken;
@@ -69,10 +69,11 @@ public void makeELToken() {
 		currentToken.makeELToken();
 	}
 
-	public void next() throws MessageDescriptorFormatException {
+	private void next() throws MessageDescriptorFormatException {
 		if ( currentPosition == originalMessageDescriptor.length() ) {
 			// give the current context the chance to complete
 			currentParserState.terminate( this );
+			currentPosition++;
 			return;
 		}
 		char currentCharacter = originalMessageDescriptor.charAt( currentPosition );
@@ -98,12 +99,12 @@ public void next() throws MessageDescriptorFormatException {
 				currentParserState.handleNonMetaCharacter( currentCharacter, this );
 			}
 		}
-		// make sure the last token is terminated
-		terminateToken();
 	}
 
 	public void parse() throws MessageDescriptorFormatException {
-		currentParserState.start( this );
+		while ( currentPosition <= originalMessageDescriptor.length() ) {
+			next();
+		}
 	}
 
 	public void transitionState(ParserState newState) {
@@ -122,4 +123,3 @@ public String getOriginalMessageDescriptor() {
 		return originalMessageDescriptor;
 	}
 }
-
diff --git a/engine/src/test/java/org/hibernate/validator/bugs/TooBigMessageTest.java b/engine/src/test/java/org/hibernate/validator/bugs/TooBigMessageTest.java
new file mode 100644
index 000000000..4eb8ffc18
--- /dev/null
+++ b/engine/src/test/java/org/hibernate/validator/bugs/TooBigMessageTest.java
@@ -0,0 +1,90 @@
+/*
+ * Hibernate Validator, declare and validate application constraints
+ *
+ * License: Apache License, Version 2.0
+ * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
+ */
+package org.hibernate.validator.bugs;
+
+import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertCorrectConstraintViolationMessages;
+import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertNumberOfViolations;
+
+import java.util.Set;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validator;
+import javax.validation.constraints.NotNull;
+
+import org.hibernate.validator.testutil.TestForIssue;
+import org.hibernate.validator.testutils.ValidatorUtil;
+import org.junit.Test;
+
+/**
+ * Ensure large error messages can be interpolated.
+ *
+ * @author Gunnar Morling
+ */
+public class TooBigMessageTest {
+
+	/**
+	 * Large enough to trigger a stack overflow with the recursive scheme, assuming default settings
+	 */
+	private static final String LARGE_MESSAGE =
+			"12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+			+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890";
+
+	@Test
+	@TestForIssue(jiraKey = "HV-1091")
+	public void largeMessageCanBeInterpolated() {
+		Validator validator = ValidatorUtil.getValidator();
+		GoldFish fish = new GoldFish();
+
+		Set<ConstraintViolation<GoldFish>> constraintViolations = validator.validate( fish );
+		assertNumberOfViolations( constraintViolations, 1 );
+		assertCorrectConstraintViolationMessages( constraintViolations, LARGE_MESSAGE );
+	}
+
+	private static class GoldFish {
+
+		@NotNull(message = LARGE_MESSAGE)
+		String name;
+	}
+}
diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/messageinterpolation/TokenIteratorTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/messageinterpolation/TokenIteratorTest.java
index a63e1996a..281f3bccf 100644
--- a/engine/src/test/java/org/hibernate/validator/test/internal/engine/messageinterpolation/TokenIteratorTest.java
+++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/messageinterpolation/TokenIteratorTest.java
@@ -6,15 +6,15 @@
  */
 package org.hibernate.validator.test.internal.engine.messageinterpolation;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
 import org.hibernate.validator.internal.engine.messageinterpolation.InterpolationTermType;
 import org.hibernate.validator.internal.engine.messageinterpolation.parser.TokenCollector;
 import org.hibernate.validator.internal.engine.messageinterpolation.parser.TokenIterator;
 import org.testng.annotations.Test;
 
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertTrue;
-
 /**
  * Tests for {@code TokenIterator}.
  *
