Skip to content

Commit cb5d2c3

Browse files
committed
Add "withMetadata" flag to "listContents" endpoint
- Will no longer include METADATA type files in response - Made RenderType in charge of the Aerie Metadata type for the backend, as that extension is expected to be fairly constant - Remove unneeded Optional wrapper from `listFiles` method
1 parent 2e23e6e commit cb5d2c3

6 files changed

Lines changed: 173 additions & 23 deletions

File tree

workspace-server/src/main/java/gov/nasa/jpl/aerie/workspace/server/DirectoryTree.java

Lines changed: 137 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
package gov.nasa.jpl.aerie.workspace.server;
22

33
import gov.nasa.jpl.aerie.workspace.server.postgres.RenderType;
4+
import gov.nasa.jpl.aerie.workspace.server.types.MetadataKeys;
45

56
import javax.json.Json;
67
import javax.json.JsonArray;
8+
import javax.json.JsonException;
9+
import javax.json.JsonObject;
710
import javax.json.JsonObjectBuilder;
11+
import javax.json.JsonValue;
12+
import java.io.FileNotFoundException;
13+
import java.io.FileReader;
814
import java.nio.file.Path;
15+
import java.time.Instant;
916
import java.util.List;
1017
import java.util.Map;
18+
import java.util.Optional;
1119
import java.util.TreeMap;
1220

