Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ public class Jsonschema2PojoTask extends Task implements GenerationConfig {
private boolean includeGeneratedAnnotation = true;

private boolean useJakartaValidation = false;

private boolean useDeduplication = false;

/**
* Execute this task (it's expected that all relevant setters will have been
* called by Ant to provide task configuration <em>before</em> this method
Expand Down Expand Up @@ -977,6 +980,15 @@ public void setUseJakartaValidation(boolean useJakartaValidation) {
this.useJakartaValidation = useJakartaValidation;
}

/**
* Sets the 'useDeduplication' property of this class
*
* @param useDeduplication Whether to deduplicate generated types for identical schemas.
*/
public void setUseDeduplication(boolean useDeduplication) {
this.useDeduplication = useDeduplication;
}

public void setFormatTypeMapping(Map<String, String> formatTypeMapping) {
this.formatTypeMapping = formatTypeMapping;
}
Expand Down Expand Up @@ -1338,4 +1350,9 @@ public boolean isIncludeGeneratedAnnotation() {
public boolean isUseJakartaValidation() {
return useJakartaValidation;
}

@Override
public boolean isUseDeduplication() {
return useDeduplication;
}
}
5 changes: 5 additions & 0 deletions jsonschema2pojo-ant/src/site/Jsonschema2PojoTask.html
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,11 @@ <h3>Parameters</h3>
</td>
<td align="center" valign="top">No (default <code>false</code>)</td>
</tr>
<tr>
<td valign="top">useDeduplication</td>
<td valign="top">Deduplicates Java classes and enums if they have identical schemas.</td>
<td align="center" valign="top">No (default <code>false</code>)</td>
</tr>
</table>
<h3>Examples</h3>
<pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ public class Arguments implements GenerationConfig {
@Parameter(names = { "--useJakartaValidation" }, description = "Whether to use annotations from jakarta.validation package instead of javax.validation package when adding JSR-303/349 annotations to generated Java types")
private boolean useJakartaValidation = false;

@Parameter(names = { "--useDeduplication" }, description = "Whether to deduplicate generated types for identical schemas")
private boolean useDeduplication = false;

@Parameter(names = { "-v", "--version"}, description = "Print version information", help = true)
private boolean printVersion = false;

Expand Down Expand Up @@ -624,4 +627,9 @@ public boolean isIncludeGeneratedAnnotation() {
public boolean isUseJakartaValidation() {
return useJakartaValidation;
}

@Override
public boolean isUseDeduplication() {
return useDeduplication;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -495,4 +495,12 @@ public boolean isIncludeGeneratedAnnotation() {
public boolean isUseJakartaValidation() {
return false;
}

/**
* @return {@code false}
*/
@Override
public boolean isUseDeduplication() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -635,4 +635,11 @@ default boolean isUseInnerClassBuilders() {
*/
boolean isUseJakartaValidation();

/**
* Deduplicates Java classes and enums if they have identical schemas.
*
* @return Whether to perform deduplication.
*/
boolean isUseDeduplication();

}
14 changes: 14 additions & 0 deletions jsonschema2pojo-core/src/main/java/org/jsonschema2pojo/Schema.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
package org.jsonschema2pojo;

import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

import com.fasterxml.jackson.databind.JsonNode;
import com.sun.codemodel.JType;
Expand Down Expand Up @@ -75,4 +80,13 @@ public boolean isGenerated() {
return javaType != null;
}

public String calculateHash() {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Using SHA-256 of the compact JSON string for de-duplication.

try {
return Base64.getEncoder()
.encodeToString(MessageDigest.getInstance("SHA-256")
.digest(content.toString().getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException("SHA-256 not available, disable de-duplication to avoid", ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright © 2024 Nokia
*
* 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.jsonschema2pojo.rules;

import com.fasterxml.jackson.databind.JsonNode;
import org.jsonschema2pojo.Schema;

import java.util.HashMap;
import java.util.Map;

/**
* Wraps an existing Rule and performs deduplication of identical schemas to the same output.
*/
class DeduplicateRule<T, R> implements Rule<T, R> {

private final Map<String, R> deduplicateCache;
private final Rule<T, R> rule;

public DeduplicateRule(Map<Class<?>, Map<String, ?>> dedupeCacheByRule, Rule<T, R> rule) {
// noinspection unchecked map is populated by us and guaranteed to be of the correct type
Map<String, R> dedupeCache = (Map<String, R>) dedupeCacheByRule.get(rule.getClass());
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Each Rule has its own individual cache

if(dedupeCache == null) {
dedupeCache = new HashMap<>();
dedupeCacheByRule.put(rule.getClass(), dedupeCache);
}
this.deduplicateCache = dedupeCache;
this.rule = rule;
}

/**
* Deduplicates based on {@link Schema#calculateHash()} and returns identical {@code R} instances.
*
* @param currentSchema
* schema to be used for deduplication
* @return same {@code R} instance if a previous schema with the same hash code was already processed
*/
@Override
public R apply(String nodeName, JsonNode node, JsonNode parent, T generatableType, Schema currentSchema) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Performs de-duplication here during rule transformation of Schema to result.

String hash = currentSchema.calculateHash();
R output = deduplicateCache.get(hash);
if (output == null) {
output = rule.apply(nodeName, node, parent, generatableType, currentSchema);
deduplicateCache.put(hash, output);
}
return output;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;

import java.util.HashMap;
import java.util.Map;

/**
* Provides factory/creation methods for the code generation rules.
*/
Expand All @@ -47,6 +50,7 @@ public class RuleFactory {
private GenerationConfig generationConfig;
private Annotator annotator;
private SchemaStore schemaStore;
private Map<Class<?>, Map<String, ?>> dedupeCache;
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Track duplicates in each RuleFactory instance


/**
* Create a new rule factory with the given generation config options.
Expand All @@ -68,6 +72,7 @@ public RuleFactory(GenerationConfig generationConfig, Annotator annotator, Schem
this.nameHelper = new NameHelper(generationConfig);
this.reflectionHelper = new ReflectionHelper(this);
this.logger = new NoopRuleLogger();
this.dedupeCache = new HashMap<>();
}

/**
Expand Down Expand Up @@ -116,7 +121,11 @@ public Rule<JDocCommentable, JDocComment> getCommentRule() {
* @return a schema rule that can handle the "enum" declaration.
*/
public Rule<JClassContainer, JType> getEnumRule() {
return new EnumRule(this);
Rule<JClassContainer, JType> rule = new EnumRule(this);
if (generationConfig.isUseDeduplication()) {
rule = new DeduplicateRule<>(dedupeCache, rule);
}
return rule;
}

/**
Expand All @@ -136,7 +145,11 @@ public Rule<JType, JType> getFormatRule() {
* @return a schema rule that can handle the "object" declaration.
*/
public Rule<JPackage, JType> getObjectRule() {
return new ObjectRule(this, new ParcelableHelper(), reflectionHelper);
Rule<JPackage, JType> rule = new ObjectRule(this, new ParcelableHelper(), reflectionHelper);
if (generationConfig.isUseDeduplication()) {
rule = new DeduplicateRule<>(dedupeCache, rule);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Wrap specific Rules that require de-duplication.

}
return rule;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Copyright © 2024 Nokia
*
* 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.jsonschema2pojo.rules;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;
import org.jsonschema2pojo.Annotator;
import org.jsonschema2pojo.RuleLogger;
import org.jsonschema2pojo.Schema;
import org.jsonschema2pojo.util.NameHelper;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.stubbing.Answer;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.Mockito.*;

public class DeduplicateRuleTest {

private Map<String, UUID> dedupeCache;
private Rule<Object, UUID> dedupeRule;

@Before
public void setupRule() {
dedupeCache = new HashMap<>();
DummyRule dummyRule = new DummyRule();
dedupeRule = new DeduplicateRule<>(ImmutableMap.of(dummyRule.getClass(), dedupeCache), dummyRule);
}

@Test
public void test() {

ObjectMapper objectMapper = new ObjectMapper();

ArrayNode arrayNode1 = objectMapper.createArrayNode().add(7).add(8);
ArrayNode arrayNode2 = objectMapper.createArrayNode().add(7).add(8);
ArrayNode arrayNode3 = objectMapper.createArrayNode().add(7).add(9);
assertEquals(arrayNode1.toString(), arrayNode2.toString());
assertNotEquals(arrayNode1.toString(), arrayNode3.toString());

Schema schema1 = new Schema(null, arrayNode1, null);
Schema schema2 = new Schema(null, arrayNode2, null);
Schema schema3 = new Schema(null, arrayNode3, null);
assertEquals(schema1.calculateHash(), schema1.calculateHash());
assertEquals(schema1.calculateHash(), schema2.calculateHash());
assertNotEquals(schema1.calculateHash(), schema3.calculateHash());

UUID uuid1 = dedupeRule.apply(null, null, null, null, schema1);
UUID uuid2 = dedupeRule.apply(null, null, null, null, schema1);
UUID uuid3 = dedupeRule.apply(null, null, null, null, schema2);
UUID uuid4 = dedupeRule.apply(null, null, null, null, schema3);

assertEquals(uuid1, uuid2);
assertEquals(uuid1, uuid3);
assertNotEquals(uuid1, uuid4);
}

public static class DummyRule implements Rule<Object, UUID> {
@Override
public UUID apply(String nodeName, JsonNode node, JsonNode parent, Object generatableType, Schema currentSchema) {
return UUID.randomUUID();
}
}
}
3 changes: 3 additions & 0 deletions jsonschema2pojo-gradle-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ jsonSchema2Pojo {
// Whether to use annotations from jakarta.validation package instead of javax.validation package
// when adding JSR-303 annotations to generated Java types
useJakartaValidation = false

// Deduplicates Java classes and enums if they have identical schemas.
useDeduplication = false
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public class JsonSchemaExtension implements GenerationConfig {
Map<String, String> formatTypeMapping
boolean includeGeneratedAnnotation
boolean useJakartaValidation
boolean useDeduplication

public JsonSchemaExtension() {
// See DefaultGenerationConfig
Expand Down Expand Up @@ -158,6 +159,7 @@ public class JsonSchemaExtension implements GenerationConfig {
formatTypeMapping = Collections.emptyMap()
includeGeneratedAnnotation = true
useJakartaValidation = false
useDeduplication = false
}

@Override
Expand Down Expand Up @@ -292,6 +294,7 @@ public class JsonSchemaExtension implements GenerationConfig {
|includeConstructorPropertiesAnnotation = ${includeConstructorPropertiesAnnotation}
|includeGeneratedAnnotation = ${includeGeneratedAnnotation}
|useJakartaValidation = ${useJakartaValidation}
|useDeduplication = ${useDeduplication}
""".stripMargin()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,27 @@ public void propertiesWithSameNameOnDifferentObjects() throws Exception {

}

@Test
public void propertiesWithSameNameOnDifferentObjectsWithDedupe() throws Exception {

final String filePath = "/" + format + "/commonSubClasses";
ClassLoader resultsClassLoader = schemaRule.generateAndCompile(filePath, "com.example", config("sourceType", format, "useDeduplication", true));

Class<?> aType = resultsClassLoader.loadClass("com.example.A");
Class<?> bType = resultsClassLoader.loadClass("com.example.B");

Object deserializedValueA = objectMapper.readValue(this.getClass().getResourceAsStream(filePath("commonSubClasses/a")), aType);
Object deserializedValueB = objectMapper.readValue(this.getClass().getResourceAsStream(filePath("commonSubClasses/b")), bType);

assertNotEquals(aType.getMethod("getAa").getReturnType().getName(),
bType.getMethod("getAa").getReturnType().getName());
assertEquals(aType.getMethod("get1").getReturnType().getName(),
bType.getMethod("get1").getReturnType().getName());

Object a1 = aType.getMethod("get1").invoke(deserializedValueA);
Object b1 = bType.getMethod("get1").invoke(deserializedValueB);

assertEquals(a1.getClass().getMethod("get3").getReturnType().getName(),
b1.getClass().getMethod("get3").getReturnType().getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"aa" : {
"aaa" : "aaaa"
},
"1" : {
"2" : {},
"3" : [1, 2, 3]
}
}
Loading