Skip to content

Commit 5a193c2

Browse files
committed
Add support for annotations file
1 parent 734163a commit 5a193c2

8 files changed

Lines changed: 217 additions & 34 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@
185185
<executions>
186186
<execution>
187187
<id>javadoc</id>
188-
<phase>verify</phase>
189188
<goals>
190189
<goal>javadoc</goal>
191190
<goal>jar</goal>
192191
</goals>
192+
<phase>verify</phase>
193193
<configuration>
194194
<show>public</show>
195195
</configuration>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package land.oras;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.stream.Collectors;
6+
import land.oras.utils.JsonUtils;
7+
import org.jspecify.annotations.NullMarked;
8+
9+
/**
10+
* Record for annotations
11+
*
12+
* @param manifestAnnotations Annotations for the manifest
13+
* @param configAnnotations Annotations for the config
14+
* @param filesAnnotations Annotations for the layers/files
15+
*/
16+
@NullMarked
17+
public record Annotations(
18+
Map<String, String> configAnnotations,
19+
Map<String, String> manifestAnnotations,
20+
Map<String, Map<String, String>> filesAnnotations) {
21+
22+
/**
23+
* Create a new annotations record with only manifest annotations
24+
* @param manifestAnnotations The manifest annotations
25+
*/
26+
public static Annotations ofManifest(Map<String, String> manifestAnnotations) {
27+
return new Annotations(new HashMap<>(), manifestAnnotations, new HashMap<>());
28+
}
29+
30+
/**
31+
* Create a new annotations record with only config annotations
32+
* @param configAnnotations The config annotations
33+
* @return The annotations
34+
*/
35+
public static Annotations ofConfig(Map<String, String> configAnnotations) {
36+
return new Annotations(configAnnotations, new HashMap<>(), new HashMap<>());
37+
}
38+
39+
/**
40+
* Empty annotations
41+
* @return The empty annotations
42+
*/
43+
public static Annotations empty() {
44+
return new Annotations(new HashMap<>(), new HashMap<>(), new HashMap<>());
45+
}
46+
47+
/**
48+
* Get the manifest annotations for a file
49+
* @param key The key
50+
* @return The annotations
51+
*/
52+
public Map<String, String> getFileAnnotations(String key) {
53+
return this.filesAnnotations().getOrDefault(key, new HashMap<>());
54+
}
55+
56+
/**
57+
* Annotations file format
58+
*/
59+
private static class AnnotationFile extends HashMap<String, Map<String, String>> {
60+
/**
61+
* Get the manifest annotations
62+
*
63+
* @return The manifest annotations
64+
*/
65+
public Map<String, String> getManifestAnnotations() {
66+
return this.getOrDefault("$manifest", new HashMap<>());
67+
}
68+
69+
/**
70+
* Get the config annotations
71+
*
72+
* @return The config annotations
73+
*/
74+
public Map<String, String> getConfigAnnotations() {
75+
return this.getOrDefault("$config", new HashMap<>());
76+
}
77+
78+
/**
79+
* Get the files annotations without the manifest and config annotations
80+
*
81+
* @return The files annotations
82+
*/
83+
public Map<String, Map<String, String>> getFilesAnnotations() {
84+
return this.entrySet().stream()
85+
.filter(entry -> !"$manifest".equals(entry.getKey())
86+
&& !"$config".equals(entry.getKey())) // Filter out $manifest and $config
87+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
88+
}
89+
90+
/**
91+
* Get the annotations for a file
92+
* @param key The key
93+
* @return The annotations
94+
*/
95+
public Map<String, String> getFileAnnotations(String key) {
96+
return this.getOrDefault(key, new HashMap<>());
97+
}
98+
}
99+
100+
/**
101+
* Convert the annotations from a JSON string
102+
*
103+
* @param json The JSON string
104+
* @return The annotations
105+
*/
106+
public static Annotations fromJson(String json) {
107+
AnnotationFile file = JsonUtils.fromJson(json, AnnotationFile.class);
108+
return new Annotations(file.getConfigAnnotations(), file.getManifestAnnotations(), file.getFilesAnnotations());
109+
}
110+
111+
/**
112+
* Convert the annotations to a JSON string
113+
*
114+
* @return The JSON string
115+
*/
116+
public String toJson() {
117+
AnnotationFile file = new AnnotationFile();
118+
file.put("$manifest", manifestAnnotations());
119+
file.put("$config", configAnnotations());
120+
file.putAll(filesAnnotations());
121+
return JsonUtils.toJson(file);
122+
}
123+
}

src/main/java/land/oras/Config.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package land.oras;
22

