/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.util;
import java.io.
IOException;
import java.io.
ObjectInputStream;
import java.io.
OutputStream;
import java.io.
Serializable;
import java.lang.reflect.
Field;
import java.nio.
ByteBuffer;
import java.util.
Random;
import static java.lang.
Integer.signum;
import static java.lang.
System.arraycopy;
import static java.util.
Arrays.copyOfRange;
import io.undertow.
UndertowMessages;
/**
* An HTTP case-insensitive Latin-1 string.
*
* @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
*/
public final class
HttpString implements
Comparable<
HttpString>,
Serializable {
private static final long
serialVersionUID = 1L;
private final byte[]
bytes;
private final transient int
hashCode;
/**
* And integer that is only set for well known header to make
* comparison fast
*/
private final int
orderInt;
private transient
String string;
private static final
Field hashCodeField;
private static final int
hashCodeBase;
static {
try {
hashCodeField =
HttpString.class.
getDeclaredField("hashCode");
hashCodeField.
setAccessible(true);
} catch (
NoSuchFieldException e) {
throw new
NoSuchFieldError(
e.
getMessage());
}
hashCodeBase = new
Random().
nextInt();
}
/**
* Empty HttpString instance.
*/
public static final
HttpString EMPTY = new
HttpString("");
/**
* Construct a new instance.
*
* @param bytes the byte array to copy
*/
public
HttpString(final byte[]
bytes) {
this(
bytes.
clone(), null);
}
/**
* Construct a new instance.
*
* @param bytes the byte array to copy
* @param offset the offset into the array to start copying
* @param length the number of bytes to copy
*/
public
HttpString(final byte[]
bytes, int
offset, int
length) {
this(
copyOfRange(
bytes,
offset,
offset +
length), null);
}
/**
* Construct a new instance by reading the remaining bytes from a buffer.
*
* @param buffer the buffer to read
*/
public
HttpString(final
ByteBuffer buffer) {
this(
take(
buffer), null);
}
/**
* Construct a new instance from a {@code String}. The {@code String} will be used
* as the cached {@code toString()} value for this {@code HttpString}.
*
* @param string the source string
*/
public
HttpString(final
String string) {
this(
string, 0);
}
HttpString(final
String string, int
orderInt) {
this.
orderInt =
orderInt;
final int
len =
string.
length();
final byte[]
bytes = new byte[
len];
for (int
i = 0;
i <
len;
i++) {
char
c =
string.
charAt(
i);
if (
c > 0xff) {
throw new
IllegalArgumentException("Invalid string contents " +
string);
}
bytes[
i] = (byte)
c;
}
this.
bytes =
bytes;
this.
hashCode =
calcHashCode(
bytes);
this.
string =
string;
checkForNewlines();
}
private void
checkForNewlines() {
for(byte
b :
bytes) {
if(
b == '\r' ||
b == '\n') {
throw
UndertowMessages.
MESSAGES.
newlineNotSupportedInHttpString(
string);
}
}
}
private
HttpString(final byte[]
bytes, final
String string) {
this.
bytes =
bytes;
this.
hashCode =
calcHashCode(
bytes);
this.
string =
string;
this.
orderInt = 0;
checkForNewlines();
}
/**
* Attempt to convert a {@code String} to an {@code HttpString}. If the string cannot be converted,
* {@code null} is returned.
*
* @param string the string to try
* @return the HTTP string, or {@code null} if the string is not in a compatible encoding
*/
public static
HttpString tryFromString(
String string) {
HttpString cached =
Headers.
fromCache(
string);
if(
cached != null) {
return
cached;
}
final int
len =
string.
length();
final byte[]
bytes = new byte[
len];
for (int
i = 0;
i <
len;
i++) {
char
c =
string.
charAt(
i);
if (
c > 0xff) {
return null;
}
bytes[
i] = (byte)
c;
}
return new
HttpString(
bytes,
string);
}
/**
* Get the string length.
*
* @return the string length
*/
public int
length() {
return
bytes.length;
}
/**
* Get the byte at an index.
*
* @return the byte at an index
*/
public byte
byteAt(int
idx) {
return
bytes[
idx];
}
/**
* Copy {@code len} bytes from this string at offset {@code srcOffs} to the given array at the given offset.
*
* @param srcOffs the source offset
* @param dst the destination
* @param offs the destination offset
* @param len the number of bytes to copy
*/
public void
copyTo(int
srcOffs, byte[]
dst, int
offs, int
len) {
arraycopy(
bytes,
srcOffs,
dst,
offs,
len);
}
/**
* Copy {@code len} bytes from this string to the given array at the given offset.
*
* @param dst the destination
* @param offs the destination offset
* @param len the number of bytes
*/
public void
copyTo(byte[]
dst, int
offs, int
len) {
copyTo(0,
dst,
offs,
len);
}
/**
* Copy all the bytes from this string to the given array at the given offset.
*
* @param dst the destination
* @param offs the destination offset
*/
public void
copyTo(byte[]
dst, int
offs) {
copyTo(
dst,
offs,
bytes.length);
}
/**
* Append to a byte buffer.
*
* @param buffer the buffer to append to
*/
public void
appendTo(
ByteBuffer buffer) {
buffer.
put(
bytes);
}
/**
* Append to an output stream.
*
* @param output the stream to write to
* @throws IOException if an error occurs
*/
public void
writeTo(
OutputStream output) throws
IOException {
output.
write(
bytes);
}
private static byte[]
take(final
ByteBuffer buffer) {
if (
buffer.
hasArray()) {
// avoid useless array clearing
try {
return
copyOfRange(
buffer.
array(),
buffer.
arrayOffset() +
buffer.
position(),
buffer.
remaining());
} finally {
buffer.
position(
buffer.
limit());
}
} else {
final byte[]
bytes = new byte[
buffer.
remaining()];
buffer.
get(
bytes);
return
bytes;
}
}
/**
* Compare this string to another in a case-insensitive manner.
*
* @param other the other string
* @return -1, 0, or 1
*/
public int
compareTo(final
HttpString other) {
if(
orderInt != 0 &&
other.
orderInt != 0) {
return
signum(
orderInt -
other.
orderInt);
}
final int
len =
Math.
min(
bytes.length,
other.
bytes.length);
int
res;
for (int
i = 0;
i <
len;
i++) {
res =
signum(
higher(
bytes[
i]) -
higher(
other.
bytes[
i]));
if (
res != 0) return
res;
}
// shorter strings sort higher
return
signum(
bytes.length -
other.
bytes.length);
}
/**
* Get the hash code.
*
* @return the hash code
*/
@
Override
public int
hashCode() {
return
hashCode;
}
/**
* Determine if this {@code HttpString} is equal to another.
*
* @param other the other object
* @return {@code true} if they are equal, {@code false} otherwise
*/
@
Override
public boolean
equals(final
Object other) {
if(
other == this) {
return true;
}
if(!(
other instanceof
HttpString)) {
return false;
}
HttpString otherString = (
HttpString)
other;
if(
orderInt > 0 &&
otherString.
orderInt > 0) {
//if the order int is set for both of them and different then we know they are different strings
return false;
}
return
bytesAreEqual(
bytes,
otherString.
bytes);
}
/**
* Determine if this {@code HttpString} is equal to another.
*
* @param other the other object
* @return {@code true} if they are equal, {@code false} otherwise
*/
public boolean
equals(final
HttpString other) {
return
other == this ||
other != null &&
bytesAreEqual(
bytes,
other.
bytes);
}
private static int
calcHashCode(final byte[]
bytes) {
int
hc = 17;
for (byte
b :
bytes) {
hc = (
hc << 4) +
hc +
higher(
b);
}
return
hc;
}
private static int
higher(byte
b) {
return
b & (
b >= 'a' &&
b <= 'z' ? 0xDF : 0xFF);
}
private static boolean
bytesAreEqual(final byte[]
a, final byte[]
b) {
return
a.length ==
b.length &&
bytesAreEquivalent(
a,
b);
}
private static boolean
bytesAreEquivalent(final byte[]
a, final byte[]
b) {
assert
a.length ==
b.length;
final int
len =
a.length;
for (int
i = 0;
i <
len;
i++) {
if (
higher(
a[
i]) !=
higher(
b[
i])) {
return false;
}
}
return true;
}
/**
* Get the {@code String} representation of this {@code HttpString}.
*
* @return the string
*/
@
Override
@
SuppressWarnings("deprecation")
public
String toString() {
if (
string == null) {
string = new
String(
bytes, 0);
}
return
string;
}
private void
readObject(
ObjectInputStream ois) throws
ClassNotFoundException,
IOException {
ois.
defaultReadObject();
try {
hashCodeField.
setInt(this,
calcHashCode(
bytes));
} catch (
IllegalAccessException e) {
throw new
IllegalAccessError(
e.
getMessage());
}
}
static int
hashCodeOf(
String headerName) {
int
hc = 17;
for (int
i = 0;
i <
headerName.
length(); ++
i) {
hc = (
hc << 4) +
hc +
higher((byte)
headerName.
charAt(
i));
}
return
hc;
}
public boolean
equalToString(
String headerName) {
if(
headerName.
length() !=
bytes.length) {
return false;
}
final int
len =
bytes.length;
for (int
i = 0;
i <
len;
i++) {
if (
higher(
bytes[
i]) !=
higher((byte)
headerName.
charAt(
i))) {
return false;
}
}
return true;
}
}