From 0696d75b2e0c51ab035968fc5d6d43ff08ad3f05 Mon Sep 17 00:00:00 2001 From: kevin-m-knight-gs Date: Thu, 6 Feb 2025 14:31:03 -0500 Subject: [PATCH] Add file path provider --- .../compiler/file/FilePathProvider.java | 160 +++++++++ .../file/FilePathProviderExtension.java | 42 +++ .../file/v1/FilePathProviderExtensionV1.java | 108 ++++++ ...on.compiler.file.FilePathProviderExtension | 1 + ...AbstractFilePathProviderExtensionTest.java | 318 ++++++++++++++++++ .../v1/TestFilePathProviderExtensionV1.java | 53 +++ 6 files changed, 682 insertions(+) create mode 100644 legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/FilePathProvider.java create mode 100644 legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/FilePathProviderExtension.java create mode 100644 legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/v1/FilePathProviderExtensionV1.java create mode 100644 legend-pure-core/legend-pure-m3-core/src/main/resources/META-INF/services/org.finos.legend.pure.m3.serialization.compiler.file.FilePathProviderExtension create mode 100644 legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/serialization/compiler/file/AbstractFilePathProviderExtensionTest.java create mode 100644 legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/serialization/compiler/file/v1/TestFilePathProviderExtensionV1.java diff --git a/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/FilePathProvider.java b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/FilePathProvider.java new file mode 100644 index 0000000000..8fe39cc1a3 --- /dev/null +++ b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/FilePathProvider.java @@ -0,0 +1,160 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed 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.finos.legend.pure.m3.serialization.compiler.file; + +import org.finos.legend.pure.m3.serialization.compiler.ExtensibleSerializer; + +import java.nio.file.FileSystems; +import java.util.Arrays; + +public class FilePathProvider extends ExtensibleSerializer +{ + private FilePathProvider(Iterable extensions, int defaultVersion) + { + super(extensions, defaultVersion); + } + + public String getElementFilePath(String elementPath) + { + return getElementFilePath(elementPath, null); + } + + public String getElementFilePath(String elementPath, String fsSeparator) + { + return getElementFilePath(elementPath, fsSeparator, getDefaultExtension()); + } + + public String getElementFilePath(String elementPath, int version) + { + return getElementFilePath(elementPath, null, version); + } + + public String getElementFilePath(String elementPath, String fsSeparator, int version) + { + return getElementFilePath(elementPath, fsSeparator, getExtension(version)); + } + + private String getElementFilePath(String elementPath, String fsSeparator, FilePathProviderExtension extension) + { + return extension.getElementFilePath( + validateNonEmpty(elementPath, "element path"), + resolveFSSeparator(fsSeparator)); + } + + public String getModuleMetadataFilePath(String moduleName) + { + return getModuleMetadataFilePath(moduleName, null); + } + + public String getModuleMetadataFilePath(String moduleName, String fsSeparator) + { + return getModuleMetadataFilePath(moduleName, fsSeparator, getDefaultExtension()); + } + + public String getModuleMetadataFilePath(String moduleName, int version) + { + return getModuleMetadataFilePath(moduleName, null, version); + } + + public String getModuleMetadataFilePath(String moduleName, String fsSeparator, int version) + { + return getModuleMetadataFilePath(moduleName, fsSeparator, getExtension(version)); + } + + private String getModuleMetadataFilePath(String moduleName, String fsSeparator, FilePathProviderExtension extension) + { + return extension.getModuleMetadataFilePath( + validateNonEmpty(moduleName, "module name"), + resolveFSSeparator(fsSeparator)); + } + + private static String validateNonEmpty(String string, String description) + { + if ((string == null) || string.isEmpty()) + { + throw new IllegalArgumentException(description + " may not be null or empty"); + } + return string; + } + + private static String resolveFSSeparator(String fsSeparator) + { + return (fsSeparator == null) ? getDefaultFSSeparator() : fsSeparator; + } + + private static String getDefaultFSSeparator() + { + return FileSystems.getDefault().getSeparator(); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder extends ExtensibleSerializer.AbstractBuilder + { + private Builder() + { + } + + public Builder withExtension(FilePathProviderExtension extension) + { + addExtension(extension); + return this; + } + + public Builder withExtensions(Iterable extensions) + { + addExtensions(extensions); + return this; + } + + public Builder withExtensions(FilePathProviderExtension... extensions) + { + return withExtensions(Arrays.asList(extensions)); + } + + public Builder withLoadedExtensions(ClassLoader classLoader) + { + loadExtensions(classLoader); + return this; + } + + public Builder withLoadedExtensions() + { + loadExtensions(); + return this; + } + + public Builder withDefaultVersion(int defaultVersion) + { + setDefaultVersion(defaultVersion); + return this; + } + + @Override + protected FilePathProvider build(Iterable extensions, int defaultVersion) + { + return new FilePathProvider(extensions, defaultVersion); + } + + @Override + protected Class getExtensionClass() + { + return FilePathProviderExtension.class; + } + } +} diff --git a/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/FilePathProviderExtension.java b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/FilePathProviderExtension.java new file mode 100644 index 0000000000..41c166f26b --- /dev/null +++ b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/FilePathProviderExtension.java @@ -0,0 +1,42 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed 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.finos.legend.pure.m3.serialization.compiler.file; + +import org.finos.legend.pure.m3.serialization.compiler.SerializerExtension; + +public interface FilePathProviderExtension extends SerializerExtension +{ + /** + * Get the relative file path for the binary file for the given element. This should be a relative file path, and + * must not start with the path separator. It should never be null or empty. Each name in the path should be no + * longer than 255 bytes when encoded in UTF-16. + * + * @param elementPath concrete element path + * @param fsSeparator filesystem path separator + * @return relative file path + */ + String getElementFilePath(String elementPath, String fsSeparator); + + /** + * Get the relative file path for the metadata file for the given module. This should be a relative file path, and + * must not start with the path separator. Tt should never be null or empty. Each name in the path should be no + * longer than 255 bytes when encoded in UTF-16. + * + * @param moduleName module name + * @param fsSeparator filesystem path separator + * @return relative file path + */ + String getModuleMetadataFilePath(String moduleName, String fsSeparator); +} diff --git a/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/v1/FilePathProviderExtensionV1.java b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/v1/FilePathProviderExtensionV1.java new file mode 100644 index 0000000000..62c3221592 --- /dev/null +++ b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/v1/FilePathProviderExtensionV1.java @@ -0,0 +1,108 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed 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.finos.legend.pure.m3.serialization.compiler.file.v1; + +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.list.ImmutableList; +import org.finos.legend.pure.m3.navigation.M3Paths; +import org.finos.legend.pure.m3.navigation.PackageableElement.PackageableElement; +import org.finos.legend.pure.m3.serialization.compiler.file.FilePathProviderExtension; + +public class FilePathProviderExtensionV1 implements FilePathProviderExtension +{ + // UTF-16 uses 2 bytes per Java char, possibly plus a byte order marker of 2 bytes + // Since the name must be <= 255 bytes in UTF-16, 126 is the length limit for names (126 * 2 + 2 = 254) + private static final int NAME_LEN_LIMIT = 126; + private static final int SUFFIX_LEN = 16; + + private static final String ELEMENT_FILE_EXTENSION = ".pelt"; + + private static final ImmutableList MODULE_FILE_DIR = Lists.immutable.with("legend", "pure", "module"); + private static final String MODULE_FILE_EXTENSION = ".pmf"; + + @Override + public int version() + { + return 1; + } + + @Override + public String getElementFilePath(String elementPath, String fsSeparator) + { + if (PackageableElement.DEFAULT_PATH_SEPARATOR.equals(elementPath)) + { + return M3Paths.Root + ELEMENT_FILE_EXTENSION; + } + + StringBuilder builder = new StringBuilder(elementPath.length() + ELEMENT_FILE_EXTENSION.length()); + int start = 0; + int sepLen = PackageableElement.DEFAULT_PATH_SEPARATOR.length(); + int end; + while ((end = elementPath.indexOf(PackageableElement.DEFAULT_PATH_SEPARATOR, start)) != -1) + { + appendName(builder, elementPath, start, end, null).append(fsSeparator); + start = end + sepLen; + } + + return appendName(builder, elementPath, start, elementPath.length(), ELEMENT_FILE_EXTENSION).toString(); + } + + @Override + public String getModuleMetadataFilePath(String moduleName, String fsSeparator) + { + StringBuilder builder = new StringBuilder(moduleName.length() + MODULE_FILE_EXTENSION.length() + (MODULE_FILE_DIR.size() * fsSeparator.length()) + 16); + MODULE_FILE_DIR.forEach(name -> builder.append(name).append(fsSeparator)); + return appendName(builder, moduleName, 0, moduleName.length(), MODULE_FILE_EXTENSION).toString(); + } + + private static StringBuilder appendName(StringBuilder builder, String string, int start, int end, String extension) + { + int extLen = (extension == null) ? 0 : extension.length(); + int len = (end - start) + extLen; + if (len < NAME_LEN_LIMIT) + { + // append the name, possibly plus extension, as it's below the limit + builder.append(string, start, end); + return (extension == null) ? builder : builder.append(extension); + } + + // if the name is too long, append an initial segment plus a fixed length suffix computed from the overage + int overageStart = start + NAME_LEN_LIMIT - SUFFIX_LEN - extLen; + if (Character.isLowSurrogate(string.charAt(overageStart)) && Character.isHighSurrogate(string.charAt(overageStart - 1))) + { + // avoid splitting the string in the middle of a supplementary pair + overageStart--; + } + builder.ensureCapacity(builder.length() + (overageStart - start) + SUFFIX_LEN + extLen); + builder.append(string, start, overageStart); + String suffix = getOverageSuffix(string, overageStart, end); + for (int i = suffix.length(); i < SUFFIX_LEN; i++) + { + builder.append('0'); + } + builder.append(suffix); + return (extension == null) ? builder : builder.append(extension); + } + + private static String getOverageSuffix(String string, int start, int end) + { + long value = 0; + for (int i = start, cp; i < end; i += Character.charCount(cp)) + { + value = (31 * value) + (cp = string.codePointAt(i)); + } + return Long.toHexString(value); + } +} diff --git a/legend-pure-core/legend-pure-m3-core/src/main/resources/META-INF/services/org.finos.legend.pure.m3.serialization.compiler.file.FilePathProviderExtension b/legend-pure-core/legend-pure-m3-core/src/main/resources/META-INF/services/org.finos.legend.pure.m3.serialization.compiler.file.FilePathProviderExtension new file mode 100644 index 0000000000..87dbd5e47f --- /dev/null +++ b/legend-pure-core/legend-pure-m3-core/src/main/resources/META-INF/services/org.finos.legend.pure.m3.serialization.compiler.file.FilePathProviderExtension @@ -0,0 +1 @@ +org.finos.legend.pure.m3.serialization.compiler.file.v1.FilePathProviderExtensionV1 diff --git a/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/serialization/compiler/file/AbstractFilePathProviderExtensionTest.java b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/serialization/compiler/file/AbstractFilePathProviderExtensionTest.java new file mode 100644 index 0000000000..c3346b8220 --- /dev/null +++ b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/serialization/compiler/file/AbstractFilePathProviderExtensionTest.java @@ -0,0 +1,318 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed 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.finos.legend.pure.m3.serialization.compiler.file; + +import org.eclipse.collections.api.factory.Maps; +import org.eclipse.collections.api.list.ListIterable; +import org.eclipse.collections.api.list.primitive.MutableIntList; +import org.eclipse.collections.api.map.MapIterable; +import org.eclipse.collections.api.map.MutableMap; +import org.eclipse.collections.impl.factory.primitive.IntLists; +import org.finos.legend.pure.m3.navigation.PackageableElement.PackageableElement; +import org.finos.legend.pure.m3.serialization.compiler.reference.AbstractReferenceTest; +import org.finos.legend.pure.m3.tools.GraphTools; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +public abstract class AbstractFilePathProviderExtensionTest extends AbstractReferenceTest +{ + protected FilePathProviderExtension extension; + protected FilePathProvider serializer; + + @Before + public void setUpExtension() + { + this.extension = getExtension(); + this.serializer = FilePathProvider.builder().withExtension(this.extension).build(); + } + + @Test + public void testVersions() + { + int expectedVersion = this.extension.version(); + + Assert.assertEquals(expectedVersion, this.serializer.getDefaultVersion()); + Assert.assertTrue(this.serializer.isVersionAvailable(expectedVersion)); + + MutableIntList versions = IntLists.mutable.empty(); + this.serializer.forEachVersion(versions::add); + Assert.assertEquals(IntLists.mutable.with(expectedVersion), versions); + } + + @Test + public void testAllElementPaths() + { + MapIterable patternBySeparator = buildPatternMap(getExpectedElementPrefixDirs(), getExpectedElementFilenameExtension()); + GraphTools.getTopLevelAndPackagedElements(processorSupport).forEach(element -> + { + String path = PackageableElement.getUserPathForPackageableElement(element); + forEachFSSeparator(fsSeparator -> + { + String filePath = this.extension.getElementFilePath(path, fsSeparator); + Assert.assertNotNull(path, filePath); + Pattern pattern = patternBySeparator.get(fsSeparator); + if (!pattern.matcher(filePath).matches()) + { + Assert.fail("File path does not match the expected pattern\n\telement path:" + path + "\n\tfile path: " + filePath + "\n\tpattern: " + pattern.pattern()); + } + int index = findInvalidName(filePath, fsSeparator); + if (index > -1) + { + Assert.fail("File path exceeds the file name size limit at index " + index + "\n\telement name:" + path + "\n\tfile path: " + filePath); + } + }); + }); + } + + @Test + public void testAllModulePaths() + { + MapIterable patternBySeparator = buildPatternMap(getExpectedModuleMetadataPrefixDirs(), getExpectedModuleMetadataFilenameExtension()); + runtime.getCodeStorage().getAllRepositories().forEach(module -> + { + String moduleName = module.getName(); + forEachFSSeparator(fsSeparator -> + { + String filePath = this.extension.getModuleMetadataFilePath(moduleName, fsSeparator); + Assert.assertNotNull(moduleName, filePath); + Pattern pattern = patternBySeparator.get(fsSeparator); + if (!pattern.matcher(filePath).matches()) + { + Assert.fail("File path does not match the expected pattern\n\tmodule name:" + moduleName + "\n\tfile path: " + filePath + "\n\tpattern: " + pattern.pattern()); + } + int index = findInvalidName(filePath, fsSeparator); + if (index > -1) + { + Assert.fail("File path exceeds the file name size limit at index " + index + "\n\tmodule name:" + moduleName + "\n\tfile path: " + filePath); + } + }); + }); + } + + @Test + public void testUnicodeInElementPaths() + { + String[] unicodeElementPaths = { + "test::model::My\uD808\uDC3BClass", + "test::model::\uD808\uDC00\uD808\uDC17::MyClass", + "test::model::\uD808\uDC00\uD808\uDC17::My\uD808\uDC3BClass", + "\uD801\uDE00\uD801\uDE01\uD801\uDE02::\uD801\uDE03\uD801\uDE04\uD801\uDE05\uD801\uDE06::\uD801\uDE07\uD801\uDE08\uD801\uDE09\uD801\uDE0A::" + + "\uD801\uDE0B\uD801\uDE0C\uD801\uDE0D\uD801\uDE0E\uD801\uDE0F\uD801\uDE10\uD801\uDE11\uD801\uDE12\uD801\uDE13\uD801\uDE14\uD801\uDE15::" + + "\uD801\uDE16\uD801\uDE17\uD801\uDE18\uD801\uDE19\uD801\uDE1A\uD801\uDE1B\uD801\uDE1C\uD801\uDE1D\uD801\uDE1E\uD801\uDE1F\uD801\uDE20::" + + "\uD801\uDE21\uD801\uDE22\uD801\uDE23\uD801\uDE24\uD801\uDE25\uD801\uDE26\uD801\uDE27\uD801\uDE28\uD801\uDE29\uD801\uDE2A\uD801\uDE2B::" + + "\uD801\uDE2C\uD801\uDE2D\uD801\uDE2E\uD801\uDE2F" + }; + MapIterable patternBySeparator = buildPatternMap(getExpectedElementPrefixDirs(), getExpectedElementFilenameExtension()); + forEachFSSeparator(fsSeparator -> + { + Pattern pattern = patternBySeparator.get(fsSeparator); + for (String path : unicodeElementPaths) + { + String filePath = this.extension.getElementFilePath(path, fsSeparator); + Assert.assertNotNull(path, filePath); + if (!pattern.matcher(filePath).matches()) + { + Assert.fail("File path does not match the expected pattern\n\telement path:" + path + "\n\tfile path: " + filePath + "\n\tpattern: " + pattern.pattern()); + } + int index = findInvalidName(filePath, fsSeparator); + if (index > -1) + { + Assert.fail("File path exceeds the file name size limit at index " + index + ": " + filePath); + } + } + }); + } + + @Test + public void testUnicodeInModuleNames() + { + String[] unicodeModuleNames = { + "mod_\uD802\uDF00\uD802\uDF01\uD802\uDF02\uD802\uDF03_\uD802\uDF04\uD802\uDF05\uD802\uDF06\uD802\uDF07_\uD802\uDF08\uD802\uDF09\uD802\uDF0A_mod", + "\uD802\uDF0B\uD802\uDF0C\uD802\uDF0D_\uD802\uDF0E\uD802\uDF0F\uD802\uDF10\uD802\uDF11\uD802\uDF12\uD802\uDF13_\uD802\uDF14\uD802\uDF15", + "\uD802\uDF16\uD802\uDF17\uD802\uDF18\uD802\uDF19\uD802\uDF1A\uD802\uDF1B\uD802\uDF1C\uD802\uDF1D\uD802\uDF1E\uD802\uDF1F\uD802\uDF20", + "\uD802\uDF21\uD802\uDF22\uD802\uDF23\uD802\uDF24\uD802\uDF25\uD802\uDF26\uD802\uDF27\uD802\uDF28\uD802\uDF29\uD802\uDF2A\uD802\uDF2B", + "a\uD802\uDF2C\uD802\uDF2D\uD802\uDF2E\uD802\uDF2F\uD802\uDF30\uD802\uDF31\uD802\uDF32\uD802\uDF33\uD802\uDF34\uD802\uDF35z" + }; + MapIterable patternBySeparator = buildPatternMap(getExpectedModuleMetadataPrefixDirs(), getExpectedModuleMetadataFilenameExtension()); + forEachFSSeparator(fsSeparator -> + { + Pattern pattern = patternBySeparator.get(fsSeparator); + for (String moduleName : unicodeModuleNames) + { + String filePath = this.extension.getModuleMetadataFilePath(moduleName, fsSeparator); + Assert.assertNotNull(moduleName, filePath); + if (!pattern.matcher(filePath).matches()) + { + Assert.fail("File path does not match the expected pattern\n\tmodule name:" + moduleName + "\n\tfile path: " + filePath + "\n\tpattern: " + pattern.pattern()); + } + int index = findInvalidName(filePath, fsSeparator); + if (index > -1) + { + Assert.fail("File path exceeds the file name size limit at index " + index + ": " + filePath); + } + } + }); + } + + @Test + public void testVeryLongElementPaths() + { + String[] longElementPaths = { + "test::model::ClassWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName", + "test::model::ClassWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName", + "test::model::ClassWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName", + "test::model::ClassWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName", + "test::model::ClassWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVery" + + "VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVery" + + "VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName", + "test::model::packageWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName::MyClass", + "test::model::packageWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVery" + + "VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVery" + + "VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName::MyClass", + "test::model::ClassWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVery\uD808\uDC00\uD808\uDC17ryLongName", + "test::model::ClassWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVery\uD808\uDC00\uD808\uDC17ryVeryVeryLongName", + "test::model::ClassWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryV\uD808\uDC00\uD808\uDC17yVeryVeryLongName", + "test::model::ClassWithAVeryVeryVery" + + "\uD801\uDE00\uD801\uDE01\uD801\uDE02\uD801\uDE03\uD801\uDE04\uD801\uDE05\uD801\uDE06\uD801\uDE07\uD801\uDE08\uD801\uDE09\uD801\uDE0A" + + "\uD801\uDE0B\uD801\uDE0C\uD801\uDE0D\uD801\uDE0E\uD801\uDE0F\uD801\uDE10\uD801\uDE11\uD801\uDE12\uD801\uDE13\uD801\uDE14\uD801\uDE15" + + "\uD801\uDE16\uD801\uDE17\uD801\uDE18\uD801\uDE19\uD801\uDE1A\uD801\uDE1B\uD801\uDE1C\uD801\uDE1D\uD801\uDE1E\uD801\uDE1F\uD801\uDE20" + + "\uD801\uDE21\uD801\uDE22\uD801\uDE23\uD801\uDE24\uD801\uDE25\uD801\uDE26\uD801\uDE27\uD801\uDE28\uD801\uDE29\uD801\uDE2A\uD801\uDE2B" + + "\uD801\uDE2C\uD801\uDE2D\uD801\uDE2E\uD801\uDE2F" + + "VeryLongName", + "test::model::ClassWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVery" + + "VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVery\uD808\uDC00\uD808\uDC17" + + "ryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName" + }; + MapIterable patternBySeparator = buildPatternMap(getExpectedElementPrefixDirs(), getExpectedElementFilenameExtension()); + forEachFSSeparator(fsSeparator -> + { + Pattern pattern = patternBySeparator.get(fsSeparator); + for (String path : longElementPaths) + { + String filePath = this.extension.getElementFilePath(path, fsSeparator); + Assert.assertNotNull(path, filePath); + if (!pattern.matcher(filePath).matches()) + { + Assert.fail("File path does not match the expected pattern\n\telement path:" + path + "\n\tfile path: " + filePath + "\n\tpattern: " + pattern.pattern()); + } + int index = findInvalidName(filePath, fsSeparator); + if (index > -1) + { + Assert.fail("File path exceeds the file name size limit at index " + index + ": " + filePath); + } + } + }); + } + + @Test + public void testVeryLongModuleNames() + { + String[] longModuleNames = { + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_long_name", + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_long_name", + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_long_name", + "very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very\uD802\uDF00ery_very_very_very_very_long_name", + "very_\uD802\uDF00\uD802\uDF01\uD802\uDF02\uD802\uDF03\uD802\uDF04\uD802\uDF05\uD802\uDF06\uD802\uDF07\uD802\uDF08\uD802\uDF09\uD802\uDF0A" + + "\uD802\uDF0B\uD802\uDF0C\uD802\uDF0D\uD802\uDF0E\uD802\uDF0F\uD802\uDF10\uD802\uDF11\uD802\uDF12\uD802\uDF13\uD802\uDF14\uD802\uDF15" + + "\uD802\uDF16\uD802\uDF17\uD802\uDF18\uD802\uDF19\uD802\uDF1A\uD802\uDF1B\uD802\uDF1C\uD802\uDF1D\uD802\uDF1E\uD802\uDF1F\uD802\uDF20" + + "\uD802\uDF21\uD802\uDF22\uD802\uDF23\uD802\uDF24\uD802\uDF25\uD802\uDF26\uD802\uDF27\uD802\uDF28\uD802\uDF29\uD802\uDF2A\uD802\uDF2B" + + "\uD802\uDF2C\uD802\uDF2D\uD802\uDF2E\uD802\uDF2F\uD802\uDF30\uD802\uDF31\uD802\uDF32\uD802\uDF33\uD802\uDF34\uD802\uDF35_long_name" + }; + MapIterable patternBySeparator = buildPatternMap(getExpectedModuleMetadataPrefixDirs(), getExpectedModuleMetadataFilenameExtension()); + forEachFSSeparator(fsSeparator -> + { + Pattern pattern = patternBySeparator.get(fsSeparator); + for (String moduleName : longModuleNames) + { + String filePath = this.extension.getModuleMetadataFilePath(moduleName, fsSeparator); + Assert.assertNotNull(moduleName, filePath); + if (!pattern.matcher(filePath).matches()) + { + Assert.fail("File path does not match the expected pattern\n\tmodule name:" + moduleName + "\n\tfile path: " + filePath + "\n\tpattern: " + pattern.pattern()); + } + int index = findInvalidName(filePath, fsSeparator); + if (index > -1) + { + Assert.fail("File path exceeds the file name size limit at index " + index + ": " + filePath); + } + } + }); + } + + protected abstract ListIterable getExpectedElementPrefixDirs(); + + protected abstract String getExpectedElementFilenameExtension(); + + protected abstract ListIterable getExpectedModuleMetadataPrefixDirs(); + + protected abstract String getExpectedModuleMetadataFilenameExtension(); + + protected abstract FilePathProviderExtension getExtension(); + + private static int findInvalidName(String filePath, String fsSeparator) + { + int start = 0; + int end; + while ((end = filePath.indexOf(fsSeparator, start)) != -1) + { + if (getUTF16Len(filePath, start, end) > 255) + { + return start; + } + start = end + fsSeparator.length(); + } + return (getUTF16Len(filePath, start, filePath.length()) > 255) ? start : -1; + } + + private static int getUTF16Len(String string, int start, int end) + { + return string.substring(start, end).getBytes(StandardCharsets.UTF_16).length; + } + + private static void forEachFSSeparator(Consumer consumer) + { + consumer.accept("/"); + consumer.accept("\\"); + } + + private static MapIterable buildPatternMap(ListIterable expectedPrefixDirs, String expectedExtension) + { + MutableMap patterns = Maps.mutable.empty(); + forEachFSSeparator(sep -> patterns.put(sep, buildFilePathPattern(expectedPrefixDirs, sep, expectedExtension))); + return patterns; + } + + private static Pattern buildFilePathPattern(ListIterable prefixDirs, String separator, String ext) + { + StringBuilder builder = new StringBuilder(); + if ((prefixDirs != null) && prefixDirs.notEmpty()) + { + builder.append("\\Q"); + prefixDirs.forEach(dir -> builder.append(dir).append(separator)); + builder.append("\\E"); + } + builder.append("([\\w$]++\\Q").append(separator).append("\\E)*+[\\w$]++"); + if ((ext != null) && !ext.isEmpty()) + { + builder.append("\\Q").append(ext).append("\\E"); + } + return Pattern.compile(builder.toString(), Pattern.UNICODE_CHARACTER_CLASS); + } +} diff --git a/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/serialization/compiler/file/v1/TestFilePathProviderExtensionV1.java b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/serialization/compiler/file/v1/TestFilePathProviderExtensionV1.java new file mode 100644 index 0000000000..e24b37d614 --- /dev/null +++ b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/serialization/compiler/file/v1/TestFilePathProviderExtensionV1.java @@ -0,0 +1,53 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed 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.finos.legend.pure.m3.serialization.compiler.file.v1; + +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.list.ListIterable; +import org.finos.legend.pure.m3.serialization.compiler.file.AbstractFilePathProviderExtensionTest; +import org.finos.legend.pure.m3.serialization.compiler.file.FilePathProviderExtension; + +public class TestFilePathProviderExtensionV1 extends AbstractFilePathProviderExtensionTest +{ + @Override + protected ListIterable getExpectedElementPrefixDirs() + { + return null; + } + + @Override + protected String getExpectedElementFilenameExtension() + { + return ".pelt"; + } + + @Override + protected ListIterable getExpectedModuleMetadataPrefixDirs() + { + return Lists.immutable.with("legend", "pure", "module"); + } + + @Override + protected String getExpectedModuleMetadataFilenameExtension() + { + return ".pmf"; + } + + @Override + protected FilePathProviderExtension getExtension() + { + return new FilePathProviderExtensionV1(); + } +}