io.permazen.encoding.NullSafeEncoding Maven / Gradle / Ivy
Show all versions of permazen-encoding Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen.encoding;
import com.google.common.base.Preconditions;
import io.permazen.util.ByteReader;
import io.permazen.util.ByteWriter;
import java.util.OptionalInt;
/**
* An {@link Encoding} that wraps any other {@link Encoding} not supporting null values and adds support for null values.
*
*
* This class pre-pends a {@code 0x01} to the binary encoding of non-null values, and uses a single {@code 0xff} byte to
* represent null values. Therefore, null values sort last. Also, the default value is null.
*
*
* This class will automatically "inline" the {@code 0xff} for null values and omit the {@code 0x01} for non-null values
* if the wrapped {@link Encoding}'s {@link Encoding#hasPrefix0xff} method returns false.
*
* @param The associated Java type
*/
public class NullSafeEncoding extends AbstractEncoding {
/**
* Not null sentinel byte value. Used only when inlining is not in effect.
*/
public static final int NOT_NULL_SENTINEL = 0x01;
/**
* Null sentinel byte value.
*/
public static final int NULL_SENTINEL = 0xff;
private static final long serialVersionUID = -6420381330755516561L;
/**
* The inner {@link Encoding} that this instance wraps.
*/
protected final Encoding inner;
private final boolean inline;
/**
* Constructor.
*
* @param encodingId encoding ID for this encoding, or null to be anonymous
* @param inner inner type that is not null safe
*/
public NullSafeEncoding(EncodingId encodingId, Encoding inner) {
super(encodingId, AbstractEncoding.noNull(inner, "inner").getTypeToken().wrap(), () -> null);
Preconditions.checkArgument(!inner.supportsNull(), "inner type is already null-safe");
this.inner = inner;
this.inline = !inner.hasPrefix0xff();
}
/**
* Get the inner {@link Encoding} that this instance wraps.
*
* @return inner type that is not null safe
*/
public Encoding getInnerEncoding() {
return this.inner;
}
// Encoding
@Override
public T read(ByteReader reader) {
Preconditions.checkArgument(reader != null);
if (this.inline) {
if (reader.peek() == NULL_SENTINEL) {
reader.skip(1);
return null;
}
return this.inner.read(reader);
} else {
switch (reader.readByte()) {
case NULL_SENTINEL:
return null;
case NOT_NULL_SENTINEL:
return this.inner.read(reader);
default:
throw new IllegalArgumentException(String.format("invalid encoding of %s", this));
}
}
}
@Override
public void write(ByteWriter writer, T value) {
Preconditions.checkArgument(writer != null);
if (value == null) {
writer.writeByte(NULL_SENTINEL);
return;
}
if (!this.inline)
writer.writeByte(NOT_NULL_SENTINEL);
this.inner.write(writer, value);
}
@Override
public void skip(ByteReader reader) {
Preconditions.checkArgument(reader != null);
if (this.inline) {
final int prefix = reader.peek();
switch (prefix) {
case NULL_SENTINEL:
reader.skip(1);
break;
default:
this.inner.skip(reader);
break;
}
} else {
final int prefix = reader.readByte();
switch (prefix) {
case NULL_SENTINEL:
break;
case NOT_NULL_SENTINEL:
this.inner.skip(reader);
break;
default:
throw new IllegalArgumentException(String.format("invalid encoding of %s", this));
}
}
}
@Override
public T fromString(String string) {
return this.inner.fromString(string);
}
@Override
public String toString(T value) {
Preconditions.checkArgument(value != null, "null value");
return this.inner.toString(value);
}
@Override
public T convert(Encoding type, S value) {
Preconditions.checkArgument(type != null, "null type");
return value == null ? null : this.inner.convert(type, value);
}
@Override
public int compare(T value1, T value2) {
if (value1 == null)
return value2 == null ? 0 : 1;
if (value2 == null)
return -1;
return this.inner.compare(value1, value2);
}
@Override
public boolean sortsNaturally() {
return inner.sortsNaturally();
}
/**
* Always returns true.
*
* @return true
*/
@Override
public boolean supportsNull() {
return true;
}
/**
* Always returns true.
*
* @return true
*/
@Override
public boolean hasPrefix0xff() {
return true;
}
@Override
public boolean hasPrefix0x00() {
return this.inline && this.inner.hasPrefix0x00();
}
@Override
public OptionalInt getFixedWidth() {
final int requiredWidth = this.inline ? 1 : 0;
final OptionalInt width = this.inner.getFixedWidth();
return width.isPresent() && width.getAsInt() == requiredWidth ? OptionalInt.of(1) : OptionalInt.empty();
}
// Object
@Override
public int hashCode() {
return super.hashCode() ^ this.inner.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!super.equals(obj))
return false;
final NullSafeEncoding> that = (NullSafeEncoding>)obj;
return this.inner.equals(that.inner);
}
}