You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by fr...@apache.org on 2019/05/09 08:41:50 UTC

[calcite-avatica] 02/02: Revert "[CALCITE-2845] Avoid duplication of exception messages"

This is an automated email from the ASF dual-hosted git repository.

francischuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite-avatica.git

commit d52c2036224911d93fe3185f521768037e62a437
Author: yuzhao.cyz <yu...@alibaba-inc.com>
AuthorDate: Thu May 9 14:50:02 2019 +0800

    Revert "[CALCITE-2845] Avoid duplication of exception messages"
    
    This reverts commit 80ccd20
---
 .../apache/calcite/avatica/AvaticaConnection.java  | 11 +---
 .../calcite/avatica/AvaticaSqlException.java       |  5 +-
 .../apache/calcite/avatica/AvaticaStatement.java   |  8 +--
 .../java/org/apache/calcite/avatica/Helper.java    |  5 +-
 .../calcite/avatica/remote/AbstractHandler.java    | 13 +---
 .../org/apache/calcite/avatica/jdbc/JdbcMeta.java  | 43 +++++++------
 .../org/apache/calcite/avatica/ExceptionUtils.java | 44 -------------
 .../apache/calcite/avatica/RemoteDriverTest.java   | 13 ++--
 .../apache/calcite/avatica/jdbc/JdbcMetaTest.java  |  6 +-
 .../calcite/avatica/remote/RemoteMetaTest.java     | 11 ++--
 .../avatica/server/BasicAuthHttpServerTest.java    |  6 +-
 .../avatica/server/DigestAuthHttpServerTest.java   |  6 +-
 .../calcite/avatica/test/StringContainsOnce.java   | 75 ----------------------
 13 files changed, 58 insertions(+), 188 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java b/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java
index e28fe46..b3552f8 100644
--- a/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java
+++ b/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java
@@ -552,11 +552,9 @@ public abstract class AvaticaConnection implements Connection {
           statement.updateCount = metaResultSet.updateCount;
           signature2 = executeResult.resultSets.get(0).signature;
         }
-      } catch (SQLException e) {
-        // We don't add meaningful info yet, so just rethrow the original exception
-        throw e;
       } catch (Exception e) {
-        throw HELPER.createException("Error while executing a prepared statement", e);
+        e.printStackTrace();
+        throw HELPER.createException(e.getMessage(), e);
       }
 
       final TimeZone timeZone = getTimeZone();
@@ -575,12 +573,9 @@ public abstract class AvaticaConnection implements Connection {
         statement.openResultSet.execute();
         isUpdateCapable(statement);
       }
-    } catch (SQLException e) {
-      // We don't add meaningful info yet, so just rethrow the original exception
-      throw e;
     } catch (Exception e) {
       throw HELPER.createException(
-          "Error while executing a resultset", e);
+          "exception while executing query: " + e.getMessage(), e);
     }
     return statement.openResultSet;
   }
diff --git a/core/src/main/java/org/apache/calcite/avatica/AvaticaSqlException.java b/core/src/main/java/org/apache/calcite/avatica/AvaticaSqlException.java
index af71381..bb02f1f 100644
--- a/core/src/main/java/org/apache/calcite/avatica/AvaticaSqlException.java
+++ b/core/src/main/java/org/apache/calcite/avatica/AvaticaSqlException.java
@@ -82,10 +82,7 @@ public class AvaticaSqlException extends SQLException {
   }
 
   void printServerStackTrace(PrintStreamOrWriter streamOrWriter) {
-    List<String> traces = this.stackTraces;
-    for (int i = 0; i < traces.size(); i++) {
-      String serverStackTrace = traces.get(i);
-      streamOrWriter.println("Remote driver error #" + i + ":");
+    for (String serverStackTrace : this.stackTraces) {
       streamOrWriter.println(serverStackTrace);
     }
   }
diff --git a/core/src/main/java/org/apache/calcite/avatica/AvaticaStatement.java b/core/src/main/java/org/apache/calcite/avatica/AvaticaStatement.java
index ac76ce3..2d3c756 100644
--- a/core/src/main/java/org/apache/calcite/avatica/AvaticaStatement.java
+++ b/core/src/main/java/org/apache/calcite/avatica/AvaticaStatement.java
@@ -160,8 +160,8 @@ public abstract class AvaticaStatement
         }
       }
     } catch (RuntimeException e) {
-      throw AvaticaConnection.HELPER.createException(
-              "Error while executing SQL \"" + sql + "\"", e);
+      throw AvaticaConnection.HELPER.createException("Error while executing SQL \"" + sql + "\": "
+          + e.getMessage(), e);
     }
 
     throw new RuntimeException("Failed to successfully execute query after "
@@ -231,8 +231,8 @@ public abstract class AvaticaStatement
       }
       return openResultSet;
     } catch (RuntimeException e) {
-      throw AvaticaConnection.HELPER.createException(
-              "Error while executing SQL \"" + sql + "\"", e);
+      throw AvaticaConnection.HELPER.createException("Error while executing SQL \"" + sql + "\": "
+          + e.getMessage(), e);
     }
   }
 
