diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/util/IdHashesTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/util/IdHashesTest.java
new file mode 100644
index 00000000000..5298126f766
--- /dev/null
+++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/util/IdHashesTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.zeppelin.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+class IdHashesTest {
+
+ /**
+ * The characters generated IDs are expected to be built from: digits 1-9 and A-Z without
+ * I, L and O. Those three letters and the digit 0 are left out so that an ID stays
+ * unambiguous when a person reads it off a URL.
+ *
+ *
Spelled out here rather than read back from IdHashes so that changing the dictionary
+ * has to be a deliberate act that updates this test too.
+ */
+ private static final String EXPECTED_CHARACTERS = "123456789ABCDEFGHJKMNPQRSTUVWXYZ";
+
+ /**
+ * IDs are derived from {@code currentTimeMillis() + SecureRandom.nextInt()}, so uniqueness
+ * can only be asserted probabilistically. The random term spans 2^32 values, which puts the
+ * chance of a collision within one sample of this size on the order of 1e-4.
+ */
+ private static final int SAMPLE_SIZE = 1000;
+
+ @Test
+ void generatedIdsContainOnlyDictionaryCharacters() {
+ for (int i = 0; i < SAMPLE_SIZE; i++) {
+ String id = IdHashes.generateId();
+ for (char c : id.toCharArray()) {
+ assertTrue(EXPECTED_CHARACTERS.indexOf(c) >= 0,
+ "generated id '" + id + "' contains '" + c + "', which is not in the dictionary");
+ }
+ }
+ }
+
+ @Test
+ void generatedIdsAreNeverEmpty() {
+ for (int i = 0; i < SAMPLE_SIZE; i++) {
+ assertFalse(IdHashes.generateId().isEmpty(), "generateId() returned an empty id");
+ }
+ }
+
+ @Test
+ void generatedIdsAreDistinctAcrossManyCalls() {
+ Set ids = new HashSet<>();
+ for (int i = 0; i < SAMPLE_SIZE; i++) {
+ ids.add(IdHashes.generateId());
+ }
+ assertEquals(SAMPLE_SIZE, ids.size(), "generateId() produced duplicate ids");
+ }
+}
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java
index 8b7622e1ef1..278fa43ecc6 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java
@@ -36,11 +36,11 @@
import org.apache.zeppelin.interpreter.remote.RemoteAngularObject;
import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry;
import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion;
-import org.apache.zeppelin.notebook.utility.IdHashes;
import org.apache.zeppelin.scheduler.ExecutorFactory;
import org.apache.zeppelin.scheduler.Job.Status;
import org.apache.zeppelin.user.AuthenticationInfo;
import org.apache.zeppelin.user.Credentials;
+import org.apache.zeppelin.util.IdHashes;
import org.apache.zeppelin.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/utility/IdHashes.java b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/utility/IdHashes.java
deleted file mode 100644
index 7b0d804de94..00000000000
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/utility/IdHashes.java
+++ /dev/null
@@ -1,76 +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.zeppelin.notebook.utility;
-
-import java.math.BigInteger;
-import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Generate Tiny ID.
- */
-public class IdHashes {
- private static final char[] DICTIONARY = new char[] {'1', '2', '3', '4', '5', '6', '7', '8', '9',
- 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
- 'W', 'X', 'Y', 'Z'};
-
- /**
- * encodes the given string into the base of the dictionary provided in the constructor.
- *
- * @param value the number to encode.
- * @return the encoded string.
- */
- private static String encode(Long value) {
-
- List result = new ArrayList<>();
- BigInteger base = new BigInteger("" + DICTIONARY.length);
- int exponent = 1;
- BigInteger remaining = new BigInteger(value.toString());
- while (true) {
- BigInteger a = base.pow(exponent); // 16^1 = 16
- BigInteger b = remaining.mod(a); // 119 % 16 = 7 | 112 % 256 = 112
- BigInteger c = base.pow(exponent - 1);
- BigInteger d = b.divide(c);
-
- // if d > dictionary.length, we have a problem. but BigInteger doesnt have
- // a greater than method :-( hope for the best. theoretically, d is always
- // an index of the dictionary!
- result.add(DICTIONARY[d.intValue()]);
- remaining = remaining.subtract(b); // 119 - 7 = 112 | 112 - 112 = 0
-
- // finished?
- if (remaining.equals(BigInteger.ZERO)) {
- break;
- }
-
- exponent++;
- }
-
- // need to reverse it, since the start of the list contains the least significant values
- StringBuffer sb = new StringBuffer();
- for (int i = result.size() - 1; i >= 0; i--) {
- sb.append(result.get(i));
- }
- return sb.toString();
- }
-
- public static String generateId() {
- return encode(System.currentTimeMillis() + new SecureRandom().nextInt());
- }
-}