How to specify DateTime in GraphQL schema? - java

I am building my GraphQL schema for my project and one of my models has a DateTime format.
How do I write out date formats on my GraphQL schema?
I tried DateTime or Date but nothing shows up.
This is the model:
public Integer Id;
public String name;
public String description;
public LocalDate birthDate;
This is what's in my GraphQL schema:
type Pet {
id: ID!
name: String!
description: String
birthDate: DateTime
}
But it says:
Unknown type DateTime

Create a custom scalar for your types that is not recognized by your framework.
I am not sure which graphql-java based framework you are using. I assume you are using the official Spring for GraphQL from Spring team.
Create a custom scalar, eg my LocalDateTime scalar.
public class LocalDateTimeScalar implements Coercing<LocalDateTime, String> {
#Override
public String serialize(Object dataFetcherResult) throws CoercingSerializeException {
if (dataFetcherResult instanceof LocalDateTime) {
return ((LocalDateTime) dataFetcherResult).format(DateTimeFormatter.ISO_DATE_TIME);
} else {
throw new CoercingSerializeException("Not a valid DateTime");
}
}
#Override
public LocalDateTime parseValue(Object input) throws CoercingParseValueException {
return LocalDateTime.parse(input.toString(), DateTimeFormatter.ISO_DATE_TIME);
}
#Override
public LocalDateTime parseLiteral(Object input) throws CoercingParseLiteralException {
if (input instanceof StringValue) {
return LocalDateTime.parse(((StringValue) input).getValue(), DateTimeFormatter.ISO_DATE_TIME);
}
throw new CoercingParseLiteralException("Value is not a valid ISO date time");
}
}
Register it in your custom RuntimeWiring bean, check here.
public class Scalars {
public static GraphQLScalarType localDateTimeType() {
return GraphQLScalarType.newScalar()
.name("LocalDateTime")
.description("LocalDateTime type")
.coercing(new LocalDateTimeScalar())
.build();
}
}
#Component
#RequiredArgsConstructor
public class PostsRuntimeWiring implements RuntimeWiringConfigurer {
private final DataFetchers dataFetchers;
#Override
public void configure(RuntimeWiring.Builder builder) {
builder
//...
.scalar(Scalars.localDateTimeType())
//...
.build();
}
}
If you are using Scalars in other graphql-java based frameworks(GraphQL Java, GraphQL Java Kickstart, GraphQL Kotlin, GraphQL SPQR, Netflix DGS etc) and spring integrations, check my Spring GraphQL Sample. The back-end principle is similar, just some different config.

Related

Convert specific fields before mapping in controller

