/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2009 Sun Microsystems, Inc. All rights reserved.
* Copyright (c) Ericsson AB, 2004-2008. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can obtain
* a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
* or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
* Sun designates this particular file as subject to the "Classpath" exception
* as provided by Sun in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the License
* Header, with the fields enclosed by brackets [] replaced by your own
* identifying information: "Portions Copyrighted [year]
* [name of copyright owner]"
*
* Contributor(s):
*
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.ericsson.ssa.sip;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.sip.ServletParseException;
/**
* A map for parameter list with name/value pairs. Minimizing the memory
* footprint compared to normal HashMap. An arbitrary separator could be
* inserted between the name/value pairs.
* <p>
* E.g ;key1=value1;key2;key3=value3 or &key1=value1&key2&key3=value3
* <p>
* The capacity of this map may differ from the size. The capacity is allocated
* with an extra BUFFER_EXTEND_SIZE and aligned in chunks of
* BUFFER_ALIGNMENT_SIZE. The capacity is never decreased.
* <p>
* Internally the parameters are stored directly as being received from network
* since it is assumed to be the most compact format. When a parameter is
* accessed via SSA interface a conversion takes place and vice versa.
*
* @author ehsroha
* @since 2006-jan-30
*/
public class ParameterByteMap implements Serializable {
private static final long serialVersionUID = 5200558265884737986L;
private static final SipURIDecoder decoder = new SipURIDecoder();
private static final SipURIEncoder encoder = new SipURIEncoder();
public static final int BUFFER_ALIGNMENT_SIZE = 8;
public static final int BUFFER_EXTEND_SIZE = 2 * BUFFER_ALIGNMENT_SIZE;
private byte[] buffer = null;
private int size = 0;
private final byte separator;
/**
* Default internal byte array size is set to zero.
*
* @param separator
*/
public ParameterByteMap(char separator) {
this.separator = (byte) separator;
}
/**
* Copies the array starting from offset until length into an internal byte
* array with internal size length. If the array is null the internal byte
* array size is set to zero.
*/
public ParameterByteMap(byte[] array, int offset, int length, char separator)
throws ServletParseException {
this(separator);
if (array != null) {
trim(array, offset, length);
if (separator == '&') {
// special handling for uri-headers,
// replace ? with &
array[offset] = '&';
}
buffer = increaseBufferIfNeeded(array, offset, size, 0, true);
}
}
/**
* Copies the array into an internal byte array with internal size set to
* length of array. If the array is null the internal byte array size is set
* to zero.
*/
public ParameterByteMap(byte[] array, char separator)
throws ServletParseException {
this(array, 0, (array == null) ? 0 : array.length, separator);
}
/**
* Remove whitespace except inside quoted string Adjusts the size member.
*
* @param array
* @param offset
* @param separator
* @return trimmed byte array
*/
private void trim(byte[] array, int offset, int length)
throws ServletParseException {
length += offset;
if (length > array.length) {
throw new ServletParseException("Illegal length");
}
byte whitespace = ' ';
byte quote = '"';
int slowPos = offset;
int fastPos = offset;
while (fastPos < length) {
// eat whitespace except inside quoted string
while ((fastPos < (length - 1)) && (array[fastPos] == whitespace)) {
fastPos++;
}
// skip quoted string...
if (array[fastPos] == quote) {
if (slowPos < fastPos) {
array[slowPos] = array[fastPos];
}
slowPos++;
fastPos++;
while ((fastPos < (length - 1)) && (array[fastPos] != quote)) {
if (slowPos < fastPos) {
array[slowPos] = array[fastPos];
}
slowPos++;
fastPos++;
}
}
// adjust for eaten spaces...
if (slowPos < fastPos) {
array[slowPos] = array[fastPos];
}
slowPos++;
fastPos++;
}
size = slowPos - offset;
}
private int size() {
return size;
}
private static int adjustedLength(int length, boolean initial) {
// add an extra capacity...
if (!initial) {
length += BUFFER_EXTEND_SIZE;
}
// should be even chunks of BUFFER_ALIGNMENT_SIZE
int rest = length % BUFFER_ALIGNMENT_SIZE;
return (rest == 0) ? length : ((length + BUFFER_ALIGNMENT_SIZE) - rest);
}
private static byte[] increaseBufferIfNeeded(byte[] src, int size,
int requestedExtraBytes) {
return increaseBufferIfNeeded(src, 0, size, requestedExtraBytes, false);
}
private static byte[] increaseBufferIfNeeded(byte[] src, int offset,
int size, int requestedExtraBytes, boolean forceCreation) {
if (((src != null) && (forceCreation == false)) &&
((size + requestedExtraBytes) <= src.length)) {
return src;
} else {
int capacity = adjustedLength(size + requestedExtraBytes, true);
byte[] newBuffer = new byte[capacity];
if (src != null) {
System.arraycopy(src, offset, newBuffer, 0, size);
}
return newBuffer;
}
}
private static byte[] escape(String str) {
if (str != null) {
return encoder.encodeParameter(str).getBytes();
} else {
return null;
}
}
private static String unescape(String str) {
try {
return decoder.decode(str);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private synchronized String getValue(byte[] key) throws UnsupportedEncodingException {
int pos = indexOf(key);
while ((pos > 0) && (buffer[pos - 1] != separator)) {
// what if key is same as data inside value:
// e.g. key=hej and buffer: ;key1=hej;hej=value2
pos = indexOf(key, pos + 1);
}
if (pos >= 0) {
if ((pos + key.length) == size()) {
// key exist without value at end of buffer...
return "";
} else if (buffer[pos + key.length] == separator) {
// key exist without value inside buffer...
return "";
}
while ((pos < size()) && (buffer[pos] != separator)) {
pos++;
if (buffer[pos] == '=') {
pos++;
int mark = pos;
while (pos < size()) {
if (buffer[pos] == separator) {
break;
}
pos++;
}
return new String(buffer, mark, pos - mark,
SipFactoryImpl.SIP_CHARSET);
}
}
}
// key is not found...
return null;
}
private int indexOf(byte[] token) {
return indexOf(token, 0);
}
private int indexOf(byte[] token, int fromIndex) {
return indexOf(buffer, 0, size(), token, 0, token.length, fromIndex);
}
private static int indexOf(byte[] source, int sourceOffset,
int sourceCount, byte[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return ((targetCount == 0) ? sourceCount : (-1));
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
byte first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
if (source[i] != first) {
while ((++i <= max) && (source[i] != first))
;
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = (j + targetCount) - 1;
for (int k = targetOffset + 1;
(j < end) && (source[j] == target[k]); j++, k++)
;
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
private static int append(byte[] src, byte[] dest, int destPos) {
System.arraycopy(src, 0, dest, destPos, src.length);
return destPos + src.length;
}
private byte[] append(byte[] src, int length, byte[] key, byte[] value,
byte separator) {
int appendLength = 0;
if (key != null) {
// key
appendLength += (key.length + 1);
}
if ((value != null) && (value.length > 0)) {
// '=' + value
appendLength += (value.length + 1);
}
byte[] newBuffer = increaseBufferIfNeeded(src, length, appendLength);
// append dest
int newPos = length;
if (key != null) {
// append separator
newBuffer[newPos] = separator;
newPos += 1;
// append key
newPos = append(key, newBuffer, newPos);
}
if ((value != null) && (value.length > 0)) {
// append =
newBuffer[newPos] = '=';
newPos += 1;
// append value
newPos = append(value, newBuffer, newPos);
}
size = newPos;
return newBuffer;
}
private byte[] replace(byte[] src, int length, int start, int end,
byte[] value) {
int equalLength = 1;
// don't need equal char if value is empty...
if (value.length == 0) {
equalLength = 0;
}
// could be negative if new value is
// shorter then previous value..
int extendLength = (value.length + equalLength) - (end - start);
byte[] newBuffer = increaseBufferIfNeeded(src, length, extendLength);
size = length + extendLength;
// append until start
if (src != newBuffer) {
System.arraycopy(src, 0, newBuffer, 0, start);
}
// move last section first, same buffer could be used & risk of
// overwrite useful data...
if ((length - end) > 0) {
int destPos = (value.length == 0) ? start : (start + 1 +
value.length);
System.arraycopy(src, end, newBuffer, destPos, length - end);
}
if (value.length > 0) {
// insert '='
newBuffer[start] = '=';
// insert value
System.arraycopy(value, 0, newBuffer, start + 1, value.length);
}
return newBuffer;
}
private byte[] remove(byte[] src, int length, int start, int end) {
size = length - (end - start);
// append from end to length of dest
if ((length - end) > 0) {
System.arraycopy(src, end, src, start, length - end);
}
return src;
}
private synchronized void removeKey(byte[] key) {
int pos = indexOf(key);
while ((pos != -1) && (buffer[pos - 1] != separator)) {
// what if key is same as data inside value:
// e.g. key=hej and buffer: ;key1=hej;hej=value2
pos = indexOf(key, pos + 1);
}
if (pos >= 0) {
if ((pos + key.length) == size()) {
// key exist without value at end of buffer...
buffer = remove(buffer, size(), pos - 1, size());
} else if (buffer[pos + key.length] == separator) {
// key exist without value inside buffer...
buffer = remove(buffer, size(), pos - 1, pos + key.length);
} else {
int mark = pos - 1;
while ((pos < size()) && (buffer[pos] != separator)) {
pos++;
}
// key and value exist...
buffer = remove(buffer, size(), mark, pos);
}
}
}
private synchronized void putValue(byte[] key, byte[] value) {
int pos = indexOf(key);
while ((pos > 0) && (buffer[pos - 1] != separator)) {
// what if key is same as data inside value:
// e.g. key=hej and buffer: ;key1=hej;hej=value2
pos = indexOf(key, pos + 1);
}
if (pos == -1) {
// key was not found, lets append new key and value
buffer = append(buffer, size(), key, value, separator);
} else if (value != null) {
// key is found, replace value if present
//
// find next separator or end of buffer
while ((pos < (size() - 1)) && (buffer[pos] != separator)) {
if (buffer[pos] == '=') {
int mark = pos;
pos++;
while (pos < size()) {
if (buffer[pos] == separator) {
break;
}
pos++;
}
// previous value should now be replaced...
buffer = replace(buffer, size(), mark, pos, value);
return;
}
pos++;
}
if (buffer[pos] == separator) {
// only key before, no previous value...
buffer = replace(buffer, size(), pos, pos, value);
} else {
// last key, no previous value...
// lets append the value to the end of the buffer
buffer = append(buffer, size(), null, value, separator);
}
}
}
@Override
public Object clone() {
ParameterByteMap newMap = new ParameterByteMap((char) separator);
byte[] newBuffer = null;
synchronized (this) {
if (buffer != null) {
newBuffer = new byte[buffer.length];
System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
newMap.size = size();
}
}
newMap.buffer = newBuffer;
return newMap;
}
private boolean isQuotedString(String str) {
return (str != null) && !str.equals("") && (str.charAt(0) == '"') &&
(str.charAt(str.length() - 1) == '"');
}
/**
* Returns the value to which the specified key is mapped in this identity
* map, or null if the map contains no mapping for this key.
*
* @param key
* the key whose associated value is to be returned.
* @return the value to which this map maps the specified key, or null if the
* map contains no mapping for this key.
*/
public String get(String key) {
byte[] escapedKey = Ascii7String.getBytes(key);
String value;
try {
value = getValue(escapedKey);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
String unescapedValue = isQuotedString(value) ? value : unescape(value);
return unescapedValue;
}
/**
* Associates the specified value with the specified key in this map. If the
* map previously contained a mapping for this key, the old value is
* replaced.
*
* @param key
* key with which the specified value is to be associated.
* @param value
* value to be associated with the specified key.
*/
public void put(String key, String value) {
byte[] encodedKey = Ascii7String.getBytes(key);
if (value == null)
value = "";
try {
byte[] encodedValue = isQuotedString(value)
? value.getBytes(SipFactoryImpl.SIP_CHARSET) : escape(value);
putValue(encodedKey, encodedValue);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Removes the mapping for this key from this map if present.
*
* @param key
* key whose mapping is to be removed from the map.
*/
public void remove(String key) {
byte[] encodedKey = Ascii7String.getBytes(key);
removeKey(encodedKey);
}
/**
* </p>
* Returns a iterator view of the keys contained in this map.
*
* @return a iterator view of the keys contained in this map.
*/
public Iterator<String> getKeys() {
return keyList().iterator();
}
private synchronized List<String> keyList() {
LinkedList<String> l = new LinkedList<String>();
int pos = 0;
int mark = 0;
while (pos < size()) {
if (buffer[pos] == separator) {
// found separator..
pos++;
mark = pos; // mark start of key
while ((pos < size()) && (buffer[pos] != separator)) {
if (buffer[pos] == '=') {
break;
}
pos++;
}
// intentionally no character conversion since
// no other than US-ASCII is supported
l.add(new String(buffer, mark, pos - mark));
} else {
pos++;
}
}
return l;
}
public Set<Map.Entry<String, String>> entrySet() {
HashMap<String, String> entryMap = new HashMap<String, String>(8);
for (String s : keyList()) {
entryMap.put(s, get(s));
}
return entryMap.entrySet();
}
/**
* Returns the map as a byte array.
*
* @return the map as a byte array.
*/
public synchronized byte[] toArray() {
if (buffer == null) {
return null;
}
byte[] newArray = new byte[size()];
System.arraycopy(buffer, 0, newArray, 0, size());
if (separator == '&') {
// special handling for uri-headers,
// replace & with ?
newArray[0] = '?';
}
return newArray;
}
@Override
public synchronized String toString() {
if (buffer == null) {
return "";
}
try {
String str = null;
if (separator == '&') {
// special handling for uri-headers,
// replace & with ?
buffer[0] = '?';
str = new String(buffer, 0, size(), SipFactoryImpl.SIP_CHARSET);
buffer[0] = '&';
} else {
str = new String(buffer, 0, size(), SipFactoryImpl.SIP_CHARSET);
}
return str;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* A user, ttl, or method uri-parameter appearing in only one URI never
* matches, even if it contains the default value.
* </p>
* A URI that includes an maddr parameter will not match a URI that contains
* no maddr parameter.
* </p>
* URI header components are never ignored
*
* @return
*/
private boolean isHeaderOrSpecialParameter(String parameter) {
return ((separator == '&') || isSpecialParameter(parameter));
}
private boolean isSpecialParameter(String parameter) {
return ((separator == '&') ||
((separator == ';') &&
(parameter.equalsIgnoreCase("user") ||
parameter.equalsIgnoreCase("method") ||
parameter.equalsIgnoreCase("ttl") ||
parameter.equalsIgnoreCase("transport") ||
parameter.equalsIgnoreCase("maddr"))));
}
/**
* Any parameter that is: user, ttl, or method uri-parameter or maddr
* parameter.
*
* @return true or false
*/
public boolean isOnlySpecialParameters() {
for (String key : keyList()) {
if (!isSpecialParameter(key)) {
return false;
}
}
return true;
}
private String findAndRemoveKey(String key, List<String> remoteKeys) {
int i = -1;
for (String remotekey : remoteKeys) {
i++;
if (key.equalsIgnoreCase(remotekey)) {
return remoteKeys.remove(i);
}
}
return null;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof ParameterByteMap) {
ParameterByteMap remote = (ParameterByteMap) obj;
List<String> keys = keyList();
List<String> remoteKeys = remote.keyList();
// if the maps don't have the same number
// of headers they are not equal.
if ((separator == '&') && (keys.size() != remoteKeys.size())) {
return false;
}
String value = null;
String remoteValue = null;
String remoteKey = null;
for (String key : keys) {
remoteKey = findAndRemoveKey(key, remoteKeys);
if (remoteKey != null) {
value = get(key);
remoteValue = remote.get(remoteKey);
// if one parameter is set in one map but
// not in the other then they are not equal
if (((value != null) && (remoteValue == null)) ||
((value == null) && (remoteValue != null))) {
return false;
} else {
// if one parameter is not equal then the whole map is not
// equal
if (((value != null) && (remoteValue != null)) &&
!value.equalsIgnoreCase(remoteValue)) {
return false;
}
}
} else {
// key only available in one list, it is ignored
// if it's not a header or special one...
if (isHeaderOrSpecialParameter(key)) {
return false;
}
}
}
// the remote list might still have parameters, they are ignored
// if they are not headers or special once...
for (String leftKey : remoteKeys) {
if (isHeaderOrSpecialParameter(leftKey)) {
return false;
}
}
return true;
}
return false;
}
}