Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add subtype module (2.17) #229

Open
wants to merge 2 commits into
base: 2.16
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ not datatype, data format, or JAX-RS provider modules.
<module>mrbean</module>
<module>osgi</module>
<module>paranamer</module>
<module>subtype</module>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll probably want to change the name, "subtype" is too generic. But let me think about better name for a while...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a difficult thing for me to give it a name...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True🥲🥲

<!-- since 2.13: -->
<module>no-ctor-deser</module>
</modules>
Expand Down
66 changes: 66 additions & 0 deletions subtype/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# jackson-module-subtype

Registering subtypes without annotating the parent class,
see [this](https://github.com/FasterXML/jackson-databind/issues/2104).

Implementation on SPI.

# Usage

Registering modules.

```
ObjectMapper mapper = new ObjectMapper().registerModule(new DynamicSubtypeModule());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think current name is SubtypeModule.

```

Ensure that the parent class has at least the `JsonTypeInfo` annotation.

```java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface Parent {
}
```

1. add the `JsonSubType` annotation to your subclass.
2. provide a non-argument constructor (SPI require it).

```java
import io.github.black.jackson.JsonSubType;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to change to new package.


@JsonSubType("first-child")
public class FirstChild {

private String foo;
// ...

public FirstChild() {
}
}
```

SPI: Put the subclasses in the `META-INF/services` directory under the interface.
Example: `META-INF/services/package.Parent`

```
package.FirstChild
```

Alternatively, you can also use the `auto-service` to auto-generate these files:

```java
import io.github.black.jackson.JsonSubType;
import com.google.auto.service.AutoService;

@AutoService(Parent.class)
@JsonSubType("first-child")
public class FirstChild {

private String foo;
// ...

public FirstChild() {
}
}
```

Done, enjoy it.
76 changes: 76 additions & 0 deletions subtype/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-modules-base</artifactId>
<version>2.16.0-SNAPSHOT</version>
</parent>
<artifactId>jackson-module-subtype</artifactId>
<name>Jackson module: Subtype Annotation Support</name>
<packaging>bundle</packaging>

<description>Registering subtypes without annotating the parent class</description>
<url>https://github.com/FasterXML/jackson-modules-base</url>

<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>

<properties>
<!-- Generate PackageVersion.java into this directory. -->
<packageVersion.dir>com/fasterxml/jackson/module/subtype</packageVersion.dir>
<packageVersion.package>com.fasterxml.jackson.module.subtype</packageVersion.package>
</properties>

<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>replacer</artifactId>
</plugin>
<!-- 14-Mar-2019, tatu: Add rudimentary JDK9+ module info. To build with JDK 8
will have to use `moduleInfoFile` as anything else requires JDK 9+
-->
<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.fasterxml.jackson.module.subtype;

import com.fasterxml.jackson.annotation.JacksonAnnotation;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeName;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Definition of a subtype, along with optional name(s). If no name is defined
* (empty Strings are ignored), class of the type will be checked for {@link JsonTypeName}
* annotation; and if that is also missing or empty, a default
* name will be constructed by type id mechanism.
* Default name is usually based on class name.
* <p>
* It's the same as {@link JsonSubTypes.Type}.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface JsonSubType {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name sounds like it's part of @JsonSubTypes of annotations module. This may confuse users quite. If module name changes as per https://github.com/FasterXML/jackson-modules-base/pull/229/files#r1390329649, can we change name here also?

/**
* Logical type name used as the type identifier for the class, if defined; empty
* String means "not defined". Used unless {@link #names} is defined as non-empty.
*
* @return subtype name
*/
String value() default "";

/**
* (optional) Logical type names used as the type identifier for the class: used if
* more than one type name should be associated with the same type.
*
* @return subtype name array
* @since 2.12
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version mismatch, can remove.

Suggested change
* @since 2.12

... then add @since 2.16 to the class

*/
String[] names() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package @package@;

import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.Versioned;
import com.fasterxml.jackson.core.util.VersionUtil;

/**
* Automatically generated from PackageVersion.java.in during
* packageVersion-generate execution of maven-replacer-plugin in
* pom.xml.
*/
public final class PackageVersion implements Versioned {
public final static Version VERSION = VersionUtil.parseVersion(
"@projectversion@", "@projectgroupid@", "@projectartifactid@");

@Override
public Version version() {
return VERSION;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.fasterxml.jackson.module.subtype;

import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.module.subtype.PackageVersion;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

/**
* Subtype module.
* <p>
* The module caches the subclass, so it's non-real-time.
* It's for registering subtypes without annotating the parent class.
* See <a href="https://github.com/FasterXML/jackson-databind/issues/2104">this issues</a> in jackson-databind.
* <p>
* When not found in the cache, it loads and caches subclasses using SPI.
* Therefore, we can {@link #unregisterType} a class and then module will reload this class's subclasses.
*/
public class SubtypeModule extends Module {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Therefore, we can {@link #unregisterType} a class and then module will reload this class's subclasses.
*/
public class SubtypeModule extends Module {
* Therefore, we can {@link #unregisterType} a class and then module will reload this class's subclasses.
*
* @since 2.16
*/
public class SubtypeModule extends Module {


private final ConcurrentHashMap<Class<?>, List<NamedType>> subtypes = new ConcurrentHashMap<>();

@Override
public String getModuleName() {
return getClass().getSimpleName();
}

@Override
public Version version() {
return PackageVersion.VERSION;
}

@Override
public void setupModule(SetupContext context) {
context.insertAnnotationIntrospector(new AnnotationIntrospector() {
@Override
public Version version() {
return PackageVersion.VERSION;
}

@Override
public List<NamedType> findSubtypes(Annotated a) {
registerTypes(a.getRawType());

List<NamedType> list1 = SubtypeModule.findSubtypes(a.getRawType(), a::getAnnotation);
List<NamedType> list2 = subtypes.getOrDefault(a.getRawType(), Collections.emptyList());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename these list1 and list2 to something less generic?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


if (list1.isEmpty()) return list2;
if (list2.isEmpty()) return list1;
Comment on lines +58 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the two lists are null-safe, no need for empty checking.

List<NamedType> list = new ArrayList<>(list1.size() + list2.size());
list.addAll(list1);
list.addAll(list2);
return list;
Comment on lines +60 to +63
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
List<NamedType> list = new ArrayList<>(list1.size() + list2.size());
list.addAll(list1);
list.addAll(list2);
return list;
List<NamedType> list = new ArrayList<>(list1.size() + list2.size());
list1.addAll(list2);
return list;

Comment on lines +60 to +63
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have duplication? If so, can we have these as set first, then return as list?

}
});
}

/**
* load parent's subclass by SPI.
*
* @param parent parent class.
* @param <S> parent class type.
*/
@SuppressWarnings("unchecked")
public <S> void registerTypes(Class<S> parent) {
if (subtypes.containsKey(parent)) {
return;
}
List<Class<S>> subclasses = new ArrayList<>();
for (S instance : ServiceLoader.load(parent)) {
subclasses.add((Class<S>) instance.getClass());
}
this.registerTypes(parent, subclasses);
}

/**
* register subtypes without SPI.
* Of course, you need to provide them :)
*
* @param parent: parent class.
* @param subclasses: children class.
* @param <S>: parent class type.
*/
public <S> void registerTypes(Class<S> parent, Iterable<Class<S>> subclasses) {
List<NamedType> result = new ArrayList<>();
for (Class<S> subclass : subclasses) {
result.addAll(findSubtypes(subclass, subclass::getAnnotation));
}
subtypes.put(parent, result);
}

public void unregisterType(Class<?> parent) {
subtypes.remove(parent);
}

private static <S> List<NamedType> findSubtypes(Class<S> clazz, Function<Class<JsonSubType>, JsonSubType> getter) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for keeping it static? if not maybe like...

Suggested change
private static <S> List<NamedType> findSubtypes(Class<S> clazz, Function<Class<JsonSubType>, JsonSubType> getter) {
private <S> List<NamedType> _findSubtypes(Class<S> clazz, Function<Class<JsonSubType>, JsonSubType> getter) {

if (clazz == null) {
return Collections.emptyList();
}
JsonSubType subtype = getter.apply(JsonSubType.class);
if (subtype == null) {
return Collections.emptyList();
}
List<NamedType> result = new ArrayList<>();
result.add(new NamedType(clazz, subtype.value()));
// [databind#2761]: alternative set of names to use
for (String name : subtype.names()) {
result.add(new NamedType(clazz, name));
}
return result;
}
}
8 changes: 8 additions & 0 deletions subtype/src/main/resources/META-INF/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
This copy of Jackson JSON processor `jackson-module-guice` module is licensed under the
Apache (Software) License, version 2.0 ("the License").
See the License for details about distribution rights, and the
specific rights regarding derivative works.

You may obtain a copy of the License at:

http://www.apache.org/licenses/LICENSE-2.0
20 changes: 20 additions & 0 deletions subtype/src/main/resources/META-INF/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Jackson JSON processor

Jackson is a high-performance, Free/Open Source JSON processing library.
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
been in development since 2007.
It is currently developed by a community of developers, as well as supported
commercially by FasterXML.com.

## Licensing

Jackson core and extension components may licensed under different licenses.
To find the details that apply to this artifact see the accompanying LICENSE file.
For more information, including possible other licensing options, contact
FasterXML.com (http://fasterxml.com).

## Credits

A list of contributors may be found from CREDITS file, which is included
in some artifacts (usually source distributions); but is always available
from the source code management (SCM) system project uses.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.fasterxml.jackson.module.subtype.SubtypeModule
8 changes: 8 additions & 0 deletions subtype/src/moditect/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module com.fasterxml.jackson.module.subtype {

requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.annotation;
requires com.fasterxml.jackson.databind;

exports com.fasterxml.jackson.module.subtype;
}