I need to convert two specific fields before mapping to Dto class.
Data i get from form:
userName: test
password: 123
active: N
enabled: N
external: N
id: -1
Dto class:
#Getter #Setter
public class UserDto {
protected Integer id;
protected String userName;
protected String password;
protected boolean active = false;
protected boolean enabled = true;
protected String external;
}
Fields active and enabled are a boolean type, but from form i get "Y" or "N" string values. Cause of it, i got an error Failed to convert property value of type 'java.lang.String' to required type 'boolean' for property 'active'; nested exception is java.lang.IllegalArgumentException: Invalid boolean value [N]
I've read about the Сonverter<T,V> and PropertyEditors, but I'm not sure if they work for me and how can i apply it only for 2 specific fields.
Controller:
#RequestMapping(value = "/user/save",method = RequestMethod.POST)
public ResponseEntity<String> saveUser(#ModelAttribute UserDto userDto, BindingResult bindingResultM, HttpServletRequest req) {
List<FieldError> errors = bindingResultM.getFieldErrors();
for (FieldError error : errors) {
log.error("bindingResultM: " + error.getObjectName() + " - " + error.getDefaultMessage());
}
try {
userService.saveUserDto(userDto);
return new ResponseEntity<>("OK", HttpStatus.OK);
} catch (Exception e){
throw new ResponseException(e.getMessage(), HttpStatus.METHOD_FAILURE);
}
}
Because you are saying you need to get data from the form to populate the UserDTO object, which is a process of translate String type to other type.
So, you need to use "org.springframework.core.convert.converter.Converter", "java.beans.PropertyEditor", or "org.springframework.format.Formatter". All of three can do the job of translating String type to other type or other type to String type.
But PropertyEditor is too complicated, so usually we always use Converter or Formatter, and these two can be set in the same method "default void addFormatters(FormatterRegistry registry) {}" of the "WebMvcConfigurer" interface.
But that's for the whole application, you only want the translation happen for "active" and "enabled" fields of UserDTO.
Then you need to use the "org.springframework.web.bind.annotation.InitBinder", which can be defined inside the Controller for specific method parameter. The "userDTO" of the "#InitBinder" means the data binder is only used for the "userDTO" method parameter inside this Controller. (This name is case-sensitive, so if you "userDto" in the method, you need to change it to userDto in the #InitBinder annotation.)
Inside this method, you can specify the Formatter only for "active" and "enabled" fields, by using this method:
public void addCustomFormatter(Formatter<?> formatter, String... fields){}
Of course you can specify the Converter for these two fields, but there is no direct method for it in the "WebDataBinder" class. So, it is eaiser to use the Formatter.
#InitBinder("userDTO")
public void forUserDto(WebDataBinder binder) {
binder.addCustomFormatter(new BooleanFormatter(), "active", "enabled");
}
Here is the BooleanFormatter class:
public class BooleanFormatter implements Formatter<Boolean>{
#Override
public String print(Boolean object, Locale locale) {
if (object) {
return "Y";
}
return "N";
}
#Override
public Boolean parse(String text, Locale locale) throws ParseException {
if (text.equals("Y")) {
return true;
}
return false;
}
}
On your DTO you can annotate your boolean fields with :
#JsonDeserialize(
using = BooleanDeserializer.class
)
protected boolean enabled;
And create the deserializer :
public class BooleanDeserializer extends JsonDeserializer<Boolean> {
public Boolean deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String booleanValue = StringUtils.trimToEmpty(jsonParser.getText().trim());
return BooleanUtils.toBoolean(booleanValue, "Y", "N");
}
}

How to stop jackson timezone conversion and not apply local offset

