diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e10bbf1..6289a82 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,9 @@ on: pull_request: types: [ opened, synchronize, reopened ] +env: + AZURE_BLOB_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_BLOB_STORAGE_CONNECTION_STRING }} + jobs: build: runs-on: ubuntu-latest @@ -25,6 +28,7 @@ jobs: run: mvn --batch-mode -DskipTests package - name: Test + timeout-minutes: 5 run: mvn --batch-mode -Dmaven.test.failure.ignore=true test - name: Report @@ -38,6 +42,7 @@ jobs: - name: Upload to Azure Blob Storage uses: bacongobbler/azure-blob-storage-upload@v3.0.0 + if: env.AZURE_BLOB_STORAGE_CONNECTION_STRING != null with: source_dir: 'target/apidocs' container_name: '$web' diff --git a/pom.xml b/pom.xml index 179f566..94056c5 100755 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,19 @@ com.google.code.gson gson 2.10.1 - test + test + + + org.openjdk.jmh + jmh-core + 1.37 + test + + + org.openjdk.jmh + jmh-generator-annprocess + 1.37 + test diff --git a/readme.md b/readme.md index 48181e4..f472293 100755 --- a/readme.md +++ b/readme.md @@ -295,7 +295,7 @@ public ServerSentEventHandler chatMessages() { Use it directly anywhere: ```java -Post("/message") +@Post("/message") public void message(@QueryParam("message") String message, @EventHandler("messages") ServerSentEventHandler chatMessagesEventHandler) { chatMessagesEventHandler.sendMessage(message); @@ -348,6 +348,73 @@ public GsonMessageConverter getGsonConverter() { } ``` +## TeenyJson + +TeenyHttpd includes a simple JSON library called TeenyJson. It is a simple, lightweight JSON library that is used to convert JSON strings to Java objects, and vice versa. +It is not as feature-rich as other JSON libraries, but it is lightweight and easy to use. + +### Parsing JSON + +```java +Person person = new TeenyJson().readValue(json, Person.class); +``` + +Similar to jackson to parse a ambiguous property, you can use the `@JsonDeserialize` annotation: + +```java +@JsonDeserialize(contentAs = ObjectC.class) +public void setList(List list) { + this.list = list; +} + +@JsonDeserialize(as = ObjectC.class) +public void setC(ObjectC c) { + this.c = c; +} +``` + +### Generating JSON + +```java +Person person = new Person("John", 30, null); +String json = new TeenyJson().writeValueAsString(person); +``` + +To give a property an alias in the JSON, you can use the `@JsonProperty` annotation: + +```java +@JsonAlias("bestSongs") +public Set getFavoriteSongs() { + return favoriteSongs; +} +``` + +To ignore a property in the JSON, you can use the `@JsonIgnore` annotation: + +```java +@JsonIgnore +public String getSecret() { + return secret; +} +``` + +To ignore all properties that are null, you can use the `@JsonIncludeNonNull` annotation: + +```java +@JsonIncludeNonNull +public class Person { +// ... +} +``` + +You can also customize the deserialization and serialization process by specifying a custom serializer or deserializer: + +```java +new TeenyJson() + .registerSerializer(String.class, String::toUpperCase) + .registerParser(String.class, (value) -> value.toString().toLowerCase()); +``` + ## Project Management Releases are performed using `mvn clean deploy -Prelease`. \ No newline at end of file diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/TeenyApplication.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/TeenyApplication.java index b02688b..ed474d6 100644 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/TeenyApplication.java +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/TeenyApplication.java @@ -53,6 +53,7 @@ private TeenyApplication() { server = new TeenyHttpd(Integer.parseInt(System.getProperty("server.port", "8080"))); this.messageConverterMap = new HashMap<>(); this.messageConverterMap.put(DefaultMessageConverter.INSTANCE.getContentType(), DefaultMessageConverter.INSTANCE); + this.messageConverterMap.put("application/json", new net.jonathangiles.tools.teenyhttpd.json.TeenyJsonMessageConverter()); } public TeenyApplication registerMessageConverter(MessageConverter messageConverter) { diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ParameterizedTypeHelper.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ParameterizedTypeHelper.java new file mode 100644 index 0000000..d3ea9ca --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ParameterizedTypeHelper.java @@ -0,0 +1,98 @@ +package net.jonathangiles.tools.teenyhttpd.implementation; + +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.Objects; + +/** A helper class to hold the type arguments of a parameterized type and provide some utility methods. */ +public final class ParameterizedTypeHelper implements Type { + private final Class firstType; + private final Class parentType;//nullable + private final Type[] typeArguments; + + public ParameterizedTypeHelper(Class firstType, Class parentType) { + this.firstType = firstType; + this.parentType = parentType; + this.typeArguments = null; + } + + ParameterizedTypeHelper(Class parentType, Type[] arguments) { + this.firstType = getRealClass(arguments[0]); + this.parentType = parentType; + this.typeArguments = arguments; + } + + /** + * Returns the real class of the given type. + * + * @param type the type to get the real class of + * @return the real class of the given type + */ + private Class getRealClass(Type type) { + if (type instanceof WildcardType) { + WildcardType wildcardType = (WildcardType) type; + return (Class) wildcardType.getUpperBounds()[0]; + } + + return (Class) type; + } + + /** + * Returns a new instance of ParameterizedTypeHelper with the given first type. + * + * @param firstType the first type + * @return a new instance of ParameterizedTypeHelper + */ + public ParameterizedTypeHelper withFirstType(Class firstType) { + if (typeArguments == null) { + return new ParameterizedTypeHelper(firstType, parentType); + } + + Type[] args = new Type[typeArguments.length]; + + args[0] = firstType; + System.arraycopy(typeArguments, 1, args, 1, typeArguments.length - 1); + + return new ParameterizedTypeHelper(parentType, args); + } + + @Override + public String getTypeName() { + return getClass().getSimpleName(); + } + + public boolean isParentTypeOf(Class type) { + return parentType != null && type.isAssignableFrom(parentType); + } + + public Type[] getTypeArguments() { + return typeArguments; + } + + public Class getFirstType() { + return firstType; + } + + /** + * @return the second type of the parameterized type + * @throws NullPointerException if the type is not a ParameterizedType + */ + public Class getSecondType() { + Objects.requireNonNull(typeArguments, "Type is not a ParameterizedType"); + return getRealClass(typeArguments[1]); + } + + public Class getParentType() { + return parentType; + } + + @Override + public String toString() { + return "ParameterizedTypeHelper{" + + "firstType=" + firstType + + ", parentType=" + parentType + + ", typeArguments=" + Arrays.toString(typeArguments) + + '}'; + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ReflectionUtils.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ReflectionUtils.java new file mode 100644 index 0000000..22f771c --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ReflectionUtils.java @@ -0,0 +1,216 @@ +package net.jonathangiles.tools.teenyhttpd.implementation; + +import net.jonathangiles.tools.teenyhttpd.json.JsonAlias; + +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A utility class to help with reflection. + */ +public final class ReflectionUtils { + + /** + * Create a new instance of the given class. + *

+ * @param clazz the class to create a new instance of + * @return a new instance of the given class + * @param the type of the class + * @throws IllegalArgumentException if the class does not have a default constructor or if the constructor fails + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz) { + Constructor constructor = Arrays.stream(clazz.getDeclaredConstructors()) + .filter(cons -> cons.getParameterCount() == 0) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No default constructor found for " + clazz.getName())); + + try { + constructor.setAccessible(true); + return (T) constructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException("Failed to create new instance of " + clazz.getName(), e); + } + } + + public static ParameterizedTypeHelper getParameterType(Parameter parameter) { + return getParameterType(parameter.getParameterizedType()); + } + + public static ParameterizedTypeHelper getParameterType(Field field) { + return getParameterType(field.getGenericType()); + } + + /** + * Creates an instance of ParameterizedTypeHelper from the given type. + *

+ * @param type the type to create the instance from + * @return the instance of ParameterizedTypeHelper or null if the type is not a ParameterizedType + * @throws IllegalStateException if the type is not a Class or a ParameterizedType + */ + public static ParameterizedTypeHelper getParameterType(Type type) { + if (!(type instanceof ParameterizedType)) { + return null; + } + + ParameterizedType parameterizedType = (ParameterizedType) type; + + return new ParameterizedTypeHelper((Class) parameterizedType.getRawType(), + parameterizedType.getActualTypeArguments()); + } + + /** + * Returns the component type of the given type. + * + * @param type the type to get the component type from + * @return the component type of the given type or null if the type is not an array + */ + public static Class getArrayType(Type type) { + if (type instanceof GenericArrayType) { + return (Class) ((GenericArrayType) type).getGenericComponentType(); + } + + if (type instanceof Class) { + return ((Class) type).getComponentType(); + } + + return null; + } + + /** + * Returns the component type of the given type. + * + * @param type the type to get the component type from + * @return the component type of the given type or null if the type is not an array + */ + public static boolean isEnum(Type type) { + if (type instanceof Class) { + return ((Class) type).isEnum(); + } + return false; + } + + /** + * Returns a map of the writable fields of the given class, + * where the key is the name of the field or his alias and the value is the field itself. + *

+ * @param clazz the class to get the fields from + * @return a map of the fields of the given class + */ + public static Map getFields(Class clazz) { + Map methodMap = Arrays.stream(clazz.getDeclaredMethods()) + .collect(Collectors.toMap(Method::getName, m -> m)); + + return Arrays.stream(clazz.getDeclaredFields()) + .filter(ReflectionUtils::isWritable) + .collect(Collectors.toMap(field -> getFieldName(field, methodMap), f -> f)); + } + + /** + * Here we seek for the alias key in the accessor method, if it exists. + */ + private static String getFieldName(Field field, Map methodMap) { + //base name of the field + String baseName = field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1); + String alias = findAlias(methodMap, baseName, field.getType()); + + return Objects.requireNonNullElse(alias, field.getName()); + } + + /** + * Here we seek for the alias key in the accessor method, if it exists. + */ + private static String getMutatorName(Method method, Map methodMap) { + //base name of the method + String baseName = method.getName().substring(3); + //seek for the alias key in the accessor method, if it exists + String alias = findAlias(methodMap, baseName, method.getParameterTypes()[0]); + if (alias != null) { + return alias; + } + + return method.getName().substring(3, 4).toLowerCase() + + method.getName().substring(4); + } + + /** + * Here we seek for the alias key in the accessor method, if it exists. + * + * @param methodMap current method map + * @param baseName base name of the method + * @param type type of the method + * @return alias key in the accessor method, if it exists + */ + private static String findAlias(Map methodMap, String baseName, Class type) { + Method accessorMethod; + + if (type == boolean.class) { + accessorMethod = methodMap.get("is" + baseName); + } else { + accessorMethod = methodMap.get("get" + baseName); + } + + if (accessorMethod != null) { + JsonAlias jsonAlias = accessorMethod.getAnnotation(JsonAlias.class); + + if (jsonAlias != null) { + return jsonAlias.value(); + } + } + + return null; + } + + /** + * Returns a map of the mutator methods of the given class, + * where the key is the name of the mutator or his alias and the value is the method itself. + *

+ * @param clazz the class to get the mutators from + * @return a map of the mutators of the given class + */ + public static Map getMutators(Class clazz) { + Map methodMap = Arrays.stream(clazz.getDeclaredMethods()) + .collect(Collectors.toMap(Method::getName, m -> m)); + + Map resultMap = new HashMap<>(); + + for (Method method : methodMap.values()) { + if (!isMutator(method)) continue; + + String mutatorName = getMutatorName(method, methodMap); + + resultMap.put(mutatorName, method); + } + + return resultMap; + } + + + /** + * Checks if the given method is a mutator. + * + * @param method the method to check + * @return true if the method is a mutator, false otherwise + */ + private static boolean isMutator(Method method) { + return method.getName().startsWith("set") && method.getParameterCount() == 1; + } + + /** + * Checks if the given field is writable. + * + * @param field the field to check + * @return true if the field is writable, false otherwise + */ + private static boolean isWritable(Field field) { + if (Modifier.isStatic(field.getModifiers())) { + return false; + } + return !Modifier.isFinal(field.getModifiers()); + } + +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/AccessorInvoker.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/AccessorInvoker.java new file mode 100644 index 0000000..3f9b6ed --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/AccessorInvoker.java @@ -0,0 +1,45 @@ +package net.jonathangiles.tools.teenyhttpd.json; + + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.function.Function; +import java.util.logging.Logger; + +/** + * A class that wraps a method and a function that can be used to invoke the method. This is used to invoke methods on + * objects that are annotated with {@link JsonIgnore}, {@link JsonIncludeNonNull}, or {@link JsonRaw}. + */ +final class AccessorInvoker { + private final Method method; + private final Function function; + + public AccessorInvoker(Method method, Function function) { + this.method = method; + this.function = function; + } + + /** + * Invokes the method on the given instance, returning the result. + * @param instance the instance to invoke the method on + * @return the result of invoking the method on the given instance (or null if an error occurred) + */ + public Object invoke(Object instance) { + try { + return function.apply(instance); + } catch (Exception ex) { + Logger.getLogger(AccessorInvoker.class.getName()) + .severe("Error invoking method: " + method.getName()); + } + + return null; + } + + public boolean isAnnotationPresent(Class annotation) { + return method.isAnnotationPresent(annotation); + } + + public T getAnnotation(Class annotation) { + return method.getAnnotation(annotation); + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonAlias.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonAlias.java new file mode 100644 index 0000000..af65055 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonAlias.java @@ -0,0 +1,15 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation to specify an alias for a field when serializing or deserializing JSON. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface JsonAlias { + String value(); +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonDecoder.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonDecoder.java new file mode 100644 index 0000000..2960992 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonDecoder.java @@ -0,0 +1,238 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * A simple JSON decoder that can decode a JSON string into a Map or List of Maps. + */ +final class JsonDecoder { + + private final Buffer buffer; + + public JsonDecoder(String json) { + this.buffer = new Buffer(json); + } + + /** + * Read the JSON string and return the parsed object. + * + * @return the parsed object which could be null. + */ + Object read() { + char current = buffer.current(); + + if (current == '{') { + return readObject(); + } + + if (current == '"') { + return readString(); + } + + if (current == '[') { + return readArray(); + } + + if (Character.isDigit(current) || + current == '-' || + current == '+' || + current == '.') { + return readNumber(); + } + + if (current == 'n' || current == 'N') { + buffer.next(4); + return null; + } + + if (current == 'f' || current == 'F' || + current == 't' || current == 'T') { + return readBoolean(current); + } + + if (shouldSkip(current)) { + buffer.advanceSpaces(); + return read(); + } + + return null; + } + + private static boolean shouldSkip(char current) { + return current == ' ' || current == '\n' || current == '\t' || current == '\r'; + } + + private Object readBoolean(char current) { + if (current == 't' || current == 'T') { + return Boolean.parseBoolean(buffer.next(4)); + } + + return Boolean.parseBoolean(buffer.next(5)); + } + + private String readNumber() { + StringBuilder sb = new StringBuilder(); + + if (buffer.current() != '+') { + sb.append(buffer.current()); + } + + char temp; + + while (buffer.next()) { + temp = buffer.current(); + + if ((temp == '}' || temp == ']') || temp == ',') { + break; + } else { + sb.append(temp); + } + } + + buffer.back(); + + return sb.toString(); + } + + private List readArray() { + List result = new LinkedList<>(); + + boolean hasNext = true; + while (hasNext) { + buffer.next(); + if (buffer.current() == ']') { + return result; + } + + result.add(read()); + + if (buffer.current() == ']') { + hasNext = false; + } + + buffer.next(); + + if (buffer.current() == ']') { + hasNext = false; + } + } + + return result; + } + + private String readString() { + StringBuilder sb = new StringBuilder(); + buffer.advanceSpaces(); + + if (buffer.current() == '"') { + while (buffer.next()) { + char temp = buffer.current(); + + if ('"' == temp) { + if ('\\' != buffer.readBefore()) { + break; + } + } + sb.append(temp); + } + } else { + return ""; + } + + return sb.toString(); + } + + private Map readObject() { + Map result = new HashMap<>(); + + boolean hasNext = true; + + while (hasNext) { + buffer.next(); + + if (buffer.current() == '}') { + return result; + } + + //readString requires the buffer to be at the start of the string + if (buffer.last() == '"') { + buffer.back(); + } + + String key = readString(); + + while (buffer.next()) { + char temp = buffer.current(); + if (temp == ':') { + buffer.next(); + buffer.advanceSpaces(); + break; + } + } + + result.put(key, read()); + buffer.next(); + + if (buffer.current() == '}') { + hasNext = false; + } + } + + return result; + } + + /** + * This class tracks the current index of the JSON string and provides methods to read the string. + */ + private static class Buffer { + private final String json; + private int index; + + Buffer(String json) { + this.json = json; + } + + char current() { + return json.charAt(index); + } + + char last() { + return json.charAt(index - 1); + } + + void back() { + index--; + } + + char readBefore() { + return json.charAt(index - 1); + } + + boolean next() { + if (index + 1 >= json.length()) { + return false; + } else { + index++; + return true; + } + } + + void advanceSpaces() { + while (shouldSkip(json.charAt(index))) { + index++; + } + } + + String next(int i) { + String value = json.substring(index, index + i); + index += i; + + return value; + } + + } + +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonDeserialize.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonDeserialize.java new file mode 100644 index 0000000..e99878c --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonDeserialize.java @@ -0,0 +1,18 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to specify how to deserialize a JSON string into a Java object. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface JsonDeserialize { + + Class contentAs() default Object.class; + + Class as() default Object.class; +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonEncoder.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonEncoder.java new file mode 100644 index 0000000..38b5834 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonEncoder.java @@ -0,0 +1,454 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.lang.invoke.*; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + + +/** + * A simple JSON encoder that can serialize objects to a JSON string. + */ +final class JsonEncoder { + + private final Map, Mapper> cache = new ConcurrentHashMap<>(); + private final Map, ValueSerializer> serializers = new ConcurrentHashMap<>(); + private final SimpleDateFormat dateFormatter = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); + + SimpleDateFormat getDateFormatter() { + return dateFormatter; + } + + /** + * Registers a serializer for a specific class. + * + * @param clazz the class to register + */ + public void registerSerializer(Class clazz, ValueSerializer serializer) { + serializers.put(clazz, serializer); + } + + /** + * Writes an object to a JSON string. + * + * @param value the object to be written + * @return the JSON string + */ + public String writeValueAsString(Object value) { + if (value == null) return null; + if (value instanceof String) return (String) value; + + if (value instanceof Collection) { + Collection collection = (Collection) value; + return "[" + collection.stream() + .map(this::serialize) + .collect(Collectors.joining(", ")) + "]"; + } + + return serialize(value); + + } + + + /** + * Serializes an object to a JSON string. + * + * @param object the object to be serialized + * @return the JSON string + */ + private String serialize(Object object) { + if (object == null) return "null"; + + Mapper cachedMapper = cache.get(object.getClass()); + + if (cachedMapper != null) { + return cachedMapper.serialize(object, this); + } + + if (object instanceof Map) { + Map map = (Map) object; + return "{" + map.entrySet() + .stream() + .map(entry -> "\"" + entry.getKey() + "\":" + serialize(entry.getValue())) + .collect(Collectors.joining(",")) + "}"; + } + + if (object instanceof Collection) { + Collection list = (Collection) object; + return "[" + list.stream() + .map(this::serialize) + .collect(Collectors.joining(",")) + "]"; + } + + if (object.getClass().isArray()) { + return serializeArray(object); + } + + final Class clazz = object.getClass(); + + if (clazz.getName().startsWith("java.lang")) { + return writeValue(object); + } + + Method[] methods = clazz.getDeclaredMethods(); + + Mapper mapper = new Mapper(clazz); + cache.put(clazz, mapper); + + for (Method method : methods) { + try { + if (method.getName().equals("hashCode")) continue; + if (method.getName().equals("toString")) continue; + + mapper.put(method); + } catch (Throwable e) { + Logger.getLogger(JsonEncoder.class.getName()) + .log(Level.SEVERE, "Error serializing object: " + object.getClass().getName(), e); + } + } + + return mapper.serialize(object, this); + } + + /** + * Serializes an array to a JSON string. + * + *

This method handles both arrays of objects and arrays of primitives. + * For arrays of objects, it serializes each object in the array and joins them with a comma. + * For arrays of primitives, it converts the array to a string representation. + * + * @param object the array to be serialized, which can be an array of objects or an array of primitives + * @return the JSON string representation of the array + * @throws IllegalArgumentException if the array is of a type that isn't handled + */ + private String serializeArray(Object object) { + if (object instanceof Object[]) { + Object[] array = (Object[]) object; + return "[" + Arrays.stream(array) + .map(this::serialize) + .collect(Collectors.joining(", ")) + "]"; + } else if (object instanceof int[]) { + int[] array = (int[]) object; + return Arrays.toString(array).replace(" ", ""); + } else if (object instanceof double[]) { + double[] array = (double[]) object; + return Arrays.toString(array).replace(" ", ""); + } else if (object instanceof long[]) { + long[] array = (long[]) object; + return Arrays.toString(array).replace(" ", ""); + } else if (object instanceof char[]) { + char[] array = (char[]) object; + return Arrays.toString(array).replace(" ", ""); + } else if (object instanceof float[]) { + float[] array = (float[]) object; + return Arrays.toString(array).replace(" ", ""); + } else if (object instanceof boolean[]) { + boolean[] array = (boolean[]) object; + return Arrays.toString(array).replace(" ", ""); + } else if (object instanceof byte[]) { + byte[] array = (byte[]) object; + return Arrays.toString(array).replace(" ", ""); + } else if (object instanceof short[]) { + short[] array = (short[]) object; + return Arrays.toString(array).replace(" ", ""); + } else { + throw new IllegalArgumentException("Unknown array type: " + object.getClass().getName()); + } + } + + /** + * Writes a field to a JSON string. + * + * @param name the name of the field + * @param value the value of the field + * @param includeNonNull whether to include null values or not + * @return the JSON representation of the field + */ + private String writeField(String name, Object value, boolean includeNonNull, AccessorInvoker invoker) { + if (includeNonNull && value == null) return null; + + if (invoker.isAnnotationPresent(JsonRaw.class) && value instanceof String) { + if (invoker.getAnnotation(JsonRaw.class).includeKey()) { + return "\"" + name + "\":" + value; + } + return (String) value; + } + + if (invoker.isAnnotationPresent(JsonIncludeNonNull.class) && value == null) return null; + + StringBuilder sb = new StringBuilder(); + + try { + sb.append("\"").append(name).append("\":"); + + if (value instanceof Map) { + sb.append("{"); + + String map = ((Map) value) + .entrySet() + .stream() + .map(entry -> "\"" + entry.getKey() + "\":" + serialize(entry.getValue())) + .collect(Collectors.joining(", ")); + + return sb.append(map) + .append("}") + .toString(); + } + + if (value instanceof Collection) { + sb.append("["); + + String collection = ((Collection) value) + .stream() + .map(this::serialize) + .collect(Collectors.joining(",")); + + return sb.append(collection) + .append("]") + .toString(); + } + + sb.append(writeValue(value)); + } catch (Exception e) { + Logger.getLogger(JsonEncoder.class.getName()) + .log(Level.SEVERE, null, e); + } + + return sb.toString(); + } + + /** + * Gets the field name of a method. + * If the method is annotated with {@link JsonAlias}, the value of the annotation is returned. + * + * @param method the method to get the field name from + * @return the field name + */ + private static String getFieldName(Method method) { + if (method.isAnnotationPresent(JsonAlias.class)) { + String alias = method.getAnnotation(JsonAlias.class).value(); + + if (!alias.isEmpty()) { + return alias; + } + } + + String name = method.getName(); + + if (name.startsWith("get") || name.startsWith("set")) { + name = name.substring(3); + } + + if (name.startsWith("is")) { + name = name.substring(2); + } + + return name.substring(0, 1).toLowerCase() + name.substring(1); + } + + /** + * Writes the value as a valid JSON value. + * @param value the value to be written + * @return the JSON representation of the value + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private String writeValue(Object value) { + if (value == null) return "null"; + + ValueSerializer serializer = serializers.get(value.getClass()); + + if (serializer != null) { + return serializer.serialize(value); + } + + if (value instanceof String) { + return "\"" + escapeJsonString((String) value) + "\""; + } + + if (value instanceof Number) { + return value.toString(); + } + + if (value instanceof Boolean) { + return value.toString(); + } + + if (value instanceof Date) { + return "\"" + dateFormatter.format(value) + "\""; + } + + if (value instanceof Character) { + return "\"" + value + "\""; + } + + if (value instanceof LocalDate) { + return "\"" + value + "\""; + } + + if (value instanceof LocalDateTime) { + return "\"" + value + "\""; + } + + if (value instanceof LocalTime) { + return "\"" + value + "\""; + } + + if (value.getClass().isEnum()) { + return "\"" + value + "\""; + } + + if (!value.getClass().getName().startsWith("java.lang") && !value.getClass().getName().startsWith("java.time")) { + return serialize(value); + } + + System.err.println("Unknown type: " + value.getClass().getName()); + + return null; + } + + /** + * Escapes a string to be a valid JSON string. + * + * @param s the string to be escaped + * @return the escaped string + */ + private String escapeJsonString(String s) { + if (s == null) return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + switch (ch) { + case '"': + case '\\': + sb.append('\\'); + sb.append(ch); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + if (ch <= 0x1F) { + String ss = Integer.toHexString(ch); + sb.append("\\u"); + for (int k = 0; k < 4 - ss.length(); k++) { + sb.append('0'); + } + sb.append(ss.toUpperCase()); + } else { + sb.append(ch); + } + } + } + return sb.toString(); + } + + /** + * This class holds all lambdas to call the getter methods of a class. + * It also holds the target class and a flag to include or not null values. + * It is used to serialize an object to a JSON string. + */ + private static class Mapper extends ConcurrentHashMap { + private final Class target; + private final boolean includeNonNull; + + public Mapper(Class target) { + this.target = target; + includeNonNull = target.isAnnotationPresent(JsonIncludeNonNull.class); + } + + private void put(Method method) throws Throwable { + if (method.isAnnotationPresent(JsonIgnore.class)) return; + if (Modifier.isStatic(method.getModifiers())) return; + if (method.getParameterCount() > 0) return; + + String fieldName = getFieldName(method); + put(fieldName, createFunction(target, method)); + } + + /** + * Serializes an object to a JSON string. + * @param object the object to be serialized + * @param encoder the encoder to be used + * @return the JSON string + */ + private String serialize(Object object, JsonEncoder encoder) { + List properties = new LinkedList<>(); + + for (Map.Entry entry : entrySet()) { + Object value = entry.getValue().invoke(object); + if (value == null && !includeNonNull) continue; + String field = encoder.writeField(entry.getKey(), value, includeNonNull, entry.getValue()); + if (field == null) continue; + + properties.add(field); + } + + return "{" + String.join(",", properties) + "}"; + } + } + + /** + * Creates a function to call the getter method of a class. + * + * @param targetClass the class to create the function for + * @param method the method to call + * @return the function to call the method + * @throws Throwable if an error occurs while creating the function + */ + private static AccessorInvoker createFunction(Class targetClass, Method method) throws Throwable { + try { + MethodHandles.Lookup lookup = getLookup(targetClass); + MethodHandle virtualMethodHandle = lookup.findVirtual(targetClass, method.getName(), MethodType.methodType(method.getReturnType())); + CallSite site = LambdaMetafactory.metafactory(lookup, + "apply", + MethodType.methodType(Function.class), + MethodType.methodType(Object.class, Object.class), + virtualMethodHandle, + MethodType.methodType(method.getReturnType(), targetClass)); + @SuppressWarnings("unchecked") + Function getterFunction = (Function) site.getTarget().invokeExact(); + return new AccessorInvoker(method, getterFunction); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Unable to create function for " + targetClass + " at " + method.getName(), e); + } + } + + /** + * Gets the lookup object for a class. + * If the class is not accessible, a private lookup is created. + * + * @param targetClass the class to get the lookup for + * @return the lookup object + */ + private static MethodHandles.Lookup getLookup(Class targetClass) { + MethodHandles.Lookup lookupMe = MethodHandles.lookup(); + + try { + return MethodHandles.privateLookupIn(targetClass, lookupMe); + } catch (IllegalAccessException e) { + return lookupMe; + } + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonIgnore.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonIgnore.java new file mode 100644 index 0000000..02bba21 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonIgnore.java @@ -0,0 +1,14 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation to specify that a field should be ignored when serializing or deserializing JSON. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface JsonIgnore { +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonIncludeNonNull.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonIncludeNonNull.java new file mode 100644 index 0000000..b5fcb77 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonIncludeNonNull.java @@ -0,0 +1,15 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation to specify that a field should be included in the JSON output only if it is not null. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface JsonIncludeNonNull { + +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonParsingException.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonParsingException.java new file mode 100644 index 0000000..0673125 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonParsingException.java @@ -0,0 +1,15 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +/** + * An exception that is thrown when there is an error parsing a JSON string. + */ +public class JsonParsingException extends Exception { + + public JsonParsingException(String message) { + super(message); + } + + public JsonParsingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonRaw.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonRaw.java new file mode 100644 index 0000000..582e21f --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/JsonRaw.java @@ -0,0 +1,16 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation to specify that a field should be serialized as raw JSON. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface JsonRaw { + boolean includeKey() default false; + +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/TeenyJson.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/TeenyJson.java new file mode 100644 index 0000000..6414542 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/TeenyJson.java @@ -0,0 +1,497 @@ +package net.jonathangiles.tools.teenyhttpd.json; + + +import net.jonathangiles.tools.teenyhttpd.implementation.ParameterizedTypeHelper; +import net.jonathangiles.tools.teenyhttpd.implementation.ReflectionUtils; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.ParseException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * A simple JSON serializer and deserializer. + */ +public final class TeenyJson { + + private final JsonEncoder encoder; + private final Map, ValueParser> parsers = new ConcurrentHashMap<>(); + + public TeenyJson() { + encoder = new JsonEncoder(); + } + + /** + * Serialize the given object to a JSON string. + * @param value the object to serialize + * @return the serialized JSON string + */ + public String writeValueAsString(Object value) { + return encoder.writeValueAsString(value); + } + + /** + * Serialize the given object to a JSON string and write it to the given output stream. + * @param outputStream the output stream to write to + * @param value the object to serialize + * @throws IOException if a problem occurs during writing + */ + public void writeValue(BufferedOutputStream outputStream, Object value) throws IOException { + if (value == null) return; + outputStream.write(writeValueAsString(value).getBytes()); + } + + /** + * Read a JSON string and return a collection of objects of the given type. + * + * @param collectionType one of List or Set + * @param json the JSON string to parse + * @return the parsed object which could be null. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public K readCollection(String json, Class collectionType, Class type) { + if (json == null) return null; + + Object result = new JsonDecoder(json) + .read(); + + if (result == null) { + return null; + } + + List list = (List) result; + + if (List.class.isAssignableFrom(collectionType)) { + return (K) list.stream() + .map(o -> parseObject(o, type)) + .collect(Collectors.toList()); + } + + if (Set.class.isAssignableFrom(collectionType)) { + return (K) list.stream() + .map(o -> parseObject(o, type)) + .collect(Collectors.toSet()); + } + + throw new IllegalStateException("Unsupported collection type: " + collectionType.getName()); + } + + /** + * + * Read a JSON string and return an object of the given type. + * + * @param json the JSON string to parse + * @param type the type of the object to parse + * + */ + @SuppressWarnings("unchecked") + public T readValue(String json, Class type) { + if (json == null) return null; + + if (type == String.class) { + return (T) json; + } + + Object result = new JsonDecoder(json).read(); + + if (result == null) { + return null; + } + //return the parsed map + if (type == Map.class && result instanceof Map) { + return (T) result; + } + + return parseObject(result, type); + } + + @SuppressWarnings("unchecked") + public T readValue(String json, Type type) throws JsonParsingException { + if (json == null) return null; + //if the type is string, just return the string + if (type == String.class) { + return (T) json; + } + + Object result = new JsonDecoder(json).read(); + + if (result == null) { + return null; + } + + if (type instanceof Class) { + return (T) parse(result, type); + } + + throw new JsonParsingException("Unsupported type: " + type.getTypeName() + " " + type.getClass().getName()); + } + + + /** + * Register a custom serializer for the given class. + *

+ * @param clazz the class to register + */ + @SuppressWarnings("UnusedReturnValue") + public TeenyJson registerSerializer(Class clazz, ValueSerializer serializer) { + encoder.registerSerializer(clazz, serializer); + return this; + } + + /** + * Register a custom parser for the given class. + *

+ * @param clazz the class to register + */ + @SuppressWarnings("unused") + public TeenyJson registerParser(Class clazz, ValueParser parser) { + parsers.put(clazz, parser); + return this; + } + + /** + * Parses an object into a target type. + * @param object the object to parse + * @param type the target type + * @return the parsed object or null if the object is null + * @param the type of the target + */ + @SuppressWarnings("unchecked") + private T parseObject(Object object, Class type) { + if (object == null) return null; + + T instance = ReflectionUtils.newInstance(type); + + Map fields = ReflectionUtils.getFields(type); + Map mutators = ReflectionUtils.getMutators(type); + Map map = (Map) object; + + for (Map.Entry entry : fields.entrySet()) { + Object source = map.get(entry.getKey()); + + if (source == null) continue; + + write(entry.getValue(), mutators.get(entry.getKey()), source, instance); + } + + return instance; + } + + /** + * Writes a value to a field or method of an instance, is something goes wrong logs the error, + * but does not throw an exception. + *

+ * @param field the field to write if the method is missing + * @param method the method to write + * @param value the value to write + * @param instance the instance to write to + */ + private void write(Field field, Method method, Object value, Object instance) { + try { + if (method != null) { + method.setAccessible(true); + + if (method.isAnnotationPresent(JsonDeserialize.class)) { + JsonDeserialize annotation = method.getAnnotation(JsonDeserialize.class); + + if (annotation.as() != Object.class) { + method.invoke(instance, parse(value, annotation.as())); + } else if (annotation.contentAs() != Object.class) { + ParameterizedTypeHelper helper = ReflectionUtils.getParameterType(method.getGenericParameterTypes()[0]); + helper = helper.withFirstType(annotation.contentAs()); + method.invoke(instance, parse(value, helper)); + } + + return; + } + + try { + method.invoke(instance, parse(value, method.getGenericParameterTypes()[0])); + } catch (JsonParsingException e) { + throw new JsonParsingException("Failed to write field: " + field.getName() + " on " + field.getDeclaringClass(), e); + } + return; + } + + // if the method is missing, try to set the field directly + field.setAccessible(true); + field.set(instance, parse(value, field.getType())); + + } catch (Exception ex) { + Logger.getLogger(TeenyJson.class.getName()) + .log(Level.SEVERE, "Failed to set field " + field.getName() + " on " + instance.getClass().getName(), ex); + } + } + + /** + * Parses a value into a target type. + *

+ * @param value the value to parse + * @param target the target type + * @return the parsed value or null if the value is null or the target type is not supported + * @throws IllegalStateException if the target type is not supported + * @throws JsonParsingException if a problem occurs during parsing + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private Object parse(Object value, Type target) throws JsonParsingException { + if (target instanceof ParameterizedType) { + return parseCollectionOrMap(value, ReflectionUtils.getParameterType(target)); + } + + if (target instanceof ParameterizedTypeHelper) { + return parseCollectionOrMap(value, (ParameterizedTypeHelper) target); + } + + if (target instanceof Class) { + Class targetClass = (Class) target; + + if (targetClass.isInterface()) { + throw new JsonParsingException("Unsupported type: " + target.getTypeName() + " " + target.getClass().getName()); + } + + if (targetClass.isPrimitive()) { + return parseSimple(value, targetClass); + } + + if (targetClass.isArray()) { + return parseArray(value, ReflectionUtils.getArrayType(target)); + } + + if (ReflectionUtils.isEnum(targetClass)) { + return Enum.valueOf((Class) target, value.toString()); + } + + if (!targetClass.getName().startsWith("java")) { + return parseObject(value, targetClass); + } + + return parseSimple(value, targetClass); + } + + throw new JsonParsingException("Unsupported type: " + target.getTypeName() + " " + target.getClass().getName()); + } + + private Object parseArray(Object value, Class componentType) throws JsonParsingException { + if (value == null) return null; + + List list = (List) value; + + Object array = java.lang.reflect.Array.newInstance(componentType, list.size()); + + for (int i = 0; i < list.size(); i++) { + java.lang.reflect.Array.set(array, i, parse(list.get(i), componentType)); + } + + return array; + } + + /** + * Parses a value into a collection or map. + * + * @param value the value to parse + * @param helper the parameterized type helper + * @return the parsed value or null if the value is null or the target type is not supported + * @throws JsonParsingException if a problem occurs during parsing + */ + @SuppressWarnings("unchecked") + private Object parseCollectionOrMap(Object value, ParameterizedTypeHelper helper) throws JsonParsingException { + if (helper.getParentType() == null) + throw new JsonParsingException("Type is not a ParameterizedType: " + helper); + + if (helper.isParentTypeOf(List.class)) { + List list = (List) value; + List parsedList; + + if (helper.getParentType() == ArrayList.class || helper.getParentType() == List.class) { + parsedList = new ArrayList<>(); + } else { + parsedList = (List) ReflectionUtils.newInstance(helper.getParentType()); + } + + for (Object o : list) { + parsedList.add(parse(o, helper.getFirstType())); + } + + return parsedList; + } + + if (helper.isParentTypeOf(Set.class)) { + List list = (List) value; + Set parsedSet; + + if (helper.getParentType() == HashSet.class || helper.getParentType() == Set.class) { + parsedSet = new HashSet<>(); + } else { + parsedSet = (Set) ReflectionUtils.newInstance(helper.getParentType()); + } + + for (Object o : list) { + parsedSet.add(parse(o, helper.getFirstType())); + } + + return parsedSet; + } + + if (helper.isParentTypeOf(Map.class)) { + Map map = (Map) value; + //json only supports string keys + Map result = new HashMap<>(); + + for (Map.Entry entry : map.entrySet()) { + result.put(entry.getKey().toString(), parse(entry.getValue(), helper.getSecondType())); + } + + return result; + } + + throw new JsonParsingException("Unsupported parameterized type: " + helper.getParentType()); + } + + + /** + * Parses a value into a primitive target type such as int, long, double, float, or boolean. + *

+ * @param obj the value to parse + * @param target the target type + * @return the parsed value or the default value for the target type if the value is null + * @throws NumberFormatException if the value cannot be parsed into a number if the target is a number + * @throws IllegalStateException if the target type is not supported + */ + private Object parsePrimitive(Object obj, Class target) { + String value = obj == null ? null : obj.toString().trim(); + + if (target == int.class) { + if (value == null) return 0; + + return Integer.parseInt(value); + } + + if (target == long.class) { + if (value == null) return 0L; + + return Long.parseLong(value); + } + + if (target == double.class) { + if (value == null) return 0.0; + + return Double.parseDouble(value); + } + + if (target == float.class) { + if (value == null) return 0.0f; + + return Float.parseFloat(value); + } + + if (target == boolean.class) { + + if (value == null) return false; + + return Boolean.parseBoolean(value); + } + + throw new IllegalStateException("Unsupported primitive type: " + target.getName()); + } + + /** + * Parses a simple value into a target type. + *

+ * @param value the value to parse + * @param target the target type + * @throws RuntimeException if a problem occurs during parsing + * @return the parsed value or null if the value is null or the target type is not supported + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private Object parseSimple(Object value, Class target) { + if (value == null) { + return null; + } + + if (target == String.class) { + return value.toString(); + } + + if (target.isPrimitive()) { + return parsePrimitive(value, target); + } + + if (target == Integer.class) { + return Integer.parseInt(value.toString()); + } + + if (target == BigDecimal.class) { + return new BigDecimal(value.toString()); + } + + if (target == BigInteger.class) { + return new BigInteger(value.toString()); + } + + if (target == Long.class) { + return Long.parseLong(value.toString()); + } + + if (target == Double.class) { + return Double.parseDouble(value.toString()); + } + + if (target == Float.class) { + return Float.parseFloat(value.toString()); + } + + if (target == Boolean.class) { + return Boolean.parseBoolean(value.toString()); + } + + if (target == LocalDateTime.class) { + return LocalDateTime.parse(value.toString()); + } + + if (target == LocalDate.class) { + return LocalDate.parse(value.toString()); + } + + if (target == LocalTime.class) { + return LocalTime.parse(value.toString()); + } + + if (target == Date.class) { + try { + return encoder.getDateFormatter() + .parse(value.toString()); + } catch (ParseException e) { + return null; + } + } + + if (target.isEnum()) { + return Enum.valueOf((Class) target, value.toString()); + } + + ValueParser parser = parsers.get(target); + + if (parser != null) { + return parser.parse(value); + } + + Logger.getLogger(TeenyJson.class.getName()) + .log(Level.WARNING, "not implemented: " + target.getName()); + + return null; + } + +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/TeenyJsonMessageConverter.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/TeenyJsonMessageConverter.java new file mode 100644 index 0000000..5173206 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/TeenyJsonMessageConverter.java @@ -0,0 +1,31 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import net.jonathangiles.tools.teenyhttpd.model.MessageConverter; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.lang.reflect.Type; + +public final class TeenyJsonMessageConverter implements MessageConverter { + + public final TeenyJson teenyJson = new TeenyJson(); + + @Override + public String getContentType() { + return "application/json"; + } + + @Override + public void write(Object value, BufferedOutputStream dataOut) throws IOException { + teenyJson.writeValue(dataOut, value); + } + + @Override + public Object read(String value, Type type) { + try { + return teenyJson.readValue(value, type); + } catch (JsonParsingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/ValueParser.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/ValueParser.java new file mode 100644 index 0000000..3b19b34 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/ValueParser.java @@ -0,0 +1,10 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +/** + * A simple interface for parsing a value into T, where the value could be a Map, List, Null or String. + */ +public interface ValueParser { + + T parse(Object value); + +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/json/ValueSerializer.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/ValueSerializer.java new file mode 100644 index 0000000..19f3c45 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/json/ValueSerializer.java @@ -0,0 +1,9 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +/** + * A simple interface for serializing a value into a String. + */ +public interface ValueSerializer { + String serialize(T value); + +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/Pet.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/Pet.java index 40e5b7d..3ca3889 100644 --- a/src/test/java/net/jonathangiles/tools/teenyhttpd/Pet.java +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/Pet.java @@ -4,9 +4,9 @@ public class Pet { - private String name; - private int age; - private String type; + public String name; + public int age; + public String type; public Pet(String name, int age, String type) { this.name = name; diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/ProductDto.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/ProductDto.java index 8f30e3f..e122d20 100644 --- a/src/test/java/net/jonathangiles/tools/teenyhttpd/ProductDto.java +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/ProductDto.java @@ -1,10 +1,18 @@ package net.jonathangiles.tools.teenyhttpd; + +import net.jonathangiles.tools.teenyhttpd.json.JsonIncludeNonNull; + +import java.util.Map; + +@JsonIncludeNonNull public class ProductDto { - private int id; - private String name; - private int price; + public int id; + public String name; + public int price; + public Map perks; + public ProductDto() { } @@ -15,6 +23,22 @@ public ProductDto(int id, String name, int price) { this.price = price; } + + public Map getPerks() { + return perks; + } + + public ProductDto put(String key, Object value) { + + if (perks == null) { + perks = new java.util.HashMap<>(); + } + + perks.put(key, value); + + return this; + } + public int getId() { return id; } @@ -41,4 +65,14 @@ public ProductDto setPrice(int price) { this.price = price; return this; } + + @Override + public String toString() { + return "ProductDto{" + + "id=" + id + + ", name='" + name + '\'' + + ", price=" + price + + ", perks=" + perks + + '}'; + } } \ No newline at end of file diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyApplicationTest.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyApplicationTest.java index aa1fb21..d1b2aa3 100644 --- a/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyApplicationTest.java +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyApplicationTest.java @@ -27,7 +27,6 @@ public class TeenyApplicationTest { private static final int TEST_PORT = 8080; - private static class ProtocolBufferMessageConverter implements MessageConverter { @Override @@ -52,12 +51,16 @@ public void setup() { System.setProperty("banner", "false"); TeenyApplication.start() - .registerMessageConverter(new GsonMessageConverter()) .registerMessageConverter(new ProtocolBufferMessageConverter()) .register(new StoreController()); } + public static void main(String[] args) { + TeenyApplication.start() + .register(new StoreController()); + } + @AfterEach public void tearDown() { TeenyApplication.stop(); diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyJsonConverter.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyJsonConverter.java new file mode 100644 index 0000000..f36633d --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyJsonConverter.java @@ -0,0 +1,39 @@ +package net.jonathangiles.tools.teenyhttpd; + +import net.jonathangiles.tools.teenyhttpd.json.TeenyJson; +import net.jonathangiles.tools.teenyhttpd.model.MessageConverter; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.lang.reflect.Type; + +public class TeenyJsonConverter implements MessageConverter { + + final TeenyJson teenyJson = new TeenyJson(); + + @Override + public String getContentType() { + return "application/json"; + } + + @Override + public void write(Object value, BufferedOutputStream dataOut) throws IOException { + + + if (value instanceof String) { + dataOut.write(((String) value).getBytes()); + return; + } + + dataOut.write(teenyJson.writeValueAsString(value).getBytes()); + } + + @Override + public Object read(String value, Type type) { + try { + return teenyJson.readValue(value, type); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/Automakers.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/Automakers.java new file mode 100644 index 0000000..f16988b --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/Automakers.java @@ -0,0 +1,6 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +public enum Automakers { + FORD, TOYOTA, HONDA, BMW, MERCEDES, AUDI, VOLKSWAGEN, TESLA, NISSAN, PORSCHE, JAGUAR, LAND_ROVER, FERRARI, LAMBORGHINI, + MASERATI, ALFA_ROMEO, FIAT, CHRYSLER, DODGE, JEEP, RAM, CHEVROLET, GMC, BUICK, CADILLAC, LINCOLN; +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/C.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/C.java new file mode 100644 index 0000000..010c3cd --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/C.java @@ -0,0 +1,9 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +public interface C { + + String getName(); + + int getAge(); + +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ComplexObject.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ComplexObject.java new file mode 100644 index 0000000..342f43d --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ComplexObject.java @@ -0,0 +1,309 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.*; + +public class ComplexObject { + + private String string; + private boolean aBoolean; + private Boolean aBoolean2; + private int anInt; + private Integer anInteger; + private long aLong; + private Long aLong2; + private float aFloat; + private Float aFloat2; + private double aDouble; + private Double aDouble2; + private BigDecimal bigDecimal; + private BigInteger bigInteger; + private LocalDate localDate; + private LocalDateTime localDateTime; + private LocalTime localTime; + private Automakers automaker; + private List list; + private Set set; + private Map map; + private ArrayList arrayList; + private Date date; + private String[] array; + private ObjectC[] arrayOfObjectC; + private LinkedList linkedList; + private LinkedHashSet linkedHashSet; + + public ComplexObject() { + } + + public ComplexObject setLinkedList(LinkedList linkedList) { + this.linkedList = linkedList; + return this; + } + + public ComplexObject setLinkedHashSet(LinkedHashSet linkedHashSet) { + this.linkedHashSet = linkedHashSet; + return this; + } + + public LinkedList getLinkedList() { + return linkedList; + } + + public LinkedHashSet getLinkedHashSet() { + return linkedHashSet; + } + + public String getString() { + return string; + } + + public ComplexObject setString(String string) { + this.string = string; + return this; + } + + public boolean isaBoolean() { + return aBoolean; + } + + public ComplexObject setaBoolean(boolean aBoolean) { + this.aBoolean = aBoolean; + return this; + } + + public Boolean getaBoolean2() { + return aBoolean2; + } + + public ComplexObject setaBoolean2(Boolean aBoolean2) { + this.aBoolean2 = aBoolean2; + return this; + } + + public int getAnInt() { + return anInt; + } + + public ComplexObject setAnInt(int anInt) { + this.anInt = anInt; + return this; + } + + public Integer getAnInteger() { + return anInteger; + } + + public ComplexObject setAnInteger(Integer anInteger) { + this.anInteger = anInteger; + return this; + } + + public long getaLong() { + return aLong; + } + + public ComplexObject setaLong(long aLong) { + this.aLong = aLong; + return this; + } + + public Long getaLong2() { + return aLong2; + } + + public ComplexObject setaLong2(Long aLong2) { + this.aLong2 = aLong2; + return this; + } + + public float getaFloat() { + return aFloat; + } + + public ComplexObject setaFloat(float aFloat) { + this.aFloat = aFloat; + return this; + } + + public Float getaFloat2() { + return aFloat2; + } + + public ComplexObject setaFloat2(Float aFloat2) { + this.aFloat2 = aFloat2; + return this; + } + + public double getaDouble() { + return aDouble; + } + + public ComplexObject setaDouble(double aDouble) { + this.aDouble = aDouble; + return this; + } + + public Double getaDouble2() { + return aDouble2; + } + + public ComplexObject setaDouble2(Double aDouble2) { + this.aDouble2 = aDouble2; + return this; + } + + public BigDecimal getBigDecimal() { + return bigDecimal; + } + + public ComplexObject setBigDecimal(BigDecimal bigDecimal) { + this.bigDecimal = bigDecimal; + return this; + } + + public BigInteger getBigInteger() { + return bigInteger; + } + + public ComplexObject setBigInteger(BigInteger bigInteger) { + this.bigInteger = bigInteger; + return this; + } + + public LocalDate getLocalDate() { + return localDate; + } + + public ComplexObject setLocalDate(LocalDate localDate) { + this.localDate = localDate; + return this; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public ComplexObject setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + return this; + } + + public LocalTime getLocalTime() { + return localTime; + } + + public ComplexObject setLocalTime(LocalTime localTime) { + this.localTime = localTime; + return this; + } + + public Automakers getAutomaker() { + return automaker; + } + + public ComplexObject setAutomaker(Automakers automaker) { + this.automaker = automaker; + return this; + } + + public List getList() { + return list; + } + + public ComplexObject setList(List list) { + this.list = list; + return this; + } + + public Set getSet() { + return set; + } + + public ComplexObject setSet(Set set) { + this.set = set; + return this; + } + + public Map getMap() { + return map; + } + + public ComplexObject setMap(Map map) { + this.map = map; + return this; + } + + public ArrayList getArrayList() { + return arrayList; + } + + public ComplexObject setArrayList(ArrayList arrayList) { + this.arrayList = arrayList; + return this; + } + + public Date getDate() { + return date; + } + + public ComplexObject setDate(Date date) { + this.date = date; + return this; + } + + public String[] getArray() { + return array; + } + + public ComplexObject setArray(String[] array) { + this.array = array; + return this; + } + + public ObjectC[] getArrayOfObjectC() { + return arrayOfObjectC; + } + + public ComplexObject setArrayOfObjectC(ObjectC[] arrayOfObjectC) { + this.arrayOfObjectC = arrayOfObjectC; + return this; + } + + + + @Override + public String toString() { + return "ComplexObject{" + + "string='" + string + '\'' + + ", aBoolean=" + aBoolean + + ", aBoolean2=" + aBoolean2 + + ", anInt=" + anInt + + ", anInteger=" + anInteger + + ", aLong=" + aLong + + ", aLong2=" + aLong2 + + ", aFloat=" + aFloat + + ", aFloat2=" + aFloat2 + + ", aDouble=" + aDouble + + ", aDouble2=" + aDouble2 + + ", bigDecimal=" + bigDecimal + + ", bigInteger=" + bigInteger + + ", localDate=" + localDate + + ", localDateTime=" + localDateTime + + ", localTime=" + localTime + + ", automaker=" + automaker + + ", list=" + list + + ", set=" + set + + ", map=" + map + + ", arrayList=" + arrayList + + ", date=" + date + + ", array=" + Arrays.toString(array) + + ", arrayOfObjectC=" + Arrays.toString(arrayOfObjectC) + + ", linkedList=" + linkedList + + ", linkedHashSet=" + linkedHashSet + + '}'; + } +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonBenchmarks.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonBenchmarks.java new file mode 100644 index 0000000..45de1df --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonBenchmarks.java @@ -0,0 +1,69 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Mode; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.*; + +public class JsonBenchmarks { + + static final ComplexObject complexObject = new ComplexObject() + .setString("string :{}") + .setAnInt(1) + .setAnInteger(2) + .setaLong(2) + .setaLong2(2L) + .setaFloat(1.0f) + .setaFloat2(1.0f) + .setaDouble(1.0) + .setaDouble2(1.0) + .setaBoolean(true) + .setaBoolean2(true) + .setBigDecimal(BigDecimal.TEN) + .setBigInteger(BigDecimal.TEN.toBigInteger()) + .setLocalDate(LocalDate.now()) + .setLocalDateTime(LocalDate.now().atStartOfDay()) + .setLocalTime(LocalTime.now()) + .setArray(new String[]{"A", "B", "C"}) + .setLinkedList(new LinkedList<>(List.of("A", "B", "C"))) + .setLinkedHashSet(new LinkedHashSet<>(List.of("A", "B", "C"))) + .setArrayOfObjectC(new ObjectC[]{new ObjectC("John Doe", 23), + new ObjectC("Jane Doe", 25), + new ObjectC("John Smith", 30)}) + .setArrayList(new ArrayList<>(List.of(1, 2, 3, 4, 5))) + .setDate(new java.util.Date()) + .setAutomaker(Automakers.TOYOTA) + .setList(List.of("A", "B", "C")) + .setSet(Set.of("A", "B", "C")) + .setMap(Map.of("A", new ObjectC("John Doe", 23), + "B", new ObjectC("Jane Doe", 25), + "C", new ObjectC("John Smith", 30))); + + static final TeenyJson teenyJson = new TeenyJson(); + + static final String json = "{\"localDateTime\": \"2024-03-10T00:00\", \"linkedHashSet\": [\"A\", \"B\", \"C\"], \"date\": \"10-03-2024 02:19:07\", \"aFloat2\": 1.0, \"string\": \"string :{}\", \"aLong\": 2, \"anInt\": 1, \"arrayOfObjectC\": [{\"name\": \"John Doe\", \"age\": 23}, {\"name\": \"Jane Doe\", \"age\": 25}, {\"name\": \"John Smith\", \"age\": 30}], \"aDouble\": 1.0, \"anInteger\": 2, \"aBoolean2\": true, \"array\": [\"A\", \"B\", \"C\"], \"arrayList\": [1, 2, 3, 4, 5], \"localDate\": \"2024-03-10\", \"bigDecimal\": 10, \"map\": {\"A\": {\"name\": \"John Doe\", \"age\": 23}, \"B\": {\"name\": \"Jane Doe\", \"age\": 25}, \"C\": {\"name\": \"John Smith\", \"age\": 30}}, \"linkedList\": [\"A\", \"B\", \"C\"], \"aBoolean\": true, \"aDouble2\": 1.0, \"set\": [\"A\", \"B\", \"C\"], \"aFloat\": 1.0, \"bigInteger\": 10, \"list\": [\"A\", \"B\", \"C\"], \"localTime\": \"02:19:07.036590\", \"automaker\": \"TOYOTA\", \"aLong2\": 2}"; + + public static void main(String[] args) throws Exception { + org.openjdk.jmh.Main.main(args); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Fork(value = 1, warmups = 2) + public void encodingBenchmark() { + teenyJson.writeValueAsString(complexObject); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Fork(value = 1, warmups = 2) + public void decodingBenchmark() { + teenyJson.readValue(json, ComplexObject.class); + } + +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonDecoderTest.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonDecoderTest.java new file mode 100644 index 0000000..7f5f058 --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonDecoderTest.java @@ -0,0 +1,567 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import com.google.gson.Gson; +import net.jonathangiles.tools.teenyhttpd.Pet; +import net.jonathangiles.tools.teenyhttpd.ProductDto; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.*; + +public class JsonDecoderTest { + + @Test + void testDecodeFromGson() { + String json = new Gson() + .toJson(new ProductDto(1, "Product 1", 100)); + + Object value = new JsonDecoder(json) + .read(); + + Assertions.assertInstanceOf(Map.class, value); + + Map map = (Map) value; + + Assertions.assertEquals(3, map.size()); + + System.out.println(value); + } + + @Test + void testEncodeAndDecode() { + String json = new TeenyJson() + .writeValueAsString(new ProductDto(1, "Product 1", 100)); + + System.out.println(json); + + Object value = new JsonDecoder(json) + .read(); + + System.out.println(value); + + Assertions.assertInstanceOf(Map.class, value); + + Map map = (Map) value; + + Assertions.assertEquals(3, map.size()); + + } + + /** + * Test that a simple object can be parsed + */ + @Test + void testParseProduct() { + String json = new Gson() + .toJson(new ProductDto(1, "Product 1", 100)); + + ProductDto product = new TeenyJson() + .readValue(json, ProductDto.class); + + System.out.println(product); + + Assertions.assertEquals(1, product.getId()); + Assertions.assertEquals("Product 1", product.getName()); + Assertions.assertEquals(100, product.getPrice()); + } + + /** + * Test that a list of objects can be parsed + */ + @Test + void testList() { + String json = new Gson() + .toJson(List.of(new ProductDto(1, "Product 1", 100), + new ProductDto(1, "Product 1", 100), + new ProductDto(1, "Product 1", 100))); + + Object value = new JsonDecoder(json) + .read(); + + Assertions.assertInstanceOf(List.class, value); + + List list = (List) value; + + Assertions.assertEquals(3, list.size()); + + System.out.println(value); + + for (Object o : list) { + Assertions.assertInstanceOf(Map.class, o); + + Map map = (Map) o; + + Assertions.assertEquals(3, map.size()); + } + } + + @Test + void testArrayTeenyJson() { + String json = new TeenyJson() + .writeValueAsString(List.of(new ProductDto(1, "Product 1", 100), + new ProductDto(1, "Product 1", 100), + new ProductDto(1, "Product 1", 100))); + + System.out.println(json); + + Object value = new JsonDecoder(json) + .read(); + + System.out.println(value); + + Assertions.assertInstanceOf(List.class, value); + + List list = (List) value; + + Assertions.assertEquals(3, list.size()); + + System.out.println(value); + + for (Object o : list) { + Assertions.assertInstanceOf(Map.class, o); + + Map map = (Map) o; + + Assertions.assertEquals(3, map.size()); + } + } + + @Test + void testParseArray() { + String json = new Gson() + .toJson(List.of(new ProductDto(1, "Product 1", 100), + new ProductDto(1, "Product 1", 100), + new ProductDto(1, "Product 1", 100))); + + List products = new TeenyJson() + .readCollection(json, List.class, ProductDto.class); + + System.out.println(products); + + Assertions.assertEquals(3, products.size()); + + for (ProductDto product : products) { + Assertions.assertEquals(1, product.getId()); + Assertions.assertEquals("Product 1", product.getName()); + Assertions.assertEquals(100, product.getPrice()); + } + } + + @Test + void parseArrayTeenyJson() { + String json = new TeenyJson() + .writeValueAsString(List.of(new ProductDto(1, "Product 1", 100), + new ProductDto(1, "Product 1", 100), + new ProductDto(1, "Product 1", 100))); + + List products = new TeenyJson() + .readCollection(json, List.class, ProductDto.class); + + System.out.println(products); + + Assertions.assertEquals(3, products.size()); + + for (ProductDto product : products) { + Assertions.assertEquals(1, product.getId()); + Assertions.assertEquals("Product 1", product.getName()); + Assertions.assertEquals(100, product.getPrice()); + } + } + + @Test + void testArray2() { + String json = new Gson() + .toJson(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + + Object value = new JsonDecoder(json) + .read(); + + Assertions.assertInstanceOf(List.class, value); + + List list = (List) value; + + Assertions.assertEquals(10, list.size()); + + Assertions.assertEquals("1", list.get(0)); + + json = new Gson() + .toJson(List.of("A", "B", "C", "D")); + + value = new JsonDecoder(json) + .read(); + + Assertions.assertInstanceOf(List.class, value); + + list = (List) value; + + Assertions.assertEquals(4, list.size()); + + Assertions.assertEquals("A", list.get(0)); + + System.out.println(value); + } + + @Test + void testArray2TeenyJson() { + String json = new TeenyJson() + .writeValueAsString(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + + Object value = new JsonDecoder(json) + .read(); + + Assertions.assertInstanceOf(List.class, value); + + List list = (List) value; + + Assertions.assertEquals(10, list.size()); + + Assertions.assertEquals("1", list.get(0)); + + json = new TeenyJson() + .writeValueAsString(List.of("A", "B", "C", "D")); + + + value = new JsonDecoder(json) + .read(); + + Assertions.assertInstanceOf(List.class, value); + + list = (List) value; + + Assertions.assertEquals(4, list.size()); + + Assertions.assertEquals("A", list.get(0)); + + System.out.println(value); + } + + @Test + void testDeepObject() { + String json = new Gson() + .toJson(new ObjectA("Alex", 25, + true, List.of("Code", "Travel"), + new ObjectB("Jonathan", 30, + new ObjectC("John Doe", 23)))); + + Object value = new JsonDecoder(json) + .read(); + + System.out.println(value); + + Assertions.assertInstanceOf(Map.class, value); + + Map map = (Map) value; + + Assertions.assertEquals(5, map.size()); + + Assertions.assertInstanceOf(Map.class, map.get("objectB")); + + Map objectB = (Map) map.get("objectB"); + + Assertions.assertEquals(3, objectB.size()); + + Assertions.assertInstanceOf(Map.class, objectB.get("objectC")); + + Map objectC = (Map) objectB.get("objectC"); + + Assertions.assertEquals(2, objectC.size()); + + } + + @Test + void testDeepObjectTeenyJson() { + String json = new TeenyJson() + .writeValueAsString(new ObjectA("Alex", 25, + true, List.of("Code", "Travel"), + new ObjectB("Jonathan", 30, + new ObjectC("John Doe", 23)))); + + Object value = new JsonDecoder(json) + .read(); + + System.out.println(value); + + Assertions.assertInstanceOf(Map.class, value); + + Map map = (Map) value; + + Assertions.assertEquals(5, map.size()); + + Assertions.assertInstanceOf(Map.class, map.get("objectB")); + + Map objectB = (Map) map.get("objectB"); + + Assertions.assertEquals(3, objectB.size()); + + Assertions.assertInstanceOf(Map.class, objectB.get("objectC")); + + Map objectC = (Map) objectB.get("objectC"); + + Assertions.assertEquals(2, objectC.size()); + + } + + @Test + void parseDeepObject() { + String json = new Gson() + .toJson(new ObjectA("Alex", 25, + true, null, + new ObjectB("Jonathan", 30, + new ObjectC("John Doe", 23)))); + + System.out.println(json); + + ObjectA objectA = new TeenyJson() + .readValue(json, ObjectA.class); + + System.out.println(objectA); + + Assertions.assertEquals("Alex", objectA.getName()); + Assertions.assertEquals(25, objectA.getAge()); + Assertions.assertTrue(objectA.isDeveloper()); + + ObjectB objectB = objectA.getObjectB(); + + Assertions.assertEquals("Jonathan", objectB.getName()); + Assertions.assertEquals(30, objectB.getAge()); + + ObjectC objectC = objectB.getObjectC(); + + Assertions.assertEquals("John Doe", objectC.getName()); + Assertions.assertEquals(23, objectC.getAge()); + + } + + @Test + void parseDeepObjectTeenyJson() { + String json = new TeenyJson() + .writeValueAsString(new ObjectA("Alex", 25, + true, null, + new ObjectB("Jonathan", 30, + new ObjectC("John Doe", 23)))); + + String json2 = new Gson() + .toJson(new ObjectA("Alex", 25, + true, null, + new ObjectB("Jonathan", 30, + new ObjectC("John Doe", 23)))); + + System.out.println(json); + System.out.println(json2); + + Assertions.assertEquals(json.length(), json2.length()); + + + ObjectA objectA = new TeenyJson() + .readValue(json, ObjectA.class); + + System.out.println(objectA); + + Assertions.assertEquals("Alex", objectA.getName()); + Assertions.assertEquals(25, objectA.getAge()); + Assertions.assertTrue(objectA.isDeveloper()); + + ObjectB objectB = objectA.getObjectB(); + + Assertions.assertEquals("Jonathan", objectB.getName()); + Assertions.assertEquals(30, objectB.getAge()); + + ObjectC objectC = objectB.getObjectC(); + + Assertions.assertEquals("John Doe", objectC.getName()); + Assertions.assertEquals(23, objectC.getAge()); + + } + + @Test + void testParseFormattedJson() { + String json = "{\n" + + " \"objectB\": {\n" + + " \"objectC\": {\n" + + " \"name\": \"John Doe\",\n" + + " \"age\": 23\n" + + " },\n" + + " \"name\": \"Jonathan\",\n" + + " \"age\": 30\n" + + " },\n" + + " \"name\": \"Alex\",\n" + + " \"developer\": true,\n" + + " \"age\": 25\n" + + "}"; + + System.out.println("Json: " + json); + + ObjectA objectA = new TeenyJson() + .readValue(json, ObjectA.class); + + System.out.println(objectA); + + Assertions.assertEquals("Alex", objectA.getName()); + Assertions.assertEquals(25, objectA.getAge()); + Assertions.assertTrue(objectA.isDeveloper()); + + ObjectB objectB = objectA.getObjectB(); + + Assertions.assertEquals("Jonathan", objectB.getName()); + Assertions.assertEquals(30, objectB.getAge()); + + ObjectC objectC = objectB.getObjectC(); + + Assertions.assertEquals("John Doe", objectC.getName()); + Assertions.assertEquals(23, objectC.getAge()); + + } + + @Test + void testJsonDeserializeAnnot() { + String json = new TeenyJson() + .writeValueAsString(new ObjectD() + .setName("Alex") + .setList(List.of(new ObjectC("John Doe", 23), + new ObjectC("Jane Doe", 25), + new ObjectC("John Smith", 30))) + .setC(new ObjectC("John Doe", 23))); + + + ObjectD objectD = new TeenyJson() + .readValue(json, ObjectD.class); + + System.out.println(objectD); + + Assertions.assertEquals("Alex", objectD.getName()); + + C c = objectD.getC(); + + Assertions.assertEquals("John Doe", c.getName()); + Assertions.assertEquals(23, c.getAge()); + Assertions.assertInstanceOf(ObjectC.class, c); + + List list = objectD.getList(); + + Assertions.assertEquals(3, list.size()); + + for (C c1 : list) { + Assertions.assertInstanceOf(ObjectC.class, c1); + } + } + + @Test + void testAlias() { + Person person = new Person() + .setName("Alex") + .setAge(25) + .setLikesIceCream(true) + .setToolList(List.of(new Tool("Mac", "Laptop"), + new Tool("IntelliJ", "IDE"), + new Tool("Coffee", "Fuel") + )) + .setBirthDate(LocalDate.of(1995, 1, 1)) + .setFavoriteSongs(Set.of("Radio GaGa", "Whatever it takes", "Feel Good Inc")) + .setPet(new Pet() + .setName("Rocky") + .setType("Dog") + .setAge(1)); + + String json = new TeenyJson() + .writeValueAsString(person); + + System.out.println(json); + + Person newPerson = new TeenyJson() + .readValue(json, Person.class); + + if (!person.equals(newPerson)) { + System.out.println(person); + System.out.println(newPerson); + + Assertions.fail("Objects are not equal"); + } + } + + /** + * {@link ComplexObject} is a complex object with various types of fields + * this tests verifies that TeenyJson can parse and serialize this object + */ + @Test + void testComplexObject() { + ComplexObject complexObject = new ComplexObject() + .setString("string :{}") + .setAnInt(1) + .setAnInteger(2) + .setaLong(2) + .setaLong2(2L) + .setaFloat(1.0f) + .setaFloat2(1.0f) + .setaDouble(1.0) + .setaDouble2(1.0) + .setaBoolean(true) + .setaBoolean2(true) + .setBigDecimal(BigDecimal.TEN) + .setBigInteger(BigDecimal.TEN.toBigInteger()) + .setLocalDate(LocalDate.now()) + .setLocalDateTime(LocalDate.now().atStartOfDay()) + .setLocalTime(LocalTime.now()) + .setArray(new String[]{"A", "B", "C"}) + .setLinkedList(new LinkedList<>(List.of("A", "B", "C"))) + .setLinkedHashSet(new LinkedHashSet<>(List.of("A", "B", "C"))) + .setArrayOfObjectC(new ObjectC[]{new ObjectC("John Doe", 23), + new ObjectC("Jane Doe", 25), + new ObjectC("John Smith", 30)}) + .setArrayList(new ArrayList<>(List.of(1, 2, 3, 4, 5))) + .setDate(new java.util.Date()) + .setAutomaker(Automakers.TOYOTA) + .setList(List.of("A", "B", "C")) + .setSet(Set.of("A", "B", "C")) + .setMap(Map.of("A", new ObjectC("John Doe", 23), + "B", new ObjectC("Jane Doe", 25), + "C", new ObjectC("John Smith", 30))); + + String json = new TeenyJson() + .writeValueAsString(complexObject); + + System.out.println("JSON: " + json); + + ComplexObject object = new TeenyJson() + .readValue(json, ComplexObject.class); + + System.out.println(object); + + if (complexObject.isaBoolean() != object.isaBoolean()) Assertions.fail("aBoolean"); + if (complexObject.getAnInt() != object.getAnInt()) Assertions.fail("anInt"); + if (complexObject.getaLong() != object.getaLong()) Assertions.fail("aLong"); + if (Float.compare(complexObject.getaFloat(), object.getaFloat()) != 0) Assertions.fail("aFloat"); + if (Double.compare(complexObject.getaDouble(), object.getaDouble()) != 0) Assertions.fail("aDouble"); + if (!complexObject.getString().equals(object.getString())) Assertions.fail("String"); + if (!complexObject.getaBoolean2().equals(object.getaBoolean2())) Assertions.fail("aBoolean2"); + if (!complexObject.getAnInteger().equals(object.getAnInteger())) Assertions.fail("AnInteger"); + if (!complexObject.getaLong2().equals(object.getaLong2())) Assertions.fail("aLong2"); + if (!complexObject.getaFloat2().equals(object.getaFloat2())) Assertions.fail("aFloat2"); + if (!complexObject.getaDouble2().equals(object.getaDouble2())) Assertions.fail("aDouble2"); + if (!complexObject.getBigDecimal().equals(object.getBigDecimal())) Assertions.fail("BigDecimal"); + if (!complexObject.getBigInteger().equals(object.getBigInteger())) Assertions.fail("BigInteger"); + if (!complexObject.getLocalDate().equals(object.getLocalDate())) Assertions.fail("LocalDate"); + if (!complexObject.getLocalDateTime().equals(object.getLocalDateTime())) Assertions.fail("LocalDateTime"); + if (!complexObject.getLocalTime().equals(object.getLocalTime())) Assertions.fail("LocalTime"); + if (complexObject.getAutomaker() != object.getAutomaker()) Assertions.fail("Automaker"); + if (!complexObject.getList().equals(object.getList())) Assertions.fail("List"); + if (!complexObject.getSet().equals(object.getSet())) Assertions.fail("Set"); + if (!complexObject.getMap().equals(object.getMap())) Assertions.fail("Map"); + if (!complexObject.getArrayList().equals(object.getArrayList())) Assertions.fail("ArrayList"); + if (!complexObject.getDate().toString().equals(object.getDate().toString())) + Assertions.fail("Date was " + object.getDate() + " expected " + complexObject.getDate()); + // Probably incorrect - comparing Object[] arrays with Arrays.equals + if (!Arrays.equals(complexObject.getArray(), object.getArray())) Assertions.fail("Array"); + // Probably incorrect - comparing Object[] arrays with Arrays.equals + if (!Arrays.equals(complexObject.getArrayOfObjectC(), object.getArrayOfObjectC())) + Assertions.fail("ArrayOfObjectC"); + if (!complexObject.getLinkedList().equals(object.getLinkedList())) Assertions.fail("LinkedList"); + if (!complexObject.getLinkedHashSet().equals(object.getLinkedHashSet())) Assertions.fail("LinkedHashSet"); + + + } + + +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonEncoderTest.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonEncoderTest.java new file mode 100644 index 0000000..a32db3a --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonEncoderTest.java @@ -0,0 +1,247 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import net.jonathangiles.tools.teenyhttpd.Pet; +import net.jonathangiles.tools.teenyhttpd.ProductDto; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class JsonEncoderTest { + + @Test + void testSerialization() { + Person person = new Person() + .setName("Alex") + .setAge(25) + .setLikesIceCream(true) + .setToolList(List.of(new Tool("Mac", "Laptop"), + new Tool("IntelliJ", "IDE"), + new Tool("Coffee", "Fuel") + )) + .setBirthDate(LocalDate.of(1995, 1, 1)) + .setFavoriteSongs(Set.of("Radio GaGa", "Whatever it takes", "Feel Good Inc")) + .setPet(new Pet() + .setName("Rocky") + .setType("Dog") + .setAge(1)); + + TeenyJson teenyJson = new TeenyJson(); + + long init = System.nanoTime(); + + String json = teenyJson.writeValueAsString(person); + + System.out.println(json + " " + (System.nanoTime() - init) + " ns"); + + init = System.nanoTime(); + + json = teenyJson.writeValueAsString(person); + + System.out.println(json + " " + (System.nanoTime() - init) + " ns"); + + + Gson gson = new GsonBuilder() + .registerTypeAdapter( + LocalDateTime.class, + (JsonDeserializer) (j, type, jsonDeserializationContext) -> + ZonedDateTime.parse(j.getAsJsonPrimitive().getAsString()).toLocalDateTime() + ) + .registerTypeAdapter( + LocalDate.class, + (JsonDeserializer) (localDate, type, jsonSerializationContext) -> + LocalDate.parse(localDate.getAsJsonPrimitive().getAsString())) + .create(); + + + Person newPerson = gson.fromJson(json, Person.class); + + + if (!person.equals(newPerson)) { + + System.out.println(person); + System.out.println(newPerson); + + Assertions.fail("Objects are not equal"); + } + + init = System.nanoTime(); + + teenyJson.registerSerializer(String.class, (value) -> "\"hi\""); + + json = teenyJson.writeValueAsString(person); + + System.out.println(json + " " + (System.nanoTime() - init) + " ns"); + + newPerson = gson.fromJson(json, Person.class); + + Assertions.assertEquals("hi", newPerson.getName()); + + } + + @Test + void parseSimple() { + + Pet pet = new Pet("Rocky", 1, "Dog"); + String json = new TeenyJson().writeValueAsString(pet); + + Pet parsedPet = new TeenyJson().readValue(json, Pet.class); + + Assertions.assertEquals(pet, parsedPet); + } + + @Test + void testSerializeProduct() { + String json = new TeenyJson().writeValueAsString(List.of( + new ProductDto(1, "MacBook", 2000), + new ProductDto(2, "iPhone", 1000) + .put("colors", Set.of("black", "blue", "pink")) + .put("warranty", Map.of("years", 2, "type", "full")) + .put("specs", List.of("A14", "5G", "FaceID")) + .put("chargerIncluded", true) + )); + + System.out.println(json); + } + + @Test + void testEncodeInteger() { + String json = new TeenyJson() + .writeValueAsString(Map.of("value", 1)); + + Assertions.assertEquals("{\"value\":1}", json); + } + + @Test + void testEncodeString() { + String json = new TeenyJson() + .writeValueAsString(Map.of("value", "hello")); + + Assertions.assertEquals("{\"value\":\"hello\"}", json); + } + + @Test + void testEncodeBoolean() { + String json = new TeenyJson() + .writeValueAsString(Map.of("value", true)); + + Assertions.assertEquals("{\"value\":true}", json); + + json = new TeenyJson() + .writeValueAsString(Map.of("value", false)); + + Assertions.assertEquals("{\"value\":false}", json); + } + + @Test + void testEncodeNull() { + Map map = new HashMap<>(); + + map.put("value", null); + + String json = new TeenyJson() + .writeValueAsString(map); + + Assertions.assertEquals("{\"value\":null}", json); + } + + @Test + void testEncodeArray() { + String json = new TeenyJson() + .writeValueAsString(Map.of("value", new int[]{1, 2, 3})); + + Assertions.assertEquals("{\"value\":[1,2,3]}", json); + } + + @Test + void testEncodeList() { + String json = new TeenyJson() + .writeValueAsString(Map.of("value", List.of(1, 2, 3))); + + Assertions.assertEquals("{\"value\":[1,2,3]}", json); + } + + @Test + void testEncodeMap() { + String json = new TeenyJson() + .writeValueAsString(Map.of("value", Map.of("key1", 1, "key2", 2))); + + String case1 = "{\"value\":{\"key2\":2,\"key1\":1}}"; + String case2 = "{\"value\":{\"key1\":1,\"key2\":2}}"; + + if (json.equals(case1) || json.equals(case2)) { + return; + } + + Assertions.fail("Expected " + case1 + " or " + case2 + " but got " + json); + } + + @Test + void testEncodeObject() { + String json = new TeenyJson() + .writeValueAsString(Map.of("value", new Person().setName("Alex").setAge(25))); + + Assertions.assertEquals("{\"value\":{\"name\":\"Alex\",\"IceCreamLover\":false,\"age\":25}}", json); + } + + @Test + void testJsonRaw() { + JsonRawObject target = new JsonRawObject() + .setName("Alex") + .setAge(30) + .setSalary(75) + .setDeveloper(true) + .setProgrammingLanguages(new TeenyJson() + .writeValueAsString(List.of("Java", "Kotlin", "JavaScript"))); + + String json = new TeenyJson().writeValueAsString(target); + + System.out.println(json); + + Map map = new TeenyJson() + .readValue(json, Map.class); + + System.out.println(map); + + Assertions.assertEquals("Alex", map.get("name")); + Assertions.assertEquals("30", map.get("age")); + Assertions.assertEquals("75.0", map.get("salary")); + Assertions.assertEquals(Boolean.TRUE, map.get("developer")); + + List list = (List) map.get("programmingLanguages"); + + Assertions.assertEquals("Java", list.get(0)); + Assertions.assertEquals("Kotlin", list.get(1)); + Assertions.assertEquals("JavaScript", list.get(2)); + + } + + @Test + void testJsonIncludeNonNull() { + JsonRawObject target = new JsonRawObject() + .setName("Alex") + .setAge(30) + .setSalary(75) + .setDeveloper(true); + + String json = new TeenyJson().writeValueAsString(target); + + System.out.println(json); + + Map map = new TeenyJson() + .readValue(json, Map.class); + + Assertions.assertNotNull(map.get("name")); + Assertions.assertNull(map.get("programmingLanguages")); + } + +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonRawObject.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonRawObject.java new file mode 100644 index 0000000..56bd37a --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/JsonRawObject.java @@ -0,0 +1,56 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +public class JsonRawObject { + private String name; + private int age; + private boolean isDeveloper; + private double salary; + private String programmingLanguages; + + public String getName() { + return name; + } + + public JsonRawObject setName(String name) { + this.name = name; + return this; + } + + public int getAge() { + return age; + } + + public JsonRawObject setAge(int age) { + this.age = age; + return this; + } + + public boolean isDeveloper() { + return isDeveloper; + } + + public JsonRawObject setDeveloper(boolean developer) { + isDeveloper = developer; + return this; + } + + public double getSalary() { + return salary; + } + + public JsonRawObject setSalary(double salary) { + this.salary = salary; + return this; + } + + @JsonIncludeNonNull + @JsonRaw(includeKey = true) + public String getProgrammingLanguages() { + return programmingLanguages; + } + + public JsonRawObject setProgrammingLanguages(String programmingLanguages) { + this.programmingLanguages = programmingLanguages; + return this; + } +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectA.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectA.java new file mode 100644 index 0000000..0cb57db --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectA.java @@ -0,0 +1,74 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.util.List; + +public class ObjectA { + + String name; + int age; + ObjectB objectB; + boolean developer; + List hobbies; + + public ObjectA() { + } + + public ObjectA(String name, int age, boolean developer, List hobbies, ObjectB objectB) { + this.name = name; + this.age = age; + this.objectB = objectB; + this.hobbies = hobbies; + this.developer = developer; + } + + public List getHobbies() { + return hobbies; + } + + public boolean isDeveloper() { + return developer; + } + + public void setHobbies(List hobbies) { + this.hobbies = hobbies; + } + + public void setDeveloper(boolean developer) { + this.developer = developer; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public ObjectB getObjectB() { + return objectB; + } + + public void setObjectB(ObjectB objectB) { + this.objectB = objectB; + } + + @Override + public String toString() { + return "ObjectA{" + + "name='" + name + '\'' + + ", age=" + age + + ", objectB=" + objectB + + ", developer=" + developer + + ", hobbies=" + hobbies + + '}'; + } +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectB.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectB.java new file mode 100644 index 0000000..5bc3da2 --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectB.java @@ -0,0 +1,50 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +public class ObjectB { + String name; + int age; + ObjectC objectC; + + public ObjectB() { + } + + public ObjectB(String name, int age, ObjectC objectC) { + this.name = name; + this.age = age; + this.objectC = objectC; + } + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public ObjectC getObjectC() { + return objectC; + } + + public void setObjectC(ObjectC objectC) { + this.objectC = objectC; + } + + @Override + public String toString() { + return "ObjectB{" + + "name='" + name + '\'' + + ", age=" + age + + ", objectC=" + objectC + + '}'; + } +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectC.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectC.java new file mode 100644 index 0000000..3c72e3a --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectC.java @@ -0,0 +1,56 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +public class ObjectC implements C { + String name; + int age; + + public ObjectC() { + } + + public ObjectC(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + + ObjectC objectC = (ObjectC) object; + + if (getAge() != objectC.getAge()) return false; + return getName().equals(objectC.getName()); + } + + @Override + public int hashCode() { + int result = getName().hashCode(); + result = 31 * result + getAge(); + return result; + } + + @Override + public String toString() { + return "ObjectC{" + + "name='" + name + '\'' + + ", age=" + age + + '}'; + } +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectD.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectD.java new file mode 100644 index 0000000..313fc82 --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/ObjectD.java @@ -0,0 +1,48 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.util.List; + +public class ObjectD { + + private String name; + private C c; + private List list; + + public String getName() { + return name; + } + + public ObjectD setName(String name) { + this.name = name; + return this; + } + + public C getC() { + return c; + } + + public List getList() { + return list; + } + + @JsonDeserialize(contentAs = ObjectC.class) + public ObjectD setList(List list) { + this.list = list; + return this; + } + + @JsonDeserialize(as = ObjectC.class) + public ObjectD setC(C c) { + this.c = c; + return this; + } + + @Override + public String toString() { + return "ObjectD{" + + "name='" + name + '\'' + + ", c=" + c + + ", list=" + list + + '}'; + } +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/Person.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/Person.java new file mode 100644 index 0000000..d850e6d --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/Person.java @@ -0,0 +1,144 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import com.google.gson.annotations.SerializedName; +import net.jonathangiles.tools.teenyhttpd.Pet; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +@JsonIncludeNonNull +public class Person { + private String name; + private int age; + private LocalDate birthDate; + private LocalDateTime lastUpdated; + @SerializedName("IceCreamLover") + private boolean likesIceCream; + private Pet pet; + @SerializedName("bestSongs") + private Set favoriteSongs; + private List toolList; + + public List getToolList() { + return toolList; + } + + public Person setToolList(List toolList) { + this.toolList = toolList; + return this; + } + + public Person setFavoriteSongs(Set favoriteSongs) { + this.favoriteSongs = favoriteSongs; + return this; + } + + @JsonAlias("bestSongs") + public Set getFavoriteSongs() { + return favoriteSongs; + } + + public Person setPet(Pet pet) { + this.pet = pet; + return this; + } + + public Pet getPet() { + return pet; + } + + public String getName() { + return name; + } + + public Person setName(String name) { + this.name = name; + return this; + } + + public int getAge() { + return age; + } + + public Person setAge(int age) { + this.age = age; + return this; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public Person setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + return this; + } + + public LocalDateTime getLastUpdated() { + return lastUpdated; + } + + public Person setLastUpdated(LocalDateTime lastUpdated) { + this.lastUpdated = lastUpdated; + return this; + } + + @JsonAlias("IceCreamLover") + public boolean isLikesIceCream() { + return likesIceCream; + } + + public Person setLikesIceCream(boolean likesIceCream) { + this.likesIceCream = likesIceCream; + return this; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + + Person person = (Person) object; + + if (age != person.age) return false; + if (likesIceCream != person.likesIceCream) return false; + if (!Objects.equals(name, person.name)) return false; + if (!Objects.equals(birthDate, person.birthDate)) return false; + if (!Objects.equals(lastUpdated, person.lastUpdated)) + return false; + if (!Objects.equals(pet, person.pet)) return false; + if (!Objects.equals(favoriteSongs, person.favoriteSongs)) + return false; + return Objects.equals(toolList, person.toolList); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + age; + result = 31 * result + (birthDate != null ? birthDate.hashCode() : 0); + result = 31 * result + (lastUpdated != null ? lastUpdated.hashCode() : 0); + result = 31 * result + (likesIceCream ? 1 : 0); + result = 31 * result + (pet != null ? pet.hashCode() : 0); + result = 31 * result + (favoriteSongs != null ? favoriteSongs.hashCode() : 0); + result = 31 * result + (toolList != null ? toolList.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Person{" + + "name='" + name + '\'' + + ", age=" + age + + ", birthDate=" + birthDate + + ", lastUpdated=" + lastUpdated + + ", likesIceCream=" + likesIceCream + + ", pet=" + pet + + ", favoriteSongs=" + favoriteSongs + + ", toolList=" + toolList + + '}'; + } +} \ No newline at end of file diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/json/Tool.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/Tool.java new file mode 100644 index 0000000..c8efbc5 --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/json/Tool.java @@ -0,0 +1,60 @@ +package net.jonathangiles.tools.teenyhttpd.json; + +import java.util.Objects; + +public class Tool { + private String name; + private String kind; + + public Tool(String name, String kind) { + this.name = name; + this.kind = kind; + } + + public Tool() { + } + + public String getName() { + return name; + } + + public Tool setName(String name) { + this.name = name; + return this; + } + + public String getKind() { + return kind; + } + + public Tool setKind(String kind) { + this.kind = kind; + return this; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + + Tool tool = (Tool) object; + + if (!Objects.equals(name, tool.name)) return false; + return Objects.equals(kind, tool.kind); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (kind != null ? kind.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Tool{" + + "name='" + name + '\'' + + ", kind='" + kind + '\'' + + '}'; + } +} \ No newline at end of file