Skip to content

Commit

Permalink
Ensure custom content-types work (#6618)
Browse files Browse the repository at this point in the history
* Add some more valdiations when registering content-type-bundles.
* Add a page on the project web site for registered content-types.
* Add tests in `:nessie-model` + `:nessie-versioned-spi` to verify that
  custom content-type-bundles and custom content-serializer-bundles
  work.

Fixes #6591
  • Loading branch information
snazy authored Apr 25, 2023
1 parent 4c89150 commit d8f3e47
Show file tree
Hide file tree
Showing 28 changed files with 765 additions and 70 deletions.
8 changes: 8 additions & 0 deletions api/model/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ dependencies {
annotationProcessor(libs.immutables.value.processor)

testCompileOnly(libs.microprofile.openapi)
testCompileOnly(libs.immutables.value.annotations)
testAnnotationProcessor(libs.immutables.value.processor)
testCompileOnly(libs.jakarta.ws.rs.api)
testCompileOnly(libs.javax.ws.rs)
testCompileOnly(libs.jakarta.validation.api)
testCompileOnly(libs.javax.validation.api)
testCompileOnly(libs.jakarta.annotation.api)
testCompileOnly(libs.findbugs.jsr305)

testImplementation(platform(libs.junit.bom))
testImplementation(libs.bundles.junit.testing)
Expand Down
9 changes: 8 additions & 1 deletion api/model/src/main/java/org/projectnessie/model/Content.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
Expand All @@ -34,7 +35,13 @@
@Schema(
type = SchemaType.OBJECT,
title = "Content",
oneOf = {IcebergTable.class, DeltaLakeTable.class, IcebergView.class, Namespace.class},
anyOf = {
IcebergTable.class,
DeltaLakeTable.class,
IcebergView.class,
Namespace.class,
Map.class
},
discriminatorMapping = {
@DiscriminatorMapping(value = "ICEBERG_TABLE", schema = IcebergTable.class),
@DiscriminatorMapping(value = "DELTA_LAKE_TABLE", schema = DeltaLakeTable.class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@
/**
* Used to provide custom {@link org.projectnessie.model.Content} implementations via the Java
* {@link java.util.ServiceLoader service loader} mechanism.
*
* <p><em>The functionality to actually use custom types is incomplete as long as ther is no
* store-worker support for custom content. </em>
*/
public interface ContentTypeBundle {
void register(ContentTypes.Registrar registrar);
void register(ContentTypeRegistry contentTypeRegistry);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 Dremio
*
* 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.
*/
package org.projectnessie.model.types;

import org.projectnessie.model.Content;

/** An implementation of this interface is passed to {@link ContentTypeBundle}s. */
public interface ContentTypeRegistry {
void register(Class<? extends Content> type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
*/
package org.projectnessie.model.types;

import static java.util.Objects.requireNonNull;

import com.fasterxml.jackson.annotation.JsonTypeName;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand All @@ -32,14 +35,6 @@
*/
public final class ContentTypes {

/**
* An implementation of this interface is passed to {@link
* org.projectnessie.model.types.ContentTypeBundle}s.
*/
public interface Registrar {
void register(String name, Class<? extends Content> type);
}

/** Retrieve an array of all registered content types. */
public static Content.Type[] all() {
return Registry.all();
Expand All @@ -51,6 +46,48 @@ public static Content.Type forName(String name) {
return Registry.forName(name);
}

static final class RegistryHelper implements ContentTypeRegistry {

private final List<Content.Type> list = new ArrayList<>();
private final Map<String, Content.Type> names = new HashMap<>();

@Override
public void register(Class<? extends Content> type) {
requireNonNull(type, "Illegal content-type registration: type must not be null");

JsonTypeName jsonTypeName = type.getAnnotation(JsonTypeName.class);
if (jsonTypeName == null) {
throw new IllegalArgumentException(
String.format(
"Content-type registration: %s has no @JsonTypeName annotation", type.getName()));
}

String name = jsonTypeName.value();
if (name == null || name.trim().isEmpty() || !name.trim().equals(name)) {
throw new IllegalArgumentException(
String.format(
"Illegal content-type registration: illegal name '%s' for %s",
name, type.getName()));
}
Content.Type contentType = new ContentTypeImpl(name, type);

Content.Type ex = names.get(name);
if (ex != null) {
throw new IllegalStateException(
String.format(
"Duplicate content type registration for %s/%s, existing: %s/%s",
name, type.getName(), ex.name(), ex.type().getName()));
}

add(contentType);
}

void add(Content.Type unknownContentType) {
list.add(unknownContentType);
names.put(unknownContentType.name(), unknownContentType);
}
}

/**
* Internal class providing the actual registry. This is a separate class to implicitly use lazy
* initialization.
Expand All @@ -61,40 +98,18 @@ private static final class Registry {
private static final Map<String, Content.Type> byName;

static {
List<Content.Type> list = new ArrayList<>();
Map<String, Content.Type> names = new HashMap<>();
RegistryHelper registryHelper = new RegistryHelper();

// Add the "DEFAULT" type.
Content.Type unknownContentType = new DefaultContentTypeImpl();
list.add(unknownContentType);
names.put(unknownContentType.name(), unknownContentType);
registryHelper.add(unknownContentType);

for (ContentTypeBundle bundle : ServiceLoader.load(ContentTypeBundle.class)) {
bundle.register(
(name, type) -> {
if (name == null
|| name.trim().isEmpty()
|| !name.trim().equals(name)
|| type == null) {
throw new IllegalArgumentException(
String.format(
"Illegal content-type registration: name=%s, type=%s", name, type));
}
Content.Type contentType = new ContentTypeImpl(name, type);
Content.Type ex = names.get(name);
if (ex != null) {
throw new IllegalStateException(
String.format(
"Duplicate content type registration for %s/%s, existing: %s/%s",
name, type, ex.name(), ex.type()));
}
list.add(contentType);
names.put(name, contentType);
});
bundle.register(registryHelper);
}

byName = Collections.unmodifiableMap(names);
all = list.toArray(new Content.Type[0]);
byName = Collections.unmodifiableMap(registryHelper.names);
all = registryHelper.list.toArray(new Content.Type[0]);
}

private static Content.Type[] all() {
Expand Down Expand Up @@ -162,11 +177,6 @@ public int hashCode() {
return 0;
}

@Override
public boolean equals(Object obj) {
return obj == this;
}

@Override
public String toString() {
return name();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import org.projectnessie.model.IcebergTable;
import org.projectnessie.model.IcebergView;
import org.projectnessie.model.Namespace;
import org.projectnessie.model.types.ContentTypes.Registrar;

/**
* Provides the {@link org.projectnessie.model.Content.Type content types} for Iceberg table + view,
Expand All @@ -28,10 +27,10 @@
public final class MainContentTypeBundle implements ContentTypeBundle {

@Override
public void register(Registrar registrar) {
registrar.register("ICEBERG_TABLE", IcebergTable.class);
registrar.register("DELTA_LAKE_TABLE", DeltaLakeTable.class);
registrar.register("ICEBERG_VIEW", IcebergView.class);
registrar.register("NAMESPACE", Namespace.class);
public void register(ContentTypeRegistry contentTypeRegistry) {
contentTypeRegistry.register(IcebergTable.class);
contentTypeRegistry.register(DeltaLakeTable.class);
contentTypeRegistry.register(IcebergView.class);
contentTypeRegistry.register(Namespace.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 Dremio
*
* 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.
*/
package org.projectnessie.model.types;

import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
import org.projectnessie.model.Content;

@Value.Immutable
@JsonSerialize(as = ImmutableCustomTestContent.class)
@JsonDeserialize(as = ImmutableCustomTestContent.class)
@JsonTypeName(CustomTestContent.TYPE)
public abstract class CustomTestContent extends Content {
static final String TYPE = "TEST_CUSTOM_CONTENT_TYPE";

public abstract long getSomeLong();

public abstract String getSomeString();

@Override
public Type getType() {
return ContentTypes.forName(TYPE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2023 Dremio
*
* 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.
*/
package org.projectnessie.model.types;

public class CustomTestContentTypeBundle implements ContentTypeBundle {

@Override
public void register(ContentTypeRegistry contentTypeRegistry) {
contentTypeRegistry.register(CustomTestContent.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2023 Dremio
*
* 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.
*/
package org.projectnessie.model.types;

import static org.projectnessie.model.CommitMeta.fromMessage;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.SoftAssertions;
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.projectnessie.model.Content;
import org.projectnessie.model.ContentKey;
import org.projectnessie.model.ImmutableOperations;
import org.projectnessie.model.Operation;
import org.projectnessie.model.Operations;

@ExtendWith(SoftAssertionsExtension.class)
public class TestCustomContentType {
static final ObjectMapper MAPPER = new ObjectMapper();

@InjectSoftAssertions protected SoftAssertions soft;

@Test
void directSerialization() throws Exception {
CustomTestContent testContent =
ImmutableCustomTestContent.builder().someLong(42L).someString("blah").build();

String json = MAPPER.writeValueAsString(testContent);

Content deserializedAsContent = MAPPER.readValue(json, Content.class);
CustomTestContent deserializedAsTestContent = MAPPER.readValue(json, CustomTestContent.class);

soft.assertThat(deserializedAsContent).isEqualTo(testContent);
soft.assertThat(deserializedAsTestContent).isEqualTo(testContent);
}

@Test
void customContentInOperation() throws Exception {
CustomTestContent testContent =
ImmutableCustomTestContent.builder().someLong(42L).someString("blah").build();

Operations operations =
ImmutableOperations.builder()
.commitMeta(fromMessage("foo"))
.addOperations(Operation.Put.of(ContentKey.of("key"), testContent))
.build();

String json = MAPPER.writeValueAsString(operations);

Operations deserializedOperations = MAPPER.readValue(json, Operations.class);

soft.assertThat(deserializedOperations).isEqualTo(operations);
}
}
Loading

0 comments on commit d8f3e47

Please sign in to comment.