Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
627fb84
remove unnecessary annotations from FixtureHistoryRepositoryTests
dennisvang Dec 2, 2025
b4c43e5
add FixtureHistoryRepository.deleteByFilenameIn() with test
dennisvang Dec 2, 2025
e0b3202
rename repositoryPopulator to repositoryPopulatorFactoryBean
dennisvang Dec 2, 2025
c7bb0f4
move resource logic into separate BootstrapConfig.getNewResources method
dennisvang Dec 2, 2025
8b133f7
implement restoreDefaultUsers and restoreDefaultMemberships using pop…
dennisvang Dec 2, 2025
ae816d1
fix type definitions
dennisvang Dec 2, 2025
6cca79a
implement generic restoreDefaultFixtures method
dennisvang Dec 2, 2025
f00c6d7
fix indentation
dennisvang Dec 2, 2025
6f7fad1
rename fixture files according to <order>_<package>_<description>.jso…
dennisvang Dec 11, 2025
b935b16
add BootstrapConfig.checkFixtureFilename with tests
dennisvang Dec 11, 2025
223c130
rename to BootstrapConfig.validateFixtureFilename and throw if invalid
dennisvang Dec 11, 2025
c6f5201
validate fixture names before populating
dennisvang Dec 11, 2025
ca112b1
replace deleteByFilenameIn by deleteByFilenameContains (wip)
dennisvang Dec 11, 2025
da4ed88
specify allowed package names in fixture filename pattern
dennisvang Dec 12, 2025
7503290
use generic arg name for deleteByFilenameContains and fix test
dennisvang Dec 12, 2025
e27cc72
add FixtureHistoryRepository.deleteByFilename with test
dennisvang Dec 12, 2025
c253a56
move fixture related methods into new BootstrapService
dennisvang Dec 12, 2025
3169903
todo move to BootstrapService
dennisvang Dec 12, 2025
75b843f
add a rudimentary test for the ResetService
dennisvang Dec 12, 2025
3ebee9c
add license headers
dennisvang Dec 12, 2025
ae89ffb
reorder ResetService class attributes
dennisvang Dec 12, 2025
1a95957
Merge branch 'proposal/634-remove-sql-test-data-migrations' into prop…
dennisvang Dec 15, 2025
36300bd
split up and rename test-fixtures to match required filename pattern
dennisvang Dec 15, 2025
e52a675
run testResetToFactoryDefaultsAll as admin
dennisvang Dec 15, 2025
921fde3
check entity creation time in testResetToFactoryDefaultsAll
dennisvang Dec 18, 2025
f90a2de
Merge branch 'feature/634-boostrapping-fdp' into proposal/634-reset
dennisvang Dec 19, 2025
88f0f55
make testResetToFactoryDefaultsAll transactional
dennisvang Dec 19, 2025
c0d2c41
fix RescourceDefinition cascade delete problem
dennisvang Dec 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
92 changes: 50 additions & 42 deletions src/main/java/org/fairdatapoint/config/BootstrapConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
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;
Expand All @@ -38,9 +37,10 @@
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;

/**
* The {@code BootstrapConfig} class configures a repository populator that loads initial data into the relational
Expand All @@ -59,53 +59,63 @@
public class BootstrapConfig {
private final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
private final BootstrapProperties bootstrap;
private final FixtureHistoryRepository fixtureHistoryRepository;
private final List<Resource> resources = new ArrayList<>();
private final BootstrapService bootstrapService;

public BootstrapConfig(BootstrapProperties bootstrapProperties, FixtureHistoryRepository fixtureHistoryRepository) {
public BootstrapConfig(BootstrapProperties bootstrapProperties, BootstrapService bootstrapService) {
this.bootstrap = bootstrapProperties;
this.fixtureHistoryRepository = fixtureHistoryRepository;
this.bootstrapService = bootstrapService;
}

@Bean
public Jackson2RepositoryPopulatorFactoryBean repositoryPopulator() {
final Jackson2RepositoryPopulatorFactoryBean factory = new Jackson2RepositoryPopulatorFactoryBean();
if (this.bootstrap.isEnabled()) {
log.info("Bootstrap repository populator enabled");
/**
* 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 Resource objects
*/
public Resource[] getNewResources() {
// TODO: move this into the BootstrapService class?
// use TreeSet with comparator for lexicographic order and uniqueness
final SortedSet<Resource> resources = new TreeSet<>(
Comparator.comparing(Resource::getFilename, Comparator.nullsLast(String::compareTo)));
// collect fixture resources from specified directories
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";
}
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<String> appliedFixtures = fixtureHistoryRepository.findAll().stream()
.map(FixtureHistory::getFilename).toList();
final List<Resource> 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]));
resources.addAll(List.of(resourceResolver.getResources(locationPattern)));
}
catch (IOException exception) {
log.error("Failed to load relational database fixtures", exception);
log.error("Failed to resolve fixture resources", exception);
}
}
// remove resources that have been applied already
final List<String> appliedFixtures = bootstrapService.getAppliedFixtures();
final List<Resource> resourcesToSkip = resources.stream()
.filter(resource -> appliedFixtures.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]);
}

@Bean
public Jackson2RepositoryPopulatorFactoryBean repositoryPopulatorFactoryBean() {
final Jackson2RepositoryPopulatorFactoryBean factory = new Jackson2RepositoryPopulatorFactoryBean();
if (this.bootstrap.isEnabled()) {
log.info("Bootstrap repository populator enabled");
// add resources to factory
factory.setResources(getNewResources());
}
else {
log.info("Bootstrap repository populator disabled");
}

return factory;
}

Expand All @@ -118,10 +128,8 @@ public void onApplicationEvent(@NotNull RepositoriesPopulatedEvent event) {
// 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());
for (final Resource resource : getNewResources()) {
bootstrapService.addToHistory(resource.getFilename());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FixtureHistory> {
Optional<FixtureHistory> findByFilename(String filename);

@Transactional
void deleteByFilename(String filename);

@Transactional
void deleteByFilenameContains(String substring);
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class ResourceDefinition extends BaseEntity {
private List<ResourceDefinitionChild> children;

@OrderBy("orderPriority")
@OneToMany(fetch = FetchType.LAZY, mappedBy = "target")
@OneToMany(fetch = FetchType.LAZY, mappedBy = "target", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ResourceDefinitionChild> parents;

@OrderBy("orderPriority")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* 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.database.db.repository.FixtureHistoryRepository;
import org.fairdatapoint.entity.bootstrap.FixtureHistory;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
public class BootstrapService {
private final FixtureHistoryRepository fixtureHistoryRepository;
private final String packageNameFormat = "_%s_";
private final String packageNamePattern = "(?<package>apikey|membership|resource|schema|search|settings|user)";

public BootstrapService(FixtureHistoryRepository fixtureHistoryRepository) {
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 = "^(?<order>[0-9]{4})"
+ packageNameFormat.formatted(packageNamePattern)
+ "(?<description>[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));
}
}

/**
* Returns a list of fixture filenames that have already been applied.
* @return list of filename strings
*/
public List<String> 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));
}
}
}
Loading
Loading