diff --git a/README.md b/README.md index f00e42c7d..394d9dad2 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Both of these features ensure that our library can be seamlessly integrated into To include in your project: ##### Gradle ```groovy -implementation 'com.cedarsoftware:java-util:2.16.0' +implementation 'com.cedarsoftware:java-util:2.17.0' ``` ##### Maven @@ -33,7 +33,7 @@ implementation 'com.cedarsoftware:java-util:2.16.0' com.cedarsoftware java-util - 2.16.0 + 2.17.0 ``` --- diff --git a/changelog.md b/changelog.md index 3eb2c631b..d24b7d711 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,8 @@ ### Revision History +#### 2.17.0 +> * `ClassUtilities.getClassLoader()` added. This will safely return the correct class loader when running in OSGi, JPMS, or neither. +> * `ArrayUtilities.createArray()` added. This method accepts a variable number of arguments and returns them as an array of type `T[].` +> * Fixed bug when converting `Map` containing "time" key (and no `date` nor `zone` keys) with value to `java.sql.Date.` The millisecond portion was set to 0. #### 2.16.0 > * `SealableMap, LRUCache,` and `TTLCache` updated to use `ConcurrentHashMapNullSafe` internally, to simplify their implementation, as they no longer have to implement the null-safe work, `ConcurrentHashMapNullSafe` does that for them. > * Added `ConcurrentNavigableMapNullSafe` and `ConcurrentNavigableSetNullSafe` diff --git a/pom.xml b/pom.xml index 96c4ef527..bc34a6fba 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.cedarsoftware java-util bundle - 2.16.0 + 2.17.0 Java Utilities https://github.com/jdereg/java-util @@ -36,8 +36,8 @@ 5.11.1 5.11.1 4.11.0 - 3.26.3 - 4.26.0 + 3.24.2 + 4.28.0 1.22.0 @@ -57,6 +57,33 @@ + + + jdk9-and-above + + [9,) + + + 8 + + + + + + + + + jdk8 + + 1.8 + + + 1.8 + 1.8 + + + + release-sign-artifacts @@ -184,9 +211,10 @@ maven-compiler-plugin ${version.maven-compiler-plugin} + ${maven.compiler.release} ${maven.compiler.source} ${maven.compiler.target} - ${maven.compiler.release} + ${project.build.sourceEncoding} diff --git a/src/main/java/com/cedarsoftware/util/ArrayUtilities.java b/src/main/java/com/cedarsoftware/util/ArrayUtilities.java index 02f898423..91715787f 100644 --- a/src/main/java/com/cedarsoftware/util/ArrayUtilities.java +++ b/src/main/java/com/cedarsoftware/util/ArrayUtilities.java @@ -99,6 +99,44 @@ public static T[] shallowCopy(final T[] array) return array.clone(); } + /** + * Creates and returns an array containing the provided elements. + * + *

This method accepts a variable number of arguments and returns them as an array of type {@code T[]}. + * It is primarily used to facilitate array creation in generic contexts, where type inference is necessary. + * + *

Example Usage: + *

{@code
+     * String[] stringArray = createArray("Apple", "Banana", "Cherry");
+     * Integer[] integerArray = createArray(1, 2, 3, 4);
+     * Person[] personArray = createArray(new Person("Alice"), new Person("Bob"));
+     * }
+ * + *

