|
1 | 1 | package org.silkframework.rule.plugins.transformer.value |
2 | 2 |
|
3 | 3 | import org.silkframework.rule.annotations.{TransformExample, TransformExamples} |
4 | | -import org.silkframework.rule.input.InlineTransformer |
5 | 4 | import org.silkframework.rule.plugins.transformer.replace.MapTransformerWithDefaultInput |
6 | 5 | import org.silkframework.runtime.plugin.annotations.{Param, Plugin, PluginReference} |
7 | | -import org.silkframework.runtime.plugin.{AutoCompletionResult, ParamValue, PluginContext, PluginParameterAutoCompletionProvider} |
8 | | -import org.silkframework.workspace.WorkspaceReadTrait |
9 | 6 |
|
10 | | -import java.nio.charset.StandardCharsets |
11 | | -import java.security.{MessageDigest, Security} |
12 | | -import scala.jdk.CollectionConverters.IterableHasAsScala |
| 7 | +import java.security.MessageDigest |
13 | 8 |
|
| 9 | + |
| 10 | +/** |
| 11 | + * Combines all input values across all connected ports into a single hash. |
| 12 | + * |
| 13 | + * Values are fed sequentially into a single [[java.security.MessageDigest]] instance — port 1 first, then port 2, |
| 14 | + * and so on; within each port, values are processed in order. No separator is inserted between values or ports. |
| 15 | + * The output is always exactly one lowercase hexadecimal string, regardless of how many values or ports are provided. |
| 16 | + * |
| 17 | + * @see [[PerValueHashTransformer]] for the per-value variant that produces one hash per input value. |
| 18 | + */ |
14 | 19 | @Plugin( |
15 | 20 | id = InputHashTransformer.pluginId, |
16 | 21 | categories = Array("Value"), |
17 | | - label = "Input hash", |
18 | | - description = """Calculates the hash sum of the input values. Generates a single hash sum for all input values combined.""", |
| 22 | + label = "Combined input hash", |
| 23 | + description = """Calculates a single hash value covering all input values combined, across all input ports. Values are fed into the hash function in port order without any separator between them.""", |
19 | 24 | documentationFile = "InputHashTransformer.md", |
20 | 25 | relatedPlugins = Array( |
| 26 | + new PluginReference( |
| 27 | + id = PerValueHashTransformer.pluginId, |
| 28 | + description = "The Per-value hash plugin hashes each input value independently and returns one hash per value, preserving cardinality. The Combined input hash plugin instead feeds all values into a single hash function, producing one combined hash regardless of input size." |
| 29 | + ), |
21 | 30 | new PluginReference( |
22 | 31 | id = MapTransformerWithDefaultInput.pluginId, |
23 | | - description = "One hash value is produced for the entire set of inputs by the Input hash plugin. The Map with default plugin instead keeps a value sequence and rewrites it position by position through the mapping, falling back to the second input where no mapping entry is found." |
| 32 | + description = "One hash value is produced for the entire set of inputs by the Combined input hash plugin. The Map with default plugin instead keeps a value sequence and rewrites it position by position through the mapping, falling back to the second input where no mapping entry is found." |
24 | 33 | ) |
25 | 34 | ) |
26 | 35 | ) |
27 | 36 | @TransformExamples(Array( |
28 | 37 | new TransformExample( |
| 38 | + description = "A single input value produces one combined SHA-256 hash.", |
29 | 39 | input1 = Array("input value"), |
30 | 40 | output = Array("f708c2afff0ed197e8551c4dd549ee5b848e0b407106cbdb8e451c8cd1479362") |
31 | 41 | ), |
| 42 | + new TransformExample( |
| 43 | + description = "Multiple values on one input are combined into a single hash.", |
| 44 | + input1 = Array("apple", "banana"), |
| 45 | + output = Array("5b692305517af54eb5ae12b9ff89eaf89e31f6a6ee208365886a18b81a2fc2f8") |
| 46 | + ), |
| 47 | + new TransformExample( |
| 48 | + description = "Reversing the value order produces a different hash, confirming order-sensitivity.", |
| 49 | + input1 = Array("banana", "apple"), |
| 50 | + output = Array("d4183362b538440bb9a5f82359791c647280e6b657a1812f16f7bcc2b8f141ca") |
| 51 | + ), |
| 52 | + new TransformExample( |
| 53 | + description = "Values from multiple ports are combined in port order, producing the same hash as the equivalent single-port sequence.", |
| 54 | + input1 = Array("apple"), |
| 55 | + input2 = Array("banana"), |
| 56 | + output = Array("5b692305517af54eb5ae12b9ff89eaf89e31f6a6ee208365886a18b81a2fc2f8") |
| 57 | + ), |
| 58 | + new TransformExample( |
| 59 | + description = "The algorithm parameter selects the hash function (MD5).", |
| 60 | + parameters = Array("algorithm", "MD5"), |
| 61 | + input1 = Array("input value"), |
| 62 | + output = Array("cee963a28f70ee97751a85ef732e66dd") |
| 63 | + ), |
| 64 | + new TransformExample( |
| 65 | + description = "The algorithm parameter selects the hash function (SHA-1).", |
| 66 | + parameters = Array("algorithm", "SHA-1"), |
| 67 | + input1 = Array("apple"), |
| 68 | + output = Array("d0be2dc421be4fcd0172e5afceea3970e2f3d940") |
| 69 | + ), |
| 70 | + new TransformExample( |
| 71 | + description = "The algorithm parameter selects the hash function (SHA-384).", |
| 72 | + parameters = Array("algorithm", "SHA-384"), |
| 73 | + input1 = Array("apple"), |
| 74 | + output = Array("3d8786fcb588c93348756c6429717dc6c374a14f7029362281a3b21dc10250ddf0d0578052749822eb08bc0dc1e68b0f") |
| 75 | + ), |
| 76 | + new TransformExample( |
| 77 | + description = "The algorithm parameter selects the hash function (SHA-512).", |
| 78 | + parameters = Array("algorithm", "SHA-512"), |
| 79 | + input1 = Array("apple"), |
| 80 | + output = Array("844d8779103b94c18f4aa4cc0c3b4474058580a991fba85d3ca698a0bc9e52c5940feb7a65a3a290e17e6b23ee943ecc4f73e7490327245b4fe5d5efb590feb2") |
| 81 | + ), |
| 82 | + new TransformExample( |
| 83 | + description = "Empty input produces the hash of an empty message.", |
| 84 | + input1 = Array(), |
| 85 | + output = Array("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") |
| 86 | + ), |
| 87 | + new TransformExample( |
| 88 | + description = "Empty algorithm string causes IllegalArgumentException.", |
| 89 | + parameters = Array("algorithm", ""), |
| 90 | + input1 = Array("foo"), |
| 91 | + throwsException = classOf[IllegalArgumentException] |
| 92 | + ), |
32 | 93 | )) |
33 | 94 | case class InputHashTransformer(@Param(value = "The hash algorithm to be used.", |
34 | 95 | autoCompletionProvider = classOf[HashAlgorithmAutoCompletionProvider], allowOnlyAutoCompletedValues = true) |
35 | | - algorithm: String = "SHA256") extends InlineTransformer { |
| 96 | + algorithm: String = "SHA256") extends HashTransformer { |
36 | 97 |
|
37 | 98 | require(algorithm.trim.nonEmpty, "Algorithm must not be empty. Please specify an algorithm, such as 'SHA256'.") |
38 | 99 |
|
39 | 100 | override def apply(values: Seq[Seq[String]]): Seq[String] = { |
40 | | - val hashSum = MessageDigest.getInstance(algorithm) |
41 | | - for(value <- values; v <- value) { |
42 | | - hashSum.update(v.getBytes(StandardCharsets.UTF_8)) |
43 | | - } |
44 | | - // Convert the byte array to a hexadecimal string |
45 | | - Seq(hashSum.digest().map("%02x".format(_)).mkString) |
| 101 | + def updateAll(digest: MessageDigest): Unit = |
| 102 | + values.flatten.foreach(updateWith(digest, _)) |
| 103 | + |
| 104 | + Seq(withDigest { digest => |
| 105 | + updateAll(digest) |
| 106 | + toHex(digest.digest()) |
| 107 | + }) |
46 | 108 | } |
47 | 109 | } |
48 | 110 |
|
49 | 111 | object InputHashTransformer { |
50 | 112 | final val pluginId = "inputHash" |
51 | 113 | } |
52 | | - |
53 | | -/** |
54 | | - * Auto-completion for project resources. |
55 | | - */ |
56 | | -case class HashAlgorithmAutoCompletionProvider() extends PluginParameterAutoCompletionProvider { |
57 | | - |
58 | | - private lazy val algorithms = { |
59 | | - Security.getAlgorithms("MessageDigest").asScala.toSeq |
60 | | - } |
61 | | - |
62 | | - override def autoComplete(searchQuery: String, dependOnParameterValues: Seq[ParamValue], |
63 | | - workspace: WorkspaceReadTrait) |
64 | | - (implicit context: PluginContext): Iterable[AutoCompletionResult] = { |
65 | | - |
66 | | - val multiSearchWords = extractSearchTerms(searchQuery) |
67 | | - algorithms |
68 | | - .filter(r => matchesSearchTerm(multiSearchWords, r.toLowerCase)) |
69 | | - .map(r => AutoCompletionResult(r, None)) |
70 | | - } |
71 | | - |
72 | | - override def valueToLabel(value: String, dependOnParameterValues: Seq[ParamValue], |
73 | | - workspace: WorkspaceReadTrait) |
74 | | - (implicit context: PluginContext): Option[String] = { |
75 | | - None |
76 | | - } |
77 | | -} |
|
0 commit comments