highlight.js

Tuesday, July 5, 2022

Serialize and Deserialize PageImpl in Jackson

It's very common to put paginated query results in Redis via Jackson serialization and deserialization. However, org.springframework.data.domain.PageImpl doesn't expose a default constructor. Even worse, that applies to org.springframework.data.domain.Sort and org.springframework.data.domain.Sort.Order as well. There are a couple of workarounds. I'd like to introduce a less intrusive approach for your reference.

The idea is to write a Jackson module which injects a custom serializer and deserializer for org.springframework.data.domain.PageImpl. Let's see how to do that.

1. Define an interface.

public interface IJPADataPage {
String _CLASS = "@class";
String CONTENT = "content";
String DIRECTION = "direction";
String IGNORE_CASE = "ignoreCase";
String NULL_HANDLING = "nullHandling";
String NUMBER = "number";
String PROPERTY = "property";
String SIZE = "size";
String SORT = "sort";
String TOTAL_ELEMENTS = "totalElements";
}

2. Define a Serializer.

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Sort;

import java.io.IOException;

@SuppressWarnings("unchecked")
public class JPADataPageSerializer extends StdSerializer<PageImpl> implements IJPADataPage {
public JPADataPageSerializer(Class<PageImpl> t) {
super(t);
}

public JPADataPageSerializer() {
this(null);
}

@Override
public void serialize(
PageImpl value,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField(_CLASS, PageImpl.class.getName());
jsonGenerator.writePOJOField(CONTENT, value.getContent());
jsonGenerator.writeNumberField(NUMBER, value.getNumber());
jsonGenerator.writeNumberField(SIZE, value.getSize());
jsonGenerator.writeNumberField(TOTAL_ELEMENTS, value.getTotalElements());
jsonGenerator.writeArrayFieldStart(SORT);
if (!value.getSort().isEmpty()) {
for (Sort.Order order : value.getSort()) {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField(PROPERTY, order.getProperty());
jsonGenerator.writeStringField(DIRECTION, order.getDirection().name());
jsonGenerator.writeBooleanField(IGNORE_CASE, order.isIgnoreCase());
jsonGenerator.writeStringField(NULL_HANDLING, order.getNullHandling().name());
jsonGenerator.writeEndObject();
}
}
jsonGenerator.writeEndArray();
jsonGenerator.writeEndObject();
}

@Override
public void serializeWithType(
PageImpl value,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider,
TypeSerializer typeSer) throws IOException {
serialize(value, jsonGenerator, serializerProvider);
}
}

3. Define a Deserializer.

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@SuppressWarnings("unchecked")
public class JPADataPageDeserializer extends StdDeserializer<PageImpl> implements IJPADataPage {
public JPADataPageDeserializer(Class<?> type) {
super(type);
}

public JPADataPageDeserializer() {
this(null);
}

@Override
public PageImpl deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
List<?> content = deserializationContext.readTreeAsValue(jsonNode.get(CONTENT), List.class);
int number = jsonNode.get(NUMBER).asInt(0);
int size = jsonNode.get(SIZE).asInt(1);
int totalElements = jsonNode.get(TOTAL_ELEMENTS).asInt(0);
List<Sort.Order> orders = new ArrayList<>();
ArrayNode arrayNode = (ArrayNode) jsonNode.get(SORT);
if (!arrayNode.isEmpty()) {
for (JsonNode jsonNodeOrder : arrayNode) {
String property = jsonNodeOrder.get(PROPERTY).asText();
Sort.Direction direction = Sort.Direction.valueOf(jsonNodeOrder.get(DIRECTION).asText());
boolean ignoreCase = jsonNodeOrder.get(IGNORE_CASE).asBoolean();
Sort.NullHandling nullHandling = Sort.NullHandling.valueOf(jsonNodeOrder.get(NULL_HANDLING).asText());
Sort.Order order = new Sort.Order(direction, property, nullHandling);
if (ignoreCase) {
order = order.ignoreCase();
}
orders.add(order);
}
}
PageRequest pageRequest = PageRequest.of(number, size, Sort.by(orders));
return new PageImpl(content, pageRequest, totalElements);
}

@Override
public Object deserializeWithType(
JsonParser jsonParser,
DeserializationContext deserializationContext,
TypeDeserializer typeDeserializer) throws IOException {
return deserialize(jsonParser, deserializationContext);
}
}

4. Define a module.