33
import java.util.Base64;
4-
import java.util.Collections;
54
import java.util.Map;
65
import land.oras.utils.Const;
76
import land.oras.utils.JsonUtils;
@@ -20,6 +19,7 @@ public final class Config {
2019

2120
/**
2221
* Annotations for the layer
22+
* Can be nullable due to serialization
2323
*/
2424
private final @Nullable Map<String, String> annotations;
2525

@@ -35,17 +35,12 @@ public final class Config {
3535
* @param digest The digest
3636
* @param size The size
3737
*/
38-
private Config(
39-
String mediaType,
40-
String digest,
41-
long size,
42-
@Nullable String data,
43-
@Nullable Map<String, String> annotations) {
38+
private Config(String mediaType, String digest, long size, @Nullable String data, Annotations annotations) {
4439
this.mediaType = mediaType;
4540
this.digest = digest;
4641
this.size = size;
4742
this.data = data;
48-
this.annotations = annotations;
43+
this.annotations = Map.copyOf(annotations.configAnnotations());
4944
}
5045

5146
/**
@@ -77,7 +72,7 @@ public long getSize() {
7772
* @param annotations The annotations
7873
* @return The new config
7974
*/
80-
public Config withAnnotations(Map<String, String> annotations) {
75+
public Config withAnnotations(Annotations annotations) {
8176
return new Config(mediaType, digest, size, data, annotations);
8277
}
8378

@@ -100,7 +95,7 @@ public Map<String, String> getAnnotations() {
10095
if (annotations == null) {
10196
return Map.of();
10297
}
103-
return Collections.unmodifiableMap(annotations);
98+
return annotations;
10499
}
105100

106101
/**
@@ -130,6 +125,6 @@ public static Config empty() {
130125
"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
131126
2,
132127
"e30=",
133-
null);
128+
Annotations.empty());
134129
}
135130
}

src/main/java/land/oras/Manifest.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ private Manifest(
3434
String artifactType,
3535
Config config,
3636
List<Layer> layers,
37-
Map<String, String> annotations) {
37+
Annotations annotations) {
3838
this.schemaVersion = schemaVersion;
3939
this.mediaType = mediaType;
4040
this.artifactType = artifactType;
4141
this.config = config;
4242
this.layers = layers;
43-
this.annotations = annotations;
43+
this.annotations = Map.copyOf(annotations.manifestAnnotations());
4444
}
4545

4646
/**
@@ -100,7 +100,8 @@ public Map<String, String> getAnnotations() {
100100
* @return The manifest
101101
*/
102102
public Manifest withArtifactType(String artifactType) {
103-
return new Manifest(schemaVersion, mediaType, artifactType, config, layers, annotations);
103+
return new Manifest(
104+
schemaVersion, mediaType, artifactType, config, layers, Annotations.ofManifest(annotations));
104105
}
105106

106107
/**
@@ -109,7 +110,8 @@ public Manifest withArtifactType(String artifactType) {
109110
* @return The manifest
110111
*/
111112
public Manifest withLayers(List<Layer> layers) {
112-
return new Manifest(schemaVersion, mediaType, artifactType, config, layers, annotations);
113+
return new Manifest(
114+
schemaVersion, mediaType, artifactType, config, layers, Annotations.ofManifest(annotations));
113115
}
114116

115117
/**
@@ -118,7 +120,8 @@ public Manifest withLayers(List<Layer> layers) {
118120
* @return The manifest
119121
*/
120122
public Manifest withConfig(Config config) {
121-
return new Manifest(schemaVersion, mediaType, artifactType, config, layers, annotations);
123+
return new Manifest(
124+
schemaVersion, mediaType, artifactType, config, layers, Annotations.ofManifest(annotations));
122125
}
123126

124127
/**
@@ -127,7 +130,8 @@ public Manifest withConfig(Config config) {
127130
* @return The manifest
128131
*/
129132
public Manifest withAnnotations(Map<String, String> annotations) {
130-
return new Manifest(schemaVersion, mediaType, artifactType, config, layers, annotations);
133+
return new Manifest(
134+
schemaVersion, mediaType, artifactType, config, layers, Annotations.ofManifest(annotations));
131135
}
132136

133137
/**
@@ -152,6 +156,6 @@ public static Manifest fromJson(String json) {
152156
* @return The empty manifest
153157
*/
154158
public static Manifest empty() {
155-
return new Manifest(2, Const.DEFAULT_MANIFEST_MEDIA_TYPE, null, Config.empty(), List.of(), Map.of());
159+
return new Manifest(2, Const.DEFAULT_MANIFEST_MEDIA_TYPE, null, Config.empty(), List.of(), Annotations.empty());
156160
}
157161
}

src/main/java/land/oras/Registry.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ public void deleteBlob(ContainerRef containerRef) {
179179
* @return The manifest
180180
*/
181181
public Manifest pushArtifact(ContainerRef containerRef, Path... paths) {
182-
return pushArtifact(containerRef, null, Map.of(), Config.empty(), paths);
182+
return pushArtifact(containerRef, null, Annotations.empty(), Config.empty(), paths);
183183
}
184184

185185
/**
@@ -190,7 +190,7 @@ public Manifest pushArtifact(ContainerRef containerRef, Path... paths) {
190190
* @return The manifest
191191
*/
192192
public Manifest pushArtifact(ContainerRef containerRef, String artifactType, Path... paths) {
193-
return pushArtifact(containerRef, artifactType, Map.of(), Config.empty(), paths);
193+
return pushArtifact(containerRef, artifactType, Annotations.empty(), Config.empty(), paths);
194194
}
195195

196196
/**
@@ -202,7 +202,7 @@ public Manifest pushArtifact(ContainerRef containerRef, String artifactType, Pat
202202
* @return The manifest
203203
*/
204204
public Manifest pushArtifact(
205-
ContainerRef containerRef, String artifactType, Map<String, String> annotations, Path... paths) {
205+
ContainerRef containerRef, String artifactType, Annotations annotations, Path... paths) {
206206
return pushArtifact(containerRef, artifactType, annotations, Config.empty(), paths);
207207
}
208208

@@ -240,25 +240,24 @@ public void pullArtifact(ContainerRef containerRef, Path path) {
240240
* Upload an ORAS artifact
241241
* @param containerRef The container
242242
* @param artifactType The artifact type. Can be null
243-
* @param manifestAnnotations The annotations
243+
* @param annotations The annotations
244244
* @param config The config
245245
* @param paths The paths
246246
* @return The manifest
247247
*/
248248
public Manifest pushArtifact(
249249
ContainerRef containerRef,
250250
@Nullable String artifactType,
251-
@Nullable Map<String, String> manifestAnnotations,
251+
Annotations annotations,
252252
@Nullable Config config,
253253
Path... paths) {
254254
Manifest manifest = Manifest.empty();
255255
if (artifactType != null) {
256256
manifest = manifest.withArtifactType(artifactType);
257257
}
258-
if (manifestAnnotations != null) {
259-
manifest = manifest.withAnnotations(manifestAnnotations);
260-
}
258+
manifest = manifest.withAnnotations(annotations.manifestAnnotations());
261259
if (config != null) {
260+
config = config.withAnnotations(annotations);
262261
manifest = manifest.withConfig(config);
263262
}
264263
List<Layer> layers = new ArrayList<>();
@@ -269,7 +268,10 @@ public Manifest pushArtifact(
269268
String fileName = path.getFileName().toString();
270269
boolean isDirectory = false;
271270

272-
Map<String, String> layerAnnotations = new HashMap<>();
271+
// Add layer annotation for the specific file
272+
Map<String, String> layerAnnotations = new HashMap<>(annotations.getFileAnnotations(fileName));
273+
274+
// Add title annotation
273275
layerAnnotations.put(Const.ANNOTATION_TITLE, fileName);
274276

275277
// Compress directory to tar.gz
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package land.oras;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.Map;
6+
import org.junit.jupiter.api.Test;
7+
8+
public class AnnotationsTest {
9+
10+
@Test
11+
public void fromJson() {
12+
Annotations annotations = Annotations.fromJson(sampleAnnotations());
13+
assertEquals(1, annotations.configAnnotations().size());
14+
assertEquals(1, annotations.manifestAnnotations().size());
15+
assertEquals(1, annotations.filesAnnotations().size());
16+
assertEquals("world", annotations.configAnnotations().get("hello"));
17+
assertEquals("bar", annotations.manifestAnnotations().get("foo"));
18+
assertEquals(
19+
"more cream", annotations.filesAnnotations().get("cake.txt").get("fun"));
20+
assertEquals("more cream", annotations.getFileAnnotations("cake.txt").get("fun"));
21+
}
22+
23+
@Test
24+
public void toJson() {
25+
Annotations annotations = new Annotations(
26+
Map.of("hello", "world"), Map.of("foo", "bar"), Map.of("cake.txt", Map.of("fun", "more cream")));
27+
assertEquals(1, annotations.configAnnotations().size());
28+
assertEquals(1, annotations.manifestAnnotations().size());
29+
assertEquals(1, annotations.filesAnnotations().size());
30+
assertEquals(Annotations.fromJson(sampleAnnotations()).toJson(), annotations.toJson());
31+
32+
// Manifest annotations only
33+
annotations = Annotations.ofManifest(Map.of("foo", "bar"));
34+
assertEquals(0, annotations.configAnnotations().size());
35+
assertEquals(1, annotations.manifestAnnotations().size());
36+
assertEquals(0, annotations.filesAnnotations().size());
37+
38+
// Config annotations only
39+
annotations = Annotations.ofConfig(Map.of("hello", "world"));
40+
assertEquals(1, annotations.configAnnotations().size());
41+
assertEquals(0, annotations.manifestAnnotations().size());
42+
assertEquals(0, annotations.filesAnnotations().size());
43+
}
44+
45+
private String sampleAnnotations() {
46+
return """
47+
{
48+
"$config": {
49+
"hello": "world"
50+
},
51+
"$manifest": {
52+
"foo": "bar"
53+
},
54+
"cake.txt": {
55+
"fun": "more cream"
56+
}
57+
}
58+
""";
59+
}
60+
}

0 commit comments

Comments
 (0)