Skip to content

Commit

Permalink
Handle misformatted and changed saved state messages
Browse files Browse the repository at this point in the history
  • Loading branch information
spyrkob committed Oct 15, 2024
1 parent 008ba37 commit 2404088
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
package org.wildfly.prospero.installation.git;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import org.wildfly.channel.ChannelMapper;
import org.wildfly.channel.version.VersionMatcher;
import org.wildfly.prospero.ProsperoLogger;
import org.wildfly.prospero.api.SavedState;
import org.wildfly.prospero.metadata.ManifestVersionRecord;

import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
* generates commit messages used in the git history storage
*/
class SavedStateParser {

public static final String SCHEMA_VERSION_1_0_0 = "1.0.0";
private static final String SCHEMA_1_0_0_FILE = "org/wildfly/prospero/savedstate/v1.0.0/schema.json";
private static final Map<String, JsonSchema> SCHEMAS = new HashMap();
private static final ObjectMapper JSON_MAPPER = new ObjectMapper(new JsonFactory());
private static final JsonSchemaFactory SCHEMA_FACTORY = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).jsonMapper(JSON_MAPPER).build();

static {
SCHEMAS.put(SCHEMA_VERSION_1_0_0, SCHEMA_FACTORY.getSchema(ChannelMapper.class.getClassLoader().getResourceAsStream(SCHEMA_1_0_0_FILE)));
}

