This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 41679ddc46 [Fix][Transform-V2] Rebuild chained transform routing after
deserialization (#12990)
41679ddc46 is described below
commit 41679ddc46117c09f42a2c9229e9148f1a56d065
Author: JeremyXin <[email protected]>
AuthorDate: Tue Oct 27 02:51:08 2026 +0000
[Fix][Transform-V2] Rebuild chained transform routing after deserialization
(#12990)
Co-authored-by: JeremyXin <--golbal>
---
.../apache/seatunnel/e2e/transform/TestCopyIT.java | 2 +-
.../copy_transform_table_transform_only.conf | 16 +-
.../transform/common/ChainedMapTransform.java | 10 +-
.../common/AbstractMultiCatalogTransformTest.java | 161 +++++++++++++++++++++
.../GenerateLegacyChainedMapFixture.java | 75 ++++++++++
.../src/test/resources/serialization/README.md | 62 ++++++++
.../serialization/legacy-chained-map.base64 | 65 +++++++++
7 files changed, 388 insertions(+), 3 deletions(-)
diff --git
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/java/org/apache/seatunnel/e2e/transform/TestCopyIT.java
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/java/org/apache/seatunnel/e2e/transform/TestCopyIT.java
index 87da6ff635..a8fcbfb95d 100644
---
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/java/org/apache/seatunnel/e2e/transform/TestCopyIT.java
+++
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/java/org/apache/seatunnel/e2e/transform/TestCopyIT.java
@@ -40,7 +40,7 @@ public class TestCopyIT extends TestSuiteBase {
Assertions.assertEquals(0, execResult.getExitCode());
}
- /** A transform configured only through {@code table_transform} entries
must be accepted. */
+ /** Table-only rules must chain in order while unmatched tables pass
through unchanged. */
@TestTemplate
public void testCopyTableTransformOnly(TestContainer container)
throws IOException, InterruptedException {
diff --git
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/resources/copy_transform_table_transform_only.conf
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/resources/copy_transform_table_transform_only.conf
index 61025e47dd..f1bb36dabd 100644
---
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/resources/copy_transform_table_transform_only.conf
+++
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-1/src/test/resources/copy_transform_table_transform_only.conf
@@ -16,7 +16,8 @@
#
######
###### Copy configured only through table_transform entries.
-###### test.xyz copies 'name' to 'name2', the other tables stay unchanged.
+###### test.xyz copies 'name' to 'name2' and then 'name2' to 'name3'.
+###### The other tables stay unchanged.
######
env {
@@ -94,10 +95,15 @@ transform {
Copy {
// No transform level rule: every table is handled by its own
table_transform entry,
// tables without an entry are passed through unchanged.
+ rule_match_mode = "ALL_MATCH"
table_transform = [{
table_path = "test.xyz"
src_field = "name"
dest_field = "name2"
+ }, {
+ table_path = "test.xyz"
+ src_field = "name2"
+ dest_field = "name3"
}]
}
}
@@ -137,6 +143,14 @@ sink {
rule_type = NOT_NULL
}
]
+ }, {
+ field_name = name3
+ field_type = string
+ field_value = [
+ {
+ rule_type = NOT_NULL
+ }
+ ]
}]
},
{
diff --git
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedMapTransform.java
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedMapTransform.java
index 2a55a83049..3c455e81ec 100644
---
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedMapTransform.java
+++
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedMapTransform.java
@@ -23,6 +23,8 @@ import org.apache.seatunnel.api.table.type.SeaTunnelRow;
import org.apache.seatunnel.api.transform.SeaTunnelMapTransform;
import org.apache.seatunnel.api.transform.SeaTunnelTransform;
+import java.io.IOException;
+import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -34,7 +36,7 @@ class ChainedMapTransform implements
SeaTunnelMapTransform<SeaTunnelRow> {
private final List<SeaTunnelTransform<SeaTunnelRow>> transforms;
/** Extra table paths of every rule, rebuilt only by {@link
#refreshExtraTablePaths()}. */
- private final List<Set<String>> extraTablePaths = new ArrayList<>();
+ private transient List<Set<String>> extraTablePaths = new ArrayList<>();
ChainedMapTransform(List<SeaTunnelTransform<SeaTunnelRow>> transforms) {
this.transforms = transforms;
@@ -42,6 +44,12 @@ class ChainedMapTransform implements
SeaTunnelMapTransform<SeaTunnelRow> {
refreshExtraTablePaths();
}
+ private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
+ in.defaultReadObject();
+ extraTablePaths = new ArrayList<>();
+ refreshExtraTablePaths();
+ }
+
/**
* Rebuilds the extra table paths of every rule. Runs on construction and
on schema changes: a
* rule derives its produced tables from its input table, which only
changes there.
diff --git
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransformTest.java
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransformTest.java
index 4173ab908d..b670522f8e 100644
---
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransformTest.java
+++
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransformTest.java
@@ -34,12 +34,22 @@ import org.apache.seatunnel.api.table.type.SeaTunnelRow;
import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
import org.apache.seatunnel.api.transform.SeaTunnelMapTransform;
import org.apache.seatunnel.api.transform.SeaTunnelTransform;
+import org.apache.seatunnel.common.utils.SerializationUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.ObjectInputStream;
+import java.io.ObjectStreamClass;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -150,6 +160,68 @@ class AbstractMultiCatalogTransformTest {
Assertions.assertEquals("second", output.getField(2));
}
+ @Test
+ void chainedMapTransformComputesLegacySerialVersionUid() {
+ Assertions.assertEquals(
+ 8769738606872706499L,
+
ObjectStreamClass.lookup(ChainedMapTransform.class).getSerialVersionUID());
+ }
+
+ @Test
+ void chainedMapTransformReadsLegacyStreamAndMapsRows() throws Exception {
+ ChainedMapTransform restored = readLegacyChainedMapTransform();
+ String path = tablePath(catalogTable());
+
+ SeaTunnelRow output = restored.map(row(path));
+
+ Assertions.assertEquals(path, output.getTableId());
+ Assertions.assertEquals(1, output.getField(0));
+ }
+
+ @Test
+ void chainedMapTransformReadsLegacyStreamAndRefreshesInputTables() throws
Exception {
+ ChainedMapTransform restored = readLegacyChainedMapTransform();
+ CatalogTable table = catalogTable();
+
+ restored.setInputCatalogTables(Collections.singletonList(table));
+
+ Assertions.assertEquals(tablePath(table),
restored.map(row(tablePath(table))).getTableId());
+ }
+
+ @Test
+ void chainedMapTransformReadsLegacyStreamAndMapsSchemaChanges() throws
Exception {
+ ChainedMapTransform restored = readLegacyChainedMapTransform();
+ CatalogTable table = catalogTable();
+
+ restored.mapSchemaChangeEvent(addColumnEvent(table));
+
+ Assertions.assertEquals(tablePath(table),
restored.map(row(tablePath(table))).getTableId());
+ }
+
+ @Test
+ void chainedMapTransformPreservesExtraTableRoutingAfterCurrentRoundTrip()
throws Exception {
+ CatalogTable table = catalogTable();
+ CatalogTable extraTable =
+ CatalogTableUtil.getCatalogTable(
+ "catalog", "database", "schema", "errors",
table.getSeaTunnelRowType());
+ ChainedMapTransform transform =
+ new ChainedMapTransform(
+ Arrays.asList(
+ new ExtraTableRoutingTransform(table,
extraTable),
+ new FailOnMapTransform(table)));
+ byte[] bytes = SerializationUtils.serialize(transform);
+
+ ChainedMapTransform restored;
+ try (ObjectInputStream input = new ObjectInputStream(new
ByteArrayInputStream(bytes))) {
+ restored = (ChainedMapTransform) input.readObject();
+ }
+
+ restored.setInputCatalogTables(Collections.singletonList(table));
+
+ Assertions.assertEquals(
+ tablePath(extraTable),
restored.map(row(tablePath(table))).getTableId());
+ }
+
@Test
void allMatchModeWorksForFlatMapTransforms() {
CatalogTable table = catalogTable();
@@ -289,6 +361,34 @@ class AbstractMultiCatalogTransformTest {
return CatalogTableUtil.getCatalogTable("catalog", "database",
"schema", "orders", rowType);
}
+ // Fixture serialized from commit 02b156e45, before extraTablePaths was
added. It verifies
+ // ChainedMapTransform compatibility only; its child transforms use
current support classes.
+ private static ChainedMapTransform readLegacyChainedMapTransform()
+ throws IOException, ClassNotFoundException {
+ try (InputStream resource =
+ AbstractMultiCatalogTransformTest.class.getResourceAsStream(
+ "/serialization/legacy-chained-map.base64")) {
+ Assertions.assertNotNull(resource);
+ BufferedReader reader =
+ new BufferedReader(new InputStreamReader(resource,
StandardCharsets.US_ASCII));
+ String line;
+ while ((line = reader.readLine()) != null && !"-----BEGIN
BASE64-----".equals(line)) {
+ // Skip the license header.
+ }
+ Assertions.assertNotNull(line);
+ StringBuilder encoded = new StringBuilder();
+ while ((line = reader.readLine()) != null) {
+ encoded.append(line);
+ }
+ try (ObjectInputStream input =
+ new ObjectInputStream(
+ new ByteArrayInputStream(
+
Base64.getDecoder().decode(encoded.toString())))) {
+ return (ChainedMapTransform) input.readObject();
+ }
+ }
+ }
+
private static String tablePath(CatalogTable table) {
return table.getTableId().toTablePath().toString();
}
@@ -410,6 +510,67 @@ class AbstractMultiCatalogTransformTest {
}
}
+ private static class ExtraTableRoutingTransform implements
SeaTunnelMapTransform<SeaTunnelRow> {
+
+ private final CatalogTable table;
+ private final CatalogTable extraTable;
+
+ private ExtraTableRoutingTransform(CatalogTable table, CatalogTable
extraTable) {
+ this.table = table;
+ this.extraTable = extraTable;
+ }
+
+ @Override
+ public String getPluginName() {
+ return "ExtraTableRouting";
+ }
+
+ @Override
+ public SeaTunnelRow map(SeaTunnelRow row) {
+ row.setTableId(tablePath(extraTable));
+ return row;
+ }
+
+ @Override
+ public CatalogTable getProducedCatalogTable() {
+ return table;
+ }
+
+ @Override
+ public List<CatalogTable> getProducedCatalogTables() {
+ return Arrays.asList(table, extraTable);
+ }
+ }
+
+ private static class FailOnMapTransform implements
SeaTunnelMapTransform<SeaTunnelRow> {
+
+ private final CatalogTable table;
+
+ private FailOnMapTransform(CatalogTable table) {
+ this.table = table;
+ }
+
+ @Override
+ public String getPluginName() {
+ return "FailOnMap";
+ }
+
+ @Override
+ public SeaTunnelRow map(SeaTunnelRow row) {
+ throw new AssertionError("Extra-table row must skip the remaining
rules");
+ }
+
+ @Override
+ public CatalogTable getProducedCatalogTable() {
+ return table;
+ }
+
+ @Override
+ public List<CatalogTable> getProducedCatalogTables() {
+ return Collections.singletonList(table);
+ }
+ }
+
private static class TestFlatMapMultiCatalogTransform
extends AbstractMultiCatalogFlatMapTransform {
diff --git
a/seatunnel-transforms-v2/src/test/resources/serialization/GenerateLegacyChainedMapFixture.java
b/seatunnel-transforms-v2/src/test/resources/serialization/GenerateLegacyChainedMapFixture.java
new file mode 100644
index 0000000000..913875aaf2
--- /dev/null
+++
b/seatunnel-transforms-v2/src/test/resources/serialization/GenerateLegacyChainedMapFixture.java
@@ -0,0 +1,75 @@
+/*
+ * 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
+ *
+ * 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 org.apache.seatunnel.transform.common;
+
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import org.apache.seatunnel.common.utils.SerializationUtils;
+
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Base64;
+
+public class GenerateLegacyChainedMapFixture {
+ private static final String HEADER =
+ "# Licensed to the Apache Software Foundation (ASF) under one or
more\n"
+ + "# contributor license agreements. See the NOTICE file
distributed with\n"
+ + "# this work for additional information regarding
copyright ownership.\n"
+ + "# The ASF licenses this file to You under the Apache
License, Version 2.0\n"
+ + "# (the \"License\"); you may not use this file except
in compliance with\n"
+ + "# the License. You may obtain a copy of the License
at\n"
+ + "#\n"
+ + "# http://www.apache.org/licenses/LICENSE-2.0\n"
+ + "#\n"
+ + "# Unless required by applicable law or agreed to in
writing, software\n"
+ + "# distributed under the License is distributed on an
\"AS IS\" BASIS,\n"
+ + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied.\n"
+ + "# See the License for the specific language governing
permissions and\n"
+ + "# limitations under the License.\n"
+ + "-----BEGIN BASE64-----\n";
+
+ public static void main(String[] args) throws Exception {
+ for (Field field : ChainedMapTransform.class.getDeclaredFields()) {
+ if (field.getName().equals("extraTablePaths")
+ || field.getName().equals("serialVersionUID")) {
+ throw new IllegalStateException("Load the pre-#12804
ChainedMapTransform first");
+ }
+ }
+ SeaTunnelRowType rowType =
+ new SeaTunnelRowType(
+ new String[] {"id"}, new SeaTunnelDataType[]
{BasicType.INT_TYPE});
+ CatalogTable table =
+ CatalogTableUtil.getCatalogTable("catalog", "database",
"schema", "orders", rowType);
+ ChainedMapTransform chain =
+ new ChainedMapTransform(
+ Arrays.asList(new IdentityMapTransform(table), new
IdentityMapTransform(table)));
+ byte[] bytes = SerializationUtils.serialize(chain);
+ String payload =
+ HEADER
+ + new String(
+ Base64.getMimeEncoder(76, new byte[]
{'\n'}).encode(bytes),
+ StandardCharsets.US_ASCII);
+ Files.write(Paths.get(args[0]),
payload.getBytes(StandardCharsets.US_ASCII));
+ }
+}
diff --git a/seatunnel-transforms-v2/src/test/resources/serialization/README.md
b/seatunnel-transforms-v2/src/test/resources/serialization/README.md
new file mode 100644
index 0000000000..09c249f98c
--- /dev/null
+++ b/seatunnel-transforms-v2/src/test/resources/serialization/README.md
@@ -0,0 +1,62 @@
+<!--
+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
+
+ 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.
+-->
+
+# Legacy `ChainedMapTransform` Serialization Fixture
+
+`legacy-chained-map.base64` contains Java serialization bytes written with
+`ChainedMapTransform` from commit `02b156e45`, before `extraTablePaths` was
+added. The archived class has the implicit UID `8769738606872706499`.
+
+The fixture is read by `AbstractMultiCatalogTransformTest` to verify that the
+current `ChainedMapTransform` can deserialize and use a legacy instance. The
+two child `IdentityMapTransform` instances use current support classes, so
+this fixture isolates `ChainedMapTransform` compatibility; it does not
+represent a complete historical job plan.
+
+## Regenerating the fixture
+
+Compile the transform module first. Then, from the repository root, use JDK 17
+to compile the old `ChainedMapTransform` source from the baseline commit with
+`GenerateLegacyChainedMapFixture.java` and generate the Base64 stream:
+
+```sh
+scratch=$(mktemp -d)
+trap 'rm -rf "$scratch"' EXIT
+./mvnw -q -f seatunnel-transforms-v2/pom.xml dependency:build-classpath \
+ -Dmdep.outputFile="$scratch/classpath.txt"
+classpath="seatunnel-transforms-v2/target/classes:seatunnel-api/target/classes:seatunnel-common/target/classes:$(cat
"$scratch/classpath.txt")"
+git show
02b156e45:seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedMapTransform.java
> "$scratch/ChainedMapTransform.java"
+mkdir "$scratch/classes"
+"$JAVA_HOME/bin/javac" -source 8 -target 8 -cp "$classpath" \
+ -d "$scratch/classes" "$scratch/ChainedMapTransform.java" \
+
seatunnel-transforms-v2/src/test/resources/serialization/GenerateLegacyChainedMapFixture.java
+"$JAVA_HOME/bin/java" -cp "$scratch/classes:$classpath" \
+ org.apache.seatunnel.transform.common.GenerateLegacyChainedMapFixture \
+ "$scratch/legacy-chained-map.base64"
+cmp "$scratch/legacy-chained-map.base64" \
+
seatunnel-transforms-v2/src/test/resources/serialization/legacy-chained-map.base64
+```
+
+The scratch directory is placed first on the runtime classpath so the generator
+loads the old `ChainedMapTransform`; API, Common, and child transform support
+classes come from the current build. The generator fails if the loaded class
+declares `extraTablePaths` or `serialVersionUID`, guarding against accidentally
+generating the fixture with the current class. `cmp` checks that the generated
+bytes match the committed fixture.
+
+Normal tests only read the committed fixture. They do not run this generator
+or require Git history, JDK 17, or a compiler.
diff --git
a/seatunnel-transforms-v2/src/test/resources/serialization/legacy-chained-map.base64
b/seatunnel-transforms-v2/src/test/resources/serialization/legacy-chained-map.base64
new file mode 100644
index 0000000000..3a156c0568
--- /dev/null
+++
b/seatunnel-transforms-v2/src/test/resources/serialization/legacy-chained-map.base64
@@ -0,0 +1,65 @@
+# 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
+#
+# 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.
+-----BEGIN BASE64-----
+rO0ABXNyADlvcmcuYXBhY2hlLnNlYXR1bm5lbC50cmFuc2Zvcm0uY29tbW9uLkNoYWluZWRNYXBU
+cmFuc2Zvcm15tF7Mg609wwIAAUwACnRyYW5zZm9ybXN0ABBMamF2YS91dGlsL0xpc3Q7eHBzcgAa
+amF2YS51dGlsLkFycmF5cyRBcnJheUxpc3TZpDy+zYgG0gIAAVsAAWF0ABNbTGphdmEvbGFuZy9P
+YmplY3Q7eHB1cgA4W0xvcmcuYXBhY2hlLnNlYXR1bm5lbC5hcGkudHJhbnNmb3JtLlNlYVR1bm5l
+bFRyYW5zZm9ybTsAHjJeQVyq1QIAAHhwAAAAAnNyADpvcmcuYXBhY2hlLnNlYXR1bm5lbC50cmFu
+c2Zvcm0uY29tbW9uLklkZW50aXR5TWFwVHJhbnNmb3JtvwccJRZUZuoCAAFMAAxjYXRhbG9nVGFi
+bGV0ADVMb3JnL2FwYWNoZS9zZWF0dW5uZWwvYXBpL3RhYmxlL2NhdGFsb2cvQ2F0YWxvZ1RhYmxl
+O3hyAEhvcmcuYXBhY2hlLnNlYXR1bm5lbC50cmFuc2Zvcm0uY29tbW9uLkFic3RyYWN0Q2F0YWxv
+Z1N1cHBvcnRNYXBUcmFuc2Zvcm1sUFW2YsLBuwIAAHhyAEBvcmcuYXBhY2hlLnNlYXR1bm5lbC50
+cmFuc2Zvcm0uY29tbW9uLkFic3RyYWN0U2VhVHVubmVsVHJhbnNmb3JtcI/yPp3zJOYCAANMABFp
+bnB1dENhdGFsb2dUYWJsZXEAfgAJTAASb3V0cHV0Q2F0YWxvZ1RhYmxlcQB+AAlMABFyb3dFcnJv
+ckhhbmRsZVdheXQANkxvcmcvYXBhY2hlL3NlYXR1bm5lbC90cmFuc2Zvcm0vY29tbW9uL0Vycm9y
+SGFuZGxlV2F5O3hwc3IAM29yZy5hcGFjaGUuc2VhdHVubmVsLmFwaS50YWJsZS5jYXRhbG9nLkNh
+dGFsb2dUYWJsZQAAAAAAAAABAgAHTAALY2F0YWxvZ05hbWV0ABJMamF2YS9sYW5nL1N0cmluZztM
+AAdjb21tZW50cQB+AA9MAAhtZXRhZGF0YXQAN0xvcmcvYXBhY2hlL3NlYXR1bm5lbC9hcGkvdGFi
+bGUvY2F0YWxvZy9NZXRhZGF0YVNjaGVtYTtMAAdvcHRpb25zdAAPTGphdmEvdXRpbC9NYXA7TAAN
+cGFydGl0aW9uS2V5c3EAfgABTAAHdGFibGVJZHQAOExvcmcvYXBhY2hlL3NlYXR1bm5lbC9hcGkv
+dGFibGUvY2F0YWxvZy9UYWJsZUlkZW50aWZpZXI7TAALdGFibGVTY2hlbWF0ADRMb3JnL2FwYWNo
+ZS9zZWF0dW5uZWwvYXBpL3RhYmxlL2NhdGFsb2cvVGFibGVTY2hlbWE7eHB0AAdjYXRhbG9ndAA9
+SXQgaXMgY29udmVydGVkIGZyb20gUm93VHlwZSBhbmQgb25seSBoYXMgY29sdW1uIGluZm9ybWF0
+aW9uLnNyADVvcmcuYXBhY2hlLnNlYXR1bm5lbC5hcGkudGFibGUuY2F0YWxvZy5NZXRhZGF0YVNj
+aGVtYQAAAAAAAAABAgAAeHIANW9yZy5hcGFjaGUuc2VhdHVubmVsLmFwaS50YWJsZS5jYXRhbG9n
+LkFic3RyYWN0U2NoZW1hAAAAAAAAAAECAAJMAAtjb2x1bW5OYW1lc3EAfgABTAAHY29sdW1uc3EA
+fgABeHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAA
+AHhzcQB+ABoAAAAAdwQAAAAAeHNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9h
+ZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHNxAH4AGgAAAAB3BAAAAAB4
+c3IANm9yZy5hcGFjaGUuc2VhdHVubmVsLmFwaS50YWJsZS5jYXRhbG9nLlRhYmxlSWRlbnRpZmll
+cgAAAAAAAAABAgAETAALY2F0YWxvZ05hbWVxAH4AD0wADGRhdGFiYXNlTmFtZXEAfgAPTAAKc2No
+ZW1hTmFtZXEAfgAPTAAJdGFibGVOYW1lcQB+AA94cHEAfgAVdAAIZGF0YWJhc2V0AAZzY2hlbWF0
+AAZvcmRlcnNzcgAyb3JnLmFwYWNoZS5zZWF0dW5uZWwuYXBpLnRhYmxlLmNhdGFsb2cuVGFibGVT
+Y2hlbWEAAAAAAAAAAQIAAkwADmNvbnN0cmFpbnRLZXlzcQB+AAFMAApwcmltYXJ5S2V5dAAzTG9y
+Zy9hcGFjaGUvc2VhdHVubmVsL2FwaS90YWJsZS9jYXRhbG9nL1ByaW1hcnlLZXk7eHEAfgAYc3EA
+fgAaAAAAAXcEAAAAAXQAAmlkeHNxAH4AGgAAAAF3BAAAAAFzcgA1b3JnLmFwYWNoZS5zZWF0dW5u
+ZWwuYXBpLnRhYmxlLmNhdGFsb2cuUGh5c2ljYWxDb2x1bW4AAAAAAAAAAQIAAHhyAC1vcmcuYXBh
+Y2hlLnNlYXR1bm5lbC5hcGkudGFibGUuY2F0YWxvZy5Db2x1bW7//////////wIADloACmlzVW5z
+aWduZWRaAAppc1plcm9GaWxsWgAIbnVsbGFibGVMAAZiaXRMZW50ABBMamF2YS9sYW5nL0xvbmc7
+TAAMY29sdW1uTGVuZ3RocQB+AC1MAAdjb21tZW50cQB+AA9MAAhkYXRhVHlwZXQAN0xvcmcvYXBh
+Y2hlL3NlYXR1bm5lbC9hcGkvdGFibGUvdHlwZS9TZWFUdW5uZWxEYXRhVHlwZTtMAAxkZWZhdWx0
+VmFsdWV0ABJMamF2YS9sYW5nL09iamVjdDtMABBsb25nQ29sdW1uTGVuZ3RocQB+AC1MAARuYW1l
+cQB+AA9MAAdvcHRpb25zcQB+ABFMAAVzY2FsZXQAE0xqYXZhL2xhbmcvSW50ZWdlcjtMAAhzaW5r
+VHlwZXEAfgAPTAAKc291cmNlVHlwZXEAfgAPeHAAAAFzcgAOamF2YS5sYW5nLkxvbmc7i+SQzI8j
+3wIAAUoABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAAAAAABxAH4A
+NHBzcgAtb3JnLmFwYWNoZS5zZWF0dW5uZWwuYXBpLnRhYmxlLnR5cGUuQmFzaWNUeXBlAAAAAAAA
+AAICAAJMAAdzcWxUeXBldAAtTG9yZy9hcGFjaGUvc2VhdHVubmVsL2FwaS90YWJsZS90eXBlL1Nx
+bFR5cGU7TAAJdHlwZUNsYXNzdAARTGphdmEvbGFuZy9DbGFzczt4cH5yACtvcmcuYXBhY2hlLnNl
+YXR1bm5lbC5hcGkudGFibGUudHlwZS5TcWxUeXBlAAAAAAAAAAASAAB4cgAOamF2YS5sYW5nLkVu
+dW0AAAAAAAAAABIAAHhwdAADSU5UdnIAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2
+YWx1ZXhxAH4AM3BxAH4ANHEAfgApcHBwcHhzcQB+ABoAAAAAdwQAAAAAeHBwfnIANG9yZy5hcGFj
+aGUuc2VhdHVubmVsLnRyYW5zZm9ybS5jb21tb24uRXJyb3JIYW5kbGVXYXkAAAAAAAAAABIAAHhx
+AH4AOnQABEZBSUxxAH4AFHNxAH4ACHEAfgAUcHEAfgBBcQB+ABQ=
\ No newline at end of file