Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright 2020 interactive instruments GmbH
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package de.ii.xtraplatform.values.api;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.JacksonYAMLParseException;
import com.google.common.hash.Hashing;
import de.ii.xtraplatform.values.domain.Identifier;
import de.ii.xtraplatform.values.domain.ValueDecoderMiddleware;
import de.ii.xtraplatform.values.domain.ValueEncoding.FORMAT;
import io.dropwizard.util.DataSize;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;

class JacksonHelper<T> {

private static final byte[] JSON_NULL = "null".getBytes();
private static final byte[] YAML_NULL = "--- null\n".getBytes();
private static final Pattern JSON_EMPTY = Pattern.compile("(\\s)*");
private static final Pattern YAML_EMPTY = Pattern.compile("---(\\s)*");

private final List<ValueDecoderMiddleware<byte[]>> decoderPreProcessor;
private final List<ValueDecoderMiddleware<T>> decoderMiddleware;
private final DataSize maxYamlFileSize;
private final Function<FORMAT, ObjectMapper> mapperProvider;

JacksonHelper(
List<ValueDecoderMiddleware<byte[]>> decoderPreProcessor,
List<ValueDecoderMiddleware<T>> decoderMiddleware,
DataSize maxYamlFileSize,
Function<FORMAT, ObjectMapper> mapperProvider) {
this.decoderPreProcessor = decoderPreProcessor;
this.decoderMiddleware = decoderMiddleware;
this.maxYamlFileSize = maxYamlFileSize;
this.mapperProvider = mapperProvider;
}

// Serialization methods
byte[] serialize(Object data, FORMAT format) {
try {
return mapperProvider.apply(format).writeValueAsBytes(data);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Unexpected serialization error", e);
}
}

@SuppressWarnings("UnstableApiUsage")
String hash(Object data) {
byte[] bytes = serialize(data, FORMAT.SMILE);
return Hashing.murmur3_128().hashBytes(bytes).toString();
}

// Deserialization methods

T deserialize(Identifier identifier, byte[] payload, FORMAT format, boolean ignoreCache)
throws IOException {
if (isNull(payload)) {
return null;
}
if (Objects.equals(format, FORMAT.NONE)) {
throw new IllegalStateException("No format given");
}

// Preprocess payload
byte[] rawData = payload;
ObjectMapper objectMapper = mapperProvider.apply(format);
for (ValueDecoderMiddleware<byte[]> preProcessor : decoderPreProcessor) {
rawData = preProcessor.process(identifier, rawData, objectMapper, null, ignoreCache);
}

return processMiddleware(identifier, rawData, objectMapper, ignoreCache);
}

boolean isEmpty(byte[] payload) {
if (isNull(payload)) {
return true;
}
String payloadString = new String(payload, StandardCharsets.UTF_8);
return JSON_EMPTY.matcher(payloadString).matches()
|| YAML_EMPTY.matcher(payloadString).matches();
}

boolean isNull(byte[] payload) {
return Arrays.equals(payload, JSON_NULL) || Arrays.equals(payload, YAML_NULL);
}

private T processMiddleware(
Identifier identifier, byte[] rawData, ObjectMapper objectMapper, boolean ignoreCache)
throws IOException {
T data = null;

try {
for (ValueDecoderMiddleware<T> middleware : decoderMiddleware) {
data = middleware.process(identifier, rawData, objectMapper, data, ignoreCache);
}
} catch (JacksonYAMLParseException e) {
if (Objects.nonNull(e.getMessage())
&& e.getMessage().contains("incoming YAML document exceeds the limit")) {
throw new IOException(
String.format(
"Maximum YAML file size of %s exceeded, increase 'store.maxYamlFileSize' to fix.",
maxYamlFileSize),
e);
}
data = tryRecovery(identifier, rawData, objectMapper, e);
} catch (Throwable e) {
data = tryRecovery(identifier, rawData, objectMapper, e);
}

return data;
}

private T tryRecovery(
Identifier identifier, byte[] rawData, ObjectMapper objectMapper, Throwable originalException)
throws IOException {
Optional<ValueDecoderMiddleware<T>> recovery =
decoderMiddleware.stream().filter(ValueDecoderMiddleware::canRecover).findFirst();

if (recovery.isPresent()) {
try {
return recovery.get().recover(identifier, rawData, objectMapper);
} catch (Throwable recoveryException) {
originalException.addSuppressed(recoveryException);
}
}

rethrowOriginalException(originalException);
return null; // unreachable
}

private void rethrowOriginalException(Throwable originalException) throws IOException {
if (originalException instanceof IOException) {
throw (IOException) originalException;
}
if (originalException instanceof RuntimeException) {
throw (RuntimeException) originalException;
}
if (originalException instanceof Error) {
throw (Error) originalException;
}
throw new IOException("Deserialization failed", originalException);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public ValueDecoderBase(Function<Identifier, T> newInstanceSupplier, ValueCache<
public T process(
Identifier identifier, byte[] payload, ObjectMapper objectMapper, T data, boolean ignoreCache)
throws IOException {
T data2 = null;
T data2;

if (valueCache.has(identifier) && !ignoreCache) {
data2 = valueCache.get(identifier);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,13 @@
import de.ii.xtraplatform.values.domain.ValueDecoderMiddleware;
import java.io.IOException;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ValueDecoderPreHash<T extends StoredValue> implements ValueDecoderMiddleware<T> {

private static final Logger LOGGER = LoggerFactory.getLogger(ValueDecoderPreHash.class);

private final Function<Identifier, ValueBuilder<T>> newBuilderSupplier;
private final Function<T, String> hasher;

// TODO: shouldPreHash from Factory
// NOPMD - TODO: shouldPreHash from Factory
public ValueDecoderPreHash(
Function<Identifier, ValueBuilder<T>> newBuilderSupplier, Function<T, String> hasher) {
this.newBuilderSupplier = newBuilderSupplier;
Expand All @@ -43,8 +39,6 @@ public T process(

builder.stableHash(hash);

// LOGGER.debug("PROC {} {}", identifier, hash);

return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,55 +10,41 @@
import static de.ii.xtraplatform.base.domain.util.JacksonModules.DESERIALIZE_IMMUTABLE_BUILDER_NESTED;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.fasterxml.jackson.dataformat.yaml.JacksonYAMLParseException;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing;
import de.ii.xtraplatform.base.domain.Jackson;
import de.ii.xtraplatform.values.domain.Identifier;
import de.ii.xtraplatform.values.domain.ValueDecoderMiddleware;
import de.ii.xtraplatform.values.domain.ValueEncoding;
import io.dropwizard.util.DataSize;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.LoaderOptions;

public class ValueEncodingJackson<T> implements ValueEncoding<T> {

private static final Logger LOGGER = LoggerFactory.getLogger(ValueEncodingJackson.class);

public static final byte[] JSON_NULL = "null".getBytes();
public static final byte[] YAML_NULL = "--- null\n".getBytes();
private static final Pattern JSON_EMPTY = Pattern.compile("(\\s)*");
private static final Pattern YAML_EMPTY = Pattern.compile("---(\\s)*");

private static final FORMAT DEFAULT_FORMAT = FORMAT.YML;

private final Map<FORMAT, ObjectMapper> mappers;
private final List<ValueDecoderMiddleware<byte[]>> decoderPreProcessor;
private final List<ValueDecoderMiddleware<T>> decoderMiddleware;
private final DataSize maxYamlFileSize;
private final JacksonHelper<T> jacksonHelper;

public ValueEncodingJackson(
Jackson jackson, DataSize maxYamlFileSize, boolean failOnUnknownProperties) {
this.maxYamlFileSize = Objects.requireNonNullElse(maxYamlFileSize, DataSize.megabytes(3));
DataSize resolvedMaxYamlFileSize =
Objects.requireNonNullElse(maxYamlFileSize, DataSize.megabytes(3));

LoaderOptions loaderOptions = new LoaderOptions();
loaderOptions.setCodePointLimit(Math.toIntExact(this.maxYamlFileSize.toBytes()));
loaderOptions.setCodePointLimit(Math.toIntExact(resolvedMaxYamlFileSize.toBytes()));

ObjectMapper jsonMapper =
jackson
Expand Down Expand Up @@ -107,6 +93,9 @@ public ValueEncodingJackson(

this.decoderMiddleware = new ArrayList<>();
this.decoderPreProcessor = new ArrayList<>();
this.jacksonHelper =
new JacksonHelper<>(
decoderPreProcessor, decoderMiddleware, resolvedMaxYamlFileSize, this::getMapper);
}

public void addDecoderPreProcessor(ValueDecoderMiddleware<byte[]> preProcessor) {
Expand All @@ -124,109 +113,37 @@ public final FORMAT getDefaultFormat() {

@Override
public final byte[] serialize(T data) {
try {
return getDefaultMapper().writeValueAsBytes(data);
} catch (JsonProcessingException e) {
// should never happen
throw new IllegalStateException("Unexpected serialization error", e);
}
return serialize(data, DEFAULT_FORMAT);
}

@Override
public final byte[] serialize(T data, FORMAT format) {
try {
return getMapper(format).writeValueAsBytes(data);
} catch (JsonProcessingException e) {
// should never happen
throw new IllegalStateException("Unexpected serialization error", e);
}
}

@Override
public byte[] serialize(Map<String, Object> data) {
try {
return getDefaultMapper().writeValueAsBytes(data);
} catch (JsonProcessingException e) {
// should never happen
throw new IllegalStateException("Unexpected serialization error", e);
}
return jacksonHelper.serialize(data, format);
}

@Override
public final T deserialize(
Identifier identifier, byte[] payload, FORMAT format, boolean ignoreCache)
throws IOException {
// "null" as payload means delete
if (isNull(payload)) {
return null;
}
if (Objects.equals(format, FORMAT.NONE)) {
throw new IllegalStateException("No format given");
}

byte[] rawData = payload;
ObjectMapper objectMapper = getMapper(format);
T data = null;

for (ValueDecoderMiddleware<byte[]> preProcessor : decoderPreProcessor) {
rawData = preProcessor.process(identifier, rawData, objectMapper, null, ignoreCache);
}

try {
for (ValueDecoderMiddleware<T> middleware : decoderMiddleware) {
data = middleware.process(identifier, rawData, objectMapper, data, ignoreCache);
}

} catch (Throwable e) {
if (e instanceof JacksonYAMLParseException
&& Objects.nonNull(e.getMessage())
&& e.getMessage().contains("incoming YAML document exceeds the limit")) {
throw new IOException(
String.format(
"Maximum YAML file size of %s exceeded, increase 'store.maxYamlFileSize' to fix.",
maxYamlFileSize));
}

Optional<ValueDecoderMiddleware<T>> recovery =
decoderMiddleware.stream().filter(ValueDecoderMiddleware::canRecover).findFirst();
if (recovery.isPresent()) {
try {
data = recovery.get().recover(identifier, rawData, objectMapper);
} catch (Throwable e2) {
throw e;
}
} else {
throw e;
}
}

return data;
}

final ObjectMapper getDefaultMapper() {
return getMapper(DEFAULT_FORMAT);
return jacksonHelper.deserialize(identifier, payload, format, ignoreCache);
}

@Override
public final ObjectMapper getMapper(FORMAT format) {
return mappers.get(format);
}

final boolean isNull(byte[] payload) {
return Arrays.equals(payload, JSON_NULL) || Arrays.equals(payload, YAML_NULL);
public final boolean isEmpty(byte[] payload) {
return jacksonHelper.isEmpty(payload);
}

public final boolean isEmpty(byte[] payload) {
String payloadString = new String(payload, StandardCharsets.UTF_8);
return JSON_EMPTY.matcher(payloadString).matches()
|| YAML_EMPTY.matcher(payloadString).matches();
@Override
public byte[] serialize(Map<String, Object> data) {
return jacksonHelper.serialize(data, DEFAULT_FORMAT);
}

@Override
@SuppressWarnings("UnstableApiUsage")
public final String hash(T data) {
byte[] bytes = serialize(data, FORMAT.SMILE);

return Hashing.murmur3_128().hashBytes(bytes).toString();
return jacksonHelper.hash(data);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ public void init(AppConfiguration configuration, Environment environment) {
// codelists/foo,maplibre-styles/bar
@Override
public void execute(Map<String, List<String>> parameters, PrintWriter output) throws Exception {
if (LOGGER.isTraceEnabled()) LOGGER.trace("Reload request: {}", parameters);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Reload request: {}", parameters);
}

List<Path> includes = getPaths(parameters).stream().map(Path::of).collect(Collectors.toList());

Expand Down
Loading