Skip to content

Commit a4a6195

Browse files
committed
v1.7.0: Java records support
1 parent c912c9c commit a4a6195

File tree

4 files changed

+199
-1
lines changed

4 files changed

+199
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ for a higher control compared to the EasyML Facade.
6060

6161
### Release Notes
6262

63+
Release 1.7.0 (requires Java 17, recommended from Java 17)
64+
- feature: support for Java records.
65+
66+
6367
Release 1.6.0 (requires Java 9, recommended up to Java 17)
6468
- XMLWriter and XMLReader use getters and setters.
6569
- feature: support for Java 9 modules.

easyml/src/net/sourceforge/easyml/EasyML.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
* objects.<br/>
8585
*
8686
* @author Victor Cordis ( cordis.victor at gmail.com)
87-
* @version 1.5.3
87+
* @version 1.7.0
8888
* @see XMLReader
8989
* @see XMLWriter
9090
* @since 1.0
@@ -222,6 +222,7 @@ public static void defaultConfiguration(XMLWriter writer) {
222222
simple.add(ClassStrategy.INSTANCE);
223223
simple.add(EnumStrategy.INSTANCE);
224224
composite.add(ObjectStrategy.INSTANCE);
225+
composite.add(RecordStrategy.INSTANCE);
225226
simple.add(StackTraceElementStrategy.INSTANCE);
226227
simple.add(StringBufferStrategy.INSTANCE);
227228
simple.add(StringBuilderStrategy.INSTANCE);
@@ -315,6 +316,7 @@ public static void defaultConfiguration(XMLReader reader) {
315316
simple.put(ClassStrategy.NAME, ClassStrategy.INSTANCE);
316317
simple.put(EnumStrategy.NAME, EnumStrategy.INSTANCE);
317318
composite.put(ObjectStrategy.NAME, ObjectStrategy.INSTANCE);
319+
composite.put(RecordStrategy.NAME, RecordStrategy.INSTANCE);
318320
simple.put(StackTraceElementStrategy.NAME, StackTraceElementStrategy.INSTANCE);
319321
simple.put(StringBufferStrategy.NAME, StringBufferStrategy.INSTANCE);
320322
simple.put(StringBuilderStrategy.NAME, StringBuilderStrategy.INSTANCE);
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright 2012 Victor Cordis
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+
* Please contact the author ( [email protected] ) if you need additional
17+
* information or have any questions.
18+
*/
19+
package net.sourceforge.easyml.marshalling.java.lang;
20+
21+
import net.sourceforge.easyml.DTD;
22+
import net.sourceforge.easyml.InvalidFormatException;
23+
import net.sourceforge.easyml.marshalling.*;
24+
25+
import java.lang.invoke.MethodHandle;
26+
import java.lang.invoke.MethodHandles;
27+
import java.lang.invoke.MethodType;
28+
import java.lang.reflect.RecordComponent;
29+
import java.util.Arrays;
30+
31+
/**
32+
* RecordStrategy class that implements {@linkplain CompositeStrategy} for
33+
* the Java records.
34+
* This implementation is thread-safe.
35+
*
36+
* @author Victor Cordis ( cordis.victor at gmail.com)
37+
* @version 1.7.0
38+
* @since 1.7.0
39+
*/
40+
public final class RecordStrategy extends AbstractStrategy implements CompositeStrategy<Record> {
41+
42+
/**
43+
* Constant defining the value used for the strategy name.
44+
*/
45+
public static final String NAME = "record";
46+
/**
47+
* Constant defining the singleton instance.
48+
*/
49+
public static final RecordStrategy INSTANCE = new RecordStrategy();
50+
private static final MethodHandles.Lookup lookup = MethodHandles.lookup();
51+
52+
private RecordStrategy() {
53+
}
54+
55+
/**
56+
* {@inheritDoc }
57+
*/
58+
@Override
59+
public boolean strict() {
60+
return false;
61+
}
62+
63+
/**
64+
* {@inheritDoc }
65+
*/
66+
@Override
67+
public Class target() {
68+
return Record.class;
69+
}
70+
71+
/**
72+
* {@inheritDoc }
73+
*/
74+
@Override
75+
public boolean appliesTo(Class<Record> c) {
76+
return c.isRecord();
77+
}
78+
79+
/**
80+
* {@inheritDoc }
81+
*/
82+
@Override
83+
public String name() {
84+
return RecordStrategy.NAME;
85+
}
86+
87+
/**
88+
* {@inheritDoc }
89+
*/
90+
@Override
91+
public void marshal(Record target, CompositeWriter writer, MarshalContext ctx) {
92+
Class cls = target.getClass();
93+
writer.startElement(RecordStrategy.NAME);
94+
writer.setAttribute(DTD.ATTRIBUTE_CLASS, ctx.aliasOrNameFor(cls));
95+
for (RecordComponent rc : cls.getRecordComponents()) {
96+
writer.write(getValue(target, rc));
97+
}
98+
writer.endElement();
99+
}
100+
101+
private static Object getValue(Record source, RecordComponent component) {
102+
try {
103+
final MethodHandle getterMH = lookup.findVirtual(
104+
source.getClass(),
105+
component.getName(),
106+
MethodType.methodType(component.getType()));
107+
return getterMH.invoke(source);
108+
} catch (Throwable e) {
109+
throw new IllegalArgumentException("component: cannot marshal record component: " + component, e);
110+
}
111+
}
112+
113+
/**
114+
* Also inits records because constructor requires all values and does not allow escaping this reference.
115+
* <p>
116+
* {@inheritDoc }
117+
*/
118+
@Override
119+
public Record unmarshalNew(CompositeReader reader, UnmarshalContext ctx) throws ClassNotFoundException {
120+
final Class cls = ctx.classFor(reader.elementRequiredAttribute(DTD.ATTRIBUTE_CLASS));
121+
if (!cls.isRecord()) {
122+
throw new InvalidFormatException(ctx.readerPositionDescriptor(), "element class not a record: " + cls);
123+
}
124+
// consume root tag:
125+
reader.next();
126+
127+
// read record components: in exactly the same order as they were written:
128+
final RecordComponent[] rcs = cls.getRecordComponents();
129+
final Object[] values = new Object[rcs.length];
130+
131+
int component = 0;
132+
while (component < rcs.length && reader.atElementStart()) {
133+
values[component] = reader.read();
134+
component++;
135+
}
136+
137+
if (component == rcs.length && reader.atElementEnd() && reader.elementName().equals(RecordStrategy.NAME)) {
138+
try {
139+
return newRecord(cls, rcs, values);
140+
} catch (Throwable e) {
141+
throw new InvalidFormatException(ctx.readerPositionDescriptor(), "could not instantiate record: " + cls);
142+
}
143+
}
144+
final String message = component != rcs.length ?
145+
"unexpected record components: " + component + ", expected " + rcs.length :
146+
"unexpected element end";
147+
throw new InvalidFormatException(ctx.readerPositionDescriptor(), message);
148+
}
149+
150+
private static Record newRecord(Class recordClass, RecordComponent[] recordComponents, Object[] args) throws Throwable {
151+
Class<?>[] paramTypes = Arrays.stream(recordComponents)
152+
.map(RecordComponent::getType)
153+
.toArray(Class<?>[]::new);
154+
final MethodHandle constructorMH = lookup.findConstructor(
155+
recordClass,
156+
MethodType.methodType(void.class, paramTypes))
157+
.asType(MethodType.methodType(Object.class, paramTypes));
158+
return (Record) constructorMH.invokeWithArguments(args);
159+
}
160+
161+
/**
162+
* {@inheritDoc }
163+
*/
164+
@Override
165+
public Record unmarshalInit(Record target, CompositeReader reader, UnmarshalContext ctx) {
166+
return target;
167+
}
168+
}

easyml/test/net/sourceforge/easyml/marshalling/VariousTypesTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import net.sourceforge.easyml.XMLReader;
2222
import net.sourceforge.easyml.XMLWriter;
23+
import net.sourceforge.easyml.marshalling.java.lang.RecordStrategy;
2324
import net.sourceforge.easyml.marshalling.java.time.InstantStrategy;
2425
import net.sourceforge.easyml.marshalling.java.time.LocalDateTimeStrategy;
2526
import net.sourceforge.easyml.marshalling.java.time.ZoneIdStrategy;
@@ -36,6 +37,7 @@
3637
import java.time.LocalDateTime;
3738
import java.time.ZoneId;
3839
import java.util.Calendar;
40+
import java.util.Date;
3941
import java.util.Optional;
4042
import java.util.TimeZone;
4143
import java.util.concurrent.atomic.AtomicReference;
@@ -178,4 +180,26 @@ public void testTimeZoneStrategy() {
178180
assertEquals(expected, xis.read());
179181
xis.close();
180182
}
183+
184+
@Test
185+
public void testRecordStrategy() {
186+
final MyRecord expected = new MyRecord("fn ln", new Date());
187+
188+
final XMLWriter xos = new XMLWriter(this.out);
189+
xos.getCompositeStrategies().add(RecordStrategy.INSTANCE);
190+
xos.write(expected);
191+
xos.close();
192+
193+
System.out.println(this.out);
194+
195+
final XMLReader xis = new XMLReader(new ByteArrayInputStream(this.out.toByteArray()));
196+
xis.getCompositeStrategies().put(RecordStrategy.INSTANCE.name(), RecordStrategy.INSTANCE);
197+
assertEquals(expected, xis.read());
198+
xis.close();
199+
}
200+
201+
public record MyRecord(
202+
String name,
203+
Date theDate) {
204+
}
181205
}

0 commit comments

Comments
 (0)