Important Considerations: + *

    + *
  • Type Safety: Due to type erasure in Java generics, this method does not perform any type checks + * beyond what is already enforced by the compiler. Ensure that all elements are of the expected type {@code T} to avoid + * {@code ClassCastException} at runtime.
  • + *
  • Heap Pollution: The method is annotated with {@link SafeVarargs} to suppress warnings related to heap + * pollution when using generics with varargs. It is safe to use because the method does not perform any unsafe operations + * on the varargs parameter.
  • + *
  • Null Elements: The method does not explicitly handle {@code null} elements. If {@code null} values + * are passed, they will be included in the returned array.
  • + *
  • Immutable Arrays: The returned array is mutable. To create an immutable array, consider wrapping it + * using {@link java.util.Collections#unmodifiableList(List)} or using third-party libraries like Guava's + * {@link com.google.common.collect.ImmutableList}.
  • + *
+ * + * @param the component type of the array + * @param elements the elements to be stored in the array + * @return an array containing the provided elements + * @throws NullPointerException if the {@code elements} array is {@code null} + */ + @SafeVarargs + public static T[] createArray(T... elements) { + return elements; + } + /** *

Adds all the elements of the given arrays into a new array. *

diff --git a/src/main/java/com/cedarsoftware/util/ClassUtilities.java b/src/main/java/com/cedarsoftware/util/ClassUtilities.java index 4e467a0c0..cd4536169 100644 --- a/src/main/java/com/cedarsoftware/util/ClassUtilities.java +++ b/src/main/java/com/cedarsoftware/util/ClassUtilities.java @@ -43,9 +43,11 @@ public class ClassUtilities private static final Map, Class> primitiveToWrapper = new HashMap<>(20, .8f); private static final Map> nameToClass = new HashMap<>(); private static final Map, Class> wrapperMap = new HashMap<>(); + // Cache for OSGi ClassLoader to avoid repeated reflection calls + private static volatile ClassLoader osgiClassLoader; + private static volatile boolean osgiChecked = false; - static - { + static { prims.add(Byte.class); prims.add(Short.class); prims.add(Integer.class); @@ -99,11 +101,11 @@ public class ClassUtilities * Add alias names for classes to allow .forName() to bring the class (.class) back with the alias name. * Because the alias to class name mappings are static, it is expected that these are set up during initialization * and not changed later. + * * @param clazz Class to add an alias for * @param alias String alias name */ - public static void addPermanentClassAlias(Class clazz, String alias) - { + public static void addPermanentClassAlias(Class clazz, String alias) { nameToClass.put(alias, clazz); } @@ -111,15 +113,16 @@ public static void addPermanentClassAlias(Class clazz, String alias) * Remove alias name for classes to prevent .forName() from fetching the class with the alias name. * Because the alias to class name mappings are static, it is expected that these are set up during initialization * and not changed later. + * * @param alias String alias name */ - public static void removePermanentClassAlias(String alias) - { + public static void removePermanentClassAlias(String alias) { nameToClass.remove(alias); } /** * Computes the inheritance distance between two classes/interfaces/primitive types. + * * @param source The source class, interface, or primitive type. * @param destination The destination class, interface, or primitive type. * @return The number of steps from the source to the destination, or -1 if no path exists. @@ -197,29 +200,27 @@ public static int computeInheritanceDistance(Class source, Class destinati * @return boolean true if the passed in class is a Java primitive, false otherwise. The Wrapper classes * Integer, Long, Boolean, etc. are considered primitives by this method. */ - public static boolean isPrimitive(Class c) - { + public static boolean isPrimitive(Class c) { return c.isPrimitive() || prims.contains(c); } /** * Compare two primitives. + * * @return 0 if they are the same, -1 if not. Primitive wrapper classes are consider the same as primitive classes. */ private static int comparePrimitiveToWrapper(Class source, Class destination) { - try - { + try { return source.getField("TYPE").get(null).equals(destination) ? 0 : -1; - } - catch (Exception e) - { + } catch (Exception e) { return -1; } } /** * Given the passed in String class name, return the named JVM class. - * @param name String name of a JVM class. + * + * @param name String name of a JVM class. * @param classLoader ClassLoader to use when searching for JVM classes. * @return Class instance of the named JVM class or null if not found. */ @@ -230,7 +231,7 @@ public static Class forName(String name, ClassLoader classLoader) { try { return internalClassForName(name, classLoader); - } catch(SecurityException e) { + } catch (SecurityException e) { throw new IllegalArgumentException("Security exception, classForName() call on: " + name, e); } catch (Exception e) { return null; @@ -272,66 +273,43 @@ private static Class loadClass(String name, ClassLoader classLoader) throws C boolean arrayType = false; Class primitiveArray = null; - while (className.startsWith("[")) - { + while (className.startsWith("[")) { arrayType = true; - if (className.endsWith(";")) - { + if (className.endsWith(";")) { className = className.substring(0, className.length() - 1); } - if (className.equals("[B")) - { + if (className.equals("[B")) { primitiveArray = byte[].class; - } - else if (className.equals("[S")) - { + } else if (className.equals("[S")) { primitiveArray = short[].class; - } - else if (className.equals("[I")) - { + } else if (className.equals("[I")) { primitiveArray = int[].class; - } - else if (className.equals("[J")) - { + } else if (className.equals("[J")) { primitiveArray = long[].class; - } - else if (className.equals("[F")) - { + } else if (className.equals("[F")) { primitiveArray = float[].class; - } - else if (className.equals("[D")) - { + } else if (className.equals("[D")) { primitiveArray = double[].class; - } - else if (className.equals("[Z")) - { + } else if (className.equals("[Z")) { primitiveArray = boolean[].class; - } - else if (className.equals("[C")) - { + } else if (className.equals("[C")) { primitiveArray = char[].class; } int startpos = className.startsWith("[L") ? 2 : 1; className = className.substring(startpos); } Class currentClass = null; - if (null == primitiveArray) - { - try - { + if (null == primitiveArray) { + try { currentClass = classLoader.loadClass(className); - } - catch (ClassNotFoundException e) - { + } catch (ClassNotFoundException e) { currentClass = Thread.currentThread().getContextClassLoader().loadClass(className); } } - if (arrayType) - { + if (arrayType) { currentClass = (null != primitiveArray) ? primitiveArray : Array.newInstance(currentClass, 0).getClass(); - while (name.startsWith("[[")) - { + while (name.startsWith("[[")) { currentClass = Array.newInstance(currentClass, 0).getClass(); name = name.substring(1); } @@ -372,4 +350,89 @@ public static Class toPrimitiveWrapperClass(Class primitiveClass) { public static boolean doesOneWrapTheOther(Class x, Class y) { return wrapperMap.get(x) == y; } + + /** + * Obtains the appropriate ClassLoader depending on whether the environment is OSGi, JPMS, or neither. + * + * @return the appropriate ClassLoader + */ + public static ClassLoader getClassLoader() { + // Attempt to detect and handle OSGi environment + ClassLoader cl = getOSGiClassLoader(); + if (cl != null) { + return cl; + } + + // Use the thread's context ClassLoader if available + cl = Thread.currentThread().getContextClassLoader(); + if (cl != null) { + return cl; + } + + // Fallback to the ClassLoader that loaded this utility class + cl = ClassUtilities.class.getClassLoader(); + if (cl != null) { + return cl; + } + + // As a last resort, use the system ClassLoader + return ClassLoader.getSystemClassLoader(); + } + + /** + * Attempts to retrieve the OSGi Bundle's ClassLoader using FrameworkUtil. + * + * @return the OSGi Bundle's ClassLoader if in an OSGi environment; otherwise, null + */ + private static ClassLoader getOSGiClassLoader() { + if (osgiChecked) { + return osgiClassLoader; + } + + synchronized (ClassUtilities.class) { + if (osgiChecked) { + return osgiClassLoader; + } + + try { + // Load the FrameworkUtil class from OSGi + Class frameworkUtilClass = Class.forName("org.osgi.framework.FrameworkUtil"); + + // Get the getBundle(Class) method + Method getBundleMethod = frameworkUtilClass.getMethod("getBundle", Class.class); + + // Invoke FrameworkUtil.getBundle(thisClass) to get the Bundle instance + Object bundle = getBundleMethod.invoke(null, ClassUtilities.class); + + if (bundle != null) { + // Get BundleWiring class + Class bundleWiringClass = Class.forName("org.osgi.framework.wiring.BundleWiring"); + + // Get the adapt(Class) method + Method adaptMethod = bundle.getClass().getMethod("adapt", Class.class); + + // Invoke bundle.adapt(BundleWiring.class) to get the BundleWiring instance + Object bundleWiring = adaptMethod.invoke(bundle, bundleWiringClass); + + if (bundleWiring != null) { + // Get the getClassLoader() method from BundleWiring + Method getClassLoaderMethod = bundleWiringClass.getMethod("getClassLoader"); + + // Invoke getClassLoader() to obtain the ClassLoader + Object classLoader = getClassLoaderMethod.invoke(bundleWiring); + + if (classLoader instanceof ClassLoader) { + osgiClassLoader = (ClassLoader) classLoader; + } + } + } + } catch (Exception e) { + // OSGi FrameworkUtil is not present; not in an OSGi environment + } finally { + osgiChecked = true; + } + } + + return osgiClassLoader; + } } diff --git a/src/main/java/com/cedarsoftware/util/convert/MapConversions.java b/src/main/java/com/cedarsoftware/util/convert/MapConversions.java index 7fce43c89..8a7e75ca3 100644 --- a/src/main/java/com/cedarsoftware/util/convert/MapConversions.java +++ b/src/main/java/com/cedarsoftware/util/convert/MapConversions.java @@ -182,7 +182,7 @@ static java.sql.Date toSqlDate(Object from, Converter converter) { if (epochTime == null) { return fromMap(from, converter, java.sql.Date.class, new String[]{EPOCH_MILLIS}, new String[]{TIME, ZONE + OPTIONAL}, new String[]{DATE, TIME, ZONE + OPTIONAL}); } - return new java.sql.Date(epochTime.getKey()); + return new java.sql.Date(epochTime.getKey() + epochTime.getValue() / 1_000_000); } static Date toDate(Object from, Converter converter) { @@ -190,7 +190,7 @@ static Date toDate(Object from, Converter converter) { if (epochTime == null) { return fromMap(from, converter, Date.class, new String[]{EPOCH_MILLIS}, new String[]{TIME, ZONE + OPTIONAL}, new String[]{DATE, TIME, ZONE + OPTIONAL}); } - return new Date(epochTime.getKey()); + return new Date(epochTime.getKey() + epochTime.getValue() / 1_000_000); } /** @@ -220,9 +220,7 @@ static Timestamp toTimestamp(Object from, Converter converter) { } Timestamp timestamp = new Timestamp(epochTime.getKey()); - if (timestamp.getTime() % 1000 == 0) { // Add nanoseconds *if* Timestamp time was only to second resolution - timestamp.setNanos(epochTime.getValue()); - } + timestamp.setNanos(epochTime.getValue()); return timestamp; } diff --git a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java index 94e90732a..520f929da 100644 --- a/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java +++ b/src/test/java/com/cedarsoftware/util/convert/ConverterEverythingTest.java @@ -1748,6 +1748,7 @@ private static void loadSqlDateTests() { {zdt("1970-01-01T00:00:00.999Z"), new java.sql.Date(999), true}, }); TEST_DB.put(pair(Map.class, java.sql.Date.class), new Object[][] { + { mapOf(TIME, 1703043551033L), new java.sql.Date(1703043551033L), false}, { mapOf(EPOCH_MILLIS, -1L, DATE, "1970-01-01", TIME, "08:59:59.999", ZONE, TOKYO_Z.toString()), new java.sql.Date(-1L), true}, { mapOf(EPOCH_MILLIS, 0L, DATE, "1970-01-01", TIME, "09:00", ZONE, TOKYO_Z.toString()), new java.sql.Date(0L), true}, { mapOf(EPOCH_MILLIS, 1L, DATE, "1970-01-01", TIME, "09:00:00.001", ZONE, TOKYO_Z.toString()), new java.sql.Date(1L), true},