diff --git a/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java new file mode 100644 index 0000000000..acf5622d02 --- /dev/null +++ b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDType.java @@ -0,0 +1,151 @@ +/***************************************************************** + * 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 + * + * https://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.cayenne.access.types; + +import java.nio.ByteBuffer; +import java.sql.CallableStatement; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Types; +import java.util.List; +import java.util.UUID; + +import org.apache.cayenne.CayenneRuntimeException; + +/** + * ExtendedType for java.util.UUID that supports string and binary JDBC columns.
+ * For char columns, stores UUID as canonical string.
+ * For binary columns, stores UUID in 16-byte big-endian form. + * + * @since 4.2 + */ +public class UUIDType implements ExtendedType { + + public static final int UUID_BYTES = 2 * Long.BYTES; + + @Override + public String getClassName() { + return UUID.class.getName(); + } + + @Override + public UUID materializeObject(ResultSet rs, int index, int type) throws Exception { + switch (Format.forJdbcType(type)) { + case CHAR: { + String s = rs.getString(index); + return s != null ? UUID.fromString(s) : null; + } + case BINARY: { + byte[] b = rs.getBytes(index); + return b != null ? fromBytes(b) : null; + } + default: { + throw new CayenneRuntimeException("Unsupported JDBC type: " + JDBCType.valueOf(type)); + } + } + } + + @Override + public UUID materializeObject(CallableStatement cs, int index, int type) throws Exception { + switch (Format.forJdbcType(type)) { + case CHAR: { + String s = cs.getString(index); + return s != null ? UUID.fromString(s) : null; + } + case BINARY: { + byte[] b = cs.getBytes(index); + return b != null ? fromBytes(b) : null; + } + default: { + throw new CayenneRuntimeException("Unsupported JDBC type: " + JDBCType.valueOf(type)); + } + } + } + + @Override + public void setJdbcObject(PreparedStatement statement, UUID value, int pos, int type, int precision) throws Exception { + if (value == null) { + statement.setNull(pos, type); + return; + } + + switch (Format.forJdbcType(type)) { + case CHAR: { + statement.setString(pos, value.toString()); + return; + } + case BINARY: { + statement.setBytes(pos, toBytes(value)); + return; + } + default: { + throw new CayenneRuntimeException("Unsupported JDBC type: " + JDBCType.valueOf(type)); + } + } + } + + @Override + public String toString(UUID value) { + return value == null ? "NULL" : value.toString(); + } + + protected static UUID fromBytes(byte[] bytes) { + if (bytes == null) { + return null; + } + if (bytes.length != UUID_BYTES) { + throw new CayenneRuntimeException("Invalid UUID length (" + bytes.length + "), expected " + UUID_BYTES); + } + ByteBuffer bb = ByteBuffer.wrap(bytes); + return new UUID(bb.getLong(), bb.getLong()); + } + + protected static byte[] toBytes(UUID uuid) { + if (uuid == null) { + return null; + } + return ByteBuffer.allocate(Long.BYTES * 2) + .putLong(uuid.getMostSignificantBits()) + .putLong(uuid.getLeastSignificantBits()) + .array(); + } + + public enum Format { + CHAR(List.of(Types.CHAR, Types.VARCHAR, Types.LONGVARCHAR)), + BINARY(List.of(Types.BINARY, Types.VARBINARY, Types.LONGVARBINARY)), + OTHER(List.of()); + + private final List jdbcTypes; + + Format(List jdbcTypes) { + this.jdbcTypes = jdbcTypes; + } + + public static Format forJdbcType(int jdbcType) { + for (Format format : values()) { + if (format.jdbcTypes.contains(jdbcType)) { + return format; + } + } + return OTHER; + } + } +} diff --git a/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDValueType.java b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDValueType.java index 60a3662e60..c09eaea261 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDValueType.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/types/UUIDValueType.java @@ -25,6 +25,7 @@ /** * @since 4.0 + * @deprecated since 4.2, use {@link UUIDType} instead */ public class UUIDValueType implements ValueObjectType { diff --git a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java index b7ba5ee1bb..f56477cd80 100644 --- a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java +++ b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/CoreModule.java @@ -394,6 +394,7 @@ public void configure(Binder binder) { .addDefaultExtendedType(new UtilDateType()) .addDefaultExtendedType(new CalendarType<>(GregorianCalendar.class)) .addDefaultExtendedType(new CalendarType<>(Calendar.class)) + .addDefaultExtendedType(new UUIDType()) .addDefaultExtendedType(new GeoJsonType()) .addDefaultExtendedType(new WktType()) @@ -402,7 +403,6 @@ public void configure(Binder binder) { // ValueObjectTypes .addValueObjectType(BigIntegerValueType.class) .addValueObjectType(BigDecimalValueType.class) - .addValueObjectType(UUIDValueType.class) .addValueObjectType(LocalDateValueType.class) .addValueObjectType(LocalTimeValueType.class) .addValueObjectType(LocalDateTimeValueType.class) diff --git a/cayenne/src/test/java/org/apache/cayenne/access/UUIDIT.java b/cayenne/src/test/java/org/apache/cayenne/access/UUIDIT.java index 9871348752..70ddd75248 100644 --- a/cayenne/src/test/java/org/apache/cayenne/access/UUIDIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/access/UUIDIT.java @@ -18,6 +18,7 @@ ****************************************************************/ package org.apache.cayenne.access; +import java.nio.ByteBuffer; import java.util.UUID; import org.apache.cayenne.Cayenne; @@ -28,6 +29,7 @@ import org.apache.cayenne.query.ObjectSelect; import org.apache.cayenne.test.jdbc.DBHelper; import org.apache.cayenne.test.jdbc.TableHelper; +import org.apache.cayenne.testdo.uuid.UuidBinTestEntity; import org.apache.cayenne.testdo.uuid.UuidPkEntity; import org.apache.cayenne.testdo.uuid.UuidTestEntity; import org.apache.cayenne.unit.di.runtime.CayenneProjects; @@ -49,10 +51,12 @@ public class UUIDIT extends RuntimeCase { private DBHelper dbHelper; private TableHelper uuidPkEntity; + private TableHelper uuidBinTable; @Before public void setUp() throws Exception { uuidPkEntity = new TableHelper(dbHelper, "UUID_PK_ENTITY", "ID"); + uuidBinTable = new TableHelper(dbHelper, "UUID_BIN_TEST"); } @Test @@ -116,4 +120,32 @@ public void testUUIDColumnSelect() throws Exception { assertEquals(id, readValue2); } + + @Test + public void testUUIDBinary_InsertSelect() { + UuidBinTestEntity test = context.newObject(UuidBinTestEntity.class); + UUID expected = UUID.randomUUID(); + test.setUuid(expected); + context.commitChanges(); + + UuidBinTestEntity testRead = ObjectSelect.query(UuidBinTestEntity.class).selectFirst(context); + assertNotNull(testRead.getUuid()); + assertEquals(expected, testRead.getUuid()); + } + + @Test + public void testUUIDBinary_RawBytes() throws Exception { + UuidBinTestEntity test = context.newObject(UuidBinTestEntity.class); + UUID expected = UUID.randomUUID(); + test.setUuid(expected); + context.commitChanges(); + + byte[] raw = uuidBinTable.getBytes("UUID"); + assertNotNull(raw); + assertEquals(16, raw.length); + + ByteBuffer bb = ByteBuffer.wrap(raw); + UUID actual = new UUID(bb.getLong(), bb.getLong()); + assertEquals(expected, actual); + } } diff --git a/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/UuidBinTestEntity.java b/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/UuidBinTestEntity.java new file mode 100644 index 0000000000..9d210130b6 --- /dev/null +++ b/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/UuidBinTestEntity.java @@ -0,0 +1,9 @@ +package org.apache.cayenne.testdo.uuid; + +import org.apache.cayenne.testdo.uuid.auto._UuidBinTestEntity; + +public class UuidBinTestEntity extends _UuidBinTestEntity { + + private static final long serialVersionUID = 1L; + +} diff --git a/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/auto/_UuidBinTestEntity.java b/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/auto/_UuidBinTestEntity.java new file mode 100644 index 0000000000..341e773f41 --- /dev/null +++ b/cayenne/src/test/java/org/apache/cayenne/testdo/uuid/auto/_UuidBinTestEntity.java @@ -0,0 +1,94 @@ +package org.apache.cayenne.testdo.uuid.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.UUID; + +import org.apache.cayenne.PersistentObject; +import org.apache.cayenne.exp.property.BaseProperty; +import org.apache.cayenne.exp.property.NumericIdProperty; +import org.apache.cayenne.exp.property.PropertyFactory; +import org.apache.cayenne.exp.property.SelfProperty; +import org.apache.cayenne.testdo.uuid.UuidBinTestEntity; + +/** + * Class _UuidBinTestEntity was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _UuidBinTestEntity extends PersistentObject { + + private static final long serialVersionUID = 1L; + + public static final SelfProperty SELF = PropertyFactory.createSelf(UuidBinTestEntity.class); + + public static final NumericIdProperty ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "UuidBinTestEntity", Integer.class); + public static final String ID_PK_COLUMN = "ID"; + + public static final BaseProperty UUID = PropertyFactory.createBase("uuid", UUID.class); + + protected UUID uuid; + + + public void setUuid(UUID uuid) { + beforePropertyWrite("uuid", this.uuid, uuid); + this.uuid = uuid; + } + + public UUID getUuid() { + beforePropertyRead("uuid"); + return this.uuid; + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "uuid": + return this.uuid; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "uuid": + this.uuid = (UUID)val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.uuid); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.uuid = (UUID)in.readObject(); + } + +} diff --git a/cayenne/src/test/java/org/apache/cayenne/unit/di/runtime/RuntimeCaseModule.java b/cayenne/src/test/java/org/apache/cayenne/unit/di/runtime/RuntimeCaseModule.java index 6b2507e7b9..023156d465 100644 --- a/cayenne/src/test/java/org/apache/cayenne/unit/di/runtime/RuntimeCaseModule.java +++ b/cayenne/src/test/java/org/apache/cayenne/unit/di/runtime/RuntimeCaseModule.java @@ -193,12 +193,12 @@ public void configure(Binder binder) { .addDefaultExtendedType(new CalendarType<>(GregorianCalendar.class)) .addDefaultExtendedType(new CalendarType<>(Calendar.class)) .addDefaultExtendedType(new DurationType()) + .addDefaultExtendedType(new UUIDType()) .addExtendedTypeFactory(new InternalUnsupportedTypeFactory()) .addValueObjectType(BigIntegerValueType.class) .addValueObjectType(BigDecimalValueType.class) - .addValueObjectType(UUIDValueType.class) .addValueObjectType(LocalDateValueType.class) .addValueObjectType(LocalTimeValueType.class) .addValueObjectType(LocalDateTimeValueType.class) diff --git a/cayenne/src/test/resources/uuid.map.xml b/cayenne/src/test/resources/uuid.map.xml index 85730d5886..5404e11796 100644 --- a/cayenne/src/test/resources/uuid.map.xml +++ b/cayenne/src/test/resources/uuid.map.xml @@ -11,10 +11,17 @@ + + + + + + +