diff --git a/core/src/main/java/apoc/ApocConfig.java b/core/src/main/java/apoc/ApocConfig.java index 9be1e5b84a..c7a3e08eb7 100644 --- a/core/src/main/java/apoc/ApocConfig.java +++ b/core/src/main/java/apoc/ApocConfig.java @@ -61,6 +61,8 @@ public class ApocConfig extends LifecycleAdapter { public static final String APOC_TRIGGER_ENABLED = "apoc.trigger.enabled"; public static final String APOC_UUID_ENABLED = "apoc.uuid.enabled"; public static final String APOC_UUID_ENABLED_DB = "apoc.uuid.enabled.%s"; + public static final String APOC_UUID_FORMAT = "apoc.uuid.format"; + public enum UuidFormatType { hex, base64 } public static final String APOC_JSON_ZIP_URL = "apoc.json.zip.url"; // TODO: check if really needed public static final String APOC_JSON_SIMPLE_JSON_URL = "apoc.json.simpleJson.url"; // TODO: check if really needed public static final String APOC_IMPORT_FILE_ALLOW__READ__FROM__FILESYSTEM = "apoc.import.file.allow_read_from_filesystem"; @@ -350,6 +352,16 @@ public boolean getBoolean(String key, boolean defaultValue) { return getConfig().getBoolean(key, defaultValue); } + public > T getEnumProperty(String key, Class cls, T defaultValue) { + var value = getConfig().getString(key, defaultValue.toString()).trim(); + try { + return T.valueOf(cls, value); + } catch (IllegalArgumentException e) { + log.error("Wrong value '{}' for parameter '{}' is provided. Default value is used: '{}'", value, key, defaultValue); + return defaultValue; + } + } + public boolean isImportFolderConfigured() { // in case we're test database import path is TestDatabaseManagementServiceBuilder.EPHEMERAL_PATH diff --git a/core/src/main/java/apoc/create/Create.java b/core/src/main/java/apoc/create/Create.java index 3e1a923cd4..a4aa87e531 100644 --- a/core/src/main/java/apoc/create/Create.java +++ b/core/src/main/java/apoc/create/Create.java @@ -3,6 +3,7 @@ import apoc.get.Get; import apoc.result.*; import apoc.util.Util; +import apoc.uuid.UuidUtil; import org.neo4j.graphdb.*; import org.neo4j.internal.helpers.collection.Iterables; import org.neo4j.procedure.*; @@ -264,6 +265,24 @@ public String uuid() { return UUID.randomUUID().toString(); } + @UserFunction + @Description("apoc.create.uuidBase64() - create an UUID encoded with Base64") + public String uuidBase64() { + return UuidUtil.generateBase64Uuid(UUID.randomUUID()); + } + + @UserFunction + @Description("apoc.create.uuidBase64ToHex() - convert between an UUID encoded with Base64 to HEX format") + public String uuidBase64ToHex(@Name("base64Uuid") String base64Uuid) { + return UuidUtil.fromBase64ToHex(base64Uuid); + } + + @UserFunction + @Description("apoc.create.uuidHexToBase64() - convert an UUID in HEX format to encoded with Base64") + public String uuidHexToBase64(@Name("uuidHex") String uuidHex) { + return UuidUtil.fromHexToBase64(uuidHex); + } + private Object toPropertyValue(Object value) { if (value instanceof Iterable) { Iterable it = (Iterable) value; diff --git a/core/src/main/java/apoc/uuid/UuidUtil.java b/core/src/main/java/apoc/uuid/UuidUtil.java new file mode 100644 index 0000000000..e42f7f4700 --- /dev/null +++ b/core/src/main/java/apoc/uuid/UuidUtil.java @@ -0,0 +1,61 @@ +package apoc.uuid; + +import java.nio.ByteBuffer; +import java.util.Base64; +import java.util.UUID; + +public class UuidUtil { + + public static String fromHexToBase64(String hexUuid) { + var uuid = UUID.fromString(hexUuid); + return generateBase64Uuid(uuid); + } + + public static String fromBase64ToHex(String base64Uuid) { + if (base64Uuid == null) { + throw new NullPointerException(); + } + if (base64Uuid.isBlank()) { + throw new IllegalStateException("Expected not empty UUID value"); + } + + String valueForConversion; + // check if Base64 text ends with '==' already (Base64 alignment) + if (base64Uuid.endsWith("==")) { + if (base64Uuid.length() != 24) { + throw new IllegalStateException("Invalid UUID length. Expected 24 characters"); + } + valueForConversion = base64Uuid; + } else { + if (base64Uuid.length() != 22) { + throw new IllegalStateException("Invalid UUID length. Expected 22 characters"); + } + valueForConversion = base64Uuid + "=="; + } + + var buffer = Base64.getDecoder().decode(valueForConversion); + + // Generate UUID from 16 byte buffer + long msb = 0L; + for (int i=0; i < 8; ++i) { + msb <<= 8; + msb |= (buffer[i] & 0xFF); + } + var lsb = 0L; + for (int i=8; i < 16; ++i) { + lsb <<= 8; + lsb |= (buffer[i] & 0xFF); + } + + var uuid = new UUID(msb, lsb); + return uuid.toString(); + } + + public static String generateBase64Uuid(UUID uuid) { + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + var encoded = Base64.getEncoder().encodeToString(bb.array()); + return encoded.substring(0, encoded.length() - 2); // skip '==' alignment + } +} diff --git a/core/src/test/java/apoc/uuid/UuidUtilTest.java b/core/src/test/java/apoc/uuid/UuidUtilTest.java new file mode 100644 index 0000000000..ecced0d8b2 --- /dev/null +++ b/core/src/test/java/apoc/uuid/UuidUtilTest.java @@ -0,0 +1,74 @@ +package apoc.uuid; + +import org.junit.Test; + +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; + +public class UuidUtilTest { + + @Test + public void fromHexToBase64() { + var input = "290d6cba-ce94-455e-b59f-029cf1e395c5"; + var output = UuidUtil.fromHexToBase64(input); + assertThat(output).isEqualTo("KQ1sus6URV61nwKc8eOVxQ"); + } + + @Test + public void fromBase64ToHex() { + var input = "KQ1sus6URV61nwKc8eOVxQ"; + var output = UuidUtil.fromBase64ToHex(input); + assertThat(output).isEqualTo("290d6cba-ce94-455e-b59f-029cf1e395c5"); + } + + @Test + public void fromBase64WithAlignmentToHex() { + var input = "KQ1sus6URV61nwKc8eOVxQ=="; + var output = UuidUtil.fromBase64ToHex(input); + assertThat(output).isEqualTo("290d6cba-ce94-455e-b59f-029cf1e395c5"); + } + + @Test + public void shouldFailIfHexFormatIsWrong() { + var input = "290d6cba-455e-b59f-029cf1e395c5"; + assertThatCode(() -> UuidUtil.fromHexToBase64(input)).hasMessageStartingWith("Invalid UUID string"); + } + + @Test + public void shouldFailIfHexFormatIsEmpty() { + var input = ""; + assertThatCode(() -> UuidUtil.fromHexToBase64(input)).hasMessageStartingWith("Invalid UUID string"); + } + + @Test + public void shouldFailIfHexFormatIsNull() { + assertThatCode(() -> UuidUtil.fromHexToBase64(null)).isInstanceOf(NullPointerException.class); + } + + @Test + public void shouldFailIfBase64LengthIsWrong() { + var input1 = "KQ1sus6URV61nwKc8eO=="; // wrong length + assertThatCode(() -> UuidUtil.fromBase64ToHex(input1)).hasMessageStartingWith("Invalid UUID length. Expected 24 characters"); + var input2 = "Q1sus6URV61nwKc8eOVxQ"; // wrong length + assertThatCode(() -> UuidUtil.fromBase64ToHex(input2)).hasMessageStartingWith("Invalid UUID length. Expected 22 characters"); + } + + @Test + public void shouldFailIfBase64IsEmpty() { + assertThatCode(() -> UuidUtil.fromBase64ToHex("")).hasMessageStartingWith("Expected not empty UUID value"); + } + + @Test + public void shouldFailIfBase64IsNull() { + assertThatCode(() -> UuidUtil.fromBase64ToHex(null)).isInstanceOf(NullPointerException.class); + } + + @Test + public void generateBase64ForSpecificUUIDs() { + var uuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); + var uuidBase64 = UuidUtil.generateBase64Uuid(uuid); + assertThat(uuidBase64).isEqualTo("AAAAAAAAAAAAAAAAAAAAAA"); + } +} \ No newline at end of file diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.create.uuidBase64.adoc b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.create.uuidBase64.adoc new file mode 100644 index 0000000000..facf4d7f6e --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.create.uuidBase64.adoc @@ -0,0 +1,5 @@ +¦xref::overview/apoc.create/apoc.create.uuidBase64.adoc[apoc.create.uuidBase64 icon:book[]] + + + - create an UUID encoded with Base64 +¦label:function[] +¦label:apoc-core[] diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.create.uuidBase64ToHex.adoc b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.create.uuidBase64ToHex.adoc new file mode 100644 index 0000000000..497a7adb69 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.create.uuidBase64ToHex.adoc @@ -0,0 +1,5 @@ +¦xref::overview/apoc.create/apoc.create.uuidBase64ToHex.adoc[apoc.create.uuidBase64ToHex icon:book[]] + + + - convert between an UUID encoded with Base64 to HEX format +¦label:function[] +¦label:apoc-core[] diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.create.uuidHexToBase64.adoc b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.create.uuidHexToBase64.adoc new file mode 100644 index 0000000000..a6d5c88125 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.create.uuidHexToBase64.adoc @@ -0,0 +1,5 @@ +¦xref::overview/apoc.create/apoc.create.uuidHexToBase64.adoc[apoc.create.uuidHexToBase64 icon:book[]] + + + - convert an UUID in HEX format to encoded with Base64 +¦label:function[] +¦label:apoc-core[] diff --git a/docs/asciidoc/modules/ROOT/pages/graph-updates/uuid.adoc b/docs/asciidoc/modules/ROOT/pages/graph-updates/uuid.adoc index dba8bdd09c..5b7c36a75f 100644 --- a/docs/asciidoc/modules/ROOT/pages/graph-updates/uuid.adoc +++ b/docs/asciidoc/modules/ROOT/pages/graph-updates/uuid.adoc @@ -8,6 +8,8 @@ The library supports manual and automation generation of UUIDs, which can be sto UUIDs are generated using the https://docs.oracle.com/javase/7/docs/api/java/util/UUID.html#randomUUID()[java randomUUID utility method], which generates a https://www.ietf.org/rfc/rfc4122.txt[v4UUID]. +UUID may be encoded into String with well-known hexadecimal presentation (32 characters, e.g. `1051af4f-b81d-4a76-8605-ecfb8ef703d5`) or Base64 (22 characters, e.g. `vX8dM5XoSe2ldoc/QzMEyw`) + [[manual-uuids]] == Manual UUIDs @@ -15,11 +17,14 @@ UUIDs are generated using the https://docs.oracle.com/javase/7/docs/api/java/uti |=== ¦Qualified Name¦Type¦Release include::example$generated-documentation/apoc.create.uuid.adoc[] +include::example$generated-documentation/apoc.create.uuidBase64.adoc[] +include::example$generated-documentation/apoc.create.uuidBase64ToHex.adoc[] +include::example$generated-documentation/apoc.create.uuidHexToBase64.adoc[] |=== - -.The following generates a UUID +=== Usage Examples +The following generates a UUID [source,cypher] ---- RETURN apoc.create.uuid() AS uuid; @@ -32,7 +37,7 @@ RETURN apoc.create.uuid() AS uuid; | "1051af4f-b81d-4a76-8605-ecfb8ef703d5" |=== -.The following creates a `Person` node, using a UUID as the merging key: +The following creates a `Person` node, using a UUID as the merging key: [source, cypher] ---- @@ -48,6 +53,9 @@ RETURN p | {"firstName":"Michael","surname":"Hunger","id":"5530953d-b85e-4939-b37f-a79d54b770a3"} |=== +include::partial$usage/apoc.create.uuidBase64.adoc[] +include::partial$usage/apoc.create.uuidHexToBase64.adoc[] +include::partial$usage/apoc.create.uuidBase64ToHex.adoc[] [[automatic-uuids]] == Automatic UUIDs @@ -58,6 +66,8 @@ Please check the following documentation to an in-depth description. Enable `apoc.uuid.enabled=true` or `apoc.uuid.enabled.[DATABASE_NAME]=true` in `$NEO4J_HOME/config/apoc.conf` first. +Configuration value `apoc.uuid.format` let you choose between different UUID encoding methods: `hex` (default option) or `base64`. + [separator=¦,opts=header,cols="5,1m,1m"] |=== ¦Qualified Name¦Type¦Release diff --git a/docs/asciidoc/modules/ROOT/pages/overview/apoc.create/apoc.create.uuidBase64.adoc b/docs/asciidoc/modules/ROOT/pages/overview/apoc.create/apoc.create.uuidBase64.adoc new file mode 100644 index 0000000000..90ad1460aa --- /dev/null +++ b/docs/asciidoc/modules/ROOT/pages/overview/apoc.create/apoc.create.uuidBase64.adoc @@ -0,0 +1,23 @@ +//// +This file is generated by DocsTest, so don't change it! +//// + += apoc.create.uuidBase64 +:description: This section contains reference documentation for the apoc.create.uuidBase64 function. + +label:function[] label:apoc-core[] + +[.emphasis] +apoc.create.uuidBase64() - create an UUID encoded with Base64 + +== Signature + +[source] +---- +apoc.create.uuidBase64() :: (STRING?) +---- + +[[usage-apoc.create.uuidBase64]] +== Usage Examples +include::partial$usage/apoc.create.uuidBase64.adoc[] + diff --git a/docs/asciidoc/modules/ROOT/pages/overview/apoc.create/apoc.create.uuidBase64ToHex.adoc b/docs/asciidoc/modules/ROOT/pages/overview/apoc.create/apoc.create.uuidBase64ToHex.adoc new file mode 100644 index 0000000000..ef73808e9e --- /dev/null +++ b/docs/asciidoc/modules/ROOT/pages/overview/apoc.create/apoc.create.uuidBase64ToHex.adoc @@ -0,0 +1,30 @@ +//// +This file is generated by DocsTest, so don't change it! +//// + += apoc.create.uuidBase64ToHex +:description: This section contains reference documentation for the apoc.create.uuidBase64ToHex function. + +label:function[] label:apoc-core[] + +[.emphasis] +apoc.create.uuidBase64ToHex() - convert between an UUID encoded with Base64 to HEX format + +== Signature + +[source] +---- +apoc.create.uuidBase64ToHex(base64Uuid :: STRING?) :: (STRING?) +---- + +== Input parameters +[.procedures, opts=header] +|=== +| Name | Type | Default +|base64Uuid|STRING?|null +|=== + +[[usage-apoc.create.uuidBase64ToHex]] +== Usage Examples +include::partial$usage/apoc.create.uuidBase64ToHex.adoc[] + diff --git a/docs/asciidoc/modules/ROOT/pages/overview/apoc.create/apoc.create.uuidHexToBase64.adoc b/docs/asciidoc/modules/ROOT/pages/overview/apoc.create/apoc.create.uuidHexToBase64.adoc new file mode 100644 index 0000000000..02529a553c --- /dev/null +++ b/docs/asciidoc/modules/ROOT/pages/overview/apoc.create/apoc.create.uuidHexToBase64.adoc @@ -0,0 +1,30 @@ +//// +This file is generated by DocsTest, so don't change it! +//// + += apoc.create.uuidHexToBase64 +:description: This section contains reference documentation for the apoc.create.uuidHexToBase64 function. + +label:function[] label:apoc-core[] + +[.emphasis] +apoc.create.uuidHexToBase64() - convert an UUID in HEX format to encoded with Base64 + +== Signature + +[source] +---- +apoc.create.uuidHexToBase64(uuidHex :: STRING?) :: (STRING?) +---- + +== Input parameters +[.procedures, opts=header] +|=== +| Name | Type | Default +|uuidHex|STRING?|null +|=== + +[[usage-apoc.create.uuidHexToBase64]] +== Usage Examples +include::partial$usage/apoc.create.uuidHexToBase64.adoc[] + diff --git a/docs/asciidoc/modules/ROOT/partials/usage/apoc.create.uuidBase64.adoc b/docs/asciidoc/modules/ROOT/partials/usage/apoc.create.uuidBase64.adoc new file mode 100644 index 0000000000..7ce603758b --- /dev/null +++ b/docs/asciidoc/modules/ROOT/partials/usage/apoc.create.uuidBase64.adoc @@ -0,0 +1,13 @@ +The following generates a new UUID encoded with Base64: + +[source,cypher] +---- +RETURN apoc.create.uuidBase64() as output; +---- + +.Results +[opts="header",cols="1"] +|=== +| Output +| "vX8dM5XoSe2ldoc/QzMEyw" +|=== \ No newline at end of file diff --git a/docs/asciidoc/modules/ROOT/partials/usage/apoc.create.uuidBase64ToHex.adoc b/docs/asciidoc/modules/ROOT/partials/usage/apoc.create.uuidBase64ToHex.adoc new file mode 100644 index 0000000000..105fdb716e --- /dev/null +++ b/docs/asciidoc/modules/ROOT/partials/usage/apoc.create.uuidBase64ToHex.adoc @@ -0,0 +1,13 @@ +The following converts an UUID encoded with Base64 to HEX representation: + +[source,cypher] +---- +RETURN apoc.create.uuidBase64ToHex("vX8dM5XoSe2ldoc/QzMEyw") as output; +---- + +.Results +[opts="header",cols="1"] +|=== +| Output +| "bd7f1d33-95e8-49ed-a576-873f433304cb" +|=== \ No newline at end of file diff --git a/docs/asciidoc/modules/ROOT/partials/usage/apoc.create.uuidHexToBase64.adoc b/docs/asciidoc/modules/ROOT/partials/usage/apoc.create.uuidHexToBase64.adoc new file mode 100644 index 0000000000..5c8092bc70 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/partials/usage/apoc.create.uuidHexToBase64.adoc @@ -0,0 +1,13 @@ +The following converts an UUID encoded with Base64 to HEX representation: + +[source,cypher] +---- +RETURN apoc.create.uuidHexToBase64("bd7f1d33-95e8-49ed-a576-873f433304cb") as output; +---- + +.Results +[opts="header",cols="1"] +|=== +| Output +| "vX8dM5XoSe2ldoc/QzMEyw" +|=== \ No newline at end of file diff --git a/full/src/main/java/apoc/uuid/Uuid.java b/full/src/main/java/apoc/uuid/Uuid.java index 37e30a7b22..44745b7199 100644 --- a/full/src/main/java/apoc/uuid/Uuid.java +++ b/full/src/main/java/apoc/uuid/Uuid.java @@ -1,5 +1,6 @@ package apoc.uuid; +import apoc.ApocConfig; import apoc.Extended; import apoc.Pools; import apoc.util.Util; @@ -33,12 +34,13 @@ public Stream install(@Name("label") String label, @Name(value UuidConfig uuidConfig = new UuidConfig(config); uuidHandler.checkConstraintUuid(tx, label, uuidConfig.getUuidProperty()); + final String uuidFunctionName = getUuidFunctionName(); Map addToExistingNodesResult = Collections.emptyMap(); if (uuidConfig.isAddToExistingNodes()) { addToExistingNodesResult = Util.inTx(db, pools, txInThread -> txInThread.execute("CALL apoc.periodic.iterate(" + "\"MATCH (n:" + Util.sanitizeAndQuote(label) + ") RETURN n\",\n" + - "\"SET n." + Util.sanitizeAndQuote(uuidConfig.getUuidProperty()) + " = apoc.create.uuid()\", {batchSize:10000, parallel:true})") + "\"SET n." + Util.sanitizeAndQuote(uuidConfig.getUuidProperty()) + " = " + uuidFunctionName + "()\", {batchSize:10000, parallel:true})") .next() ); } @@ -109,4 +111,15 @@ public static class UuidInstallInfo extends UuidInfo { } + private String getUuidFunctionName() { + ApocConfig.UuidFormatType formatType = ApocConfig.apocConfig().getEnumProperty(ApocConfig.APOC_UUID_FORMAT, ApocConfig.UuidFormatType.class, ApocConfig.UuidFormatType.hex); + switch(formatType) { + case base64: + return "apoc.create.uuidBase64"; + case hex: + default: + return "apoc.create.uuid"; + } + } + } diff --git a/full/src/main/java/apoc/uuid/UuidHandler.java b/full/src/main/java/apoc/uuid/UuidHandler.java index 57ba3019c5..1fa94e1b97 100644 --- a/full/src/main/java/apoc/uuid/UuidHandler.java +++ b/full/src/main/java/apoc/uuid/UuidHandler.java @@ -33,6 +33,7 @@ import java.util.stream.StreamSupport; import static apoc.ApocConfig.APOC_UUID_ENABLED; +import static apoc.ApocConfig.APOC_UUID_FORMAT; public class UuidHandler extends LifecycleAdapter implements TransactionEventListener { @@ -41,6 +42,7 @@ public class UuidHandler extends LifecycleAdapter implements TransactionEventLis private final DatabaseManagementService databaseManagementService; private final ApocConfig apocConfig; private final ConcurrentHashMap configuredLabelAndPropertyNames = new ConcurrentHashMap<>(); + private final ApocConfig.UuidFormatType uuidFormat; public static final String NOT_ENABLED_ERROR = "UUID have not been enabled." + " Set 'apoc.uuid.enabled=true' or 'apoc.uuid.enabled.%s=true' in your apoc.conf file located in the $NEO4J_HOME/conf/ directory."; @@ -50,6 +52,7 @@ public UuidHandler(GraphDatabaseAPI db, DatabaseManagementService databaseManage this.databaseManagementService = databaseManagementService; this.log = log; this.apocConfig = apocConfig; + this.uuidFormat = apocConfig.getEnumProperty(APOC_UUID_FORMAT, ApocConfig.UuidFormatType.class, ApocConfig.UuidFormatType.hex); } @Override @@ -107,7 +110,7 @@ public Void beforeCommit(TransactionData txData, Transaction transaction, GraphD try { nodes.forEach(node -> { if (node.hasLabel(Label.label(label)) && !node.hasProperty(propertyName)) { - String uuid = UUID.randomUUID().toString(); + String uuid = generateUuidValue(); node.setProperty(propertyName, uuid); } }); @@ -131,13 +134,23 @@ public void afterRollback(TransactionData data, Void state, GraphDatabaseService } - private void checkEnabled() { if (!isEnabled()) { throw new RuntimeException(String.format(NOT_ENABLED_ERROR, this.db.databaseName()) ); } } + private String generateUuidValue() { + UUID uuid = UUID.randomUUID(); + switch (uuidFormat) { + case base64: + return UuidUtil.generateBase64Uuid(uuid); + case hex: + default: + return uuid.toString(); + } + } + public void checkConstraintUuid(Transaction tx, String label, String propertyName) { Schema schema = tx.schema(); Stream constraintDefinitionStream = StreamSupport.stream(schema.getConstraints(Label.label(label)).spliterator(), false);