diff --git a/core/src/main/java/org/apache/calcite/avatica/Helper.java b/core/src/main/java/org/apache/calcite/avatica/Helper.java
index 215e2b9..fe716db 100644
--- a/core/src/main/java/org/apache/calcite/avatica/Helper.java
+++ b/core/src/main/java/org/apache/calcite/avatica/Helper.java
@@ -50,10 +50,7 @@ public class Helper {
       if (null != rte.getRpcMetadata()) {
         serverAddress = rte.getRpcMetadata().serverAddress;
       }
-      // Note: we don't add "e" as a cause, so we add its message to the message of
-      // the newly created exception
-      return new AvaticaSqlException(message + ". " + e.getMessage(),
-          rte.getSqlState(), rte.getErrorCode(),
+      return new AvaticaSqlException(message, rte.getSqlState(), rte.getErrorCode(),
           rte.getServerExceptions(), serverAddress);
     }
     return new SQLException(message, e);
diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java b/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java
index ab9c7c6..b291d4c 100644
--- a/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java
+++ b/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java
@@ -145,7 +145,7 @@ public abstract class AbstractHandler<T> implements Handler<T> {
    * @param e The Exception to summarize.
    * @return A summary message for the Exception.
    */
-  private String getCausalChain(Throwable e) {
+  private String getCausalChain(Exception e) {
     StringBuilder sb = new StringBuilder(16);
     Throwable curr = e;
     // Could use Guava, but that would increase dependency set unnecessarily.
@@ -156,17 +156,6 @@ public abstract class AbstractHandler<T> implements Handler<T> {
       String message = curr.getMessage();
       sb.append(curr.getClass().getSimpleName()).append(": ");
       sb.append(null == message ? NULL_EXCEPTION_MESSAGE : message);
-      Throwable[] suppressed = curr.getSuppressed();
-      if (suppressed.length > 0) {
-        sb.append(", suppressed: [");
-        String sep = "";
-        for (Throwable throwable : suppressed) {
-          sb.append(sep);
-          sb.append(getCausalChain(throwable));
-          sep = ", ";
-        }
-        sb.append("]");
-      }
       curr = curr.getCause();
     }
     if (sb.length() == 0) {
diff --git a/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java b/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
index 824e65e..7f19b11 100644
--- a/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
+++ b/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
@@ -304,7 +304,7 @@ public class JdbcMeta implements ProtobufMeta {
       }
       return map;
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -315,7 +315,7 @@ public class JdbcMeta implements ProtobufMeta {
       try {
         propertyValue = p.method.invoke(metaData);
       } catch (IllegalAccessException | InvocationTargetException e) {
-        throw propagate(e);
+        throw new RuntimeException(e);
       }
     } else {
       propertyValue = p.defaultValue;
@@ -333,7 +333,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -359,7 +359,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -370,7 +370,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -380,7 +380,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -390,7 +390,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -403,7 +403,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -416,7 +416,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -429,7 +429,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -442,7 +442,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -457,7 +457,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -470,7 +470,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -483,7 +483,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -509,7 +509,7 @@ public class JdbcMeta implements ProtobufMeta {
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -630,7 +630,7 @@ public class JdbcMeta implements ProtobufMeta {
         throw new RuntimeException("Connection already exists: " + ch.id);
       }
     } catch (SQLException e) {
-      throw propagate(e);
+      throw new RuntimeException(e);
     }
   }
 
@@ -691,9 +691,14 @@ public class JdbcMeta implements ProtobufMeta {
     }
   }
 
-  static <E extends Throwable> RuntimeException propagate(Throwable e) throws E {
-    // We have nothing to add, so just throw the original exception
-    throw (E) e;
+  RuntimeException propagate(Throwable e) {
+    if (e instanceof RuntimeException) {
+      throw (RuntimeException) e;
+    } else if (e instanceof Error) {
+      throw (Error) e;
+    } else {
+      throw new RuntimeException(e);
+    }
   }
 
   public StatementHandle prepare(ConnectionHandle ch, String sql,
diff --git a/server/src/test/java/org/apache/calcite/avatica/ExceptionUtils.java b/server/src/test/java/org/apache/calcite/avatica/ExceptionUtils.java
deleted file mode 100644
index eff382f..0000000
--- a/server/src/test/java/org/apache/calcite/avatica/ExceptionUtils.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you 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.apache.calcite.avatica;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.Objects;
-
-/** Prints full stacktrace of {@link java.lang.Throwable} as a {@code String}. */
-public class ExceptionUtils {
-  private ExceptionUtils() {
-    // The constructor is extremely secret
-  }
-
-  /** Prints full stacktrace of {@link java.lang.Throwable} as a {@code String}.
-   * @param e exception
-   * @return string representation of a given exception
-   */
-  public static String toString(Exception e) {
-    //noinspection ThrowableResultOfMethodCallIgnored
-    Objects.requireNonNull(e);
-    StringWriter sw = new StringWriter();
-    PrintWriter pw = new PrintWriter(sw);
-    e.printStackTrace(pw);
-    pw.flush();
-    return sw.toString();
-  }
-}
-
-// End ExceptionUtils.java
diff --git a/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java b/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java
index 4d473ef..66fcf8c 100644
--- a/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java
@@ -67,12 +67,11 @@ import java.util.TimeZone;
 import java.util.UUID;
 import java.util.concurrent.Callable;
 
-import static org.apache.calcite.avatica.test.StringContainsOnce.containsStringOnce;
-
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.core.StringContains.containsString;
 import static org.hamcrest.core.StringStartsWith.startsWith;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -915,9 +914,8 @@ public class RemoteDriverTest {
       fail("expected error, got " + resultSet);
     } catch (SQLException e) {
       LOG.info("Caught expected error", e);
-      String message = ExceptionUtils.toString(e);
-      assertThat(message,
-          containsStringOnce("unbound parameter"));
+      assertThat(e.getMessage(),
+          containsString("exception while executing query: unbound parameter"));
     }
 
     final ParameterMetaData parameterMetaData = ps.getParameterMetaData();
@@ -1844,9 +1842,8 @@ public class RemoteDriverTest {
 
     @Override public Response _apply(Request request) {
       final RequestLogger logger = THREAD_LOG.get();
-      String jsonRequest = null;
       try {
-        jsonRequest = JsonService.MAPPER.writeValueAsString(request);
+        String jsonRequest = JsonService.MAPPER.writeValueAsString(request);
         logger.requestStart(jsonRequest);
 
         Response response = super._apply(request);
@@ -1856,7 +1853,7 @@ public class RemoteDriverTest {
 
         return response;
       } catch (Exception e) {
-        throw new RuntimeException("Request " + jsonRequest + " failed", e);
+        throw new RuntimeException(e);
       }
     }
   }
diff --git a/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java b/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
index 0c80f44..f3ebc8a 100644
--- a/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
@@ -50,11 +50,13 @@ public class JdbcMetaTest {
   @Test public void testExceptionPropagation() throws SQLException {
     JdbcMeta meta = new JdbcMeta("url");
     final Throwable e = new Exception();
+    final RuntimeException rte;
     try {
       meta.propagate(e);
       fail("Expected an exception to be thrown");
-    } catch (Throwable caughtException) {
-      assertThat(caughtException, is(e));
+    } catch (RuntimeException caughtException) {
+      rte = caughtException;
+      assertThat(rte.getCause(), is(e));
     }
   }
 
diff --git a/server/src/test/java/org/apache/calcite/avatica/remote/RemoteMetaTest.java b/server/src/test/java/org/apache/calcite/avatica/remote/RemoteMetaTest.java
index 452309c..2a1fe80 100644
--- a/server/src/test/java/org/apache/calcite/avatica/remote/RemoteMetaTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/remote/RemoteMetaTest.java
@@ -329,8 +329,10 @@ public class RemoteMetaTest {
       DriverManager.getConnection(url, "john", "doe");
       fail("expected exception");
     } catch (RuntimeException e) {
-      assertEquals("Remote driver error: "
-          + "SQLInvalidAuthorizationSpecException: invalid authorization specification - "
+      assertEquals("Remote driver error: RuntimeException: "
+          + "java.sql.SQLInvalidAuthorizationSpecException: invalid authorization specification"
+          + " - not found: john"
+          + " -> SQLInvalidAuthorizationSpecException: invalid authorization specification - "
           + "not found: john"
           + " -> HsqlException: invalid authorization specification - not found: john",
           e.getMessage());
@@ -355,8 +357,9 @@ public class RemoteMetaTest {
         stmt2.executeQuery("select * from buffer");
         fail("expected exception");
       } catch (Exception e) {
-        assertEquals("Error -1 (00000) : Error while executing SQL \"select * from buffer\". "
-            + "Remote driver error: "
+        assertEquals("Error -1 (00000) : Error while executing SQL \"select * from buffer\": "
+            + "Remote driver error: RuntimeException: java.sql.SQLSyntaxErrorException: "
+            + "user lacks privilege or object not found: BUFFER -> "
             + "SQLSyntaxErrorException: user lacks privilege or object not found: BUFFER -> "
             + "HsqlException: user lacks privilege or object not found: BUFFER",
             e.getMessage());
diff --git a/server/src/test/java/org/apache/calcite/avatica/server/BasicAuthHttpServerTest.java b/server/src/test/java/org/apache/calcite/avatica/server/BasicAuthHttpServerTest.java
index e11f1cb..9f9914a 100644
--- a/server/src/test/java/org/apache/calcite/avatica/server/BasicAuthHttpServerTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/server/BasicAuthHttpServerTest.java
@@ -149,8 +149,10 @@ public class BasicAuthHttpServerTest extends HttpAuthBase {
       readWriteData(url, "DISALLOWED_DB_USER", props);
       fail("Expected an exception");
     } catch (RuntimeException e) {
-      assertEquals("Remote driver error: "
-          + "SQLInvalidAuthorizationSpecException: invalid authorization specification - "
+      assertEquals("Remote driver error: RuntimeException: "
+          + "java.sql.SQLInvalidAuthorizationSpecException: invalid authorization specification"
+          + " - not found: USER1"
+          + " -> SQLInvalidAuthorizationSpecException: invalid authorization specification - "
           + "not found: USER1"
           + " -> HsqlException: invalid authorization specification - not found: USER1",
           e.getMessage());
diff --git a/server/src/test/java/org/apache/calcite/avatica/server/DigestAuthHttpServerTest.java b/server/src/test/java/org/apache/calcite/avatica/server/DigestAuthHttpServerTest.java
index 73b5bb5..a5d8e86 100644
--- a/server/src/test/java/org/apache/calcite/avatica/server/DigestAuthHttpServerTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/server/DigestAuthHttpServerTest.java
@@ -163,8 +163,10 @@ public class DigestAuthHttpServerTest extends HttpAuthBase {
       readWriteData(url, "DISALLOWED_HSQLDB_USER", props);
       fail("Expected a failure");
     } catch (RuntimeException e) {
-      assertEquals("Remote driver error: "
-          + "SQLInvalidAuthorizationSpecException: invalid authorization specification - "
+      assertEquals("Remote driver error: RuntimeException: "
+          + "java.sql.SQLInvalidAuthorizationSpecException: invalid authorization specification"
+          + " - not found: USER1"
+          + " -> SQLInvalidAuthorizationSpecException: invalid authorization specification - "
           + "not found: USER1"
           + " -> HsqlException: invalid authorization specification - not found: USER1",
           e.getMessage());
diff --git a/server/src/test/java/org/apache/calcite/avatica/test/StringContainsOnce.java b/server/src/test/java/org/apache/calcite/avatica/test/StringContainsOnce.java
deleted file mode 100644
index 3a55c1e..0000000
--- a/server/src/test/java/org/apache/calcite/avatica/test/StringContainsOnce.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you 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.apache.calcite.avatica.test;
-
-import org.hamcrest.Description;
-import org.hamcrest.Factory;
-import org.hamcrest.Matcher;
-import org.hamcrest.core.SubstringMatcher;
-
-/**
- * Tests if the argument is a string that contains a substring exactly once.
- */
-public class StringContainsOnce extends SubstringMatcher {
-  public StringContainsOnce(String substring) {
-    super(substring);
-  }
-
-  @Override public void describeMismatchSafely(String item, Description mismatchDescription) {
-    int cnt = countMatches(item);
-    if (cnt == 0) {
-      mismatchDescription.appendText("pattern is not found in \"");
-    } else if (cnt == 2) {
-      mismatchDescription.appendText("pattern is present more than once in \"");
-    }
-    mismatchDescription.appendText(item);
-    mismatchDescription.appendText("\"");
-  }
-
-  @Override protected boolean evalSubstringOf(String s) {
-    return countMatches(s) == 1;
-  }
-
-  private int countMatches(String s) {
-    int indexOf = s.indexOf(substring);
-    if (indexOf < 0) {
-      return 0;
-    }
-    // There should be just a single match
-    return s.indexOf(substring, indexOf + 1) == -1 ? 1 : 2;
-  }
-
-  @Override protected String relationship() {
-    return "containing exactly once";
-  }
-
-  /**
-   * Creates a matcher that matches if the examined {@link String} contains the specified
-   * {@link String} anywhere exactly once.
-   *
-   * <p>For example:
-   * <pre>assertThat("myStringOfNote", containsStringOnce("ring"))</pre>
-   * @param substring substring
-   * @return matcher
-   */
-  @Factory
-  public static Matcher<String> containsStringOnce(String substring) {
-    return new StringContainsOnce(substring);
-  }
-}
-
-// End StringContainsOnce.java