import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.data.domain.PageImpl;

public class JPADataModule extends SimpleModule {
public static final String NAME = "JPA Data Module";
public static final Version VERSION = new Version(0, 1, 0, null, null, null);

public JPADataModule() {
super(NAME, VERSION);
addSerializer(PageImpl.class, new JPADataPageSerializer());
addDeserializer(PageImpl.class, new JPADataPageDeserializer());
}
}

5. Register the module.

objectMapper.registerModule(new JPADataModule());

Now Jackson is able to handle PageImpl and the Redis cache works.

Saturday, April 30, 2022

IDEA / HandBrake / WSL Port Conflict

Recently I upgraded HandBrake to the latest version, however, it stopped working. The root cause is TCP port conflict. As usual, I ran net stop winnat && net start winnat and it worked. But, that broke WSL network.

I didn't want to reset the winnat every time I started Windows with a broken WSL. Finally, I found the root cause: Hyper-V reserves huge amount of TCP ports after Windows is up. The fix is to tell Hyper-V to avoid those commonly used TCP ports.

The following command can show you the excluded TCP port ranges.

> netsh int ipv4 show excludedportrange protocol=tcp

Protocol tcp Port Exclusion Ranges

Start Port    End Port
----------    --------
        80          80
      1000        1010
      1020        1030
      ......

As you can see, huge amount of TCP ports are reserved by Hyper-V. So, let's tell Hyper-V not to be that greedy by executing the following command.

> netsh int ipv4 set dynamic tcp start=49152 num=16384

That command tells Windows to allow dynamic TCP ports from 49152 so that Hyper-V is only able to reserve TCP ports starting from 49152. The following command can verify the setting is correct.

> netsh int ipv4 show dynamic protocol=tcp

Protocol tcp Dynamic Port Range
---------------------------------
Start Port      : 49152
Number of Ports : 16384

Once the new dynamic TCP port range is set, just reboot your machine and everything goes back to normal. IDEA can start smoothly, WSL network is always on, HandBrake works all the time.

Tuesday, January 11, 2022

dlopen failed: cannot locate symbol "__aarch64_ldadd4_relax"

In Android development, it's rare to meet the following error.

dlopen failed: cannot locate symbol "__aarch64_ldadd4_relax"

I searched the whole internet for a solution, but couldn't get a practical one. Actually, the root cause is simple: The Android NDK is too old.

The solution in my case is:

  1. Upgrade CMake to the latest version.
  2. Upgrade Android NDK to the latest version.

Wednesday, November 3, 2021

Monday, October 25, 2021

Javet for Android is Released

Javet is Java + V8 (JAVa + V + EighT). It is an awesome way of embedding Node.js and V8 in Java.

It's been more than half a year for the Javet users to wait for the Android support. Now, Javet has officially supported Android.

The API and coding experience are identical to the ones on Linux, Mac OS and Windows.




Friday, October 22, 2021

Javet for Android is on the Way

Javet is Java + V8 (JAVa + V + EighT). It is an awesome way of embedding Node.js and V8 in Java.

It's been a long while for many Javet users to wait for the Android support. Now, it is coming true as the first Android build is being tested. If you are interested, please join us at discord.



Friday, September 10, 2021

MacBook Air mid-2012 from Lion to Catalina

I had been asked by potential Javet users for Mac OS release for many moths before the first Mac OS release was published. I have to admit I have no plan to purchase Mac OS devices in the new future for financial reason.

Luckily, I have a MacBook Air mid-2012 resting in the dust. I revived it and would like to upgrade it to the latest Mac OS.


The problem was I had to upgrade it to Lion, then to El Capitan, Mojave, eventually to Catalina which is the last version supported by MacBook Air mid-2012.


It took ~8 hours to get there. Then, I installed xcode and related tools. Luckily, it met the lowest requirement for building V8 v9.2.

Obviously, MacBook Air mid-2012 is tooooo slow in building modern applications. It took ~4 hours to build V8, and another ~4 hours to build Node.js. The result was good as Javet for Mac OS x86_64 was built successfully.

However, MacBook Air mid-2012 will soon been retired by a new release of V8. When that day comes, I will no longer release Mac OS x86_64 version unless I get enough donation for me to purchase a new device.

Serialize and Deserialize PageImpl in Jackson

It's very common to put paginated query results in Redis via Jackson serialization and deserialization. However, org.springframework.dat...