diff --git a/fixtures/0010_settings.json b/fixtures/0010_settings_defaults.json similarity index 100% rename from fixtures/0010_settings.json rename to fixtures/0010_settings_defaults.json diff --git a/fixtures/0100_user-accounts.json b/fixtures/0100_user_user-accounts.json similarity index 100% rename from fixtures/0100_user-accounts.json rename to fixtures/0100_user_user-accounts.json diff --git a/fixtures/0110_api-keys.json b/fixtures/0110_apikey_api-keys.json similarity index 100% rename from fixtures/0110_api-keys.json rename to fixtures/0110_apikey_api-keys.json diff --git a/fixtures/0120_saved-queries.json b/fixtures/0120_search_saved-queries.json similarity index 100% rename from fixtures/0120_saved-queries.json rename to fixtures/0120_search_saved-queries.json diff --git a/fixtures/0200_metadata-schemas_resource.json b/fixtures/0200_schema_resource.json similarity index 100% rename from fixtures/0200_metadata-schemas_resource.json rename to fixtures/0200_schema_resource.json diff --git a/fixtures/0210_metadata-schemas_data-service.json b/fixtures/0210_schema_data-service.json similarity index 100% rename from fixtures/0210_metadata-schemas_data-service.json rename to fixtures/0210_schema_data-service.json diff --git a/fixtures/0220_metadata-schemas_metadata-service.json b/fixtures/0220_schema_metadata-service.json similarity index 100% rename from fixtures/0220_metadata-schemas_metadata-service.json rename to fixtures/0220_schema_metadata-service.json diff --git a/fixtures/0230_metadata-schemas_fdp.json b/fixtures/0230_schema_fdp.json similarity index 100% rename from fixtures/0230_metadata-schemas_fdp.json rename to fixtures/0230_schema_fdp.json diff --git a/fixtures/0240_metadata-schemas_catalog.json b/fixtures/0240_schema_catalog.json similarity index 100% rename from fixtures/0240_metadata-schemas_catalog.json rename to fixtures/0240_schema_catalog.json diff --git a/fixtures/0250_metadata-schemas_dataset.json b/fixtures/0250_schema_dataset.json similarity index 100% rename from fixtures/0250_metadata-schemas_dataset.json rename to fixtures/0250_schema_dataset.json diff --git a/fixtures/0260_metadata-schemas_distribution.json b/fixtures/0260_schema_distribution.json similarity index 100% rename from fixtures/0260_metadata-schemas_distribution.json rename to fixtures/0260_schema_distribution.json diff --git a/fixtures/0300_resource-definitions_distribution.json b/fixtures/0300_resource_distribution.json similarity index 100% rename from fixtures/0300_resource-definitions_distribution.json rename to fixtures/0300_resource_distribution.json diff --git a/fixtures/0310_resource-definitions_dataset.json b/fixtures/0310_resource_dataset.json similarity index 100% rename from fixtures/0310_resource-definitions_dataset.json rename to fixtures/0310_resource_dataset.json diff --git a/fixtures/0320_resource-definitions_catalog.json b/fixtures/0320_resource_catalog.json similarity index 100% rename from fixtures/0320_resource-definitions_catalog.json rename to fixtures/0320_resource_catalog.json diff --git a/fixtures/0330_resource-definitions_repository.json b/fixtures/0330_resource_repository.json similarity index 100% rename from fixtures/0330_resource-definitions_repository.json rename to fixtures/0330_resource_repository.json diff --git a/fixtures/0400_memberships_owner.json b/fixtures/0400_membership_owner.json similarity index 100% rename from fixtures/0400_memberships_owner.json rename to fixtures/0400_membership_owner.json diff --git a/fixtures/0410_memberships_data-provider.json b/fixtures/0410_membership_data-provider.json similarity index 100% rename from fixtures/0410_memberships_data-provider.json rename to fixtures/0410_membership_data-provider.json diff --git a/src/main/java/org/fairdatapoint/config/BootstrapConfig.java b/src/main/java/org/fairdatapoint/config/BootstrapConfig.java index 3a26265ec..9cb6df630 100644 --- a/src/main/java/org/fairdatapoint/config/BootstrapConfig.java +++ b/src/main/java/org/fairdatapoint/config/BootstrapConfig.java @@ -25,103 +25,90 @@ import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; import org.fairdatapoint.config.properties.BootstrapProperties; -import org.fairdatapoint.database.db.repository.FixtureHistoryRepository; -import org.fairdatapoint.entity.bootstrap.FixtureHistory; +import org.fairdatapoint.service.bootstrap.BootstrapService; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.data.repository.init.Jackson2RepositoryPopulatorFactoryBean; import org.springframework.data.repository.init.RepositoriesPopulatedEvent; +import org.springframework.data.repository.init.ResourceReaderRepositoryPopulator; import org.springframework.stereotype.Component; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; +import java.lang.reflect.Field; +import java.util.Collection; /** * The {@code BootstrapConfig} class configures a repository populator that loads initial data into the relational * database, based on JSON fixture files. + * The fixture filenames must match the regular expression pattern specified in {@code BootstrapService}, + * which can be described as {@code __.json}. * The default fixture files are located in the {@code /fixtures} directory. - * Additional fixture directories can also be specified, using the {@code dbFixturesDirs} property. + * Additional fixture directories can also be specified, using the {@code bootstrap.locations} property. * Fixture files are collected from all specified directories and are applied in lexicographic order. - * A FixtureHistory repository keeps track of fixture files that have been applied, so they are only applied once. + * A fixture history repository keeps track of fixture files that have been applied, so they are only applied once. * To add custom fixtures and/or override any of the default fixtures in a docker compose setup, we can bind-mount * individual fixture files or entire directories. - * For example: {@code ./my-fixtures/0100_user-accounts.json:/fdp/fixtures/0100_user-accounts.json:ro} + * For example: {@code ./my-fixtures/0100_user_user-accounts.json:/fdp/fixtures/0100_user_user-accounts.json:ro} * Note that bind-mounting the entire directory, instead of individual files, would hide all default files. */ @Configuration @Slf4j public class BootstrapConfig { - private final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); - private final BootstrapProperties bootstrap; - private final FixtureHistoryRepository fixtureHistoryRepository; - private final List resources = new ArrayList<>(); + private final BootstrapProperties bootstrapProperties; + private final BootstrapService bootstrapService; - public BootstrapConfig(BootstrapProperties bootstrapProperties, FixtureHistoryRepository fixtureHistoryRepository) { - this.bootstrap = bootstrapProperties; - this.fixtureHistoryRepository = fixtureHistoryRepository; + /** + * Constructor (autowired). + * @param bootstrapProperties Bootstrap properties. + * @param bootstrapService Bootstrap service. + */ + public BootstrapConfig(BootstrapProperties bootstrapProperties, BootstrapService bootstrapService) { + this.bootstrapProperties = bootstrapProperties; + this.bootstrapService = bootstrapService; } + /** + * Sets up a factory bean which creates a repository populator that takes care of loading initial data + * from JSON fixture files into the relational database. + * @return Repository populator factory bean. + */ @Bean - public Jackson2RepositoryPopulatorFactoryBean repositoryPopulator() { + public Jackson2RepositoryPopulatorFactoryBean repositoryPopulatorFactoryBean() { final Jackson2RepositoryPopulatorFactoryBean factory = new Jackson2RepositoryPopulatorFactoryBean(); - if (this.bootstrap.isEnabled()) { + if (bootstrapProperties.isEnabled()) { log.info("Bootstrap repository populator enabled"); - try { - // collect fixture resources - log.info("Looking for db fixtures in the following locations: {}", - String.join(", ", this.bootstrap.getLocations())); - for (String location : this.bootstrap.getLocations()) { - // Only look for JSON files - String locationPattern = location; - if (!locationPattern.endsWith(".json")) { - // naive append may lead to redundant slashes, but the OS ignores those - locationPattern += "/*.json"; - } - resources.addAll(List.of(resourceResolver.getResources(locationPattern))); - } - // remove resources that have been applied already - final List appliedFixtures = fixtureHistoryRepository.findAll().stream() - .map(FixtureHistory::getFilename).toList(); - final List resourcesToSkip = resources.stream() - .filter(resource -> appliedFixtures.contains(resource.getFilename())).toList(); - resources.removeAll(resourcesToSkip); - // sort resources to guarantee lexicographic order - resources.sort(Comparator.comparing(Resource::getFilename, Comparator.nullsLast(String::compareTo))); - // add resources to factory - log.info("Applying {} db fixtures ({} have been applied already)", - resources.size(), resourcesToSkip.size()); - factory.setResources(resources.toArray(new Resource[0])); - } - catch (IOException exception) { - log.error("Failed to load relational database fixtures", exception); - } + // add resources to factory + factory.setResources(bootstrapService.getNewResources()); } else { log.info("Bootstrap repository populator disabled"); } - return factory; } + /** + * Updates the fixture history after the repository populator has finished. + */ @Component public class RepositoriesPopulatedEventListener implements ApplicationListener { @Override public void onApplicationEvent(@NotNull RepositoriesPopulatedEvent event) { log.info("Repository populator finished."); - // Create fixture history records for all resources that have been applied. - // Note: This assumes that all items in the resources list have been *successfully* applied. However, I'm - // not sure if this can be guaranteed. If it does turn out to be a problem, we could try e.g. extending the - // ResourceReaderRepositoryPopulator.persist() method, so the history record is added there. - for (final Resource resource : resources) { - final String filename = resource.getFilename(); - final FixtureHistory fixtureHistory = fixtureHistoryRepository.save(new FixtureHistory(filename)); - log.debug("Fixture history updated: {} ({})", fixtureHistory.getFilename(), fixtureHistory.getUuid()); + if (event.getSource() instanceof ResourceReaderRepositoryPopulator populator) { + try { + // use reflection to make the private resources field accessible + final Field resourcesField = populator.getClass().getDeclaredField("resources"); + resourcesField.setAccessible(true); + // add the populator's resources to history + if (resourcesField.get(populator) instanceof Collection collection) { + log.info("Updating fixture history with {} resources", collection.size()); + bootstrapService.updateHistory(collection.toArray(new Resource[0])); + } + } + catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException exception) { + log.error("Failed to access resources field of ResourceReaderRepositoryPopulator", exception); + } } } } diff --git a/src/main/java/org/fairdatapoint/database/db/repository/FixtureHistoryRepository.java b/src/main/java/org/fairdatapoint/database/db/repository/FixtureHistoryRepository.java index 6912e2318..2ef6802f7 100644 --- a/src/main/java/org/fairdatapoint/database/db/repository/FixtureHistoryRepository.java +++ b/src/main/java/org/fairdatapoint/database/db/repository/FixtureHistoryRepository.java @@ -25,10 +25,17 @@ import org.fairdatapoint.database.db.repository.base.BaseRepository; import org.fairdatapoint.entity.bootstrap.FixtureHistory; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Repository public interface FixtureHistoryRepository extends BaseRepository { Optional findByFilename(String filename); + + @Transactional + void deleteByFilename(String filename); + + @Transactional + void deleteByFilenameContains(String substring); } diff --git a/src/main/java/org/fairdatapoint/entity/resource/ResourceDefinition.java b/src/main/java/org/fairdatapoint/entity/resource/ResourceDefinition.java index 0555e284d..077f406e0 100644 --- a/src/main/java/org/fairdatapoint/entity/resource/ResourceDefinition.java +++ b/src/main/java/org/fairdatapoint/entity/resource/ResourceDefinition.java @@ -54,7 +54,7 @@ public class ResourceDefinition extends BaseEntity { private List children; @OrderBy("orderPriority") - @OneToMany(fetch = FetchType.LAZY, mappedBy = "target") + @OneToMany(fetch = FetchType.LAZY, mappedBy = "target", cascade = CascadeType.ALL, orphanRemoval = true) private List parents; @OrderBy("orderPriority") @@ -62,7 +62,7 @@ public class ResourceDefinition extends BaseEntity { private List externalLinks; @OrderBy("orderPriority") - @OneToMany(mappedBy = "resourceDefinition", fetch = FetchType.LAZY) + @OneToMany(fetch = FetchType.LAZY, mappedBy = "resourceDefinition", cascade = CascadeType.ALL, orphanRemoval = true) private List metadataSchemaUsages; public boolean isRoot() { diff --git a/src/main/java/org/fairdatapoint/entity/resource/ResourceDefinitionChild.java b/src/main/java/org/fairdatapoint/entity/resource/ResourceDefinitionChild.java index 344c8fcf1..5daf3f83e 100644 --- a/src/main/java/org/fairdatapoint/entity/resource/ResourceDefinitionChild.java +++ b/src/main/java/org/fairdatapoint/entity/resource/ResourceDefinitionChild.java @@ -57,6 +57,7 @@ public class ResourceDefinitionChild extends BaseEntity { @Column(name = "order_priority", nullable = false) private Integer orderPriority; + // TODO: replace all @NotNull @ManyToOne combinations by @ManyToOne(optional = false) @NotNull @ManyToOne @JoinColumn(name = "source_resource_definition_id", nullable = false) diff --git a/src/main/java/org/fairdatapoint/service/bootstrap/BootstrapService.java b/src/main/java/org/fairdatapoint/service/bootstrap/BootstrapService.java new file mode 100644 index 000000000..59747e135 --- /dev/null +++ b/src/main/java/org/fairdatapoint/service/bootstrap/BootstrapService.java @@ -0,0 +1,174 @@ +/** + * The MIT License + * Copyright © 2016-2024 FAIR Data Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.fairdatapoint.service.bootstrap; + +import jakarta.validation.ValidationException; +import lombok.extern.slf4j.Slf4j; +import org.fairdatapoint.config.properties.BootstrapProperties; +import org.fairdatapoint.database.db.repository.FixtureHistoryRepository; +import org.fairdatapoint.entity.bootstrap.FixtureHistory; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +@Slf4j +@Service +public class BootstrapService { + + private final BootstrapProperties bootstrapProperties; + + private final FixtureHistoryRepository fixtureHistoryRepository; + + private final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); + + private final String packageNameFormat = "_%s_"; + + private final String packageNamePattern = "(?apikey|membership|resource|schema|search|settings|user)"; + + /** + * Constructor (autowired). + * @param bootstrapProperties Bootstrap properties + * @param fixtureHistoryRepository Fixture history repository + */ + public BootstrapService( + BootstrapProperties bootstrapProperties, + FixtureHistoryRepository fixtureHistoryRepository + ) { + this.bootstrapProperties = bootstrapProperties; + this.fixtureHistoryRepository = fixtureHistoryRepository; + } + + /** + * Raises a ValidationException if the filename does not match the specified regular expression pattern. + * The package name part is required to ensure that {@code removeByPackagename} works as expected. + * @param filename Name of a fixture file + */ + public void validateFilename(String filename) { + final String pattern = "^(?[0-9]{4})" + + packageNameFormat.formatted(packageNamePattern) + + "(?[a-zA-Z0-9\\-]+)\\.json$"; + if (!filename.matches(pattern)) { + throw new ValidationException("Filename %s does not match pattern %s".formatted(filename, pattern)); + } + } + + /** + * Raises a ValidationException if the package name does not match the specified regular expression pattern. + * @param packageName Name of an entity package + */ + public void validatePackageName(String packageName) { + if (!packageName.matches(packageNamePattern)) { + throw new ValidationException( + "Package name %s does not match pattern %s".formatted(packageName, packageNamePattern)); + } + } + + /** + * Creates a sorted array of unique fixture resources, representing files in the specified fixtures directories. + * Checks the fixture history repository for files that have already been applied and removes them from the array. + * @return Sorted array of unique {@code Resource} objects + */ + public Resource[] getNewResources() { + // use TreeSet with comparator for lexicographic order and uniqueness + final SortedSet resources = new TreeSet<>( + Comparator.comparing(Resource::getFilename, Comparator.nullsLast(String::compareTo))); + // collect fixture resources from specified directories + for (String location : bootstrapProperties.getLocations()) { + // Only look for JSON files + String locationPattern = location; + if (!locationPattern.endsWith(".json")) { + // naive append may lead to redundant slashes, but the OS ignores those + locationPattern += "/*.json"; + } + try { + log.info("Fixture resources location: {}", locationPattern); + resources.addAll(List.of(resourceResolver.getResources(locationPattern))); + } + catch (IOException exception) { + log.error("Failed to resolve fixture resources", exception); + } + } + // remove resources that have been applied already + final List resourcesToSkip = resources.stream() + .filter(resource -> getAppliedFixtures().contains(resource.getFilename())) + .toList(); + resourcesToSkip.forEach(resources::remove); + // return the result + log.info("Found {} new db fixture files ({} have been applied already)", + resources.size(), resourcesToSkip.size()); + return resources.toArray(new Resource[0]); + } + + /** + * Returns a list of fixture filenames that have already been applied. + * @return List of filename strings + */ + public List getAppliedFixtures() { + return fixtureHistoryRepository.findAll().stream().map(FixtureHistory::getFilename).toList(); + } + + /** + * Creates a fixture history record for the specified filename. + * Raises {@code ValidationException} if filename does not match the required pattern. + * @param filename Name of a fixture file + */ + public void addToHistory(String filename) { + validateFilename(filename); + final FixtureHistory fixtureHistory = fixtureHistoryRepository.save(new FixtureHistory(filename)); + log.debug("Fixture history updated: {} ({})", fixtureHistory.getFilename(), fixtureHistory.getUuid()); + } + + /** + * Removes records from the fixture history repository based on specified package names. + * @param packageNames Array of package name strings + */ + public void removeFromHistory(String[] packageNames) { + log.debug("Removing fixture history for the following packages: {}", String.join(", ", packageNames)); + for (String packageName : packageNames) { + validatePackageName(packageName); + fixtureHistoryRepository.deleteByFilenameContains(packageNameFormat.formatted(packageName)); + } + } + + /** + * Creates fixture history records for all specified resources. + * This assumes that all specified resources have been applied successfully. + * @param resources Array of {@code Resource} objects + */ + public void updateHistory(Resource[] resources) { + // Note that it may not be guaranteed that all specified resources have been applied successfully at this point. + // If this turns out to be a problem, we could try e.g. adding the history record by extending the + // ResourceReaderRepositoryPopulator.persist() method. + for (final Resource resource : resources) { + addToHistory(resource.getFilename()); + log.debug("Fixture history updated: {}", resource.getFilename()); + } + } +} diff --git a/src/main/java/org/fairdatapoint/service/reset/ResetService.java b/src/main/java/org/fairdatapoint/service/reset/ResetService.java index 0a086cc23..b30f4ed45 100644 --- a/src/main/java/org/fairdatapoint/service/reset/ResetService.java +++ b/src/main/java/org/fairdatapoint/service/reset/ResetService.java @@ -26,6 +26,7 @@ import org.fairdatapoint.api.dto.reset.ResetDTO; import org.fairdatapoint.database.db.repository.*; import org.fairdatapoint.entity.resource.ResourceDefinition; +import org.fairdatapoint.service.bootstrap.BootstrapService; import org.fairdatapoint.service.metadata.exception.MetadataServiceException; import org.fairdatapoint.service.metadata.generic.GenericMetadataService; import org.fairdatapoint.service.resource.ResourceDefinitionCache; @@ -39,6 +40,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.data.repository.init.ResourceReaderRepositoryPopulator; +import org.springframework.data.repository.support.Repositories; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.acls.model.AclCache; import org.springframework.stereotype.Service; @@ -52,6 +56,9 @@ @Service public class ResetService { + @Autowired + private ApplicationContext applicationContext; + @Autowired @Qualifier("persistentUrl") private String persistentUrl; @@ -71,9 +78,18 @@ public class ResetService { @Autowired private Repository draftsRepository; + @Autowired + private BootstrapService bootstrapService; + + @Autowired + private ResourceReaderRepositoryPopulator populator; + @Autowired private ApiKeyRepository apiKeyRepository; + @Autowired + private MembershipPermissionRepository membershipPermissionRepository; + @Autowired private MembershipRepository membershipRepository; @@ -109,22 +125,20 @@ public void resetToFactoryDefaults(ResetDTO reqDto) throws Exception { } if (reqDto.isUsers() || reqDto.isMetadata()) { clearMemberships(); - restoreDefaultMemberships(); + repopulate(new String[]{"membership"}); } if (reqDto.isUsers()) { clearApiKeys(); clearUsers(); - restoreDefaultUsers(); + repopulate(new String[]{"apikey", "user", "search"}); } if (reqDto.isMetadata()) { clearMetadata(); restoreDefaultMetadata(); } if (reqDto.isResourceDefinitions()) { - clearResourceDefinitions(); - clearMetadataSchemas(); - restoreDefaultMetadataSchemas(); - restoreDefaultResourceDefinitions(); + clearMetadataSchemasAndResourceDefinitions(); + repopulate(new String[]{"schema", "resource"}); } resourceDefinitionCache.computeCache(); resourceDefinitionTargetClassesCache.computeCache(); @@ -136,6 +150,8 @@ private void clearApiKeys() { } private void clearMemberships() { + log.debug("Clearing membership permissions"); + membershipPermissionRepository.deleteAll(); log.debug("Clearing memberships"); membershipRepository.deleteAll(); log.debug("Clearing ACL cache"); @@ -147,12 +163,10 @@ private void clearUsers() { userRepository.deleteAll(); } - private void clearMetadataSchemas() { + private void clearMetadataSchemasAndResourceDefinitions() { + // note these rely on cascade delete log.debug("Clearing metadata schemas"); metadataSchemaRepository.deleteAll(); - } - - private void clearResourceDefinitions() { log.debug("Clearing resource definitions"); resourceDefinitionRepository.deleteAll(); } @@ -166,16 +180,6 @@ private void clearMetadata() throws MetadataServiceException { } } - private void restoreDefaultUsers() { - log.debug("Creating default users"); - // TODO: data seed from specs - } - - private void restoreDefaultMemberships() { - log.debug("Creating default memberships"); - // TODO: data seed from specs - } - private void restoreDefaultMetadata() { log.debug("Creating default metadata"); try (RepositoryConnection conn = mainRepository.getConnection()) { @@ -192,13 +196,15 @@ private void restoreDefaultMetadata() { } } - private void restoreDefaultMetadataSchemas() throws Exception { - log.debug("Creating default metadata schemas"); - // TODO: data seed from specs - } - - private void restoreDefaultResourceDefinitions() { - log.debug("Creating default resource definitions"); - // TODO: data seed from specs + /** + * Reloads data from JSON fixture files into the relational database. + * This works by clearing history records for the specified packages and then re-running the repository populator. + * Note that it may be necessary to delete existing entities from the relevant repositories first. + * @param packageNames Array of names of entity packages to be repopulated + */ + private void repopulate(String[] packageNames) { + bootstrapService.removeFromHistory(packageNames); + populator.setResources(bootstrapService.getNewResources()); + populator.populate(new Repositories(applicationContext)); } } diff --git a/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java b/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java index c59ec6dc0..291200f7d 100644 --- a/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java +++ b/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java @@ -175,31 +175,19 @@ public Optional update(UUID uuid, ResourceDefinitionChang @Transactional @PreAuthorize("hasRole('ADMIN')") public boolean deleteByUuid(UUID uuid) { - // 1. Get resource definition - final Optional oRd = resourceDefinitionRepository.findByUuid(uuid); - if (oRd.isEmpty()) { - return false; - } - final ResourceDefinition rd = oRd.get(); - - // 2. Delete from parent resource definitions - rd.getParents().forEach(this::deleteChild); - - // 3. Delete resource definition (incl. children and links) - deleteDependents(rd); - resourceDefinitionRepository.delete(rd); - - // 4. Delete entity from membership - membershipService.removeFromMembership(rd); - entityManager.flush(); - - // 5. Recompute cache - resourceDefinitionCache.computeCache(); - targetClassesCache.computeCache(); - - // 6. Delete from OpenAPI docs - openApiService.removeGenericPaths(rd); - return true; + final Optional optionalResourceDefinition = resourceDefinitionRepository.findByUuid(uuid); + optionalResourceDefinition.ifPresent( + resourceDefinition -> { + // Delete resource definition entry from OpenAPI docs + openApiService.removeGenericPaths(resourceDefinition); + // Delete resource definition (and children) from database + resourceDefinitionRepository.delete(resourceDefinition); + // Recompute cache + resourceDefinitionCache.computeCache(); + targetClassesCache.computeCache(); + } + ); + return optionalResourceDefinition.isPresent(); } @Transactional @@ -246,20 +234,13 @@ protected void createDependents(ResourceDefinition definition, ResourceDefinitio @Transactional @PreAuthorize("hasRole('ADMIN')") protected void deleteDependents(ResourceDefinition resourceDefinition) { - resourceDefinition.getChildren().forEach(this::deleteChild); + childRepository.deleteAll(resourceDefinition.getChildren()); linkRepository.deleteAll(resourceDefinition.getExternalLinks()); usageRepository.deleteAll(resourceDefinition.getMetadataSchemaUsages()); entityManager.flush(); entityManager.refresh(resourceDefinition); } - @Transactional - @PreAuthorize("hasRole('ADMIN')") - protected void deleteChild(ResourceDefinitionChild child) { - childMetadataRepository.deleteAll(child.getMetadata()); - childRepository.delete(child); - } - public List getTargetClassUris(ResourceDefinition resourceDefinition) { final Set result = targetClassesCache .getByUuid(resourceDefinition.getUuid().toString()); diff --git a/src/test/java/org/fairdatapoint/database/db/repository/bootstrap/DatabaseBootstrapTests.java b/src/test/java/org/fairdatapoint/database/db/repository/bootstrap/DatabaseBootstrapTests.java index ec1c50517..d112bfab0 100644 --- a/src/test/java/org/fairdatapoint/database/db/repository/bootstrap/DatabaseBootstrapTests.java +++ b/src/test/java/org/fairdatapoint/database/db/repository/bootstrap/DatabaseBootstrapTests.java @@ -39,8 +39,7 @@ import java.util.Optional; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; @AutoConfigureTestEntityManager @Transactional diff --git a/src/test/java/org/fairdatapoint/database/db/repository/bootstrap/FixtureHistoryRepositoryTests.java b/src/test/java/org/fairdatapoint/database/db/repository/bootstrap/FixtureHistoryRepositoryTests.java index 7bfc3a580..301ad6cb4 100644 --- a/src/test/java/org/fairdatapoint/database/db/repository/bootstrap/FixtureHistoryRepositoryTests.java +++ b/src/test/java/org/fairdatapoint/database/db/repository/bootstrap/FixtureHistoryRepositoryTests.java @@ -26,26 +26,28 @@ import org.fairdatapoint.BaseIntegrationTest; import org.fairdatapoint.database.db.repository.FixtureHistoryRepository; import org.fairdatapoint.entity.bootstrap.FixtureHistory; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestEntityManager; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.test.context.TestPropertySource; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; -@AutoConfigureTestEntityManager @Transactional -@TestPropertySource(properties = "bootstrap.enabled=false") public class FixtureHistoryRepositoryTests extends BaseIntegrationTest { @Autowired FixtureHistoryRepository repository; final String filename = "0001-whatever.json"; + @BeforeEach + public void clearFixtureHistory() { + repository.deleteAll(); + } + @Test public void testSave() { FixtureHistory fixtureHistory = repository.saveAndFlush(new FixtureHistory(filename)); @@ -79,4 +81,24 @@ public void testFindByFilenameWithExistingFilename() { Optional appliedFixture = repository.findByFilename(filename); assertTrue(appliedFixture.isPresent()); } + + @Test + public void testDeleteByFilename() { + // prepare + repository.saveAndFlush(new FixtureHistory(filename)); + assertEquals(1, repository.count()); + // test + repository.deleteByFilename(filename); + assertEquals(0, repository.count()); + } + + @Test + public void testDeleteByFilenameContains() { + // prepare + repository.saveAndFlush(new FixtureHistory(filename)); + assertEquals(1, repository.count()); + // test + repository.deleteByFilenameContains("whatever"); + assertEquals(0, repository.count()); + } } diff --git a/src/test/java/org/fairdatapoint/service/bootstrap/BootstrapServiceTest.java b/src/test/java/org/fairdatapoint/service/bootstrap/BootstrapServiceTest.java new file mode 100644 index 000000000..77e22612c --- /dev/null +++ b/src/test/java/org/fairdatapoint/service/bootstrap/BootstrapServiceTest.java @@ -0,0 +1,116 @@ +/** + * The MIT License + * Copyright © 2016-2024 FAIR Data Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.fairdatapoint.service.bootstrap; + +import jakarta.validation.ValidationException; +import org.fairdatapoint.BaseIntegrationTest; +import org.fairdatapoint.database.db.repository.FixtureHistoryRepository; +import org.fairdatapoint.entity.bootstrap.FixtureHistory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + + +public class BootstrapServiceTest extends BaseIntegrationTest { + final String validFilename = "0100_user_test-user-accounts.json"; + + @Autowired + private BootstrapService bootstrapService; + + @Autowired + private FixtureHistoryRepository fixtureHistoryRepository; + + @BeforeEach + public void clearFixtureHistory() { + fixtureHistoryRepository.deleteAll(); + } + + @Test + public void testValidateFilenameValid() { + // test + assertDoesNotThrow(() -> bootstrapService.validateFilename(validFilename)); + } + + @Test + public void testValidateFilenameInvalid() { + // given + final String invalidFilename = "0100_search_invalid_description.json"; + // test + assertThrows(ValidationException.class, () -> bootstrapService.validateFilename(invalidFilename)); + } + + @Test + public void testValidatePackageNameValid() { + // given + final String validPackageName = "user"; + // test + assertDoesNotThrow(() -> bootstrapService.validatePackageName(validPackageName)); + } + + @Test + public void testValidatePackageNameInvalid() { + // given + final String invalidPackageName = "users"; + // test + assertThrows(ValidationException.class, () -> bootstrapService.validatePackageName(invalidPackageName)); + } + + @Test + public void testGetAppliedFixtures() { + // given + fixtureHistoryRepository.saveAndFlush(new FixtureHistory(validFilename)); + // test + assertEquals(List.of(validFilename), bootstrapService.getAppliedFixtures()); + } + + @Test + public void testAddToHistory() { + // test + bootstrapService.addToHistory(validFilename); + assertTrue(fixtureHistoryRepository.findByFilename(validFilename).isPresent()); + } + + @Test + public void testRemoveFromHistory() { + // given + final String remainingFilename = "0003_resource_dummy-resource-definitions.json"; + final List filenames = List.of( + "0001_user_dummy-user-account-1.json", + "0002_user_dummy-user-account-2.json", + remainingFilename + ); + for (String filename : filenames) { + fixtureHistoryRepository.saveAndFlush(new FixtureHistory(filename)); + } + assertEquals(filenames.size(), fixtureHistoryRepository.count()); + // test + final String[] packageNames = {"user"}; + bootstrapService.removeFromHistory(packageNames); + assertEquals(1, fixtureHistoryRepository.count()); + assertTrue(fixtureHistoryRepository.findByFilename(remainingFilename).isPresent()); + } +} diff --git a/src/test/java/org/fairdatapoint/service/reset/ResetServiceTest.java b/src/test/java/org/fairdatapoint/service/reset/ResetServiceTest.java new file mode 100644 index 000000000..0fbd7641b --- /dev/null +++ b/src/test/java/org/fairdatapoint/service/reset/ResetServiceTest.java @@ -0,0 +1,109 @@ +/** + * The MIT License + * Copyright © 2016-2024 FAIR Data Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.fairdatapoint.service.reset; + +import org.fairdatapoint.BaseIntegrationTest; +import org.fairdatapoint.api.dto.reset.ResetDTO; +import org.fairdatapoint.database.db.repository.*; +import org.fairdatapoint.database.db.repository.base.BaseRepository; +import org.fairdatapoint.entity.base.BaseEntity; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ResetServiceTest extends BaseIntegrationTest { + + @Autowired + private ApiKeyRepository apiKeyRepository; + + @Autowired + private MembershipRepository membershipRepository; + + @Autowired + private MembershipPermissionRepository membershipPermissionRepository; + + @Autowired + private MetadataSchemaRepository metadataSchemaRepository; + + @Autowired + private ResetService resetService; + + @Autowired + private ResourceDefinitionChildRepository resourceDefinitionChildRepository; + + @Autowired + private ResourceDefinitionRepository resourceDefinitionRepository; + + @Autowired + private UserAccountRepository userAccountRepository; + + @Test + @Transactional + @WithMockUser(roles = {"ADMIN"}) + public void testResetToFactoryDefaultsAll() throws Exception { + // given: repositories have been populated *before* reference time + final List> repositories = List.of( + apiKeyRepository, + membershipPermissionRepository, + membershipRepository, + metadataSchemaRepository, + resourceDefinitionChildRepository, + resourceDefinitionRepository, + userAccountRepository + ); + Instant referenceTime = Instant.now(); + checkEntityCreationTimes(repositories, referenceTime, true); + // when: everything is reset + ResetDTO resetAll = new ResetDTO(true, true, true, true); + resetService.resetToFactoryDefaults(resetAll); + // then: repositories have been repopulated *after* reference time (actually "not before") + checkEntityCreationTimes(repositories, referenceTime, false); + } + + private void checkEntityCreationTimes( + List> repositories, + Instant referenceTime, + boolean before + ) { + repositories.forEach(repository -> { + // repository should be non-empty + assertTrue(repository.count() > 0); + // all entities should have creation time before (or not before) reference time + for (Object entity : repository.findAll()) { + if (entity instanceof BaseEntity) { + assertEquals(before, ((BaseEntity) entity).getCreatedAt().isBefore(referenceTime)); + } + else { + throw new RuntimeException("Unexpected entity type: " + entity.getClass()); + } + } + }); + } +} diff --git a/src/test/resources/test-fixtures/0130_user_test-users.json b/src/test/resources/test-fixtures/0130_user_test-users.json new file mode 100644 index 000000000..f847d6778 --- /dev/null +++ b/src/test/resources/test-fixtures/0130_user_test-users.json @@ -0,0 +1,38 @@ +[ + { + "_class": "org.fairdatapoint.entity.user.UserAccount", + "uuid": "95589e50-d261-492b-8852-9324e9a66a42", + "firstName": "Admin", + "lastName": "von Universe", + "email": "admin@example.com", + "passwordHash": "$2a$10$L.0OZ8QjV3yLhoCDvU04gu.WP1wGQih41MsBdvtQOshJJntaugBxe", + "role": "ADMIN" + }, + { + "_class": "org.fairdatapoint.entity.user.UserAccount", + "uuid": "7e64818d-6276-46fb-8bb1-732e6e09f7e9", + "firstName": "Albert", + "lastName": "Einstein", + "email": "albert.einstein@example.com", + "passwordHash": "$2a$10$hZF1abbZ48Tf.3RndC9W6OlDt6gnBoD/2HbzJayTs6be7d.5DbpnW", + "role": "USER" + }, + { + "_class": "org.fairdatapoint.entity.user.UserAccount", + "uuid": "b5b92c69-5ed9-4054-954d-0121c29b6800", + "firstName": "Nikola", + "lastName": "Tesla", + "email": "nikola.tesla@example.com", + "passwordHash": "$2a$10$tMbZUZg9AbYL514R.hZ0tuzvfZJR5NQhSVeJPTQhNwPf6gv/cvrna", + "role": "USER" + }, + { + "_class": "org.fairdatapoint.entity.user.UserAccount", + "uuid": "8d1a4c06-bb0e-4d03-a01f-14fa49bbc152", + "firstName": "Isaac", + "lastName": "Newton", + "email": "isaac.newton@example.com", + "passwordHash": "$2a$10$DLkI7NAZDzWVaKG1lVtloeoPNLPoAgDDBqQKQiSAYDZXrf2QKkuHC", + "role": "USER" + } +] \ No newline at end of file diff --git a/src/test/resources/test-fixtures/0140_apikey_test-api-keys.json b/src/test/resources/test-fixtures/0140_apikey_test-api-keys.json new file mode 100644 index 000000000..db00cccb4 --- /dev/null +++ b/src/test/resources/test-fixtures/0140_apikey_test-api-keys.json @@ -0,0 +1,18 @@ +[ + { + "_class": "org.fairdatapoint.entity.apikey.ApiKey", + "uuid": "a1c00673-24c5-4e0a-bdbe-22e961ee7548", + "token": "a274793046e34a219fd0ea6362fcca61a001500b71724f4c973a017031653c20", + "userAccount": { + "uuid": "7e64818d-6276-46fb-8bb1-732e6e09f7e9" + } + }, + { + "_class": "org.fairdatapoint.entity.apikey.ApiKey", + "uuid": "62657760-21fe-488c-a0ea-f612a70493da", + "token": "dd5dc3b53b6145cfa9f6c58b72ebad21cd2f860ace62451ba4e3c74a0e63540a", + "userAccount": { + "uuid": "b5b92c69-5ed9-4054-954d-0121c29b6800" + } + } +] \ No newline at end of file diff --git a/src/test/resources/test-fixtures/0130_test-users-with-api-keys-and-saved-queries.json b/src/test/resources/test-fixtures/0150_search_test-saved-queries.json similarity index 57% rename from src/test/resources/test-fixtures/0130_test-users-with-api-keys-and-saved-queries.json rename to src/test/resources/test-fixtures/0150_search_test-saved-queries.json index a7d28cf9b..92ac6995c 100644 --- a/src/test/resources/test-fixtures/0130_test-users-with-api-keys-and-saved-queries.json +++ b/src/test/resources/test-fixtures/0150_search_test-saved-queries.json @@ -1,58 +1,4 @@ [ - { - "_class": "org.fairdatapoint.entity.user.UserAccount", - "uuid": "95589e50-d261-492b-8852-9324e9a66a42", - "firstName": "Admin", - "lastName": "von Universe", - "email": "admin@example.com", - "passwordHash": "$2a$10$L.0OZ8QjV3yLhoCDvU04gu.WP1wGQih41MsBdvtQOshJJntaugBxe", - "role": "ADMIN" - }, - { - "_class": "org.fairdatapoint.entity.user.UserAccount", - "uuid": "7e64818d-6276-46fb-8bb1-732e6e09f7e9", - "firstName": "Albert", - "lastName": "Einstein", - "email": "albert.einstein@example.com", - "passwordHash": "$2a$10$hZF1abbZ48Tf.3RndC9W6OlDt6gnBoD/2HbzJayTs6be7d.5DbpnW", - "role": "USER" - }, - { - "_class": "org.fairdatapoint.entity.user.UserAccount", - "uuid": "b5b92c69-5ed9-4054-954d-0121c29b6800", - "firstName": "Nikola", - "lastName": "Tesla", - "email": "nikola.tesla@example.com", - "passwordHash": "$2a$10$tMbZUZg9AbYL514R.hZ0tuzvfZJR5NQhSVeJPTQhNwPf6gv/cvrna", - "role": "USER" - }, - { - "_class": "org.fairdatapoint.entity.user.UserAccount", - "uuid": "8d1a4c06-bb0e-4d03-a01f-14fa49bbc152", - "firstName": "Isaac", - "lastName": "Newton", - "email": "isaac.newton@example.com", - "passwordHash": "$2a$10$DLkI7NAZDzWVaKG1lVtloeoPNLPoAgDDBqQKQiSAYDZXrf2QKkuHC", - "role": "USER" - }, - - { - "_class": "org.fairdatapoint.entity.apikey.ApiKey", - "uuid": "a1c00673-24c5-4e0a-bdbe-22e961ee7548", - "token": "a274793046e34a219fd0ea6362fcca61a001500b71724f4c973a017031653c20", - "userAccount": { - "uuid": "7e64818d-6276-46fb-8bb1-732e6e09f7e9" - } - }, - { - "_class": "org.fairdatapoint.entity.apikey.ApiKey", - "uuid": "62657760-21fe-488c-a0ea-f612a70493da", - "token": "dd5dc3b53b6145cfa9f6c58b72ebad21cd2f860ace62451ba4e3c74a0e63540a", - "userAccount": { - "uuid": "b5b92c69-5ed9-4054-954d-0121c29b6800" - } - }, - { "_class" : "org.fairdatapoint.entity.search.SearchSavedQuery", "uuid": "d31e3da1-2cfa-4b55-a8cb-71d1acf01aef", diff --git a/src/test/resources/test-fixtures/0500_test-settings.json b/src/test/resources/test-fixtures/0500_settings_test-settings.json similarity index 100% rename from src/test/resources/test-fixtures/0500_test-settings.json rename to src/test/resources/test-fixtures/0500_settings_test-settings.json diff --git a/src/test/resources/test-fixtures/0600_test-schemas.json b/src/test/resources/test-fixtures/0600_schema_test-schemas.json similarity index 100% rename from src/test/resources/test-fixtures/0600_test-schemas.json rename to src/test/resources/test-fixtures/0600_schema_test-schemas.json