1321
/**
@@ -29,7 +37,7 @@ public class DirectoryTree {
2937
* @param extensionMappings a map of file extensions to RenderTypes.
3038
* Used to determine the RenderType of file paths
3139
*/
32-
public DirectoryTree(Path root, List<Path> inputList, Map<String, RenderType> extensionMappings) {
40+
public DirectoryTree(Path root, List<Path> inputList, Map<String, RenderType> extensionMappings, boolean withMetadata) {
3341
if(!root.toFile().isDirectory()) {
3442
throw new IllegalArgumentException("Cannot create a DirectoryTree from a file.");
3543
}
@@ -39,7 +47,8 @@ public DirectoryTree(Path root, List<Path> inputList, Map<String, RenderType> ex
3947
if(path.toFile().isDirectory()) {
4048
this.root.addChild(new DirectoryNode(path));
4149
} else {
42-
this.root.addChild(new FileNode(path, RenderType.getRenderType(path.getFileName().toString(), extensionMappings)));
50+
final var rType = RenderType.getRenderType(path.getFileName().toString(), extensionMappings);
51+
this.root.addChild(new FileNode(path, rType, withMetadata));
4352
}
4453
}
4554
}
@@ -49,16 +58,125 @@ private static class FileNode {
4958
final String name;
5059
final Path path;
5160

61+
final Optional<JsonObject> metadata;
62+
final Optional<MetadataStatus> metadataStatus;
63+
64+
private enum MetadataStatus {
65+
ok, // Metadata file exists and is valid
66+
missing, // Metadata file is absent
67+
malformed // Metadata JSON is invalid
68+
}
69+
5270
FileNode(Path path, RenderType renderType) {
5371
this.path = path;
5472
this.renderType = renderType;
5573
this.name = path.getFileName().toString();
74+
this.metadata = Optional.empty();
75+
this.metadataStatus = Optional.empty();
76+
}
77+
78+
FileNode(Path path, RenderType renderType, boolean getMetadata) {
79+
this.path = path;
80+
this.renderType = renderType;
81+
this.name = path.getFileName().toString();
82+
83+
// Check if we even need to get the file's metadata
84+
if (!getMetadata || renderType == RenderType.METADATA) {
85+
this.metadata = Optional.empty();
86+
this.metadataStatus = Optional.empty();
87+
return;
88+
}
89+
90+
// Check if the metadata file exists
91+
final var metadataFile = this.path.resolveSibling(RenderType.toMetadataFileName(name)).toFile();
92+
if (!metadataFile.exists() || metadataFile.isDirectory()) {
93+
this.metadata = Optional.empty();
94+
this.metadataStatus = Optional.of(MetadataStatus.missing);
95+
return;
96+
}
97+
98+
// Attempt to read the metadata file
99+
final JsonObject fileContents;
100+
try(final var reader = Json.createReader(new FileReader(metadataFile))){
101+
fileContents = reader.readObject();
102+
} catch (FileNotFoundException fnf) {
103+
this.metadata = Optional.empty();
104+
this.metadataStatus = Optional.of(MetadataStatus.missing);
105+
return;
106+
} catch (JsonException je) {
107+
this.metadata = Optional.empty();
108+
this.metadataStatus = Optional.of(MetadataStatus.malformed);
109+
return;
110+
}
111+
112+
// Validate metadata file
113+
if(!validateMetadataFile(fileContents)) {
114+
this.metadata = Optional.empty();
115+
this.metadataStatus = Optional.of(MetadataStatus.malformed);
116+
return;
117+
}
118+
119+
this.metadata = Optional.of(fileContents);
120+
this.metadataStatus = Optional.of(MetadataStatus.ok);
121+
}
122+
123+
private static boolean validateMetadataFile(JsonObject metadata) throws JsonException {
124+
// Check that there's the right amount of keys
125+
if(metadata.size() > MetadataKeys.values().length || metadata.size() < MetadataKeys.mandatoryKeys.size()) {
126+
return false;
127+
}
128+
129+
// Check that all the keys are real keys and the mandatory keys are present
130+
if(!MetadataKeys.keySet.containsAll(metadata.keySet()) || !metadata.keySet().containsAll(MetadataKeys.mandatoryKeys)) {
131+
return false;
132+
}
133+
134+
// Validate that the keys have the correct types
135+
try {
136+
final var version = metadata.getString("version");
137+
// Version should be a recognized version
138+
if (!version.equals("1")) {
139+
return false;
140+
}
141+
142+
metadata.getString("createdBy");
143+
144+
final var createdAt = metadata.getString("createdAt");
145+
Instant.parse(createdAt);
146+
147+
metadata.getString("lastEditedBy");
148+
final var lastEditedAt = metadata.getString("lastEditedAt");
149+
Instant.parse(lastEditedAt);
150+
151+
152+
// Validate that the optional keys have the correct type, if they're present
153+
if (metadata.containsKey("readOnly")) {
154+
metadata.getBoolean("readOnly");
155+
}
156+
if(metadata.containsKey("user")) {
157+
metadata.getJsonObject("user");
158+
}
159+
} catch (Exception cce) {
160+
return false;
161+
}
162+
return true;
56163
}
57164

58165
JsonObjectBuilder toJsonBuilder() {
59-
return Json.createObjectBuilder()
60-
.add("name", name)
61-
.add("type", renderType.name());
166+
final var builder = Json.createObjectBuilder()
167+
.add("name", name)
168+
.add("type", renderType.name());
169+
170+
metadataStatus.ifPresent(status -> {
171+
builder.add("metadataStatus", status.name());
172+
if(status == MetadataStatus.ok && metadata.isPresent()) {
173+
builder.add("metadata", metadata.get());
174+
} else {
175+
builder.add("metadata", JsonValue.NULL);
176+
}
177+
});
178+
179+
return builder;
62180
}
63181
}
64182

@@ -93,7 +211,12 @@ void addChild(FileNode child) {
93211
@Override
94212
JsonObjectBuilder toJsonBuilder() {
95213
final var contentsArray = Json.createArrayBuilder();
96-
children.forEach((key, child) -> contentsArray.add(child.toJsonBuilder()));
214+
children.forEach((key, child) -> {
215+
// Skip Metadata files by default
216+
if(child.renderType != RenderType.METADATA) {
217+
contentsArray.add(child.toJsonBuilder());
218+
}
219+
});
97220
return Json.createObjectBuilder()
98221
.add("name", name)
99222
.add("type", renderType.name())
@@ -102,11 +225,17 @@ JsonObjectBuilder toJsonBuilder() {
102225
}
103226

104227
/**
105-
* Build a JsonArray representing the contents of this DirectoryTree
228+
* Build a JsonArray representing the contents of this DirectoryTree.
229+
* By default, skips METADATA type files.
106230
*/
107231
public JsonArray toJson() {
108232
final var contentsArray = Json.createArrayBuilder();
109-
root.children.forEach((key, child) -> contentsArray.add(child.toJsonBuilder()));
233+
root.children.forEach((key, child) -> {
234+
// Skip Metadata files
235+
if(child.renderType != RenderType.METADATA) {
236+
contentsArray.add(child.toJsonBuilder());
237+
}
238+
});
110239
return contentsArray.build();
111240
}
112241
}

workspace-server/src/main/java/gov/nasa/jpl/aerie/workspace/server/WorkspaceBindings.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ String fileName() {
8080
}
8181

8282
String metadataFileName() {
83-
return "."+fileName()+".meta.seqdev";
83+
return RenderType.toMetadataFileName(fileName());
8484
}
8585
}
8686

@@ -378,19 +378,17 @@ private void listWorkspaceContents(Context context) {
378378
private void listContents(Context context) {
379379
final var workspaceId = Integer.parseInt(context.pathParam("workspaceId"));
380380

381-
final Optional<Path> directoryPath;
382-
if(context.pathParamMap().containsKey("path")) {
383-
directoryPath = Optional.of(Path.of(context.pathParam("path")));
384-
} else {
385-
directoryPath = Optional.empty();
386-
}
381+
final var directoryPath = context.pathParamMap().containsKey("path")
382+
? Path.of(context.pathParam("path"))
383+
: Path.of("");
387384

388385
// Query params
389386
final var depthString = context.queryParam("depth");
390387
final int depth = depthString != null ? Integer.parseInt(depthString) : -1;
388+
final boolean withMetadata = Boolean.parseBoolean(context.queryParam("withMetadata"));
391389

392390
try {
393-
final var fileTree = workspaceService.listFiles(workspaceId, directoryPath, depth);
391+
final var fileTree = workspaceService.listFiles(workspaceId, directoryPath, depth, withMetadata);
394392
if (fileTree == null) {
395393
context.status(404).json(new FormattedError("No such directory."));
396394
return;

workspace-server/src/main/java/gov/nasa/jpl/aerie/workspace/server/WorkspaceFileSystemService.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ private Path resolveMetadataPath(final Path rootPath, final Path filePath) throw
127127
}
128128

129129
// Convert base file path to metadata file path
130-
final var metadataFileName = "."+baseFilePath.getFileName()+".meta.seqdev";
130+
final var metadataFileName = RenderType.toMetadataFileName(baseFilePath.getFileName().toString());
131131
final var metadataFilePath = baseFilePath.resolveSibling(metadataFileName); // Metadata files are hidden sibling files
132132

133133
if(metadataFilePath.toFile().isDirectory()) {
@@ -404,20 +404,20 @@ public boolean deleteFile(final int workspaceId, final Path filePath)
404404

405405
//region Directory Operations
406406
@Override
407-
public DirectoryTree listFiles(final int workspaceId, final Optional<Path> directoryPath, final int depth)
407+
public DirectoryTree listFiles(final int workspaceId, final Path directoryPath, final int depth, final boolean withMetadata)
408408
throws SQLException, NoSuchWorkspaceException, IOException {
409-
final var path = resolveReadingPath(workspaceId, directoryPath.orElse(Path.of("")));
409+
final var path = resolveReadingPath(workspaceId, directoryPath);
410410

411411
if(!path.toFile().isDirectory()) {
412412
return null;
413413
}
414414

415-
// Converting our API to the Files API
415+
// Convert to our API from the Files API
416416
final var walkDepth = depth == -1 ? Integer.MAX_VALUE : depth + 1;
417417
try(final Stream<Path> walkOutput = Files.walk(path, walkDepth)) {
418418
final var walkList = new ArrayList<>(walkOutput.toList());
419419
walkList.removeFirst(); // remove the initial path
420-
return new DirectoryTree(path, walkList, postgresRepository.getExtensionMapping());
420+
return new DirectoryTree(path, walkList, postgresRepository.getExtensionMapping(), withMetadata);
421421
}
422422
}
423423

workspace-server/src/main/java/gov/nasa/jpl/aerie/workspace/server/WorkspaceService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ boolean moveFile(final int oldWorkspaceId, final Path oldFilePath, final int new
8282
boolean deleteFile(final int workspaceId, final Path filePath)
8383
throws NoSuchWorkspaceException, WorkspaceFileOpException;
8484

85-
DirectoryTree listFiles(final int workspaceId, final Optional<Path> directoryPath, final int depth)
85+
DirectoryTree listFiles(final int workspaceId, final Path directoryPath, final int depth, final boolean withMetadata)
8686
throws SQLException, NoSuchWorkspaceException, IOException;
8787

8888
boolean createDirectory(final int workspaceId, final Path directoryPath)

workspace-server/src/main/java/gov/nasa/jpl/aerie/workspace/server/postgres/RenderType.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,18 @@ public static RenderType getRenderType(String fileName, Map<String, RenderType>
5757

5858
return RenderType.UNKNOWN;
5959
}
60+
61+
// Helpers for SeqDev Metadata Files
62+
public static final String aerieMetadataExtension = ".meta.seqdev";
63+
public static boolean isAerieMetadataFile(String fileName) {
64+
return getRenderType(fileName, Map.of(aerieMetadataExtension, METADATA)) == METADATA;
65+
}
66+
67+
/**
68+
* Convert a fileName to the naming pattern of a PlanDev metadata file.
69+
* @param fileName The base file's filename
70+
*/
71+
public static String toMetadataFileName(String fileName) {
72+
return "." + fileName + aerieMetadataExtension;
73+
}
6074
}

workspace-server/src/main/java/gov/nasa/jpl/aerie/workspace/server/types/MetadataKeys.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,14 @@ public enum MetadataKeys {
1111
readOnly,
1212
user;
1313

14-
public static final Set<String> whitelist = Set.of("readOnly", "user");
14+
public static final Set<String> whitelist = Set.of(readOnly.name(), user.name());
15+
public static final Set<String> keySet = Set.of(
16+
version.name(),
17+
createdBy.name(), createdAt.name(),
18+
lastEditedBy.name(), lastEditedAt.name(),
19+
readOnly.name(), user.name());
20+
public static final Set<String> mandatoryKeys = Set.of(version.name(),
21+
createdBy.name(), createdAt.name(),
22+
lastEditedBy.name(), lastEditedAt.name());
23+
1524
}

0 commit comments

Comments
 (0)