Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Teeny json suport #3

Merged
merged 18 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,6 +28,7 @@ jobs:
run: mvn --batch-mode -DskipTests package

- name: Test
timeout-minutes: 5
Copy link
Owner

Choose a reason for hiding this comment

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

Nice addition!

run: mvn --batch-mode -Dmaven.test.failure.ignore=true test

- name: Report
Expand All @@ -38,6 +42,7 @@ jobs:

- name: Upload to Azure Blob Storage
uses: bacongobbler/[email protected]
if: env.AZURE_BLOB_STORAGE_CONNECTION_STRING != null
with:
source_dir: 'target/apidocs'
container_name: '$web'
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
<scope>test</scope>
<scope>test</scope>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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;

ParameterizedTypeHelper(Class<?> type) {
this.firstType = type;
this.parentType = null;
this.typeArguments = null;
}

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;
}

private Class<?> getRealClass(Type type) {
if (type instanceof WildcardType) {
WildcardType wildcardType = (WildcardType) type;
return (Class<?>) wildcardType.getUpperBounds()[0];
}

return (Class<?>) type;
}

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 && parentType.isAssignableFrom(type);
}

public Type[] getTypeArguments() {
return typeArguments;
}

public Class<?> getFirstType() {
return firstType;
}

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) +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
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.
* <p>
* @param clazz the class to create a new instance of
* @return a new instance of the given class
* @param <T> 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> T newInstance(Class<T> 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.
* <p>
* @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 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.
* <p>
* @param clazz the class to get the fields from
* @return a map of the fields of the given class
*/
public static Map<String, Field> getFields(Class<?> clazz) {
Map<String, Method> 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<String, Method> 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());

JonathanGiles marked this conversation as resolved.
Show resolved Hide resolved
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<String, Method> 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);
}

private static String findAlias(Map<String, Method> 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.
* <p>
* @param clazz the class to get the mutators from
* @return a map of the mutators of the given class
*/
public static Map<String, Method> getMutators(Class<?> clazz) {
Map<String, Method> methodMap = Arrays.stream(clazz.getDeclaredMethods())
.collect(Collectors.toMap(Method::getName, m -> m));

Map<String, Method> resultMap = new HashMap<>();

for (Method method : methodMap.values()) {
if (!isMutator(method)) continue;

String mutatorName = getMutatorName(method, methodMap);

resultMap.put(mutatorName, method);
}

return resultMap;
}


private static boolean isMutator(Method method) {
return method.getName().startsWith("set") && method.getParameterCount() == 1;
}

private static boolean isWritable(Field field) {
if (Modifier.isStatic(field.getModifiers())) {
return false;
}
return !Modifier.isFinal(field.getModifiers());
}

}
Original file line number Diff line number Diff line change
@@ -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 {
JonathanGiles marked this conversation as resolved.
Show resolved Hide resolved
String value();
}
Loading
Loading