All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.finos.legend.sdlc.server.project.ProjectStructure Maven / Gradle / Ivy

There is a newer version: 0.173.2
Show newest version
// Copyright 2020 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.sdlc.server.project;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.StreamReadFeature;
import com.fasterxml.jackson.core.StreamWriteFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.factory.Maps;
import org.eclipse.collections.api.factory.Sets;
import org.eclipse.collections.api.list.MutableList;
import org.eclipse.collections.api.map.MutableMap;
import org.eclipse.collections.api.set.MutableSet;
import org.eclipse.collections.api.tuple.Pair;
import org.eclipse.collections.impl.list.mutable.ListAdapter;
import org.eclipse.collections.impl.tuple.Tuples;
import org.eclipse.collections.impl.utility.Iterate;
import org.eclipse.collections.impl.utility.LazyIterate;
import org.eclipse.collections.impl.utility.ListIterate;
import org.finos.legend.sdlc.domain.model.entity.Entity;
import org.finos.legend.sdlc.domain.model.project.ProjectType;
import org.finos.legend.sdlc.domain.model.project.configuration.ArtifactGeneration;
import org.finos.legend.sdlc.domain.model.project.configuration.ArtifactType;
import org.finos.legend.sdlc.domain.model.project.configuration.ArtifactTypeGenerationConfiguration;
import org.finos.legend.sdlc.domain.model.project.configuration.MetamodelDependency;
import org.finos.legend.sdlc.domain.model.project.configuration.PlatformConfiguration;
import org.finos.legend.sdlc.domain.model.project.configuration.ProjectConfiguration;
import org.finos.legend.sdlc.domain.model.project.configuration.ProjectDependency;
import org.finos.legend.sdlc.domain.model.project.configuration.ProjectStructureVersion;
import org.finos.legend.sdlc.domain.model.project.workspace.WorkspaceType;
import org.finos.legend.sdlc.domain.model.revision.Revision;
import org.finos.legend.sdlc.domain.model.version.VersionId;
import org.finos.legend.sdlc.serialization.EntitySerializer;
import org.finos.legend.sdlc.serialization.EntitySerializers;
import org.finos.legend.sdlc.server.domain.api.project.ProjectConfigurationUpdater;
import org.finos.legend.sdlc.server.domain.api.project.source.SourceSpecification;
import org.finos.legend.sdlc.server.domain.api.workspace.WorkspaceSpecification;
import org.finos.legend.sdlc.server.error.LegendSDLCServerException;
import org.finos.legend.sdlc.server.project.ProjectFileAccessProvider.FileAccessContext;
import org.finos.legend.sdlc.server.project.ProjectFileAccessProvider.ProjectFile;
import org.finos.legend.sdlc.server.project.ProjectFileAccessProvider.WorkspaceAccessType;
import org.finos.legend.sdlc.server.project.extension.ProjectStructureExtension;
import org.finos.legend.sdlc.server.project.extension.ProjectStructureExtensionProvider;
import org.finos.legend.sdlc.server.tools.StringTools;
import org.finos.legend.sdlc.tools.entity.EntityPaths;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.lang.model.SourceVersion;
import javax.ws.rs.core.Response.Status;

