com.unboundid.ldap.sdk.schema.SchemaElement Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of unboundid-ldapsdk Show documentation
Show all versions of unboundid-ldapsdk Show documentation
The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use
Java API for communicating with LDAP directory servers and performing
related tasks like reading and writing LDIF, encoding and decoding data
using base64 and ASN.1 BER, and performing secure communication. This
package contains the Standard Edition of the LDAP SDK, which is a
complete, general-purpose library for communicating with LDAPv3 directory
servers.
/*
* Copyright 2007-2022 Ping Identity Corporation
* All Rights Reserved.
*/
/*
* Copyright 2007-2022 Ping Identity Corporation
*
* 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.
*/
/*
* Copyright (C) 2007-2022 Ping Identity Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (GPLv2 only)
* or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
package com.unboundid.ldap.sdk.schema;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.NotExtensible;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
/**
* This class provides a superclass for all schema element types, and defines a
* number of utility methods that may be used when parsing schema element
* strings.
*/
@NotExtensible()
@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_THREADSAFE)
public abstract class SchemaElement
implements Serializable
{
/**
* Indicates whether schema elements will be permitted to use an empty
* quoted string as the value of the {@code DESC} component.
*/
private static boolean allowEmptyDescription = Boolean.getBoolean(
"com.unboundid.ldap.sdk.schema.AllowEmptyDescription");
/**
* The serial version UID for this serializable class.
*/
private static final long serialVersionUID = -8249972237068748580L;
/**
* Indicates whether to allow schema elements to contain an empty string as
* the value for the {@code DESC} component. Although quoted strings are not
* allowed in schema elements as per RFC 4512 section 4.1, some directory
* servers allow it, and it may be necessary to support schema definitions
* used in conjunction with those servers.
*
* The LDAP SDK does not allow empty schema element descriptions by default,
* but it may be updated to allow it using either the
* {@link #setAllowEmptyDescription} method or by setting the value of the
* {@code com.unboundid.ldap.sdk.schema.AllowEmptyDescription} system property
* to {@code true} before this class is loaded.
*
* @return {@code true} if the LDAP SDK should allow schema elements with
* empty descriptions, or {@code false} if not.
*/
public static boolean allowEmptyDescription()
{
return allowEmptyDescription;
}
/**
* Specifies whether to allow schema elements to contain an empty string as
* the value for the {@code DESC} component. If specified, this will override
* the value of the
* {@code com.unboundid.ldap.sdk.schema.AllowEmptyDescription} system
* property.
*
* @param allowEmptyDescription Indicates whether to allow schema elements
* to contain an empty string as the value for
* the {@code DESC} component.
*/
public static void setAllowEmptyDescription(
final boolean allowEmptyDescription)
{
SchemaElement.allowEmptyDescription = allowEmptyDescription;
}
/**
* Skips over any any spaces in the provided string.
*
* @param s The string in which to skip the spaces.
* @param startPos The position at which to start skipping spaces.
* @param length The position of the end of the string.
*
* @return The position of the next non-space character in the string.
*
* @throws LDAPException If the end of the string was reached without
* finding a non-space character.
*/
static int skipSpaces(@NotNull final String s, final int startPos,
final int length)
throws LDAPException
{
int pos = startPos;
while ((pos < length) && (s.charAt(pos) == ' '))
{
pos++;
}
if (pos >= length)
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_SKIP_SPACES_NO_CLOSE_PAREN.get(s));
}
return pos;
}
/**
* Reads one or more hex-encoded bytes from the specified portion of the RDN
* string.
*
* @param s The string from which the data is to be read.
* @param startPos The position at which to start reading. This should
* be the first hex character immediately after the
* initial backslash.
* @param length The position of the end of the string.
* @param componentName The name of the component in the schema element
* definition whose value is being read.
* @param buffer The buffer to which the decoded string portion
* should be appended.
*
* @return The position at which the caller may resume parsing.
*
* @throws LDAPException If a problem occurs while reading hex-encoded
* bytes.
*/
private static int readEscapedHexString(@NotNull final String s,
final int startPos,
final int length,
@NotNull final String componentName,
@NotNull final StringBuilder buffer)
throws LDAPException
{
int pos = startPos;
final ByteBuffer byteBuffer = ByteBuffer.allocate(length - pos);
while (pos < length)
{
final byte b;
switch (s.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_ATTRIBUTE_SYNTAX,
ERR_SCHEMA_ELEM_INVALID_HEX_CHAR.get(s, s.charAt(pos-1),
(pos-1), componentName));
}
if (pos >= length)
{
throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
ERR_SCHEMA_ELEM_MISSING_HEX_CHAR.get(s, componentName));
}
switch (s.charAt(pos++))
{
case '0':
byteBuffer.put(b);
break;
case '1':
byteBuffer.put((byte) (b | 0x01));
break;
case '2':
byteBuffer.put((byte) (b | 0x02));
break;
case '3':
byteBuffer.put((byte) (b | 0x03));
break;
case '4':
byteBuffer.put((byte) (b | 0x04));
break;
case '5':
byteBuffer.put((byte) (b | 0x05));
break;
case '6':
byteBuffer.put((byte) (b | 0x06));
break;
case '7':
byteBuffer.put((byte) (b | 0x07));
break;
case '8':
byteBuffer.put((byte) (b | 0x08));
break;
case '9':
byteBuffer.put((byte) (b | 0x09));
break;
case 'a':
case 'A':
byteBuffer.put((byte) (b | 0x0A));
break;
case 'b':
case 'B':
byteBuffer.put((byte) (b | 0x0B));
break;
case 'c':
case 'C':
byteBuffer.put((byte) (b | 0x0C));
break;
case 'd':
case 'D':
byteBuffer.put((byte) (b | 0x0D));
break;
case 'e':
case 'E':
byteBuffer.put((byte) (b | 0x0E));
break;
case 'f':
case 'F':
byteBuffer.put((byte) (b | 0x0F));
break;
default:
throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
ERR_SCHEMA_ELEM_INVALID_HEX_CHAR.get(s, s.charAt(pos-1),
(pos-1), componentName));
}
if (((pos+1) < length) && (s.charAt(pos) == '\\') &&
StaticUtils.isHex(s.charAt(pos+1)))
{
// It appears that there are more hex-encoded bytes to follow, so keep
// reading.
pos++;
continue;
}
else
{
break;
}
}
byteBuffer.flip();
final byte[] byteArray = new byte[byteBuffer.limit()];
byteBuffer.get(byteArray);
buffer.append(StaticUtils.toUTF8String(byteArray));
return pos;
}
/**
* Reads a single-quoted string from the provided string.
*
* @param s The string from which to read the single-quoted
* string.
* @param startPos The position at which to start reading.
* @param length The position of the end of the string.
* @param componentName The name of the component in the schema element
* definition whose value is being read.
* @param buffer The buffer into which the single-quoted string
* should be placed (without the surrounding single
* quotes).
*
* @return The position of the first space immediately following the closing
* quote.
*
* @throws LDAPException If a problem is encountered while attempting to
* read the single-quoted string.
*/
static int readQDString(@NotNull final String s, final int startPos,
final int length, @NotNull final String componentName,
@NotNull final StringBuilder buffer)
throws LDAPException
{
// The first character must be a single quote.
if (s.charAt(startPos) != '\'')
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_EXPECTED_SINGLE_QUOTE.get(s, startPos,
componentName));
}
// Read until we find the next closing quote. If we find any hex-escaped
// characters along the way, then decode them.
int pos = startPos + 1;
while (pos < length)
{
final char c = s.charAt(pos++);
if (c == '\'')
{
// This is the end of the quoted string.
break;
}
else if (c == '\\')
{
// This designates the beginning of one or more hex-encoded bytes.
if (pos >= length)
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_ENDS_WITH_BACKSLASH.get(s, componentName));
}
pos = readEscapedHexString(s, pos, length, componentName, buffer);
}
else
{
buffer.append(c);
}
}
if ((pos >= length) || ((s.charAt(pos) != ' ') && (s.charAt(pos) != ')')))
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_NO_CLOSING_PAREN.get(s, componentName));
}
if (buffer.length() == 0)
{
if (! (allowEmptyDescription && componentName.equalsIgnoreCase("DESC")))
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_EMPTY_QUOTES.get(s, componentName));
}
}
return pos;
}
/**
* Reads one a set of one or more single-quoted strings from the provided
* string. The value to read may be either a single string enclosed in
* single quotes, or an opening parenthesis followed by a space followed by
* one or more space-delimited single-quoted strings, followed by a space and
* a closing parenthesis.
*
* @param s The string from which to read the single-quoted
* strings.
* @param startPos The position at which to start reading.
* @param length The position of the end of the string.
* @param componentName The name of the component in the schema element
* definition whose value is being read.
* @param valueList The list into which the values read may be placed.
*
* @return The position of the first space immediately following the end of
* the values.
*
* @throws LDAPException If a problem is encountered while attempting to
* read the single-quoted strings.
*/
static int readQDStrings(@NotNull final String s, final int startPos,
final int length,
@NotNull final String componentName,
@NotNull final ArrayList valueList)
throws LDAPException
{
// Look at the first character. It must be either a single quote or an
// opening parenthesis.
char c = s.charAt(startPos);
if (c == '\'')
{
// It's just a single value, so use the readQDString method to get it.
final StringBuilder buffer = new StringBuilder();
final int returnPos = readQDString(s, startPos, length, componentName,
buffer);
valueList.add(buffer.toString());
return returnPos;
}
else if (c == '(')
{
int pos = startPos + 1;
while (true)
{
pos = skipSpaces(s, pos, length);
c = s.charAt(pos);
if (c == ')')
{
// This is the end of the value list.
pos++;
break;
}
else if (c == '\'')
{
// This is the next value in the list.
final StringBuilder buffer = new StringBuilder();
pos = readQDString(s, pos, length, componentName, buffer);
valueList.add(buffer.toString());
}
else
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(s, startPos,
componentName));
}
}
if (valueList.isEmpty())
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_EMPTY_STRING_LIST.get(s, componentName));
}
if ((pos >= length) ||
((s.charAt(pos) != ' ') && (s.charAt(pos) != ')')))
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_NO_SPACE_AFTER_QUOTE.get(s, componentName));
}
return pos;
}
else
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_EXPECTED_QUOTE_OR_PAREN.get(s, startPos,
componentName));
}
}
/**
* Reads an OID value from the provided string. The OID value may be either a
* numeric OID or a string name. This implementation will be fairly lenient
* with regard to the set of characters that may be present, and it will
* allow the OID to be enclosed in single quotes.
*
* @param s The string from which to read the OID string.
* @param startPos The position at which to start reading.
* @param length The position of the end of the string.
* @param buffer The buffer into which the OID string should be placed.
*
* @return The position of the first space immediately following the OID
* string.
*
* @throws LDAPException If a problem is encountered while attempting to
* read the OID string.
*/
static int readOID(@NotNull final String s, final int startPos,
final int length, @NotNull final StringBuilder buffer)
throws LDAPException
{
// Read until we find the first space.
int pos = startPos;
boolean lastWasQuote = false;
while (pos < length)
{
final char c = s.charAt(pos);
if ((c == ' ') || (c == '$') || (c == ')'))
{
if (buffer.length() == 0)
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_EMPTY_OID.get(s));
}
return pos;
}
else if (((c >= 'a') && (c <= 'z')) ||
((c >= 'A') && (c <= 'Z')) ||
((c >= '0') && (c <= '9')) ||
(c == '-') || (c == '.') || (c == '_') ||
(c == '{') || (c == '}'))
{
if (lastWasQuote)
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s, (pos-1)));
}
buffer.append(c);
}
else if (c == '\'')
{
if (buffer.length() != 0)
{
lastWasQuote = true;
}
}
else
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID.get(s, pos));
}
pos++;
}
// We hit the end of the string before finding a space.
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID.get(s));
}
/**
* Reads one a set of one or more OID strings from the provided string. The
* value to read may be either a single OID string or an opening parenthesis
* followed by a space followed by one or more space-delimited OID strings,
* followed by a space and a closing parenthesis.
*
* @param s The string from which to read the OID strings.
* @param startPos The position at which to start reading.
* @param length The position of the end of the string.
* @param componentName The name of the component in the schema element
* definition whose value is being read.
* @param valueList The list into which the values read may be placed.
*
* @return The position of the first space immediately following the end of
* the values.
*
* @throws LDAPException If a problem is encountered while attempting to
* read the OID strings.
*/
static int readOIDs(@NotNull final String s, final int startPos,
final int length, @NotNull final String componentName,
@NotNull final ArrayList valueList)
throws LDAPException
{
// Look at the first character. If it's an opening parenthesis, then read
// a list of OID strings. Otherwise, just read a single string.
char c = s.charAt(startPos);
if (c == '(')
{
int pos = startPos + 1;
while (true)
{
pos = skipSpaces(s, pos, length);
c = s.charAt(pos);
if (c == ')')
{
// This is the end of the value list.
pos++;
break;
}
else if (c == '$')
{
// This is the delimiter before the next value in the list.
pos++;
pos = skipSpaces(s, pos, length);
final StringBuilder buffer = new StringBuilder();
pos = readOID(s, pos, length, buffer);
valueList.add(buffer.toString());
}
else if (valueList.isEmpty())
{
// This is the first value in the list.
final StringBuilder buffer = new StringBuilder();
pos = readOID(s, pos, length, buffer);
valueList.add(buffer.toString());
}
else
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_UNEXPECTED_CHAR_IN_OID_LIST.get(s, pos,
componentName));
}
}
if (valueList.isEmpty())
{
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_EMPTY_OID_LIST.get(s, componentName));
}
if (pos >= length)
{
// Technically, there should be a space after the closing parenthesis,
// but there are known cases in which servers (like Active Directory)
// omit this space, so we'll be lenient and allow a missing space. But
// it can't possibly be the end of the schema element definition, so
// that's still an error.
throw new LDAPException(ResultCode.DECODING_ERROR,
ERR_SCHEMA_ELEM_NO_SPACE_AFTER_OID_LIST.get(s, componentName));
}
return pos;
}
else
{
final StringBuilder buffer = new StringBuilder();
final int returnPos = readOID(s, startPos, length, buffer);
valueList.add(buffer.toString());
return returnPos;
}
}
/**
* Appends a properly-encoded representation of the provided value to the
* given buffer.
*
* @param value The value to be encoded and placed in the buffer.
* @param buffer The buffer to which the encoded value is to be appended.
*/
static void encodeValue(@NotNull final String value,
@NotNull final StringBuilder buffer)
{
final int length = value.length();
for (int i=0; i < length; i++)
{
final char c = value.charAt(i);
if ((c < ' ') || (c > '~') || (c == '\\') || (c == '\''))
{
StaticUtils.hexEncode(c, buffer);
}
else
{
buffer.append(c);
}
}
}
/**
* Retrieves the type of schema element that this object represents.
*
* @return The type of schema element that this object represents.
*/
@NotNull()
public abstract SchemaElementType getSchemaElementType();
/**
* Retrieves a hash code for this schema element.
*
* @return A hash code for this schema element.
*/
public abstract int hashCode();
/**
* Indicates whether the provided object is equal to this schema element.
*
* @param o The object for which to make the determination.
*
* @return {@code true} if the provided object may be considered equal to
* this schema element, or {@code false} if not.
*/
public abstract boolean equals(@Nullable Object o);
/**
* Indicates whether the two extension maps are equivalent.
*
* @param m1 The first schema element to examine.
* @param m2 The second schema element to examine.
*
* @return {@code true} if the provided extension maps are equivalent, or
* {@code false} if not.
*/
protected static boolean extensionsEqual(
@NotNull final Map m1,
@NotNull final Map m2)
{
if (m1.isEmpty())
{
return m2.isEmpty();
}
if (m1.size() != m2.size())
{
return false;
}
for (final Map.Entry e : m1.entrySet())
{
final String[] v1 = e.getValue();
final String[] v2 = m2.get(e.getKey());
if (! StaticUtils.arraysEqualOrderIndependent(v1, v2))
{
return false;
}
}
return true;
}
/**
* Converts the provided collection of strings to an array.
*
* @param c The collection to convert to an array. It may be {@code null}.
*
* @return A string array if the provided collection is non-{@code null}, or
* {@code null} if the provided collection is {@code null}.
*/
@Nullable()
static String[] toArray(@Nullable final Collection c)
{
if (c == null)
{
return null;
}
return c.toArray(StaticUtils.NO_STRINGS);
}
/**
* Retrieves a string representation of this schema element, in the format
* described in RFC 4512.
*
* @return A string representation of this schema element, in the format
* described in RFC 4512.
*/
@Override()
@NotNull()
public abstract String toString();
}