Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.finos.legend.sdlc.server.project.ProjectStructure Maven / Gradle / Ivy
// 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.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.Sets;
import org.eclipse.collections.api.list.MutableList;
import org.eclipse.collections.impl.list.mutable.ListAdapter;
import org.eclipse.collections.impl.utility.Iterate;
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.Dependency;
import org.finos.legend.sdlc.domain.model.project.configuration.MetamodelDependency;
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.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.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.extension.ProjectStructureExtension;
import org.finos.legend.sdlc.server.tools.StringTools;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
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.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
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)
.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-z0-9_]*+(-[a-z][a-z0-9_]*+)*+$");
public static final String PROJECT_CONFIG_PATH = "/project.json";
private static final String PACKAGE_SEPARATOR = "::";
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);
}
public abstract void collectUpdateProjectConfigurationOperations(ProjectStructure oldStructure, FileAccessContext fileAccessContext, BiFunction versionFileAccessContextProvider, Consumer operationConsumer);
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)
{
List unsupportedGenerations = projectConfig.getArtifactGenerations().stream().filter(g -> !isSupportedArtifactType(g.getType())).collect(Collectors.toList());
if (!unsupportedGenerations.isEmpty())
{
unsupportedGenerations.sort(Comparator.comparing(ArtifactGeneration::getName));
throw new IllegalStateException(unsupportedGenerations.stream().map(g -> g.getName() + " (" + g.getType() + ")").collect(Collectors.joining(", ", "Unsupported artifact generations: ", "")));
}
}
}
protected static ProjectStructure getProjectStructureForProjectDependency(ProjectDependency projectDependency, BiFunction versionFileAccessContextProvider)
{
FileAccessContext versionFileAccessContext = versionFileAccessContextProvider.apply(projectDependency.getProjectId(), projectDependency.getVersionId());
ProjectConfiguration versionConfig = ProjectStructure.getProjectConfiguration(versionFileAccessContext);
if (versionConfig == null)
{
throw new LegendSDLCServerException("Invalid version of project " + projectDependency.getProjectId() + ": " + projectDependency.getVersionId().toVersionIdString());
}
return ProjectStructure.getProjectStructure(versionConfig);
}
public static int getLatestProjectStructureVersion()
{
return PROJECT_STRUCTURE_FACTORY.getLatestVersion();
}
public static ProjectStructure getProjectStructure(String projectId, String workspaceId, String revisionId, ProjectFileAccessProvider projectFileAccessor, ProjectFileAccessProvider.WorkspaceAccessType workspaceAccessType)
{
return getProjectStructure(projectFileAccessor.getFileAccessContext(projectId, workspaceId, workspaceAccessType, revisionId));
}
public static ProjectStructure getProjectStructure(FileAccessContext fileAccessContext)
{
ProjectConfiguration config = getProjectConfiguration(fileAccessContext);
return getProjectStructure(config);
}
public static ProjectStructure getProjectStructure(ProjectConfiguration projectConfiguration)
{
return PROJECT_STRUCTURE_FACTORY.newProjectStructure(projectConfiguration);
}
public static ProjectConfiguration getProjectConfiguration(String projectId, String workspaceId, String revisionId, ProjectFileAccessProvider projectFileAccessProvider, ProjectFileAccessProvider.WorkspaceAccessType workspaceAccessType)
{
return getProjectConfiguration(projectFileAccessProvider.getFileAccessContext(projectId, workspaceId, workspaceAccessType, 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, ProjectType projectType)
{
return new SimpleProjectConfiguration(projectId, projectType, ProjectStructureVersion.newProjectStructureVersion(0), null, null, null, null, null);
}
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();
}
static Revision buildProjectStructure(ProjectConfigurationUpdateBuilder configurationUpdater)
{
if (!configurationUpdater.hasProjectStructureVersion())
{
configurationUpdater.setProjectStructureVersion(getLatestProjectStructureVersion());
}
if (configurationUpdater.hasProjectStructureExtensionVersion() && !configurationUpdater.hasProjectStructureExtensionProvider())
{
throw new IllegalArgumentException("Project structure extension version specified (" + configurationUpdater.getProjectStructureExtensionVersion() + ") with no project structure extension provider");
}
if (!configurationUpdater.hasProjectStructureExtensionVersion() && configurationUpdater.hasProjectStructureExtensionProvider())
{
configurationUpdater.setProjectStructureExtensionVersion(configurationUpdater.getProjectStructureExtensionProvider().getLatestVersionForProjectStructureVersion(configurationUpdater.getProjectStructureVersion()));
}
return updateProjectConfiguration(configurationUpdater, false);
}
static Revision updateProjectConfiguration(ProjectConfigurationUpdateBuilder configurationUpdater)
{
return updateProjectConfiguration(configurationUpdater, true);
}
private static Revision updateProjectConfiguration(ProjectConfigurationUpdateBuilder updateBuilder, boolean requireRevisionId)
{
ProjectFileAccessProvider projectFileAccessProvider = CachingProjectFileAccessProvider.wrap(updateBuilder.getProjectFileAccessProvider());
ProjectFileAccessProvider.WorkspaceAccessType workspaceAccessType = updateBuilder.getWorkspaceAccessType();
if (updateBuilder.hasGroupId() && !isValidGroupId(updateBuilder.getGroupId()))
{
throw new LegendSDLCServerException("Invalid groupId: " + updateBuilder.getGroupId(), Status.BAD_REQUEST);
}
if (updateBuilder.hasArtifactId() && !isValidArtifactId(updateBuilder.getArtifactId()))
{
throw new LegendSDLCServerException("Invalid artifactId: " + updateBuilder.getArtifactId(), Status.BAD_REQUEST);
}
ProjectType projectType = updateBuilder.getProjectType();
String projectId = updateBuilder.getProjectId();
String workspaceId = updateBuilder.getWorkspaceId();
String revisionId = updateBuilder.getRevisionId();
// if revisionId not specified, get the current revision
if (revisionId == null)
{
Revision currentRevision = projectFileAccessProvider.getRevisionAccessContext(projectId, workspaceId, 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(workspaceAccessType.getLabel()).append(" ").append(workspaceId).append("in ");
}
builder.append("project ").append(projectId).append(": it may be corrupt");
throw new LegendSDLCServerException(builder.toString());
}
}
// find out what we need to update for project structure
FileAccessContext fileAccessContext = CachingFileAccessContext.wrap(projectFileAccessProvider.getFileAccessContext(projectId, workspaceId, workspaceAccessType, revisionId));
ProjectFile configFile = getProjectConfigurationFile(fileAccessContext);
ProjectConfiguration currentConfig = (configFile == null) ? getDefaultProjectConfiguration(projectId, projectType) : readProjectConfiguration(configFile);
if (projectType != currentConfig.getProjectType())
{
throw new LegendSDLCServerException("Project type mismatch for project " + projectId + ": got " + projectType + ", found " + currentConfig.getProjectType(), Status.BAD_REQUEST);
}
boolean updateProjectStructureVersion = updateBuilder.hasProjectStructureVersion() && (updateBuilder.getProjectStructureVersion() != currentConfig.getProjectStructureVersion().getVersion());
boolean updateProjectStructureExtensionVersion = updateBuilder.hasProjectStructureExtensionVersion() && !updateBuilder.getProjectStructureExtensionVersion().equals(currentConfig.getProjectStructureVersion().getExtensionVersion());
boolean updateGroupId = updateBuilder.hasGroupId() && !updateBuilder.getGroupId().equals(currentConfig.getGroupId());
boolean updateArtifactId = updateBuilder.hasArtifactId() && !updateBuilder.getArtifactId().equals(currentConfig.getArtifactId());
// find out which dependencies we need to update
boolean updateProjectDependencies = false;
Set projectDependencies = Sets.mutable.withAll(currentConfig.getProjectDependencies());
if (updateBuilder.hasProjectDependenciesToRemove())
{
updateProjectDependencies |= projectDependencies.removeAll(updateBuilder.getProjectDependenciesToRemove());
}
// add new dependencies to the list of dependencies while also validate that there are no unknown/non-prod dependencies
if (updateBuilder.hasProjectDependenciesToAdd())
{
List unknownDependencies = Lists.mutable.empty();
List nonProdDependencies = Lists.mutable.empty();
SortedMap accessExceptions = new TreeMap<>();
for (ProjectDependency projectDependency : updateBuilder.getProjectDependenciesToAdd())
{
if (projectDependencies.add(projectDependency))
{
updateProjectDependencies = true;
try
{
ProjectConfiguration dependencyConfig = getProjectConfiguration(projectFileAccessProvider.getFileAccessContext(projectDependency.getProjectId(), projectDependency.getVersionId()));
if (dependencyConfig == null)
{
unknownDependencies.add(projectDependency);
}
else if (dependencyConfig.getProjectType() != ProjectType.PRODUCTION)
{
nonProdDependencies.add(projectDependency);
}
}
catch (Exception e)
{
accessExceptions.put(projectDependency, e);
}
}
}
if (!unknownDependencies.isEmpty() || !nonProdDependencies.isEmpty() || !accessExceptions.isEmpty())
{
StringBuilder builder = new StringBuilder("There were issues with one or more added project dependencies");
if (!unknownDependencies.isEmpty())
{
builder.append("; unknown ").append((unknownDependencies.size() == 1) ? "dependency" : "dependencies").append(": ");
unknownDependencies.sort(Comparator.naturalOrder());
unknownDependencies.forEach(d -> d.appendDependencyString((d == unknownDependencies.get(0)) ? builder : builder.append(", ")));
}
if (!nonProdDependencies.isEmpty())
{
builder.append("; non-production ").append((unknownDependencies.size() == 1) ? "dependency" : "dependencies").append(": ");
nonProdDependencies.sort(Comparator.naturalOrder());
nonProdDependencies.forEach(d -> d.appendDependencyString((d == nonProdDependencies.get(0)) ? builder : builder.append(", ")));
}
if (!accessExceptions.isEmpty())
{
builder.append("; access ").append((accessExceptions.size() == 1) ? "exception" : "exceptions").append(": ");
ProjectDependency first = accessExceptions.firstKey();
accessExceptions.forEach((d, e) -> d.appendDependencyString((d == first) ? builder : builder.append(", ")).append(" (").append(e.getMessage()).append(')'));
}
throw new LegendSDLCServerException(builder.toString(), Status.BAD_REQUEST);
}
}
// validate if there are any conflicts between the dependencies
if (updateProjectDependencies)
{
validateDependencyConflicts(
projectDependencies,
ProjectDependency::getProjectId,
(id, deps) ->
{
if ((deps.size() <= 1) || deps.stream().allMatch(dep -> getProjectStructure(projectFileAccessProvider.getFileAccessContext(dep.getProjectId(), dep.getVersionId())).isSupportedArtifactType(ArtifactType.versioned_entities)))
{
return null;
}
List supported = Lists.mutable.empty();
List unsupported = Lists.mutable.empty();
deps.forEach(dep -> (getProjectStructure(projectFileAccessProvider.getFileAccessContext(dep.getProjectId(), dep.getVersionId())).isSupportedArtifactType(ArtifactType.versioned_entities) ? supported : unsupported).add(dep));
StringBuilder message = new StringBuilder();
unsupported.forEach(dep -> dep.appendVersionIdString((message.length() == 0) ? message : message.append(", ")));
message.append((unsupported.size() == 1) ? " does" : " do").append(" not support multi-version dependency");
if (!supported.isEmpty())
{
int startLen = message.length();
supported.forEach(dep -> dep.appendVersionIdString(message.append((message.length() == startLen) ? "; " : ", ")));
message.append((supported.size() == 1) ? " does" : " do");
}
return message.toString();
},
"projects");
}
// check if we need to update any metamodel dependencies
boolean updateMetamodelDependencies = false;
Set metamodelDependencies = Sets.mutable.withAll(currentConfig.getMetamodelDependencies());
if (updateBuilder.hasMetamodelDependenciesToRemove())
{
updateMetamodelDependencies |= metamodelDependencies.removeAll(updateBuilder.getMetamodelDependenciesToRemove());
}
// add new metamodel dependencies to the list of metamodel dependencies while also validate that there are no unknown metamodel dependencies
if (updateBuilder.hasMetamodelDependenciesToAdd())
{
List unknownDependencies = Lists.mutable.empty();
for (MetamodelDependency metamodelDependency : updateBuilder.getMetamodelDependenciesToAdd())
{
if (metamodelDependencies.add(metamodelDependency))
{
updateMetamodelDependencies = true;
if (!isKnownMetamodel(metamodelDependency))
{
unknownDependencies.add(metamodelDependency);
}
}
}
if (!unknownDependencies.isEmpty())
{
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(Comparator.naturalOrder());
unknownDependencies.forEach(d -> d.appendDependencyString((d == unknownDependencies.get(0)) ? builder : builder.append(", ")));
throw new LegendSDLCServerException(builder.toString(), Status.BAD_REQUEST);
}
}
// validate that there are no conflicts between the metamodel dependencies
if (updateMetamodelDependencies)
{
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");
}
boolean updateGeneration = false;
Map generationsByName = currentConfig.getArtifactGenerations().stream().collect(Collectors.toMap(ArtifactGeneration::getName, Function.identity()));
if (updateBuilder.hasArtifactGenerationsToRemove())
{
updateGeneration = generationsByName.keySet().stream().anyMatch(updateBuilder.getArtifactGenerationToRemove()::contains);
updateBuilder.getArtifactGenerationToRemove().forEach(generationsByName::remove);
}
if (updateBuilder.hasArtifactGenerationsToAdd())
{
validateArtifactGenerations(generationsByName, updateBuilder.getArtifactGenerationToAdd());
updateGeneration = updateBuilder.getArtifactGenerationToAdd().stream().noneMatch(generationsByName.values()::contains);
updateBuilder.getArtifactGenerationToAdd().forEach(art -> generationsByName.put(art.getName(), art));
}
// abort if there is no change to make
if (!updateProjectStructureVersion && !updateProjectStructureExtensionVersion && !updateGroupId && !updateArtifactId && !updateProjectDependencies && !updateMetamodelDependencies && !updateGeneration)
{
return null;
}
// Collect operations
List operations = Lists.mutable.empty();
// New configuration
SimpleProjectConfiguration newConfig = new SimpleProjectConfiguration(currentConfig);
if (updateProjectStructureVersion)
{
if (updateBuilder.hasProjectStructureExtensionVersion())
{
newConfig.setProjectStructureVersion(updateBuilder.getProjectStructureVersion(), updateBuilder.getProjectStructureExtensionVersion());
}
else if (updateBuilder.hasProjectStructureExtensionProvider())
{
newConfig.setProjectStructureVersion(updateBuilder.getProjectStructureVersion(), updateBuilder.getProjectStructureExtensionProvider().getLatestVersionForProjectStructureVersion(updateBuilder.getProjectStructureVersion()));
}
else
{
newConfig.setProjectStructureVersion(updateBuilder.getProjectStructureVersion(), null);
}
}
else if (updateProjectStructureExtensionVersion)
{
newConfig.setProjectStructureVersion(currentConfig.getProjectStructureVersion().getVersion(), updateBuilder.getProjectStructureExtensionVersion());
}
if (updateGroupId)
{
newConfig.setGroupId(updateBuilder.getGroupId());
}
if (updateArtifactId)
{
newConfig.setArtifactId(updateBuilder.getArtifactId());
}
if (updateProjectDependencies)
{
List projectDependencyList = Lists.mutable.withAll(projectDependencies);
projectDependencyList.sort(Comparator.naturalOrder());
newConfig.setProjectDependencies(projectDependencyList);
}
if (updateMetamodelDependencies)
{
List metamodelDependencyList = Lists.mutable.withAll(metamodelDependencies);
metamodelDependencyList.sort(Comparator.naturalOrder());
newConfig.setMetamodelDependencies(metamodelDependencyList);
}
if (updateGeneration)
{
List artifactGenerationsList = Lists.mutable.withAll(generationsByName.values());
artifactGenerationsList.sort(Comparator.comparing(ArtifactGeneration::getName));
newConfig.setArtifactGeneration(artifactGenerationsList);
}
// prevent downgrading project
if (newConfig.getProjectStructureVersion().compareTo(currentConfig.getProjectStructureVersion()) < 0)
{
throw new LegendSDLCServerException("Cannot change project " + projectId + " from project structure version " + currentConfig.getProjectStructureVersion().toVersionString() + " to version " + newConfig.getProjectStructureVersion().toVersionString(), Status.BAD_REQUEST);
}
String serializedNewConfig = serializeProjectConfiguration(newConfig);
operations.add((configFile == null) ? ProjectFileOperation.addFile(PROJECT_CONFIG_PATH, serializedNewConfig) : ProjectFileOperation.modifyFile(PROJECT_CONFIG_PATH, serializedNewConfig));
ProjectStructure currentProjectStructure = getProjectStructure(currentConfig);
ProjectStructure newProjectStructure = getProjectStructure(newConfig);
// 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
newProjectStructure.collectUpdateProjectConfigurationOperations(currentProjectStructure, fileAccessContext, projectFileAccessProvider::getFileAccessContext, operations::add);
// Collect update operations from any project structure extension
if (updateBuilder.hasProjectStructureExtensionProvider() && (newConfig.getProjectStructureVersion().getExtensionVersion() != null))
{
ProjectStructureExtension projectStructureExtension = updateBuilder.getProjectStructureExtensionProvider().getProjectStructureExtension(newConfig.getProjectStructureVersion().getVersion(), newConfig.getProjectStructureVersion().getExtensionVersion());
projectStructureExtension.collectUpdateProjectConfigurationOperations(currentConfig, newConfig, fileAccessContext, operations::add);
}
// Submit changes
return projectFileAccessProvider.getFileModificationContext(projectId, workspaceId, workspaceAccessType, revisionId).submit(updateBuilder.getMessage(), operations);
}
protected static , K extends Comparable> void validateDependencyConflicts(Collection dependencies, Function indexKeyFn, BiFunction, String> conflictFn, String description)
{
Map> index = dependencies.stream().collect(Collectors.groupingBy(indexKeyFn, Collectors.toCollection(TreeSet::new)));
SortedMap conflictMessages = new TreeMap<>();
index.forEach((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)
{
int current = 0;
int next = packageablePath.indexOf(PACKAGE_SEPARATOR);
while (next != -1)
{
builder.append('/').append(packageablePath, current, next);
current = next + 2;
next = packageablePath.indexOf(PACKAGE_SEPARATOR, current);
}
builder.append('/').append(packageablePath, current, packageablePath.length());
}
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(PACKAGE_SEPARATOR);
current = next + 1;
next = filePath.indexOf('/', current);
}
builder.append(filePath, current, end);
}
private static String serializeProjectConfiguration(ProjectConfiguration projectConfiguration)
{
try
{
return JSON.writeValueAsString(projectConfiguration);
}
catch (Exception e)
{
StringBuilder message = new StringBuilder("Error creating project configuration file");
String errorMessage = e.getMessage();
if (errorMessage != null)
{
message.append(": ").append(errorMessage);
}
throw new RuntimeException(message.toString(), e);
}
}
private static ProjectFile getProjectConfigurationFile(FileAccessContext accessContext)
{
return accessContext.getFile(PROJECT_CONFIG_PATH);
}
private static ProjectConfiguration readProjectConfiguration(ProjectFile file)
{
try (Reader reader = file.getContentAsReader())
{
return JSON.readValue(reader, SimpleProjectConfiguration.class);
}
catch (Exception e)
{
StringBuilder message = new StringBuilder("Error reading project configuration");
String errorMessage = e.getMessage();
if (errorMessage != null)
{
message.append(": ").append(errorMessage);
}
throw new RuntimeException(message.toString(), 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();
}
private static void validateArtifactGenerations(Map artifactGenerations, List artifactGenerationToAdd)
{
boolean isValid = true;
StringBuilder builder = new StringBuilder("There were issues with one or more added artifact generations");
if (artifactGenerationToAdd.stream().map(ArtifactGeneration::getType).anyMatch(FORBIDDEN_ARTIFACT_GENERATION_TYPES::contains))
{
isValid = false;
builder.append(FORBIDDEN_ARTIFACT_GENERATION_TYPES.stream().map(ArtifactType::getLabel).sorted().collect(Collectors.joining(", ", ": generation types ", " are not allowed")));
}
if (artifactGenerationToAdd.stream().map(ArtifactGeneration::getName).anyMatch(artifactGenerations::containsKey))
{
isValid = false;
builder.append(": trying to add duplicate artifact generations");
}
if (artifactGenerationToAdd.stream().map(ArtifactGeneration::getName).distinct().count() != artifactGenerationToAdd.size())
{
isValid = false;
builder.append(": generations to add contain duplicates");
}
if (!isValid)
{
throw new LegendSDLCServerException(builder.toString(), Status.BAD_REQUEST);
}
}
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);
}
}
}