Skip to content

Commit

Permalink
Merge pull request #551 from folio-org/modfqmmgr-594
Browse files Browse the repository at this point in the history
* other files update

* add comment

* logging

* test fix

* debug

* safer updates

* properly iterate dates

* more fix

* Test migration utils

* add exceptional check

* refactor migration constant storage to remove cycle

* Add V4 specific migration test

* fix test

* imports

* Use proper suffixes for dates
  • Loading branch information
ncovercash authored Dec 11, 2024
2 parents fb81183 + 7e422cc commit e84114e
Show file tree
Hide file tree
Showing 13 changed files with 645 additions and 83 deletions.
2 changes: 2 additions & 0 deletions docs/Migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Any change to an entity type that results in a field being removed or renamed sh
1. Describe the [warnings](#warnings) in this migration by implementing any of the applicable methods.
1. Add your new strategy to `src/main/java/org/folio/fqm/migration/MigrationStrategyRepository.java`.
1. Update the `CURRENT_VERSION` in `src/main/java/org/folio/fqm/service/MigrationService.java`.
1. If something fancy is being done, or you want to go above and beyond, write a custom test. You can see `src/test/java/org/folio/fqm/migration/strategies/TestTemplate` and `V0POCMigrationTest` for a framework that can easily be extended for common test case formats.
- Implementations of `AbstractSimpleMigrationStrategy` are automatically tested via `MigrationStrategyRepositoryTest`, however, this is just for basic smoke tests and contains no logic to test specifics of an actual migration.

Not all use cases can be covered with `AbstractSimpleMigrationStrategy`; in that case, a custom migration will be needed. See [advanced migrations](#advanced-migrations) for more details.

Expand Down
23 changes: 23 additions & 0 deletions src/main/java/org/folio/fqm/config/MigrationConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.folio.fqm.config;

import java.util.UUID;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MigrationConfiguration {

public static final String VERSION_KEY = "_version";
public static final UUID REMOVED_ENTITY_TYPE_ID = UUID.fromString("deadbeef-dead-dead-dead-deaddeadbeef");

private static final String CURRENT_VERSION = "5";
// TODO: replace this with current version in the future?
private static final String DEFAULT_VERSION = "0";

public String getCurrentVersion() {
return CURRENT_VERSION;
}

public String getDefaultVersion() {
return DEFAULT_VERSION;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.HashSet;
Expand All @@ -16,13 +14,13 @@
import java.util.function.Function;
import lombok.extern.log4j.Log4j2;
import org.folio.fql.service.FqlService;
import org.folio.fqm.config.MigrationConfiguration;
import org.folio.fqm.migration.warnings.EntityTypeWarning;
import org.folio.fqm.migration.warnings.FieldWarning;
import org.folio.fqm.migration.warnings.QueryBreakingWarning;
import org.folio.fqm.migration.warnings.RemovedEntityWarning;
import org.folio.fqm.migration.warnings.RemovedFieldWarning;
import org.folio.fqm.migration.warnings.Warning;
import org.folio.fqm.service.MigrationService;

@Log4j2
public abstract class AbstractSimpleMigrationStrategy implements MigrationStrategy {
Expand Down Expand Up @@ -86,8 +84,10 @@ public MigratableQueryInformation apply(FqlService fqlService, MigratableQueryIn
if (entityTypeWarning.isPresent() && entityTypeWarning.get() instanceof RemovedEntityWarning) {
return MigratableQueryInformation
.builder()
.entityTypeId(MigrationService.REMOVED_ENTITY_TYPE_ID)
.fqlQuery(objectMapper.writeValueAsString(Map.of(MigrationService.VERSION_KEY, this.getTargetVersion())))
.entityTypeId(MigrationConfiguration.REMOVED_ENTITY_TYPE_ID)
.fqlQuery(
objectMapper.writeValueAsString(Map.of(MigrationConfiguration.VERSION_KEY, this.getTargetVersion()))
)
.fields(List.of())
.warning(getEntityTypeWarnings().get(src.entityTypeId()).apply(src.fqlQuery()))
.build();
Expand All @@ -98,23 +98,34 @@ public MigratableQueryInformation apply(FqlService fqlService, MigratableQueryIn
if (src.fqlQuery() == null) {
result =
result.withFqlQuery(
objectMapper.writeValueAsString(Map.of(MigrationService.VERSION_KEY, this.getTargetVersion()))
objectMapper.writeValueAsString(Map.of(MigrationConfiguration.VERSION_KEY, this.getTargetVersion()))
);
}

result =
result.withEntityTypeId(this.getEntityTypeChanges().getOrDefault(src.entityTypeId(), src.entityTypeId()));

ObjectNode fql = (ObjectNode) objectMapper.readTree(result.fqlQuery());
fql.set(MigrationService.VERSION_KEY, objectMapper.valueToTree(this.getTargetVersion()));

Map<String, String> fieldChanges = this.getFieldChanges().getOrDefault(src.entityTypeId(), Map.of());
Map<String, BiFunction<String, String, FieldWarning>> fieldWarnings =
this.getFieldWarnings().getOrDefault(src.entityTypeId(), Map.of());
if (!fieldChanges.isEmpty() || !fieldWarnings.isEmpty()) {
// map query fields
fql = migrateFqlTree(fieldChanges, fieldWarnings, fql, warnings);

String newFql = MigrationUtils.migrateFql(
result.fqlQuery(),
originalVersion -> this.getTargetVersion(),
(fql, key, value) -> {
if (fieldWarnings.containsKey(key)) {
FieldWarning warning = fieldWarnings.get(key).apply(key, value.toPrettyString());

warnings.add(warning);
if (warning instanceof RemovedFieldWarning || warning instanceof QueryBreakingWarning) {
return;
}
}
fql.set(getNewFieldName(fieldChanges, key), value);
}
);

if (!fieldChanges.isEmpty() || !fieldWarnings.isEmpty()) {
// map fields list
result =
result.withFields(
Expand All @@ -139,7 +150,7 @@ public MigratableQueryInformation apply(FqlService fqlService, MigratableQueryIn
);
}

result = result.withFqlQuery(objectMapper.writeValueAsString(fql));
result = result.withFqlQuery(newFql);

return result.withWarnings(new ArrayList<>(warnings));
} catch (JsonProcessingException e) {
Expand All @@ -149,51 +160,12 @@ public MigratableQueryInformation apply(FqlService fqlService, MigratableQueryIn
}

protected static String getNewFieldName(Map<String, String> fieldChanges, String oldFieldName) {
if (MigrationService.VERSION_KEY.equals(oldFieldName)) {
if (MigrationConfiguration.VERSION_KEY.equals(oldFieldName)) {
return oldFieldName;
} else if (fieldChanges.containsKey("*")) {
return fieldChanges.get("*").formatted(oldFieldName);
} else {
return fieldChanges.getOrDefault(oldFieldName, oldFieldName);
}
}

protected static ObjectNode migrateFqlTree(
Map<String, String> fieldChanges,
Map<String, BiFunction<String, String, FieldWarning>> fieldWarnings,
ObjectNode fql,
Set<Warning> warnings
) {
ObjectNode result = new ObjectMapper().createObjectNode();
// iterate through fields in source
fql
.fields()
.forEachRemaining(entry -> {
if ("$and".equals(entry.getKey())) {
ArrayNode resultContents = new ObjectMapper().createArrayNode();
((ArrayNode) entry.getValue()).elements()
.forEachRemaining(node -> {
ObjectNode innerResult = migrateFqlTree(fieldChanges, fieldWarnings, (ObjectNode) node, warnings);
// handle removed fields
if (!innerResult.isEmpty()) {
resultContents.add(innerResult);
}
});
result.set("$and", resultContents);
} else {
if (fieldWarnings.containsKey(entry.getKey())) {
FieldWarning warning = fieldWarnings
.get(entry.getKey())
.apply(entry.getKey(), entry.getValue().toPrettyString());
warnings.add(warning);
if (warning instanceof RemovedFieldWarning || warning instanceof QueryBreakingWarning) {
return;
}
}
result.set(getNewFieldName(fieldChanges, entry.getKey()), entry.getValue());
}
});

return result;
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
package org.folio.fqm.migration;

import java.util.List;
import org.folio.fqm.client.ConfigurationClient;
import org.folio.fqm.migration.strategies.V0POCMigration;
import org.folio.fqm.migration.strategies.V1ModeOfIssuanceConsolidation;
import org.folio.fqm.migration.strategies.V2ResourceTypeConsolidation;
import org.folio.fqm.migration.strategies.V3RamsonsFieldCleanup;
import org.folio.fqm.migration.strategies.V4DateFieldTimezoneAddition;
import org.springframework.stereotype.Component;

@Component
public class MigrationStrategyRepository {

// prevent re-initialization on each call
private static final List<MigrationStrategy> MIGRATION_STRATEGIES = List.of(
new V0POCMigration(),
new V1ModeOfIssuanceConsolidation(),
new V2ResourceTypeConsolidation(),
new V3RamsonsFieldCleanup()
);
private final List<MigrationStrategy> migrationStrategies;

public MigrationStrategyRepository(ConfigurationClient configurationClient) {
this.migrationStrategies =
List.of(
new V0POCMigration(),
new V1ModeOfIssuanceConsolidation(),
new V2ResourceTypeConsolidation(),
new V3RamsonsFieldCleanup(),
new V4DateFieldTimezoneAddition(configurationClient)
);
}

public List<MigrationStrategy> getMigrationStrategies() {
return MIGRATION_STRATEGIES;
return this.migrationStrategies;
}
}
104 changes: 104 additions & 0 deletions src/main/java/org/folio/fqm/migration/MigrationUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.folio.fqm.migration;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.UncheckedIOException;
import java.util.Optional;
import java.util.function.UnaryOperator;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.function.TriConsumer;
import org.folio.fqm.config.MigrationConfiguration;

@Log4j2
@UtilityClass
public class MigrationUtils {

private static final ObjectMapper objectMapper = new ObjectMapper();

/**
* Helper function to transform an FQL query. This changes a version to a new one, and runs a given
* function on each field in the query. See {@link #migrateFqlTree(ObjectNode, TriConsumer)} for more
* details on the field transformation function.
*
* @param fqlQuery The root query to migrate
* @param versionTransformer A function that takes the current (potentially null) version and
* returns the new one to be persisted in the query
* @param handler something that takes the result node, the field name, and the field's query object,
* applies some transformation, and stores the results back in result
* @throws JsonMappingException
* @throws JsonProcessingException
*/
public static String migrateFql(
String fqlQuery,
UnaryOperator<String> versionTransformer,
TriConsumer<ObjectNode, String, JsonNode> handler
) {
try {
ObjectNode fql = (ObjectNode) objectMapper.readTree(fqlQuery);

fql.set(
MigrationConfiguration.VERSION_KEY,
objectMapper.valueToTree(
versionTransformer.apply(
Optional.ofNullable(fql.get(MigrationConfiguration.VERSION_KEY)).map(JsonNode::asText).orElse(null)
)
)
);

fql = migrateFqlTree(fql, handler);

return objectMapper.writeValueAsString(fql);
} catch (JsonProcessingException e) {
log.error("Unable to process JSON", e);
throw new UncheckedIOException(e);
}
}

/**
* Call `handler` for each field in the FQL query tree, returning a new tree.
* Note that `handler` is responsible for inserting what should be left in the tree, if anything;
* if the function is a no-op, an empty FQL tree will be returned.
*
* A true "no-op" here would look like (result, key, value) -> result.set(key, value).
*
* This conveniently handles `$and`s, allowing logic to be handled on fields only.
*
* @param fql the fql node
* @param handler something that takes the result node, the field name, and the field's query object,
* applies some transformation, and stores the results back in result
* @return
*/
private static ObjectNode migrateFqlTree(ObjectNode fql, TriConsumer<ObjectNode, String, JsonNode> handler) {
ObjectNode result = new ObjectMapper().createObjectNode();
// iterate through fields in source
fql
.fields()
.forEachRemaining(entry -> {
if ("$and".equals(entry.getKey())) {
ArrayNode resultContents = new ObjectMapper().createArrayNode();
((ArrayNode) entry.getValue()).elements()
.forEachRemaining(node -> {
ObjectNode innerResult = migrateFqlTree((ObjectNode) node, handler);
// handle removed fields
if (!innerResult.isEmpty()) {
resultContents.add(innerResult);
}
});
result.set("$and", resultContents);
// ensure we don't run this on the _version
} else if (!MigrationConfiguration.VERSION_KEY.equals(entry.getKey())) {
handler.accept(result, entry.getKey(), entry.getValue());
} else {
// keep _version as-is
result.set(entry.getKey(), entry.getValue());
}
});

return result;
}
}
Loading

0 comments on commit e84114e

Please sign in to comment.