diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/configuration/ConfigurationService.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/configuration/ConfigurationService.java index f6626baa..4fcd1c03 100644 --- a/bellatrix.core/src/main/java/solutions/bellatrix/core/configuration/ConfigurationService.java +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/configuration/ConfigurationService.java @@ -86,4 +86,4 @@ public static String getFileAsString(String fileName) { return ""; } } -} +} \ No newline at end of file diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/ObjectFactory.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/ObjectFactory.java index eb08dc30..ca80a409 100644 --- a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/ObjectFactory.java +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/ObjectFactory.java @@ -22,11 +22,15 @@ public abstract class ObjectFactory { protected static T newInstance(Class clazz, Object... args) throws InvocationTargetException, InstantiationException, IllegalAccessException, ConstructorNotFoundException { Constructor suitableConstructor = getSuitableConstructor(clazz, args); - - if (suitableConstructor.isVarArgs() && args.length < suitableConstructor.getParameterCount()) { - return (T)suitableConstructor.newInstance(addNullVarArgsTo(args)); - } else - return (T)suitableConstructor.newInstance(args); + try { + if (suitableConstructor.isVarArgs() && args.length < suitableConstructor.getParameterCount()) { + return (T)suitableConstructor.newInstance(addNullVarArgsTo(args)); + } else + return (T)suitableConstructor.newInstance(args); + } catch (Exception e) { + var exception = (InvocationTargetException)e; + throw new InvocationTargetException(exception.getTargetException()); + } } private static Object[] addNullVarArgsTo(Object... args) { @@ -77,7 +81,7 @@ private static Constructor findVarArgsConstructor(Class clazz, Class[] ar args[args.length - 1] = getVarArgsType(clazz, argumentTypes); return clazz.getDeclaredConstructor(args); - } catch (NoSuchMethodException|NullPointerException ignored) { + } catch (NoSuchMethodException | NullPointerException ignored) { } throw new NoSuchMethodException("No matching constructor found for the provided argument types."); @@ -101,7 +105,7 @@ private static boolean lengthMatches(Parameter[] parameters, Class[] argumentTyp } private static boolean parameterTypesMatch(Parameter[] parameters, Class[] argumentTypes) { - for (int i = 0; i < argumentTypes.length; i ++) { + for (int i = 0; i < argumentTypes.length; i++) { if (!parameters[i].getType().equals(argumentTypes[i])) return false; } @@ -138,4 +142,4 @@ public ConstructorNotFoundException(String message, Throwable cause, boolean ena super(message, cause, enableSuppression, writableStackTrace); } } -} +} \ No newline at end of file diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/SingletonFactory.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/SingletonFactory.java index 8b44851f..99e224c1 100644 --- a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/SingletonFactory.java +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/SingletonFactory.java @@ -38,9 +38,12 @@ public static T getInstance(Class classOf, Object... initargs) { private static T tryGetInstance(Class classOf, Object... initargs) { try { return newInstance(classOf, initargs); - } catch (InvocationTargetException | InstantiationException | IllegalAccessException | + } catch (InvocationTargetException e) { + Log.error(e.getTargetException().getMessage(), e); + return null; + } catch (InstantiationException | IllegalAccessException | ConstructorNotFoundException e) { - Log.error("Failed to create instance of the class %s.\nException was:\n%s".formatted(classOf.getName(), e)); + Log.error("Failed to create instance of the class %s.\nException was:\n%s".formatted(classOf.getName(), e.getMessage())); return null; } } @@ -62,8 +65,8 @@ public static boolean containsKey(Class classOf) { public static boolean containsValue(Object object) { return mapHolder.get().containsValue(object); } - + public static void clear() { mapHolder.get().clear(); } -} +} \ No newline at end of file diff --git a/bellatrix.data/pom.xml b/bellatrix.data/pom.xml new file mode 100644 index 00000000..43b8061a --- /dev/null +++ b/bellatrix.data/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + solutions.bellatrix + bellatrix + 1.0-SNAPSHOT + + + bellatrix.data + + + 19 + 19 + UTF-8 + 5.5.5 + 2.13.1 + + + + + com.google.code.gson + gson + ${gson.version} + compile + + + org.projectlombok + lombok + ${lombok.version} + provided + + + solutions.bellatrix + bellatrix.core + 1.0 + compile + + + io.rest-assured + rest-assured + ${rest.assured.version} + compile + + + \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/configuration/DataSettings.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/configuration/DataSettings.java new file mode 100644 index 00000000..a7f4bf2e --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/configuration/DataSettings.java @@ -0,0 +1,13 @@ +package solutions.bellatrix.data.configuration; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import solutions.bellatrix.data.http.configuration.HttpSettings; + +@Data +public class DataSettings { + @SerializedName("dataSourceType") + private String datasourceType; + @SerializedName("httpSettings") + private HttpSettings httpSettings; +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/configuration/RepositoryFactory.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/configuration/RepositoryFactory.java new file mode 100644 index 00000000..0fe9e32a --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/configuration/RepositoryFactory.java @@ -0,0 +1,29 @@ +package solutions.bellatrix.data.configuration; + +import solutions.bellatrix.core.utilities.SingletonFactory; +import solutions.bellatrix.data.contracts.Repository; +import solutions.bellatrix.data.http.infrastructure.Entity; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public enum RepositoryFactory { + INSTANCE; + + private final Map, Class> repositories = new ConcurrentHashMap<>(); + + public void registerRepository(Class entityClass, Class> repositoryClass) { + repositories.put(entityClass, repositoryClass); + } + + public Repository getRepository(Class entityClass) { + var repositoryClassType = repositories.get(entityClass); + + if (Objects.isNull(repositoryClassType)) { + throw new IllegalArgumentException("No repository registered for entity class: " + entityClass.getName()); + } + + return (Repository)SingletonFactory.getInstance(repositoryClassType); + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/contracts/Repository.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/contracts/Repository.java new file mode 100644 index 00000000..4172d6b7 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/contracts/Repository.java @@ -0,0 +1,17 @@ +package solutions.bellatrix.data.contracts; + +import solutions.bellatrix.data.http.infrastructure.Entity; + +import java.util.List; + +public interface Repository { + T getById(T entity); + + List getAll(); + + T create(T entity); + + T update(T entity); + + void delete(T entity); +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/authentication/AuthSchemaFactory.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/authentication/AuthSchemaFactory.java new file mode 100644 index 00000000..052009c4 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/authentication/AuthSchemaFactory.java @@ -0,0 +1,39 @@ +package solutions.bellatrix.data.http.authentication; + +import io.restassured.authentication.AuthenticationScheme; +import io.restassured.authentication.BasicAuthScheme; +import io.restassured.authentication.NoAuthScheme; +import io.restassured.authentication.PreemptiveOAuth2HeaderScheme; + +public class AuthSchemaFactory { + public static AuthenticationScheme getAuthenticationScheme(Authentication authentication) { + var authType = authentication.getAuthenticationMethod(); + var option = authentication.getAuthenticationOptions().stream().filter(x -> x.get("type").equals(authType.getMethod())).findFirst(); + if (option.isEmpty()) { + throw new IllegalArgumentException("Authentication type not found: %s, Supported types : ".formatted(authType)); + } + + switch (authType) { + case BASIC -> { + var basicAuth = option.get(); + String username = basicAuth.get("username").toString(); + String password = basicAuth.get("password").toString(); + var basicSchema = new BasicAuthScheme(); + basicSchema.setUserName(username); + basicSchema.setPassword(password); + return basicSchema; + } + case BEARER -> { + var bearerAuth = option.get(); + String token = bearerAuth.get("token").toString(); + var bearerSchema = new PreemptiveOAuth2HeaderScheme(); + bearerSchema.setAccessToken(token); + return bearerSchema; + } + case QUERY_PARAMETER -> { + return new NoAuthScheme(); + } + default -> throw new IllegalArgumentException("Unsupported authentication type: %s".formatted(authType)); + } + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/authentication/Authentication.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/authentication/Authentication.java new file mode 100644 index 00000000..062a8f16 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/authentication/Authentication.java @@ -0,0 +1,24 @@ +package solutions.bellatrix.data.http.authentication; + +import com.google.gson.annotations.SerializedName; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.util.LinkedHashMap; +import java.util.LinkedList; + +public class Authentication { + @SerializedName("method") + private String method; + @SerializedName("options") + @Getter private LinkedList> authenticationOptions; + + @Setter(AccessLevel.PRIVATE) + private transient AuthenticationMethod authenticationMethod; + + public AuthenticationMethod getAuthenticationMethod() { + setAuthenticationMethod(AuthenticationMethod.parse(method)); + return authenticationMethod; + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/authentication/AuthenticationMethod.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/authentication/AuthenticationMethod.java new file mode 100644 index 00000000..733b7559 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/authentication/AuthenticationMethod.java @@ -0,0 +1,27 @@ +package solutions.bellatrix.data.http.authentication; + +import lombok.Getter; + +@Getter +public enum AuthenticationMethod { + BEARER("Bearer"), + BASIC("Basic"), + QUERY_PARAMETER("QueryParameters"); + + private final String method; + + AuthenticationMethod(String method) { + this.method = method; + } + + public static AuthenticationMethod parse(String y) { + for (var state : values()) { + String enumDisplayValue = state.getMethod(); + if (enumDisplayValue != null && enumDisplayValue.equalsIgnoreCase(y)) { + return state; + } + } + + throw new IllegalArgumentException("No enum constant with value: %s".formatted(y)); + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/configuration/HttpSettings.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/configuration/HttpSettings.java new file mode 100644 index 00000000..fd338ebb --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/configuration/HttpSettings.java @@ -0,0 +1,46 @@ +package solutions.bellatrix.data.http.configuration; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import solutions.bellatrix.core.configuration.ConfigurationService; +import solutions.bellatrix.data.configuration.DataSettings; +import solutions.bellatrix.data.http.authentication.Authentication; +import solutions.bellatrix.data.http.authentication.AuthenticationMethod; + +import java.util.function.Consumer; + +@Data +public class HttpSettings { + @SerializedName("baseUrl") + private String baseUrl; + @SerializedName("basePath") + private String basePath; + @SerializedName("contentType") + private String contentType; + @SerializedName("authentication") + private Authentication authentication; + @SerializedName("urlEncoderEnabled") + private boolean urlEncoderEnabled; + + public HttpSettings(HttpSettings httpSettings) { + setBaseUrl(httpSettings.getBaseUrl()); + setBasePath(httpSettings.getBasePath()); + setContentType(httpSettings.getContentType()); + setUrlEncoderEnabled(httpSettings.isUrlEncoderEnabled()); + setAuthentication(httpSettings.getAuthentication()); + } + + public static HttpSettings custom(Consumer httpSettings) { + var settings = ConfigurationService.get(DataSettings.class).getHttpSettings(); + if (settings == null) { + throw new IllegalStateException("Include the httpSettings section in your config file"); + + } + httpSettings.accept(settings); + return settings; + } + + public AuthenticationMethod getAuthenticationMethod() { + return authentication.getAuthenticationMethod(); + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/contracts/ObjectConverter.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/contracts/ObjectConverter.java new file mode 100644 index 00000000..156ad9a6 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/contracts/ObjectConverter.java @@ -0,0 +1,11 @@ +package solutions.bellatrix.data.http.contracts; + +import java.util.List; + +public interface ObjectConverter { + String toString(T object); + + T fromString(String data, Class type); + + List fromStringToList(String json, Class elementType); +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/contracts/Queryable.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/contracts/Queryable.java new file mode 100644 index 00000000..c0379209 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/contracts/Queryable.java @@ -0,0 +1,38 @@ +package solutions.bellatrix.data.http.contracts; + +import com.google.gson.annotations.SerializedName; +import solutions.bellatrix.data.http.infrastructure.QueryParameter; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Objects; + +public interface Queryable { + default LinkedList toQueryParams() { + try { + var queryParameters = new LinkedList(); + Class clazz = this.getClass(); + + while (clazz!=null) { + Field[] fields = clazz.getDeclaredFields(); + Arrays.stream(fields).forEach(x -> x.setAccessible(true)); + for (Field field : fields) { + if (field.isAnnotationPresent(SerializedName.class)) { + var queryParameterName = field.getAnnotation(SerializedName.class).value(); + var value = field.get(this); + if (Objects.nonNull(value)) { + queryParameters.add(new QueryParameter(queryParameterName, value)); + } + } + } + + clazz = clazz.getSuperclass(); + } + + return queryParameters; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/httpContext/HttpContext.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/httpContext/HttpContext.java new file mode 100644 index 00000000..9ce76a6d --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/httpContext/HttpContext.java @@ -0,0 +1,161 @@ +package solutions.bellatrix.data.http.httpContext; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.filter.log.LogDetail; +import io.restassured.specification.RequestSpecification; +import lombok.Getter; +import lombok.Setter; +import solutions.bellatrix.data.http.authentication.AuthSchemaFactory; +import solutions.bellatrix.data.http.authentication.AuthenticationMethod; +import solutions.bellatrix.data.http.configuration.HttpSettings; +import solutions.bellatrix.data.http.infrastructure.HTTPMethod; +import solutions.bellatrix.data.http.infrastructure.QueryParameter; + +import java.util.*; +import java.util.function.Consumer; + +public class HttpContext { + private final HttpSettings httpSettings; + private final LinkedList pathParameters; + private final LinkedHashSet queryParameters; + @Setter + private RequestSpecBuilder specBuilder; + @Getter + private String requestBody; + @Getter private HTTPMethod httpMethod; + + public HttpContext(HttpSettings settings) { + this.httpSettings = settings; + this.pathParameters = new LinkedList<>(); + this.queryParameters = new LinkedHashSet<>(); + this.specBuilder = createInitialSpecBuilder(httpSettings); + } + + public HttpContext(HttpContext httpContext) { + this.httpSettings = httpContext.getHttpSettings(); + this.queryParameters = httpContext.getQueryParameters(); + this.pathParameters = httpContext.getPathParameters(); + this.specBuilder = httpContext.getRequestBuilder(); + } + + public HttpSettings getHttpSettings() { + return new HttpSettings(httpSettings); + } + + public void updateRequestSpecification(Consumer specBuilderConsumer) { + specBuilderConsumer.accept(specBuilder); + } + + public void addRequestBody(String body) { + this.requestBody = body; + } + + public void addRequestMethod(HTTPMethod httpMethod) { + this.httpMethod = httpMethod; + } + + public LinkedList getPathParameters() { + return new LinkedList<>(pathParameters); + } + + public LinkedHashSet getQueryParameters() { + return new LinkedHashSet<>(queryParameters); + } + + private RequestSpecBuilder getRequestBuilder() { + return new RequestSpecBuilder().addRequestSpecification(specBuilder.build()); + } + + public RequestSpecification requestSpecification() { + if (requestBody != null) { + specBuilder.setBody(requestBody); + } + + if (!queryParameters.isEmpty()) { + specBuilder.addQueryParams(getRequestQueryParameters()); + } + + specBuilder.setBasePath(buildRequestPath()); + + return specBuilder.build(); + } + + public void addPathParameter(Object param) { + if (Objects.isNull(param)) { + throw new IllegalArgumentException("Path parameter cannot be null."); + } + + pathParameters.add(String.valueOf(param)); + } + + public void addPathParameters(Collection params) { + for (Object param : params) { + addPathParameter(param); + } + } + + public void addQueryParameters(Collection parameters) { + parameters.forEach(this::addQueryParameter); + } + + public void addQueryParameter(QueryParameter parameter) { + queryParameters.add(parameter); + } + + private Map getRequestQueryParameters() { + //todo: this logic should be extracted because create tightly coupling with settings + LinkedHashMap queryParams = new LinkedHashMap<>(); + if (httpSettings.getAuthenticationMethod() == AuthenticationMethod.QUERY_PARAMETER) { + var option = httpSettings.getAuthentication().getAuthenticationOptions().stream().filter(x -> x.get("type").equals("QueryParameters")).findFirst().get(); + String insertionOrder = (String)option.get("insertionOrder"); + if (insertionOrder.equals("start")) { + for (var key : option.keySet()) { + if (!key.equals("type") && !key.equals("insertionOrder")) { + queryParams.put(key, option.get(key).toString()); + } + } + for (QueryParameter queryParameter : queryParameters) { + queryParams.put(queryParameter.getKey(), String.valueOf(queryParameter.getValue())); + } + } else { + for (QueryParameter queryParameter : queryParameters) { + queryParams.put(queryParameter.getKey(), queryParameter.getValue().toString()); + } + for (var key : option.keySet()) { + if (!key.equals("type") && !key.equals("insertionOrder")) { + queryParams.put(key, option.get(key).toString()); + } + } + + } + return queryParams; + + } + + for (QueryParameter queryParameter : queryParameters) { + queryParams.put(queryParameter.getKey(), queryParameter.getValue().toString()); + } + + return queryParams; + } + + + public String buildRequestPath() { + if (!pathParameters.isEmpty()) { + String[] pathList = pathParameters.stream().filter(path -> !String.valueOf(path).isEmpty()).map(String::valueOf).toArray(String[]::new); + String formattedPath = String.join("/", pathList); + return httpSettings.getBasePath() + "/%s".formatted(formattedPath); + } + + return httpSettings.getBasePath(); + } + + private RequestSpecBuilder createInitialSpecBuilder(HttpSettings httpSettings) { + return new RequestSpecBuilder() + .setBaseUri(httpSettings.getBaseUrl()) + .setBasePath(httpSettings.getBasePath()).setContentType(httpSettings.getContentType()) + .setAuth(AuthSchemaFactory.getAuthenticationScheme(httpSettings.getAuthentication())) + .setUrlEncodingEnabled(httpSettings.isUrlEncoderEnabled()) + .log(LogDetail.ALL); + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/Entity.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/Entity.java new file mode 100644 index 00000000..f16e45c1 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/Entity.java @@ -0,0 +1,30 @@ +package solutions.bellatrix.data.http.infrastructure; + +import lombok.experimental.SuperBuilder; +import solutions.bellatrix.data.configuration.RepositoryFactory; +import solutions.bellatrix.data.contracts.Repository; + +@SuperBuilder +public abstract class Entity { + public TEntity get() { + var repository = (Repository)RepositoryFactory.INSTANCE.getRepository(this.getClass()); + return (TEntity)repository.getById(this); + } + + public TEntity create() { + var repository = (Repository)RepositoryFactory.INSTANCE.getRepository(this.getClass()); + return (TEntity)repository.create(this); + } + + public TEntity update() { + var repository = (Repository)RepositoryFactory.INSTANCE.getRepository(this.getClass()); + return (TEntity)repository.update(this); + } + + public void delete() { + var repository = (Repository)RepositoryFactory.INSTANCE.getRepository(this.getClass()); + repository.delete(this); + } + + public abstract TIdentifier getIdentifier(); +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HTTPMethod.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HTTPMethod.java new file mode 100644 index 00000000..fdf9c559 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HTTPMethod.java @@ -0,0 +1,18 @@ +package solutions.bellatrix.data.http.infrastructure; + +import lombok.Getter; + +@Getter +public enum HTTPMethod { + GET("GET"), + POST("POST"), + PUT("PUT"), + DELETE("DELETE"), + PATCH("PATCH"); + + private final String method; + + HTTPMethod(String method) { + this.method = method; + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HttpEntity.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HttpEntity.java new file mode 100644 index 00000000..3cbe16bd --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HttpEntity.java @@ -0,0 +1,20 @@ +package solutions.bellatrix.data.http.infrastructure; + +import io.restassured.response.Response; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; +import solutions.bellatrix.data.http.contracts.Queryable; + +import java.util.Objects; + +@Data +@SuperBuilder +@EqualsAndHashCode(callSuper = true) +public abstract class HttpEntity extends Entity implements Queryable { + private transient Response response; + + public boolean hasInvalidIdentifier() { + return Objects.isNull(this.getIdentifier()); + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HttpRepository.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HttpRepository.java new file mode 100644 index 00000000..1cbca753 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HttpRepository.java @@ -0,0 +1,186 @@ +package solutions.bellatrix.data.http.infrastructure; + +import io.restassured.RestAssured; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import solutions.bellatrix.core.plugins.EventListener; +import solutions.bellatrix.data.contracts.Repository; +import solutions.bellatrix.data.http.contracts.ObjectConverter; +import solutions.bellatrix.data.http.httpContext.HttpContext; +import solutions.bellatrix.data.http.infrastructure.events.*; +import solutions.bellatrix.data.http.infrastructure.internal.DeserializationMode; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static solutions.bellatrix.data.http.infrastructure.HTTPMethod.*; + +public abstract class HttpRepository implements Repository { + public static final EventListener SENDING_REQUEST = new EventListener<>(); + public static final EventListener REQUEST_SENT = new EventListener<>(); + public static final EventListener CREATING_ENTITY = new EventListener<>(); + public static final EventListener ENTITY_CREATED = new EventListener<>(); + public static final EventListener UPDATING_ENTITY = new EventListener<>(); + public static final EventListener ENTITY_UPDATED = new EventListener<>(); + public static final EventListener DELETING_ENTITY = new EventListener<>(); + public static final EventListener ENTITY_DELETED = new EventListener<>(); + + + protected final HttpContext repositoryContext; + private final Class entityType; + private final ObjectConverter objectConverter; + private HttpContext requestContext; + + protected HttpRepository(Class entityType, ObjectConverter objectConverter, Supplier repositoryContext) { + this.entityType = entityType; + this.objectConverter = objectConverter; + this.repositoryContext = repositoryContext.get(); + this.requestContext = new HttpContext(this.repositoryContext); + } + + @Override + public THttpEntity getById(HttpEntity entity) { + if (entity.hasInvalidIdentifier()) { + throw new IllegalArgumentException("Entity identifier cannot be null."); + } + updateRequestContext(requestContext -> { + requestContext.addPathParameter(entity.getIdentifier()); + requestContext.addRequestMethod(GET); + }); + + var response = handleResponse(() -> sendRequest(requestContext)); + + var record = (THttpEntity)deserializeInternal(response, DeserializationMode.SINGLE); + + return record; + } + + @Override + public THttpEntity create(THttpEntity entity) { + updateRequestContext(requestContext -> { + requestContext.addRequestBody((objectConverter.toString(entity))); + requestContext.addRequestMethod(POST); + }); + + CREATING_ENTITY.broadcast(new EntityCreatedEventArgs(entity)); + var response = handleResponse(() -> sendRequest(requestContext)); + + var record = (THttpEntity)deserializeInternal(response, DeserializationMode.SINGLE); + + ENTITY_CREATED.broadcast(new EntityCreatedEventArgs(record)); + + return record; + } + + @Override + public List getAll() { + updateRequestContext(requestContext -> { + requestContext.addRequestMethod(GET); + }); + + var response = handleResponse(() -> sendRequest(requestContext)); + + var entities = (List)deserializeInternal(response, DeserializationMode.LIST); + + return entities; + } + + @Override + public void delete(THttpEntity entity) { + if (entity.hasInvalidIdentifier()) { + throw new IllegalArgumentException("Entity identifier cannot be null."); + } + + updateRequestContext(requestContext -> { + requestContext.addPathParameter(String.valueOf(entity.getIdentifier())); + requestContext.addRequestMethod(DELETE); + }); + + DELETING_ENTITY.broadcast(new EntityDeletedEventArgs(entity)); + + sendRequest(requestContext); + + ENTITY_DELETED.broadcast(new EntityDeletedEventArgs(entity)); + } + + @Override + public THttpEntity update(THttpEntity entity) { + if (entity.hasInvalidIdentifier()) { + throw new IllegalArgumentException("Entity identifier cannot be null."); + } + + updateRequestContext(requestContext -> { + requestContext.addRequestMethod(PUT); + requestContext.addPathParameter(String.valueOf(entity.getIdentifier())); + requestContext.updateRequestSpecification(specBuilder -> { + specBuilder.setBody(objectConverter.toString(entity)); + }); + }); + + UPDATING_ENTITY.broadcast(new EntityUpdatedEventArgs(entity)); + var response = handleResponse(() -> sendRequest(requestContext)); + + var record = (THttpEntity)deserializeInternal(response, DeserializationMode.SINGLE); + + ENTITY_UPDATED.broadcast(new EntityUpdatedEventArgs(entity)); + return record; + } + + protected Response sendRequest(HttpContext requestContext) { + return switch (requestContext.getHttpMethod()) { + case GET -> broadcastRequest(() -> client().get()); + case POST -> broadcastRequest(() -> client().post()); + case PUT -> broadcastRequest(() -> client().put()); + case DELETE -> broadcastRequest(() -> client().delete()); + case PATCH -> broadcastRequest(() -> client().patch()); + }; + } + + protected ObjectConverter getObjectConverter() { + return objectConverter; + } + + protected HttpResponse handleResponse(Supplier response) { + return new HttpResponse(response.get().getBody().asString(), response.get()); + } + + protected void updateRequestContext(Consumer requestConfigConsumer) { + requestConfigConsumer.accept(requestContext); + } + + private void restoreRequestContext() { + requestContext = new HttpContext(repositoryContext); + } + + private RequestSpecification client() { + return RestAssured.given().spec(requestContext.requestSpecification()); + } + + private R deserializeInternal(HttpResponse response, DeserializationMode mode) { + try { + if (mode == DeserializationMode.LIST) { + List entities = objectConverter.fromStringToList(response.getBody(), entityType); + entities.forEach(entity -> entity.setResponse(response.getResponse())); + return (R)entities; + } else { + THttpEntity entity = objectConverter.fromString(response.getBody(), entityType); + entity.setResponse(response.getResponse()); + return (R)entity; + } + } catch (Exception e) { + throw new RuntimeException("Failed to parse response: " + response.getBody(), e); + } + } + + private Response broadcastRequest(Supplier responseSupplier) { + try { + SENDING_REQUEST.broadcast(new HttpRequestEventArgs(requestContext)); + Response response = responseSupplier.get(); + REQUEST_SENT.broadcast(new ResponseProcessingEventArgs(response)); + return response; + } finally { + restoreRequestContext(); + } + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HttpResponse.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HttpResponse.java new file mode 100644 index 00000000..4ebab26a --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/HttpResponse.java @@ -0,0 +1,15 @@ +package solutions.bellatrix.data.http.infrastructure; + +import io.restassured.response.Response; +import lombok.Getter; + +@Getter +public class HttpResponse { + private final String body; + private final Response response; + + public HttpResponse(String body, Response response) { + this.body = body; + this.response = response; + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/JsonConverter.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/JsonConverter.java new file mode 100644 index 00000000..56fc31c1 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/JsonConverter.java @@ -0,0 +1,98 @@ +package solutions.bellatrix.data.http.infrastructure; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import solutions.bellatrix.data.http.contracts.ObjectConverter; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public class JsonConverter implements ObjectConverter { + protected final Gson gson; + + public JsonConverter() { + this(builder -> { + }); + } + + public JsonConverter(Consumer consumer) { + this.gson = getInstance(consumer); + } + + @Override + public String toString(T object) { + if (Objects.isNull(object)) { + return ""; + } + + return gson.toJson(object); + } + + public String toString(T object, Class type) { + if (Objects.isNull(object)) { + return ""; + } + + if (Objects.isNull(type)) { + throw new IllegalArgumentException("Type cannot be null"); + } + + try { + return gson.toJson(object, type); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize object of type: " + type.getName(), e); + } + } + + @Override + public T fromString(String data, Class type) { + if (Objects.isNull(data) || data.trim().isEmpty()) { + return null; + } + + if (Objects.isNull(type)) { + throw new IllegalArgumentException("Type cannot be null"); + } + + try { + return gson.fromJson(data, type); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize JSON to type: " + type.getName(), e); + } + } + + @Override + public List fromStringToList(String json, Class type) { + if (Objects.isNull(json) || json.trim().isEmpty()) { + return new ArrayList<>(); + } + + if (Objects.isNull(type)) { + throw new IllegalArgumentException("Element type cannot be null"); + } + + Type listType = TypeToken.getParameterized(List.class, type).getType(); + + try { + return gson.fromJson(json, listType); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize JSON to type: " + type.getName(), e); + } + } + + private Gson getInstance(Consumer consumer) { + //todo: make it readable from config + GsonBuilder builder = new GsonBuilder(); + builder.setFieldNamingPolicy(com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); + builder.disableHtmlEscaping(); + builder.setLenient(); + builder.setPrettyPrinting(); + consumer.accept(builder); + + return builder.create(); + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/QueryParameter.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/QueryParameter.java new file mode 100644 index 00000000..a609ba56 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/QueryParameter.java @@ -0,0 +1,30 @@ +package solutions.bellatrix.data.http.infrastructure; + +import lombok.Getter; + +import java.util.Objects; + +@Getter +public class QueryParameter { + private final String key; + private final Object value; + + public QueryParameter(String key, Object value) { + if ((Objects.isNull(key)) || key.isBlank()) { + throw new IllegalArgumentException("QueryParameter key cannot be null or blank"); + } + + if (Objects.isNull(value)) { + throw new IllegalArgumentException("QueryParameter value cannot be null"); + } + + this.key = key; + this.value = value; + } + + @Override + public boolean equals(Object obj) { + QueryParameter that = (QueryParameter)obj; + return key.equals(that.key); + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityCreatedEventArgs.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityCreatedEventArgs.java new file mode 100644 index 00000000..922acd85 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityCreatedEventArgs.java @@ -0,0 +1,9 @@ +package solutions.bellatrix.data.http.infrastructure.events; + +import solutions.bellatrix.data.http.infrastructure.Entity; + +public class EntityCreatedEventArgs extends EntityOperationsEventArgs { + public EntityCreatedEventArgs(Entity entity) { + super(entity); + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityDeletedEventArgs.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityDeletedEventArgs.java new file mode 100644 index 00000000..2db10245 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityDeletedEventArgs.java @@ -0,0 +1,9 @@ +package solutions.bellatrix.data.http.infrastructure.events; + +import solutions.bellatrix.data.http.infrastructure.Entity; + +public class EntityDeletedEventArgs extends EntityOperationsEventArgs { + public EntityDeletedEventArgs(Entity entity) { + super(entity); + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityOperationsEventArgs.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityOperationsEventArgs.java new file mode 100644 index 00000000..173a5838 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityOperationsEventArgs.java @@ -0,0 +1,13 @@ +package solutions.bellatrix.data.http.infrastructure.events; + +import lombok.Getter; +import solutions.bellatrix.data.http.infrastructure.Entity; + +@Getter +public abstract class EntityOperationsEventArgs { + private final Entity entity; + + protected EntityOperationsEventArgs(Entity entity) { + this.entity = entity; + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityUpdatedEventArgs.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityUpdatedEventArgs.java new file mode 100644 index 00000000..fee04909 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/EntityUpdatedEventArgs.java @@ -0,0 +1,9 @@ +package solutions.bellatrix.data.http.infrastructure.events; + +import solutions.bellatrix.data.http.infrastructure.Entity; + +public class EntityUpdatedEventArgs extends EntityOperationsEventArgs { + public EntityUpdatedEventArgs(Entity entity) { + super(entity); + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/HttpRequestEventArgs.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/HttpRequestEventArgs.java new file mode 100644 index 00000000..d45400c7 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/HttpRequestEventArgs.java @@ -0,0 +1,13 @@ +package solutions.bellatrix.data.http.infrastructure.events; + +import lombok.Getter; +import solutions.bellatrix.data.http.httpContext.HttpContext; + +@Getter +public class HttpRequestEventArgs { + public final HttpContext requestConfiguration; + + public HttpRequestEventArgs(HttpContext requestConfiguration) { + this.requestConfiguration = requestConfiguration; + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/ResponseProcessingEventArgs.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/ResponseProcessingEventArgs.java new file mode 100644 index 00000000..03c9201e --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/events/ResponseProcessingEventArgs.java @@ -0,0 +1,13 @@ +package solutions.bellatrix.data.http.infrastructure.events; + +import io.restassured.response.Response; +import lombok.Getter; + +@Getter +public class ResponseProcessingEventArgs { + private final Response response; + + public ResponseProcessingEventArgs(Response response) { + this.response = response; + } +} \ No newline at end of file diff --git a/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/internal/DeserializationMode.java b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/internal/DeserializationMode.java new file mode 100644 index 00000000..6176afe3 --- /dev/null +++ b/bellatrix.data/src/main/java/solutions/bellatrix/data/http/infrastructure/internal/DeserializationMode.java @@ -0,0 +1,6 @@ +package solutions.bellatrix.data.http.infrastructure.internal; + +public enum DeserializationMode { + SINGLE, + LIST +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index b40c83bd..c7ef85be 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ bellatrix.android bellatrix.ios bellatrix.api + bellatrix.data framework-tests/bellatrix.core.tests framework-tests/bellatrix.web.tests