/**
* Copyright 2011-2017 Asakusa Framework Team.
*
* 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.asakusafw.runtime.value;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.text.MessageFormat;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableUtils;
import com.asakusafw.runtime.io.util.WritableRawComparable;
/**
* Represents a character string value which can be {@code null}.
* The following snippet works well for comparing {@link StringOption} with a constant value:
<pre><code>
class Something {
static final StringOption TARGET = new StringOption("something");
void something(Hoge hoge) {
if (hoge.getValueOption().equals(TARGET)) {
....
}
}
}
</code></pre>
* @since 0.1.0
* @version 0.9.1
*/
public final class StringOption extends ValueOption<StringOption> {
static final ThreadLocal<Text> BUFFER_POOL = ThreadLocal.withInitial(Text::new);
private final Text entity = new Text();
/**
* Creates a new instance which represents {@code null} value.
*/
public StringOption() {
this.nullValue = true;
}
/**
* Creates a new instance which represents the specified value.
* @param textOrNull the initial value (nullable)
*/
public StringOption(String textOrNull) {
if (textOrNull == null) {
this.nullValue = true;
} else {
entity.set(textOrNull);
this.nullValue = false;
}
}
/**
* Returns the value which this object represents.
* @return the value which this object represents, never {@code null}
* @throws NullPointerException if this object represents {@code null}
*/
public Text get() {
if (nullValue) {
throw new NullPointerException();
}
return entity;
}
/**
* Returns the value which this object represents.
* @return the value which this object represents, never {@code null}
* @throws NullPointerException if this object represents {@code null}
* @see #appendTo(StringBuilder)
*/
public String getAsString() {
if (nullValue) {
throw new NullPointerException();
}
return entity.toString();
}
/**
* Appends the text in this object into the given {@link StringBuilder}.
* @param buffer the destination {@link StringBuilder}
* @return the appended {@link StringBuilder}
* @throws NullPointerException if this object represents {@code null}
* @since 0.9.1
*/
public StringBuilder appendTo(StringBuilder buffer) {
if (nullValue) {
throw new NullPointerException();
}
StringOptionUtil.append(buffer, this);
return buffer;
}
/**
* Returns whether or not this text is empty.
* @return {@code true} if this text is empty, or {@code false} if it has any characters
* @throws NullPointerException if this object represents {@code null}
* @since 0.9.1
*/
public boolean isEmpty() {
if (nullValue) {
throw new NullPointerException();
}
return entity.getLength() == 0;
}
/**
* Returns the value which this object represents.
* @param alternate the alternative value for {@code null}
* @return the value which this object represents, or the alternative one if this object represents {@code null}
*/
public Text or(Text alternate) {
if (nullValue) {
return alternate;
}
return get();
}
/**
* Returns the value which this object represents.
* @param alternate the alternative value for {@code null}
* @return the value which this object represents, or the alternative one if this object represents {@code null}
*/
public String or(String alternate) {
if (nullValue) {
return alternate;
}
return getAsString();
}
/**
* Reset this object to an empty character string.
* This method makes the object non-null, an empty string even if the object just represents {@code null}.
*/
public void reset() {
entity.clear();
nullValue = false;
}
/**
* Sets the value.
* @param newText the value (nullable)
* @return this
* @deprecated Application developer should not use this method directly
*/
@Deprecated
public StringOption modify(Text newText) {
if (newText == null) {
this.nullValue = true;
} else {
this.nullValue = false;
entity.set(newText);
}
return this;
}
/**
* Sets the value.
* @param newText the value (nullable)
* @return this
* @deprecated Application developer should not use this method directly
*/
@Deprecated
public StringOption modify(String newText) {
if (newText == null) {
this.nullValue = true;
} else {
this.nullValue = false;
entity.set(newText);
}
return this;
}
/**
* Sets the UTF-8 encode text contents.
* @param utf8 the UTF-8 encode byte array
* @param offset the offset in the byte array
* @param length the content length in bytes
* @return this
* @deprecated Application developer should not use this method directly
* @since 0.8.0
*/
@Deprecated
public StringOption modify(byte[] utf8, int offset, int length) {
entity.set(utf8, offset, length);
this.nullValue = false;
return this;
}
@Override
@Deprecated
public void copyFrom(StringOption optionOrNull) {
if (this == optionOrNull) {
return;
} else if (optionOrNull == null || optionOrNull.nullValue) {
this.nullValue = true;
} else {
modify(optionOrNull.entity);
}
}
@Override
public int hashCode() {
final int prime = 31;
if (isNull()) {
return 1;
}
int result = 1;
result = prime * result + entity.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
StringOption other = (StringOption) obj;
if (nullValue != other.nullValue) {
return false;
}
if (nullValue) {
return other.nullValue;
}
Text a = entity;
Text b = other.entity;
return equalsTexts(a, b);
}
/**
* Returns whether both this object and the specified value represents an equivalent value or not.
* @param other the target value (nullable)
* @return {@code true} if this object has the specified value, otherwise {@code false}
*/
public boolean has(String other) {
if (isNull()) {
return other == null;
}
if (other == null) {
return false;
}
Text buffer = BUFFER_POOL.get();
buffer.set(other);
return entity.equals(buffer);
}
/**
* Returns whether both this object and the specified value represents an equivalent value or not.
* @param other the target value (nullable)
* @return {@code true} if this object has the specified value, otherwise {@code false}
*/
public boolean has(Text other) {
if (isNull()) {
return other == null;
}
if (other == null) {
return false;
}
return equalsTexts(entity, other);
}
/**
* Returns whether or not this text contains the given substring.
* @param sub the substring
* @return {@code true} if this has the given substring or the substring is empty, otherwise {@code false}
* @throws NullPointerException if this or the substring is {@code null}
* @since 0.9.1
*/
public boolean contains(String sub) {
if (isNull() || sub == null) {
throw new NullPointerException();
}
if (sub.isEmpty()) {
return true;
}
Text buffer = BUFFER_POOL.get();
buffer.set(sub);
return contains(entity, buffer);
}
/**
* Returns whether or not this text contains the given substring.
* @param sub the substring
* @return {@code true} if this has the given substring or the substring is empty, otherwise {@code false}
* @throws NullPointerException if this or the substring is {@code null}
* @since 0.9.1
*/
public boolean contains(StringOption sub) {
if (isNull() || sub == null || sub.isNull()) {
throw new NullPointerException();
}
if (sub.isEmpty()) {
return true;
}
return contains(entity, sub.entity);
}
private boolean contains(Text a, Text b) {
int subLen = b.getLength();
if (subLen == 0) {
return true;
}
if (a.getLength() < subLen) {
return false;
}
byte[] aBuf = a.getBytes();
byte[] bBuf = b.getBytes();
LOOP: for (int i = 0, n = a.getLength() - subLen; i <= n; i++) {
if (aBuf[i] == bBuf[0]) {
for (int j = 1; j < subLen; j++) {
if (aBuf[i + j] != bBuf[j]) {
continue LOOP;
}
}
return true;
}
}
return false;
}
/**
* Returns whether or not this text starts with the given prefix.
* @param prefix the prefix
* @return {@code true} if this has the given prefix or the prefix is empty, otherwise {@code false}
* @throws NullPointerException if this or the prefix is {@code null}
* @since 0.9.1
*/
public boolean startsWith(String prefix) {
if (isNull() || prefix == null) {
throw new NullPointerException();
}
if (prefix.isEmpty()) {
return true;
}
Text buffer = BUFFER_POOL.get();
buffer.set(prefix);
return startsWith(entity, buffer);
}
/**
* Returns whether or not this text starts with the given prefix.
* @param prefix the prefix
* @return {@code true} if this has the given prefix or the prefix is empty, otherwise {@code false}
* @throws NullPointerException if this or the prefix is {@code null}
* @since 0.9.1
*/
public boolean startsWith(StringOption prefix) {
if (isNull() || prefix == null || prefix.isNull()) {
throw new NullPointerException();
}
if (prefix.isEmpty()) {
return true;
}
return startsWith(entity, prefix.entity);
}
private static boolean startsWith(Text a, Text b) {
if (a.getLength() < b.getLength()) {
return false;
}
byte[] aBuf = a.getBytes();
byte[] bBuf = b.getBytes();
for (int i = 0, n = b.getLength(); i < n; i++) {
if (aBuf[i] != bBuf[i]) {
return false;
}
}
return true;
}
/**
* Returns whether or not this text ends with the given suffix.
* @param suffix the suffix
* @return {@code true} if this has the given suffix or the suffix is empty, otherwise {@code false}
* @throws NullPointerException if this or the suffix is {@code null}
* @since 0.9.1
*/
public boolean endsWith(String suffix) {
if (isNull() || suffix == null) {
throw new NullPointerException();
}
if (suffix.isEmpty()) {
return true;
}
Text buffer = BUFFER_POOL.get();
buffer.set(suffix);
return endsWith(entity, buffer);
}
/**
* Returns whether or not this text ends with the given suffix.
* @param suffix the suffix
* @return {@code true} if this has the given suffix or the suffix is empty, otherwise {@code false}
* @throws NullPointerException if this or the suffix is {@code null}
* @since 0.9.1
*/
public boolean endsWith(StringOption suffix) {
if (isNull() || suffix == null || suffix.isNull()) {
throw new NullPointerException();
}
if (suffix.isEmpty()) {
return true;
}
return endsWith(entity, suffix.entity);
}
private static boolean endsWith(Text a, Text b) {
if (a.getLength() < b.getLength()) {
return false;
}
byte[] aBuf = a.getBytes();
byte[] bBuf = b.getBytes();
int base = a.getLength() - b.getLength();
for (int i = 0, n = b.getLength(); i < n; i++) {
if (aBuf[i + base] != bBuf[i]) {
return false;
}
}
return true;
}
@Override
public int compareTo(WritableRawComparable o) {
StringOption other = (StringOption) o;
if (nullValue | other.nullValue) {
if (nullValue & other.nullValue) {
return 0;
}
return nullValue ? -1 : +1;
}
return compareTexts(entity, other.entity);
}
@Override
public String toString() {
if (isNull()) {
return String.valueOf((Object) null);
} else {
return getAsString();
}
}
@Override
public void write(DataOutput out) throws IOException {
if (isNull()) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
entity.write(out);
}
}
@SuppressWarnings("deprecation")
@Override
public void readFields(DataInput in) throws IOException {
if (in.readBoolean() == false) {
setNull();
} else {
nullValue = false;
entity.readFields(in);
}
}
@SuppressWarnings("deprecation")
@Override
public int restore(byte[] bytes, int offset, int limit) throws IOException {
if (limit - offset == 0) {
throw new IOException(MessageFormat.format(
"Cannot restore a String field ({0})",
"invalid length"));
}
if (bytes[offset] == 0) {
setNull();
return 1;
}
int size = WritableUtils.decodeVIntSize(bytes[offset + 1]);
if (limit - offset < size + 1) {
throw new IOException(MessageFormat.format(
"Cannot restore a String field ({0})",
"invalid length"));
}
int length = (int) ByteArrayUtil.readVLong(bytes, offset + 1);
if (limit - offset >= size + 1 + length) {
nullValue = false;
entity.set(bytes, offset + size + 1, length);
return size + 1 + length;
} else {
throw new IOException(MessageFormat.format(
"Cannot restore a String field ({0})",
"invalid length"));
}
}
@Override
public int getSizeInBytes(byte[] buf, int offset) throws IOException {
return getBytesLength(buf, offset, buf.length - offset);
}
@Override
public int compareInBytes(byte[] b1, int o1, byte[] b2, int o2) throws IOException {
return compareBytes(b1, o1, b1.length - o1, b2, o2, b2.length - o2);
}
/**
* Returns the actual number of bytes from the serialized byte array.
* @param bytes the target byte array
* @param offset the beginning index in the byte array (inclusive)
* @param length the limit length of the byte array
* @return the comparison result
*/
public static int getBytesLength(byte[] bytes, int offset, int length) {
if (bytes[offset] == 0) {
return 1;
}
int size = WritableUtils.decodeVIntSize(bytes[offset + 1]);
int textLength = (int) ByteArrayUtil.readVLong(bytes, offset + 1);
return 1 + size + textLength;
}
/**
* Compares between the two objects in serialized form.
* @param b1 the first byte array to be compared
* @param s1 the beginning index in {@code b1}
* @param l1 the limit byte size in {@code b1}
* @param b2 the second byte array to be compared
* @param s2 the beginning index in {@code b2}
* @param l2 the limit byte size in {@code b2}
* @return the comparison result
*/
public static int compareBytes(
byte[] b1, int s1, int l1,
byte[] b2, int s2, int l2) {
if (b1[s1] == 0 || b2[s2] == 0) {
return ByteArrayUtil.compare(b1[s1], b2[s2]);
}
int n1 = WritableUtils.decodeVIntSize(b1[s1 + 1]);
int n2 = WritableUtils.decodeVIntSize(b2[s2 + 1]);
int len1 = (int) ByteArrayUtil.readVLong(b1, s1 + 1);
int len2 = (int) ByteArrayUtil.readVLong(b2, s2 + 1);
return ByteArrayUtil.compare(
b1, s1 + 1 + n1, len1,
b2, s2 + 1 + n2, len2);
}
private static boolean equalsTexts(Text a, Text b) {
return ByteArrayUtil.equals(
a.getBytes(), 0, a.getLength(),
b.getBytes(), 0, b.getLength());
}
private static int compareTexts(Text a, Text b) {
return ByteArrayUtil.compare(
a.getBytes(), 0, a.getLength(),
b.getBytes(), 0, b.getLength());
}
}