2727import static java .lang .foreign .ValueLayout .JAVA_BYTE ;
2828import static org .apache .datasketches .hash .MurmurHash3 .hash ;
2929
30+ import java .io .File ;
31+ import java .io .IOException ;
32+ import java .io .InputStream ;
3033import java .lang .foreign .MemorySegment ;
34+ import java .net .URI ;
35+ import java .net .URISyntaxException ;
36+ import java .net .URL ;
3137import java .nio .ByteOrder ;
38+ import java .nio .file .Files ;
39+ import java .nio .file .StandardCopyOption ;
3240import java .util .Comparator ;
41+ import java .util .Objects ;
3342
3443/**
3544 * Common utility functions.
@@ -45,6 +54,8 @@ public final class Util {
4554 }
4655 }
4756
57+ /** Common static constants ******************/
58+
4859 /**
4960 * The java line separator character as a String.
5061 */
@@ -100,7 +111,7 @@ public final class Util {
100111
101112 private Util () {}
102113
103- // Byte Conversions
114+ /** Byte Conversions *************************/
104115
105116 /**
106117 * Returns an int extracted from a Little-Endian byte array.
@@ -162,7 +173,7 @@ public static byte[] longToBytes(final long v, final byte[] arr) {
162173 return arr ;
163174 }
164175
165- // Byte array conversions
176+ /** Byte array conversions *******************/
166177
167178 static long [] convertToLongArray (final byte [] byteArr , final boolean littleEndian ) {
168179 final int len = byteArr .length ;
@@ -187,7 +198,7 @@ static long[] convertToLongArray(final byte[] byteArr, final boolean littleEndia
187198 return longArr ;
188199 }
189200
190- // String Related
201+ /** String Related ***************************/
191202
192203 /**
193204 * Returns a string of spaced hex bytes in Big-Endian order.
@@ -295,7 +306,24 @@ public static String characterPad(final String s, final int fieldLength, final c
295306 return s ;
296307 }
297308
298- //Memory byte alignment
309+ /**
310+ * Converts the given number to a string prepended with spaces, if necessary, to
311+ * match the given length.
312+ *
313+ * <p>For example, assume a sequence of integers from 1 to 1000. The largest value has
314+ * four decimal digits. Convert the entire sequence of strings to the form " 1" to "1000".
315+ * When these strings are sorted they will be in numerical sequence: " 1", " 2", ... "1000".</p>
316+ *
317+ * @param number the given number
318+ * @param length the desired string length.
319+ * @return the given number to a string prepended with spaces
320+ */
321+ public static String longToFixedLengthString (final long number , final int length ) {
322+ final String num = Long .toString (number );
323+ return characterPad (num , length , ' ' , false );
324+ }
325+
326+ /** Memory byte alignment ********************/
299327
300328 /**
301329 * Checks if parameter v is a multiple of 8 and greater than zero.
@@ -319,7 +347,7 @@ public static boolean isMultipleOf8AndGT0(final long v) {
319347 return ((v & 0X7L ) == 0L ) && (v > 0L );
320348 }
321349
322- // Powers of 2 or powers of base related
350+ /** Powers of 2 or powers of base related ****/
323351
324352 /**
325353 * Returns true if given long argument is exactly a positive power of 2.
@@ -591,8 +619,8 @@ public static double floorPowerBaseOfDouble(final double base, final double n) {
591619 return Math .round (pow (base , floor (logBaseOfX (base , x ))));
592620 }
593621
594- // Logarithm related
595-
622+ /** Logarithm related ************************/
623+
596624 /**
597625 * The log<sub>2</sub>(value)
598626 * @param value the given value
@@ -690,8 +718,8 @@ public static int exactLog2OfLong(final long powerOf2) {
690718 return Long .numberOfTrailingZeros (powerOf2 );
691719 }
692720
693- // Checks that throw
694-
721+ /** Checks that throw ************************/
722+
695723 /**
696724 * Check the requested offset and length against the allocated size.
697725 * The invariants equation is: {@code 0 <= reqOff <= reqLen <= reqOff + reqLen <= allocSize}.
@@ -724,7 +752,7 @@ public static void checkProbability(final double p, final String argName) {
724752 + "\" must be between 0.0 inclusive and 1.0 inclusive: " + p );
725753 }
726754
727- // Boolean Checks
755+ /** Boolean Checks ***************************/
728756
729757 /**
730758 * Unsigned compare with longs.
@@ -754,7 +782,7 @@ public static boolean isOdd(final long n) {
754782 return (n & 1L ) == 1L ;
755783 }
756784
757- //Other
785+ /** Bit Number ******************************/
758786
759787 /**
760788 * Returns a one if the bit at bitPos is a one, otherwise zero.
@@ -767,6 +795,8 @@ public static int bitAt(final long number, final int bitPos) {
767795 return (number & (1L << bitPos )) > 0 ? 1 : 0 ;
768796 }
769797
798+ /** Decimal Digits ***************************/
799+
770800 /**
771801 * Computes the number of decimal digits of the number n
772802 * @param n the given number
@@ -777,25 +807,8 @@ public static int numDigits(long n) {
777807 return (int ) ceil (log (n ) / log (10 ));
778808 }
779809
780- /**
781- * Converts the given number to a string prepended with spaces, if necessary, to
782- * match the given length.
783- *
784- * <p>For example, assume a sequence of integers from 1 to 1000. The largest value has
785- * four decimal digits. Convert the entire sequence of strings to the form " 1" to "1000".
786- * When these strings are sorted they will be in numerical sequence: " 1", " 2", ... "1000".</p>
787- *
788- * @param number the given number
789- * @param length the desired string length.
790- * @return the given number to a string prepended with spaces
791- */
792- public static String longToFixedLengthString (final long number , final int length ) {
793- final String num = Long .toString (number );
794- return characterPad (num , length , ' ' , false );
795- }
796-
797- //Generic tests
798-
810+ /** Generic relational tests *****************/
811+
799812 /**
800813 * Finds the minimum of two generic items
801814 * @param <T> the type
@@ -844,7 +857,7 @@ public static <T> boolean le(final Object item1, final Object item2, final Compa
844857 return c .compare ((T )item1 , (T )item2 ) <= 0 ;
845858 }
846859
847- // MemorySegment related
860+ /** MemorySegment related ********************/
848861
849862 /**
850863 * Clears all bytes of this MemorySegment to zero.
@@ -957,6 +970,8 @@ public static void setBits(final MemorySegment seg, final long offsetBytes, fina
957970 seg .set (JAVA_BYTE , offsetBytes , (byte )(b | bitMask ));
958971 }
959972
973+ /** Seed Hashes ******************************/
974+
960975 /**
961976 * Computes and checks the 16-bit seed hash from the given long seed.
962977 * The seed hash may not be zero in order to maintain compatibility with older serialized
@@ -989,5 +1004,103 @@ public static short checkSeedHashes(final short seedHashA, final short seedHashB
9891004 }
9901005 return seedHashA ;
9911006 }
1007+
1008+ /** Files and File Bytes ******************************/
1009+
1010+ /**
1011+ * Windows, POSIX, and JAR friendly, returns a byte array of the contents of the file defined by the given
1012+ * resourceName.
1013+ * If the resource is in a JAR it will be copied into the File System as a temporary file first.
1014+ *
1015+ * @param resourceName the short name or the full path name.
1016+ * @return a byte array of the contents of the file defined by the given resourceName.
1017+ */
1018+ public static byte [] getResourceBytes (final String resourceName ) {
1019+ Objects .requireNonNull (resourceName , "Given resourceName must not be null" );
1020+
1021+ String normalizedName = resourceName .replace ('\\' , '/' );
1022+ if (normalizedName .startsWith ("/" )) {
1023+ normalizedName = normalizedName .substring (1 );
1024+ }
1025+
1026+ final ClassLoader loader = Util .class .getClassLoader ();
1027+ try (InputStream in = loader .getResourceAsStream (normalizedName )) {
1028+ if (in == null ) {
1029+ throw new IllegalArgumentException ("Resource not found: " + normalizedName );
1030+ }
1031+ return in .readAllBytes ();
1032+ } catch (final IOException e ) {
1033+ throw new IllegalArgumentException ("Cannot read resource: " + normalizedName + Util .LS + e );
1034+ }
1035+ }
1036+
1037+ /**
1038+ * Windows, POSIX, and JAR friendly get Resource File.
1039+ * If the resource is in a JAR it will be copied into the File System as a temporary file first.
1040+ *
1041+ * <p>While tempFile.deleteOnExit() works, keep in mind that JVMs running long processes won't delete those files
1042+ * until the JVM completely terminates, which can lead to memory/disk leaks if this method is called frequently.
1043+ * If you're using this for memory-mapped files, consider explicitly deleting the file once the memory-mapped buffer
1044+ * is no longer in use, or registering a custom shutdown hook if deleteOnExit() is insufficient.</p>
1045+ * @param resourceName the simple file name or full path name.
1046+ * Any back-slashes will be converted to forward slashes and a leading forward slash will be removed.
1047+ * No other special characters allowed.
1048+ * @return a File System File
1049+ */
1050+ public static File getResourceFile (final String resourceName ) {
1051+ Objects .requireNonNull (resourceName , "Given resourceName must not be null" );
1052+ if (resourceName .isEmpty ()) { throw new IllegalArgumentException ("Given resourceName must not be empty" ); }
1053+ // Normalize name: ClassLoaders MUST use forward slashes even on Windows
1054+ String normalizedName = resourceName .replace ('\\' , '/' );
1055+ if (normalizedName .startsWith ("/" )) { normalizedName = normalizedName .substring (1 ); }
1056+
1057+ final ClassLoader loader = Util .class .getClassLoader ();
1058+ final URL url = loader .getResource (normalizedName );
1059+ if (url == null ) { throw new IllegalArgumentException ("Resource not found: " + normalizedName ); }
1060+
1061+ // If it's a real file, return it directly
1062+ if ("file" .equals (url .getProtocol ())) {
1063+ try {
1064+ final URI uri = url .toURI ();
1065+ return new File (uri ); }
1066+ catch (final URISyntaxException e ) { return new File (url .getPath ()); }
1067+ }
1068+
1069+ // If it's in a JAR, we must extract it for Memory.map() to work
1070+ // We use a prefix that won't collide with Windows reserved names
1071+ // We use NIO Files.createTempFile to ensure secure default permissions (0600)
1072+ final File tempFile ;
1073+ try { tempFile = Files .createTempFile ("datasketches-" , ".bin" ).toFile (); }
1074+ catch (final IOException e1 ) { throw new IllegalArgumentException (e1 ); }
1075+ tempFile .deleteOnExit ();
1076+
1077+ try (InputStream in = loader .getResourceAsStream (normalizedName )) {
1078+ if (in == null ) { throw new IllegalArgumentException ("Could not open stream for " + normalizedName ); }
1079+
1080+ // Use REPLACE_EXISTING to avoid "File Already Exists" errors on Windows retries
1081+ Files .copy (in , tempFile .toPath (), StandardCopyOption .REPLACE_EXISTING );
1082+ } catch (final IOException e ) { throw new IllegalArgumentException (e ); }
1083+
1084+ // Final Windows Fix: Ensure the file is actually writable if you need to setReadOnly later
1085+ //tempFile.setWritable(true);
1086+
1087+ return tempFile ;
1088+ }
1089+
1090+ /**
1091+ * Windows, POSIX, and JAR friendly, checks if the given resourceName exists and sets it to Read-Only.
1092+ * If the resource is in a JAR it will be copied into the File System as a temporary file first.
1093+ * This will not work if the file is currently memory-mapped.
1094+ * If it is memory-mapped, close the mapping first.
1095+ * @param resourceName the given resource.
1096+ * @return the read only file.
1097+ */
1098+ public static File setResourceReadOnly (final String resourceName ) {
1099+ final File file = getResourceFile (resourceName );
1100+ if (!file .setReadOnly ()) {
1101+ throw new IllegalArgumentException ("Could not set read-only for resource file: " + file .getAbsolutePath ());
1102+ }
1103+ return file ;
1104+ }
9921105
9931106}
0 commit comments