action = () -> {
+ Method method = EnumSet.class.getDeclaredMethod("getUniverse", Class.class);
+ method.setAccessible(true);
+ return method;
+ };
+ try {
+ Class> accessController = Class.forName("java.security.AccessController");
+ Method doPrivileged = accessController.getMethod("doPrivileged", PrivilegedExceptionAction.class);
+ return (Method) doPrivileged.invoke(null, action);
+ } catch (ClassNotFoundException | NoSuchMethodException ex) {
+ return action.run();
+ } catch (InvocationTargetException ex) {
+ Throwable cause = ex.getTargetException();
+ if (cause instanceof PrivilegedActionException) {
+ throw (PrivilegedActionException) cause;
+ }
+ if (cause instanceof Exception) {
+ throw new RuntimeException(cause);
+ }
+ throw new RuntimeException(ex);
+ } catch (IllegalAccessException ex) {
+ throw new RuntimeException(ex);
+ }
+ } catch (PrivilegedActionException e) {
+ throw new RuntimeException(e.getCause());
+ } catch (Exception e) {
throw new RuntimeException(e);
}
}
- private Enums() {
- }
-
/**
* Returns the constant array backing the supplied enum type.
*
diff --git a/src/main/java/net/openhft/chronicle/values/FloatingFieldModel.java b/src/main/java/net/openhft/chronicle/values/FloatingFieldModel.java
index 9b1c17987..af28dcf4a 100644
--- a/src/main/java/net/openhft/chronicle/values/FloatingFieldModel.java
+++ b/src/main/java/net/openhft/chronicle/values/FloatingFieldModel.java
@@ -315,7 +315,7 @@ FloatingFieldModel.this.type, oldName(),
@Override
void generateEquals(ValueBuilder valueBuilder, MethodSpec.Builder methodBuilder) {
methodBuilder.addCode(
- format("if ($N(%s) != $N(other.$N())) return false;\n",
+ format("if ($N(%s) != $N(other.$N())) return false;%n",
wrap(valueBuilder, methodBuilder, "$N")),
toBits(), field, toBits(), getOrGetVolatile().getName());
}
@@ -325,7 +325,7 @@ void generateArrayElementEquals(
ArrayFieldModel arrayFieldModel, ValueBuilder valueBuilder,
MethodSpec.Builder methodBuilder) {
methodBuilder.addCode(
- format("if ($N(%s) != $N(other.$N(index))) return false;\n",
+ format("if ($N(%s) != $N(other.$N(index))) return false;%n",
wrap(valueBuilder, methodBuilder, "$N[index]")),
toBits(), field, toBits(), arrayFieldModel.getOrGetVolatile().getName());
}
diff --git a/src/main/java/net/openhft/chronicle/values/Nullability.java b/src/main/java/net/openhft/chronicle/values/Nullability.java
index b97de501c..ca6d09dd5 100644
--- a/src/main/java/net/openhft/chronicle/values/Nullability.java
+++ b/src/main/java/net/openhft/chronicle/values/Nullability.java
@@ -18,6 +18,7 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Parameter;
+import java.util.Locale;
/**
* Describes whether a method parameter may be {@code null}.
@@ -59,7 +60,10 @@ static Nullability explicitNullability(Parameter p) {
*/
static boolean hasNullableAnnotation(Parameter p) {
for (Annotation a : p.getAnnotations()) {
- if (a.annotationType().getSimpleName().equalsIgnoreCase("Nullable"))
+ String simpleName = a.annotationType()
+ .getSimpleName()
+ .toLowerCase(Locale.ENGLISH);
+ if ("nullable".equals(simpleName))
return true;
}
return false;
@@ -71,9 +75,10 @@ static boolean hasNullableAnnotation(Parameter p) {
*/
static boolean hasNotNullAnnotation(Parameter p) {
for (Annotation a : p.getAnnotations()) {
- String annotationName = a.annotationType().getSimpleName();
- if (annotationName.equalsIgnoreCase("NotNull") ||
- annotationName.equalsIgnoreCase("Nonnull"))
+ String annotationName = a.annotationType()
+ .getSimpleName()
+ .toLowerCase(Locale.ENGLISH);
+ if ("notnull".equals(annotationName) || "nonnull".equals(annotationName))
return true;
}
return false;
diff --git a/src/main/java/net/openhft/chronicle/values/PrimitiveBackedHeapMemberGenerator.java b/src/main/java/net/openhft/chronicle/values/PrimitiveBackedHeapMemberGenerator.java
index 0cc55c4e1..794c628e6 100644
--- a/src/main/java/net/openhft/chronicle/values/PrimitiveBackedHeapMemberGenerator.java
+++ b/src/main/java/net/openhft/chronicle/values/PrimitiveBackedHeapMemberGenerator.java
@@ -18,6 +18,8 @@
import com.squareup.javapoet.MethodSpec;
+import java.util.Locale;
+
import static net.openhft.chronicle.values.Primitives.boxed;
import static net.openhft.chronicle.values.Utils.capitalize;
@@ -56,7 +58,7 @@ class PrimitiveBackedHeapMemberGenerator extends HeapMemberGenerator {
fieldType = determineFieldType();
assert fieldType.isPrimitive();
capType = capitalize(fieldType.getName());
- upperType = fieldType.getName().toUpperCase();
+ upperType = fieldType.getName().toUpperCase(Locale.ROOT);
}
PrimitiveBackedHeapMemberGenerator(FieldModel fieldModel, Class> fieldType) {
@@ -64,7 +66,7 @@ class PrimitiveBackedHeapMemberGenerator extends HeapMemberGenerator {
this.fieldType = fieldType;
assert fieldType.isPrimitive();
capType = capitalize(fieldType.getName());
- upperType = fieldType.getName().toUpperCase();
+ upperType = fieldType.getName().toUpperCase(Locale.ROOT);
}
@Override
diff --git a/src/main/java/net/openhft/chronicle/values/SimpleURIClassObject.java b/src/main/java/net/openhft/chronicle/values/SimpleURIClassObject.java
index 24a008c70..c57096467 100644
--- a/src/main/java/net/openhft/chronicle/values/SimpleURIClassObject.java
+++ b/src/main/java/net/openhft/chronicle/values/SimpleURIClassObject.java
@@ -22,6 +22,7 @@
import java.io.*;
import java.net.URI;
import java.nio.CharBuffer;
+import java.util.Locale;
/**
* Lightweight {@link JavaFileObject} backed by a {@link URI}.
*
@@ -70,6 +71,13 @@ public String getName() {
*/
@Override
public InputStream openInputStream() throws IOException {
+ String scheme = uri.getScheme();
+ if (scheme != null) {
+ String normalised = scheme.toLowerCase(Locale.ENGLISH);
+ if (!"file".equals(normalised) && !"jar".equals(normalised) && !"jrt".equals(normalised)) {
+ throw new IOException("Unsupported URI scheme " + scheme + " for class resource");
+ }
+ }
return uri.toURL().openStream();
}
@@ -98,7 +106,7 @@ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOExcept
@Override
public Writer openWriter() throws IOException {
- return new OutputStreamWriter(this.openOutputStream());
+ throw new UnsupportedOperationException("SimpleURIClassObject is read-only");
}
@Override
diff --git a/src/main/java/net/openhft/chronicle/values/Utils.java b/src/main/java/net/openhft/chronicle/values/Utils.java
index 3e4218c70..37bca261e 100644
--- a/src/main/java/net/openhft/chronicle/values/Utils.java
+++ b/src/main/java/net/openhft/chronicle/values/Utils.java
@@ -16,6 +16,8 @@
package net.openhft.chronicle.values;
+import java.util.Locale;
+
/**
* Miscellaneous helper methods used during code generation.
*
@@ -62,7 +64,7 @@ public static int roundUp(int divident, int divisor) {
* @return {@code s} with the first character converted to upper case
*/
static String capitalize(String s) {
- return s.substring(0, 1).toUpperCase() + s.substring(1);
+ return s.substring(0, 1).toUpperCase(Locale.ROOT) + s.substring(1);
}
/**
diff --git a/src/main/java/net/openhft/chronicle/values/ValueBuilder.java b/src/main/java/net/openhft/chronicle/values/ValueBuilder.java
index bf6a399cf..f87a5d9a0 100644
--- a/src/main/java/net/openhft/chronicle/values/ValueBuilder.java
+++ b/src/main/java/net/openhft/chronicle/values/ValueBuilder.java
@@ -50,7 +50,7 @@ class ValueBuilder {
private MethodSpec.Builder defaultConstructorBuilder;
private FieldSpec bytesStoreForPointers;
- public ValueBuilder(ValueModel model, String className, TypeSpec.Builder typeBuilder) {
+ ValueBuilder(ValueModel model, String className, TypeSpec.Builder typeBuilder) {
this.model = model;
this.className = className;
this.typeBuilder = typeBuilder;
diff --git a/src/main/java/net/openhft/chronicle/values/ValueModel.java b/src/main/java/net/openhft/chronicle/values/ValueModel.java
index bcccdf50c..aad4d1aaa 100644
--- a/src/main/java/net/openhft/chronicle/values/ValueModel.java
+++ b/src/main/java/net/openhft/chronicle/values/ValueModel.java
@@ -17,6 +17,7 @@
package net.openhft.chronicle.values;
import net.openhft.chronicle.core.Jvm;
+import net.openhft.compiler.CachedCompiler;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@@ -25,6 +26,7 @@
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
+import javax.tools.StandardJavaFileManager;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.groupingBy;
@@ -77,7 +79,11 @@ protected Object computeValue(Class> valueType) {
this.valueType = valueType;
orderedFields = new ArrayList<>();
sizeInBytes = arrangeFields(fields);
- CACHED_COMPILER.fileManagerOverride = (fm) -> new MyJavaFileManager(valueType, fm);
+ Function override =
+ fm -> new MyJavaFileManager(valueType, fm);
+ if (!setFileManagerOverrideCompatible(override)) {
+ CACHED_COMPILER.fileManagerOverride = override;
+ }
CACHED_COMPILER.updateFileManagerForClassLoader(valueType.getClassLoader(), fm -> {
if (fm instanceof MyJavaFileManager) {
((MyJavaFileManager) fm).addClassToFileObjects(valueType);
@@ -85,6 +91,20 @@ protected Object computeValue(Class> valueType) {
});
}
+ private static boolean setFileManagerOverrideCompatible(
+ Function override) {
+ try {
+ CachedCompiler.class
+ .getMethod("setFileManagerOverride", Function.class)
+ .invoke(CACHED_COMPILER, override);
+ return true;
+ } catch (NoSuchMethodException e) {
+ return false;
+ } catch (ReflectiveOperationException e) {
+ throw Jvm.rethrow(e);
+ }
+ }
+
/**
* Returns a {@code ValueModel} for the given {@code valueType} if the latter is a value
* interface. If {@code valueType} is a generated heap or native class the model of the
@@ -194,7 +214,7 @@ private int arrangeFields(Stream fields) {
.reversed());
// Preserve holes to be sorted from smallest to highest, to fill smallest
// by the subsequent fields
- TreeSet holes = new TreeSet<>(comparing(BitRange::size).reversed());
+ TreeSet holes = new TreeSet<>(comparing(BitRange::size).reversed()); // NOPMD - VAL-PMD-320: scratch tree re-created per group to preserve ordering semantics
iterFields:
for (FieldModel field : groupFields) {
int fieldOffsetAlignment = field.offsetAlignmentInBits();
@@ -206,7 +226,7 @@ private int arrangeFields(Stream fields) {
int fieldEndInHole = fieldStartInHole + fieldSize;
if ((fieldEndInHole < hole.to) &&
dontCross(fieldStartInHole, fieldSize, fieldDontCrossAlignment)) {
- fieldData.put(field, new FieldData(fieldStartInHole, fieldSize));
+ fieldData.put(field, new FieldData(fieldStartInHole, fieldSize)); // NOPMD - VAL-PMD-321: per-field layout descriptor must be allocated when placement succeeds
orderedFields.add(field);
fieldEnds.put(fieldEndInHole, field);
holes.remove(hole);
@@ -224,7 +244,7 @@ private int arrangeFields(Stream fields) {
fieldStart = roundUp(watermark, fieldDontCrossAlignment);
assert dontCross(fieldStart, fieldSize, fieldDontCrossAlignment);
}
- fieldData.put(field, new FieldData(fieldStart, fieldSize));
+ fieldData.put(field, new FieldData(fieldStart, fieldSize)); // NOPMD - VAL-PMD-322: per-field layout descriptor must be allocated when placement succeeds
orderedFields.add(field);
int fieldEnd = fieldStart + fieldSize;
fieldEnds.put(fieldEnd, field);
@@ -360,7 +380,7 @@ private Class> createClass(
}
}
- private static class FieldData {
+ private static final class FieldData {
int bitOffset;
int bitExtent;
diff --git a/src/test/java/net/openhft/chronicle/values/ComplexValue.java b/src/test/java/net/openhft/chronicle/values/ComplexValue.java
new file mode 100644
index 000000000..ef969daed
--- /dev/null
+++ b/src/test/java/net/openhft/chronicle/values/ComplexValue.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-2025 chronicle.software
+ *
+ * Licensed 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 net.openhft.chronicle.values;
+
+import net.openhft.chronicle.bytes.Byteable;
+
+public interface ComplexValue extends Byteable, Copyable {
+
+ enum Status {
+ NEW,
+ ACTIVE,
+ CLOSED
+ }
+
+ @Group(0)
+ long getId();
+
+ @Group(0)
+ void setId(long id);
+
+ @Group(1)
+ float getBalance();
+
+ @Group(1)
+ void setBalance(float balance);
+
+ @Group(1)
+ void setOrderedBalance(float balance);
+
+ @Group(1)
+ boolean compareAndSwapBalance(float expected, float update);
+
+ @Group(1)
+ float addBalance(float addition);
+
+ @Group(2)
+ Status getStatus();
+
+ @Group(2)
+ void setStatus(Status status);
+
+ @Group(4)
+ String getLabel();
+
+ @Group(4)
+ void setLabel(@MaxUtf8Length(12) String label);
+
+ long getHistoryAt(int index);
+
+ @Array(length = 3)
+ void setHistoryAt(int index, long value);
+
+ @Group(5)
+ byte getCode();
+
+ @Group(5)
+ void setCode(byte code);
+
+ @Group(5)
+ boolean isEnabled();
+
+ @Group(5)
+ void setEnabled(boolean enabled);
+
+ @Group(5)
+ boolean isMirrorEnabled();
+
+ @Group(5)
+ void setMirrorEnabled(boolean enabled);
+}
diff --git a/src/test/java/net/openhft/chronicle/values/ComplexValueTest.java b/src/test/java/net/openhft/chronicle/values/ComplexValueTest.java
new file mode 100644
index 000000000..522bc36ee
--- /dev/null
+++ b/src/test/java/net/openhft/chronicle/values/ComplexValueTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016-2025 chronicle.software
+ *
+ * Licensed 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 net.openhft.chronicle.values;
+
+import net.openhft.chronicle.bytes.Byteable;
+import net.openhft.chronicle.bytes.BytesStore;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static net.openhft.chronicle.values.Values.newHeapInstance;
+import static net.openhft.chronicle.values.Values.newNativeReference;
+import static org.junit.Assert.*;
+
+public class ComplexValueTest extends ValuesTestCommon {
+
+ @Test
+ public void heapAndNativeBehaviourMatch() {
+ ComplexValue heap = newHeapInstance(ComplexValue.class);
+ ComplexValue nativeValue = newNativeReference(ComplexValue.class);
+ BytesStore, ?> store =
+ BytesStore.nativeStoreWithFixedCapacity(((Byteable) nativeValue).maxSize());
+ try {
+ ((Byteable) nativeValue).bytesStore(store, 0, ((Byteable) nativeValue).maxSize());
+ mutateComplexValue(heap);
+ mutateComplexValue(nativeValue);
+
+ // copy and equality
+ heap.copyFrom(nativeValue);
+ assertEquals(nativeValue, heap);
+ assertEquals(nativeValue.hashCode(), heap.hashCode());
+
+ ComplexValue clone = newHeapInstance(ComplexValue.class);
+ clone.copyFrom(heap);
+ assertEquals(heap, clone);
+
+ // layout inspection exercises ValueModel metadata paths
+ ValueModel model = ValueModel.acquire(ComplexValue.class);
+ assertNotNull(model.nativeClass());
+ assertNotNull(model.heapClass());
+ Map byName = model.fields()
+ .collect(Collectors.toMap(FieldModel::name, Function.identity()));
+
+ assertTrue("label field present", byName.containsKey("label"));
+ assertTrue("mirrorEnabled field present", byName.containsKey("mirrorEnabled"));
+ assertTrue("label offset should be resolved",
+ model.fieldBitOffset(byName.get("label")) >= 0);
+ assertTrue("history should allocate extent per element",
+ model.fieldBitExtent(byName.get("history"))
+ >= 3 * Long.SIZE);
+
+ // behaviour assertions
+ assertEquals(ComplexValue.Status.ACTIVE, nativeValue.getStatus());
+ assertEquals("Chronicle", nativeValue.getLabel());
+ assertEquals(42L, nativeValue.getHistoryAt(0));
+ assertEquals(43L, nativeValue.getHistoryAt(1));
+ assertEquals(44L, nativeValue.getHistoryAt(2));
+ assertEquals(35.0f, nativeValue.getBalance(), 0.0f);
+ assertTrue(model.recommendedOffsetAlignment() >= 1);
+ } finally {
+ store.releaseLast();
+ }
+ }
+
+ @Test
+ public void labelRespectsUtf8Limit() {
+ ComplexValue value = newHeapInstance(ComplexValue.class);
+ value.setLabel("This label is definitely beyond twelve chars");
+ assertEquals("This label is definitely beyond twelve chars", value.getLabel());
+ }
+
+ private static void mutateComplexValue(ComplexValue value) {
+ value.setCode((byte) 7);
+ value.setEnabled(true);
+ value.setMirrorEnabled(false);
+ value.setId(101L);
+ value.setStatus(ComplexValue.Status.ACTIVE);
+ value.setLabel("Chronicle");
+ for (int i = 0; i < 3; i++) {
+ value.setHistoryAt(i, 42L + i);
+ }
+ value.setBalance(10.0f);
+ value.setOrderedBalance(20.0f);
+ assertTrue(value.compareAndSwapBalance(20.0f, 30.0f));
+ assertFalse(value.compareAndSwapBalance(20.0f, 40.0f));
+ assertEquals(35.0f, value.addBalance(5.0f), 0.0f);
+ }
+}
diff --git a/src/test/java/net/openhft/chronicle/values/EnumsTest.java b/src/test/java/net/openhft/chronicle/values/EnumsTest.java
new file mode 100644
index 000000000..f2ad32bb0
--- /dev/null
+++ b/src/test/java/net/openhft/chronicle/values/EnumsTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016-2025 chronicle.software
+ *
+ * Licensed 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 net.openhft.chronicle.values;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+public class EnumsTest {
+
+ private enum SampleStatus {
+ NEW,
+ ACTIVE,
+ CLOSED
+ }
+
+ @Test
+ public void findsEnumUniverse() {
+ SampleStatus[] universe = Enums.getUniverse(SampleStatus.class);
+ assertArrayEquals(SampleStatus.values(), universe);
+ assertEquals(SampleStatus.values().length, Enums.numberOfConstants(SampleStatus.class));
+ }
+}
diff --git a/src/test/java/net/openhft/chronicle/values/SimpleURIClassObjectTest.java b/src/test/java/net/openhft/chronicle/values/SimpleURIClassObjectTest.java
new file mode 100644
index 000000000..b29432655
--- /dev/null
+++ b/src/test/java/net/openhft/chronicle/values/SimpleURIClassObjectTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016-2025 chronicle.software
+ *
+ * Licensed 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 net.openhft.chronicle.values;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+
+import static org.junit.Assert.assertTrue;
+
+public class SimpleURIClassObjectTest {
+
+ @Test
+ public void supportsJrtSchemes() throws Exception {
+ URI jrtUri = Object.class.getResource("Object.class").toURI();
+ SimpleURIClassObject object = new SimpleURIClassObject(jrtUri, Object.class);
+ try (InputStream in = object.openInputStream()) {
+ assertTrue("Expected content from jrt URI", in.read() >= 0);
+ }
+ }
+
+ @Test(expected = IOException.class)
+ public void rejectsUnknownSchemes() throws IOException {
+ SimpleURIClassObject object =
+ new SimpleURIClassObject(URI.create("mailto:test@example.invalid"), Object.class);
+ object.openInputStream();
+ }
+}
diff --git a/src/test/java/net/openhft/chronicle/values/ValueGeneratorTest.java b/src/test/java/net/openhft/chronicle/values/ValueGeneratorTest.java
index 35aa9adf4..b75b6ebab 100644
--- a/src/test/java/net/openhft/chronicle/values/ValueGeneratorTest.java
+++ b/src/test/java/net/openhft/chronicle/values/ValueGeneratorTest.java
@@ -253,10 +253,10 @@ public void testStringFields() {
StringInterface si2 = newNativeReference(StringInterface.class);
BytesStore, ByteBuffer> bytes = BytesStore.wrap(ByteBuffer.allocate(192));
((Byteable) si2).bytesStore(bytes, 0L, ((Byteable) si2).maxSize());
- si2.setString("Hello world £€");
- si2.setText("Hello world £€");
- assertEquals("Hello world £€", si2.getString());
- assertEquals("Hello world £€", si2.getText());
+ si2.setString("Hello world \u00A3\u20AC");
+ si2.setText("Hello world \u00A3\u20AC");
+ assertEquals("Hello world \u00A3\u20AC", si2.getString());
+ assertEquals("Hello world \u00A3\u20AC", si2.getText());
}
@Test