Compare commits

...

10 commits

Author SHA1 Message Date
Shautvast
0a02524d86 1.7 generic array handling, updated escaping 2023-07-15 18:18:05 +02:00
Shautvast
f483844d65 v1.6 improved LocalDate support 2023-06-25 15:40:34 +02:00
Shautvast
b3687452f6 v1.4 LocalDate support 2023-06-25 15:36:22 +02:00
Shautvast
06a15f3feb v1.4 enum support 2023-06-25 15:28:33 +02:00
Shautvast
4522f36e67 bugfix uuids need a quote 2023-06-25 14:58:54 +02:00
Shautvast
6417930897 bugfix 2023-06-25 14:49:31 +02:00
Shautvast
3ca89dadb5 V1.1 support for UUID and Big numbers 2023-06-25 14:08:36 +02:00
Shautvast
514702a40d distribtionManagement 2023-06-25 11:42:08 +02:00
Shautvast
899bcb16ca v1.0 2023-06-25 11:33:33 +02:00
Shautvast
680106597b javadoc 2023-06-25 11:25:42 +02:00
11 changed files with 254 additions and 330 deletions

View file

@ -5,12 +5,12 @@
<parent> <parent>
<groupId>nl.sander</groupId> <groupId>nl.sander</groupId>
<artifactId>jsonthingy-pom</artifactId> <artifactId>jsonthingy-pom</artifactId>
<version>0.1-SNAPSHOT</version> <version>1.7</version>
</parent> </parent>
<name>JsonToy-JMH</name> <name>JsonToy-JMH</name>
<artifactId>jsonthingy-jmhtests</artifactId> <artifactId>jsonthingy-jmhtests</artifactId>
<version>0.1-SNAPSHOT</version> <version>1.7</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
@ -19,7 +19,6 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>${project.groupId}</groupId> <groupId>${project.groupId}</groupId>

View file

@ -0,0 +1,79 @@
package nl.sanderhautvast.json.jmh;
import org.openjdk.jmh.annotations.*;
import java.lang.reflect.Array;
import java.util.concurrent.TimeUnit;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ArrayReflectionBenchmarks {
private static final int ITERATIONS = 10;
public static void main(String[] args) {
new ArrayReflectionBenchmarks().testReflectiveArray();
}
@Benchmark
public void testReflectiveArray() {
int[] r1 = {1};
int[] r2 = {1, 2};
int[][] table1 = {r1, r2};
int[][] table2 = {r1, r2};
int[][][] schema = {table1, table2};
for (int i = 0; i < ITERATIONS; i++) {
addArrayElements(schema);
}
}
@Benchmark
public void testNonReflectiveArray() {
int[] r1 = {1};
int[] r2 = {1, 2};
int[][] table1 = {r1, r2};
int[][] table2 = {r1, r2};
int[][][] schema = {table1, table2};
for (int i = 0; i < ITERATIONS; i++) {
addIntegerArray(schema);
}
}
private int addArrayElements(Object o) {
int sum = 0;
if (o.getClass().isArray()) {
int length = Array.getLength(o);
for (int i = 0; i < length; i++) {
sum += addArrayElements(Array.get(o, i));
}
} else {
sum += (Integer) o;
}
return sum;
}
private int addIntegerArray(int[][][] o) {
int sum = 0;
for (int[][] ints : o) {
sum += addIntegerArray2(ints);
}
return sum;
}
private int addIntegerArray2(int[][] o) {
int sum = 0;
for (int[] ints : o) {
sum += addIntegerArray3(ints);
}
return sum;
}
private int addIntegerArray3(int[] o) {
int sum = 0;
for (int j : o) {
sum += j;
}
return sum;
}
}

View file

