package com.hwlcn.ldap.ldap.sdk;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
import com.hwlcn.ldap.asn1.ASN1OctetString;
import com.hwlcn.ldap.ldap.matchingrules.MatchingRule;
import com.hwlcn.ldap.ldap.sdk.schema.AttributeTypeDefinition;
import com.hwlcn.ldap.ldap.sdk.schema.Schema;
import com.hwlcn.core.annotation.NotMutable;
import com.hwlcn.core.annotation.ThreadSafety;
import com.hwlcn.ldap.util.ThreadSafetyLevel;
import static com.hwlcn.ldap.ldap.sdk.LDAPMessages.*;
import static com.hwlcn.ldap.util.Debug.*;
import static com.hwlcn.ldap.util.StaticUtils.*;
import static com.hwlcn.ldap.util.Validator.*;
@NotMutable()
@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
public final class RDN
implements Comparable<RDN>, Comparator<RDN>, Serializable
{
private static final long serialVersionUID = 2923419812807188487L;
private final ASN1OctetString[] attributeValues;
private final Schema schema;
private volatile String normalizedString;
private volatile String rdnString;
private final String[] attributeNames;
public RDN(final String attributeName, final String attributeValue)
{
this(attributeName, attributeValue, null);
}
public RDN(final String attributeName, final String attributeValue,
final Schema schema)
{
ensureNotNull(attributeName, attributeValue);
this.schema = schema;
attributeNames = new String[] { attributeName };
attributeValues =
new ASN1OctetString[] { new ASN1OctetString(attributeValue) };
}
public RDN(final String attributeName, final byte[] attributeValue)
{
this(attributeName, attributeValue, null);
}
public RDN(final String attributeName, final byte[] attributeValue,
final Schema schema)
{
ensureNotNull(attributeName, attributeValue);
this.schema = schema;
attributeNames = new String[] { attributeName };
attributeValues =
new ASN1OctetString[] { new ASN1OctetString(attributeValue) };
}
public RDN(final String[] attributeNames, final String[] attributeValues)
{
this(attributeNames, attributeValues, null);
}
public RDN(final String[] attributeNames, final String[] attributeValues,
final Schema schema)
{
ensureNotNull(attributeNames, attributeValues);
ensureTrue(attributeNames.length == attributeValues.length,
"RDN.attributeNames and attributeValues must be the same size.");
ensureTrue(attributeNames.length > 0,
"RDN.attributeNames must not be empty.");
this.attributeNames = attributeNames;
this.schema = schema;
this.attributeValues = new ASN1OctetString[attributeValues.length];
for (int i=0; i < attributeValues.length; i++)
{
this.attributeValues[i] = new ASN1OctetString(attributeValues[i]);
}
}
public RDN(final String[] attributeNames, final byte[][] attributeValues)
{
this(attributeNames, attributeValues, null);
}
public RDN(final String[] attributeNames, final byte[][] attributeValues,
final Schema schema)
{
ensureNotNull(attributeNames, attributeValues);
ensureTrue(attributeNames.length == attributeValues.length,
"RDN.attributeNames and attributeValues must be the same size.");
ensureTrue(attributeNames.length > 0,
"RDN.attributeNames must not be empty.");
this.attributeNames = attributeNames;
this.schema = schema;
this.attributeValues = new ASN1OctetString[attributeValues.length];
for (int i=0; i < attributeValues.length; i++)
{
this.attributeValues[i] = new ASN1OctetString(attributeValues[i]);
}
}
RDN(final String attributeName, final ASN1OctetString attributeValue,
final Schema schema, final String rdnString)
{
this.rdnString = rdnString;
this.schema = schema;
attributeNames = new String[] { attributeName };
attributeValues = new ASN1OctetString[] { attributeValue };
}
RDN(final String[] attributeNames, final ASN1OctetString[] attributeValues,
final Schema schema, final String rdnString)
{
this.rdnString = rdnString;
this.schema = schema;
this.attributeNames = attributeNames;
this.attributeValues = attributeValues;
}
public RDN(final String rdnString)
throws LDAPException
{
this(rdnString, (Schema) null);
}
public RDN(final String rdnString, final Schema schema)
throws LDAPException
{
ensureNotNull(rdnString);
this.rdnString = rdnString;
this.schema = schema;
int pos = 0;
final int length = rdnString.length();
while ((pos < length) && (rdnString.charAt(pos) == ' '))
{
pos++;
}
int attrStartPos = pos;
while (pos < length)
{
final char c = rdnString.charAt(pos);
if ((c == ' ') || (c == '='))
{
break;
}
pos++;
}
String attrName = rdnString.substring(attrStartPos, pos);
if (attrName.length() == 0)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_NO_ATTR_NAME.get());
}
while ((pos < length) && (rdnString.charAt(pos) == ' '))
{
pos++;
}
if ((pos >= length) || (rdnString.charAt(pos) != '='))
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_NO_EQUAL_SIGN.get(attrName));
}
pos++;
while ((pos < length) && (rdnString.charAt(pos) == ' '))
{
pos++;
}
if (pos >= length)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_NO_ATTR_VALUE.get(attrName));
}
ASN1OctetString value;
if (rdnString.charAt(pos) == '#')
{
final byte[] valueArray = readHexString(rdnString, ++pos);
value = new ASN1OctetString(valueArray);
pos += (valueArray.length * 2);
}
else
{
final StringBuilder buffer = new StringBuilder();
pos = readValueString(rdnString, pos, buffer);
value = new ASN1OctetString(buffer.toString());
}
while ((pos < length) && (rdnString.charAt(pos) == ' '))
{
pos++;
}
if (pos >= length)
{
attributeNames = new String[] { attrName };
attributeValues = new ASN1OctetString[] { value };
return;
}
final ArrayList<String> nameList = new ArrayList<String>(5);
final ArrayList<ASN1OctetString> valueList =
new ArrayList<ASN1OctetString>(5);
nameList.add(attrName);
valueList.add(value);
if (rdnString.charAt(pos) == '+')
{
pos++;
}
else
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_VALUE_NOT_FOLLOWED_BY_PLUS.get());
}
if (pos >= length)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_PLUS_NOT_FOLLOWED_BY_AVP.get());
}
int numValues = 1;
while (pos < length)
{
while ((pos < length) && (rdnString.charAt(pos) == ' '))
{
pos++;
}
attrStartPos = pos;
while (pos < length)
{
final char c = rdnString.charAt(pos);
if ((c == ' ') || (c == '='))
{
break;
}
pos++;
}
attrName = rdnString.substring(attrStartPos, pos);
if (attrName.length() == 0)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_NO_ATTR_NAME.get());
}
while ((pos < length) && (rdnString.charAt(pos) == ' '))
{
pos++;
}
if ((pos >= length) || (rdnString.charAt(pos) != '='))
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_NO_EQUAL_SIGN.get(attrName));
}
pos++;
while ((pos < length) && (rdnString.charAt(pos) == ' '))
{
pos++;
}
if (pos >= length)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_NO_ATTR_VALUE.get(attrName));
}
if (rdnString.charAt(pos) == '#')
{
final byte[] valueArray = readHexString(rdnString, ++pos);
value = new ASN1OctetString(valueArray);
pos += (valueArray.length * 2);
}
else
{
final StringBuilder buffer = new StringBuilder();
pos = readValueString(rdnString, pos, buffer);
value = new ASN1OctetString(buffer.toString());
}
while ((pos < length) && (rdnString.charAt(pos) == ' '))
{
pos++;
}
nameList.add(attrName);
valueList.add(value);
numValues++;
if (pos >= length)
{
break;
}
else
{
if (rdnString.charAt(pos) == '+')
{
pos++;
}
else
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_VALUE_NOT_FOLLOWED_BY_PLUS.get());
}
}
if (pos >= length)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_PLUS_NOT_FOLLOWED_BY_AVP.get());
}
}
attributeNames = new String[numValues];
attributeValues = new ASN1OctetString[numValues];
for (int i=0; i < numValues; i++)
{
attributeNames[i] = nameList.get(i);
attributeValues[i] = valueList.get(i);
}
}
static byte[] readHexString(final String rdnString, final int startPos)
throws LDAPException
{
final int length = rdnString.length();
int pos = startPos;
final ByteBuffer buffer = ByteBuffer.allocate(length-pos);
hexLoop:
while (pos < length)
{
byte hexByte;
switch (rdnString.charAt(pos++))
{
case '0':
hexByte = 0x00;
break;
case '1':
hexByte = 0x10;
break;
case '2':
hexByte = 0x20;
break;
case '3':
hexByte = 0x30;
break;
case '4':
hexByte = 0x40;
break;
case '5':
hexByte = 0x50;
break;
case '6':
hexByte = 0x60;
break;
case '7':
hexByte = 0x70;
break;
case '8':
hexByte = (byte) 0x80;
break;
case '9':
hexByte = (byte) 0x90;
break;
case 'a':
case 'A':
hexByte = (byte) 0xA0;
break;
case 'b':
case 'B':
hexByte = (byte) 0xB0;
break;
case 'c':
case 'C':
hexByte = (byte) 0xC0;
break;
case 'd':
case 'D':
hexByte = (byte) 0xD0;
break;
case 'e':
case 'E':
hexByte = (byte) 0xE0;
break;
case 'f':
case 'F':
hexByte = (byte) 0xF0;
break;
case ' ':
case '+':
case ',':
case ';':
break hexLoop;
default:
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_INVALID_HEX_CHAR.get(
rdnString.charAt(pos-1), (pos-1)));
}
if (pos >= length)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_MISSING_HEX_CHAR.get());
}
switch (rdnString.charAt(pos++))
{
case '0':
break;
case '1':
hexByte |= 0x01;
break;
case '2':
hexByte |= 0x02;
break;
case '3':
hexByte |= 0x03;
break;
case '4':
hexByte |= 0x04;
break;
case '5':
hexByte |= 0x05;
break;
case '6':
hexByte |= 0x06;
break;
case '7':
hexByte |= 0x07;
break;
case '8':
hexByte |= 0x08;
break;
case '9':
hexByte |= 0x09;
break;
case 'a':
case 'A':
hexByte |= 0x0A;
break;
case 'b':
case 'B':
hexByte |= 0x0B;
break;
case 'c':
case 'C':
hexByte |= 0x0C;
break;
case 'd':
case 'D':
hexByte |= 0x0D;
break;
case 'e':
case 'E':
hexByte |= 0x0E;
break;
case 'f':
case 'F':
hexByte |= 0x0F;
break;
default:
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_INVALID_HEX_CHAR.get(
rdnString.charAt(pos-1), (pos-1)));
}
buffer.put(hexByte);
}
buffer.flip();
final byte[] valueArray = new byte[buffer.limit()];
buffer.get(valueArray);
return valueArray;
}
static int readValueString(final String rdnString, final int startPos,
final StringBuilder buffer)
throws LDAPException
{
final int bufferLength = buffer.length();
final int length = rdnString.length();
int pos = startPos;
boolean inQuotes = false;
valueLoop:
while (pos < length)
{
char c = rdnString.charAt(pos);
switch (c)
{
case '\\':
if ((pos+1) >= length)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_ENDS_WITH_BACKSLASH.get());
}
else
{
pos++;
c = rdnString.charAt(pos);
if (isHex(c))
{
pos = readEscapedHexString(rdnString, pos, buffer) - 1;
}
else
{
buffer.append(c);
}
}
break;
case '"':
if (inQuotes)
{
pos++;
while (pos < length)
{
c = rdnString.charAt(pos);
if ((c == '+') || (c == ',') || (c == ';'))
{
break;
}
else if (c != ' ')
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_CHAR_OUTSIDE_QUOTES.get(c,
(pos-1)));
}
pos++;
}
inQuotes = false;
break valueLoop;
}
else
{
if (pos == startPos)
{
inQuotes = true;
}
else
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_UNEXPECTED_DOUBLE_QUOTE.get(pos));
}
}
break;
case ' ':
if (inQuotes ||
(((pos+1) < length) && (rdnString.charAt(pos+1) != ' ')))
{
buffer.append(' ');
}
break;
case ',':
case ';':
case '+':
if (inQuotes)
{
buffer.append(c);
}
else
{
break valueLoop;
}
break;
default:
buffer.append(c);
break;
}
pos++;
}
if (inQuotes)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_UNCLOSED_DOUBLE_QUOTE.get());
}
int bufferPos = buffer.length() - 1;
int rdnStrPos = pos - 2;
while ((bufferPos > 0) && (buffer.charAt(bufferPos) == ' '))
{
if (rdnString.charAt(rdnStrPos) == '\\')
{
break;
}
else
{
buffer.deleteCharAt(bufferPos--);
rdnStrPos--;
}
}
if (buffer.length() == bufferLength)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_EMPTY_VALUE.get());
}
return pos;
}
private static int readEscapedHexString(final String rdnString,
final int startPos,
final StringBuilder buffer)
throws LDAPException
{
final int length = rdnString.length();
int pos = startPos;
final ByteBuffer byteBuffer = ByteBuffer.allocate(length - pos);
while (pos < length)
{
byte b;
switch (rdnString.charAt(pos++))
{
case '0':
b = 0x00;
break;
case '1':
b = 0x10;
break;
case '2':
b = 0x20;
break;
case '3':
b = 0x30;
break;
case '4':
b = 0x40;
break;
case '5':
b = 0x50;
break;
case '6':
b = 0x60;
break;
case '7':
b = 0x70;
break;
case '8':
b = (byte) 0x80;
break;
case '9':
b = (byte) 0x90;
break;
case 'a':
case 'A':
b = (byte) 0xA0;
break;
case 'b':
case 'B':
b = (byte) 0xB0;
break;
case 'c':
case 'C':
b = (byte) 0xC0;
break;
case 'd':
case 'D':
b = (byte) 0xD0;
break;
case 'e':
case 'E':
b = (byte) 0xE0;
break;
case 'f':
case 'F':
b = (byte) 0xF0;
break;
default:
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_INVALID_HEX_CHAR.get(
rdnString.charAt(pos-1), (pos-1)));
}
if (pos >= length)
{
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_MISSING_HEX_CHAR.get());
}
switch (rdnString.charAt(pos++))
{
case '0':
break;
case '1':
b |= 0x01;
break;
case '2':
b |= 0x02;
break;
case '3':
b |= 0x03;
break;
case '4':
b |= 0x04;
break;
case '5':
b |= 0x05;
break;
case '6':
b |= 0x06;
break;
case '7':
b |= 0x07;
break;
case '8':
b |= 0x08;
break;
case '9':
b |= 0x09;
break;
case 'a':
case 'A':
b |= 0x0A;
break;
case 'b':
case 'B':
b |= 0x0B;
break;
case 'c':
case 'C':
b |= 0x0C;
break;
case 'd':
case 'D':
b |= 0x0D;
break;
case 'e':
case 'E':
b |= 0x0E;
break;
case 'f':
case 'F':
b |= 0x0F;
break;
default:
throw new LDAPException(ResultCode.INVALID_DN_SYNTAX,
ERR_RDN_INVALID_HEX_CHAR.get(
rdnString.charAt(pos-1), (pos-1)));
}
byteBuffer.put(b);
if (((pos+1) < length) && (rdnString.charAt(pos) == '\\') &&
isHex(rdnString.charAt(pos+1)))
{
pos++;
continue;
}
else
{
break;
}
}
byteBuffer.flip();
final byte[] byteArray = new byte[byteBuffer.limit()];
byteBuffer.get(byteArray);
try
{
buffer.append(toUTF8String(byteArray));
}
catch (final Exception e)
{
debugException(e);
buffer.append(new String(byteArray));
}
return pos;
}
public static boolean isValidRDN(final String s)
{
try
{
new RDN(s);
return true;
}
catch (LDAPException le)
{
return false;
}
}
public boolean isMultiValued()
{
return (attributeNames.length != 1);
}
public String[] getAttributeNames()
{
return attributeNames;
}
public String[] getAttributeValues()
{
final String[] stringValues = new String[attributeValues.length];
for (int i=0; i < stringValues.length; i++)
{
stringValues[i] = attributeValues[i].stringValue();
}
return stringValues;
}
public byte[][] getByteArrayAttributeValues()
{
final byte[][] byteValues = new byte[attributeValues.length][];
for (int i=0; i < byteValues.length; i++)
{
byteValues[i] = attributeValues[i].getValue();
}
return byteValues;
}
Schema getSchema()
{
return schema;
}
public boolean hasAttribute(final String attributeName)
{
for (final String name : attributeNames)
{
if (name.equalsIgnoreCase(attributeName))
{
return true;
}
}
return false;
}
public boolean hasAttributeValue(final String attributeName,
final String attributeValue)
{
for (int i=0; i < attributeNames.length; i++)
{
if (attributeNames[i].equalsIgnoreCase(attributeName))
{
final Attribute a =
new Attribute(attributeName, schema, attributeValue);
final Attribute b = new Attribute(attributeName, schema,
attributeValues[i].stringValue());
if (a.equals(b))
{
return true;
}
}
}
return false;
}
public boolean hasAttributeValue(final String attributeName,
final byte[] attributeValue)
{
for (int i=0; i < attributeNames.length; i++)
{
if (attributeNames[i].equalsIgnoreCase(attributeName))
{
final Attribute a =
new Attribute(attributeName, schema, attributeValue);
final Attribute b = new Attribute(attributeName, schema,
attributeValues[i].getValue());
if (a.equals(b))
{
return true;
}
}
}
return false;
}
@Override()
public String toString()
{
if (rdnString == null)
{
final StringBuilder buffer = new StringBuilder();
toString(buffer, false);
rdnString = buffer.toString();
}
return rdnString;
}
public String toMinimallyEncodedString()
{
final StringBuilder buffer = new StringBuilder();
toString(buffer, true);
return buffer.toString();
}
public void toString(final StringBuilder buffer)
{
toString(buffer, false);
}
public void toString(final StringBuilder buffer,
final boolean minimizeEncoding)
{
if ((rdnString != null) && (! minimizeEncoding))
{
buffer.append(rdnString);
return;
}
for (int i=0; i < attributeNames.length; i++)
{
if (i > 0)
{
buffer.append('+');
}
buffer.append(attributeNames[i]);
buffer.append('=');
final String valueString = attributeValues[i].stringValue();
final int length = valueString.length();
for (int j=0; j < length; j++)
{
final char c = valueString.charAt(j);
switch (c)
{
case '\\':
case '#':
case '=':
case '"':
case '+':
case ',':
case ';':
case '<':
case '>':
buffer.append('\\');
buffer.append(c);
break;
case ' ':
if ((j == 0) || ((j+1) == length) ||
(((j+1) < length) && (valueString.charAt(j+1) == ' ')))
{
buffer.append("\\ ");
}
else
{
buffer.append(' ');
}
break;
case '\u0000':
buffer.append("\\00");
break;
default:
if ((! minimizeEncoding) && ((c < ' ') || (c > '~')))
{
hexEncode(c, buffer);
}
else
{
buffer.append(c);
}
break;
}
}
}
}
public String toNormalizedString()
{
if (normalizedString == null)
{
final StringBuilder buffer = new StringBuilder();
toNormalizedString(buffer);
normalizedString = buffer.toString();
}
return normalizedString;
}
public void toNormalizedString(final StringBuilder buffer)
{
if (attributeNames.length == 1)
{
final String name = normalizeAttrName(attributeNames[0]);
buffer.append(name);
buffer.append('=');
buffer.append(normalizeValue(name, attributeValues[0]));
}
else
{
final TreeMap<String,ASN1OctetString> valueMap =
new TreeMap<String,ASN1OctetString>();
for (int i=0; i < attributeNames.length; i++)
{
final String name = normalizeAttrName(attributeNames[i]);
valueMap.put(name, attributeValues[i]);
}
int i=0;
for (final Map.Entry<String,ASN1OctetString> entry : valueMap.entrySet())
{
if (i++ > 0)
{
buffer.append('+');
}
buffer.append(entry.getKey());
buffer.append('=');
buffer.append(normalizeValue(entry.getKey(), entry.getValue()));
}
}
}
private String normalizeAttrName(final String name)
{
String n = name;
if (schema != null)
{
final AttributeTypeDefinition at = schema.getAttributeType(name);
if (at != null)
{
n = at.getNameOrOID();
}
}
return toLowerCase(n);
}
public static String normalize(final String s)
throws LDAPException
{
return normalize(s, null);
}
public static String normalize(final String s, final Schema schema)
throws LDAPException
{
return new RDN(s, schema).toNormalizedString();
}
private StringBuilder normalizeValue(final String attributeName,
final ASN1OctetString value)
{
final MatchingRule matchingRule =
MatchingRule.selectEqualityMatchingRule(attributeName, schema);
ASN1OctetString rawNormValue;
try
{
rawNormValue = matchingRule.normalize(value);
}
catch (final Exception e)
{
debugException(e);
rawNormValue =
new ASN1OctetString(toLowerCase(value.stringValue()));
}
final String valueString = rawNormValue.stringValue();
final int length = valueString.length();
final StringBuilder buffer = new StringBuilder(length);
for (int i=0; i < length; i++)
{
final char c = valueString.charAt(i);
switch (c)
{
case '\\':
case '#':
case '=':
case '"':
case '+':
case ',':
case ';':
case '<':
case '>':
buffer.append('\\');
buffer.append(c);
break;
case ' ':
if ((i == 0) || ((i+1) == length) ||
(((i+1) < length) && (valueString.charAt(i+1) == ' ')))
{
buffer.append("\\ ");
}
else
{
buffer.append(' ');
}
break;
default:
if ((c < ' ') || (c > '~'))
{
hexEncode(c, buffer);
}
else
{
buffer.append(c);
}
break;
}
}
return buffer;
}
@Override()
public int hashCode()
{
return toNormalizedString().hashCode();
}
@Override()
public boolean equals(final Object o)
{
if (o == null)
{
return false;
}
if (o == this)
{
return true;
}
if (! (o instanceof RDN))
{
return false;
}
final RDN rdn = (RDN) o;
return (toNormalizedString().equals(rdn.toNormalizedString()));
}
public boolean equals(final String s)
throws LDAPException
{
if (s == null)
{
return false;
}
return equals(new RDN(s, schema));
}
public static boolean equals(final String s1, final String s2)
throws LDAPException
{
return new RDN(s1).equals(new RDN(s2));
}
public int compareTo(final RDN rdn)
{
return compare(this, rdn);
}
public int compare(final RDN rdn1, final RDN rdn2)
{
ensureNotNull(rdn1, rdn2);
return(rdn1.toNormalizedString().compareTo(rdn2.toNormalizedString()));
}
public static int compare(final String s1, final String s2)
throws LDAPException
{
return compare(s1, s2, null);
}
public static int compare(final String s1, final String s2,
final Schema schema)
throws LDAPException
{
return new RDN(s1, schema).compareTo(new RDN(s2, schema));
}
}