This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/dev/pr-12990-7b686d8387557ae4d90de35176d6dcafdc0e1fff in repository https://gitbox.apache.org/repos/asf/seatunnel.git
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
