package org.openmhealth.shim.common.mapper; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Splitter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Optional; import java.util.function.Function; import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; import static java.util.Optional.empty; /** * A set of utility methods to help with mapping {@link JsonNode} objects. * * @author Emerson Farrugia */ public class JsonNodeMappingSupport { private static final Logger logger = LoggerFactory.getLogger(JsonNodeMappingSupport.class); /** * @param parentNode a parent node * @param path a path to a child node * @return the child node reached by traversing the path, where dots denote nested nodes * @throws MissingJsonNodeMappingException if the child node doesn't exist */ public static JsonNode asRequiredNode(JsonNode parentNode, String path) { Iterable<String> pathSegments = Splitter.on(".").split(path); JsonNode node = parentNode; for (String pathSegment : pathSegments) { if (!node.hasNonNull(pathSegment)) { throw new MissingJsonNodeMappingException(node, pathSegment); } node = node.path(pathSegment); } return node; } /** * @param parentNode a parent node * @param path the path to a child node * @param typeChecker the function to check if the type is compatible * @param converter the function to convert the node to a value * @param <T> the type of the value to convert to * @return the value of the child node * @throws MissingJsonNodeMappingException if the child doesn't exist * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't compatible */ public static <T> T asRequiredValue(JsonNode parentNode, String path, Function<JsonNode, Boolean> typeChecker, Function<JsonNode, T> converter, Class<T> targetType) { JsonNode childNode = asRequiredNode(parentNode, path); if (!typeChecker.apply(childNode)) { throw new IncompatibleJsonNodeMappingException(parentNode, path, targetType); } return converter.apply(childNode); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a string * @throws MissingJsonNodeMappingException if the child doesn't exist * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't textual */ public static String asRequiredString(JsonNode parentNode, String path) { return asRequiredValue(parentNode, path, JsonNode::isTextual, JsonNode::textValue, String.class); } // TODO add tests /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a boolean * @throws MissingJsonNodeMappingException if the child doesn't exist * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't a boolean */ public static Boolean asRequiredBoolean(JsonNode parentNode, String path) { return asRequiredValue(parentNode, path, JsonNode::isBoolean, JsonNode::booleanValue, Boolean.class); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a long * @throws MissingJsonNodeMappingException if the child doesn't exist * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't an integer */ public static Long asRequiredLong(JsonNode parentNode, String path) { return asRequiredValue(parentNode, path, JsonNode::isIntegralNumber, JsonNode::longValue, Long.class); } // TODO add tests /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as an integer * @throws MissingJsonNodeMappingException if the child doesn't exist * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't an integer */ public static Integer asRequiredInteger(JsonNode parentNode, String path) { return asRequiredValue(parentNode, path, JsonNode::isIntegralNumber, JsonNode::intValue, Integer.class); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a double * @throws MissingJsonNodeMappingException if the child doesn't exist * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't numeric */ public static Double asRequiredDouble(JsonNode parentNode, String path) { return asRequiredValue(parentNode, path, JsonNode::isNumber, JsonNode::doubleValue, Double.class); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a BigDecimal * @throws MissingJsonNodeMappingException if the child doesn't exist * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't numeric */ public static BigDecimal asRequiredBigDecimal(JsonNode parentNode, String path) { return asRequiredValue(parentNode, path, JsonNode::isNumber, JsonNode::decimalValue, BigDecimal.class); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a {@link LocalDate} * @throws MissingJsonNodeMappingException if the child doesn't exist * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't a date */ // TODO overload with a DateTimeFormatter parameter public static LocalDate asRequiredLocalDate(JsonNode parentNode, String path) { String string = asRequiredString(parentNode, path); try { return LocalDate.parse(string); } catch (DateTimeParseException e) { throw new IncompatibleJsonNodeMappingException(parentNode, path, LocalDate.class, e); } } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as an {@link OffsetDateTime} * @throws MissingJsonNodeMappingException if the child doesn't exist * @throws IncompatibleJsonNodeMappingException if the value of the child node isn't a date time */ public static OffsetDateTime asRequiredOffsetDateTime(JsonNode parentNode, String path) { String string = asRequiredString(parentNode, path); try { return OffsetDateTime.parse(string); } catch (DateTimeParseException e) { throw new IncompatibleJsonNodeMappingException(parentNode, path, OffsetDateTime.class, e); } } /** * @param parentNode a parent node * @param path a path to a child node, where dots denote nested nodes * @return the child node reached by traversing the path, or an empty optional if the child doesn't exist */ public static Optional<JsonNode> asOptionalNode(final JsonNode parentNode, final String path) { Iterable<String> pathSegments = Splitter.on(".").split(path); JsonNode node = parentNode; for (String pathSegment : pathSegments) { JsonNode childNode = node.path(pathSegment); if (childNode.isMissingNode()) { logger.debug("A '{}' field wasn't found in node '{}'.", pathSegment, node); return empty(); } if (childNode.isNull()) { logger.debug("The '{}' field is null in node '{}'.", pathSegment, node); return empty(); } node = childNode; } return Optional.of(node); } /** * @param parentNode a parent node * @param path the path to a child node * @param typeChecker the function to check if the type is compatible * @param converter the function to convert the node to a value * @param <T> the type of the value to convert to * @return the value of the child node, or an empty optional if the child doesn't exist or if the * value of the child node isn't compatible */ public static <T> Optional<T> asOptionalValue(JsonNode parentNode, String path, Function<JsonNode, Boolean> typeChecker, Function<JsonNode, T> converter) { JsonNode childNode = asOptionalNode(parentNode, path).orElse(null); if (childNode == null) { return empty(); } if (!typeChecker.apply(childNode)) { logger.warn("The '{}' field in node '{}' isn't compatible.", path, parentNode); return empty(); } return Optional.of(converter.apply(childNode)); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a string, or an empty optional if the child doesn't exist or if the * value of the child node isn't textual */ public static Optional<String> asOptionalString(JsonNode parentNode, String path) { return asOptionalValue(parentNode, path, JsonNode::isTextual, JsonNode::textValue); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a boolean, or an empty optional if the child doesn't exist or if the * value of the child node isn't boolean */ public static Optional<Boolean> asOptionalBoolean(JsonNode parentNode, String path) { return asOptionalValue(parentNode, path, JsonNode::isBoolean, JsonNode::booleanValue); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a date time, or an empty optional if the child doesn't exist or if the * value of the child node isn't a date time */ public static Optional<OffsetDateTime> asOptionalOffsetDateTime(JsonNode parentNode, String path) { Optional<String> string = asOptionalString(parentNode, path); if (!string.isPresent()) { return empty(); } OffsetDateTime dateTime = null; try { dateTime = OffsetDateTime.parse(string.get()); } catch (DateTimeParseException e) { logger.warn("The '{}' field in node '{}' with value '{}' isn't a valid timestamp.", path, parentNode, string.get(), e); } return Optional.ofNullable(dateTime); } /** * @param parentNode a parent node * @param path the path to a child node * @param formatter the formatter to use to parse the value of the child node * @return the value of the child node as a date time, or an empty optional if the child doesn't exist or if the * value of the child node isn't a date time */ public static Optional<LocalDateTime> asOptionalLocalDateTime(JsonNode parentNode, String path, DateTimeFormatter formatter) { Optional<String> string = asOptionalString(parentNode, path); if (!string.isPresent()) { return empty(); } LocalDateTime dateTime = null; try { dateTime = LocalDateTime.parse(string.get(), formatter); } catch (DateTimeParseException e) { logger.warn("The '{}' field in node '{}' with value '{}' isn't a valid timestamp.", path, parentNode, string.get(), e); } return Optional.ofNullable(dateTime); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a date time, or an empty optional if the child doesn't exist or if the * value of the child node isn't a date time matching {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} */ public static Optional<LocalDateTime> asOptionalLocalDateTime(JsonNode parentNode, String path) { return asOptionalLocalDateTime(parentNode, path, ISO_LOCAL_DATE_TIME); } // TODO refactor this by delegating to existing methods, then add tests public static Optional<LocalDateTime> asOptionalLocalDateTime(JsonNode parentNode, String pathToDate, String pathToTime) { Optional<String> time = asOptionalString(parentNode, pathToTime); Optional<String> date = asOptionalString(parentNode, pathToDate); if (!time.isPresent() || !date.isPresent()) { return empty(); } LocalDateTime dateTime = null; try { dateTime = LocalDateTime.parse(date.get() + "T" + time.get(), ISO_LOCAL_DATE_TIME); } catch (DateTimeParseException e) { logger.warn( "The '{}' and '{}' fields in node '{}' with values '{}' and '{}' do not make-up a valid timestamp.", pathToDate, pathToTime, parentNode, date.get(), time.get(), e); } return Optional.ofNullable(dateTime); } // TODO add Javadoc and tests public static Optional<LocalDate> asOptionalLocalDate(JsonNode parentNode, String path) { return asOptionalLocalDate(parentNode, path, DateTimeFormatter.ISO_LOCAL_DATE); } // TODO add Javadoc and tests public static Optional<LocalDate> asOptionalLocalDate(JsonNode parentNode, String path, DateTimeFormatter dateFormat) { Optional<String> string = asOptionalString(parentNode, path); if (!string.isPresent()) { return empty(); } LocalDate localDate = null; try { localDate = LocalDate.parse(string.get(), dateFormat); } catch (DateTimeParseException e) { logger.warn("The '{}' field in node '{}' with value '{}' isn't a valid date.", path, parentNode, string.get(), e); } return Optional.ofNullable(localDate); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a double, or an empty optional if the child doesn't exist or if the * value of the child node isn't numeric */ public static Optional<Double> asOptionalDouble(JsonNode parentNode, String path) { return asOptionalValue(parentNode, path, JsonNode::isNumber, JsonNode::doubleValue); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a long, or an empty optional if the child doesn't exist or if the * value of the child node isn't an integer */ public static Optional<Long> asOptionalLong(JsonNode parentNode, String path) { return asOptionalValue(parentNode, path, JsonNode::isIntegralNumber, JsonNode::longValue); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as an integer, or an empty optional if the child doesn't exist or if the * value of the child node isn't an integer */ public static Optional<Integer> asOptionalInteger(JsonNode parentNode, String path) { return asOptionalValue(parentNode, path, JsonNode::isIntegralNumber, JsonNode::intValue); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a Big Decimal, or an empty optional if the child doesn't exist or if the * value of the child node isn't numeric */ public static Optional<BigDecimal> asOptionalBigDecimal(JsonNode parentNode, String path) { return asOptionalValue(parentNode, path, JsonNode::isNumber, JsonNode::decimalValue); } /** * @param parentNode a parent node * @param path the path to a child node * @return the value of the child node as a {@link ZoneId}, or an empty optional if the child doesn't exist or if * the value of the child node isn't a valid time zone */ public static Optional<ZoneId> asOptionalZoneId(JsonNode parentNode, String path) { Optional<String> string = asOptionalString(parentNode, path); if (!string.isPresent()) { return empty(); } ZoneId zoneId = null; try { zoneId = ZoneId.of(string.get()); } catch (DateTimeException e) { logger.warn("The '{}' field in node '{}' with value '{}' isn't a valid time zone.", path, parentNode, string.get(), e); } return Optional.ofNullable(zoneId); } }