diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java index d813ab04a..52ee6e35a 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/Annotations.java @@ -261,6 +261,14 @@ public Set getAnnotationNames() { return annotationsMap.keySet(); } + public Annotations removeAnnotations(DotName... annotations) { + Map newAnnotationsMap = new HashMap<>(annotationsMap); + for (DotName annotation : annotations) { + newAnnotationsMap.remove(annotation); + } + return new Annotations(newAnnotationsMap, this.parentAnnotations); + } + /** * Get a specific annotation * diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ReferenceCreator.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ReferenceCreator.java index 1cee789dd..263e9cbda 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ReferenceCreator.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/creator/ReferenceCreator.java @@ -247,23 +247,19 @@ public Reference createReference(Direction direction, ClassInfo classInfo, boole Reference reference = new Reference(className, name, referenceType, parametrizedTypeArgumentsReferences, addParametrizedTypeNameExtension); - // Adapt to Scalar - boolean shouldCreateAdapedToType = AdaptToHelper.shouldCreateTypeInSchema(annotationsForClass); + // Adaptation Optional adaptTo = AdaptToHelper.getAdaptTo(reference, annotationsForClass); reference.setAdaptTo(adaptTo.orElse(null)); - // Now add it to the correct map - if (shouldCreateAdapedToType && createAdapedToType) { - putIfAbsent(name, reference, referenceType); - } - - // Adapt with adapter - boolean shouldCreateAdapedWithType = AdaptWithHelper.shouldCreateTypeInSchema(annotationsForClass); Optional adaptWith = AdaptWithHelper.getAdaptWith(direction, this, reference, annotationsForClass); reference.setAdaptWith(adaptWith.orElse(null)); // Now add it to the correct map - if (shouldCreateAdapedWithType && createAdapedWithType) { + boolean shouldCreateAdapedToType = AdaptToHelper.shouldCreateTypeInSchema(annotationsForClass); + boolean shouldCreateAdapedWithType = AdaptWithHelper.shouldCreateTypeInSchema(annotationsForClass); + + // We ignore the field that is being adapted + if (shouldCreateAdapedToType && createAdapedToType && shouldCreateAdapedWithType && createAdapedWithType) { putIfAbsent(name, reference, referenceType); } return reference; @@ -298,7 +294,7 @@ private Reference getReference(Direction direction, Annotations annotations, Reference parentObjectReference) { - // In some case, like operations and interfaces, there is not fieldType + // In some case, like operations and interfaces, there is no fieldType if (fieldType == null) { fieldType = methodType; } @@ -315,7 +311,7 @@ private Reference getReference(Direction direction, } return Scalars.getScalar(fieldTypeName); } else if (fieldType.kind().equals(Type.Kind.ARRAY)) { - // java Array + // Java Array Type typeInArray = fieldType.asArrayType().component(); Type typeInMethodArray = methodType.asArrayType().component(); return getReference(direction, typeInArray, typeInMethodArray, annotations, parentObjectReference); diff --git a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java index 1bbe828e5..1e09014b5 100644 --- a/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java +++ b/common/schema-builder/src/main/java/io/smallrye/graphql/schema/helper/AdaptWithHelper.java @@ -82,7 +82,12 @@ public static Optional getAdaptWith(Direction direction, ReferenceCre if (Scalars.isScalar(to.name().toString())) { adaptWith.setToReference(Scalars.getScalar(to.name().toString())); } else { - Reference toRef = referenceCreator.createReferenceForAdapter(direction, to, annotations); + Annotations annotationsAplicableToMe = annotations.removeAnnotations(Annotations.ADAPT_WITH, + Annotations.JSONB_TYPE_ADAPTER); + + // Remove the adaption annotation, as this is the type being adapted to + Reference toRef = referenceCreator.createReferenceForAdapter(direction, to, + annotationsAplicableToMe); toRef.setWrapper(WrapperCreator.createWrapper(to).orElse(null)); adaptWith.setToReference(toRef); diff --git a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java index 86792a5e2..eabe355a2 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/bootstrap/Bootstrap.java @@ -129,7 +129,6 @@ private Bootstrap(Schema schema, boolean skipInjectionValidation) { */ private void verifyInjectionIsAvailable() { LookupService lookupService = LookupService.get(); - ClassloadingService classloadingService = ClassloadingService.get(); // This crazy stream operation basically collects all class names where we need to verify that // it belongs to an injectable bean Stream.of( diff --git a/server/runner/pom.xml b/server/runner/pom.xml index 1da6ed44b..ddd9a067e 100644 --- a/server/runner/pom.xml +++ b/server/runner/pom.xml @@ -129,7 +129,7 @@ org.wildfly.plugins wildfly-maven-plugin - 3.0.0.Final + 2.1.0.Final ${version.wildfly} standalone-microprofile.xml diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/to/api/AdaptToResource.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/to/api/AdaptToResource.java index 9d53db705..02b376394 100644 --- a/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/to/api/AdaptToResource.java +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/to/api/AdaptToResource.java @@ -1,6 +1,7 @@ package io.smallrye.graphql.test.apps.adapt.to.api; import java.util.ArrayList; +import java.util.Date; import java.util.List; import org.eclipse.microprofile.graphql.GraphQLApi; @@ -47,6 +48,19 @@ public AdaptToData updateAdaptToData(AdaptToData adaptToData) { return adaptToData; } + @Mutation + public Dummy addDummy(Dummy dummy) { + return dummy; + } + + @Query + public Dummy getDummy() { + Dummy d = new Dummy(); + d.id = new DummyId(new Date()); + d.name = "foo"; + return d; + } + public static class AdaptToData { public Long id; diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/to/api/Dummy.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/to/api/Dummy.java new file mode 100644 index 000000000..f4c3fc12b --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/to/api/Dummy.java @@ -0,0 +1,12 @@ +package io.smallrye.graphql.test.apps.adapt.to.api; + +import io.smallrye.graphql.api.AdaptToScalar; +import io.smallrye.graphql.api.Scalar; + +public class Dummy { + + public String name; + @AdaptToScalar(Scalar.String.class) + public DummyId id; + +} diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/to/api/DummyId.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/to/api/DummyId.java new file mode 100644 index 000000000..9d1067e36 --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/to/api/DummyId.java @@ -0,0 +1,433 @@ +package io.smallrye.graphql.test.apps.adapt.to.api; + +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; + +public final class DummyId implements Comparable, Serializable { + + // unused, as this class uses a proxy for serialization + private static final long serialVersionUID = 1L; + + private static final int OBJECT_ID_LENGTH = 12; + private static final int LOW_ORDER_THREE_BYTES = 0x00ffffff; + + // Use primitives to represent the 5-byte random value. + private static final int RANDOM_VALUE1; + private static final short RANDOM_VALUE2; + + private static final AtomicInteger NEXT_COUNTER = new AtomicInteger(new SecureRandom().nextInt()); + + private static final char[] HEX_CHARS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + /** + * The timestamp + */ + private final int timestamp; + /** + * The counter. + */ + private final int counter; + /** + * the first four bits of randomness. + */ + private final int randomValue1; + /** + * The last two bits of randomness. + */ + private final short randomValue2; + + /** + * Gets a new object id. + * + * @return the new id + */ + public static DummyId get() { + return new DummyId(); + } + + /** + * Gets a new object id with the given date value and all other bits zeroed. + *

+ * The returned object id will compare as less than or equal to any other object id within the same second as the given + * date, and + * less than any later date. + *

+ * + * @param date the date + * @return the ObjectId + * @since 4.1 + */ + public static DummyId getSmallestWithDate(final Date date) { + return new DummyId(dateToTimestampSeconds(date), 0, (short) 0, 0, false); + } + + /** + * Checks if a string could be an {@code ObjectId}. + * + * @param hexString a potential ObjectId as a String. + * @return whether the string could be an object id + * @throws IllegalArgumentException if hexString is null + */ + public static boolean isValid(final String hexString) { + if (hexString == null) { + throw new IllegalArgumentException(); + } + + int len = hexString.length(); + if (len != 24) { + return false; + } + + for (int i = 0; i < len; i++) { + char c = hexString.charAt(i); + if (c >= '0' && c <= '9') { + continue; + } + if (c >= 'a' && c <= 'f') { + continue; + } + if (c >= 'A' && c <= 'F') { + continue; + } + + return false; + } + + return true; + } + + /** + * Create a new object id. + */ + public DummyId() { + this(new Date()); + } + + /** + * Constructs a new instance using the given date. + * + * @param date the date + */ + public DummyId(final Date date) { + this(dateToTimestampSeconds(date), NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES, false); + } + + /** + * Constructs a new instances using the given date and counter. + * + * @param date the date + * @param counter the counter + * @throws IllegalArgumentException if the high order byte of counter is not zero + */ + public DummyId(final Date date, final int counter) { + this(dateToTimestampSeconds(date), counter, true); + } + + /** + * Creates an ObjectId using the given time and counter. + * + * @param timestamp the time in seconds + * @param counter the counter + * @throws IllegalArgumentException if the high order byte of counter is not zero + */ + public DummyId(final int timestamp, final int counter) { + this(timestamp, counter, true); + } + + private DummyId(final int timestamp, final int counter, final boolean checkCounter) { + this(timestamp, RANDOM_VALUE1, RANDOM_VALUE2, counter, checkCounter); + } + + private DummyId(final int timestamp, final int randomValue1, final short randomValue2, final int counter, + final boolean checkCounter) { + if ((randomValue1 & 0xff000000) != 0) { + throw new IllegalArgumentException("The random value must be between 0 and 16777215 (it must fit in three bytes)."); + } + if (checkCounter && ((counter & 0xff000000) != 0)) { + throw new IllegalArgumentException("The counter must be between 0 and 16777215 (it must fit in three bytes)."); + } + this.timestamp = timestamp; + this.counter = counter & LOW_ORDER_THREE_BYTES; + this.randomValue1 = randomValue1; + this.randomValue2 = randomValue2; + } + + /** + * Constructs a new instance from a 24-byte hexadecimal string representation. + * + * @param hexString the string to convert + * @throws IllegalArgumentException if the string is not a valid hex string representation of an ObjectId + */ + public DummyId(final String hexString) { + this(parseHexString(hexString)); + } + + /** + * Constructs a new instance from the given byte array + * + * @param bytes the byte array + * @throws IllegalArgumentException if array is null or not of length 12 + */ + public DummyId(final byte[] bytes) { + this(ByteBuffer.wrap(bytes)); + } + + /** + * Constructs a new instance from the given ByteBuffer + * + * @param buffer the ByteBuffer + * @throws IllegalArgumentException if the buffer is null or does not have at least 12 bytes remaining + * @since 3.4 + */ + public DummyId(final ByteBuffer buffer) { + + // Note: Cannot use ByteBuffer.getInt because it depends on tbe buffer's byte order + // and ObjectId's are always in big-endian order. + timestamp = makeInt(buffer.get(), buffer.get(), buffer.get(), buffer.get()); + randomValue1 = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get()); + randomValue2 = makeShort(buffer.get(), buffer.get()); + counter = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get()); + } + + /** + * Convert to a byte array. Note that the numbers are stored in big-endian order. + * + * @return the byte array + */ + public byte[] toByteArray() { + ByteBuffer buffer = ByteBuffer.allocate(OBJECT_ID_LENGTH); + putToByteBuffer(buffer); + return buffer.array(); // using .allocate ensures there is a backing array that can be returned + } + + /** + * Convert to bytes and put those bytes to the provided ByteBuffer. + * Note that the numbers are stored in big-endian order. + * + * @param buffer the ByteBuffer + * @throws IllegalArgumentException if the buffer is null or does not have at least 12 bytes remaining + * @since 3.4 + */ + public void putToByteBuffer(final ByteBuffer buffer) { + buffer.put(int3(timestamp)); + buffer.put(int2(timestamp)); + buffer.put(int1(timestamp)); + buffer.put(int0(timestamp)); + buffer.put(int2(randomValue1)); + buffer.put(int1(randomValue1)); + buffer.put(int0(randomValue1)); + buffer.put(short1(randomValue2)); + buffer.put(short0(randomValue2)); + buffer.put(int2(counter)); + buffer.put(int1(counter)); + buffer.put(int0(counter)); + } + + /** + * Gets the timestamp (number of seconds since the Unix epoch). + * + * @return the timestamp + */ + public int getTimestamp() { + return timestamp; + } + + /** + * Gets the timestamp as a {@code Date} instance. + * + * @return the Date + */ + public Date getDate() { + return new Date((timestamp & 0xFFFFFFFFL) * 1000L); + } + + /** + * Converts this instance into a 24-byte hexadecimal string representation. + * + * @return a string representation of the ObjectId in hexadecimal format + */ + public String toHexString() { + char[] chars = new char[OBJECT_ID_LENGTH * 2]; + int i = 0; + for (byte b : toByteArray()) { + chars[i++] = HEX_CHARS[b >> 4 & 0xF]; + chars[i++] = HEX_CHARS[b & 0xF]; + } + return new String(chars); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DummyId objectId = (DummyId) o; + + if (counter != objectId.counter) { + return false; + } + if (timestamp != objectId.timestamp) { + return false; + } + + if (randomValue1 != objectId.randomValue1) { + return false; + } + + if (randomValue2 != objectId.randomValue2) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = timestamp; + result = 31 * result + counter; + result = 31 * result + randomValue1; + result = 31 * result + randomValue2; + return result; + } + + @Override + public int compareTo(final DummyId other) { + if (other == null) { + throw new NullPointerException(); + } + + byte[] byteArray = toByteArray(); + byte[] otherByteArray = other.toByteArray(); + for (int i = 0; i < OBJECT_ID_LENGTH; i++) { + if (byteArray[i] != otherByteArray[i]) { + return ((byteArray[i] & 0xff) < (otherByteArray[i] & 0xff)) ? -1 : 1; + } + } + return 0; + } + + @Override + public String toString() { + return toHexString(); + } + + /** + * Write the replacement object. + * + *

+ * See https://docs.oracle.com/javase/6/docs/platform/serialization/spec/output.html + *

+ * + * @return a proxy for the document + */ + private Object writeReplace() { + return new SerializationProxy(this); + } + + /** + * Prevent normal deserialization. + * + *

+ * See https://docs.oracle.com/javase/6/docs/platform/serialization/spec/input.html + *

+ * + * @param stream the stream + * @throws InvalidObjectException in all cases + */ + private void readObject(final ObjectInputStream stream) throws InvalidObjectException { + throw new InvalidObjectException("Proxy required"); + } + + private static class SerializationProxy implements Serializable { + private static final long serialVersionUID = 1L; + + private final byte[] bytes; + + SerializationProxy(final DummyId objectId) { + bytes = objectId.toByteArray(); + } + + private Object readResolve() { + return new DummyId(bytes); + } + } + + static { + try { + SecureRandom secureRandom = new SecureRandom(); + RANDOM_VALUE1 = secureRandom.nextInt(0x01000000); + RANDOM_VALUE2 = (short) secureRandom.nextInt(0x00008000); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static byte[] parseHexString(final String s) { + if (!isValid(s)) { + throw new IllegalArgumentException("invalid hexadecimal representation of an ObjectId: [" + s + "]"); + } + + byte[] b = new byte[OBJECT_ID_LENGTH]; + for (int i = 0; i < b.length; i++) { + b[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16); + } + return b; + } + + private static int dateToTimestampSeconds(final Date time) { + return (int) (time.getTime() / 1000); + } + + // Big-Endian helpers, in this class because all other BSON numbers are little-endian + + private static int makeInt(final byte b3, final byte b2, final byte b1, final byte b0) { + // CHECKSTYLE:OFF + return (((b3) << 24) | + ((b2 & 0xff) << 16) | + ((b1 & 0xff) << 8) | + ((b0 & 0xff))); + // CHECKSTYLE:ON + } + + private static short makeShort(final byte b1, final byte b0) { + // CHECKSTYLE:OFF + return (short) (((b1 & 0xff) << 8) | ((b0 & 0xff))); + // CHECKSTYLE:ON + } + + private static byte int3(final int x) { + return (byte) (x >> 24); + } + + private static byte int2(final int x) { + return (byte) (x >> 16); + } + + private static byte int1(final int x) { + return (byte) (x >> 8); + } + + private static byte int0(final int x) { + return (byte) (x); + } + + private static byte short1(final short x) { + return (byte) (x >> 8); + } + + private static byte short0(final short x) { + return (byte) (x); + } + +} \ No newline at end of file diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/AdapterResource.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/AdapterResource.java index bacc6e7b7..58ee400a3 100644 --- a/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/AdapterResource.java +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/AdapterResource.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -59,6 +60,19 @@ public Map mapMutationEnum(Map in) { return in; } + @Mutation + public Dommie addDommie(Dommie dommie) { + return dommie; + } + + @Query + public Dommie getDommie() { + Dommie d = new Dommie(); + d.id = new DommieId(new Date()); + d.name = "foo"; + return d; + } + public Map mapSourceBasic(@Source AdapterData adapterData) { Map m = createBasicMap(); return m; diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/Dommie.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/Dommie.java new file mode 100644 index 000000000..e2fdab6df --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/Dommie.java @@ -0,0 +1,11 @@ +package io.smallrye.graphql.test.apps.adapt.with.api; + +import io.smallrye.graphql.api.AdaptWith; + +public class Dommie { + + public String name; + @AdaptWith(DommieIdAdapter.class) + public DommieId id; + +} diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/DommieId.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/DommieId.java new file mode 100644 index 000000000..5cf02accc --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/DommieId.java @@ -0,0 +1,433 @@ +package io.smallrye.graphql.test.apps.adapt.with.api; + +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; + +public final class DommieId implements Comparable, Serializable { + + // unused, as this class uses a proxy for serialization + private static final long serialVersionUID = 1L; + + private static final int OBJECT_ID_LENGTH = 12; + private static final int LOW_ORDER_THREE_BYTES = 0x00ffffff; + + // Use primitives to represent the 5-byte random value. + private static final int RANDOM_VALUE1; + private static final short RANDOM_VALUE2; + + private static final AtomicInteger NEXT_COUNTER = new AtomicInteger(new SecureRandom().nextInt()); + + private static final char[] HEX_CHARS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + /** + * The timestamp + */ + private final int timestamp; + /** + * The counter. + */ + private final int counter; + /** + * the first four bits of randomness. + */ + private final int randomValue1; + /** + * The last two bits of randomness. + */ + private final short randomValue2; + + /** + * Gets a new object id. + * + * @return the new id + */ + public static DommieId get() { + return new DommieId(); + } + + /** + * Gets a new object id with the given date value and all other bits zeroed. + *

+ * The returned object id will compare as less than or equal to any other object id within the same second as the given + * date, and + * less than any later date. + *

+ * + * @param date the date + * @return the ObjectId + * @since 4.1 + */ + public static DommieId getSmallestWithDate(final Date date) { + return new DommieId(dateToTimestampSeconds(date), 0, (short) 0, 0, false); + } + + /** + * Checks if a string could be an {@code ObjectId}. + * + * @param hexString a potential ObjectId as a String. + * @return whether the string could be an object id + * @throws IllegalArgumentException if hexString is null + */ + public static boolean isValid(final String hexString) { + if (hexString == null) { + throw new IllegalArgumentException(); + } + + int len = hexString.length(); + if (len != 24) { + return false; + } + + for (int i = 0; i < len; i++) { + char c = hexString.charAt(i); + if (c >= '0' && c <= '9') { + continue; + } + if (c >= 'a' && c <= 'f') { + continue; + } + if (c >= 'A' && c <= 'F') { + continue; + } + + return false; + } + + return true; + } + + /** + * Create a new object id. + */ + public DommieId() { + this(new Date()); + } + + /** + * Constructs a new instance using the given date. + * + * @param date the date + */ + public DommieId(final Date date) { + this(dateToTimestampSeconds(date), NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES, false); + } + + /** + * Constructs a new instances using the given date and counter. + * + * @param date the date + * @param counter the counter + * @throws IllegalArgumentException if the high order byte of counter is not zero + */ + public DommieId(final Date date, final int counter) { + this(dateToTimestampSeconds(date), counter, true); + } + + /** + * Creates an ObjectId using the given time and counter. + * + * @param timestamp the time in seconds + * @param counter the counter + * @throws IllegalArgumentException if the high order byte of counter is not zero + */ + public DommieId(final int timestamp, final int counter) { + this(timestamp, counter, true); + } + + private DommieId(final int timestamp, final int counter, final boolean checkCounter) { + this(timestamp, RANDOM_VALUE1, RANDOM_VALUE2, counter, checkCounter); + } + + private DommieId(final int timestamp, final int randomValue1, final short randomValue2, final int counter, + final boolean checkCounter) { + if ((randomValue1 & 0xff000000) != 0) { + throw new IllegalArgumentException("The random value must be between 0 and 16777215 (it must fit in three bytes)."); + } + if (checkCounter && ((counter & 0xff000000) != 0)) { + throw new IllegalArgumentException("The counter must be between 0 and 16777215 (it must fit in three bytes)."); + } + this.timestamp = timestamp; + this.counter = counter & LOW_ORDER_THREE_BYTES; + this.randomValue1 = randomValue1; + this.randomValue2 = randomValue2; + } + + /** + * Constructs a new instance from a 24-byte hexadecimal string representation. + * + * @param hexString the string to convert + * @throws IllegalArgumentException if the string is not a valid hex string representation of an ObjectId + */ + public DommieId(final String hexString) { + this(parseHexString(hexString)); + } + + /** + * Constructs a new instance from the given byte array + * + * @param bytes the byte array + * @throws IllegalArgumentException if array is null or not of length 12 + */ + public DommieId(final byte[] bytes) { + this(ByteBuffer.wrap(bytes)); + } + + /** + * Constructs a new instance from the given ByteBuffer + * + * @param buffer the ByteBuffer + * @throws IllegalArgumentException if the buffer is null or does not have at least 12 bytes remaining + * @since 3.4 + */ + public DommieId(final ByteBuffer buffer) { + + // Note: Cannot use ByteBuffer.getInt because it depends on tbe buffer's byte order + // and ObjectId's are always in big-endian order. + timestamp = makeInt(buffer.get(), buffer.get(), buffer.get(), buffer.get()); + randomValue1 = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get()); + randomValue2 = makeShort(buffer.get(), buffer.get()); + counter = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get()); + } + + /** + * Convert to a byte array. Note that the numbers are stored in big-endian order. + * + * @return the byte array + */ + public byte[] toByteArray() { + ByteBuffer buffer = ByteBuffer.allocate(OBJECT_ID_LENGTH); + putToByteBuffer(buffer); + return buffer.array(); // using .allocate ensures there is a backing array that can be returned + } + + /** + * Convert to bytes and put those bytes to the provided ByteBuffer. + * Note that the numbers are stored in big-endian order. + * + * @param buffer the ByteBuffer + * @throws IllegalArgumentException if the buffer is null or does not have at least 12 bytes remaining + * @since 3.4 + */ + public void putToByteBuffer(final ByteBuffer buffer) { + buffer.put(int3(timestamp)); + buffer.put(int2(timestamp)); + buffer.put(int1(timestamp)); + buffer.put(int0(timestamp)); + buffer.put(int2(randomValue1)); + buffer.put(int1(randomValue1)); + buffer.put(int0(randomValue1)); + buffer.put(short1(randomValue2)); + buffer.put(short0(randomValue2)); + buffer.put(int2(counter)); + buffer.put(int1(counter)); + buffer.put(int0(counter)); + } + + /** + * Gets the timestamp (number of seconds since the Unix epoch). + * + * @return the timestamp + */ + public int getTimestamp() { + return timestamp; + } + + /** + * Gets the timestamp as a {@code Date} instance. + * + * @return the Date + */ + public Date getDate() { + return new Date((timestamp & 0xFFFFFFFFL) * 1000L); + } + + /** + * Converts this instance into a 24-byte hexadecimal string representation. + * + * @return a string representation of the ObjectId in hexadecimal format + */ + public String toHexString() { + char[] chars = new char[OBJECT_ID_LENGTH * 2]; + int i = 0; + for (byte b : toByteArray()) { + chars[i++] = HEX_CHARS[b >> 4 & 0xF]; + chars[i++] = HEX_CHARS[b & 0xF]; + } + return new String(chars); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DommieId objectId = (DommieId) o; + + if (counter != objectId.counter) { + return false; + } + if (timestamp != objectId.timestamp) { + return false; + } + + if (randomValue1 != objectId.randomValue1) { + return false; + } + + if (randomValue2 != objectId.randomValue2) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = timestamp; + result = 31 * result + counter; + result = 31 * result + randomValue1; + result = 31 * result + randomValue2; + return result; + } + + @Override + public int compareTo(final DommieId other) { + if (other == null) { + throw new NullPointerException(); + } + + byte[] byteArray = toByteArray(); + byte[] otherByteArray = other.toByteArray(); + for (int i = 0; i < OBJECT_ID_LENGTH; i++) { + if (byteArray[i] != otherByteArray[i]) { + return ((byteArray[i] & 0xff) < (otherByteArray[i] & 0xff)) ? -1 : 1; + } + } + return 0; + } + + @Override + public String toString() { + return toHexString(); + } + + /** + * Write the replacement object. + * + *

+ * See https://docs.oracle.com/javase/6/docs/platform/serialization/spec/output.html + *

+ * + * @return a proxy for the document + */ + private Object writeReplace() { + return new SerializationProxy(this); + } + + /** + * Prevent normal deserialization. + * + *

+ * See https://docs.oracle.com/javase/6/docs/platform/serialization/spec/input.html + *

+ * + * @param stream the stream + * @throws InvalidObjectException in all cases + */ + private void readObject(final ObjectInputStream stream) throws InvalidObjectException { + throw new InvalidObjectException("Proxy required"); + } + + private static class SerializationProxy implements Serializable { + private static final long serialVersionUID = 1L; + + private final byte[] bytes; + + SerializationProxy(final DommieId objectId) { + bytes = objectId.toByteArray(); + } + + private Object readResolve() { + return new DommieId(bytes); + } + } + + static { + try { + SecureRandom secureRandom = new SecureRandom(); + RANDOM_VALUE1 = secureRandom.nextInt(0x01000000); + RANDOM_VALUE2 = (short) secureRandom.nextInt(0x00008000); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static byte[] parseHexString(final String s) { + if (!isValid(s)) { + throw new IllegalArgumentException("invalid hexadecimal representation of an ObjectId: [" + s + "]"); + } + + byte[] b = new byte[OBJECT_ID_LENGTH]; + for (int i = 0; i < b.length; i++) { + b[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16); + } + return b; + } + + private static int dateToTimestampSeconds(final Date time) { + return (int) (time.getTime() / 1000); + } + + // Big-Endian helpers, in this class because all other BSON numbers are little-endian + + private static int makeInt(final byte b3, final byte b2, final byte b1, final byte b0) { + // CHECKSTYLE:OFF + return (((b3) << 24) | + ((b2 & 0xff) << 16) | + ((b1 & 0xff) << 8) | + ((b0 & 0xff))); + // CHECKSTYLE:ON + } + + private static short makeShort(final byte b1, final byte b0) { + // CHECKSTYLE:OFF + return (short) (((b1 & 0xff) << 8) | ((b0 & 0xff))); + // CHECKSTYLE:ON + } + + private static byte int3(final int x) { + return (byte) (x >> 24); + } + + private static byte int2(final int x) { + return (byte) (x >> 16); + } + + private static byte int1(final int x) { + return (byte) (x >> 8); + } + + private static byte int0(final int x) { + return (byte) (x); + } + + private static byte short1(final short x) { + return (byte) (x >> 8); + } + + private static byte short0(final short x) { + return (byte) (x); + } + +} diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/DommieIdAdapter.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/DommieIdAdapter.java new file mode 100644 index 000000000..931248d89 --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/DommieIdAdapter.java @@ -0,0 +1,24 @@ +package io.smallrye.graphql.test.apps.adapt.with.api; + +import io.smallrye.graphql.api.Adapter; + +/** + * Using an adapter + * + * @author Phillip Kruger (phillip.kruger@redhat.com) + */ +public class DommieIdAdapter implements Adapter { + + @Override + public Id to(DommieId o) throws Exception { + Id id = new Id(); + id.value = o.toHexString(); + return id; + } + + @Override + public DommieId from(Id a) throws Exception { + return new DommieId(a.value); + } + +} \ No newline at end of file diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/Id.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/Id.java new file mode 100644 index 000000000..970ee1f00 --- /dev/null +++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/adapt/with/api/Id.java @@ -0,0 +1,5 @@ +package io.smallrye.graphql.test.apps.adapt.with.api; + +public class Id { + public String value; +}