@ -8,14 +8,14 @@ import org.openjdk.jmh.annotations.*;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@State(Scope.Thread) //@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime) //@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS) //@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class Benchmarks { public class Benchmarks {
private static final int ITERATIONS = 10; private static final int ITERATIONS = 10;
@Benchmark // @Benchmark
public void testJson() { public void testJson() {
Bean1 bean1; Bean1 bean1;
Bean2 bean2; Bean2 bean2;
@ -30,7 +30,7 @@ public class Benchmarks {
} }
} }
@Benchmark // @Benchmark
public void testJackson() throws JsonProcessingException { public void testJackson() throws JsonProcessingException {
Bean1 bean1; Bean1 bean1;
Bean2 bean2; Bean2 bean2;

View file

@ -5,12 +5,12 @@
<parent> <parent>
<groupId>nl.sander</groupId> <groupId>nl.sander</groupId>
<artifactId>jsonthingy-pom</artifactId> <artifactId>jsonthingy-pom</artifactId>
<version>0.1-SNAPSHOT</version> <version>1.7</version>
</parent> </parent>
<name>JsonToy</name> <name>JsonToy</name>
<artifactId>jsonthingy</artifactId> <artifactId>jsonthingy</artifactId>
<version>0.1-SNAPSHOT</version> <version>1.7</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>

View file

@ -4,25 +4,32 @@ import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter; import org.objectweb.asm.ClassWriter;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.temporal.Temporal;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* Mapper.json(StringBuilder b, ...)
* TODO write to outputstream
*/
public class Mapper { public class Mapper {
private static final Map<Class<?>, BaseMapper<?>> mappers = new ConcurrentHashMap<>(); private static final Map<Class<?>, BaseMapper<?>> mappers = new ConcurrentHashMap<>();
private static final ByteClassLoader generatedClassesLoader = new ByteClassLoader(); private static final ByteClassLoader generatedClassesLoader = new ByteClassLoader();
private static final StringMapper stringMapper = new StringMapper(); public static final char[] TAB = {'\\', 't'};
private static final BooleanMapper booleanMapper = new BooleanMapper(); public static final char[] DOUBLEQUOTE = {'\\', '\"'};
public static final char[] SLASH = {'\\', '/'};
private static final IntegerMapper integerMapper = new IntegerMapper(); public static final char[] RETURN = {'\\', 'r'};
private static final LongMapper longMapper = new LongMapper(); public static final char[] BACKSLASH = {'\\', '\\'};
private static final ShortMapper shortMapper = new ShortMapper(); public static final char[] NEWLINE = {'\\', 'n'};
private static final ByteMapper byteMapper = new ByteMapper(); public static final char[] BELL = {'\\', 'b'};
private static final CharMapper charMapper = new CharMapper(); public static final char[] FORMFEED = {'\\', 'f'};
private static final FloatMapper floatMapper = new FloatMapper(); private static final char[][] MAP = createEscapeMap();
private static final DoubleMapper doubleMapper = new DoubleMapper();
/** /**
@ -45,56 +52,42 @@ public class Mapper {
return b.toString(); return b.toString();
} }
@SuppressWarnings({"unchecked", "rawtypes"}) @SuppressWarnings({"unchecked", "rawtypes", "UnnecessaryToStringCall"})
public static void json(StringBuilder b, Object value) { public static void json(StringBuilder b, Object value) {
if (value == null) { if (value == null) {
b.append("null"); b.append("null");
} else { } else {
Class<?> type = value.getClass(); Class<?> type = value.getClass();
if (type.isArray()) { if (type.isArray()) {
if (value instanceof byte[]) { array(b, value);
array(b, (byte[]) value);
} else if (value instanceof int[]) {
array(b, (int[]) value);
} else if (value instanceof short[]) {
array(b, (short[]) value);
} else if (value instanceof boolean[]) {
array(b, (boolean[]) value);
} else if (value instanceof char[]) {
array(b, (char[]) value);
} else if (value instanceof long[]) {
array(b, (long[]) value);
} else if (value instanceof float[]) {
array(b, (float[]) value);
} else if (value instanceof double[]) {
array(b, (double[]) value);
} else {
array(b, (Object[]) value);
}
} else if (value instanceof Collection) { } else if (value instanceof Collection) {
list(b, (Collection) value); list(b, (Collection) value);
} else if (value instanceof Map) { } else if (value instanceof Map) {
object(b, (Map) value); object(b, (Map) value);
} else { } else {
if (type.equals(String.class)) { if (type == String.class) {
stringMapper.json(b, (String) value); b.append("\"");
} else if (type.equals(Boolean.class)) { Mapper.escape(b, (String) value);
b.append("\"");
booleanMapper.json(b, (Boolean) value); } else if (type == Character.class) {
} else if (type.equals(Integer.class)) { b.append("\"");
integerMapper.json(b, (Integer) value); Mapper.escape(b, (Character) value);
} else if (type.equals(Long.class)) { b.append("\"");
longMapper.json(b, (Long) value); } else if (type == UUID.class || value instanceof Temporal || type.isEnum()) {
} else if (type.equals(Short.class)) { b.append("\"");
shortMapper.json(b, (Short) value); b.append(value.toString());
} else if (type.equals(Byte.class)) { b.append("\"");
byteMapper.json(b, (Byte) value); } else if (type == Boolean.class
} else if (type.equals(Character.class)) { || type == Integer.class
charMapper.json(b, (Character) value); || type == Long.class
} else if (type.equals(Float.class)) { || type == Float.class
floatMapper.json(b, (Float) value); || type == Double.class
} else if (type.equals(Double.class)) { || type == Byte.class
doubleMapper.json(b, (Double) value); || type == Short.class
|| type == BigInteger.class
|| type == BigDecimal.class
) {
b.append(value.toString()); // prevents another nullcheck
} else { } else {
BaseMapper mapper = mappers.computeIfAbsent(type, key -> createObjectMapper(type)); BaseMapper mapper = mappers.computeIfAbsent(type, key -> createObjectMapper(type));
mapper.json(b, value); mapper.json(b, value);
@ -103,141 +96,17 @@ public class Mapper {
} }
} }
private static void array(StringBuilder b, Object[] array) { //TODO make this more performant
if (array.length == 0) { private static void array(StringBuilder b, Object value) {
b.append("[]");
} else {
Object first = array[0];
b.append("["); b.append("[");
Mapper.json(b, first); StringJoiner joiner = new StringJoiner(",");
Arrays.stream(array).skip(1) for (int i = 0; i < Array.getLength(value); i++) {
.forEach(element -> { Object arrayElement = Array.get(value, i);
b.append(","); joiner.add(Mapper.json(arrayElement)); // recursie
Mapper.json(b, element); }
}); b.append(joiner);
b.append("]"); b.append("]");
} }
}
private static void array(StringBuilder b, byte[] array) {
if (array.length == 0) {
b.append("[]");
} else {
byte first = array[0];
b.append("[");
json(b, first);
for (int i = 1; i < array.length; i++) {
b.append(",");
json(b, array[i]);
}
b.append("]");
}
}
private static void array(StringBuilder b, short[] array) {
if (array.length == 0) {
b.append("[]");
} else {
short first = array[0];
b.append("[");
json(b, first);
for (int i = 1; i < array.length; i++) {
b.append(",");
json(b, array[i]);
}
b.append("]");
}
}
private static void array(StringBuilder b, long[] array) {
if (array.length == 0) {
b.append("[]");
} else {
long first = array[0];
b.append("[");
json(b, first);
for (int i = 1; i < array.length; i++) {
b.append(",");
json(b, array[i]);
}
b.append("]");
}
}
private static void array(StringBuilder b, boolean[] array) {
if (array.length == 0) {
b.append("[]");
} else {
boolean first = array[0];
b.append("[");
json(b, first);
for (int i = 1; i < array.length; i++) {
b.append(",");
json(b, array[i]);
}
b.append("]");
}
}
private static void array(StringBuilder b, double[] array) {
if (array.length == 0) {
b.append("[]");
} else {
double first = array[0];
b.append("[");
json(b, first);
for (int i = 1; i < array.length; i++) {
b.append(",");
json(b, array[i]);
}
b.append("]");
}
}
private static void array(StringBuilder b, char[] array) {
if (array.length == 0) {
b.append("[]");
} else {
char first = array[0];
b.append("[");
json(b, first);
for (int i = 1; i < array.length; i++) {
b.append(",");
json(b, array[i]);
}
b.append("]");
}
}
private static void array(StringBuilder b, float[] array) {
if (array.length == 0) {
b.append("[]");
} else {
float first = array[0];
b.append("[");
json(b, first);
for (int i = 1; i < array.length; i++) {
b.append(",");
json(b, array[i]);
}
b.append("]");
}
}
private static void array(StringBuilder b, int[] array) {
if (array.length == 0) {
b.append("[]");
} else {
int first = array[0];
b.append("[");
json(b, first);
for (int i = 1; i < array.length; i++) {
b.append(",");
json(b, array[i]);
}
b.append("]");
}
}
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
private static void list(StringBuilder b, Collection list) { private static void list(StringBuilder b, Collection list) {
@ -341,134 +210,64 @@ public class Mapper {
escape(b, String.valueOf(c)); escape(b, String.valueOf(c));
} }
private static char[][] createEscapeMap() {
char[][] charmap = new char[0x2100][];
for (int i = 0; i < 0x2100; i++) {
char c = (char) i;
char[] replacement;
if (c == '\t') {
replacement = TAB;
} else if (c == '\"') {
replacement = DOUBLEQUOTE;
} else if (c == '/') {
replacement = SLASH;
} else if (c == '\r') {
replacement = RETURN;
} else if (c == '\\') {
replacement = BACKSLASH;
} else if (c == '\n') {
replacement = NEWLINE;
} else if (c == '\b') {
replacement = BELL;
} else if (c == '\f') {
replacement = FORMFEED;
} else if ((c <= '\u001F') || (c >= '\u007F' && c <= '\u009F') || (c >= '\u2000')) {
replacement = new char[6];
replacement[0] = '\\';
replacement[1] = 'u';
String hex = Integer.toHexString(c).toUpperCase();
int hexlen = hex.length();
for (int k = 0; k < 4 - hexlen; k++) {
replacement[k + 2] = '0';
}
for (int k = 0; k < hexlen; k++) {
replacement[6 - hexlen + k] = hex.charAt(k);
}
} else {
replacement = new char[]{c};
}
charmap[i] = replacement;
}
return charmap;
}
//both methods are equally slow it seems
//mainly because we can't do a batch copy into the stringbuilder and have to map every character individually
static void escape(StringBuilder b, String value) { static void escape(StringBuilder b, String value) {
int offset = b.length(); for (int i = 0; i < value.length(); i++) {
b.append(value); int c = value.charAt(i);
int i = offset;
while (i < b.length()) { if (c < 0x2100) {
char c = b.charAt(i); b.append(MAP[c]);
switch (c) { } else {
case '\t': b.append((char) c);
b.replace(i, i + 1, "\\");
b.insert(i + 1, "t");
break;
case '\"':
b.replace(i, i + 1, "\\");
b.insert(++i, "\"");
break;
case '/':
b.replace(i, i + 1, "\\");
b.insert(++i, "/");
break;
case '\r':
b.replace(i, i + 1, "\\");
b.insert(++i, "r");
break;
case '\n':
b.replace(i, i + 1, "\\");
b.insert(++i, "n");
break;
case '\b':
b.replace(i, i + 1, "\\");
b.insert(++i, "b");
break;
case '\f':
b.replace(i, i + 1, "\\");
b.insert(++i, "f");
break;
case '\\':
b.replace(i, i + 1, "\\");
b.insert(++i, "\\");
break;
case '\'':
break;
default:
if ((c <= '\u001F') || (c >= '\u007F' && c <= '\u009F') || (c >= '\u2000' && c <= '\u20FF')) {
String ss = Integer.toHexString(c);
b.replace(i, i + 1, "\\");
b.insert(++i, "u");
for (int k = 0; k < 4 - ss.length(); k++) {
b.insert(++i, '0');
}
b.insert(++i, ss.toUpperCase());
}
}
i++;
} }
} }
} }
class BooleanMapper extends BaseMapper<Boolean> {
@Override
public void json(StringBuilder b, Boolean value) {
b.append(value);
}
} }
class ShortMapper extends BaseMapper<Short> {
@Override
public void json(StringBuilder b, Short value) {
b.append(value);
}
}
class StringMapper extends BaseMapper<String> {
@Override
public void json(StringBuilder b, String value) {
b.append("\"");
Mapper.escape(b, value);
b.append("\"");
}
}
class IntegerMapper extends BaseMapper<Integer> {
@Override
public void json(StringBuilder b, Integer value) {
b.append(value);
}
}
class LongMapper extends BaseMapper<Long> {
@Override
public void json(StringBuilder b, Long value) {
b.append(value);
}
}
class ByteMapper extends BaseMapper<Byte> {
@Override
protected void json(StringBuilder b, Byte value) {
b.append(value);
}
}
class CharMapper extends BaseMapper<Character> {
@Override
protected void json(StringBuilder b, Character value) {
b.append("\"");
Mapper.escape(b, value);
b.append("\"");
}
}
class FloatMapper extends BaseMapper<Float> {
@Override
protected void json(StringBuilder b, Float value) {
b.append(value);
}
}
class DoubleMapper extends BaseMapper<Double> {
@Override
protected void json(StringBuilder b, Double value) {
b.append(value);
}
}

View file

@ -92,7 +92,7 @@ public class MapperFactory extends ClassVisitor {
} }
getterInsnList.add(new VarInsnNode(ALOAD, 1)); getterInsnList.add(new VarInsnNode(ALOAD, 1));
getterInsnList.add(new LdcInsnNode("\"" + getterMethodName.substring(startIndex).toLowerCase() + "\":")); getterInsnList.add(new LdcInsnNode("\"" + correctName(getterMethodName, startIndex) + "\":"));
getterInsnList.add(new MethodInsnNode(INVOKEVIRTUAL, STRINGBUILDER, APPEND, APPEND_SIGNATURE)); getterInsnList.add(new MethodInsnNode(INVOKEVIRTUAL, STRINGBUILDER, APPEND, APPEND_SIGNATURE));
getterInsnList.add(new InsnNode(POP)); getterInsnList.add(new InsnNode(POP));
getterInsnList.add(new VarInsnNode(ALOAD, 1)); getterInsnList.add(new VarInsnNode(ALOAD, 1));
@ -101,6 +101,11 @@ public class MapperFactory extends ClassVisitor {
getterInsnList.add(new MethodInsnNode(INVOKESTATIC, MAPPER, "json", "(Ljava/lang/StringBuilder;" + genericReturnType(returnType) + ")V")); getterInsnList.add(new MethodInsnNode(INVOKESTATIC, MAPPER, "json", "(Ljava/lang/StringBuilder;" + genericReturnType(returnType) + ")V"));
} }
private String correctName(String getterMethodName, int startIndex) {
String tmp = getterMethodName.substring(startIndex);
return tmp.substring(0, 1).toLowerCase() + tmp.substring(1);
}
private static String genericReturnType(String returnType) { private static String genericReturnType(String returnType) {
char firstChar = returnType.charAt(0); char firstChar = returnType.charAt(0);
if (firstChar == 'L' || firstChar == '[') { if (firstChar == 'L' || firstChar == '[') {

View file

@ -0,0 +1,17 @@
package nl.sanderhautvast.json.ser;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class EnumTest {
enum Answer {
YES, NO
}
@Test
void testEnums() {
assertEquals("\"YES\"", Mapper.json(Answer.YES));
}
}

View file

@ -22,7 +22,7 @@ public class JacksonComparisonTest {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
Bean1 bean1 = new Bean1(); Bean1 bean1 = new Bean1();
Bean2 bean2 = new Bean2(); Bean2 bean2 = new Bean2();
bean1.setData1(UUID.randomUUID().toString()); bean1.setData1(UUID.randomUUID());
bean1.setBean2(bean2); bean1.setBean2(bean2);
bean2.setData2(UUID.randomUUID().toString()); bean2.setData2(UUID.randomUUID().toString());
String valueAsString; String valueAsString;
@ -34,7 +34,7 @@ public class JacksonComparisonTest {
for (int i = 0; i < INNERLOOP_COUNT; i++) { for (int i = 0; i < INNERLOOP_COUNT; i++) {
bean1 = new Bean1(); bean1 = new Bean1();
bean2 = new Bean2(); bean2 = new Bean2();
bean1.setData1(UUID.randomUUID().toString()); bean1.setData1(UUID.randomUUID());
bean1.setBean2(bean2); bean1.setBean2(bean2);
bean2.setData2(UUID.randomUUID().toString()); bean2.setData2(UUID.randomUUID().toString());
valueAsString = objectMapper.writeValueAsString(bean1); valueAsString = objectMapper.writeValueAsString(bean1);
@ -47,7 +47,7 @@ public class JacksonComparisonTest {
for (int i = 0; i < INNERLOOP_COUNT; i++) { for (int i = 0; i < INNERLOOP_COUNT; i++) {
bean1 = new Bean1(); bean1 = new Bean1();
bean2 = new Bean2(); bean2 = new Bean2();
bean1.setData1(UUID.randomUUID().toString()); bean1.setData1(UUID.randomUUID());
bean1.setBean2(bean2); bean1.setBean2(bean2);
bean2.setData2(UUID.randomUUID().toString()); bean2.setData2(UUID.randomUUID().toString());
jsonString = Mapper.json(bean1); jsonString = Mapper.json(bean1);

View file

@ -0,0 +1,21 @@
package nl.sanderhautvast.json.ser;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
class LocalDateTest {
@Test
void testLocalDate() {
assertEquals("\"2023-10-11\"", Mapper.json(LocalDate.of(2023, 10, 11)));
}
@Test
void testLocalDateTime() {
assertEquals("\"2023-10-11T13:15:27\"", Mapper.json(LocalDateTime.of(2023, 10, 11,13,15,27)));
}
}

View file

@ -1,16 +1,18 @@
package nl.sanderhautvast.json.ser.nested; package nl.sanderhautvast.json.ser.nested;
import java.util.UUID;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class Bean1 { public class Bean1 {
private String data1; private UUID data1;
private Bean2 bean2; private Bean2 bean2;
public String getData1() { public UUID getData1() {
return data1; return data1;
} }
public void setData1(String data1) { public void setData1(UUID data1) {
this.data1 = data1; this.data1 = data1;
} }

16
pom.xml
View file

@ -5,20 +5,22 @@
<name>JsonToy</name> <name>JsonToy</name>
<groupId>nl.sander</groupId> <groupId>nl.sander</groupId>
<artifactId>jsonthingy-pom</artifactId> <artifactId>jsonthingy-pom</artifactId>
<version>0.1-SNAPSHOT</version> <version>1.7</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<properties>
<maven.compiler.source>9</maven.compiler.source>
<maven.compiler.target>9</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<modules> <modules>
<module>lib</module> <module>lib</module>
<module>jmh</module> <module>jmh</module>
</modules> </modules>
<distributionManagement>
<repository>
<id>github</id>
<name>GitHub OWNER Apache Maven Packages</name>
<url>https://maven.pkg.github.com/shautvast/JsonToy</url>
</repository>
</distributionManagement>
<build> <build>
<pluginManagement> <pluginManagement>
<plugins> <plugins>