Files
qbit-backups/src/main/java/com/kingsrook/qbits/backups/codec/FieldValueCodec.java
T
james.maesandClaude Sonnet 5.5 eea9f63878 feat: backups foundation - tables, scope, codecs, manifest, backup engine, file copy, home
Tables (spec 4), permissions, scope resolver and table roles, canonical JSONL
and CSV codecs, per-part SHA-256 and rowsHash, manifest written last, the backup
engine with referenced-file copy into the deduplicated store, a narrow storage
interface with an in-memory implementation, Run backup now (streamed ETL), and the
Backups app home. Stand-ins for unreleased core hooks are marked in the code.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 16:14:32 -05:00

173 lines
6.2 KiB
Java

/*
* Copyright 2026 Kingsrook, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.backups.codec;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import com.fasterxml.jackson.databind.JsonNode;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** The canonical, lossless text encoding of a field value, by QFieldType
** (spec 8.2). The same canonical text is used by JSONL and CSV, so a value
** means the same thing in both, and by the content hash.
**
** STRING, TEXT, HTML, PASSWORD: string
** INTEGER, LONG: number
** DECIMAL: string (BigDecimal.toPlainString), so there is no float rounding
** BOOLEAN: true or false
** DATE: yyyy-MM-dd
** TIME: HH:mm:ss, with .SSSSSS only when there are fractional seconds
** DATE_TIME: an ISO-8601 instant in UTC, with .SSSSSS only when there are fractional seconds
** BLOB: base64
*******************************************************************************/
public final class FieldValueCodec
{
private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final DateTimeFormatter TIME_FORMAT_MICROS = DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSS");
private static final DateTimeFormatter INSTANT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
private static final DateTimeFormatter INSTANT_FORMAT_MICRO = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'").withZone(ZoneOffset.UTC);
/*******************************************************************************
** Utility class.
*******************************************************************************/
private FieldValueCodec()
{
}
/*******************************************************************************
** Whether the canonical JSON form of a type is a bare number or boolean (not quoted).
*******************************************************************************/
public static boolean isBareJson(QFieldType type)
{
return (type == QFieldType.INTEGER || type == QFieldType.LONG || type == QFieldType.BOOLEAN);
}
/*******************************************************************************
** The canonical text of a non-null value.
*******************************************************************************/
public static String toCanonicalText(QFieldType type, Serializable value)
{
switch(type)
{
case INTEGER, LONG:
return (String.valueOf(ValueUtils.getValueAsLong(value)));
case DECIMAL:
return (ValueUtils.getValueAsBigDecimal(value).toPlainString());
case BOOLEAN:
return (String.valueOf(ValueUtils.getValueAsBoolean(value)));
case DATE:
return (ValueUtils.getValueAsLocalDate(value).toString());
case TIME:
{
LocalTime time = ValueUtils.getValueAsLocalTime(value);
return (time.getNano() == 0 ? TIME_FORMAT.format(time) : TIME_FORMAT_MICROS.format(time));
}
case DATE_TIME:
{
Instant instant = ValueUtils.getValueAsInstant(value);
return (instant.getNano() == 0 ? INSTANT_FORMAT.format(instant) : INSTANT_FORMAT_MICRO.format(instant));
}
case BLOB:
return (Base64.getEncoder().encodeToString(blobBytes(value)));
default:
return (String.valueOf(value));
}
}
/*******************************************************************************
**
*******************************************************************************/
private static byte[] blobBytes(Serializable value)
{
if(value instanceof byte[] bytes)
{
return (bytes);
}
return (String.valueOf(value).getBytes(java.nio.charset.StandardCharsets.UTF_8));
}
/*******************************************************************************
** Parse canonical text back to a Java value.
*******************************************************************************/
public static Serializable fromCanonicalText(QFieldType type, String text)
{
if(text == null)
{
return (null);
}
switch(type)
{
case INTEGER:
return (Integer.valueOf(text));
case LONG:
return (Long.valueOf(text));
case DECIMAL:
return (new BigDecimal(text));
case BOOLEAN:
return (Boolean.valueOf(text));
case DATE:
return (LocalDate.parse(text));
case TIME:
return (LocalTime.parse(text));
case DATE_TIME:
return (Instant.parse(text));
case BLOB:
return (Base64.getDecoder().decode(text));
default:
return (text);
}
}
/*******************************************************************************
** Convert a parsed JSON value to a Java value.
*******************************************************************************/
public static Serializable fromJson(QFieldType type, JsonNode node)
{
if(node == null || node.isNull())
{
return (null);
}
if(type == QFieldType.BOOLEAN)
{
return (node.asBoolean());
}
return (fromCanonicalText(type, node.isNumber() ? node.asText() : node.textValue()));
}
}