Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
58 changes: 58 additions & 0 deletions bellatrix.servicenow/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>solutions.bellatrix</groupId>
<artifactId>bellatrix</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>bellatrix.servicenow</artifactId>

<properties>
<maven.compiler.source>19</maven.compiler.source>
<maven.compiler.target>19</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<bellatrix.framework.version>1.0</bellatrix.framework.version>
</properties>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>solutions.bellatrix</groupId>
<artifactId>bellatrix.core</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>solutions.bellatrix</groupId>
<artifactId>bellatrix.web</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>solutions.bellatrix</groupId>
<artifactId>bellatrix.api</artifactId>
<version>${bellatrix.framework.version}</version>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>solutions.bellatrix</groupId>
<artifactId>bellatrix.data</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package data.contracts;

public interface ServiceNowTable {
String getTableName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package data.converters;

import data.converters.typeAdapters.DateTimeAdapterFactory;
import data.converters.typeAdapters.StringTypeAdapter;
import solutions.bellatrix.data.http.infrastructure.JsonConverter;

import java.util.List;

public class TableApiConverter extends JsonConverter {
public TableApiConverter() {
super(gson -> {
gson.registerTypeAdapter(String.class, new StringTypeAdapter());
gson.registerTypeAdapterFactory(new DateTimeAdapterFactory());
});
}

@Override
public <T> List<T> fromStringToList(String json, Class<T> type) {
return super.fromStringToList(json, type);
}

@Override
public <T> T fromString(String data, Class<T> type) {
return super.fromString(data, type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package data.converters.typeAdapters;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;

import java.time.LocalDate;
import java.time.LocalDateTime;

public class DateTimeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType() == LocalDateTime.class) {
return (TypeAdapter<T>) new LocalDateTimeTypeAdapter();
} else if (type.getRawType() == LocalDate.class) {
return (TypeAdapter<T>) new LocalDateTypeAdapter();
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package data.converters.typeAdapters;

import com.google.gson.*;

import java.lang.reflect.Type;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

public class DurationConverter implements JsonSerializer<Duration>, JsonDeserializer<Duration> {
DateTimeFormatter serializeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

@Override
public JsonElement serialize(Duration duration, Type srcType, JsonSerializationContext context) {
var endDate = LocalDateTime.ofEpochSecond(0,(int)duration.toNanos(), ZoneOffset.of("+00:00"));

return new JsonPrimitive(serializeFormatter.format(endDate));
}

@Override
public Duration deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
var elementAsString = jsonElement.getAsString();

if (elementAsString.isEmpty()) {
return null;
}

var endDateTime = LocalDateTime.parse(elementAsString, serializeFormatter);
var startDateTime = LocalDateTime.parse("1970-01-01 00:00:00", serializeFormatter);

return Duration.between(startDateTime, endDateTime);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package data.converters.typeAdapters;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Objects;

public class LocalDateTimeTypeAdapter extends TypeAdapter<LocalDateTime> {
private final DateTimeFormatter formatter;

public LocalDateTimeTypeAdapter() {
this.formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.of("America/Los_Angeles"));
}

@Override
public void write(JsonWriter out, LocalDateTime value) throws IOException {
if (value == null) {
out.nullValue();
} else {
out.value(formatter.format(value));
}
}

@Override
public LocalDateTime read(JsonReader in) throws IOException {
if (Objects.isNull(in.peek())) {
return null;
}

String dateTimeString = in.nextString();
if (dateTimeString.isEmpty()) {
return null;
}

return LocalDateTime.parse(dateTimeString, formatter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package data.converters.typeAdapters;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class LocalDateTypeAdapter extends TypeAdapter<LocalDate> {
private static final String PACIFIC_TIME_ZONE = "America/Los_Angeles";
private final DateTimeFormatter serializeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

@Override
public void write(JsonWriter out, LocalDate value) throws IOException {
if (value == null) {
out.nullValue();
} else {
out.value(serializeFormatter.format(value));
}
}

@Override
public LocalDate read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}

String dateStr = in.nextString().trim();

if (dateStr.isEmpty()) {
return null;
}

DateTimeFormatter formatter;

if (dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$")) {
// Example: 2025-07-16T13:45:00.123Z
dateStr = dateStr.substring(0, 10);
formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
} else if (dateStr.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$")) {
// Example: 2025-07-16 13:45:00
dateStr = dateStr.substring(0, 10);
formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
} else if (dateStr.matches("^\\d{2}/\\d{2}/\\d{4}$")) {
// Example: 07/16/2025
formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");
} else {
// Default to yyyy-MM-dd with timezone (ignored in LocalDate parsing)
formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
.withZone(ZoneId.of(PACIFIC_TIME_ZONE));
}

return LocalDate.parse(dateStr, formatter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package data.converters.typeAdapters;

import com.google.gson.TypeAdapter;
import com.google.gson.internal.bind.TypeAdapters;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.util.Objects;

public class StringTypeAdapter extends TypeAdapter<String> {
@Override
public void write(JsonWriter out, String value) throws IOException {
TypeAdapters.STRING.write(out, value);
}

@Override
public String read(JsonReader in) throws IOException {
var result = TypeAdapters.STRING.read(in);

if (Objects.isNull(result) || result.isEmpty()) {
return null;
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package data.entities;


import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
import solutions.bellatrix.data.http.infrastructure.HttpEntity;

import java.time.LocalDateTime;

@Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
public abstract class ServiceNowEntity<TEntity extends ServiceNowEntity> extends HttpEntity<String, TEntity> {
@SerializedName("sys_id")
private String sysId;
@SerializedName("sys_domain")
private String sysDomain;
@SerializedName("sys_created_on")
private LocalDateTime sysCreatedOn;
@EqualsAndHashCode.Exclude
@SerializedName("sys_created_by")
private String sysCreatedBy;
@EqualsAndHashCode.Exclude
@SerializedName("sys_updated_on")
private LocalDateTime sysUpdatedOn;
@EqualsAndHashCode.Exclude
@SerializedName("sys_updated_by")
private String sysUpdatedBy;
@EqualsAndHashCode.Exclude
@SerializedName("sys_mod_count")
private String sysModCount;
@SerializedName("sys_domain_path")
private String sysDomainPath;
@SerializedName("sys_tags")
private String sysTags;
@SerializedName("sys_class_name")
private String sysClassName;

@Override
public String getIdentifier() {
return sysId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package data.repositories;

import solutions.bellatrix.data.http.contracts.ObjectConverter;
import solutions.bellatrix.data.http.httpContext.HttpContext;
import solutions.bellatrix.data.http.infrastructure.HttpEntity;
import solutions.bellatrix.data.http.infrastructure.HttpRepository;

import java.util.function.Supplier;

public abstract class ServiceNowRepository<T extends HttpEntity> extends HttpRepository<T> {
protected ServiceNowRepository(Class<T> type, ObjectConverter serializer, Supplier<HttpContext> context) {
super(type, serializer, context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package data.repositories;

import data.entities.ServiceNowEntity;
import data.contracts.ServiceNowTable;
import data.converters.TableApiConverter;
import io.restassured.response.Response;
import solutions.bellatrix.data.http.configuration.HttpSettings;
import solutions.bellatrix.data.http.httpContext.HttpContext;
import solutions.bellatrix.data.http.infrastructure.HttpResponse;
import solutions.bellatrix.data.http.infrastructure.QueryParameter;

import java.util.Map;
import java.util.function.Supplier;

public abstract class TableApiRepository<T extends ServiceNowEntity> extends ServiceNowRepository<T> {
protected TableApiRepository(Class<T> entityType, ServiceNowTable serviceNowTable) {
super(entityType, new TableApiConverter(), () -> {
HttpSettings httpSettings = HttpSettings.custom(x -> x.setBasePath("api/now/table/%s".formatted(serviceNowTable.getTableName())));
var httpContext = new HttpContext(httpSettings);
httpContext.addQueryParameter(new QueryParameter("sysparm_exclude_reference_link", "true"));
return httpContext;
});
}

@Override
protected HttpResponse handleResponse(Supplier<Response> responseSupplier) {
var suppliedResponse = responseSupplier.get();
var entityJson = suppliedResponse.jsonPath().getJsonObject("result");
TableApiConverter servicenowDeserializer = (TableApiConverter)getObjectConverter();
String body = servicenowDeserializer.toString(entityJson, Map.class);
return new HttpResponse(body, suppliedResponse);
}
}
Loading