String write(SavedState.Type recordType, ManifestVersionRecord currentVersions) throws IOException {
if (currentVersions == null) {
Expand All @@ -29,8 +51,9 @@ String write(SavedState.Type recordType, ManifestVersionRecord currentVersions)

SavedState read(String hash, Instant now, String text) throws IOException {
final SavedState.Type type;
final String originalText = text;
final String msg;
final List<SavedState.Version> versions = new ArrayList<>();
List<SavedState.Version> versions = Collections.emptyList();

if (!text.contains(" ")) {
// the message is only type
Expand All @@ -42,6 +65,10 @@ SavedState read(String hash, Instant now, String text) throws IOException {
text = text.substring(endOfType + 1);
}

if (type == SavedState.Type.UNKNOWN) {
return new SavedState(hash, now, type, shortMessage(originalText), versions);
}

if (!text.contains("\n\n")) {
// the message contains type and short description
msg = text;
Expand All @@ -52,23 +79,96 @@ SavedState read(String hash, Instant now, String text) throws IOException {

text = text.substring(endOfShortDesc).trim();
if (!text.isEmpty()) {
final ManifestVersionRecord record = JSON_MAPPER.readValue(text, ManifestVersionRecord.class);
record.getMavenManifests().forEach(m -> versions.add(new SavedState.Version(
m.getGroupId() + ":" + m.getArtifactId(),
m.getVersion(), m.getDescription())));
record.getUrlManifests().forEach(m -> versions.add(new SavedState.Version(
m.getUrl(), m.getHash(), m.getDescription()
)));
record.getOpenManifests().forEach(m -> versions.add(new SavedState.Version(
"unknown", "unknown", m.getSummary()
)));
try {
versions = readVersions(text);
} catch (JsonParseException e) {
ProsperoLogger.ROOT_LOGGER.error("Unable to parse a history record [" + text + "]", e);
}
}
}

return new SavedState(hash, now, type, msg.isEmpty() ? null : msg, versions);
return new SavedState(hash, now, type, msg, versions);
}

private static String shortMessage(String originalText) {
originalText = originalText.trim();

if (originalText.isEmpty()) {
return null;
} else if (originalText.contains("\n")) {
return originalText.split("\n") [0].trim();
} else {
return originalText;
}
}

private static List<SavedState.Version> readVersions(String text) throws JsonProcessingException {
JsonNode node = JSON_MAPPER.readTree(text);
JsonSchema schema = getSchema(node);

if (schema == null) {
return Collections.emptyList();
}

Set<ValidationMessage> validationMessages = schema.validate(node);
if (!validationMessages.isEmpty()) {
for (ValidationMessage validationMessage : validationMessages) {
ProsperoLogger.ROOT_LOGGER.error("Invalid Saved State in history " + validationMessage);
}
return Collections.emptyList();
}

final ManifestVersionRecord record = JSON_MAPPER.readValue(text, ManifestVersionRecord.class);
final List<SavedState.Version> versions = new ArrayList<>();
record.getMavenManifests().forEach(m -> versions.add(new SavedState.Version(
m.getGroupId() + ":" + m.getArtifactId(),
m.getVersion(), m.getDescription())));
record.getUrlManifests().forEach(m -> versions.add(new SavedState.Version(
m.getUrl(), m.getHash(), m.getDescription()
)));
record.getOpenManifests().forEach(m -> versions.add(new SavedState.Version(
"unknown", "unknown", m.getSummary()
)));
return versions;
}

private String toJson(ManifestVersionRecord currentVersions) throws IOException {
return JSON_MAPPER.writeValueAsString(currentVersions);
}

private static JsonSchema getSchema(JsonNode node) {
JsonNode schemaVersion = node.path("schemaVersion");
String version = schemaVersion.asText();
if (version == null || version.isEmpty()) {
ProsperoLogger.ROOT_LOGGER.error("Invalid Saved State record in history - schema version is not specified");
return null;
}

JsonSchema schema = SCHEMAS.get(version);
if (schema != null) {
return schema;
} else {
final String[] parts = version.split("\\.");
StringBuilder versionPattern = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
if (i == 0) {
versionPattern.append(parts[i]);
} else if (i == parts.length -1 ) {
versionPattern.append(".*");
} else {
versionPattern.append("\\.").append(parts[i]);
}
}

final Optional<String> latestCompatibleSchemaVersion = SCHEMAS.keySet().stream()
.filter(v -> v.matches(versionPattern.toString()))
.max(VersionMatcher.COMPARATOR);

return latestCompatibleSchemaVersion.map(SCHEMAS::get)
.orElseGet(()->{
ProsperoLogger.ROOT_LOGGER.error("Invalid Saved State record in history - unknown schema version " + schemaVersion);
return null;
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"$id": "https://wildfly.org/prospero/savedstate/v1.0.0/schema.json",
"$schema": "https://json-schema.org/draft/2019-09/schema#",
"type": "object",
"required": ["schemaVersion"],
"properties": {
"schemaVersion": {
"description": "The version of the schema defining a saved state.",
"type": "string",
"pattern": "^[0-9]+.[0-9]+.[0-9]+$"
},
"maven": {
"type": "array",
"items": {
"type" : "object",
"properties": {
"groupId": {
"description": "GroupID Maven coordinate of the manifest",
"type": "string"
},
"artifactId": {
"description": "ArtifactID Maven coordinate of the manifest",
"type": "string"
},
"version": {
"description": "Version Maven coordinate of the manifest",
"type": "string"
},
"description": {
"description": "A human readable description of the manifest version",
"type": "string"
}
}
}
},
"url": {
"type": "array",
"items": {
"type" : "object",
"properties": {
"url": {
"description": "A URL the manifest was resolved from",
"type": "string"
},
"hash": {
"description": "A SHA-1 hash of the manifest content",
"type": "string"
},
"description": {
"description": "A human readable description of the manifest version",
"type": "string"
}
}
}
},
"open": {
"type": "array",
"items": {
"type" : "object",
"properties": {
"repos": {
"type": "array",
"description": "Repositories used to assemble the manifest",
"items": {
"type": "string"
}
},
"strategy": {
"description": "Resolution strategy for the channel",
"type": "string"
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void readSavedStateOperationOnly() throws Exception {
final SavedState state = savedStateParser.read(A_HASH, A_TIMESTAMP, SavedState.Type.INSTALL.toString());

assertThat(state)
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.INSTALL, null, Collections.emptyList()));
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.INSTALL, "", Collections.emptyList()));
}

@Test
Expand All @@ -47,13 +47,63 @@ public void readSavedStateFullRecord() throws Exception {
Collections.emptyList(), Collections.emptyList());
final String msg = savedStateParser.write(SavedState.Type.INSTALL, record);

System.out.println(msg);

final SavedState state = savedStateParser.read(A_HASH, A_TIMESTAMP, msg);

assertThat(state)
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.INSTALL, "[org.foo:bar::1.0.0]",
List.of(new SavedState.Version("org.foo:bar", "1.0.0", "Update 1"))));
}

@Test
public void readSavedStateWithAdditionalFieldRecord() throws Exception {
final String msg = "INSTALL [org.foo:bar::1.0.0]\n\n" + "{\"schemaVersion\":\"1.0.0\",\"maven\":" +
"[{\"groupId\":\"org.foo\",\"artifactId\":\"bar\",\"version\":\"1.0.0\",\"description\":\"Update 1\", \"idontexit\":\"foobar\"}]}";

final SavedState state = savedStateParser.read(A_HASH, A_TIMESTAMP, msg);

assertThat(state)
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.INSTALL, "[org.foo:bar::1.0.0]",
List.of(new SavedState.Version("org.foo:bar", "1.0.0", "Update 1"))));
}

@Test
public void readUnknownMicroSchemaVersion() throws Exception {
final String msg = "INSTALL [org.foo:bar::1.0.0]\n\n" + "{\"schemaVersion\":\"1.0.999\",\"maven\":" +
"[{\"groupId\":\"org.foo\",\"artifactId\":\"bar\",\"version\":\"1.0.0\",\"description\":\"Update 1\", \"idontexit\":\"foobar\"}]}";

final SavedState state = savedStateParser.read(A_HASH, A_TIMESTAMP, msg);

assertThat(state)
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.INSTALL, "[org.foo:bar::1.0.0]",
List.of(new SavedState.Version("org.foo:bar", "1.0.0", "Update 1"))));
}

@Test
public void readUnknownMajorSchemaVersion() throws Exception {
final String msg = "INSTALL [org.foo:bar::1.0.0]\n\n" + "{\"schemaVersion\":\"999999.0.0\",\"maven\":" +
"[{\"groupId\":\"org.foo\",\"artifactId\":\"bar\",\"version\":\"1.0.0\",\"description\":\"Update 1\", \"idontexit\":\"foobar\"}]}";

final SavedState state = savedStateParser.read(A_HASH, A_TIMESTAMP, msg);

assertThat(state)
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.INSTALL, "[org.foo:bar::1.0.0]",
Collections.emptyList()));
}