In my java dto i am converting json date ("dispOn": "11/28/2020",) to java.util.Date.since i am not sending any timezone jackson is taking UTC as default time zone but when desterilizing it is taking the offset of CST(My local time)as a result, date in my code(11/27/2020) is one day behind
import java.util.Date;
#JsonFormat(pattern = "MM/dd/yyyy")
private Date dispOn;
How can i get the same date sending in json into my code
As Andreas mentioned in his comment, it is sufficient to change from Date to LocalDate.
For additional info, I used LocalDateTime to show you how the format should be configured. I'm also using Lombok's annotations on class-level for logging and getter/setter generation.
Keep in mind that LocalDate and LocalDateTime are only available since Java 8.
Below a complete example of a DTO in which I parse a JSON
#Slf4j
#Getter
#Setter
#JsonIgnoreProperties(ignoreUnknown = true) // optional annotation
public class MyMessage implements Deserializer<MyMessage> {
#JsonProperty("EVENT_TIMESTAMP")
#JsonFormat(shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
private LocalDateTime eventTimestamp;
#JsonProperty("MESSAGE")
private String message;
#Override
public void configure(Map<String, ?> configs, boolean isKey) {
Deserializer.super.configure(configs, isKey);
}
#Override
public MyMessage deserialize(String s, byte[] bytes) {
ObjectMapper mapper = new ObjectMapper();
MyMessage event = null;
try {
event = mapper
.registerModule(new JavaTimeModule())
.readValue(bytes, MyMessage.class);
} catch (Exception e) {
log.error("Something went wrong during the deserialization of the MyMessage: {}", e.getMessage());
}
return event;
}
#Override
public MyMessage deserialize(String message, Headers headers, byte[] data) {
return Deserializer.super.deserialize(message, headers, data);
}
#Override
public void close() {
Deserializer.super.close();
}
And the JSON I'm sending:
{
"EVENT_TIMESTAMP": "2022-03-15T14:14:05.945",
"MESSAGE": "Test 123"
}

Ideal way to pass date from angular datepicker to java backend?

I'm learning the basics of angular with a simple todo app. I have a simple spring boot backend which works fine. I'm currently struggling the best way to pass the date from a angular bootstrap datepicker to the backend. As the code is now the datatypes do not match.
Is the ideal way to convert it to seconds and convert the seconds back to a java date on the backend?
My ToDo entity:
#Id
#GeneratedValue
private long id;
private #NonNull
String taskName;
private Date dueDate;
private String extraNote;
private boolean taskCompleted;
Where I get the user input when he creates a new todo:
#Input() toDoData = { taskName: '', taskCompleted: false, extraNote: '', dueDate: Date};
addToDo() {
this.todoService.addToDo(this.toDoData).subscribe((result) => {
this.todoService.addToDo(this.toDoData);
});
}
Add todo part of my todoService:
addToDo(todo): Observable<any> {
console.log(todo);
return this.http.post<any>(this.API + 'todos', JSON.stringify(todo), this.httpOptions).pipe(
tap((todo) => console.log(`added todo w/ id=${todo.id}`)),
catchError(this.handleError<any>('addTodo'))
);
}
Thanks for any help!
EDIT (Added ToDoController):
#RestController
public class ToDoController {
private ToDoRepository repository;
public ToDoController(ToDoRepository repository) {
this.repository = repository;
}
#GetMapping("/todos")
List<ToDo> all() {
return repository.findAll();
}
#PostMapping("/todos")
ToDo newToDo(#RequestBody ToDo newToDo) {
return repository.save(newToDo);
}
#GetMapping("/todos/{id}")
ToDo one(#PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new ToDoNotFoundException(id));
}
#PutMapping("/todos/{id}")
ToDo replaceToDo(#RequestBody ToDo newToDo, #PathVariable Long id) {
return repository.findById(id)
.map(toDo -> {
toDo.setTaskName(newToDo.getTaskName());
toDo.setDueDate(newToDo.getDueDate());
toDo.setExtraNote(newToDo.getExtraNote());
toDo.setTaskCompleted(newToDo.getTaskCompleted());
return repository.save(toDo);
})
.orElseGet(() -> {
newToDo.setId(id);
return repository.save(newToDo);
});
}
#DeleteMapping("/todos/{id}")
void deleteToDo(#PathVariable Long id) {
repository.deleteById(id);
}
#GetMapping("/deleteall")
#CrossOrigin(origins = "http://localhost:4200")
public void deleteAll() {
repository.deleteAll();
}
#GetMapping("/init")
#CrossOrigin(origins = "http://localhots:4200")
public void createDefaults() {
Date date = new Date();
repository.save(new ToDo("PMB", date, false));
repository.save(new ToDo("GMDU", date, false));
repository.save(new ToDo("INMA", date, true));
repository.save(new ToDo("SLGP", date, false));
}
}
First of all. When using dates you should take into account summer/winter time issues and therefore I would suggest to use a LocalDate(Time) class.
That said:
I would create a
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper serializingObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// Mirror the default Spring Boot Jackson settings
objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateSerializer());
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateDeserializer());
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
public static class LocalDateSerializer extends JsonSerializer<LocalDateTime> {
#Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
}
public static class LocalDateDeserializer extends JsonDeserializer<LocalDateTime> {
#Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
}
}
}
that converts all dates/times etc to milliseconds and from milliseconds back to localDate(Time). You can find plenty examples on the internet.
This acts as a filter for all dates/times that pass the endpoints in your application.
That way you do not need to concern yourself with any conversion issues anymore.
Then you will need to implement a dateToMillisecond and visa versa routine in Angular and use it where you need to convert te dates. You could intercept http traffic and do the same, but that might be a bit more complicated to implement.
Sample
function timeFromMillis(millis) {
if (_.isEmpty(millis)) {
return undefined;
}
const momentTimeStamp = moment.unix(millis / 1000);
if (!momentTimeStamp.isValid()) {
return undefined;
}
return momentTimeStamp.toDate();
}
function timeToMillis(obj) {
if (!_.isEmpty(obj)) {
if (obj instanceof moment) {
return obj.valueOf();
} else if (obj instanceof Date) {
return obj.getTime();
else if (angular.isString(obj)) {
return parseDateString(obj).getTime();
} else {
return angular.undefined;
}
}
}
function parseDateString(dateString) {
if (angular.isDefined(dateString) && dateString) {
return moment(dateString, 'YYYY-MM-DD').toDate();
}
}
you have 2 options using timeStamp as long pasing long from angular to beckend and from back to angular This is the way i prefer because timestamp is unique
Or you can use custom serializer deserializer
public class CustomDateTimeSerializer extends StdSerializer<DateTime> {
private static DateTimeFormatter formatter =
DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");
public CustomDateTimeSerializer() {
this(null);
}
protected CustomDateTimeSerializer(Class<DateTime> t) {
super(t);
}
#Override
public void serialize(DateTime value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
gen.writeString(formatter.print(value));
}
public class CustomDateTimeDeserializer extends JsonDeserializer<DateTime> {
#Override
public DateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");
DateTime dateTime = formatter.parseDateTime(jsonParser.getText());
return dateTime;
}
}
ps: i have used yoda time in serializer and deserializer
I ran into this and it turned out to be timezone issue. From frontend it was sending json with "2019-04-01" but backend was converting it to "2019-03-31"
A lot of the code is not using java.time so I found adding below to application.properties file to be best solution. I also added global date format, but change timezone to what you need.
spring.jackson.date-format=yyyy-MM-dd
spring.jackson.time-zone=America/Chicago

JDBI: Inserting Dates as longs (milliseconds)

In my application I store dates as milliseconds
public class Model {
private long date = System.currentTimeMillis();
public void setDate(long date) {
this.date = date;
}
public long getDate() {
return date;
}
}
I have a JDBI Data Access Object which looks like:
public interface ModelDAO {
#SqlBatch("REPLACE INTO model (date) VALUES (:date)")
#BatchChunkSize(1000)
void insertModels(#BindBean List<Model> models);
#SqlQuery("SELECT * FROM model ORDER BY date DESC")
List<Model> getModels();
}
However when I try to insert I get:
org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException:
java.sql.BatchUpdateException: Data truncation: Incorrect datetime
value: '1430262000000' for column 'date'
Is there a way I can tell JDBI how to convert this without requiring something like the below for all my classes with dates in?
#BindingAnnotation(BindModel.ModelBindingFactor.class)
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.PARAMETER})
public #interface BindModel {
public static class ModelBindingFactor implements BinderFactory {
public Binder build(Annotation annotation) {
return new Binder<BindModel, Model>() {
public void bind(SQLStatement q, BindModel bind, Model model) {
q.bind("date", new Timestamp(model.getDate()));
}
};
}
}
}
I would be willing to switch my models to use a DateTime object if it makes things cleaner.
It will be easier and more cleaner if you can use DateTime to store date values. You should have a argumentFactory to convert DateTime to sql date. You can use the following one.
public class DateTimeAsSqlDateArgument implements ArgumentFactory<DateTime> {
#Override
public boolean accepts(Class<?> expectedType, Object value, StatementContext ctx) {
return value != null && DateTime.class.isAssignableFrom(value.getClass());
}
#Override
public Argument build(Class<?> expectedType, final DateTime value, StatementContext ctx) {
return new Argument() {
#Override
public void apply(int position, PreparedStatement statement, StatementContext ctx) throws SQLException {
statement.setTimestamp(position, new Timestamp(value.getMillis()));
}
};
}
}
Register this argument factory to dbi. That's all you need. JDBI will use this factory whenever it see DateTime object.
dbi.registerArgumentFactory(new DateTimeAsSqlDateArgument());

