From 275eaf63724f2cc743b23fd7f896ddbc6288b9e2 Mon Sep 17 00:00:00 2001 From: dae won Date: Tue, 4 Aug 2026 14:58:41 +0900 Subject: [PATCH] [ZEPPELIN-6484] Deduplicate IdHashes and add unit tests --- .../apache/zeppelin/util/IdHashesTest.java | 74 ++++++++++++++++++ .../org/apache/zeppelin/notebook/Note.java | 2 +- .../zeppelin/notebook/utility/IdHashes.java | 76 ------------------- 3 files changed, 75 insertions(+), 77 deletions(-) create mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/util/IdHashesTest.java delete mode 100644 zeppelin-server/src/main/java/org/apache/zeppelin/notebook/utility/IdHashes.java 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()); - } -}