Skip to content

Commit ccc3352

Browse files
committed
feat(normalize-class): add NormalizeClassPlugin for class deduplication during code generation
1 parent 9f6125a commit ccc3352

2 files changed

Lines changed: 328 additions & 0 deletions

File tree

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
/*
2+
* Copyright 2026 Rawvoid(https://github.com/rawvoid)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.github.rawvoid.jaxb.plugin;
18+
19+
import com.sun.codemodel.*;
20+
import com.sun.tools.xjc.Options;
21+
import com.sun.tools.xjc.outline.ClassOutline;
22+
import com.sun.tools.xjc.outline.Outline;
23+
import com.sun.tools.xjc.outline.PackageOutline;
24+
import org.xml.sax.ErrorHandler;
25+
import org.xml.sax.SAXException;
26+
27+
import java.util.*;
28+
import java.util.stream.Collectors;
29+
import java.util.stream.StreamSupport;
30+
31+
/**
32+
* @author Rawvoid
33+
*/
34+
@Option(name = "Xnormalize-class", description = "Normalize generated classes")
35+
public class NormalizeClassPlugin extends AbstractPlugin {
36+
37+
@Override
38+
public boolean run(Outline outline, Options opt, ErrorHandler errorHandler) throws SAXException {
39+
outline.getAllPackageContexts().forEach(packageOutline -> {
40+
removeDuplicateClasses(packageOutline);
41+
});
42+
return true;
43+
}
44+
45+
private void removeDuplicateClasses(PackageOutline packageOutline) {
46+
var classOutlines = packageOutline.getClasses();
47+
48+
var groupedClasses = groupingEqualClasses(classOutlines);
49+
for (var groupedClass : groupedClasses) {
50+
if (groupedClass.size() <= 1) {
51+
continue;
52+
}
53+
groupedClass.sort(Comparator.comparing(this::innerDepth));
54+
var savingClass = groupedClass.getFirst();
55+
56+
var removeClasses = groupedClass.subList(1, groupedClass.size());
57+
removeAndReplaceClass(removeClasses, savingClass);
58+
}
59+
}
60+
61+
private int innerDepth(ClassOutline classOutline) {
62+
return innerDepth(classOutline.implClass);
63+
}
64+
65+
private int innerDepth(JDefinedClass definedClass) {
66+
var depth = 0;
67+
var currentContainer = definedClass.parentContainer();
68+
while (currentContainer instanceof JDefinedClass) {
69+
depth++;
70+
currentContainer = currentContainer.parentContainer();
71+
}
72+
return depth;
73+
}
74+
75+
private List<List<ClassOutline>> groupingEqualClasses(Set<? extends ClassOutline> classes) {
76+
List<List<ClassOutline>> groupedClasses = new ArrayList<>();
77+
for (var classOutline : classes) {
78+
groupedClasses.stream()
79+
.filter(group -> isEqual(group.getFirst().implClass, classOutline.implClass))
80+
.findFirst()
81+
.ifPresentOrElse(group -> group.add(classOutline),
82+
() -> groupedClasses.add(new ArrayList<>(List.of(classOutline))));
83+
}
84+
return groupedClasses;
85+
}
86+
87+
private void removeAndReplaceClass(List<ClassOutline> removeClasses, ClassOutline replaceClassOutline) {
88+
var replaceClass = replaceClassOutline.implClass;
89+
var packageOutline = replaceClassOutline._package();
90+
var outline = replaceClassOutline.parent();
91+
92+
removeClasses.forEach(classOutline -> {
93+
var removeClass = classOutline.implClass;
94+
removeFromParentContainer(removeClass);
95+
var jPackage = removeClass.getPackage();
96+
97+
outline.getClasses().forEach(c -> {
98+
var definedClass = c.implClass;
99+
100+
definedClass.fields().forEach((fieldName, fieldVar) -> {
101+
replaceType(fieldVar.type(), removeClass, replaceClass, () -> fieldVar.type(replaceClass));
102+
});
103+
104+
definedClass.methods().forEach(method -> {
105+
replaceType(method.type(), removeClass, replaceClass, () -> method.type(replaceClass));
106+
107+
method.params().forEach(param -> {
108+
replaceType(param.type(), removeClass, replaceClass, () -> param.type(replaceClass));
109+
});
110+
});
111+
});
112+
});
113+
}
114+
115+
private void removeFromParentContainer(JDefinedClass definedClass) {
116+
var parentContainer = definedClass.parentContainer();
117+
118+
var classes = parentContainer.classes();
119+
while (classes.hasNext()) {
120+
var nextClass = classes.next();
121+
if (nextClass == definedClass) {
122+
classes.remove();
123+
break;
124+
}
125+
}
126+
}
127+
128+
private void replaceType(JType currentType, JDefinedClass targetType, JDefinedClass newTargetType, Runnable typeSetter) {
129+
try {
130+
var clazz = currentType.getClass();
131+
if (currentType instanceof JDefinedClass) {
132+
if (currentType == targetType) {
133+
typeSetter.run();
134+
}
135+
} else if (clazz.getSimpleName().equals("JNarrowedClass")) {
136+
var argsField = clazz.getField("args");
137+
argsField.setAccessible(true);
138+
var args = (List<JClass>) argsField.get(currentType);
139+
140+
args.replaceAll(arg -> arg == targetType ? newTargetType : arg);
141+
} else if (clazz.getSimpleName().equals("JArrayClass")) {
142+
var componentTypeField = clazz.getField("componentType");
143+
componentTypeField.setAccessible(true);
144+
var componentType = (JType) componentTypeField.get(currentType);
145+
146+
if (componentType == targetType) {
147+
componentTypeField.set(currentType, newTargetType);
148+
}
149+
}
150+
} catch (NoSuchFieldException | IllegalAccessException e) {
151+
throw new IllegalStateException("Failed to replace type for " + currentType.fullName(), e);
152+
}
153+
}
154+
155+
private boolean isEqual(JDefinedClass class1, JDefinedClass class2) {
156+
if (!class1.name().equals(class2.name())) {
157+
return false;
158+
}
159+
160+
if (class1.classes().hasNext() || class2.classes().hasNext()) {
161+
162+
return false;
163+
}
164+
165+
if (class1.superClass() != class2.superClass()) {
166+
return false;
167+
}
168+
169+
var implements1 = StreamSupport.stream(Spliterators
170+
.spliteratorUnknownSize(class1._implements(), Spliterator.ORDERED), false)
171+
.collect(Collectors.toSet());
172+
var implements2 = StreamSupport.stream(Spliterators
173+
.spliteratorUnknownSize(class2._implements(), Spliterator.ORDERED), false)
174+
.collect(Collectors.toSet());
175+
176+
if (!implements1.equals(implements2)) {
177+
return false;
178+
}
179+
180+
if (!isEqual(class1.annotations(), class2.annotations())) {
181+
return false;
182+
}
183+
184+
var fields1 = class1.fields();
185+
var fields2 = class2.fields();
186+
187+
if (fields1.size() != fields2.size()) {
188+
return false;
189+
}
190+
191+
for (var entry : fields1.entrySet()) {
192+
var fieldName = entry.getKey();
193+
var field1 = entry.getValue();
194+
var field2 = fields2.get(fieldName);
195+
196+
if (field2 == null) {
197+
return false;
198+
}
199+
200+
if (!field1.type().fullName().equals(field2.type().fullName())) {
201+
return false;
202+
}
203+
204+
if (!isEqual(field1.annotations(), field2.annotations())) {
205+
return false;
206+
}
207+
}
208+
209+
return true;
210+
}
211+
212+
private boolean isEqual(Collection<JAnnotationUse> annos1, Collection<JAnnotationUse> annos2) {
213+
if (annos1.size() != annos2.size()) {
214+
return false;
215+
}
216+
217+
var map1 = annos1.stream()
218+
.collect(Collectors.groupingBy(a -> a.getAnnotationClass().fullName()));
219+
var map2 = annos2.stream()
220+
.collect(Collectors.groupingBy(a -> a.getAnnotationClass().fullName()));
221+
222+
if (!map1.keySet().equals(map2.keySet())) {
223+
return false;
224+
}
225+
226+
for (var fullName : map1.keySet()) {
227+
var list1 = map1.get(fullName);
228+
var list2 = map2.get(fullName);
229+
230+
if (list1.size() != list2.size()) {
231+
return false;
232+
}
233+
234+
for (var i = 0; i < list1.size(); i++) {
235+
var anno1 = list1.get(i);
236+
var anno2 = list2.get(i);
237+
238+
if (!isEqual(anno1, anno2)) {
239+
return false;
240+
}
241+
}
242+
}
243+
244+
return true;
245+
}
246+
247+
private boolean isEqual(JAnnotationUse anno1, JAnnotationUse anno2) {
248+
var members1 = anno1.getAnnotationMembers();
249+
var members2 = anno2.getAnnotationMembers();
250+
251+
if (members1.size() != members2.size()) {
252+
return false;
253+
}
254+
255+
for (var entry : members1.entrySet()) {
256+
var memberName = entry.getKey();
257+
var member1 = entry.getValue();
258+
var member2 = members2.get(memberName);
259+
260+
if (member2 == null) {
261+
return false;
262+
}
263+
264+
if (member1.getClass() != member2.getClass()) {
265+
return false;
266+
}
267+
268+
if (!isEqual(member1, member2)) {
269+
return false;
270+
}
271+
}
272+
273+
return true;
274+
}
275+
276+
private boolean isEqual(JAnnotationValue value1, JAnnotationValue value2) {
277+
if (value1.getClass() != value2.getClass()) {
278+
return false;
279+
}
280+
281+
if (value1 instanceof JAnnotationStringValue) {
282+
var stringValue1 = value1.toString();
283+
var stringValue2 = value2.toString();
284+
if (!stringValue1.equals(stringValue2)) {
285+
return false;
286+
}
287+
} else if (value1 instanceof JAnnotationClassValue) {
288+
var type1 = ((JAnnotationClassValue) value1).type();
289+
var type2 = ((JAnnotationClassValue) value2).type();
290+
if (!type1.fullName().equals(type2.fullName())) {
291+
return false;
292+
}
293+
294+
var value1Value = ((JAnnotationClassValue) value1).value();
295+
var value2Value = ((JAnnotationClassValue) value2).value();
296+
if (!value1Value.equals(value2Value)) {
297+
return false;
298+
}
299+
} else if (value1 instanceof JAnnotationArrayMember) {
300+
var values1 = ((JAnnotationArrayMember) value1).annotations2()
301+
.stream().toList();
302+
var values2 = ((JAnnotationArrayMember) value2).annotations2()
303+
.stream().toList();
304+
305+
if (values1.size() != values2.size()) {
306+
return false;
307+
}
308+
309+
for (var i = 0; i < values1.size(); i++) {
310+
var value1Item = values1.get(i);
311+
var value2Item = values2.get(i);
312+
if (!isEqual(value1Item, value2Item)) {
313+
return false;
314+
}
315+
}
316+
} else if (value1 instanceof JAnnotationUse anno1) {
317+
var anno2 = (JAnnotationUse) value2;
318+
if (!isEqual(anno1, anno2)) {
319+
return false;
320+
}
321+
} else {
322+
throw new IllegalArgumentException("Unknown annotation value type: " + value1.getClass());
323+
}
324+
325+
return true;
326+
}
327+
}

plugins/src/main/resources/META-INF/services/com.sun.tools.xjc.Plugin

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ io.github.rawvoid.jaxb.plugin.ConvertNamePlugin
77
io.github.rawvoid.jaxb.plugin.ElementWrapperPlugin
88
io.github.rawvoid.jaxb.plugin.NsPrefixPlugin
99
io.github.rawvoid.jaxb.plugin.FlattenInnerClassPlugin
10+
io.github.rawvoid.jaxb.plugin.NormalizeClassPlugin

0 commit comments

Comments
 (0)