Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
76 changes: 76 additions & 0 deletions src/main/java/land/oras/ArtifactType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*-
* =LICENSE=
* ORAS Java SDK
* ===
* Copyright (C) 2024 - 2025 ORAS
* ===
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =LICENSEEND=
*/

package land.oras;

import land.oras.exception.OrasException;
import land.oras.utils.Const;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* Class for artifact type
*/
@NullMarked
public class ArtifactType {

private final String mediaType;

private ArtifactType(String mediaType) {
this.mediaType = mediaType;
}

/**
* Get the media type
* @return The media type
*/
public String getMediaType() {
return mediaType;
}

@Override
public String toString() {
return mediaType;
}

/**
* Create an artifact type
* @param artifactType The artifact type. Can be null
* @return The artifact type
*/
public static ArtifactType from(@Nullable String artifactType) {
if (artifactType == null) {
return unknown();
}
// Must match https://datatracker.ietf.org/doc/html/rfc6838
if (!artifactType.matches("^[a-zA-Z0-9!#$&-^_]+/[a-zA-Z0-9!#$&-^_]+$")) {
throw new OrasException("Invalid artifact type: %s".formatted(artifactType));
}
return new ArtifactType(artifactType);
}

/**
* Create an unknown artifact type
* @return The unknown artifact type
*/
public static ArtifactType unknown() {
return new ArtifactType(Const.DEFAULT_ARTIFACT_MEDIA_TYPE);
}
}
4 changes: 2 additions & 2 deletions src/main/java/land/oras/ContainerRef.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,12 @@ public String getTagsPath() {
* @param artifactType The optional artifact type
* @return The referrers URL
*/
public String getReferrersPath(@Nullable String artifactType) {
public String getReferrersPath(@Nullable ArtifactType artifactType) {
if (artifactType == null) {
return "%s/referrers/%s".formatted(getApiPrefix(), digest);
}
return "%s/referrers/%s?artifactType=%s"
.formatted(getApiPrefix(), digest, URLEncoder.encode(artifactType, StandardCharsets.UTF_8));
.formatted(getApiPrefix(), digest, URLEncoder.encode(artifactType.toString(), StandardCharsets.UTF_8));
}

/**
Expand Down
50 changes: 25 additions & 25 deletions src/main/java/land/oras/Manifest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import land.oras.utils.JsonUtils;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

/**
* Class for manifest
Expand All @@ -50,15 +51,15 @@ public final class Manifest {
private Manifest(
int schemaVersion,
String mediaType,
String artifactType,
ArtifactType artifactType,
ManifestDescriptor descriptor,
Config config,
Subject subject,
List<Layer> layers,
Annotations annotations) {
this.schemaVersion = schemaVersion;
this.mediaType = mediaType;
this.artifactType = artifactType;
this.artifactType = artifactType != null ? artifactType.getMediaType() : null;
this.descriptor = descriptor;
this.config = config;
this.subject = subject;
Expand Down Expand Up @@ -86,8 +87,15 @@ public String getMediaType() {
* Get the artifact type
* @return The artifact type
*/
public String getArtifactType() {
return artifactType;
public @NonNull ArtifactType getArtifactType() {
if (artifactType != null) {
return ArtifactType.from(artifactType);
}
if (config != null) {
return ArtifactType.from(
config.getMediaType() != null ? config.getMediaType() : Const.DEFAULT_ARTIFACT_MEDIA_TYPE);
}
return ArtifactType.unknown();
}

/**
Expand All @@ -98,21 +106,6 @@ public ManifestDescriptor getDescriptor() {
return descriptor;
}

/**
* Determine the artifact type from artifact type or config media type
* @return The artifact type
*/
public @NonNull String determineArtifactType() {
// If artifact type is not set, return the media type from config
if (artifactType != null) {
return artifactType;
}
if (config != null) {
return config.getMediaType() != null ? config.getMediaType() : Const.DEFAULT_ARTIFACT_MEDIA_TYPE;
}
return Const.DEFAULT_ARTIFACT_MEDIA_TYPE;
}

/**
* Get the config
* @return The config
Expand Down Expand Up @@ -153,7 +146,7 @@ public Map<String, String> getAnnotations() {
* @param artifactType The artifact type
* @return The manifest
*/
public Manifest withArtifactType(String artifactType) {
public Manifest withArtifactType(ArtifactType artifactType) {
return new Manifest(
schemaVersion,
mediaType,
Expand All @@ -174,7 +167,7 @@ public Manifest withLayers(List<Layer> layers) {
return new Manifest(
schemaVersion,
mediaType,
artifactType,
getTopLevelArtifactType(),
descriptor,
config,
subject,
Expand All @@ -191,7 +184,7 @@ public Manifest withConfig(Config config) {
return new Manifest(
schemaVersion,
mediaType,
artifactType,
getTopLevelArtifactType(),
descriptor,
config,
subject,
Expand All @@ -208,7 +201,7 @@ public Manifest withSubject(Subject subject) {
return new Manifest(
schemaVersion,
mediaType,
artifactType,
getTopLevelArtifactType(),
descriptor,
config,
subject,
Expand All @@ -225,7 +218,7 @@ public Manifest withAnnotations(Map<String, String> annotations) {
return new Manifest(
schemaVersion,
mediaType,
artifactType,
getTopLevelArtifactType(),
descriptor,
config,
subject,
Expand All @@ -242,7 +235,7 @@ public Manifest withDescriptor(ManifestDescriptor descriptor) {
return new Manifest(
schemaVersion,
mediaType,
artifactType,
getTopLevelArtifactType(),
descriptor,
config,
subject,
Expand All @@ -267,6 +260,13 @@ public static Manifest fromJson(String json) {
return JsonUtils.fromJson(json, Manifest.class);
}

private @Nullable ArtifactType getTopLevelArtifactType() {
if (artifactType != null) {
return ArtifactType.from(artifactType);
}
return null;
}

/**
* Return a copy of an empty manifest
* @return The empty manifest
Expand Down
23 changes: 9 additions & 14 deletions src/main/java/land/oras/Registry.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public List<String> getTags(ContainerRef containerRef) {
* @param artifactType The optional artifact type
* @return The referrers
*/
public Referrers getReferrers(ContainerRef containerRef, @Nullable String artifactType) {
public Referrers getReferrers(ContainerRef containerRef, @Nullable ArtifactType artifactType) {
if (containerRef.getDigest() == null) {
throw new OrasException("Digest is required to get referrers");
}
Expand Down Expand Up @@ -243,7 +243,7 @@ public void deleteBlob(ContainerRef containerRef) {
* @return The manifest
*/
public Manifest pushArtifact(ContainerRef containerRef, LocalPath... paths) {
return pushArtifact(containerRef, null, Annotations.empty(), Config.empty(), paths);
return pushArtifact(containerRef, ArtifactType.unknown(), Annotations.empty(), Config.empty(), paths);
}

/**
Expand All @@ -253,7 +253,7 @@ public Manifest pushArtifact(ContainerRef containerRef, LocalPath... paths) {
* @param paths The paths
* @return The manifest
*/
public Manifest pushArtifact(ContainerRef containerRef, String artifactType, LocalPath... paths) {
public Manifest pushArtifact(ContainerRef containerRef, ArtifactType artifactType, LocalPath... paths) {
return pushArtifact(containerRef, artifactType, Annotations.empty(), Config.empty(), paths);
}

Expand All @@ -266,7 +266,7 @@ public Manifest pushArtifact(ContainerRef containerRef, String artifactType, Loc
* @return The manifest
*/
public Manifest pushArtifact(
ContainerRef containerRef, String artifactType, Annotations annotations, LocalPath... paths) {
ContainerRef containerRef, ArtifactType artifactType, Annotations annotations, LocalPath... paths) {
return pushArtifact(containerRef, artifactType, annotations, Config.empty(), paths);
}

Expand Down Expand Up @@ -326,17 +326,12 @@ public void pullArtifact(ContainerRef containerRef, Path path, boolean overwrite
*/
public Manifest pushArtifact(
ContainerRef containerRef,
@Nullable String artifactType,
ArtifactType artifactType,
Annotations annotations,
@Nullable Config config,
LocalPath... paths) {
Manifest manifest = Manifest.empty();
if (artifactType != null) {
manifest = manifest.withArtifactType(artifactType);
} else {
manifest = manifest.withArtifactType(Const.DEFAULT_ARTIFACT_MEDIA_TYPE);
}
Map<String, String> manifestAnnotations = annotations.manifestAnnotations();
Manifest manifest = Manifest.empty().withArtifactType(artifactType);
Map<String, String> manifestAnnotations = new HashMap<>(annotations.manifestAnnotations());
if (!manifestAnnotations.containsKey(Const.ANNOTATION_CREATED) && containerRef.getDigest() == null) {
manifestAnnotations.put(Const.ANNOTATION_CREATED, Const.currentTimestamp());
}
Expand Down Expand Up @@ -404,7 +399,7 @@ public void copy(Registry targetRegistry, ContainerRef sourceContainer, Containe
* @param paths The paths
* @return The manifest of the new artifact
*/
public Manifest attachArtifact(ContainerRef containerRef, String artifactType, LocalPath... paths) {
public Manifest attachArtifact(ContainerRef containerRef, ArtifactType artifactType, LocalPath... paths) {
return attachArtifact(containerRef, artifactType, Annotations.empty(), paths);
}

Expand All @@ -417,7 +412,7 @@ public Manifest attachArtifact(ContainerRef containerRef, String artifactType, L
* @return The manifest of the new artifact
*/
public Manifest attachArtifact(
ContainerRef containerRef, String artifactType, Annotations annotations, LocalPath... paths) {
ContainerRef containerRef, ArtifactType artifactType, Annotations annotations, LocalPath... paths) {

// Push layers
List<Layer> layers = pushLayers(containerRef, paths);
Expand Down
49 changes: 49 additions & 0 deletions src/test/java/land/oras/ArtifactTypeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*-
* =LICENSE=
* ORAS Java SDK
* ===
* Copyright (C) 2024 - 2025 ORAS
* ===
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =LICENSEEND=
*/

package land.oras;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import land.oras.exception.OrasException;
import land.oras.utils.Const;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

@Execution(ExecutionMode.CONCURRENT)
public class ArtifactTypeTest {

@Test
void validateArtifactType() {
ArtifactType unknown = ArtifactType.unknown();
assertEquals(Const.DEFAULT_ARTIFACT_MEDIA_TYPE, unknown.getMediaType());
assertThrows(OrasException.class, () -> ArtifactType.from("invalid"));
assertEquals("foo/bar", ArtifactType.from("foo/bar").getMediaType());
}

@Test
void validateUnknown() {
ArtifactType unknown = ArtifactType.from(null);
assertEquals(Const.DEFAULT_ARTIFACT_MEDIA_TYPE, unknown.getMediaType());
assertEquals(Const.DEFAULT_ARTIFACT_MEDIA_TYPE, unknown.toString());
}
}
8 changes: 8 additions & 0 deletions src/test/java/land/oras/ManifestTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ void shouldHaveEmptyManifest() {
Manifest.fromJson(emptyManifest()).toJson(), Manifest.empty().toJson());
}

@Test
void shouldGetArtifactTest() {
Manifest manifest1 = Manifest.empty().withArtifactType(ArtifactType.from("test/plain"));
assertEquals("test/plain", manifest1.getArtifactType().getMediaType());
Manifest manifest2 = Manifest.empty().withConfig(Config.empty().withMediaType("test/plain"));
assertEquals("test/plain", manifest2.getArtifactType().getMediaType());
}

private String emptyManifest() {
return """
{
Expand Down
Loading