@Test
public void readNoSchemaVersion() throws Exception {
final String msg = "INSTALL [org.foo:bar::1.0.0]\n\n" + "{\"maven\":" +
"[{\"groupId\":\"org.foo\",\"artifactId\":\"bar\",\"version\":\"1.0.0\",\"description\":\"Update 1\", \"idontexit\":\"foobar\"}]}";

final SavedState state = savedStateParser.read(A_HASH, A_TIMESTAMP, msg);

assertThat(state)
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.INSTALL, "[org.foo:bar::1.0.0]",
Collections.emptyList()));
}

@Test
public void readSavedStateShortStatusWithTrailingNewLines() throws Exception {
final String msg = SavedState.Type.INSTALL + " [foo:bar]\n\n";
Expand All @@ -64,4 +114,34 @@ public void readSavedStateShortStatusWithTrailingNewLines() throws Exception {
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.INSTALL, "[foo:bar]", Collections.emptyList()));
}

@Test
public void garbageSingleLineIn_ProducesUnknownStateWithShortMessage() throws Exception {
final String msg = "A random text that makes no sense";

final SavedState state = savedStateParser.read(A_HASH, A_TIMESTAMP, msg);

assertThat(state)
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.UNKNOWN, msg, Collections.emptyList()));
}

@Test
public void garbageMultiLineIn_ProducesUnknownStateWithShortMessage() throws Exception {
final String msg = "A random text that \n\n makes no sense";

final SavedState state = savedStateParser.read(A_HASH, A_TIMESTAMP, msg);

assertThat(state)
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.UNKNOWN, "A random text that", Collections.emptyList()));
}

@Test
public void garbageVersions_ProducesCorrectStateTypeWithShortMessage() throws Exception {
final String msg = "UPDATE A random text that \n\n makes no sense";

final SavedState state = savedStateParser.read(A_HASH, A_TIMESTAMP, msg);

assertThat(state)
.isEqualTo(new SavedState(A_HASH, A_TIMESTAMP, SavedState.Type.UPDATE, "A random text that", Collections.emptyList()));
}

}

0 comments on commit 2404088

Please sign in to comment.