Jackson JsonDeserialize not being called for #QueryParam

I have mapped a custom deserializer to convert Strings on dd/MM/yyyy pattern to LocalDate so I can call my services with a more readable signature..
This is my dto class that is used as a Jersey #BeanParam to transport data between layers:
public class ProdutoFilterDto implements FilterDto {
private static final long serialVersionUID = -4998167328470565406L;
#QueryParam("dataInicial")
#JsonDeserialize(using = CustomLocalDateDeserializer.class)
private LocalDate dataInicial;
#QueryParam("dataInicial")
#JsonDeserialize(using = CustomLocalDateDeserializer.class)
private LocalDate dataFinal;
public LocalDate getDataInicial() {
return dataInicial;
}
public void setDataInicial(LocalDate dataInicial) {
this.dataInicial = dataInicial;
}
public LocalDate getDataFinal() {
return dataFinal;
}
public void setDataFinal(LocalDate dataFinal) {
this.dataFinal = dataFinal;
}
}
and this is my custom deserializer:
public class CustomLocalDateDeserializer extends JsonDeserializer<LocalDate> {
#Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
final String data = p.getValueAsString();
return (LocalDate) formatter.parse(data);
}
}
Its being used on this jersey service:
#Path("produto")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class ProdutoService {
...
#GET
#Path("query")
#Override
public Response query(
#QueryParam("offset") #DefaultValue(value = "0") Integer offSet,
#QueryParam("limit") #DefaultValue(value = "10") Integer limit,
#BeanParam ProdutoFilterDto filter) { ... }
...
}
I am calling like this:
${host goes here}/produto/query?dataInicial=11/09/1992
The problem is that the deserializer method is never called and the bean param variable remains null..
MessageBodyReaders aren't used for #QueryParam. You seem to be expecting the Jackson MessageBodyReader to handle this deserialization, but it doesn't work like that.
Instead you will want to use a ParamConverter, which will need to be registered through a ParamConverterProvider. For example:
#Provider
public class LocalDateParamConverterProvider implements ParamConverterProvider {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
#Override
public <T> ParamConverter<T> getConverter(
Class<T> rawType, Type genericType, Annotation[] antns) {
if (LocalDate.class == rawType) {
return new ParamConverter<T>() {
#Override
public T fromString(String string) {
try {
LocalDate localDate = LocalDate.parse(string, formatter);
return rawType.cast(localDate);
} catch (Exception ex) {
throw new BadRequestException(ex);
}
}
#Override
public String toString(T t) {
LocalDate localDate = (LocalDate) t;
return formatter.format(localDate);
}
};
}
return null;
}
}
Now LocalDate will work with #QueryParam and other #XxxParams also.
Some things to note:
If your goal is to parse both your #XxxParams and your JSON body into a bean this will not work. I'm not sure how that would work, but I'm sure it would involve a lot of hacking, and I wouldn't recommend it.
Your cast to (LocalDate) won't work. It's an illegal cast to java.time.format.Parsed. See correct way in code example.
Related to the above point. I was pulling out my hair for a good hour trying to figure out why I was getting a 404, using your parse code. With a 404, the last place I thought to look was in the ParamConverter. But it seems any uncaught exceptions that are thrown in the ParamConverter, will cause a 404. Doesn't make much sense right? The head pounding led me to this, which led me to this, which seems to be a poor specification
"if the field or property is annotated with
#MatrixParam, #QueryParam or #PathParam then an implementation MUST generate an instance of
NotFoundException (404 status) that wraps the thrown exception and no entity
"
Moral of the story: make sure to catch any possible exceptions in the ParamConverter!
See Also:
Good article on ParamConverters

Categories