public abstract class ProjectStructure
{
    private static final JsonMapper JSON = JsonMapper.builder()
            .enable(SerializationFeature.INDENT_OUTPUT)
            .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)
            .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
            .disable(StreamWriteFeature.AUTO_CLOSE_TARGET)
            .disable(StreamReadFeature.AUTO_CLOSE_SOURCE)
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .build();

    private static final ProjectStructureFactory PROJECT_STRUCTURE_FACTORY = ProjectStructureFactory.newFactory(ProjectStructure.class.getClassLoader());

    private static final Pattern VALID_ARTIFACT_ID_PATTERN = Pattern.compile("[a-z][a-z\\d_]*+(-[a-z][a-z\\d_]*+)*+");
    private static final Pattern STRICT_VERSION_ID_PATTERN = Pattern.compile("((0|([1-9]\\d*+))\\.){2}(0|([1-9]\\d*+))");

    public static final String PROJECT_CONFIG_PATH = "/project.json";

    private static final Set FORBIDDEN_ARTIFACT_GENERATION_TYPES = Collections.unmodifiableSet(EnumSet.of(ArtifactType.entities, ArtifactType.versioned_entities, ArtifactType.service_execution));

    private final ProjectConfiguration projectConfiguration;
    private final MutableList entitySourceDirectories;

    protected ProjectStructure(ProjectConfiguration projectConfiguration, MutableList entitySourceDirectories)
    {
        this.projectConfiguration = projectConfiguration;
        this.entitySourceDirectories = entitySourceDirectories.asUnmodifiable();
    }

    protected ProjectStructure(ProjectConfiguration projectConfiguration, List entitySourceDirectories)
    {
        this(projectConfiguration, ListAdapter.adapt(entitySourceDirectories));
    }

    protected ProjectStructure(ProjectConfiguration projectConfiguration, EntitySourceDirectory entitySourceDirectory)
    {
        this(projectConfiguration, Lists.fixedSize.with(entitySourceDirectory));
    }

    protected ProjectStructure(ProjectConfiguration projectConfiguration, String entitiesDirectory, EntitySerializer entitySerializer)
    {
        this(projectConfiguration, newEntitySourceDirectory(entitiesDirectory, entitySerializer));
    }

    protected ProjectStructure(ProjectConfiguration projectConfiguration, String entitiesDirectory)
    {
        this(projectConfiguration, entitiesDirectory, EntitySerializers.getDefaultJsonSerializer());
    }

    @Override
    public String toString()
    {
        return "";
    }

    public int getVersion()
    {
        return getProjectConfiguration().getProjectStructureVersion().getVersion();
    }

    public ProjectConfiguration getProjectConfiguration()
    {
        return this.projectConfiguration;
    }

    public List getEntitySourceDirectories()
    {
        return this.entitySourceDirectories;
    }

    /**
     * Find the path for the file that represents the given entity.
     *
     * @param entityPath        entity path
     * @param fileAccessContext file access context
     * @return entity file path, if it exists
     */
    public String findEntityFile(String entityPath, FileAccessContext fileAccessContext)
    {
        for (EntitySourceDirectory sourceDirectory : this.entitySourceDirectories)
        {
            String filePath = sourceDirectory.entityPathToFilePath(entityPath);
            if (fileAccessContext.fileExists(filePath))
            {
                return filePath;
            }
        }
        return null;
    }

    /**
     * Find the first source directory where the given entity can be serialized.
     *
     * @param entity entity to serialize
     * @return source directory where the entity can be serialized
     */
    public EntitySourceDirectory findSourceDirectoryForEntity(Entity entity)
    {
        return this.entitySourceDirectories.detectWith(EntitySourceDirectory::canSerialize, entity);
    }

    /**
     * Find the first source directory where the given file is possibly an entity file.
     *
     * @param path entity file path
     * @return source directory where the file is possibly an entity file
     */
    public EntitySourceDirectory findSourceDirectoryForEntityFilePath(String path)
    {
        return this.entitySourceDirectories.detectWith(EntitySourceDirectory::isPossiblyEntityFilePath, path);
    }

    @Deprecated
    public void collectUpdateProjectConfigurationOperations(ProjectStructure oldStructure, FileAccessContext fileAccessContext, BiFunction versionFileAccessContextProvider, Consumer operationConsumer)
    {
        // retained for backward compatibility
    }

    protected void collectUpdateProjectConfigurationOperations(ProjectStructure oldStructure, FileAccessContext fileAccessContext, Consumer operationConsumer)
    {
        // Nothing by default
    }

    protected void addOrModifyFile(String path, String newContent, FileAccessContext fileAccessContext, Consumer operationConsumer)
    {
        ProjectFile file = fileAccessContext.getFile(path);
        if (file == null)
        {
            operationConsumer.accept(ProjectFileOperation.addFile(path, newContent));
        }
        else if (!newContent.equals(file.getContentAsString()))
        {
            operationConsumer.accept(ProjectFileOperation.modifyFile(path, newContent));
        }
    }

    protected void moveOrAddOrModifyFile(String oldPath, String newPath, String newContent, FileAccessContext fileAccessContext, Consumer operationConsumer)
    {
        ProjectFile oldFile = fileAccessContext.getFile(oldPath);
        ProjectFile newFile = newPath.equals(oldPath) ? oldFile : fileAccessContext.getFile(newPath);
        if (oldFile == null)
        {
            if (newFile == null)
            {
                // neither old nor new exists: add file in new location
                operationConsumer.accept(ProjectFileOperation.addFile(newPath, newContent));
            }
            else if (!newContent.equals(newFile.getContentAsString()))
            {
                // old does not exist, but new does and it doesn't have the desired content: modify new
                operationConsumer.accept(ProjectFileOperation.modifyFile(newPath, newContent));
            }
        }
        else if (newFile == null)
        {
            // old exists but new does not: move old to new
            operationConsumer.accept(ProjectFileOperation.moveFile(oldPath, newPath, newContent));
        }
        else
        {
            // both old and new exist: delete old (if it's different from new) and modify new (if it doesn't have the desired content)
            if (newFile != oldFile)
            {
                operationConsumer.accept(ProjectFileOperation.deleteFile(oldPath));
            }
            if (!newContent.equals(newFile.getContentAsString()))
            {
                operationConsumer.accept(ProjectFileOperation.modifyFile(newPath, newContent));
            }
        }
    }

    protected void deleteFileIfPresent(String path, FileAccessContext fileAccessContext, Consumer operationConsumer)
    {
        if (fileAccessContext.getFile(path) != null)
        {
            operationConsumer.accept(ProjectFileOperation.deleteFile(path));
        }
    }

    public boolean isSupportedArtifactType(ArtifactType type)
    {
        return getSupportedArtifactTypes().contains(type);
    }

    public abstract Set getSupportedArtifactTypes();

    public abstract Stream getArtifactIdsForType(ArtifactType type);

    public Stream getAllArtifactIds()
    {
        ProjectConfiguration configuration = getProjectConfiguration();
        return (configuration == null) ? Stream.empty() : Stream.of(configuration.getArtifactId());
    }

    public Stream getArtifactIds(Collection types)
    {
        if (types.isEmpty())
        {
            return Stream.empty();
        }

        Stream stream = types.stream().flatMap(this::getArtifactIdsForType);

        if (types.size() > 1)
        {
            stream = stream.distinct();
        }
        return stream;
    }

    protected List getProjectDependencies()
    {
        ProjectConfiguration configuration = getProjectConfiguration();
        List projectDependencies = (configuration == null) ? null : configuration.getProjectDependencies();
        return (projectDependencies == null) ? Collections.emptyList() : projectDependencies;
    }

    protected void validate()
    {
        ProjectConfiguration projectConfig = getProjectConfiguration();
        if (projectConfig != null)
        {
            MutableList unsupportedGenerations = Iterate.select(projectConfig.getArtifactGenerations(), g -> !isSupportedArtifactType(g.getType()), Lists.mutable.empty());
            if (unsupportedGenerations.notEmpty())
            {
                StringBuilder builder = new StringBuilder("Unsupported artifact generations: ");
                unsupportedGenerations.sortThis(Comparator.comparing(ArtifactGeneration::getName))
                        .forEachWithIndex((g, i) -> ((i == 0) ? builder : builder.append(", ")).append(g.getName()).append(" (").append(g.getType()).append(")"));
                throw new IllegalStateException(builder.toString());
            }
        }
    }

    public static int getLatestProjectStructureVersion()
    {
        return PROJECT_STRUCTURE_FACTORY.getLatestVersion();
    }

    public static ProjectStructure getProjectStructure(String projectId, SourceSpecification sourceSpecification, String revisionId, ProjectFileAccessProvider projectFileAccessor)
    {
        return getProjectStructure(projectFileAccessor.getFileAccessContext(projectId, sourceSpecification, revisionId));
    }

    public static ProjectStructure getProjectStructure(FileAccessContext fileAccessContext)
    {
        ProjectConfiguration config = getProjectConfiguration(fileAccessContext);
        return getProjectStructure(config);
    }

    public static ProjectStructure getProjectStructure(ProjectConfiguration projectConfiguration)
    {
        return getProjectStructure(projectConfiguration, null);
    }

    public static ProjectStructure getProjectStructure(ProjectConfiguration projectConfiguration, ProjectStructurePlatformExtensions projectStructurePlatformExtensions)
    {
        return PROJECT_STRUCTURE_FACTORY.newProjectStructure(projectConfiguration, projectStructurePlatformExtensions);
    }

    // for backward compatibility
    @Deprecated
    public static ProjectConfiguration getProjectConfiguration(String projectId, String workspaceId, String revisionId, ProjectFileAccessProvider projectFileAccessProvider, WorkspaceAccessType workspaceAccessType)
    {
        return getProjectConfiguration(projectId, SourceSpecification.newSourceSpecification(workspaceId, WorkspaceType.USER, workspaceAccessType), revisionId, projectFileAccessProvider);
    }

    public static ProjectConfiguration getProjectConfiguration(String projectId, SourceSpecification sourceSpecification, String revisionId, ProjectFileAccessProvider projectFileAccessProvider)
    {
        return getProjectConfiguration(projectFileAccessProvider.getFileAccessContext(projectId, sourceSpecification, revisionId));
    }

    public static ProjectConfiguration getProjectConfiguration(String projectId, VersionId versionId, ProjectFileAccessProvider projectFileAccessProvider)
    {
        return getProjectConfiguration(projectFileAccessProvider.getFileAccessContext(projectId, versionId));
    }

    public static ProjectConfiguration getProjectConfiguration(FileAccessContext fileAccessContext)
    {
        ProjectFile configFile = getProjectConfigurationFile(fileAccessContext);
        return (configFile == null) ? null : readProjectConfiguration(configFile);
    }

    public static ProjectConfiguration getDefaultProjectConfiguration(String projectId)
    {
        return SimpleProjectConfiguration.newConfiguration(projectId, ProjectStructureVersion.newProjectStructureVersion(0), null, null, null, null, null);
    }

    public static boolean isValidProjectType(ProjectType projectType)
    {
        return projectType == ProjectType.MANAGED || projectType == ProjectType.EMBEDDED;
    }

    public static boolean isValidGroupId(String groupId)
    {
        return (groupId != null) && !groupId.isEmpty() && SourceVersion.isName(groupId);
    }

    public static boolean isValidArtifactId(String artifactId)
    {
        return (artifactId != null) && !artifactId.isEmpty() && VALID_ARTIFACT_ID_PATTERN.matcher(artifactId).matches();
    }

    public static boolean isProperProjectDependency(ProjectDependency dependency)
    {
        return (dependency != null) && isValidProjectDependencyProjectId(dependency.getProjectId()) && isStrictVersionId(dependency.getVersionId());
    }

    private static boolean isValidProjectDependencyProjectId(String projectId)
    {
        return (projectId != null) && projectId.codePoints().anyMatch(c -> !Character.isWhitespace(c));
    }

    public static boolean isStrictVersionId(String versionId)
    {
        return (versionId != null) && (versionId.length() >= 5) && STRICT_VERSION_ID_PATTERN.matcher(versionId).matches();
    }

    private static Revision updateProjectConfiguration(UpdateBuilder updateBuilder, boolean requireRevisionId)
    {
        String projectId = updateBuilder.getProjectId();
        String workspaceId = updateBuilder.getSourceSpecification().getWorkspaceId();
        VersionId patchReleaseVersionId = updateBuilder.getSourceSpecification().getPatchReleaseVersionId();
        WorkspaceType workspaceType = updateBuilder.getSourceSpecification().getWorkspaceType();
        WorkspaceAccessType workspaceAccessType = updateBuilder.getSourceSpecification().getWorkspaceAccessType();
        ProjectFileAccessProvider projectFileAccessProvider = updateBuilder.getProjectFileAccessProvider();

        String revisionId;
        if (updateBuilder.getRevisionId() == null)
        {
            // if revisionId not specified, get the current revision
            Revision currentRevision = projectFileAccessProvider.getRevisionAccessContext(projectId, workspaceId, workspaceType, workspaceAccessType).getCurrentRevision();
            if (currentRevision != null)
            {
                revisionId = currentRevision.getId();
            }
            else if (requireRevisionId)
            {
                StringBuilder builder = new StringBuilder("Could not find current revision for ");
                if (workspaceId != null)
                {
                    builder.append(workspaceType.getLabel()).append(" ").append(workspaceAccessType.getLabel()).append(" ").append(workspaceId).append("in ");
                }
                builder.append("project ").append(projectId).append(": it may be corrupt");
                throw new LegendSDLCServerException(builder.toString());
            }
            else
            {
                revisionId = null;
            }
        }
        else
        {
            revisionId = updateBuilder.getRevisionId();
        }

        FileAccessContext fileAccessContext = CachingFileAccessContext.wrap(projectFileAccessProvider.getFileAccessContext(projectId, SourceSpecification.newSourceSpecification(workspaceId, workspaceType, workspaceAccessType, patchReleaseVersionId), revisionId));

        ProjectFile configFile = getProjectConfigurationFile(fileAccessContext);
        ProjectConfiguration currentConfig = (configFile == null) ? getDefaultProjectConfiguration(projectId) : readProjectConfiguration(configFile);
        ProjectType oldProjectType = currentConfig.getProjectType();
        // Upgrade old project types
        if (!ProjectStructure.isValidProjectType(oldProjectType))
        {
            oldProjectType = ProjectType.MANAGED;
            if (updateBuilder.configUpdater.getProjectType() == null)
            {
                updateBuilder.configUpdater.setProjectType(oldProjectType);
            }
        }
        ProjectType newProjectType = updateBuilder.configUpdater.getProjectType() == null ? oldProjectType : updateBuilder.configUpdater.getProjectType();
        // For MANAGED projects
        if (updateBuilder.projectStructureExtensionProvider != null && newProjectType != ProjectType.EMBEDDED)
        {
            // converted from EMBEDDED set version
            if (oldProjectType == ProjectType.EMBEDDED && updateBuilder.configUpdater.getProjectStructureVersion() == null)
            {
                updateBuilder.configUpdater.setProjectStructureVersion(currentConfig.getProjectStructureVersion().getVersion());
            }
            // if extension version was not specified, use the latest one for given version
            if (updateBuilder.configUpdater.getProjectStructureVersion() != null && updateBuilder.configUpdater.getProjectStructureExtensionVersion() == null)
            {
                updateBuilder.configUpdater.setProjectStructureExtensionVersion(updateBuilder.projectStructureExtensionProvider.getLatestVersionForProjectStructureVersion(updateBuilder.configUpdater.getProjectStructureVersion()));
            }
        }
        ProjectConfiguration newConfig = updateLegacyDependencies(updateBuilder.getProjectConfigurationUpdater().update(currentConfig), projectFileAccessProvider);

        validateProjectConfiguration(newConfig);

        ProjectStructureVersion oldProjectVersion = currentConfig.getProjectStructureVersion();
        // prevent extensions on non-managed projects
        if (newConfig.getProjectType() == ProjectType.EMBEDDED)
        {
            if (newConfig.getProjectStructureVersion().getExtensionVersion() != null)
            {
                throw new LegendSDLCServerException("Cannot set extensions on project " + projectId + " with " + newConfig.getProjectType() + " type", Status.BAD_REQUEST);
            }
            oldProjectVersion = ProjectStructureVersion.newProjectStructureVersion(oldProjectVersion.getVersion());
        }
        // prevent downgrading project
        if (newConfig.getProjectStructureVersion().compareTo(oldProjectVersion) < 0)
        {
            throw new LegendSDLCServerException("Cannot change project " + projectId + " from project structure version " + currentConfig.getProjectStructureVersion().toVersionString() + " to version " + newConfig.getProjectStructureVersion().toVersionString(), Status.BAD_REQUEST);
        }

        // Serialize new configuration and check if it differs from the old
        byte[] serializedNewConfig = serializeProjectConfiguration(newConfig);
        if ((configFile != null) && Arrays.equals(serializedNewConfig, configFile.getContentAsBytes()))
        {
            // new configuration file is the same as the old
            return null;
        }

        List operations = Lists.mutable.empty();
        operations.add((configFile == null) ? ProjectFileOperation.addFile(PROJECT_CONFIG_PATH, serializedNewConfig) : ProjectFileOperation.modifyFile(PROJECT_CONFIG_PATH, serializedNewConfig));

        ProjectStructure currentProjectStructure = getProjectStructure(currentConfig, updateBuilder.getProjectStructurePlatformExtensions());
        ProjectStructure newProjectStructure = getProjectStructure(newConfig, updateBuilder.getProjectStructurePlatformExtensions());

        // Move or re-serialize entities if necessary
        List currentEntityDirectories = currentProjectStructure.getEntitySourceDirectories();
        List newEntityDirectories = newProjectStructure.getEntitySourceDirectories();
        if (!currentEntityDirectories.equals(newEntityDirectories))
        {
            currentEntityDirectories.forEach(currentSourceDirectory -> fileAccessContext.getFilesInDirectory(currentSourceDirectory.getDirectory()).forEach(file ->
            {
                String currentPath = file.getPath();
                if (currentSourceDirectory.isPossiblyEntityFilePath(currentPath))
                {
                    byte[] currentBytes = file.getContentAsBytes();
                    Entity entity;
                    try
                    {
                        entity = currentSourceDirectory.deserialize(currentBytes);
                    }
                    catch (Exception e)
                    {
                        StringBuilder builder = new StringBuilder("Error deserializing entity from file \"").append(currentPath).append('"');
                        StringTools.appendThrowableMessageIfPresent(builder, e);
                        throw new LegendSDLCServerException(builder.toString(), e);
                    }
                    EntitySourceDirectory newSourceDirectory = Iterate.detectWith(newEntityDirectories, EntitySourceDirectory::canSerialize, entity);
                    if (newSourceDirectory == null)
                    {
                        throw new LegendSDLCServerException("Could not find a new source directory for entity " + entity.getPath() + ", currently in " + currentPath);
                    }
                    if (!currentSourceDirectory.equals(newSourceDirectory))
                    {
                        String newPath = newSourceDirectory.entityPathToFilePath(entity.getPath());
                        byte[] newBytes = newSourceDirectory.serializeToBytes(entity);
                        if (!newPath.equals(currentPath))
                        {
                            operations.add(ProjectFileOperation.moveFile(currentPath, newPath, newBytes));
                        }
                        else if (!Arrays.equals(currentBytes, newBytes))
                        {
                            operations.add(ProjectFileOperation.modifyFile(currentPath, newBytes));
                        }
                    }
                }
            }));
        }

        // Collect any further update operations
        if (newConfig.getProjectType() != ProjectType.EMBEDDED)
        {
            newProjectStructure.collectUpdateProjectConfigurationOperations(currentProjectStructure, fileAccessContext, operations::add);
        }

        // Call legacy method
        newProjectStructure.collectUpdateProjectConfigurationOperations(currentProjectStructure, fileAccessContext, (x, y) ->
        {
            throw new UnsupportedOperationException();
        }, operations::add);

        // Collect update operations from any project structure extension
        if (newConfig.getProjectStructureVersion().getExtensionVersion() != null)
        {
            ProjectStructureExtensionProvider extensionProvider = updateBuilder.getProjectStructureExtensionProvider();
            if (extensionProvider != null)
            {
                ProjectStructureExtension projectStructureExtension = extensionProvider.getProjectStructureExtension(newConfig.getProjectStructureVersion().getVersion(), newConfig.getProjectStructureVersion().getExtensionVersion());
                projectStructureExtension.collectUpdateProjectConfigurationOperations(currentConfig, newConfig, fileAccessContext, operations::add);
            }
        }

        // Submit changes
        return projectFileAccessProvider.getFileModificationContext(projectId, workspaceId, workspaceType, workspaceAccessType, revisionId).submit(updateBuilder.getMessage(), operations);
    }

    private static void validateProjectConfiguration(ProjectConfiguration config)
    {
        // Group id
        if (!isValidGroupId(config.getGroupId()))
        {
            throw new LegendSDLCServerException("Invalid groupId: " + config.getGroupId(), Status.BAD_REQUEST);
        }

        // Artifact id
        if (!isValidArtifactId(config.getArtifactId()))
        {
            throw new LegendSDLCServerException("Invalid artifactId: " + config.getArtifactId() + ". ArtifactId must follow pattern that starts with a lowercase letter and can include lowercase letters, digits, underscores, and hyphens between segments.", Status.BAD_REQUEST);
        }

        // Project type
        if (!isValidProjectType(config.getProjectType()))
        {
            throw new LegendSDLCServerException("Invalid projectType: " + config.getProjectType(), Status.BAD_REQUEST);
        }

        // Platform configurations
        if (config.getPlatformConfigurations() != null)
        {
            MutableSet platformNames = Sets.mutable.empty();
            MutableSet platformNameConflicts = LazyIterate.collect(config.getPlatformConfigurations(), PlatformConfiguration::getName).reject(platformNames::add, Sets.mutable.empty());
            if (platformNameConflicts.notEmpty())
            {
                throw new LegendSDLCServerException(platformNameConflicts.toSortedList().makeString("Platform configuration conflicts: \"", "\", \"", "\""), Status.BAD_REQUEST);
            }
        }

        // Metamodel dependencies
        validateMetamodelDependencies(config.getMetamodelDependencies());

        // Artifact generations
        validateArtifactGenerations(config.getArtifactGenerations());
    }

    private static void validateMetamodelDependencies(List metamodelDependencies)
    {
        MutableList unknownDependencies = ListIterate.reject(metamodelDependencies, ProjectStructure::isKnownMetamodel);
        if (unknownDependencies.notEmpty())
        {
            StringBuilder builder = new StringBuilder("There were issues with one or more added metamodel dependencies");
            builder.append("; unknown ").append((unknownDependencies.size() == 1) ? "dependency" : "dependencies").append(": ");
            unknownDependencies.sort(getMetamodelDependencyComparator());
            unknownDependencies.forEach(d -> d.appendDependencyString((d == unknownDependencies.get(0)) ? builder : builder.append(", ")));
            throw new LegendSDLCServerException(builder.toString(), Status.BAD_REQUEST);
        }
        validateDependencyConflicts(
                metamodelDependencies,
                MetamodelDependency::getMetamodel,
                (id, deps) -> (deps.size() > 1) ? deps.stream().collect(StringBuilder::new, (builder, dep) -> dep.appendVersionIdString(builder.append((builder.length() == 0) ? "multiple versions not allowed: " : ", ")), StringBuilder::append).toString() : null,
                "metamodels");
    }

    private static void validateArtifactGenerations(List artifactGenerations)
    {
        String initString = "There were issues with one or more added artifact generations";
        StringBuilder builder = null;

        if (Iterate.anySatisfy(artifactGenerations, g -> FORBIDDEN_ARTIFACT_GENERATION_TYPES.contains(g.getType())))
        {
            builder = new StringBuilder(initString);
            LazyIterate.collect(FORBIDDEN_ARTIFACT_GENERATION_TYPES, ArtifactType::getLabel)
                    .toSortedList()
                    .appendString(builder, ": generation types ", ", ", " are not allowed");
        }

        MutableSet artifactGenerationNames = Sets.mutable.empty();
        MutableSet artifactGenerationConflicts = LazyIterate.collect(artifactGenerations, ArtifactGeneration::getName).reject(artifactGenerationNames::add, Sets.mutable.empty());
        if (artifactGenerationConflicts.notEmpty())
        {
            if (builder == null)
            {
                builder = new StringBuilder(initString).append(": ");
            }
            else
            {
                builder.append("; ");
            }
            artifactGenerationConflicts.toSortedList().appendString(builder, "duplicate generations: \"", "\", \"", "\"");
        }

        if (builder != null)
        {
            throw new LegendSDLCServerException(builder.toString(), Status.BAD_REQUEST);
        }
    }

    protected static boolean isLegacyProjectDependency(ProjectDependency projectDependency)
    {
        return (projectDependency != null) &&
                (projectDependency.getProjectId() != null) &&
                (projectDependency.getProjectId().indexOf(':') == -1);
    }

    private static ProjectConfiguration updateLegacyDependencies(ProjectConfiguration config, ProjectFileAccessProvider projectFileAccessProvider)
    {
        MutableList legacyProjectDependencies = Iterate.select(config.getProjectDependencies(), ProjectStructure::isLegacyProjectDependency, Lists.mutable.empty());
        if (legacyProjectDependencies.isEmpty())
        {
            return config;
        }

        MutableSet projectDependencies = Sets.mutable.withAll(config.getProjectDependencies());
        MutableList unknownDependencies = Lists.mutable.empty();
        MutableList> accessExceptions = Lists.mutable.empty();
        legacyProjectDependencies.forEach(dep ->
        {
            try
            {
                ProjectConfiguration dependencyConfig = getProjectConfiguration(projectFileAccessProvider.getFileAccessContext(dep.getProjectId(), VersionId.parseVersionId(dep.getVersionId())));
                if ((dependencyConfig == null) || (dependencyConfig.getArtifactId() == null) || (dependencyConfig.getGroupId() == null))
                {
                    unknownDependencies.add(dep);
                }
                else
                {
                    projectDependencies.remove(dep);
                    projectDependencies.add(ProjectDependency.newProjectDependency(dependencyConfig.getGroupId() + ":" + dependencyConfig.getArtifactId(), dep.getVersionId()));
                }
            }
            catch (Exception e)
            {
                accessExceptions.add(Tuples.pair(dep, e));
            }
        });
        Comparator comparator = getProjectDependencyComparator();
        if (unknownDependencies.notEmpty() || accessExceptions.notEmpty())
        {
            StringBuilder builder = new StringBuilder("There were issues with one or more legacy project dependencies");
            if (unknownDependencies.notEmpty())
            {
                builder.append("; unknown ").append((unknownDependencies.size() == 1) ? "dependency" : "dependencies").append(": ");
                unknownDependencies.sortThis(comparator).forEach(d -> d.appendDependencyString((d == unknownDependencies.get(0)) ? builder : builder.append(", ")));
            }
            if (accessExceptions.notEmpty())
            {
                builder.append("; access ").append((accessExceptions.size() == 1) ? "exception" : "exceptions").append(": ");
                accessExceptions.sortThis(Comparator.comparing(Pair::getOne, comparator)).forEach(p -> p.getOne().appendDependencyString((p == accessExceptions.get(0)) ? builder : builder.append(", ")).append(" (").append(p.getTwo().getMessage()).append(')'));
            }
            throw new LegendSDLCServerException(builder.toString(), Status.BAD_REQUEST);
        }

        SimpleProjectConfiguration newConfig = new SimpleProjectConfiguration(config);
        newConfig.setProjectDependencies(projectDependencies.toSortedList(comparator));
        return newConfig;
    }

    protected static Comparator getProjectDependencyComparator()
    {
        return ProjectDependency.getDefaultComparator();
    }

    protected static Comparator getMetamodelDependencyComparator()
    {
        return MetamodelDependency.getDefaultComparator();
    }

    protected static > void validateDependencyConflicts(Collection dependencies, Function indexKeyFn, BiFunction, String> conflictFn, String description)
    {
        MutableMap> index = Maps.mutable.empty();
        dependencies.forEach(dep -> index.getIfAbsentPut(indexKeyFn.apply(dep), Sets.mutable::empty).add(dep));
        SortedMap conflictMessages = new TreeMap<>();
        index.forEachKeyValue((key, deps) ->
        {
            String conflictMessage = conflictFn.apply(key, deps);
            if (conflictMessage != null)
            {
                conflictMessages.put(key, conflictMessage);
            }
        });
        if (!conflictMessages.isEmpty())
        {
            StringBuilder builder = new StringBuilder(conflictMessages.size() * 64);
            conflictMessages.forEach((key, message) -> ((builder.length() == 0) ? builder.append("The following ").append(description).append(" have conflicts: ") : builder.append(", ")).append(key).append(" (").append(message).append(')'));
            throw new LegendSDLCServerException(builder.toString(), Status.BAD_REQUEST);
        }
    }

    private static void appendPackageablePathAsFilePath(StringBuilder builder, String packageablePath)
    {
        EntityPaths.forEachPathElement(packageablePath, elt -> builder.append('/').append(elt));
    }

    private static void appendFilePathAsPackageablePath(StringBuilder builder, String filePath, int start, int end)
    {
        int current = start;
        int next = filePath.indexOf('/', current);
        while ((next != -1) && (next < end))
        {
            builder.append(filePath, current, next).append(EntityPaths.PACKAGE_SEPARATOR);
            current = next + 1;
            next = filePath.indexOf('/', current);
        }
        builder.append(filePath, current, end);
    }

    private static ProjectFile getProjectConfigurationFile(FileAccessContext accessContext)
    {
        return accessContext.getFile(PROJECT_CONFIG_PATH);
    }

    private static byte[] serializeProjectConfiguration(ProjectConfiguration projectConfiguration)
    {
        try
        {
            return JSON.writeValueAsBytes(projectConfiguration);
        }
        catch (Exception e)
        {
            throw new RuntimeException(StringTools.appendThrowableMessageIfPresent("Error creating project configuration file", e), e);
        }
    }

    private static ProjectConfiguration readProjectConfiguration(ProjectFile file)
    {
        try (InputStream stream = file.getContentAsInputStream())
        {
            return JSON.readValue(stream, SimpleProjectConfiguration.class);
        }
        catch (Exception e)
        {
            throw new RuntimeException(StringTools.appendThrowableMessageIfPresent("Error reading project configuration", e), e);
        }
    }

    private static boolean isKnownMetamodel(MetamodelDependency metamodelDependency)
    {
        // This is a hack until we have a proper metamodel registry
        String metamodel = metamodelDependency.getMetamodel();
        int version = metamodelDependency.getVersion();
        if (metamodel == null)
        {
            return false;
        }
        switch (metamodel)
        {
            case "pure":
            {
                return (version == 0) || (version == 1);
            }
            case "lineage":
            case "service":
            case "tds":
            {
                return version == 1;
            }
            default:
            {
                return false;
            }
        }
    }

    public List getAvailableGenerationConfigurations()
    {
        // None by default
        return Collections.emptyList();
    }

    protected static EntitySourceDirectory newEntitySourceDirectory(String directory, EntitySerializer serializer)
    {
        return new EntitySourceDirectory(directory, serializer);
    }

    public static class EntitySourceDirectory
    {
        private final String directory;
        private final EntitySerializer serializer;

        private EntitySourceDirectory(String directory, EntitySerializer serializer)
        {
            this.directory = directory;
            this.serializer = serializer;
        }

        @Override
        public boolean equals(Object other)
        {
            if (this == other)
            {
                return true;
            }
            if (!(other instanceof EntitySourceDirectory))
            {
                return false;
            }
            EntitySourceDirectory that = (EntitySourceDirectory) other;
            return this.directory.equals(that.directory) && this.serializer.getName().equals(that.serializer.getName());
        }

        @Override
        public int hashCode()
        {
            return this.directory.hashCode() ^ this.serializer.getName().hashCode();
        }

        @Override
        public String toString()
        {
            return "";
        }

        // File paths

        public String getDirectory()
        {
            return this.directory;
        }

        /**
         * Return whether the given file path is possibly an entity file path. Note that this is a purely syntactic
         * check and does not imply anything about whether the file actually exists or what it contains.
         *
         * @param filePath file path
         * @return whether filePath is possibly an entity file path
         */
        public boolean isPossiblyEntityFilePath(String filePath)
        {
            return (filePath != null) && (filePath.length() > (this.directory.length() + this.serializer.getDefaultFileExtension().length() + 2)) && filePathStartsWithDirectory(filePath) && filePathHasEntityExtension(filePath);
        }

        /**
         * Return the file path corresponding to the given entity path. The slash character ('/') is used to separate
         * directories within the path. Paths will always begin with /, and will never be empty. Note that the file
         * path will be returned regardless of whether the file actually exists.
         *
         * @param entityPath entity path
         * @return corresponding file path
         */
        public String entityPathToFilePath(String entityPath)
        {
            StringBuilder builder = new StringBuilder(this.directory.length() + entityPath.length() + this.serializer.getDefaultFileExtension().length());
            builder.append(this.directory);
            appendPackageablePathAsFilePath(builder, entityPath);
            builder.append('.').append(this.serializer.getDefaultFileExtension());
            return builder.toString();
        }

        /**
         * Return the entity path that corresponds to the given file path.
         *
         * @param filePath file path
         * @return corresponding entity path
         * @throws IllegalArgumentException if filePath is not a valid file path
         */
        public String filePathToEntityPath(String filePath)
        {
            int start = this.directory.length() + 1;
            int end = filePath.length() - (this.serializer.getDefaultFileExtension().length() + 1);
            int length = end - start;
            StringBuilder builder = new StringBuilder(length + (length / 4));
            appendFilePathAsPackageablePath(builder, filePath, start, end);
            return builder.toString();
        }

        /**
         * Return the file path corresponding to the given package path. The slash character ('/') is used to separate
         * directories within the path. Paths will always begin with /, and will never be empty. Note the the file path
         * will refer to a directory and will be returned regardless of whether the directory actually exists.
         *
         * @param packagePath package path
         * @return corresponding file path
         */
        public String packagePathToFilePath(String packagePath)
        {
            StringBuilder builder = new StringBuilder(this.directory.length() + packagePath.length());
            builder.append(this.directory);
            appendPackageablePathAsFilePath(builder, packagePath);
            return builder.toString();
        }

        private boolean filePathStartsWithDirectory(String filePath)
        {
            return filePath.startsWith(this.directory) && ((filePath.length() == this.directory.length()) || (filePath.charAt(this.directory.length()) == '/'));
        }

        private boolean filePathHasEntityExtension(String filePath)
        {
            String extension = this.serializer.getDefaultFileExtension();
            return filePath.endsWith(extension) && (filePath.length() > extension.length()) && (filePath.charAt(filePath.length() - (extension.length() + 1)) == '.');
        }

        // Serialization

        public EntitySerializer getSerializer()
        {
            return this.serializer;
        }

        public boolean canSerialize(Entity entity)
        {
            return this.serializer.canSerialize(entity);
        }

        public byte[] serializeToBytes(Entity entity)
        {
            try
            {
                return this.serializer.serializeToBytes(entity);
            }
            catch (Exception e)
            {
                StringBuilder message = new StringBuilder("Error serializing entity ").append(entity.getPath());
                StringTools.appendThrowableMessageIfPresent(message, e);
                throw new LegendSDLCServerException(message.toString(), e);
            }
        }

        public Entity deserialize(ProjectFile projectFile)
        {
            try (InputStream stream = projectFile.getContentAsInputStream())
            {
                return this.serializer.deserialize(stream);
            }
            catch (Exception e)
            {
                String eMessage = e.getMessage();
                if ((e instanceof RuntimeException) && (eMessage != null) && eMessage.startsWith("Error deserializing entity "))
                {
                    throw (RuntimeException) e;
                }
                StringBuilder builder = new StringBuilder("Error deserializing entity from file ").append(projectFile.getPath());
                if (eMessage != null)
                {
                    builder.append(": ").append(eMessage);
                }
                throw new LegendSDLCServerException(builder.toString(), e);
            }
        }

        public Entity deserialize(byte[] content) throws IOException
        {
            return this.serializer.deserialize(content);
        }
    }

    public static UpdateBuilder newUpdateBuilder(ProjectFileAccessProvider projectFileAccessProvider, String projectId)
    {
        return newUpdateBuilder(projectFileAccessProvider, projectId, null);
    }

    public static UpdateBuilder newUpdateBuilder(ProjectFileAccessProvider projectFileAccessProvider, String projectId, ProjectConfigurationUpdater configUpdater)
    {
        return new UpdateBuilder(projectFileAccessProvider, projectId, configUpdater);
    }

    public static class UpdateBuilder
    {
        private final ProjectFileAccessProvider projectFileAccessProvider;
        private final String projectId;
        private SourceSpecification sourceSpecification;
        private ProjectConfigurationUpdater configUpdater;
        private String revisionId;
        private String message;
        private ProjectStructureExtensionProvider projectStructureExtensionProvider;
        private ProjectStructurePlatformExtensions projectStructurePlatformExtensions;

        private UpdateBuilder(ProjectFileAccessProvider projectFileAccessProvider, String projectId, ProjectConfigurationUpdater configUpdater)
        {
            this.projectFileAccessProvider = projectFileAccessProvider;
            this.projectId = projectId;
            this.sourceSpecification = SourceSpecification.projectSourceSpecification();
            this.configUpdater = (configUpdater == null) ? getDefaultProjectConfigurationUpdater() : configUpdater;
        }

        // Project id

        public String getProjectId()
        {
            return this.projectId;
        }

        public SourceSpecification getSourceSpecification()
        {
            return this.sourceSpecification;
        }

        // Project file access provider

        public ProjectFileAccessProvider getProjectFileAccessProvider()
        {
            return this.projectFileAccessProvider;
        }

        // Project configuration updater

        public ProjectConfigurationUpdater getProjectConfigurationUpdater()
        {
            return this.configUpdater;
        }

        public void setProjectConfigurationUpdater(ProjectConfigurationUpdater updater)
        {
            this.configUpdater = (updater == null) ? getDefaultProjectConfigurationUpdater() : updater;
        }

        public UpdateBuilder withProjectConfigurationUpdater(ProjectConfigurationUpdater updater)
        {
            setProjectConfigurationUpdater(updater);
            return this;
        }

        private ProjectConfigurationUpdater getDefaultProjectConfigurationUpdater()
        {
            return ProjectConfigurationUpdater.newUpdater().withProjectId(this.projectId);
        }

        // Source specification

        public void setSourceSpecification(SourceSpecification sourceSpec)
        {
            this.sourceSpecification = sourceSpec;
        }

        public UpdateBuilder withSourceSpecification(SourceSpecification sourceSpec)
        {
            setSourceSpecification(sourceSpec);
            return this;
        }

        public UpdateBuilder withWorkspace(WorkspaceSpecification workspaceSpec)
        {
            return withSourceSpecification(SourceSpecification.workspaceSourceSpecification(workspaceSpec));
        }

        @Deprecated
        public UpdateBuilder withWorkspace(SourceSpecification sourceSpecification)
        {
            return withSourceSpecification(sourceSpecification);
        }

        // Revision id

        public String getRevisionId()
        {
            return this.revisionId;
        }

        public void setRevisionId(String revisionId)
        {
            this.revisionId = revisionId;
        }

        public UpdateBuilder withRevisionId(String revisionId)
        {
            setRevisionId(revisionId);
            return this;
        }

        // Message

        public String getMessage()
        {
            return this.message;
        }

        public void setMessage(String message)
        {
            this.message = message;
        }

        public UpdateBuilder withMessage(String message)
        {
            setMessage(message);
            return this;
        }

        // Project structure extension provider

        public ProjectStructureExtensionProvider getProjectStructureExtensionProvider()
        {
            return this.projectStructureExtensionProvider;
        }

        public void setProjectStructureExtensionProvider(ProjectStructureExtensionProvider projectStructureExtensionProvider)
        {
            this.projectStructureExtensionProvider = projectStructureExtensionProvider;
        }

        public UpdateBuilder withProjectStructureExtensionProvider(ProjectStructureExtensionProvider projectStructureExtensionProvider)
        {
            setProjectStructureExtensionProvider(projectStructureExtensionProvider);
            return this;
        }

        // Project structure platform extensions

        public ProjectStructurePlatformExtensions getProjectStructurePlatformExtensions()
        {
            return this.projectStructurePlatformExtensions;
        }

        public void setProjectStructurePlatformExtensions(ProjectStructurePlatformExtensions projectStructurePlatformExtensions)
        {
            this.projectStructurePlatformExtensions = projectStructurePlatformExtensions;
        }

        public UpdateBuilder withProjectStructurePlatformExtensions(ProjectStructurePlatformExtensions projectStructurePlatformExtensions)
        {
            setProjectStructurePlatformExtensions(projectStructurePlatformExtensions);
            return this;
        }

        // Update

        public Revision update()
        {
            return update(true);
        }

        public Revision build()
        {
            if (this.configUpdater.getProjectStructureVersion() == null)
            {
                this.configUpdater.setProjectStructureVersion(getLatestProjectStructureVersion());
            }
            return update(false);
        }

        private Revision update(boolean requireRevisionId)
        {
            if (this.projectId != null)
            {
                if (this.configUpdater.getProjectId() == null)
                {
                    this.configUpdater.setProjectId(this.projectId);
                }
                else if (!this.projectId.equals(this.configUpdater.getProjectId()))
                {
                    throw new IllegalArgumentException("Conflicting project ids: \"" + this.projectId + "\" vs \"" + this.configUpdater.getProjectId() + "\"");
                }
            }
            else if (this.configUpdater.getProjectId() == null)
            {
                throw new IllegalArgumentException("No project id specified");
            }
            if (this.projectStructureExtensionProvider == null && this.configUpdater.getProjectStructureExtensionVersion() != null)
            {
                    throw new IllegalArgumentException("Project structure extension version specified (" + this.configUpdater.getProjectStructureExtensionVersion() + ") with no project structure extension provider");
            }
            return updateProjectConfiguration(this, requireRevisionId);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy