net.sandius.rembulan.StringByteString Maven / Gradle / Ivy
/*
* Copyright 2016 Miroslav Janíček
*
* 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 net.sandius.rembulan;
import net.sandius.rembulan.util.ByteIterator;
import net.sandius.rembulan.util.CharsetEncoderByteIterator;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Objects;
/**
* A byte string backed by a {@link java.lang.String}.
*/
class StringByteString extends ByteString {
private final String string;
private final Charset charset;
private int byteHashCode;
private int byteLength;
StringByteString(String s, Charset charset) {
this.string = Objects.requireNonNull(s);
this.charset = Objects.requireNonNull(charset);
if (!charset.canEncode()) {
throw new IllegalArgumentException("Charset cannot encode: " + charset.name());
}
this.byteHashCode = 0;
this.byteLength = string.isEmpty() ? 0 : -1;
}
@Override
protected boolean equals(ByteString that) {
if (this.isEmpty() && that.isEmpty()) return true;
// don't force hashCode computation, but use if already known
int thisHash = this.maybeHashCode();
int thatHash = that.maybeHashCode();
if (thisHash != 0 && thatHash != 0 && thisHash != thatHash) return false;
// don't force length computation, but use if already known
int thisLength = this.maybeLength();
int thatLength = that.maybeLength();
if (thisLength >= 0 && thatLength >= 0 && thisLength != thatLength) return false;
// compare byte-by-byte
ByteIterator thisIterator = this.byteIterator();
ByteIterator thatIterator = that.byteIterator();
while (thisIterator.hasNext() && thatIterator.hasNext()) {
byte thisByte = thisIterator.nextByte();
byte thatByte = thatIterator.nextByte();
if (thisByte != thatByte) {
return false;
}
}
return thisIterator.hasNext() == thatIterator.hasNext();
}
private int computeHashCode() {
int hc = 0;
ByteIterator it = new CharsetEncoderByteIterator(string, charset);
while (it.hasNext()) {
hc = (hc * 31) + (it.nextByte() & 0xff);
}
return hc;
}
@Override
public int hashCode() {
int hc = byteHashCode;
if (hc == 0 && !string.isEmpty()) {
hc = computeHashCode();
// update cached hashCode
byteHashCode = hc;
}
return hc;
}
@Override
int maybeHashCode() {
return byteHashCode;
}
private int computeLength() {
int len = 0;
ByteIterator it = new CharsetEncoderByteIterator(string, charset);
while (it.hasNext()) {
it.nextByte();
len++;
}
return len;
}
@Override
public int length() {
int len = byteLength;
if (len < 0) {
len = computeLength();
byteLength = len;
}
return len;
}
@Override
int maybeLength() {
return byteLength;
}
@Override
public boolean isEmpty() {
return string.isEmpty();
}
// must not escape, may be an array from the cache!
private byte[] toBytes() {
// TODO: cache the result
return string.getBytes(charset);
}
@Override
public byte[] getBytes() {
byte[] bytes = toBytes();
// must make a defensive copy
return Arrays.copyOf(bytes, bytes.length);
}
@Override
public byte byteAt(int index) {
if (index < 0) {
// don't even have to convert to bytes
throw new IndexOutOfBoundsException(String.valueOf(index));
}
return toBytes()[index];
}
@Override
public ByteIterator byteIterator() {
return new CharsetEncoderByteIterator(string, charset);
}
private static void checkSubstringBounds(int start, int end, int len) {
if (start > end) throw new IndexOutOfBoundsException("start > end (" + start + " > " + end + ")");
else if (start < 0) throw new IndexOutOfBoundsException("start < 0 (" + start + " < 0)");
else if (end < 0) throw new IndexOutOfBoundsException("end < 0 (" + end + " < 0)");
else if (end > len) throw new IndexOutOfBoundsException("end > length (" + start + " > " + len + ")");
}
@Override
public ByteString substring(int start, int end) {
byte[] bytes = toBytes();
checkSubstringBounds(start, end, bytes.length);
return new ArrayByteString(Arrays.copyOfRange(bytes, start, end));
}
@Override
public String toString() {
return string;
}
@Override
public String decode(Charset charset) {
if (this.charset.equals(charset)) {
return string;
}
else {
return super.decode(charset);
}
}
@Override
public String toRawString() {
byte[] bytes = toBytes();
char[] chars = new char[bytes.length];
for (int i = 0; i < chars.length; i++) {
chars[i] = (char) (bytes[i] & 0xff);
}
return String.valueOf(chars);
}
@Override
public void putTo(ByteBuffer buffer) {
// ByteBuffer cannot be directly extended: it's safe to use a possibly cached array
buffer.put(toBytes());
}
@Override
public void writeTo(OutputStream stream) throws IOException {
// OutputStream can be extended: pass a defensive copy
stream.write(getBytes());
}
@Override
public ByteString concat(ByteString other) {
if (other instanceof StringByteString) {
StringByteString that = (StringByteString) other;
if (this.charset.equals(that.charset)) {
// Caveat: preserves malformed characters and characters unmappable by charset
return ByteString.of(this.string.concat(that.string));
}
}
return super.concat(other);
}
@Override
public boolean startsWith(byte b) {
if (string.isEmpty()) return false;
ByteIterator it = new CharsetEncoderByteIterator(string, charset);
return it.hasNext() && it.nextByte() == b;
}
}