initial commit

This commit is contained in:
Jörg Prante 2024-08-05 23:28:21 +02:00
commit bdb39ae448
529 changed files with 69284 additions and 0 deletions

16
.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
/.settings
/.classpath
/.project
/.gradle
**/data
**/work
**/logs
**/.idea
**/target
**/out
**/build
.DS_Store
*.iml
*~
*.key
*.crt

202
LICENSE.txt Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View file

@ -0,0 +1,3 @@
module org.xbib.net.ldap.asnone {
exports org.xbib.asn1;
}

View file

@ -0,0 +1,55 @@
package org.xbib.asn1;
/**
* Abstract base class for custom DER tag types.
*
*/
public abstract class AbstractDERTag implements DERTag {
/**
* Tag number.
*/
private final int tagNo;
/**
* Flag indicating whether value is primitive or constructed.
*/
private final boolean constructed;
/**
* Creates a new tag with given tag number.
*
* @param number Tag number.
* @param isConstructed True for constructed tag, false otherwise.
*/
public AbstractDERTag(final int number, final boolean isConstructed) {
tagNo = number;
constructed = isConstructed;
}
@Override
public int getTagNo() {
return tagNo;
}
@Override
public boolean isConstructed() {
return constructed;
}
@Override
public int getTagByte() {
return constructed ? tagNo | ASN_CONSTRUCTED : tagNo;
}
@Override
public String toString() {
return name() + "(" + tagNo + ")";
}
}

View file

@ -0,0 +1,81 @@
package org.xbib.asn1;
import java.nio.ByteBuffer;
/**
* Provides functionality common to DER types implementations.
*
*/
public abstract class AbstractDERType {
/**
* Length of short form integers.
*/
private static final int SHORT_FORM_INT_LENGTH = 127;
/**
* Constructed tag.
*/
private final int derTag;
/**
* Creates a new abstract der type.
*
* @param tag to encode for this type
*/
public AbstractDERType(final DERTag tag) {
derTag = tag.getTagByte();
}
/**
* DER encodes the supplied items with the tag associated with this type. If the length is greater than 127 bytes the
* long form is always expressed using 4 bytes.
*
* @param items to encode
* @return DER encoded items
*/
protected byte[] encode(final byte[]... items) {
int itemLength = 0;
if (items != null) {
for (byte[] b : items) {
if (b != null) {
itemLength += b.length;
}
}
}
final byte[] lengthBytes;
if (itemLength <= SHORT_FORM_INT_LENGTH) {
lengthBytes = new byte[]{(byte) itemLength};
} else {
// use 4 bytes for all long form integers
// CheckStyle:MagicNumber OFF
lengthBytes = new byte[]{
(byte) 0x84,
(byte) (itemLength >>> 24),
(byte) (itemLength >>> 16),
(byte) (itemLength >>> 8),
(byte) itemLength,
};
// CheckStyle:MagicNumber ON
}
// add 1 for the type tag, 1 or 5 for the length
final ByteBuffer encodedItem = ByteBuffer.allocate(itemLength + 1 + lengthBytes.length);
encodedItem.put((byte) derTag);
for (byte b : lengthBytes) {
encodedItem.put(b);
}
if (items != null) {
for (byte[] b : items) {
if (b != null) {
encodedItem.put(b);
}
}
}
return encodedItem.array();
}
}

View file

@ -0,0 +1,35 @@
package org.xbib.asn1;
/**
* Parse handler for managing and initializing an object.
*
* @param <T> type of object initialized by this handler
*/
public abstract class AbstractParseHandler<T> implements ParseHandler {
/**
* Object to initialize.
*/
private final T object;
/**
* Creates a new abstract parse handler.
*
* @param t object to initialize
*/
public AbstractParseHandler(final T t) {
object = t;
}
/**
* Returns the object.
*
* @return object
*/
public T getObject() {
return object;
}
}

View file

@ -0,0 +1,48 @@
package org.xbib.asn1;
/**
* Generic application-specific tag.
*
*/
public class ApplicationDERTag extends AbstractDERTag {
/**
* Generic tag name "APP" for an application-specific type.
*/
public static final String TAG_NAME = "APP";
/**
* Application class is 01b in first two high-order bits.
*/
public static final int TAG_CLASS = 0x40;
/**
* Creates a new application-specific tag with given tag number.
*
* @param number Tag number.
* @param isConstructed True for constructed tag, false otherwise.
*/
public ApplicationDERTag(final int number, final boolean isConstructed) {
super(number, isConstructed);
}
@Override
public int getTagByte() {
return super.getTagByte() | TAG_CLASS;
}
@Override
public String name() {
return TAG_NAME + "(" + getTagNo() + ")";
}
@Override
public String toString() {
return name();
}
}

View file

@ -0,0 +1,87 @@
package org.xbib.asn1;
/**
* Converts booleans to and from their DER encoded format.
*
*/
public class BooleanType extends AbstractDERType implements DEREncoder {
/**
* Boolean true byte representation.
*/
private static final byte TRUE_BYTE = (byte) 0xff;
/**
* Boolean false byte representation.
*/
private static final byte FALSE_BYTE = (byte) 0x00;
/**
* Boolean to encode.
*/
private final byte[] derItem;
/**
* Creates a new boolean type.
*
* @param item to DER encode
*/
public BooleanType(final boolean item) {
super(UniversalDERTag.BOOL);
derItem = toBytes(item);
}
/**
* Creates a new boolean type.
*
* @param tag der tag associated with this type
* @param item to DER encode
* @throws IllegalArgumentException if the der tag is constructed
*/
public BooleanType(final DERTag tag, final boolean item) {
super(tag);
if (tag.isConstructed()) {
throw new IllegalArgumentException("DER tag must not be constructed");
}
derItem = toBytes(item);
}
/**
* Converts bytes in the buffer to a boolean by reading from the current position to the limit.
*
* @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of boolean bytes
* and the limit is set beyond the last byte of integer data.
* @return decoded bytes as a boolean.
*/
public static boolean decode(final DERBuffer encoded) {
final byte[] bytes = encoded.getRemainingBytes();
if (bytes.length > 1) {
throw new IllegalArgumentException("Boolean cannot be longer than 1 byte");
}
if (bytes[0] == TRUE_BYTE) {
return true;
} else if (bytes[0] == FALSE_BYTE) {
return false;
} else {
throw new IllegalArgumentException("Invalid boolean value: " + (int) bytes[0]);
}
}
/**
* Converts the supplied boolean to a byte array.
*
* @param b to convert
* @return byte array
*/
public static byte[] toBytes(final boolean b) {
return new byte[]{b ? TRUE_BYTE : FALSE_BYTE};
}
@Override
public byte[] encode() {
return encode(derItem);
}
}

View file

@ -0,0 +1,51 @@
package org.xbib.asn1;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* Encodes constructed types to their DER format.
*
*/
public class ConstructedDEREncoder extends AbstractDERType implements DEREncoder {
/**
* Encoders in this sequence.
*/
private final DEREncoder[] derEncoders;
/**
* Creates a new sequence encoder.
*
* @param tag der tag associated with this type
* @param encoders to encode in this sequence
*/
public ConstructedDEREncoder(final DERTag tag, final DEREncoder... encoders) {
super(tag);
if (!tag.isConstructed()) {
throw new IllegalArgumentException("DER tag must be constructed");
}
if (encoders == null || encoders.length == 0) {
throw new IllegalArgumentException("Encoders cannot be null or empty");
}
derEncoders = encoders;
}
@Override
public byte[] encode() {
final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
try (bytes) {
for (DEREncoder encoder : derEncoders) {
bytes.write(encoder.encode());
}
}
} catch (IOException e) {
throw new IllegalStateException("Encode failed", e);
}
return encode(bytes.toByteArray());
}
}

View file

@ -0,0 +1,42 @@
package org.xbib.asn1;
/**
* Generic context-specific tag.
*
*/
public class ContextDERTag extends AbstractDERTag {
/**
* Generic tag name "CTX" for a context-specific type.
*/
public static final String TAG_NAME = "CTX";
/**
* Context-specific class is 10b in first two high-order bits.
*/
public static final int TAG_CLASS = 0x80;
/**
* Creates a new context-specific tag with given tag number.
*
* @param number Tag number.
* @param isConstructed True for constructed tag, false otherwise.
*/
public ContextDERTag(final int number, final boolean isConstructed) {
super(number, isConstructed);
}
@Override
public int getTagByte() {
return super.getTagByte() | TAG_CLASS;
}
@Override
public String name() {
return TAG_NAME + "(" + getTagNo() + ")";
}
}

View file

@ -0,0 +1,67 @@
package org.xbib.asn1;
import java.math.BigInteger;
/**
* Converts context types to their DER encoded format.
*
*/
public class ContextType extends AbstractDERType implements DEREncoder {
/**
* Data to encode.
*/
private final byte[] derItem;
/**
* Creates a new context type.
*
* @param index of this item in the context
* @param item to encode
*/
public ContextType(final int index, final byte[] item) {
super(new ContextDERTag(index, false));
derItem = item;
}
/**
* Creates a new context type.
*
* @param index of this item in the context
* @param item to encode
*/
public ContextType(final int index, final String item) {
this(index, OctetStringType.toBytes(item));
}
/**
* Creates a new context type.
*
* @param index of this item in the context
* @param item to encode
*/
public ContextType(final int index, final boolean item) {
this(index, BooleanType.toBytes(item));
}
/**
* Creates a new context type.
*
* @param index of this item in the context
* @param item to encode
*/
public ContextType(final int index, final BigInteger item) {
this(index, IntegerType.toBytes(item));
}
@Override
public byte[] encode() {
return encode(derItem);
}
}

View file

@ -0,0 +1,40 @@
package org.xbib.asn1;
/**
* Describes the tag of an application-specific, context-specific, or private DER type where the tag name may be
* specified for clarity in application code.
*
*/
public class CustomDERTag extends AbstractDERTag {
/**
* Tag name.
*/
private final String tagName;
/**
* Creates a new custom DER tag.
*
* @param number of the tag
* @param name of the tag
* @param isConstructed whether this tag is primitive or constructed
*/
public CustomDERTag(final int number, final String name, final boolean isConstructed) {
super(number, isConstructed);
tagName = name;
}
@Override
public String name() {
return tagName;
}
@Override
public String toString() {
return name() + "(" + getTagNo() + ")";
}
}

View file

@ -0,0 +1,128 @@
package org.xbib.asn1;
/**
* Byte buffer used for DER parsing.
*
*/
public interface DERBuffer {
/**
* Returns this buffer's position.
*
* @return position of this buffer
*/
int position();
/**
* Sets this buffer's position.
*
* @param newPosition The new position value; must be non-negative
* and no larger than the current limit
* @return This buffer
* @throws IllegalArgumentException if the preconditions on newPosition do not hold
*/
DERBuffer position(int newPosition);
/**
* Returns this buffer's limit.
*
* @return limit of this buffer
*/
int limit();
/**
* Sets this buffer's limit.
*
* @param newLimit The new limit value; must be non-negative
* and no larger than this buffer's capacity
* @return This buffer
* @throws IllegalArgumentException if the preconditions on newLimit do not hold
*/
DERBuffer limit(int newLimit);
/**
* Sets the position to zero and the limit to the capacity.
*
* <p>This method does not actually erase the data in the buffer.</p>
*
* @return This buffer
*/
DERBuffer clear();
/**
* Returns the number of elements between the current position and the limit.
*
* @return number of elements remaining in this buffer
*/
default int remaining() {
return limit() - position();
}
/**
* Returns whether there are any elements between the current position and the limit.
*
* @return true iff there is at least one element remaining in this buffer
*/
default boolean hasRemaining() {
return position() < limit();
}
/**
* Returns this buffer's capacity.
*
* @return capacity of this buffer
*/
int capacity();
/**
* Relative <i>get</i> method. Reads the byte at this buffer's current position and then increments the position.
*
* @return byte at the buffer's current position
*/
byte get();
/**
* Relative bulk <i>get</i> method.
*
* @param dst destination array
* @return This buffer
*/
DERBuffer get(byte[] dst);
/**
* Returns the bytes remaining in the buffer. Those bytes between {@link #position()} and {@link #limit()}.
*
* @return remaining bytes
*/
default byte[] getRemainingBytes() {
final byte[] b = new byte[remaining()];
get(b);
return b;
}
/**
* Creates a new DER buffer whose content is a shared sub-sequence of this buffer's content.
*
* <p>The content of the new buffer will start at this buffer's current position. Changes to this buffer's content
* will be visible in the new buffer, and vice versa; the two buffers' position and limit will be independent.</p>
*
* <p>The new buffer's position will be zero, its capacity and its limit will be the number of bytes remaining in this
* buffer.</p>
*
* @return The new byte buffer
*/
DERBuffer slice();
}

View file

@ -0,0 +1,17 @@
package org.xbib.asn1;
/**
* Interface for encoding DER objects.
*
*/
public interface DEREncoder {
/**
* Encode this object into its DER type.
*
* @return DER encoded object
*/
byte[] encode();
}

View file

@ -0,0 +1,213 @@
package org.xbib.asn1;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
/**
* This class provides a SAX-like parsing facility for DER-encoded data where elements of interest in the parse tree may
* be registered to handlers via the {@link #registerHandler} methods. {@link DERPath} strings are used to map handlers
* to elements of interest.
*/
public class DERParser {
/**
* Handlers for DER paths.
*/
private final Map<DERPath, ParseHandler> handlerMap = new HashMap<>();
/**
* Permutations of the current path.
*/
private final Queue<DERPath> permutations = new ArrayDeque<>();
public DERParser() {
}
/**
* Registers the supplied handler to fire when the supplied path is encountered.
*
* @param path to register
* @param handler to associate with the path
*/
public void registerHandler(final DERPath path, final ParseHandler handler) {
handlerMap.put(path, handler);
}
/**
* Parse a DER-encoded data structure by calling registered handlers when points of interest are encountered in the
* parse tree.
*
* @param encoded DER-encoded bytes.
*/
public void parse(final DERBuffer encoded) {
parseTags(encoded);
}
/**
* Reads a DER tag from a single byte at the current position of the given buffer. The buffer position is naturally
* advanced one byte in this operation.
*
* @param encoded Buffer containing DER-encoded bytes positioned at tag.
* @return Tag or null if no universal tag or application-specific tag is known that matches the byte read in.
*/
public DERTag readTag(final DERBuffer encoded) {
if (encoded.position() >= encoded.limit()) {
return null;
}
final DERTag tag;
final byte b = encoded.get();
// CheckStyle:MagicNumber OFF
final int tagNo = b & 0x1F;
final boolean constructed = (b & 0x20) == 0x20;
// Read class from first two high-order bits
switch (b & 0xC0) {
case UniversalDERTag.TAG_CLASS:
tag = UniversalDERTag.fromTagNo(tagNo);
break;
case ApplicationDERTag.TAG_CLASS:
tag = new ApplicationDERTag(tagNo, constructed);
break;
case ContextDERTag.TAG_CLASS:
tag = new ContextDERTag(tagNo, constructed);
break;
default:
// Private class (class 11b)
throw new IllegalArgumentException("Private classes not supported.");
}
// CheckStyle:MagicNumber ON
return tag;
}
/**
* Reads the length of a DER-encoded value from the given byte buffer. The buffer is expected to be positioned at the
* byte immediately following the tag byte, which is where the length byte(s) begin(s). Invocation of this method has
* two generally beneficial side effects:
*
* <ol>
* <li>Buffer is positioned at <em>start</em> of value bytes.</li>
* <li>Buffer limit is set to the <em>end</em> of value bytes.</li>
* </ol>
*
* @param encoded buffer containing DER-encoded bytes positioned at start of length byte(s).
* @return number of bytes occupied by tag value.
*/
public int readLength(final DERBuffer encoded) {
int length = 0;
final byte b = encoded.get();
// CheckStyle:MagicNumber OFF
if ((b & 0x80) == 0x80) {
final int len = b & 0x7F;
if (len > 0) {
final int limit = encoded.limit();
encoded.limit(encoded.position() + len);
length = IntegerType.decodeUnsignedPrimitive(encoded);
encoded.limit(limit);
}
} else {
length = b;
}
// CheckStyle:MagicNumber ON
return length;
}
/**
* Reads the supplied DER encoded bytes and invokes handlers as configured paths are encountered.
*
* @param encoded to parse
*/
private void parseTags(final DERBuffer encoded) {
int index = 0;
while (encoded.position() < encoded.limit() && !handlerMap.isEmpty()) {
final DERTag tag = readTag(encoded);
if (tag != null) {
addTag(tag, index++);
parseTag(tag, encoded);
removeTag();
}
}
}
/**
* Invokes the parse handler for the current path and advances to the next position in the encoded bytes.
*
* @param tag to inspect for internal tags
* @param encoded to parse
*/
private void parseTag(final DERTag tag, final DERBuffer encoded) {
final int limit = encoded.limit();
final int end = readLength(encoded) + encoded.position();
final int start = encoded.position();
// Invoke handlers for all permutations of current path
ParseHandler handler;
for (DERPath p : permutations) {
handler = handlerMap.get(p);
if (handler != null) {
encoded.limit(end).position(start);
handler.handle(this, encoded);
}
}
if (tag.isConstructed()) {
parseTags(encoded);
}
encoded.limit(limit).position(end);
}
/**
* Add the given tag at the specified index to all permutations of the current parser path and increases the number of
* permutations as necessary to satisfy the following relation:
*
* <pre>size = 2^n</pre>
*
* <p>where n is the path length.</p>
*
* @param tag to add to path.
* @param index of tag relative to parent.
*/
private void addTag(final DERTag tag, final int index) {
if (permutations.isEmpty()) {
permutations.add(new DERPath().pushNode(tag.name()));
permutations.add(new DERPath().pushNode(tag.name(), index));
} else {
final Collection<DERPath> generation = new ArrayDeque<>(permutations.size());
for (DERPath p : permutations) {
generation.add(new DERPath(p).pushNode(tag.name()));
p.pushNode(tag.name(), index);
}
permutations.addAll(generation);
}
}
/**
* Removes the tag at the leaf position of all permutations of the current parser path, and reduces the number of
* permutations as necessary to satisfy the following relation:
*
* <pre>size = 2^n</pre>
*
* <p>where n is the path length.</p>
*/
private void removeTag() {
final int half = permutations.size() / 2;
while (permutations.size() > half) {
permutations.remove();
}
permutations.forEach(DERPath::popNode);
}
}

View file

@ -0,0 +1,353 @@
package org.xbib.asn1;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Describes paths to individual elements of an encoded DER object that may be addressed during parsing to associate a
* parsed element with a handler to handle that element. Consider the following production rule for a complex type that
* may be DER encoded:
*
* <pre>
*
* BankAccountSet ::= SET OF {
* account BankAccount
* }
*
* BankAccount ::= SEQUENCE OF {
* accountNumber OCTET STRING,
* accountName OCTET STRING,
* accountType AccountType,
* balance REAL
* }
*
* AccountType ::= ENUM {
* checking (0),
* savings (1)
* }
*
* </pre>
*
* <p>Given an instance of BankAccountSet with two elements, the path to the balance of each bank account in the set is
* given by the following expression:</p>
*
* <pre>/SET/SEQ/REAL</pre>
*
* <p>Individual child elements can be accessed by explicitly mentioning the index of the item relative to its parent.
* For example, the second bank account in the set can be accessed as follows:</p>
*
* <pre>/SET/SEQ[1]</pre>
*
* <p>Node names in DER paths are constrained to the following:</p>
*
* <ul>
* <li>{@link UniversalDERTag} tag names</li>
* <li>{@link ApplicationDERTag#TAG_NAME}</li>
* <li>{@link ContextDERTag#TAG_NAME}</li>
* </ul>
*
* @see DERParser
*/
public class DERPath {
/**
* Separates nodes in a path specification.
*/
public static final String PATH_SEPARATOR = "/";
/**
* General pattern for DER path nodes.
*/
private static final Pattern NODE_PATTERN;
/**
* hash code seed.
*/
private static final int HASH_CODE_SEED = 601;
static {
final StringBuilder validNames = new StringBuilder();
validNames.append(ApplicationDERTag.TAG_NAME).append("\\(\\d+\\)|");
validNames.append(ContextDERTag.TAG_NAME).append("\\(\\d+\\)|");
for (UniversalDERTag tag : UniversalDERTag.values()) {
validNames.append('|').append(tag.name());
}
NODE_PATTERN = Pattern.compile(String.format("(%s)(?:\\[(\\d+)\\])?", validNames));
}
/**
* Describes the path as a FIFO set of nodes.
*/
private final Deque<Node> nodeStack = new ArrayDeque<>();
/**
* Creates an empty path specification.
*/
public DERPath() {
this(PATH_SEPARATOR);
}
/**
* Copy constructor.
*
* @param path to read nodes from
*/
public DERPath(final DERPath path) {
nodeStack.addAll(path.nodeStack);
}
/**
* Creates a path specification from its string representation.
*
* @param pathSpec string representation of a path, e.g. /SEQ[1]/CHOICE.
*/
public DERPath(final String pathSpec) {
final String[] nodes = pathSpec.split(PATH_SEPARATOR);
for (String node : nodes) {
if ("".equals(node)) {
continue;
}
// Normalize node names to upper case
nodeStack.add(toNode(LdapUtils.toUpperCaseAscii(node)));
}
}
/**
* Converts a string representation of a node into a {@link Node} object.
*
* @param node String representation of node.
* @return Node corresponding to given string representation.
* @throws IllegalArgumentException for an invalid node name.
*/
static Node toNode(final String node) {
final Matcher matcher = NODE_PATTERN.matcher(node);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid node: " + node);
}
final String name = matcher.group(1);
final String index = matcher.group(2);
if (index != null) {
return new Node(name, Integer.parseInt(index));
}
return new Node(name);
}
/**
* Appends a node to the path.
*
* @param name of the path element to add
* @return This instance with new node appended.
*/
public DERPath pushNode(final String name) {
nodeStack.addLast(new Node(name));
return this;
}
/**
* Appends a node to the path with the given child index.
*
* @param name of the path element to add
* @param index child index
* @return This instance with new node appended.
*/
public DERPath pushNode(final String name, final int index) {
nodeStack.addLast(new Node(name, index));
return this;
}
/**
* Examines the first node in the path without removing it.
*
* @return first node in the path or null if no nodes remain
*/
public String peekNode() {
return nodeStack.peek().toString();
}
/**
* Removes the last node in the path.
*
* @return last node in the path or null if no more nodes remain.
*/
public String popNode() {
if (nodeStack.isEmpty()) {
return null;
}
return nodeStack.removeLast().toString();
}
/**
* Gets the number of nodes in the path.
*
* @return node count.
*/
public int getSize() {
return nodeStack.size();
}
/**
* Determines whether the path contains any nodes.
*
* @return True if path contains 0 nodes, false otherwise.
*/
public boolean isEmpty() {
return nodeStack.isEmpty();
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (o instanceof DERPath v) {
return LdapUtils.areEqual(
nodeStack != null ? nodeStack.toArray() : null,
v.nodeStack != null ? v.nodeStack.toArray() : null);
}
return false;
}
@Override
public int hashCode() {
int hc = HASH_CODE_SEED;
if (nodeStack != null && !nodeStack.isEmpty()) {
for (Node n : nodeStack) {
hc = HASH_CODE_SEED * hc + n.hashCode();
}
}
return hc;
}
@Override
public String toString() {
if (nodeStack == null) {
return "";
}
final StringBuilder sb = new StringBuilder(nodeStack.size() * 10);
for (Node node : nodeStack) {
sb.append(PATH_SEPARATOR);
node.toString(sb);
}
return sb.toString();
}
/**
* DER path node encapsulates the path name and its location among other children that share a common parent.
*
*/
static class Node {
/**
* hash code seed.
*/
private static final int HASH_CODE_SEED = 607;
/**
* Name of this node.
*/
private final String name;
/**
* Index of this node.
*/
private final int childIndex;
/**
* Creates a new node with an indeterminate index.
*
* @param n name of this node
*/
Node(final String n) {
name = n;
childIndex = -1;
}
/**
* Creates a new node with the given index.
*
* @param n name of this node
* @param i child index location of this node in the path
*/
Node(final String n, final int i) {
if (i < 0) {
throw new IllegalArgumentException("Child index cannot be negative.");
}
name = n;
childIndex = i;
}
/**
* Returns the name.
*
* @return name
*/
public String getName() {
return name;
}
/**
* Returns the child index.
*
* @return child index
*/
public int getChildIndex() {
return childIndex;
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (o instanceof Node v) {
return LdapUtils.areEqual(name, v.name) &&
LdapUtils.areEqual(childIndex, v.childIndex);
}
return false;
}
@Override
public int hashCode() {
int result = HASH_CODE_SEED + (name == null ? 0 : name.hashCode());
result = HASH_CODE_SEED * result + childIndex;
return result;
}
@Override
public String toString() {
// CheckStyle:MagicNumber OFF
final StringBuilder sb = new StringBuilder(name.length() + 4);
// CheckStyle:MagicNumber ON
toString(sb);
return sb.toString();
}
/**
* Appends the string representation of this instance to the given string builder.
*
* @param builder Builder to hold string representation of this instance.
*/
public void toString(final StringBuilder builder) {
builder.append(name);
if (childIndex < 0) {
return;
}
builder.append('[').append(childIndex).append(']');
}
}
}

View file

@ -0,0 +1,46 @@
package org.xbib.asn1;
/**
* Describes the tag of a DER-encoded type.
*
*/
public interface DERTag {
/**
* Constructed tags should have the 6th bit set.
*/
int ASN_CONSTRUCTED = 0x20;
/**
* Gets the decimal value of the tag.
*
* @return decimal tag number.
*/
int getTagNo();
/**
* Gets the name of the tag.
*
* @return tag name.
*/
String name();
/**
* Determines whether the tag is constructed or primitive.
*
* @return true if constructed, false if primitive.
*/
boolean isConstructed();
/**
* Gets the value of this tag for encoding.
*
* @return byte value of this tag
*/
int getTagByte();
}

View file

@ -0,0 +1,127 @@
package org.xbib.asn1;
import java.nio.ByteBuffer;
/**
* {@link DERBuffer} that uses a {@link ByteBuffer}.
*
*/
public class DefaultDERBuffer implements DERBuffer {
/**
* Underlying byte buffer.
*/
private final ByteBuffer buffer;
/**
* Creates a new default DER buffer. See {@link ByteBuffer#allocate(int)}.
*
* @param capacity of this buffer
*/
public DefaultDERBuffer(final int capacity) {
buffer = ByteBuffer.allocate(capacity);
}
/**
* Creates a new default DER buffer. See {@link ByteBuffer#wrap(byte[])}.
*
* @param array contents of the buffer
*/
public DefaultDERBuffer(final byte[] array) {
buffer = ByteBuffer.wrap(array);
}
/**
* Creates a new default DER buffer.
*
* @param buf existing byte buffer
*/
public DefaultDERBuffer(final ByteBuffer buf) {
buffer = buf;
}
/**
* Creates a new default DER buffer and sets the initial position and limit.
*
* @param buf existing byte buffer
* @param pos initial buffer position
* @param lim initial buffer limit
*/
public DefaultDERBuffer(final ByteBuffer buf, final int pos, final int lim) {
buffer = buf;
buffer.position(pos);
buffer.limit(lim);
}
@Override
public int position() {
return buffer.position();
}
@Override
public DERBuffer position(final int newPosition) {
buffer.position(newPosition);
return this;
}
@Override
public int limit() {
return buffer.limit();
}
@Override
public int capacity() {
return buffer.capacity();
}
@Override
public DERBuffer limit(final int newLimit) {
buffer.limit(newLimit);
return this;
}
@Override
public DERBuffer clear() {
buffer.clear();
return this;
}
@Override
public byte get() {
return buffer.get();
}
@Override
public DERBuffer get(final byte[] dst) {
buffer.get(dst);
return this;
}
@Override
public DERBuffer slice() {
return new DefaultDERBuffer(buffer.slice());
}
@Override
public String toString() {
return getClass().getName() + "@" + hashCode() + "::" +
"pos=" + position() + ", " +
"lim=" + limit() + ", " +
"cap=" + capacity();
}
}

View file

@ -0,0 +1,133 @@
package org.xbib.asn1;
import java.math.BigInteger;
/**
* Converts arbitrary-precision integers to and from their DER encoded format.
*
*/
public class IntegerType extends AbstractDERType implements DEREncoder {
/**
* Integer to encode.
*/
private final byte[] derItem;
/**
* Creates a new integer type.
*
* @param item to DER encode
*/
public IntegerType(final BigInteger item) {
super(UniversalDERTag.INT);
derItem = item.toByteArray();
}
/**
* Creates a new integer type.
*
* @param item to DER encode
*/
public IntegerType(final int item) {
super(UniversalDERTag.INT);
derItem = BigInteger.valueOf(item).toByteArray();
}
/**
* Creates a new integer type.
*
* @param tag der tag associated with this type
* @param item to DER encode
* @throws IllegalArgumentException if the der tag is constructed
*/
public IntegerType(final DERTag tag, final BigInteger item) {
super(tag);
if (tag.isConstructed()) {
throw new IllegalArgumentException("DER tag must not be constructed");
}
derItem = item.toByteArray();
}
/**
* Creates a new integer type.
*
* @param tag der tag associated with this type
* @param item to DER encode
* @throws IllegalArgumentException if the der tag is constructed
*/
public IntegerType(final DERTag tag, final int item) {
super(tag);
if (tag.isConstructed()) {
throw new IllegalArgumentException("DER tag must not be constructed");
}
derItem = BigInteger.valueOf(item).toByteArray();
}
/**
* Converts bytes in the buffer to an integer by reading from the current position to the limit, which assumes the
* bytes of the integer are in big-endian order.
*
* @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of integer bytes
* and the limit is set beyond the last byte of integer data.
* @return decoded bytes as an integer of arbitrary size.
*/
public static BigInteger decode(final DERBuffer encoded) {
return new BigInteger(encoded.getRemainingBytes());
}
/**
* Converts bytes in the buffer to an unsigned integer by reading from the current position to the limit, which
* assumes the bytes of the integer are in big-endian order.
*
* @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of integer bytes
* and the limit is set beyond the last byte of integer data.
* @return decoded bytes as an unsigned integer of arbitrary size.
*/
public static BigInteger decodeUnsigned(final DERBuffer encoded) {
return new BigInteger(1, encoded.getRemainingBytes());
}
/**
* Converts bytes in the buffer to an unsigned primitive integer by reading from the current position to the limit,
* which assumes the bytes of the integer are in big-endian order. This method reads up to 4 bytes from the buffer.
*
* @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of integer bytes
* and the limit is set beyond the last byte of integer data.
* @return decoded bytes as an unsigned integer.
* @throws IllegalArgumentException if the buffer contains more than 4 bytes
*/
public static int decodeUnsignedPrimitive(final DERBuffer encoded) {
// CheckStyle:MagicNumber OFF
final byte[] bytes = encoded.getRemainingBytes();
if (bytes.length > 4) {
throw new IllegalArgumentException("Buffer length must be <= 4 bytes");
}
int i = 0;
for (byte b : bytes) {
i <<= 8;
i |= b & 0xFF;
}
return i;
// CheckStyle:MagicNumber ON
}
/**
* Converts the supplied big integer to a byte array.
*
* @param i to convert
* @return byte array
*/
public static byte[] toBytes(final BigInteger i) {
return i.toByteArray();
}
@Override
public byte[] encode() {
return encode(derItem);
}
}

View file

@ -0,0 +1,508 @@
package org.xbib.asn1;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.regex.Pattern;
/**
* Provides utility methods for this package.
*
*/
public final class LdapUtils {
/**
* Size of buffer in bytes to use when reading files.
*/
private static final int READ_BUFFER_SIZE = 128;
/**
* Prime number to assist in calculating hash codes.
*/
private static final int HASH_CODE_PRIME = 113;
/**
* Pattern to match ipv4 addresses.
*/
private static final Pattern IPV4_PATTERN = Pattern.compile(
"^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)" +
"(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");
/**
* Pattern to match ipv6 addresses.
*/
private static final Pattern IPV6_STD_PATTERN = Pattern.compile("^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$");
/**
* Pattern to match ipv6 hex compressed addresses.
*/
private static final Pattern IPV6_HEX_COMPRESSED_PATTERN = Pattern.compile(
"^((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::" +
"((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)$");
/**
* Pattern that matches control characters.
*/
private static final Pattern CNTRL_PATTERN = Pattern.compile("\\p{Cntrl}");
/**
* Default constructor.
*/
private LdapUtils() {
}
/**
* This will convert the supplied value to a base64 encoded string. Returns null if the supplied byte array is null.
*
* @param value to base64 encode
* @return base64 encoded value
*/
public static String base64Encode(final byte... value) {
return value != null ? new String(Base64.getEncoder().encode(value), StandardCharsets.UTF_8) : null;
}
/**
* This will convert the supplied value to a base64 encoded string. Returns null if the supplied string is null.
*
* @param value to base64 encode
* @return base64 encoded value
*/
public static String base64Encode(final String value) {
return value != null ? base64Encode(value.getBytes(StandardCharsets.UTF_8)) : null;
}
/**
* This will convert the supplied value to a UTF-8 encoded string. Returns null if the supplied byte array is null.
*
* @param value to UTF-8 encode
* @return UTF-8 encoded value
*/
public static String utf8Encode(final byte[] value) {
return utf8Encode(value, true);
}
/**
* This will convert the supplied value to a UTF-8 encoded string.
*
* @param value to UTF-8 encode
* @param allowNull whether to throw {@link NullPointerException} if value is null
* @return UTF-8 encoded value
* @throws NullPointerException if allowNull is false and value is null
*/
public static String utf8Encode(final byte[] value, final boolean allowNull) {
if (!allowNull && value == null) {
throw new NullPointerException("Cannot UTF-8 encode null value");
}
return value != null ? new String(value, StandardCharsets.UTF_8) : null;
}
/**
* This will convert the supplied value to a UTF-8 encoded byte array. Returns null if the supplied string is null.
*
* @param value to UTF-8 encode
* @return UTF-8 encoded value
*/
public static byte[] utf8Encode(final String value) {
return utf8Encode(value, true);
}
/**
* This will convert the supplied value to a UTF-8 encoded byte array.
*
* @param value to UTF-8 encode
* @param allowNull whether to throw {@link NullPointerException} if value is null
* @return UTF-8 encoded value
* @throws NullPointerException if allowNull is false and value is null
*/
public static byte[] utf8Encode(final String value, final boolean allowNull) {
if (!allowNull && value == null) {
throw new NullPointerException("Cannot UTF-8 encode null value");
}
return value != null ? value.getBytes(StandardCharsets.UTF_8) : null;
}
/**
* Removes the space character from both the beginning and end of the supplied value.
*
* @param value to trim space character from
* @return trimmed value or same value if no trim was performed
*/
public static String trimSpace(final String value) {
if (value == null || value.isEmpty()) {
return value;
}
int startIndex = 0;
int endIndex = value.length();
while (startIndex < endIndex && value.charAt(startIndex) == ' ') {
startIndex++;
}
while (startIndex < endIndex && value.charAt(endIndex - 1) == ' ') {
endIndex--;
}
if (startIndex == 0 && endIndex == value.length()) {
return value;
}
return value.substring(startIndex, endIndex);
}
/**
* Changes the supplied value by replacing multiple spaces with a single space.
*
* @param value to compress spaces
* @param trim whether to remove any leading or trailing space characters
* @return normalized value or value if no compress was performed
*/
public static String compressSpace(final String value, final boolean trim) {
if (value == null || value.isEmpty()) {
return value;
}
final StringBuilder sb = new StringBuilder();
boolean foundSpace = false;
for (int i = 0; i < value.length(); i++) {
final char ch = value.charAt(i);
if (ch == ' ') {
if (i == value.length() - 1) {
// last char is a space
sb.append(ch);
}
foundSpace = true;
} else {
if (foundSpace) {
sb.append(' ');
}
sb.append(ch);
foundSpace = false;
}
}
if (sb.length() == 0 && foundSpace) {
return trim ? "" : " ";
}
if (trim) {
if (sb.length() > 0 && sb.charAt(0) == ' ') {
sb.deleteCharAt(0);
}
if (sb.length() > 0 && sb.charAt(sb.length() - 1) == ' ') {
sb.deleteCharAt(sb.length() - 1);
}
}
return sb.toString();
}
/**
* This will decode the supplied value as a base64 encoded string to a byte[]. Returns null if the supplied string is
* null.
*
* @param value to base64 decode
* @return base64 decoded value
*/
public static byte[] base64Decode(final String value) {
try {
return value != null ? Base64.getDecoder().decode(value.getBytes(StandardCharsets.UTF_8)) : null;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Error decoding value: " + value, e);
}
}
/**
* Converts the supplied string to lower case. If the string contains non-ascii characters, {@link Locale#ROOT} is
* used.
*
* @param s to lower case
* @return new lower case string
*/
public static String toLowerCase(final String s) {
if (s == null || s.isEmpty()) {
return s;
}
// CheckStyle:MagicNumber OFF
// if string contains non-ascii, use locale specific lowercase
if (s.chars().anyMatch(c -> c > 0x7F)) {
return s.toLowerCase(Locale.ROOT);
}
return toLowerCaseAscii(s);
}
/**
* Converts the characters A-Z to a-z.
*
* @param s to lower case
* @return new string with lower case alphabetical characters
* @throws IllegalArgumentException if the supplied string contains non-ascii characters
*/
public static String toLowerCaseAscii(final String s) {
if (s == null || s.isEmpty()) {
return s;
}
// mutate A-Z to a-z
// CheckStyle:MagicNumber OFF
final char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (chars[i] > 0x7F) {
throw new IllegalArgumentException("String contains non-ascii characters: " + s);
} else if (chars[i] >= 'A' && chars[i] <= 'Z') {
chars[i] = (char) (chars[i] + 32);
}
}
// CheckStyle:MagicNumber ON
return new String(chars);
}
/**
* Converts the supplied string to upper case. If the string contains non-ascii characters, {@link Locale#ROOT} is
* used.
*
* @param s to upper case
* @return new upper case string
*/
public static String toUpperCase(final String s) {
if (s == null || s.isEmpty()) {
return s;
}
// CheckStyle:MagicNumber OFF
// if string contains non-ascii, use locale specific uppercase
if (s.chars().anyMatch(c -> c > 0x7F)) {
return s.toUpperCase(Locale.ROOT);
}
return toUpperCaseAscii(s);
}
/**
* Converts the characters a-z to A-Z.
*
* @param s to upper case
* @return new string with upper case alphabetical characters
* @throws IllegalArgumentException if the supplied string contains non-ascii characters
*/
public static String toUpperCaseAscii(final String s) {
if (s == null || s.isEmpty()) {
return s;
}
// mutate a-z to A-Z
// CheckStyle:MagicNumber OFF
final char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (chars[i] > 0x7F) {
throw new IllegalArgumentException("String contains non-ascii characters: " + s);
} else if (chars[i] >= 'a' && chars[i] <= 'z') {
chars[i] = (char) (chars[i] - 32);
}
}
// CheckStyle:MagicNumber ON
return new String(chars);
}
/**
* Reads the data in the supplied stream and returns it as a byte array.
*
* @param is stream to read
* @return bytes read from the stream
* @throws IOException if an error occurs reading data
*/
public static byte[] readInputStream(final InputStream is)
throws IOException {
final ByteArrayOutputStream data = new ByteArrayOutputStream();
try (is; data) {
final byte[] buffer = new byte[READ_BUFFER_SIZE];
int length;
while ((length = is.read(buffer)) != -1) {
data.write(buffer, 0, length);
}
}
return data.toByteArray();
}
/**
* Concatenates multiple arrays together.
*
* @param <T> type of array
* @param first array to concatenate. Cannot be null.
* @param rest of the arrays to concatenate. May be null.
* @return array containing the concatenation of all parameters
*/
@SuppressWarnings("unchecked")
public static <T> T[] concatArrays(final T[] first, final T[]... rest) {
int totalLength = first.length;
for (T[] array : rest) {
if (array != null) {
totalLength += array.length;
}
}
final T[] result = Arrays.copyOf(first, totalLength);
int offset = first.length;
for (T[] array : rest) {
if (array != null) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
}
return result;
}
/**
* Determines equality of the supplied objects. Array types are automatically detected.
*
* @param o1 to test equality of
* @param o2 to test equality of
* @return whether o1 equals o2
*/
public static boolean areEqual(final Object o1, final Object o2) {
if (o1 == o2) {
return true;
}
final boolean areEqual;
if (o1 instanceof boolean[] && o2 instanceof boolean[]) {
areEqual = Arrays.equals((boolean[]) o1, (boolean[]) o2);
} else if (o1 instanceof byte[] && o2 instanceof byte[]) {
areEqual = Arrays.equals((byte[]) o1, (byte[]) o2);
} else if (o1 instanceof char[] && o2 instanceof char[]) {
areEqual = Arrays.equals((char[]) o1, (char[]) o2);
} else if (o1 instanceof double[] && o2 instanceof double[]) {
areEqual = Arrays.equals((double[]) o1, (double[]) o2);
} else if (o1 instanceof float[] && o2 instanceof float[]) {
areEqual = Arrays.equals((float[]) o1, (float[]) o2);
} else if (o1 instanceof int[] && o2 instanceof int[]) {
areEqual = Arrays.equals((int[]) o1, (int[]) o2);
} else if (o1 instanceof long[] && o2 instanceof long[]) {
areEqual = Arrays.equals((long[]) o1, (long[]) o2);
} else if (o1 instanceof short[] && o2 instanceof short[]) {
areEqual = Arrays.equals((short[]) o1, (short[]) o2);
} else if (o1 instanceof Object[] && o2 instanceof Object[]) {
areEqual = Arrays.deepEquals((Object[]) o1, (Object[]) o2);
} else {
areEqual = o1 != null && o1.equals(o2);
}
return areEqual;
}
/**
* Computes a hash code for the supplied objects using the supplied seed. If a Collection type is found it is iterated
* over.
*
* @param seed odd/prime number
* @param objects to calculate hashCode for
* @return hash code for the supplied objects
*/
public static int computeHashCode(final int seed, final Object... objects) {
if (objects == null || objects.length == 0) {
return seed * HASH_CODE_PRIME;
}
int hc = seed;
for (Object object : objects) {
hc = HASH_CODE_PRIME * hc;
if (object != null) {
if (object instanceof List<?> || object instanceof Queue<?>) {
int index = 1;
for (Object o : (Collection<?>) object) {
hc += computeHashCode(o) * index++;
}
} else if (object instanceof Collection<?>) {
for (Object o : (Collection<?>) object) {
hc += computeHashCode(o);
}
} else {
hc += computeHashCode(object);
}
}
}
return hc;
}
/**
* Computes a hash code for the supplied object. Checks for arrays of primitives and Objects then delegates to the
* {@link Arrays} class. Otherwise {@link Object#hashCode()} is invoked.
*
* @param object to calculate hash code for
* @return hash code
*/
private static int computeHashCode(final Object object) {
int hc = 0;
if (object instanceof boolean[]) {
hc += Arrays.hashCode((boolean[]) object);
} else if (object instanceof byte[]) {
hc += Arrays.hashCode((byte[]) object);
} else if (object instanceof char[]) {
hc += Arrays.hashCode((char[]) object);
} else if (object instanceof double[]) {
hc += Arrays.hashCode((double[]) object);
} else if (object instanceof float[]) {
hc += Arrays.hashCode((float[]) object);
} else if (object instanceof int[]) {
hc += Arrays.hashCode((int[]) object);
} else if (object instanceof long[]) {
hc += Arrays.hashCode((long[]) object);
} else if (object instanceof short[]) {
hc += Arrays.hashCode((short[]) object);
} else if (object instanceof Object[]) {
hc += Arrays.deepHashCode((Object[]) object);
} else {
hc += object.hashCode();
}
return hc;
}
/**
* Returns whether the supplied string represents an IP address. Matches both IPv4 and IPv6 addresses.
*
* @param s to match
* @return whether the supplied string represents an IP address
*/
public static boolean isIPAddress(final String s) {
return
s != null &&
(IPV4_PATTERN.matcher(s).matches() || IPV6_STD_PATTERN.matcher(s).matches() ||
IPV6_HEX_COMPRESSED_PATTERN.matcher(s).matches());
}
/**
* Looks for the supplied system property value and loads a class with that name. The default constructor for that
* class is then returned.
*
* @param property whose value is a class
* @return class constructor or null if no system property was found
* @throws IllegalArgumentException if an error occurs instantiating the constructor
*/
public static Constructor<?> createConstructorFromProperty(final String property) {
final String clazz = System.getProperty(property);
if (clazz != null) {
try {
return Class.forName(clazz).getDeclaredConstructor();
} catch (Exception e) {
throw new IllegalArgumentException("Error getting declared constructor for " + clazz, e);
}
}
return null;
}
}

View file

@ -0,0 +1,25 @@
package org.xbib.asn1;
/**
* Convenience type for a tag with a null value.
*
*/
public class NullType extends AbstractDERType implements DEREncoder {
/**
* Creates a new null type.
*
* @param tag der tag associated with this type
*/
public NullType(final DERTag tag) {
super(tag);
}
@Override
public byte[] encode() {
return encode((byte[]) null);
}
}

View file

@ -0,0 +1,90 @@
package org.xbib.asn1;
/**
* Converts strings to and from their DER encoded format.
*
*/
public class OctetStringType extends AbstractDERType implements DEREncoder {
/**
* String to encode.
*/
private final byte[] derItem;
/**
* Creates a new octet string type.
*
* @param item to DER encode
*/
public OctetStringType(final String item) {
this(LdapUtils.utf8Encode(item, false));
}
/**
* Creates a new octet string type.
*
* @param item to DER encode
*/
public OctetStringType(final byte[] item) {
super(UniversalDERTag.OCTSTR);
derItem = item;
}
/**
* Creates a new octet string type.
*
* @param tag der tag associated with this type
* @param item to DER encode
* @throws IllegalArgumentException if the der tag is constructed
*/
public OctetStringType(final DERTag tag, final String item) {
this(tag, LdapUtils.utf8Encode(item, false));
}
/**
* Creates a new octet string type.
*
* @param tag der tag associated with this type
* @param item to DER encode
* @throws IllegalArgumentException if the der tag is constructed
*/
public OctetStringType(final DERTag tag, final byte[] item) {
super(tag);
if (tag.isConstructed()) {
throw new IllegalArgumentException("DER tag must not be constructed");
}
derItem = item;
}
/**
* Converts bytes in the buffer to a string by reading from the current position to the limit, which assumes the bytes
* of the string are in big-endian order.
*
* @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of string bytes and
* the limit is set beyond the last byte of string data.
* @return decoded bytes as an string
*/
public static String decode(final DERBuffer encoded) {
return LdapUtils.utf8Encode(encoded.getRemainingBytes(), false);
}
/**
* Converts the supplied string to a byte array using the UTF-8 encoding.
*
* @param s to convert
* @return byte array
*/
public static byte[] toBytes(final String s) {
return LdapUtils.utf8Encode(s, false);
}
@Override
public byte[] encode() {
return encode(derItem);
}
}

View file

@ -0,0 +1,248 @@
package org.xbib.asn1;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.StringTokenizer;
/**
* Converts object identifiers to and from their DER encoded format.
*
*/
public class OidType extends AbstractDERType implements DEREncoder {
/**
* Integer to encode.
*/
private final byte[] derItem;
/**
* Creates a new oid type.
*
* @param item to DER encode
*/
public OidType(final String item) {
super(UniversalDERTag.OID);
derItem = toBytes(parse(item));
}
/**
* Creates a new oid type.
*
* @param item to DER encode
*/
public OidType(final int[] item) {
super(UniversalDERTag.OID);
derItem = toBytes(item);
}
/**
* Creates a new oid type.
*
* @param tag der tag associated with this type
* @param item to DER encode
* @throws IllegalArgumentException if the der tag is constructed
*/
public OidType(final DERTag tag, final String item) {
super(tag);
if (tag.isConstructed()) {
throw new IllegalArgumentException("DER tag must not be constructed");
}
derItem = toBytes(parse(item));
}
/**
* Creates a new oid type.
*
* @param tag der tag associated with this type
* @param item to DER encode
* @throws IllegalArgumentException if the der tag is constructed
*/
public OidType(final DERTag tag, final int[] item) {
super(tag);
if (tag.isConstructed()) {
throw new IllegalArgumentException("DER tag must not be constructed");
}
derItem = toBytes(item);
}
/**
* Converts bytes in the buffer to an OID by reading from the current position to the limit, which assumes the bytes
* of the integer are in big-endian order.
*
* @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of OID bytes and
* the limit is set beyond the last byte of OID data.
* @return decoded bytes as an OID.
*/
public static String decode(final DERBuffer encoded) {
final StringBuilder sb = new StringBuilder();
final int firstId = encoded.get();
// CheckStyle:MagicNumber OFF
if (firstId < 40) {
sb.append("0").append(".").append(firstId).append(".");
} else if (firstId < 80) {
sb.append("1").append(".").append(firstId - 40).append(".");
} else {
sb.append("2").append(".").append(firstId - 80).append(".");
}
// CheckStyle:MagicNumber ON
while (encoded.hasRemaining()) {
sb.append(readInt(encoded)).append(".");
}
sb.setLength(sb.length() - 1);
return sb.toString();
}
/**
* Converts the supplied list of oid components to a byte array.
*
* @param oid to convert
* @return byte array
* @throws IllegalArgumentException if the oid is not valid. See {@link #isValid(int[])}
*/
public static byte[] toBytes(final int[] oid) {
isValid(oid);
final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
try {
// CheckStyle:MagicNumber OFF
if (oid[0] < 2) {
// should always fit into one byte, since oid[1] must be <= 38
bytes.write((oid[0] * 40) + oid[1]);
} else {
bytes.write(toBytes((oid[0] * 40) + oid[1]));
}
for (int i = 2; i < oid.length; i++) {
bytes.write(toBytes(oid[i]));
}
// CheckStyle:MagicNumber ON
} finally {
bytes.close();
}
} catch (IOException e) {
throw new IllegalStateException("Byte conversion failed", e);
}
return bytes.toByteArray();
}
/**
* Checks whether the supplied oid is valid. Oids must meet the following criteria:
*
* <ul>
* <li>must not be null and must have at least 2 elements</li>
* <li>components must not be negative</li>
* <li>first component must be 0, 1, or 2</li>
* <li>if first component 0 or 1, second component must be &lt;= 38</li>
* </ul>
*
* @param oid to check
* @throws IllegalArgumentException if the oid is not valid.
*/
protected static void isValid(final int[] oid) {
// CheckStyle:MagicNumber OFF
if (oid == null || oid.length < 2) {
throw new IllegalArgumentException("OIDs must have at least two components");
}
if (oid[0] < 0 || oid[0] > 2) {
throw new IllegalArgumentException("The first OID must be 0, 1, or 2");
}
if (oid[0] < 2 && oid[1] > 39) {
throw new IllegalArgumentException("The second OID must be less than or equal to 38");
}
for (int i : oid) {
if (i < 0) {
throw new IllegalArgumentException("OIDs cannot be negative");
}
}
// CheckStyle:MagicNumber ON
}
/**
* Converts the supplied oid component to a byte array. The length of the byte array is the minimal size needed to
* contain the oid component.
*
* @param component to convert to bytes
* @return oid bytes
*/
protected static byte[] toBytes(final int component) {
// CheckStyle:MagicNumber OFF
final byte[] buffer = new byte[4];
int size = 0;
int val = component;
while (val != 0) {
if (size > 0) {
buffer[size++] = (byte) ((val & 0x7F) | 0x80);
} else {
buffer[size++] = (byte) (val & 0x7F);
}
val >>>= 7;
}
final byte[] bytes = new byte[size];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = buffer[--size];
}
return bytes;
// CheckStyle:MagicNumber ON
}
/**
* Reads the necessary encoded bytes from the supplied buffer to create an integer.
*
* @param buffer to read
* @return OID component integer
*/
protected static int readInt(final DERBuffer buffer) {
// CheckStyle:MagicNumber OFF
int val = 0;
for (int i = 0; i < 4; i++) {
final byte b = buffer.get();
if (i == 0 && b == 0x80) {
throw new IllegalArgumentException("Component starts with 0x80");
}
val <<= 7;
val |= b & 0x7F;
if ((b & 0x80) == 0) {
return val;
}
}
// CheckStyle:MagicNumber ON
throw new IllegalArgumentException("Integer greater than 4 bytes in size");
}
/**
* Converts the supplied oid into an array on integers.
*
* @param oid to parse
* @return array of oid components
* @throws IllegalArgumentException if the oid is not valid. See {@link #isValid(int[])}
*/
public static int[] parse(final String oid) {
if (oid == null) {
throw new IllegalArgumentException("OID cannot be null");
}
final StringTokenizer st = new StringTokenizer(oid, ".");
final int[] oids = new int[st.countTokens()];
int i = 0;
while (st.hasMoreTokens()) {
try {
oids[i++] = Integer.parseInt(st.nextToken());
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
}
}
isValid(oids);
return oids;
}
@Override
public byte[] encode() {
return encode(derItem);
}
}

View file

@ -0,0 +1,18 @@
package org.xbib.asn1;
/**
* Provides a hook in the DER parser for handling specific paths as they are encountered.
*
*/
public interface ParseHandler {
/**
* Invoked when a DER path is encountered that belongs to this parse handler.
*
* @param parser that invoked this handler
* @param encoded to handle
*/
void handle(DERParser parser, DERBuffer encoded);
}

View file

@ -0,0 +1,248 @@
package org.xbib.asn1;
import java.util.HashMap;
import java.util.Map;
/**
* Enumeration with common BER/DER universal tag types.
*
*/
public enum UniversalDERTag implements DERTag {
/**
* BOOLEAN type.
*/
BOOL(1, false),
/**
* INTEGER type.
*/
INT(2, false),
/**
* BITSTRING type.
*/
BITSTR(3, false),
/**
* OCTETSTRING type.
*/
OCTSTR(4, false),
/**
* NULL type.
*/
NULL(5, false),
/**
* OBJECT IDENTIFIER type.
*/
OID(6, false),
/**
* ObjectDescriptor type.
*/
ODESC(7, false),
/**
* EXTERNAL type.
*/
EXT(8, false),
/**
* REAL type.
*/
REAL(9, false),
/**
* ENUMERATED type.
*/
ENUM(10, false),
/**
* EMBEDDED PDV type.
*/
EMBPDV(11, false),
/**
* UTF8String type.
*/
UTF8STR(12, false),
/**
* RELATIVE-OID type.
*/
ROID(13, false),
/**
* SEQUENCE type.
*/
SEQ(16, true),
/**
* SET type.
*/
SET(17, true),
/**
* NumericString type.
*/
NUMSTR(18, false),
/**
* PrintableString type.
*/
PRINTSTR(19, false),
/**
* T61String type.
*/
T61STR(20, false),
/**
* VideotexString type.
*/
VTEXSTR(21, false),
/**
* IA5String type.
*/
IA5STR(22, false),
/**
* UTCTime type.
*/
UTCTIME(23, false),
/**
* GeneralizedTime type.
*/
GENTIME(24, false),
/**
* GraphicString type.
*/
GRAPHICSTR(25, false),
/**
* ISO646String type.
*/
ISO646STR(26, false),
/**
* GeneralString type.
*/
GENSTR(27, false),
/**
* UniversalString type.
*/
UNISTR(28, false),
/**
* CharacterString type.
*/
CHARSTR(29, false),
/**
* BMPString type.
*/
BMPSTR(30, false);
/**
* Universal tag class is 00b in first two high-order bytes.
*/
public static final int TAG_CLASS = 0;
/**
* Maps tag numbers to tags.
*/
private static final Map<Integer, UniversalDERTag> TAGNO_MAP = new HashMap<>();
/**
* Maps tag names to tags.
*/
private static final Map<String, UniversalDERTag> TAGNAME_MAP = new HashMap<>();
static {
// Initializes tag mapping
for (UniversalDERTag tag : UniversalDERTag.values()) {
TAGNO_MAP.put(tag.getTagNo(), tag);
TAGNAME_MAP.put(tag.name(), tag);
}
}
/**
* Tag number.
*/
private final int tagNo;
/**
* Flag indicating whether value is primitive or constructed.
*/
private final boolean constructed;
/**
* Creates a new universal DER tag.
*
* @param number of the tag
* @param isConstructed whether this tag is primitive or constructed
*/
UniversalDERTag(final int number, final boolean isConstructed) {
tagNo = number;
constructed = isConstructed;
}
/**
* Looks up a universal tag from a tag number.
*
* @param number tag number.
* @return tag object corresponding to given number.
* @throws IllegalArgumentException if tag is unknown
*/
public static UniversalDERTag fromTagNo(final int number) {
final UniversalDERTag derTag = TAGNO_MAP.get(number);
if (derTag == null) {
throw new IllegalArgumentException("Unknown tag number: " + number);
}
return derTag;
}
/**
* Looks up a universal tag from a tag name. This method differs from {@link #valueOf(String)} in that it does not
* throw for unknown names.
*
* @param name tag name.
* @return tag object corresponding to given name or null if no tag of the given name is found.
*/
public static UniversalDERTag fromTagName(final String name) {
return TAGNAME_MAP.get(name);
}
/**
* Gets the decimal value of the tag.
*
* @return decimal tag number.
*/
@Override
public int getTagNo() {
return tagNo;
}
/**
* Determines whether the tag is constructed or primitive.
*
* @return true if constructed, false if primitive.
*/
@Override
public boolean isConstructed() {
return constructed;
}
@Override
public int getTagByte() {
return constructed ? tagNo | ASN_CONSTRUCTED : tagNo;
}
}

View file

@ -0,0 +1,100 @@
package org.xbib.asn1;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
* Converts UUIDs to and from their DER encoded format. See RFC 4122.
*
*/
public class UuidType extends AbstractDERType implements DEREncoder {
/**
* Number of bytes in a uuid.
*/
private static final int UUID_LENGTH = 16;
/**
* UUID to encode.
*/
private final byte[] derItem;
/**
* Creates a new uuid type.
*
* @param item to DER encode
*/
public UuidType(final UUID item) {
super(UniversalDERTag.OCTSTR);
derItem = toBytes(item);
}
/**
* Creates a new uuid type.
*
* @param tag der tag associated with this type
* @param item to DER encode
* @throws IllegalArgumentException if the der tag is constructed
*/
public UuidType(final DERTag tag, final UUID item) {
super(tag);
if (tag.isConstructed()) {
throw new IllegalArgumentException("DER tag must not be constructed");
}
derItem = toBytes(item);
}
/**
* Converts bytes in the buffer to a uuid by reading from the current position to the limit.
*
* @param encoded buffer containing DER-encoded data where the buffer is positioned at the start of uuid bytes and
* the limit is set beyond the last byte of uuid data.
* @return decoded bytes as a uuid.
*/
public static UUID decode(final DERBuffer encoded) {
final long mostSig = readLong(encoded);
final long leastSig = readLong(encoded);
return new UUID(mostSig, leastSig);
}
/**
* Reads the next 8 bytes from the supplied buffer to create a long.
*
* @param buffer to read
* @return UUID component integer
*/
protected static long readLong(final DERBuffer buffer) {
// CheckStyle:MagicNumber OFF
return
(((long) buffer.get()) << 56) |
(((long) buffer.get() & 0xff) << 48) |
(((long) buffer.get() & 0xff) << 40) |
(((long) buffer.get() & 0xff) << 32) |
(((long) buffer.get() & 0xff) << 24) |
(((long) buffer.get() & 0xff) << 16) |
(((long) buffer.get() & 0xff) << 8) |
(((long) buffer.get() & 0xff));
// CheckStyle:MagicNumber ON
}
/**
* Converts the supplied uuid to a byte array.
*
* @param uuid to convert
* @return byte array
*/
public static byte[] toBytes(final UUID uuid) {
final ByteBuffer buffer = ByteBuffer.wrap(new byte[UUID_LENGTH]);
buffer.putLong(uuid.getMostSignificantBits());
buffer.putLong(uuid.getLeastSignificantBits());
return buffer.array();
}
@Override
public byte[] encode() {
return encode(derItem);
}
}

36
build.gradle Normal file
View file

@ -0,0 +1,36 @@
plugins {
id 'maven-publish'
id 'signing'
id "io.github.gradle-nexus.publish-plugin" version "2.0.0-rc-1"
}
wrapper {
gradleVersion = libs.versions.gradle.get()
distributionType = Wrapper.DistributionType.BIN
}
ext {
user = 'joerg'
name = 'net-ldap'
description = 'LDAP classes for Java'
inceptionYear = '2034'
url = 'https://xbib.org/' + user + '/' + name
scmUrl = 'https://xbib.org/' + user + '/' + name
scmConnection = 'scm:git:git://xbib.org/' + user + '/' + name + '.git'
scmDeveloperConnection = 'scm:git:ssh://forgejo@xbib.org:' + user + '/' + name + '.git'
issueManagementSystem = 'Github'
issueManagementUrl = ext.scmUrl + '/issues'
licenseName = 'The Apache License, Version 2.0'
licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
subprojects {
apply from: rootProject.file('gradle/ide/idea.gradle')
apply from: rootProject.file('gradle/repositories/maven.gradle')
apply from: rootProject.file('gradle/compile/java.gradle')
apply from: rootProject.file('gradle/test/junit5.gradle')
apply from: rootProject.file('gradle/publish/maven.gradle')
}
apply from: rootProject.file('gradle/publish/sonatype.gradle')
apply from: rootProject.file('gradle/publish/forgejo.gradle')

3
gradle.properties Normal file
View file

@ -0,0 +1,3 @@
group = org.xbib
name = net-ldap
version = 1.0.0

View file

@ -0,0 +1,45 @@
apply plugin: 'java-library'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
modularity.inferModulePath.set(true)
withSourcesJar()
withJavadocJar()
}
jar {
manifest {
attributes('Implementation-Version': project.version)
}
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
tasks.withType(JavaCompile) {
doFirst {
options.fork = true
options.forkOptions.jvmArgs += ['-Duser.language=en','-Duser.country=US']
options.encoding = 'UTF-8'
options.compilerArgs.add('-Xlint:all')
// enforce presence of module-info.java
options.compilerArgs.add("--module-path")
options.compilerArgs.add(classpath.asPath)
classpath = files()
}
}
tasks.withType(Javadoc) {
doFirst {
options.addStringOption('Xdoclint:none', '-quiet')
options.encoding = 'UTF-8'
}
}
tasks.withType(JavaExec) {
doFirst {
jvmArguments.add("--module-path")
jvmArguments.add(classpath.asPath)
classpath = files()
}
}

View file

@ -0,0 +1,13 @@
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
asciidoctor {
attributes 'source-highlighter': 'coderay',
toc: 'left',
doctype: 'book',
icons: 'font',
encoding: 'utf-8',
sectlink: true,
sectanchors: true,
linkattrs: true,
imagesdir: 'img'
}

8
gradle/ide/idea.gradle Normal file
View file

@ -0,0 +1,8 @@
apply plugin: 'idea'
idea {
module {
outputDir file('build/classes/java/main')
testOutputDir file('build/classes/java/test')
}
}

View file

@ -0,0 +1,16 @@
if (project.hasProperty('forgeJoToken')) {
publishing {
repositories {
maven {
url 'https://xbib.org/api/packages/joerg/maven'
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = "token ${project.property('forgeJoToken')}"
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
}
}

27
gradle/publish/ivy.gradle Normal file
View file

@ -0,0 +1,27 @@
apply plugin: 'ivy-publish'
publishing {
repositories {
ivy {
url = "https://xbib.org/repo"
}
}
publications {
ivy(IvyPublication) {
from components.java
descriptor {
license {
name = 'The Apache License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
author {
name = 'Jörg Prante'
url = 'http://example.com/users/jane'
}
descriptor.description {
text = rootProject.ext.description
}
}
}
}
}

View file

@ -0,0 +1,51 @@
publishing {
publications {
"${project.name}"(MavenPublication) {
from components.java
pom {
artifactId = project.name
name = project.name
description = rootProject.ext.description
url = rootProject.ext.url
inceptionYear = rootProject.ext.inceptionYear
packaging = 'jar'
organization {
name = 'xbib'
url = 'https://xbib.org'
}
developers {
developer {
id = 'jprante'
name = 'Jörg Prante'
email = 'joergprante@gmail.com'
url = 'https://xbib.org/joerg'
}
}
scm {
url = rootProject.ext.scmUrl
connection = rootProject.ext.scmConnection
developerConnection = rootProject.ext.scmDeveloperConnection
}
issueManagement {
system = rootProject.ext.issueManagementSystem
url = rootProject.ext.issueManagementUrl
}
licenses {
license {
name = rootProject.ext.licenseName
url = rootProject.ext.licenseUrl
distribution = 'repo'
}
}
}
}
}
}
if (project.hasProperty("signing.keyId")) {
apply plugin: 'signing'
signing {
sign publishing.publications."${project.name}"
}
}

View file

@ -0,0 +1,12 @@
if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) {
nexusPublishing {
repositories {
sonatype {
username = project.property('ossrhUsername')
password = project.property('ossrhPassword')
packageGroup = "org.xbib"
}
}
}
}

View file

@ -0,0 +1,19 @@
apply plugin: 'checkstyle'
tasks.withType(Checkstyle) {
ignoreFailures = true
reports {
xml.getRequired().set(true)
html.getRequired().set(true)
}
}
checkstyle {
configFile = rootProject.file('gradle/quality/checkstyle.xml')
ignoreFailures = true
showViolations = true
checkstyleMain {
source = sourceSets.main.allSource
}
}

View file

@ -0,0 +1,333 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<!-- This is a checkstyle configuration file. For descriptions of
what the following rules do, please see the checkstyle configuration
page at http://checkstyle.sourceforge.net/config.html -->
<module name="Checker">
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value=".*(Example|Test|module-info)(\$.*)?"/>
</module>
<module name="FileTabCharacter">
<!-- Checks that there are no tab characters in the file.
-->
</module>
<module name="NewlineAtEndOfFile">
<property name="lineSeparator" value="lf"/>
</module>
<module name="RegexpSingleline">
<!-- Checks that FIXME is not used in comments. TODO is preferred.
-->
<property name="format" value="((//.*)|(\*.*))FIXME" />
<property name="message" value='TODO is preferred to FIXME. e.g. "TODO(johndoe): Refactor when v2 is released."' />
</module>
<module name="RegexpSingleline">
<!-- Checks that TODOs are named. (Actually, just that they are followed
by an open paren.)
-->
<property name="format" value="((//.*)|(\*.*))TODO[^(]" />
<property name="message" value='All TODOs should be named. e.g. "TODO(johndoe): Refactor when v2 is released."' />
</module>
<module name="JavadocPackage">
<!-- Checks that each Java package has a Javadoc file used for commenting.
Only allows a package-info.java, not package.html. -->
</module>
<!-- All Java AST specific tests live under TreeWalker module. -->
<module name="TreeWalker">
<!--
IMPORT CHECKS
-->
<module name="RedundantImport">
<!-- Checks for redundant import statements. -->
<property name="severity" value="error"/>
</module>
<module name="ImportOrder">
<!-- Checks for out of order import statements. -->
<property name="severity" value="warning"/>
<!-- <property name="tokens" value="IMPORT, STATIC_IMPORT"/> -->
<property name="separated" value="false"/>
<property name="groups" value="*"/>
<!-- <property name="option" value="above"/> -->
<property name="sortStaticImportsAlphabetically" value="true"/>
</module>
<module name="CustomImportOrder">
<!-- <property name="customImportOrderRules" value="THIRD_PARTY_PACKAGE###SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE###STATIC"/> -->
<!-- <property name="specialImportsRegExp" value="^javax\."/> -->
<!-- <property name="standardPackageRegExp" value="^java\."/> -->
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="false"/>
</module>
<!--
JAVADOC CHECKS
-->
<!-- Checks for Javadoc comments. -->
<!-- See http://checkstyle.sf.net/config_javadoc.html -->
<module name="JavadocMethod">
<property name="accessModifiers" value="protected"/>
<property name="severity" value="warning"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
</module>
<module name="JavadocType">
<property name="scope" value="protected"/>
<property name="severity" value="error"/>
</module>
<module name="JavadocStyle">
<property name="severity" value="warning"/>
</module>
<!--
NAMING CHECKS
-->
<!-- Item 38 - Adhere to generally accepted naming conventions -->
<module name="PackageName">
<!-- Validates identifiers for package names against the
supplied expression. -->
<!-- Here the default checkstyle rule restricts package name parts to
seven characters, this is not in line with common practice at Google.
-->
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]{1,})*$"/>
<property name="severity" value="warning"/>
</module>
<module name="TypeNameCheck">
<!-- Validates static, final fields against the
expression "^[A-Z][a-zA-Z0-9]*$". -->
<metadata name="altname" value="TypeName"/>
<property name="severity" value="warning"/>
</module>
<module name="ConstantNameCheck">
<!-- Validates non-private, static, final fields against the supplied
public/package final fields "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$". -->
<metadata name="altname" value="ConstantName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="false"/>
<property name="format" value="^([A-Z][A-Z0-9]*(_[A-Z0-9]+)*|FLAG_.*)$"/>
<message key="name.invalidPattern"
value="Variable ''{0}'' should be in ALL_CAPS (if it is a constant) or be private (otherwise)."/>
<property name="severity" value="warning"/>
</module>
<module name="StaticVariableNameCheck">
<!-- Validates static, non-final fields against the supplied
expression "^[a-z][a-zA-Z0-9]*_?$". -->
<metadata name="altname" value="StaticVariableName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="true"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*_?$"/>
<property name="severity" value="warning"/>
</module>
<module name="MemberNameCheck">
<!-- Validates non-static members against the supplied expression. -->
<metadata name="altname" value="MemberName"/>
<property name="applyToPublic" value="true"/>
<property name="applyToProtected" value="true"/>
<property name="applyToPackage" value="true"/>
<property name="applyToPrivate" value="true"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
<property name="severity" value="warning"/>
</module>
<module name="MethodNameCheck">
<!-- Validates identifiers for method names. -->
<metadata name="altname" value="MethodName"/>
<property name="format" value="^[a-z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*$"/>
<property name="severity" value="warning"/>
</module>
<module name="ParameterName">
<!-- Validates identifiers for method parameters against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<module name="LocalFinalVariableName">
<!-- Validates identifiers for local final variables against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<module name="LocalVariableName">
<!-- Validates identifiers for local variables against the
expression "^[a-z][a-zA-Z0-9]*$". -->
<property name="severity" value="warning"/>
</module>
<!--
LENGTH and CODING CHECKS
-->
<module name="LeftCurly">
<!-- Checks for placement of the left curly brace ('{'). -->
<property name="severity" value="warning"/>
</module>
<module name="RightCurly">
<!-- Checks right curlies on CATCH, ELSE, and TRY blocks are on
the same line. e.g., the following example is fine:
<pre>
if {
...
} else
</pre>
-->
<!-- This next example is not fine:
<pre>
if {
...
}
else
</pre>
-->
<property name="option" value="same"/>
<property name="severity" value="warning"/>
</module>
<!-- Checks for braces around if and else blocks -->
<module name="NeedBraces">
<property name="severity" value="warning"/>
<property name="tokens" value="LITERAL_IF, LITERAL_ELSE, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO"/>
</module>
<module name="UpperEll">
<!-- Checks that long constants are defined with an upper ell.-->
<property name="severity" value="error"/>
</module>
<module name="FallThrough">
<!-- Warn about falling through to the next case statement. Similar to
javac -Xlint:fallthrough, but the check is suppressed if a single-line comment
on the last non-blank line preceding the fallen-into case contains 'fall through' (or
some other variants which we don't publicized to promote consistency).
-->
<property name="reliefPattern"
value="fall through|Fall through|fallthru|Fallthru|falls through|Falls through|fallthrough|Fallthrough|No break|NO break|no break|continue on"/>
<property name="severity" value="error"/>
</module>
<!--
MODIFIERS CHECKS
-->
<module name="ModifierOrder">
<!-- Warn if modifier order is inconsistent with JLS3 8.1.1, 8.3.1, and
8.4.3. The prescribed order is:
public, protected, private, abstract, static, final, transient, volatile,
synchronized, native, strictfp
-->
</module>
<!--
WHITESPACE CHECKS
-->
<module name="WhitespaceAround">
<!-- Checks that various tokens are surrounded by whitespace.
This includes most binary operators and keywords followed
by regular or curly braces.
-->
<property name="tokens" value="ASSIGN, BAND, BAND_ASSIGN, BOR,
BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN,
EQUAL, GE, GT, LAND, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE,
LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,
LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS,
MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION,
SL, SL_ASSIGN, SR_ASSIGN, STAR, STAR_ASSIGN"/>
<property name="severity" value="error"/>
</module>
<module name="WhitespaceAfter">
<!-- Checks that commas, semicolons and typecasts are followed by
whitespace.
-->
<property name="tokens" value="COMMA, SEMI, TYPECAST"/>
</module>
<module name="NoWhitespaceAfter">
<!-- Checks that there is no whitespace after various unary operators.
Linebreaks are allowed.
-->
<property name="tokens" value="BNOT, DEC, DOT, INC, LNOT, UNARY_MINUS,
UNARY_PLUS"/>
<property name="allowLineBreaks" value="true"/>
<property name="severity" value="error"/>
</module>
<module name="NoWhitespaceBefore">
<!-- Checks that there is no whitespace before various unary operators.
Linebreaks are allowed.
-->
<property name="tokens" value="SEMI, DOT, POST_DEC, POST_INC"/>
<property name="allowLineBreaks" value="true"/>
<property name="severity" value="error"/>
</module>
<module name="ParenPad">
<!-- Checks that there is no whitespace before close parens or after
open parens.
-->
<property name="severity" value="warning"/>
</module>
</module>
<module name="LineLength">
<!-- Checks if a line is too long. -->
<property name="max" value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.max}" default="128"/>
<property name="severity" value="error"/>
<!--
The default ignore pattern exempts the following elements:
- import statements
- long URLs inside comments
-->
<property name="ignorePattern"
value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.ignorePattern}"
default="^(package .*;\s*)|(import .*;\s*)|( *(\*|//).*https?://.*)$"/>
</module>
</module>

View file

@ -0,0 +1,11 @@
cyclonedxBom {
includeConfigs = [ 'runtimeClasspath' ]
skipConfigs = [ 'compileClasspath', 'testCompileClasspath' ]
projectType = "library"
schemaVersion = "1.4"
destination = file("build/reports")
outputName = "bom"
outputFormat = "json"
includeBomSerialNumber = true
componentVersion = "2.0.0"
}

17
gradle/quality/pmd.gradle Normal file
View file

@ -0,0 +1,17 @@
apply plugin: 'pmd'
tasks.withType(Pmd) {
ignoreFailures = true
reports {
xml.getRequired().set(true)
html.getRequired().set(true)
}
}
pmd {
ignoreFailures = true
consoleOutput = false
toolVersion = "6.51.0"
ruleSetFiles = rootProject.files('gradle/quality/pmd/category/java/bestpractices.xml')
}

View file

@ -0,0 +1,10 @@
sonarqube {
properties {
property "sonar.projectName", "${project.group} ${project.name}"
property "sonar.sourceEncoding", "UTF-8"
property "sonar.tests", "src/test/java"
property "sonar.scm.provider", "git"
property "sonar.junit.reportsPath", "build/test-results/test/"
}
}

View file

@ -0,0 +1,15 @@
apply plugin: 'com.github.spotbugs'
spotbugs {
effort = "max"
reportLevel = "low"
ignoreFailures = true
}
spotbugsMain {
reports {
xml.getRequired().set(false)
html.getRequired().set(true)
}
}

View file

@ -0,0 +1,4 @@
repositories {
mavenLocal()
mavenCentral()
}

22
gradle/test/jmh.gradle Normal file
View file

@ -0,0 +1,22 @@
sourceSets {
jmh {
java.srcDirs = ['src/jmh/java']
resources.srcDirs = ['src/jmh/resources']
compileClasspath += sourceSets.main.runtimeClasspath
}
}
dependencies {
jmhImplementation 'org.openjdk.jmh:jmh-core:1.34'
jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.34'
}
task jmh(type: JavaExec, group: 'jmh', dependsOn: jmhClasses) {
mainClass.set('org.openjdk.jmh.Main')
classpath = sourceSets.jmh.compileClasspath + sourceSets.jmh.runtimeClasspath
project.file('build/reports/jmh').mkdirs()
args '-rf', 'json'
args '-rff', project.file('build/reports/jmh/result.json')
}
classes.finalizedBy(jmhClasses)

34
gradle/test/junit5.gradle Normal file
View file

@ -0,0 +1,34 @@
dependencies {
testImplementation testLibs.junit.jupiter.api
testImplementation testLibs.hamcrest
testRuntimeOnly testLibs.junit.jupiter.engine
testRuntimeOnly testLibs.junit.jupiter.platform.launcher
}
test {
useJUnitPlatform()
failFast = false
jvmArgs '--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED',
'--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED',
'--add-exports=java.base/sun.nio.ch=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED',
'--add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED',
'--add-opens=java.base/java.lang=ALL-UNNAMED',
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED',
'--add-opens=java.base/java.io=ALL-UNNAMED',
'--add-opens=java.base/java.nio=ALL-UNNAMED',
'--add-opens=java.base/java.util=ALL-UNNAMED'
systemProperty 'java.util.logging.config.file', 'src/test/resources/logging.properties'
testLogging {
events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED'
}
afterSuite { desc, result ->
if (!desc.parent) {
println "\nTest result: ${result.resultType}"
println "Test summary: ${result.testCount} tests, " +
"${result.successfulTestCount} succeeded, " +
"${result.failedTestCount} failed, " +
"${result.skippedTestCount} skipped"
}
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
gradlew vendored Executable file
View file

@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
gradlew.bat vendored Normal file
View file

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

7
net-ldap/build.gradle Normal file
View file

@ -0,0 +1,7 @@
dependencies {
api project(':asn1')
api libs.netty.handler
api libs.netty.epoll
api libs.netty.kqueue
}

View file

@ -0,0 +1,11 @@
module org.xbib.net.ldap {
requires java.security.sasl;
requires java.naming;
requires jdk.security.auth;
requires io.netty.buffer;
requires io.netty.handler;
requires io.netty.transport;
requires io.netty.codec;
requires io.netty.common;
requires org.xbib.net.ldap.asnone;
}

View file

@ -0,0 +1,103 @@
package org.xbib.net.ldap;
import org.xbib.asn1.ApplicationDERTag;
import org.xbib.asn1.DEREncoder;
import org.xbib.asn1.IntegerType;
/**
* LDAP abandon request defined as:
*
* <pre>
* AbandonRequest ::= [APPLICATION 16] MessageID
* </pre>
*
*/
public class AbandonRequest extends AbstractRequestMessage {
/**
* Protocol operation identifier.
*/
public static final int PROTOCOL_OP = 16;
/**
* Protocol message ID.
*/
private int messageID;
/**
* Default constructor.
*/
private AbandonRequest() {
}
/**
* Creates a new abandon request.
*
* @param id message ID
*/
public AbandonRequest(final int id) {
messageID = id;
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
public int getMessageID() {
return messageID;
}
@Override
protected DEREncoder[] getRequestEncoders(final int id) {
return new DEREncoder[]{
new IntegerType(id),
new IntegerType(new ApplicationDERTag(PROTOCOL_OP, false), messageID),
};
}
@Override
public String toString() {
return super.toString() + ", " +
"messageID=" + messageID;
}
/**
* Abandon request builder.
*/
public static class Builder extends AbstractRequestMessage.AbstractBuilder<AbandonRequest.Builder, AbandonRequest> {
/**
* Default constructor.
*/
protected Builder() {
super(new AbandonRequest());
}
@Override
protected Builder self() {
return this;
}
/**
* Sets the message ID.
*
* @param id message ID
* @return this builder
*/
public Builder id(final int id) {
object.messageID = id;
return self();
}
}
}

View file

@ -0,0 +1,45 @@
package org.xbib.net.ldap;
/**
* Provides common implementations for configuration objects.
*
*/
public abstract class AbstractConfig {
/**
* Verifies that an array does not contain a null element.
*
* @param array to verify
* @throws IllegalArgumentException if the array contains null
*/
protected void checkArrayContainsNull(final Object[] array) {
if (array != null) {
for (Object o : array) {
if (o == null) {
throw new IllegalArgumentException("Array element cannot be null");
}
}
}
}
/**
* Verifies that a string is not null or empty.
*
* @param s to verify
* @param allowNull whether null strings are valid
* @throws IllegalArgumentException if the string is null or empty
*/
protected void checkStringInput(final String s, final boolean allowNull) {
if (allowNull) {
if ("".equals(s)) {
throw new IllegalArgumentException("Input cannot be empty");
}
} else {
if (s == null || "".equals(s)) {
throw new IllegalArgumentException("Input cannot be null or empty");
}
}
}
}

View file

@ -0,0 +1,171 @@
package org.xbib.net.ldap;
import java.time.Instant;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Base class for connection strategy implementations.
*
*/
public abstract class AbstractConnectionStrategy implements ConnectionStrategy {
/**
* Set of LDAP URLs to attempt connections to.
*/
protected LdapURLSet ldapURLSet;
/**
* Whether this strategy has been successfully initialized.
*/
private boolean initialized;
/**
* Condition used to determine whether to activate a URL.
*/
private Predicate<LdapURL> activateCondition;
/**
* Condition used to determine whether to test an inactive URL.
*/
private Predicate<LdapURL> retryCondition = new Predicate<>() {
@Override
public boolean test(final LdapURL url) {
return Instant.now().isAfter(url.getRetryMetadata().getFailureTime().plus(LdapURLActivatorService.getPeriod()));
}
@Override
public String toString() {
return "DEFAULT_RETRY_CONDITION";
}
};
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public synchronized void initialize(final String urls, final Predicate<LdapURL> condition) {
if (isInitialized()) {
throw new IllegalStateException("Strategy has already been initialized");
}
ldapURLSet = new LdapURLSet(this, urls);
activateCondition = condition;
initialized = true;
}
@Override
public void populate(final String urls, final LdapURLSet urlSet) {
if (urls == null || urls.isEmpty()) {
throw new IllegalArgumentException("urls cannot be empty or null");
}
if (urls.contains(" ")) {
urlSet.populate(Stream.of(urls.split(" "))
.map(s -> {
final LdapURL url = new LdapURL(s);
url.setRetryMetadata(new LdapURLRetryMetadata(this));
return url;
}).collect(Collectors.toList()));
} else {
final LdapURL url = new LdapURL(urls);
url.setRetryMetadata(new LdapURLRetryMetadata(this));
urlSet.populate(Collections.singletonList(url));
}
}
@Override
public Predicate<LdapURL> getActivateCondition() {
return activateCondition;
}
@Override
public Predicate<LdapURL> getRetryCondition() {
return retryCondition;
}
/**
* Sets the retry condition which determines whether an attempt should be made to activate a URL.
*
* @param condition that determines whether to test an inactive URL
*/
public void setRetryCondition(final Predicate<LdapURL> condition) {
retryCondition = condition;
}
@Override
public void success(final LdapURL url) {
url.activate();
url.getRetryMetadata().recordSuccess(Instant.now());
}
@Override
public void failure(final LdapURL url) {
url.deactivate();
url.getRetryMetadata().recordFailure(Instant.now());
LdapURLActivatorService.getInstance().registerUrl(url);
}
@Override
public String toString() {
return "[" +
getClass().getName() + "@" + hashCode() + "::" +
"ldapURLSet=" + ldapURLSet + ", " +
"activateCondition=" + activateCondition + ", " +
"retryCondition=" + retryCondition + ", " +
"initialized=" + initialized + "]";
}
/**
* Default iterator implementation.
*/
protected static class DefaultLdapURLIterator implements Iterator<LdapURL> {
/**
* URLs to iterate over.
*/
private final List<LdapURL> ldapUrls;
/**
* Iterator index.
*/
private int i;
/**
* Creates a new default LDAP URL iterator.
*
* @param urls to iterate over
*/
public DefaultLdapURLIterator(final List<LdapURL> urls) {
ldapUrls = urls;
}
@Override
public boolean hasNext() {
return i < ldapUrls.size();
}
@Override
public LdapURL next() {
return ldapUrls.get(i++);
}
}
}

View file

@ -0,0 +1,290 @@
package org.xbib.net.ldap;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Base class for connection validator implementations.
*
*/
public abstract class AbstractConnectionValidator implements ConnectionValidator {
/**
* Default validation period, value is 30 minutes.
*/
public static final Duration DEFAULT_VALIDATE_PERIOD = Duration.ofMinutes(30);
/**
* Default per connection validate timeout, value is 5 seconds.
*/
public static final Duration DEFAULT_VALIDATE_TIMEOUT = Duration.ofSeconds(5);
/**
* Validation period.
*/
private Duration validatePeriod;
/**
* Maximum length of time a connection validation should block.
*/
private Duration validateTimeout;
/**
* Consumer to execute on a successful validation.
*/
private Consumer<Connection> onSuccess;
/**
* Consumer to execute on a failed validation.
*/
private Consumer<Connection> onFailure;
/**
* Whether the occurrence of a timeout should result in a validation failure.
*/
private boolean timeoutIsFailure = true;
@Override
public Duration getValidatePeriod() {
return validatePeriod;
}
public void setValidatePeriod(final Duration period) {
if (period == null || period.isNegative() || period.isZero()) {
throw new IllegalArgumentException("Period cannot be null, negative or zero");
}
validatePeriod = period;
}
@Override
public Duration getValidateTimeout() {
return validateTimeout;
}
/**
* Sets the validate timeout.
*
* @param timeout to set
*/
public void setValidateTimeout(final Duration timeout) {
if (timeout == null || timeout.isNegative()) {
throw new IllegalArgumentException("Timeout cannot be null or negative");
}
validateTimeout = timeout;
}
/**
* Returns a consumer to handle a connection that has been successfully validated.
*
* @return success consumer
*/
public Consumer<Connection> getOnSuccess() {
return onSuccess;
}
/**
* Sets a consumer to handle a connection that has been successfully validated.
*
* @param consumer to invoke on success
*/
public void setOnSuccess(final Consumer<Connection> consumer) {
onSuccess = consumer;
}
/**
* Returns a consumer to handle a connection that has failed validation.
*
* @return failure consumer
*/
public Consumer<Connection> getOnFailure() {
return onFailure;
}
/**
* Sets a consumer to handle a connection that has failed validation.
*
* @param consumer to invoke on failure
*/
public void setOnFailure(final Consumer<Connection> consumer) {
onFailure = consumer;
}
/**
* Returns whether a timeout should be considered a validation failure.
*
* @return whether a timeout should be considered a validation failure
*/
public boolean getTimeoutIsFailure() {
return timeoutIsFailure;
}
/**
* Sets whether a timeout should be considered a validation failure.
*
* @param failure whether a timeout should be considered a validation failure
*/
public void setTimeoutIsFailure(final boolean failure) {
timeoutIsFailure = failure;
}
@Override
public Boolean apply(final Connection conn) {
if (conn == null) {
if (onFailure != null) {
onFailure.accept(null);
}
return false;
}
final Boolean result = applyAsync(conn).get();
if (result && onSuccess != null) {
onSuccess.accept(conn);
} else if (!result && onFailure != null) {
onFailure.accept(conn);
}
return result;
}
@Override
public Supplier<Boolean> applyAsync(final Connection conn) {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicBoolean result = new AtomicBoolean();
applyAsync(conn, value -> {
result.compareAndSet(false, value);
latch.countDown();
});
return () -> {
try {
if (Duration.ZERO.equals(getValidateTimeout())) {
// waits indefinitely for the validation response
latch.await();
} else {
if (!latch.await(getValidateTimeout().toMillis(), TimeUnit.MILLISECONDS) && !timeoutIsFailure) {
result.compareAndSet(false, true);
}
}
} catch (Exception e) {
//
}
return result.get();
};
}
@Override
public String toString() {
return
getClass().getName() + "@" + hashCode() + "::" +
"validatePeriod=" + validatePeriod + ", " +
"validateTimeout=" + validateTimeout + ", " +
"onSuccess=" + onSuccess + ", " +
"onFailure=" + onFailure + ", " +
"timeoutIsFailure=" + timeoutIsFailure;
}
/**
* Base class for validator builders.
*
* @param <B> type of builder
* @param <T> type of validator
*/
protected abstract static class AbstractBuilder<B, T extends AbstractConnectionValidator> {
/**
* Validator to build.
*/
protected final T object;
/**
* Creates a new abstract builder.
*
* @param t validator to build
*/
protected AbstractBuilder(final T t) {
object = t;
}
/**
* Returns this builder.
*
* @return builder
*/
protected abstract B self();
/**
* Sets the validation period.
*
* @param period to set
* @return this builder
*/
public B period(final Duration period) {
object.setValidatePeriod(period);
return self();
}
/**
* Sets the validation timeout.
*
* @param timeout to set
* @return this builder
*/
public B timeout(final Duration timeout) {
object.setValidateTimeout(timeout);
return self();
}
public B onSuccess(final Consumer<Connection> consumer) {
object.setOnSuccess(consumer);
return self();
}
public B onFailure(final Consumer<Connection> consumer) {
object.setOnFailure(consumer);
return self();
}
/**
* Sets whether timeout is a validation failure.
*
* @param failure whether timeout is a validation failure
* @return this builder
*/
public B timeoutIsFailure(final boolean failure) {
object.setTimeoutIsFailure(failure);
return self();
}
/**
* Returns the connection validator.
*
* @return connection validator
*/
public T build() {
return object;
}
}
}

View file

@ -0,0 +1,313 @@
package org.xbib.net.ldap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.xbib.asn1.AbstractParseHandler;
import org.xbib.asn1.BooleanType;
import org.xbib.asn1.DERBuffer;
import org.xbib.asn1.DERParser;
import org.xbib.asn1.DERPath;
import org.xbib.asn1.IntegerType;
import org.xbib.asn1.OctetStringType;
import org.xbib.net.ldap.control.ControlFactory;
import org.xbib.net.ldap.control.ResponseControl;
/**
* LDAP message envelope defined as:
*
* <pre>
* LDAPMessage ::= SEQUENCE {
* messageID MessageID,
* protocolOp CHOICE {
* ...,
* controls [0] Controls OPTIONAL }
*
* Control ::= SEQUENCE {
* controlType LDAPOID,
* criticality BOOLEAN DEFAULT FALSE,
* controlValue OCTET STRING OPTIONAL }
* </pre>
*
*/
public abstract class AbstractMessage implements Message {
/**
* Protocol message ID.
*/
private int messageID;
/**
* LDAP controls.
*/
private List<ResponseControl> controls = new ArrayList<>();
@Override
public int getMessageID() {
return messageID;
}
public void setMessageID(final int id) {
messageID = id;
}
@Override
public ResponseControl[] getControls() {
return controls != null ? controls.toArray(new ResponseControl[0]) : null;
}
/**
* Adds the supplied controls to this message.
*
* @param cntrls to add
*/
public void addControls(final ResponseControl... cntrls) {
Collections.addAll(controls, cntrls);
}
/**
* Copies the property values from the supplied message to this message.
*
* @param <T> type of message
* @param message to copy from
*/
protected <T extends Message> void copyValues(final T message) {
setMessageID(message.getMessageID());
addControls(message.getControls());
}
// CheckStyle:EqualsHashCode OFF
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (o instanceof AbstractMessage) {
final AbstractMessage v = (AbstractMessage) o;
return LdapUtils.areEqual(getMessageID(), v.getMessageID()) &&
LdapUtils.areEqual(getControls(), v.getControls());
}
return false;
}
// CheckStyle:EqualsHashCode ON
/**
* Returns the hash code for this object.
*
* @return hash code
*/
@Override
public abstract int hashCode();
@Override
public String toString() {
return getClass().getName() + "@" + hashCode() + "::" + "messageID=" + messageID + ", " + "controls=" + controls;
}
/**
* Parse handler implementation for the message ID.
*/
protected static class MessageIDHandler extends AbstractParseHandler<AbstractMessage> {
/**
* DER path to message id.
*/
public static final DERPath PATH = new DERPath("/SEQ/INT[0]");
/**
* Creates a new message ID handler.
*
* @param response to configure
*/
public MessageIDHandler(final AbstractMessage response) {
super(response);
}
@Override
public void handle(final DERParser parser, final DERBuffer encoded) {
getObject().setMessageID(IntegerType.decodeUnsignedPrimitive(encoded));
}
}
/**
* Parse handler implementation for the message controls.
*/
protected static class ControlsHandler extends AbstractParseHandler<AbstractMessage> {
/**
* DER path to controls.
*/
public static final DERPath PATH = new DERPath("/SEQ/CTX(0)/SEQ");
/**
* Creates a new controls handler.
*
* @param response to configure
*/
public ControlsHandler(final AbstractMessage response) {
super(response);
}
@Override
public void handle(final DERParser parser, final DERBuffer encoded) {
final ControlParser p = new ControlParser();
p.parse(encoded);
getObject().addControls(
ControlFactory.createResponseControl(
p.getOid().isPresent() ? p.getOid().get() : null,
p.getCritical().isPresent() ? p.getCritical().get() : false,
p.getValue().isPresent() ? p.getValue().get() : null));
}
}
/**
* Parses a buffer containing an LDAP control.
*/
protected static class ControlParser {
/**
* DER path to criticality.
*/
private static final DERPath CRITICAL_PATH = new DERPath("/BOOL[1]");
/**
* DER path to OID.
*/
private static final DERPath OID_PATH = new DERPath("/OCTSTR[0]");
/**
* DER path to value.
*/
private static final DERPath VALUE_PATH = new DERPath("/OCTSTR[1]");
/**
* DER path to alternate value.
*/
private static final DERPath ALT_VALUE_PATH = new DERPath("/OCTSTR[2]");
/**
* Parser for decoding LDAP controls.
*/
private final DERParser parser = new DERParser();
/**
* Control criticality.
*/
private Boolean critical;
/**
* Control oid.
*/
private String oid;
/**
* Control value.
*/
private DERBuffer value;
/**
* Creates a new control parser.
*/
public ControlParser() {
parser.registerHandler(CRITICAL_PATH, (p, e) -> critical = BooleanType.decode(e));
parser.registerHandler(OID_PATH, (p, e) -> oid = OctetStringType.decode(e));
parser.registerHandler(VALUE_PATH, (p, e) -> value = e.slice());
parser.registerHandler(ALT_VALUE_PATH, (p, e) -> {
if (value == null) {
value = e.slice();
}
});
}
/**
* Examines the supplied buffer and parses an LDAP control if one is found.
*
* @param buffer to parse
*/
public void parse(final DERBuffer buffer) {
parser.parse(buffer);
}
/**
* Returns the control criticality.
*
* @return criticality or empty
*/
public Optional<Boolean> getCritical() {
return Optional.ofNullable(critical);
}
/**
* Returns the control oid.
*
* @return control oid or empty
*/
public Optional<String> getOid() {
return Optional.ofNullable(oid);
}
/**
* Returns the control value.
*
* @return control value or empty
*/
public Optional<DERBuffer> getValue() {
return Optional.ofNullable(value);
}
}
// CheckStyle:OFF
protected abstract static class AbstractBuilder<B, T extends AbstractMessage> {
protected final T object;
protected AbstractBuilder(final T t) {
object = t;
}
protected abstract B self();
public B messageID(final int id) {
object.setMessageID(id);
return self();
}
public B controls(final ResponseControl... controls) {
object.addControls(controls);
return self();
}
public T build() {
return object;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,378 @@
package org.xbib.net.ldap;
import java.util.Arrays;
import org.xbib.net.ldap.handler.ExceptionHandler;
import org.xbib.net.ldap.handler.IntermediateResponseHandler;
import org.xbib.net.ldap.handler.ReferralHandler;
import org.xbib.net.ldap.handler.RequestHandler;
import org.xbib.net.ldap.handler.ResponseControlHandler;
import org.xbib.net.ldap.handler.ResultHandler;
import org.xbib.net.ldap.handler.ResultPredicate;
import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler;
/**
* Base class for operations.
*
* @param <Q> type of request
* @param <S> type of result
*/
public abstract class AbstractOperation<Q extends Request, S extends Result> implements Operation<Q, S> {
/**
* Connection factory.
*/
private ConnectionFactory connectionFactory;
/**
* Functions to handle requests.
*/
private RequestHandler<Q>[] requestHandlers;
/**
* Functions to handle response results.
*/
private ResultHandler[] resultHandlers;
/**
* Functions to handle response controls.
*/
private ResponseControlHandler[] controlHandlers;
/**
* Functions to handle referrals.
*/
private ReferralHandler[] referralHandlers;
/**
* Functions to handle intermediate responses.
*/
private IntermediateResponseHandler[] intermediateResponseHandlers;
/**
* Function to handle exceptions.
*/
private ExceptionHandler exceptionHandler;
/**
* Function to test results.
*/
private ResultPredicate throwCondition;
/**
* Functions to handle unsolicited notifications.
*/
private UnsolicitedNotificationHandler[] unsolicitedNotificationHandlers;
/**
* Default constructor.
*/
public AbstractOperation() {
}
/**
* Creates a new abstract operation.
*
* @param factory connection factory
*/
public AbstractOperation(final ConnectionFactory factory) {
setConnectionFactory(factory);
}
public ConnectionFactory getConnectionFactory() {
return connectionFactory;
}
public void setConnectionFactory(final ConnectionFactory factory) {
connectionFactory = factory;
}
public RequestHandler<Q>[] getRequestHandlers() {
return requestHandlers;
}
@SuppressWarnings("unchecked")
public void setRequestHandlers(final RequestHandler<Q>... handlers) {
requestHandlers = handlers;
}
public ResultHandler[] getResultHandlers() {
return resultHandlers;
}
public void setResultHandlers(final ResultHandler... handlers) {
resultHandlers = handlers;
}
public ResponseControlHandler[] getControlHandlers() {
return controlHandlers;
}
public void setControlHandlers(final ResponseControlHandler... handlers) {
controlHandlers = handlers;
}
public ReferralHandler[] getReferralHandlers() {
return referralHandlers;
}
public void setReferralHandlers(final ReferralHandler... handlers) {
referralHandlers = handlers;
}
public IntermediateResponseHandler[] getIntermediateResponseHandlers() {
return intermediateResponseHandlers;
}
public void setIntermediateResponseHandlers(final IntermediateResponseHandler... handlers) {
intermediateResponseHandlers = handlers;
}
public ExceptionHandler getExceptionHandler() {
return exceptionHandler;
}
public void setExceptionHandler(final ExceptionHandler handler) {
exceptionHandler = handler;
}
public ResultPredicate getThrowCondition() {
return throwCondition;
}
public void setThrowCondition(final ResultPredicate function) {
throwCondition = function;
}
public UnsolicitedNotificationHandler[] getUnsolicitedNotificationHandlers() {
return unsolicitedNotificationHandlers;
}
public void setUnsolicitedNotificationHandlers(final UnsolicitedNotificationHandler... handlers) {
unsolicitedNotificationHandlers = handlers;
}
/**
* Applies any configured request handlers to the supplied request. Returns the supplied request unaltered if no
* request handlers are configured.
*
* @param request to configure
* @return configured request
*/
protected Q configureRequest(final Q request) {
if (requestHandlers == null || requestHandlers.length == 0) {
return request;
}
Q req = request;
for (RequestHandler<Q> func : requestHandlers) {
req = func.apply(req);
}
return req;
}
/**
* Adds configured functions to the supplied handle.
*
* @param handle to configure
* @return configured handle
*/
protected OperationHandle<Q, S> configureHandle(final OperationHandle<Q, S> handle) {
return handle
.onControl(getControlHandlers())
.onReferral(getReferralHandlers())
.onIntermediate(getIntermediateResponseHandlers())
.onException(getExceptionHandler())
.throwIf(getThrowCondition())
.onUnsolicitedNotification(getUnsolicitedNotificationHandlers())
.onResult(getResultHandlers());
}
@Override
public String toString() {
return getClass().getName() + "@" + hashCode() + "::" +
"connectionFactory=" + connectionFactory + ", " +
"requestHandlers=" + Arrays.toString(requestHandlers) + ", " +
"resultHandlers=" + Arrays.toString(resultHandlers) + ", " +
"controlHandlers=" + Arrays.toString(controlHandlers) + ", " +
"referralHandlers=" + Arrays.toString(referralHandlers) + ", " +
"intermediateResponseHandlers=" + Arrays.toString(intermediateResponseHandlers) + ", " +
"exceptionHandler=" + exceptionHandler + ", " +
"throwCondition=" + throwCondition + ", " +
"unsolicitedNotificationHandlers=" + Arrays.toString(unsolicitedNotificationHandlers);
}
/**
* Base class for operation builders.
*
* @param <B> type of builder
* @param <T> type of operation
*/
protected abstract static class AbstractBuilder<B, T extends AbstractOperation> {
/**
* Operation to build.
*/
protected final T object;
/**
* Creates a new abstract builder.
*
* @param t operation to build
*/
protected AbstractBuilder(final T t) {
object = t;
}
/**
* Returns this builder.
*
* @return builder
*/
protected abstract B self();
/**
* Sets the connection factory.
*
* @param factory to set
* @return this builder
*/
public B factory(final ConnectionFactory factory) {
object.setConnectionFactory(factory);
return self();
}
/**
* Sets the functions to execute before a request is sent.
*
* @param handlers to execute on a request
* @return this builder
*/
@SuppressWarnings("unchecked")
public B onRequest(final RequestHandler... handlers) {
object.setRequestHandlers(handlers);
return self();
}
/**
* Sets the functions to execute when a result is received.
*
* @param handlers to execute on a result
* @return this builder
*/
public B onResult(final ResultHandler... handlers) {
object.setResultHandlers(handlers);
return self();
}
/**
* Sets the functions to execute when a control is received.
*
* @param handlers to execute on a control
* @return this builder
*/
public B onControl(final ResponseControlHandler... handlers) {
object.setControlHandlers(handlers);
return self();
}
/**
* Sets the functions to execute when a referral is received.
*
* @param handlers to execute on a referral
* @return this builder
*/
public B onReferral(final ReferralHandler... handlers) {
object.setReferralHandlers(handlers);
return self();
}
/**
* Sets the functions to execute when an intermediate response is received.
*
* @param handlers to execute on an intermediate response
* @return this builder
*/
public B onIntermediate(final IntermediateResponseHandler... handlers) {
object.setIntermediateResponseHandlers(handlers);
return self();
}
/**
* Sets the functions to execute when an unsolicited notification is received.
*
* @param handlers to execute on an unsolicited notification
* @return this builder
*/
public B onUnsolicitedNotification(final UnsolicitedNotificationHandler... handlers) {
object.setUnsolicitedNotificationHandlers(handlers);
return self();
}
/**
* Sets the function to execute when an exception occurs.
*
* @param handler to execute on an exception occurs
* @return this builder
*/
public B onException(final ExceptionHandler handler) {
object.setExceptionHandler(handler);
return self();
}
/**
* Sets the function to test a result.
*
* @param function to test a result
* @return this builder
*/
public B throwIf(final ResultPredicate function) {
object.setThrowCondition(function);
return self();
}
/**
* Returns the operation.
*
* @return operation
*/
public T build() {
return object;
}
}
}

View file

@ -0,0 +1,163 @@
package org.xbib.net.ldap;
import java.util.Arrays;
import java.util.function.Consumer;
/**
* Base class for validators that use an operation to perform validation. By default, validation is considered
* successful if the operation result contains any result code. Stricter validation can be configured by setting {@link
* #validResultCodes}.
*
* @param <Q> type of request
* @param <S> type of result
*/
public abstract class AbstractOperationConnectionValidator<Q extends Request, S extends Result>
extends AbstractConnectionValidator {
/**
* Operation request.
*/
private Q request;
/**
* Valid result codes.
*/
private ResultCode[] validResultCodes;
/**
* Returns the operation request.
*
* @return operation request
*/
public Q getRequest() {
return request;
}
/**
* Sets the operation request.
*
* @param req operation request
*/
public void setRequest(final Q req) {
request = req;
}
/**
* Returns the valid result codes.
*
* @return valid result codes
*/
public ResultCode[] getValidResultCodes() {
return validResultCodes;
}
/**
* Sets the valid result codes.
*
* @param codes that represent a valid connection
*/
public void setValidResultCodes(final ResultCode... codes) {
validResultCodes = codes;
}
/**
* Perform the operation for this validator.
*
* @param conn to validate
* @return operation handle
*/
protected abstract OperationHandle<Q, S> performOperation(Connection conn);
@Override
public void applyAsync(final Connection conn, final Consumer<Boolean> function) {
if (conn == null) {
function.accept(false);
} else {
final OperationHandle<Q, S> h = performOperation(conn);
h.onResult(r -> {
if (validResultCodes != null) {
function.accept(Arrays.stream(validResultCodes).anyMatch(rc -> rc.equals(r.getResultCode())));
} else {
function.accept(r.getResultCode() != null);
}
});
h.onException(e -> {
if (e != null && e.getResultCode() == ResultCode.LDAP_TIMEOUT && !getTimeoutIsFailure()) {
function.accept(true);
} else {
function.accept(false);
}
});
h.send();
}
}
@Override
public String toString() {
return super.toString() + ", request=" + request + ", validResultCodes=" + Arrays.toString(validResultCodes);
}
/**
* Base class for operation validator builders.
*
* @param <Q> type of request
* @param <S> type of result
* @param <B> type of builder
* @param <T> type of validator
*/
protected abstract static class AbstractBuilder
<Q extends Request, S extends Result, B, T extends AbstractOperationConnectionValidator<Q, S>> extends
AbstractConnectionValidator.AbstractBuilder<B, T> {
/**
* Creates a new abstract builder.
*
* @param t validator to build
*/
protected AbstractBuilder(final T t) {
super(t);
}
/**
* Returns this builder.
*
* @return builder
*/
protected abstract B self();
/**
* Sets the request to use for validation.
*
* @param request operation request
* @return this builder
*/
public B request(final Q request) {
object.setRequest(request);
return self();
}
/**
* Sets the result codes to use for validation.
*
* @param codes valid result codes
* @return this builder
*/
public B validResultCodes(final ResultCode... codes) {
object.setValidResultCodes(codes);
return self();
}
}
}

View file

@ -0,0 +1,202 @@
package org.xbib.net.ldap;
import java.time.Duration;
import java.util.Arrays;
import org.xbib.asn1.BooleanType;
import org.xbib.asn1.ConstructedDEREncoder;
import org.xbib.asn1.ContextDERTag;
import org.xbib.asn1.DEREncoder;
import org.xbib.asn1.OctetStringType;
import org.xbib.asn1.UniversalDERTag;
import org.xbib.net.ldap.control.RequestControl;
/**
* LDAP message envelope defined as:
*
* <pre>
* LDAPMessage ::= SEQUENCE {
* messageID MessageID,
* protocolOp CHOICE {
* ...,
* controls [0] Controls OPTIONAL }
*
* Control ::= SEQUENCE {
* controlType LDAPOID,
* criticality BOOLEAN DEFAULT FALSE,
* controlValue OCTET STRING OPTIONAL }
* </pre>
*
*/
public abstract class AbstractRequestMessage implements Request {
/**
* LDAP controls.
*/
private RequestControl[] controls;
/**
* Duration of time to wait for a response. This property is not part of the request specification.
*/
private Duration responseTimeout;
public RequestControl[] getControls() {
return controls;
}
public void setControls(final RequestControl... cntrls) {
controls = cntrls;
}
/**
* Returns the response timeout.
*
* @return timeout
*/
public Duration getResponseTimeout() {
return responseTimeout;
}
/**
* Sets the maximum amount of time to wait for a response from this request.
*
* @param time timeout for a response
*/
public void setResponseTimeout(final Duration time) {
if (time == null || time.isNegative()) {
throw new IllegalArgumentException("Response timeout cannot be null or negative");
}
responseTimeout = time;
}
@Override
public byte[] encode(final int id) {
final DEREncoder[] requestEncoders = getRequestEncoders(id);
final DEREncoder controlEncoder = getControlEncoder();
final DEREncoder[] encoders;
if (controlEncoder != null) {
encoders = LdapUtils.concatArrays(requestEncoders, new DEREncoder[]{controlEncoder});
} else {
encoders = requestEncoders;
}
final ConstructedDEREncoder se = new ConstructedDEREncoder(UniversalDERTag.SEQ, encoders);
return se.encode();
}
/**
* Returns the request encoders for this message.
*
* @param id message ID
* @return request encoders
*/
protected abstract DEREncoder[] getRequestEncoders(int id);
/**
* Returns the encoder to any controls that may be set on this message.
*
* @return control encoder
*/
private DEREncoder getControlEncoder() {
if (controls == null || controls.length == 0) {
return null;
}
final DEREncoder[] controlEncoders = new DEREncoder[controls.length];
for (int i = 0; i < controls.length; i++) {
if (controls[i].hasValue()) {
controlEncoders[i] = new ConstructedDEREncoder(
UniversalDERTag.SEQ,
new OctetStringType(controls[i].getOID()),
new BooleanType(controls[i].getCriticality()),
new OctetStringType(controls[i].encode()));
} else {
controlEncoders[i] = new ConstructedDEREncoder(
UniversalDERTag.SEQ,
new OctetStringType(controls[i].getOID()),
new BooleanType(controls[i].getCriticality()));
}
}
return new ConstructedDEREncoder(new ContextDERTag(0, true), controlEncoders);
}
@Override
public String toString() {
return getClass().getName() + "@" + hashCode() + "::" +
"controls=" + Arrays.toString(controls) + ", " +
"responseTimeout=" + responseTimeout;
}
/**
* Base class for request builders.
*
* @param <B> type of builder
* @param <T> type of message
*/
protected abstract static class AbstractBuilder<B, T extends AbstractRequestMessage> {
/**
* Message to build.
*/
protected final T object;
/**
* Creates a new abstract builder.
*
* @param t message to build
*/
protected AbstractBuilder(final T t) {
object = t;
}
/**
* Returns this builder.
*
* @return builder
*/
protected abstract B self();
/**
* Sets controls on the message.
*
* @param cntrls controls
* @return this builder
*/
public B controls(final RequestControl... cntrls) {
object.setControls(cntrls);
return self();
}
/**
* Sets the response timeout on the message.
*
* @param time response timeout
* @return this builder
*/
public B responseTimeout(final Duration time) {
object.setResponseTimeout(time);
return self();
}
/**
* Returns the message.
*
* @return message
*/
public T build() {
return object;
}
}
}

View file

@ -0,0 +1,267 @@
package org.xbib.net.ldap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.xbib.asn1.AbstractParseHandler;
import org.xbib.asn1.DERBuffer;
import org.xbib.asn1.DERParser;
import org.xbib.asn1.IntegerType;
import org.xbib.asn1.OctetStringType;
/**
* LDAP result message defined as:
*
* <pre>
* LDAPResult ::= SEQUENCE {
* resultCode ENUMERATED {
* ... },
* matchedDN LDAPDN,
* diagnosticMessage LDAPString,
* referral [3] Referral OPTIONAL }
*
* Referral ::= SEQUENCE SIZE (1..MAX) OF uri URI
*
* URI ::= LDAPString -- limited to characters permitted in
* -- URIs
* </pre>
*
*/
public abstract class AbstractResult extends AbstractMessage implements Result {
/**
* Result code.
*/
private ResultCode resultCode;
/**
* Matched DN.
*/
private String matchedDN;
/**
* Diagnostic message.
*/
private String diagnosticMessage;
/**
* Referral URLS.
*/
private final List<String> referralURLs = new ArrayList<>();
public ResultCode getResultCode() {
return resultCode;
}
public void setResultCode(final ResultCode code) {
resultCode = code;
}
public String getMatchedDN() {
return matchedDN;
}
public void setMatchedDN(final String dn) {
matchedDN = dn;
}
public String getDiagnosticMessage() {
return diagnosticMessage;
}
public void setDiagnosticMessage(final String message) {
diagnosticMessage = message;
}
public String[] getReferralURLs() {
return referralURLs != null ? referralURLs.toArray(new String[0]) : null;
}
/**
* Adds referral URLs to the result.
*
* @param urls to add
*/
public void addReferralURLs(final String... urls) {
Collections.addAll(referralURLs, urls);
}
/**
* Copies the property values from the supplied result to this result.
*
* @param <T> type of result
* @param result to copy from
*/
protected <T extends Result> void copyValues(final T result) {
super.copyValues(result);
setResultCode(result.getResultCode());
setMatchedDN(result.getMatchedDN());
setDiagnosticMessage(result.getDiagnosticMessage());
addReferralURLs(result.getReferralURLs());
}
// CheckStyle:EqualsHashCode OFF
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (o instanceof AbstractResult v && super.equals(o)) {
return LdapUtils.areEqual(getResultCode(), v.getResultCode()) &&
LdapUtils.areEqual(getMatchedDN(), v.getMatchedDN()) &&
LdapUtils.areEqual(getDiagnosticMessage(), v.getDiagnosticMessage()) &&
LdapUtils.areEqual(getReferralURLs(), v.getReferralURLs());
}
return false;
}
// CheckStyle:EqualsHashCode ON
@Override
public String toString() {
return super.toString() + ", " +
"resultCode=" + resultCode + ", " +
"matchedDN=" + matchedDN + ", " +
"diagnosticMessage=" + getEncodedDiagnosticMessage() + ", " +
"referralURLs=" + referralURLs;
}
/**
* Parse handler implementation for the LDAP result code.
*/
protected static class ResultCodeHandler extends AbstractParseHandler<AbstractResult> {
/**
* Creates a new LDAP result code handler.
*
* @param response to configure
*/
public ResultCodeHandler(final AbstractResult response) {
super(response);
}
@Override
public void handle(final DERParser parser, final DERBuffer encoded) {
getObject().setResultCode(ResultCode.valueOf(IntegerType.decodeUnsignedPrimitive(encoded)));
}
}
/**
* Parse handler implementation for the LDAP matched DN.
*/
protected static class MatchedDNHandler extends AbstractParseHandler<AbstractResult> {
/**
* Creates a new LDAP matched DN handler.
*
* @param response to configure
*/
public MatchedDNHandler(final AbstractResult response) {
super(response);
}
@Override
public void handle(final DERParser parser, final DERBuffer encoded) {
getObject().setMatchedDN(OctetStringType.decode(encoded));
}
}
/**
* Parse handler implementation for the LDAP diagnostic message.
*/
protected static class DiagnosticMessageHandler extends AbstractParseHandler<AbstractResult> {
/**
* Creates a new LDAP diagnostic message handler.
*
* @param response to configure
*/
public DiagnosticMessageHandler(final AbstractResult response) {
super(response);
}
@Override
public void handle(final DERParser parser, final DERBuffer encoded) {
getObject().setDiagnosticMessage(OctetStringType.decode(encoded));
}
}
/**
* Parse handler implementation for the LDAP referral.
*/
protected static class ReferralHandler extends AbstractParseHandler<AbstractResult> {
/**
* Creates a new LDAP referral handler.
*
* @param response to configure
*/
public ReferralHandler(final AbstractResult response) {
super(response);
}
@Override
public void handle(final DERParser parser, final DERBuffer encoded) {
getObject().addReferralURLs(OctetStringType.decode(encoded));
}
}
// CheckStyle:OFF
protected abstract static class AbstractBuilder<B, T extends AbstractResult>
extends AbstractMessage.AbstractBuilder<B, T> {
protected AbstractBuilder(final T t) {
super(t);
}
public B resultCode(final ResultCode code) {
object.setResultCode(code);
return self();
}
public B matchedDN(final String dn) {
object.setMatchedDN(dn);
return self();
}
public B diagnosticMessage(final String message) {
object.setDiagnosticMessage(message);
return self();
}
public B referralURLs(final String... url) {
object.addReferralURLs(url);
return self();
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,63 @@
package org.xbib.net.ldap;
import java.time.Instant;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Common implementation of retry metadata.
*
*/
public abstract class AbstractRetryMetadata implements RetryMetadata {
/**
* Attempt count.
*/
private final AtomicInteger attempts = new AtomicInteger();
/**
* Time at which the last success occurred.
*/
protected Instant successTime;
/**
* Time at which the failure occurred.
*/
protected Instant failureTime;
@Override
public Instant getSuccessTime() {
return successTime;
}
@Override
public Instant getFailureTime() {
return failureTime;
}
@Override
public int getAttempts() {
return attempts.get();
}
@Override
public void recordSuccess(final Instant time) {
successTime = time;
}
@Override
public void recordFailure(final Instant time) {
failureTime = time;
attempts.incrementAndGet();
}
@Override
public String toString() {
return getClass().getName() + "@" + hashCode() + "::" +
"attempts=" + attempts + ", " +
"failureTime=" + failureTime;
}
}

View file

@ -0,0 +1,377 @@
package org.xbib.net.ldap;
import org.xbib.net.ldap.handler.ExceptionHandler;
import org.xbib.net.ldap.handler.IntermediateResponseHandler;
import org.xbib.net.ldap.handler.LdapEntryHandler;
import org.xbib.net.ldap.handler.ReferralHandler;
import org.xbib.net.ldap.handler.RequestHandler;
import org.xbib.net.ldap.handler.ResponseControlHandler;
import org.xbib.net.ldap.handler.ResultHandler;
import org.xbib.net.ldap.handler.ResultPredicate;
import org.xbib.net.ldap.handler.SearchReferenceHandler;
import org.xbib.net.ldap.handler.SearchResultHandler;
import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler;
/**
* Base class for classes that perform searches.
*
*/
public abstract class AbstractSearchOperationFactory implements ConnectionFactoryManager {
/**
* Connection factory.
*/
private ConnectionFactory factory;
/**
* Functions to handle requests.
*/
private RequestHandler<SearchRequest>[] requestHandlers;
/**
* Functions to handle response results.
*/
private ResultHandler[] resultHandlers;
/**
* Functions to handle response controls.
*/
private ResponseControlHandler[] controlHandlers;
/**
* Functions to handle referrals.
*/
private ReferralHandler[] referralHandlers;
/**
* Functions to handle intermediate responses.
*/
private IntermediateResponseHandler[] intermediateResponseHandlers;
/**
* Function to handle exceptions.
*/
private ExceptionHandler exceptionHandler;
/**
* Function to test results.
*/
private ResultPredicate throwCondition;
/**
* Functions to handle unsolicited notifications.
*/
private UnsolicitedNotificationHandler[] unsolicitedNotificationHandlers;
/**
* Functions to handle entries.
*/
private LdapEntryHandler[] entryHandlers;
/**
* Functions to handle response references.
*/
private SearchReferenceHandler[] referenceHandlers;
/**
* Functions to handle search response results.
*/
private SearchResultHandler[] searchResultHandlers;
/**
* Returns the connection factory.
*
* @return connection factory
*/
public ConnectionFactory getConnectionFactory() {
return factory;
}
/**
* Sets the connection factory.
*
* @param cf connection factory
*/
public void setConnectionFactory(final ConnectionFactory cf) {
factory = cf;
}
/**
* Returns the search request handlers.
*
* @return search request handlers
*/
public RequestHandler<SearchRequest>[] getRequestHandlers() {
return requestHandlers;
}
/**
* Sets the search request handlers.
*
* @param handlers search request handler
*/
@SuppressWarnings("unchecked")
public void setRequestHandlers(final RequestHandler<SearchRequest>... handlers) {
requestHandlers = handlers;
}
/**
* Returns the search result handlers.
*
* @return search result handlers
*/
public ResultHandler[] getResultHandlers() {
return resultHandlers;
}
/**
* Sets the search result handlers.
*
* @param handlers search result handlers
*/
public void setResultHandlers(final ResultHandler... handlers) {
resultHandlers = handlers;
}
/**
* Returns the control handlers.
*
* @return control handlers
*/
public ResponseControlHandler[] getControlHandlers() {
return controlHandlers;
}
/**
* Sets the control handlers.
*
* @param handlers control handlers
*/
public void setControlHandlers(final ResponseControlHandler... handlers) {
controlHandlers = handlers;
}
/**
* Returns the referral handlers.
*
* @return referral handlers
*/
public ReferralHandler[] getReferralHandlers() {
return referralHandlers;
}
/**
* Sets the referral handlers.
*
* @param handlers referral handlers
*/
public void setReferralHandlers(final ReferralHandler... handlers) {
referralHandlers = handlers;
}
/**
* Returns the intermediate response handlers.
*
* @return intermediate response handlers
*/
public IntermediateResponseHandler[] getIntermediateResponseHandlers() {
return intermediateResponseHandlers;
}
/**
* Sets the intermediate response handlers.
*
* @param handlers intermediate response handlers
*/
public void setIntermediateResponseHandlers(final IntermediateResponseHandler... handlers) {
intermediateResponseHandlers = handlers;
}
/**
* Returns the search exception handler.
*
* @return search exception handler
*/
public ExceptionHandler getExceptionHandler() {
return exceptionHandler;
}
/**
* Sets the search exception handler.
*
* @param handler search exception handler
*/
public void setExceptionHandler(final ExceptionHandler handler) {
exceptionHandler = handler;
}
/**
* Returns the throw condition.
*
* @return throw condition
*/
public ResultPredicate getThrowCondition() {
return throwCondition;
}
/**
* Sets the throw condition.
*
* @param function throw condition
*/
public void setThrowCondition(final ResultPredicate function) {
throwCondition = function;
}
/**
* Returns the unsolicited notification handlers.
*
* @return unsolicited notification handlers
*/
public UnsolicitedNotificationHandler[] getUnsolicitedNotificationHandlers() {
return unsolicitedNotificationHandlers;
}
/**
* Sets the unsolicited notification handlers.
*
* @param handlers unsolicited notification handlers
*/
public void setUnsolicitedNotificationHandlers(final UnsolicitedNotificationHandler... handlers) {
unsolicitedNotificationHandlers = handlers;
}
/**
* Returns the search entry handlers.
*
* @return search entry handlers
*/
public LdapEntryHandler[] getEntryHandlers() {
return entryHandlers;
}
/**
* Sets the search entry handlers.
*
* @param handlers search entry handlers
*/
public void setEntryHandlers(final LdapEntryHandler... handlers) {
entryHandlers = handlers;
}
/**
* Returns the search reference handlers.
*
* @return search reference handlers
*/
public SearchReferenceHandler[] getReferenceHandlers() {
return referenceHandlers;
}
/**
* Sets the search reference handlers.
*
* @param handlers search reference handlers
*/
public void setReferenceHandlers(final SearchReferenceHandler... handlers) {
referenceHandlers = handlers;
}
/**
* Returns the search result handlers.
*
* @return search result handlers
*/
public SearchResultHandler[] getSearchResultHandlers() {
return searchResultHandlers;
}
/**
* Sets the search result handlers.
*
* @param handlers search result handlers
*/
public void setSearchResultHandlers(final SearchResultHandler... handlers) {
searchResultHandlers = handlers;
}
/**
* Creates a new search operation configured with the properties on this factory.
*
* @return search operation
*/
protected SearchOperation createSearchOperation() {
return createSearchOperation(factory);
}
/**
* Creates a new search operation configured with the properties on this factory.
*
* @param cf connection factory to set on the search operation
* @return search operation
*/
protected SearchOperation createSearchOperation(final ConnectionFactory cf) {
final SearchOperation op = new SearchOperation(cf);
if (requestHandlers != null) {
op.setRequestHandlers(requestHandlers);
}
if (resultHandlers != null) {
op.setResultHandlers(resultHandlers);
}
if (controlHandlers != null) {
op.setControlHandlers(controlHandlers);
}
if (referralHandlers != null) {
op.setReferralHandlers(referralHandlers);
}
if (intermediateResponseHandlers != null) {
op.setIntermediateResponseHandlers(intermediateResponseHandlers);
}
if (exceptionHandler != null) {
op.setExceptionHandler(exceptionHandler);
}
if (throwCondition != null) {
op.setThrowCondition(throwCondition);
}
if (unsolicitedNotificationHandlers != null) {
op.setUnsolicitedNotificationHandlers(unsolicitedNotificationHandlers);
}
if (entryHandlers != null) {
op.setEntryHandlers(entryHandlers);
}
if (referenceHandlers != null) {
op.setReferenceHandlers(referenceHandlers);
}
if (searchResultHandlers != null) {
op.setSearchResultHandlers(searchResultHandlers);
}
return op;
}
}

View file

@ -0,0 +1,57 @@
package org.xbib.net.ldap;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
/**
* Connection strategy that attempts hosts ordered exactly the way they are configured. This means that the first host
* will always be attempted first, followed by each host in the list.
*
*/
public class ActivePassiveConnectionStrategy extends AbstractConnectionStrategy {
/**
* Custom iterator function.
*/
private final Function<List<LdapURL>, Iterator<LdapURL>> iterFunction;
/**
* Default constructor.
*/
public ActivePassiveConnectionStrategy() {
this(null);
}
/**
* Creates a new active passive connection strategy.
*
* @param function that produces a custom iterator
*/
public ActivePassiveConnectionStrategy(final Function<List<LdapURL>, Iterator<LdapURL>> function) {
iterFunction = function;
}
@Override
public Iterator<LdapURL> iterator() {
if (!isInitialized()) {
throw new IllegalStateException("Strategy is not initialized");
}
if (iterFunction != null) {
return iterFunction.apply(ldapURLSet.getUrls());
}
return new DefaultLdapURLIterator(ldapURLSet.getUrls());
}
@Override
public ActivePassiveConnectionStrategy newInstance() {
final ActivePassiveConnectionStrategy strategy = new ActivePassiveConnectionStrategy(iterFunction);
strategy.setRetryCondition(getRetryCondition());
return strategy;
}
}

View file

@ -0,0 +1,149 @@
package org.xbib.net.ldap;
/**
* Executes an ldap add operation.
*
*/
public class AddOperation extends AbstractOperation<AddRequest, AddResponse> {
/**
* Default constructor.
*/
public AddOperation() {
}
/**
* Creates a new add operation.
*
* @param factory connection factory
*/
public AddOperation(final ConnectionFactory factory) {
super(factory);
}
/**
* Sends an add request. See {@link OperationHandle#send()}.
*
* @param factory connection factory
* @param request add request
* @return operation handle
* @throws LdapException if the connection cannot be opened
*/
public static OperationHandle<AddRequest, AddResponse> send(
final ConnectionFactory factory,
final AddRequest request)
throws LdapException {
final Connection conn = factory.getConnection();
try {
conn.open();
} catch (Exception e) {
conn.close();
throw e;
}
return conn.operation(request).onComplete(conn::close).send();
}
/**
* Executes an add request. See {@link OperationHandle#execute()}.
*
* @param factory connection factory
* @param request add request
* @return add result
* @throws LdapException if the connection cannot be opened
*/
public static AddResponse execute(final ConnectionFactory factory, final AddRequest request)
throws LdapException {
try (Connection conn = factory.getConnection()) {
conn.open();
return conn.operation(request).execute();
}
}
/**
* Returns a new add operation with the same properties as the supplied operation.
*
* @param operation to copy
* @return copy of the supplied add operation
*/
public static AddOperation copy(final AddOperation operation) {
final AddOperation op = new AddOperation();
op.setRequestHandlers(operation.getRequestHandlers());
op.setResultHandlers(operation.getResultHandlers());
op.setControlHandlers(operation.getControlHandlers());
op.setReferralHandlers(operation.getReferralHandlers());
op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers());
op.setExceptionHandler(operation.getExceptionHandler());
op.setThrowCondition(operation.getThrowCondition());
op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers());
op.setConnectionFactory(operation.getConnectionFactory());
return op;
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Sends an add request. See {@link OperationHandle#send()}.
*
* @param request add request
* @return operation handle
* @throws LdapException if the connection cannot be opened
*/
@Override
public OperationHandle<AddRequest, AddResponse> send(final AddRequest request)
throws LdapException {
final Connection conn = getConnectionFactory().getConnection();
try {
conn.open();
} catch (Exception e) {
conn.close();
throw e;
}
return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send();
}
/**
* Executes an add request. See {@link OperationHandle#execute()}.
*
* @param request add request
* @return add result
* @throws LdapException if the connection cannot be opened
*/
@Override
public AddResponse execute(final AddRequest request)
throws LdapException {
try (Connection conn = getConnectionFactory().getConnection()) {
conn.open();
return configureHandle(conn.operation(configureRequest(request))).execute();
}
}
/**
* Add operation builder.
*/
public static class Builder extends AbstractOperation.AbstractBuilder<AddOperation.Builder, AddOperation> {
/**
* Creates a new builder.
*/
protected Builder() {
super(new AddOperation());
}
@Override
protected Builder self() {
return this;
}
}
}

View file

@ -0,0 +1,181 @@
package org.xbib.net.ldap;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Stream;
import org.xbib.asn1.ApplicationDERTag;
import org.xbib.asn1.ConstructedDEREncoder;
import org.xbib.asn1.DEREncoder;
import org.xbib.asn1.IntegerType;
import org.xbib.asn1.OctetStringType;
import org.xbib.asn1.UniversalDERTag;
/**
* LDAP add request defined as:
*
* <pre>
* AddRequest ::= [APPLICATION 8] SEQUENCE {
* entry LDAPDN,
* attributes AttributeList }
*
* AttributeList ::= SEQUENCE OF attribute Attribute
* </pre>
*
*/
public class AddRequest extends AbstractRequestMessage {
/**
* BER protocol number.
*/
public static final int PROTOCOL_OP = 8;
/**
* LDAP DN to add.
*/
private String ldapDn;
/**
* Attributes to add to the entry.
*/
private LdapAttribute[] attributes;
/**
* Default constructor.
*/
private AddRequest() {
}
/**
* Creates a new add request.
*
* @param dn DN to add
* @param attrs to add to the entry
*/
public AddRequest(final String dn, final LdapAttribute... attrs) {
ldapDn = dn;
attributes = attrs;
}
/**
* Creates a new add request.
*
* @param dn DN to add
* @param attrs to add to the entry
*/
public AddRequest(final String dn, final Collection<LdapAttribute> attrs) {
ldapDn = dn;
attributes = attrs.toArray(LdapAttribute[]::new);
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns the DN.
*
* @return DN
*/
public String getDn() {
return ldapDn;
}
/**
* Returns the attributes.
*
* @return add attributes
*/
public LdapAttribute[] getAttributes() {
return attributes;
}
@Override
protected DEREncoder[] getRequestEncoders(final int id) {
return new DEREncoder[]{
new IntegerType(id),
new ConstructedDEREncoder(
new ApplicationDERTag(PROTOCOL_OP, true),
new OctetStringType(ldapDn),
new ConstructedDEREncoder(
UniversalDERTag.SEQ,
Stream.of(attributes).map(a ->
new ConstructedDEREncoder(
UniversalDERTag.SEQ,
new OctetStringType(a.getName()),
new ConstructedDEREncoder(
UniversalDERTag.SET,
a.getBinaryValues().stream().map(
OctetStringType::new).toArray(DEREncoder[]::new)))).toArray(DEREncoder[]::new))),
};
}
@Override
public String toString() {
return super.toString() + ", " + "dn=" + ldapDn + ", " + "attributes=" + Arrays.toString(attributes);
}
/**
* Add request builder.
*/
public static class Builder extends AbstractRequestMessage.AbstractBuilder<AddRequest.Builder, AddRequest> {
/**
* Default constructor.
*/
protected Builder() {
super(new AddRequest());
}
@Override
protected Builder self() {
return this;
}
/**
* Sets the ldap DN.
*
* @param dn ldap DN
* @return this builder
*/
public Builder dn(final String dn) {
object.ldapDn = dn;
return self();
}
/**
* Sets the attributes.
*
* @param attrs attributes
* @return this builder
*/
public Builder attributes(final LdapAttribute... attrs) {
object.attributes = attrs;
return self();
}
/**
* Sets the attributes.
*
* @param attrs attributes
* @return this builder
*/
public Builder attributes(final Collection<LdapAttribute> attrs) {
object.attributes = attrs.toArray(LdapAttribute[]::new);
return self();
}
}
}

View file

@ -0,0 +1,117 @@
package org.xbib.net.ldap;
import org.xbib.asn1.DERBuffer;
import org.xbib.asn1.DERParser;
import org.xbib.asn1.DERPath;
/**
* LDAP add response defined as:
*
* <pre>
* AddResponse ::= [APPLICATION 9] LDAPResult
* </pre>
*
*/
public class AddResponse extends AbstractResult {
/**
* BER protocol number.
*/
public static final int PROTOCOL_OP = 9;
/**
* hash code seed.
*/
private static final int HASH_CODE_SEED = 10211;
/**
* DER path to result code.
*/
private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(9)/ENUM[0]");
/**
* DER path to matched DN.
*/
private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(9)/OCTSTR[1]");
/**
* DER path to diagnostic message.
*/
private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(9)/OCTSTR[2]");
/**
* DER path to referral.
*/
private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(9)/CTX(3)/OCTSTR[0]");
/**
* Default constructor.
*/
private AddResponse() {
}
/**
* Creates a new add response.
*
* @param buffer to decode
*/
public AddResponse(final DERBuffer buffer) {
final DERParser parser = new DERParser();
parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this));
parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this));
parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this));
parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this));
parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this));
parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this));
parser.parse(buffer);
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
return o instanceof AddResponse && super.equals(o);
}
@Override
public int hashCode() {
return
LdapUtils.computeHashCode(
HASH_CODE_SEED,
getMessageID(),
getControls(),
getResultCode(),
getMatchedDN(),
getDiagnosticMessage(),
getReferralURLs());
}
// CheckStyle:OFF
public static class Builder extends AbstractResult.AbstractBuilder<Builder, AddResponse> {
protected Builder() {
super(new AddResponse());
}
@Override
protected Builder self() {
return this;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,59 @@
package org.xbib.net.ldap;
import org.xbib.asn1.ApplicationDERTag;
import org.xbib.asn1.ConstructedDEREncoder;
import org.xbib.asn1.ContextType;
import org.xbib.asn1.DEREncoder;
import org.xbib.asn1.IntegerType;
import org.xbib.asn1.OctetStringType;
/**
* LDAP anonymous bind request.
*
*/
public class AnonymousBindRequest extends AbstractRequestMessage implements BindRequest {
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
@Override
protected DEREncoder[] getRequestEncoders(final int id) {
return new DEREncoder[]{
new IntegerType(id),
new ConstructedDEREncoder(
new ApplicationDERTag(PROTOCOL_OP, true),
new IntegerType(VERSION),
new OctetStringType(""),
new ContextType(0, (byte[]) null)),
};
}
/**
* Simple bind request builder.
*/
public static class Builder extends
AbstractRequestMessage.AbstractBuilder<AnonymousBindRequest.Builder, AnonymousBindRequest> {
/**
* Default constructor.
*/
protected Builder() {
super(new AnonymousBindRequest());
}
@Override
protected Builder self() {
return this;
}
}
}

View file

@ -0,0 +1,76 @@
package org.xbib.net.ldap;
/**
* LDAP modification defined as:
*
* <pre>
* modification PartialAttribute
*
* PartialAttribute ::= SEQUENCE {
* type AttributeDescription,
* vals SET OF value AttributeValue }
* </pre>
*
*/
public class AttributeModification {
/**
* Modification type.
*/
private final Type operation;
/**
* Attribute to modify.
*/
private final LdapAttribute attribute;
/**
* Creates a new modification.
*
* @param type of modification
* @param attr attribute to modify
*/
public AttributeModification(final Type type, final LdapAttribute attr) {
operation = type;
attribute = attr;
}
public Type getOperation() {
return operation;
}
public LdapAttribute getAttribute() {
return attribute;
}
@Override
public String toString() {
return getClass().getName() + "@" + hashCode() + "::" + "operation=" + operation + ", " + "attribute=" + attribute;
}
/**
* Modification type.
*/
public enum Type {
/**
* Add a new attribute.
*/
ADD,
/**
* Delete an attribute.
*/
DELETE,
/**
* Replace an attribute.
*/
REPLACE,
/**
* Increment the value of an attribute.
*/
INCREMENT,
}
}

View file

@ -0,0 +1,279 @@
package org.xbib.net.ldap;
import java.util.Arrays;
import org.xbib.net.ldap.control.RequestControl;
import org.xbib.net.ldap.sasl.CramMD5BindRequest;
import org.xbib.net.ldap.sasl.DigestMD5BindRequest;
import org.xbib.net.ldap.sasl.GssApiBindRequest;
import org.xbib.net.ldap.sasl.Mechanism;
import org.xbib.net.ldap.sasl.SaslBindRequest;
import org.xbib.net.ldap.sasl.SaslConfig;
import org.xbib.net.ldap.sasl.ScramBindRequest;
/**
* Initializes a connection by performing a bind operation. Useful if you need all connections to bind as the same
* principal.
*
*/
public class BindConnectionInitializer implements ConnectionInitializer {
/**
* DN to bind as before performing operations.
*/
private String bindDn;
/**
* Credential for the bind DN.
*/
private Credential bindCredential;
/**
* Configuration for bind SASL authentication.
*/
private SaslConfig bindSaslConfig;
/**
* Bind controls.
*/
private RequestControl[] bindControls;
/**
* Default constructor.
*/
public BindConnectionInitializer() {
}
/**
* Creates a new bind connection initializer.
*
* @param dn bind dn
* @param credential bind credential
*/
public BindConnectionInitializer(final String dn, final String credential) {
setBindDn(dn);
setBindCredential(new Credential(credential));
}
/**
* Creates a new bind connection initializer.
*
* @param dn bind dn
* @param credential bind credential
*/
public BindConnectionInitializer(final String dn, final Credential credential) {
setBindDn(dn);
setBindCredential(credential);
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns the bind DN.
*
* @return DN to bind as
*/
public String getBindDn() {
return bindDn;
}
/**
* Sets the bind DN to authenticate as before performing operations.
*
* @param dn to bind as
*/
public void setBindDn(final String dn) {
bindDn = dn;
}
/**
* Returns the credential used with the bind DN.
*
* @return bind DN credential
*/
public Credential getBindCredential() {
return bindCredential;
}
/**
* Sets the credential of the bind DN.
*
* @param credential to use with bind DN
*/
public void setBindCredential(final Credential credential) {
bindCredential = credential;
}
/**
* Returns the bind sasl config.
*
* @return sasl config
*/
public SaslConfig getBindSaslConfig() {
return bindSaslConfig;
}
/**
* Sets the bind sasl config.
*
* @param config sasl config
*/
public void setBindSaslConfig(final SaslConfig config) {
bindSaslConfig = config;
}
/**
* Returns the bind controls.
*
* @return controls
*/
public RequestControl[] getBindControls() {
return bindControls;
}
/**
* Sets the bind controls.
*
* @param cntrls controls to set
*/
public void setBindControls(final RequestControl... cntrls) {
bindControls = cntrls;
}
@Override
public Result initialize(final Connection c)
throws LdapException {
final Result result;
if (bindSaslConfig != null) {
switch (bindSaslConfig.getMechanism()) {
case EXTERNAL:
result = c.operation(SaslBindRequest.builder()
.mechanism(Mechanism.EXTERNAL.mechanism())
.credentials(bindSaslConfig.getAuthorizationId() != null ? bindSaslConfig.getAuthorizationId() : "")
.controls(bindControls).build()).execute();
break;
case DIGEST_MD5:
result = c.operation(new DigestMD5BindRequest(
bindDn,
bindSaslConfig.getAuthorizationId(),
bindCredential != null ? bindCredential.getString() : null,
bindSaslConfig.getRealm(),
DigestMD5BindRequest.createProperties(bindSaslConfig)));
break;
case CRAM_MD5:
result = c.operation(new CramMD5BindRequest(
bindDn,
bindCredential != null ? bindCredential.getString() : null));
break;
case GSSAPI:
result = c.operation(new GssApiBindRequest(
bindDn,
bindSaslConfig.getAuthorizationId(),
bindCredential != null ? bindCredential.getString() : null,
bindSaslConfig.getRealm(),
GssApiBindRequest.createProperties(bindSaslConfig)));
break;
case SCRAM_SHA_1:
result = c.operation(new ScramBindRequest(Mechanism.SCRAM_SHA_1, bindDn, bindCredential.getString()));
break;
case SCRAM_SHA_256:
result = c.operation(new ScramBindRequest(Mechanism.SCRAM_SHA_256, bindDn, bindCredential.getString()));
break;
case SCRAM_SHA_512:
result = c.operation(new ScramBindRequest(Mechanism.SCRAM_SHA_512, bindDn, bindCredential.getString()));
break;
default:
throw new IllegalStateException("Unknown SASL mechanism: " + bindSaslConfig.getMechanism());
}
} else if (bindDn == null && bindCredential == null) {
result = c.operation(AnonymousBindRequest.builder()
.controls(bindControls).build()).execute();
} else {
result = c.operation(SimpleBindRequest.builder()
.dn(bindDn)
.password(bindCredential.getString())
.controls(bindControls).build()).execute();
}
return result;
}
/**
* Returns whether this connection initializer contains any configuration data.
*
* @return whether all properties are null
*/
public boolean isEmpty() {
return bindDn == null && bindCredential == null && bindSaslConfig == null && bindControls == null;
}
@Override
public String toString() {
return getClass().getName() + "@" + hashCode() + "::" +
"bindDn=" + bindDn + ", " +
"bindSaslConfig=" + bindSaslConfig + ", " +
"bindControls=" + Arrays.toString(bindControls);
}
// CheckStyle:OFF
public static class Builder {
private final BindConnectionInitializer object = new BindConnectionInitializer();
protected Builder() {
}
public Builder dn(final String dn) {
object.setBindDn(dn);
return this;
}
public Builder credential(final Credential credential) {
object.setBindCredential(credential);
return this;
}
public Builder credential(final String credential) {
object.setBindCredential(new Credential(credential));
return this;
}
public Builder credential(final byte[] credential) {
object.setBindCredential(new Credential(credential));
return this;
}
public Builder saslConfig(final SaslConfig config) {
object.setBindSaslConfig(config);
return this;
}
public Builder controls(final RequestControl... controls) {
object.setBindControls(controls);
return this;
}
public BindConnectionInitializer build() {
return object;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,149 @@
package org.xbib.net.ldap;
/**
* Executes an ldap bind operation.
*
*/
public class BindOperation extends AbstractOperation<BindRequest, BindResponse> {
/**
* Default constructor.
*/
public BindOperation() {
}
/**
* Creates a new bind operation.
*
* @param factory connection factory
*/
public BindOperation(final ConnectionFactory factory) {
super(factory);
}
/**
* Sends a bind request. See {@link OperationHandle#send()}.
*
* @param factory connection factory
* @param request bind request
* @return operation handle
* @throws LdapException if the connection cannot be opened
*/
public static OperationHandle<BindRequest, BindResponse> send(
final ConnectionFactory factory,
final BindRequest request)
throws LdapException {
final Connection conn = factory.getConnection();
try {
conn.open();
} catch (Exception e) {
conn.close();
throw e;
}
return conn.operation(request).onComplete(conn::close).send();
}
/**
* Executes a bind request. See {@link OperationHandle#execute()}.
*
* @param factory connection factory
* @param request bind request
* @return bind result
* @throws LdapException if the connection cannot be opened
*/
public static BindResponse execute(final ConnectionFactory factory, final BindRequest request)
throws LdapException {
try (Connection conn = factory.getConnection()) {
conn.open();
return conn.operation(request).execute();
}
}
/**
* Returns a new bind operation with the same properties as the supplied operation.
*
* @param operation to copy
* @return copy of the supplied bind operation
*/
public static BindOperation copy(final BindOperation operation) {
final BindOperation op = new BindOperation();
op.setRequestHandlers(operation.getRequestHandlers());
op.setResultHandlers(operation.getResultHandlers());
op.setControlHandlers(operation.getControlHandlers());
op.setReferralHandlers(operation.getReferralHandlers());
op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers());
op.setExceptionHandler(operation.getExceptionHandler());
op.setThrowCondition(operation.getThrowCondition());
op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers());
op.setConnectionFactory(operation.getConnectionFactory());
return op;
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Sends a bind request. See {@link OperationHandle#send()}.
*
* @param request bind request
* @return operation handle
* @throws LdapException if the connection cannot be opened
*/
@Override
public OperationHandle<BindRequest, BindResponse> send(final BindRequest request)
throws LdapException {
final Connection conn = getConnectionFactory().getConnection();
try {
conn.open();
} catch (Exception e) {
conn.close();
throw e;
}
return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send();
}
/**
* Executes a bind request. See {@link OperationHandle#execute()}.
*
* @param request bind request
* @return bind result
* @throws LdapException if the connection cannot be opened
*/
@Override
public BindResponse execute(final BindRequest request)
throws LdapException {
try (Connection conn = getConnectionFactory().getConnection()) {
conn.open();
return configureHandle(conn.operation(configureRequest(request))).execute();
}
}
/**
* Bind operation builder.
*/
public static class Builder extends AbstractOperation.AbstractBuilder<BindOperation.Builder, BindOperation> {
/**
* Creates a new builder.
*/
protected Builder() {
super(new BindOperation());
}
@Override
protected Builder self() {
return this;
}
}
}

View file

@ -0,0 +1,38 @@
package org.xbib.net.ldap;
/**
* LDAP bind request defined as:
*
* <pre>
* BindRequest ::= [APPLICATION 0] SEQUENCE {
* version INTEGER (1 .. 127),
* name LDAPDN,
* authentication AuthenticationChoice }
*
* AuthenticationChoice ::= CHOICE {
* simple [0] OCTET STRING,
* -- 1 and 2 reserved
* sasl [3] SaslCredentials,
* ... }
*
* SaslCredentials ::= SEQUENCE {
* mechanism LDAPString,
* credentials OCTET STRING OPTIONAL }
* </pre>
*
*/
// CheckStyle:InterfaceIsType OFF
public interface BindRequest extends Request {
/**
* BER protocol number.
*/
int PROTOCOL_OP = 0;
/**
* bind protocol version.
*/
int VERSION = 3;
}
// CheckStyle:InterfaceIsType ON

View file

@ -0,0 +1,173 @@
package org.xbib.net.ldap;
import org.xbib.asn1.AbstractParseHandler;
import org.xbib.asn1.DERBuffer;
import org.xbib.asn1.DERParser;
import org.xbib.asn1.DERPath;
/**
* LDAP bind response defined as:
*
* <pre>
* BindResponse ::= [APPLICATION 1] SEQUENCE {
* COMPONENTS OF LDAPResult,
* serverSaslCreds [7] OCTET STRING OPTIONAL }
* </pre>
*
*/
public class BindResponse extends AbstractResult {
/**
* BER protocol number.
*/
public static final int PROTOCOL_OP = 1;
/**
* hash code seed.
*/
private static final int HASH_CODE_SEED = 10243;
/**
* DER path to result code.
*/
private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(1)/ENUM[0]");
/**
* DER path to matched DN.
*/
private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(1)/OCTSTR[1]");
/**
* DER path to diagnostic message.
*/
private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(1)/OCTSTR[2]");
/**
* DER path to referral.
*/
private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(1)/CTX(3)/OCTSTR[0]");
/**
* DER path to SASL credentials.
*/
private static final DERPath SASL_CREDENTIALS_PATH = new DERPath("/SEQ/APP(1)/CTX(7)");
/**
* Server SASL credentials.
*/
private byte[] serverSaslCreds;
/**
* Default constructor.
*/
private BindResponse() {
}
/**
* Creates a new bind response.
*
* @param buffer to decode
*/
public BindResponse(final DERBuffer buffer) {
final DERParser parser = new DERParser();
parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this));
parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this));
parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this));
parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this));
parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this));
parser.registerHandler(SASL_CREDENTIALS_PATH, new SASLCredsHandler(this));
parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this));
parser.parse(buffer);
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
public byte[] getServerSaslCreds() {
return serverSaslCreds;
}
public void setServerSaslCreds(final byte[] creds) {
serverSaslCreds = creds;
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (o instanceof BindResponse v && super.equals(o)) {
return LdapUtils.areEqual(serverSaslCreds, v.serverSaslCreds);
}
return false;
}
@Override
public int hashCode() {
return
LdapUtils.computeHashCode(
HASH_CODE_SEED,
getMessageID(),
getControls(),
getResultCode(),
getMatchedDN(),
getDiagnosticMessage(),
getReferralURLs(),
serverSaslCreds);
}
/**
* Parse handler implementation for the server SASL creds.
*/
protected static class SASLCredsHandler extends AbstractParseHandler<BindResponse> {
/**
* Creates a new server SASL creds handler.
*
* @param response to configure
*/
SASLCredsHandler(final BindResponse response) {
super(response);
}
@Override
public void handle(final DERParser parser, final DERBuffer encoded) {
if (encoded.remaining() > 0) {
getObject().setServerSaslCreds(encoded.getRemainingBytes());
}
}
}
// CheckStyle:OFF
public static class Builder extends AbstractResult.AbstractBuilder<Builder, BindResponse> {
protected Builder() {
super(new BindResponse());
}
@Override
protected Builder self() {
return this;
}
public Builder serverSaslCreds(final byte[] creds) {
object.setServerSaslCreds(creds);
return this;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,44 @@
package org.xbib.net.ldap;
import java.time.Instant;
/**
* Retry metadata used when a connection is unexpectedly closed.
*
*/
public class ClosedRetryMetadata extends AbstractRetryMetadata {
/**
* Last thrown exception.
*/
protected final Throwable failureException;
/**
* Creates a new closed retry metadata.
*
* @param time of last successful connection
* @param ex exception that caused the connection to close
*/
public ClosedRetryMetadata(final Instant time, final Throwable ex) {
successTime = time;
failureException = ex;
}
/**
* Returns the exception that caused the closed connection.
*
* @return failure exception
*/
public Throwable getFailureException() {
return failureException;
}
@Override
public String toString() {
return super.toString() + ", " + "failureException=" + failureException;
}
}

View file

@ -0,0 +1,108 @@
package org.xbib.net.ldap;
import java.time.Duration;
/**
* Validates a connection is healthy by performing a compare operation. Unless {@link
* #setValidResultCodes(ResultCode...)} is set, validation is considered successful if the compare result contains any
* result code.
*
*/
public class CompareConnectionValidator extends AbstractOperationConnectionValidator<CompareRequest, CompareResponse> {
/**
* Creates a new compare validator.
*/
public CompareConnectionValidator() {
this(CompareRequest.builder().dn("").name("objectClass").value("top").build());
}
/**
* Creates a new compare validator.
*
* @param cr to use for compares
*/
public CompareConnectionValidator(final CompareRequest cr) {
this(DEFAULT_VALIDATE_PERIOD, DEFAULT_VALIDATE_TIMEOUT, cr);
}
/**
* Creates a new compare validator.
*
* @param period execution period
* @param timeout execution timeout
* @param request to use for searches
*/
public CompareConnectionValidator(final Duration period, final Duration timeout, final CompareRequest request) {
setValidatePeriod(period);
setValidateTimeout(timeout);
setRequest(request);
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns the compare request.
*
* @return compare request
* @deprecated use {@link AbstractOperationConnectionValidator#getRequest()}
*/
@Deprecated
public CompareRequest getCompareRequest() {
return getRequest();
}
/**
* Sets the compare request.
*
* @param cr compare request
* @deprecated use {@link AbstractOperationConnectionValidator#setRequest(Request)}
*/
@Deprecated
public void setCompareRequest(final CompareRequest cr) {
setRequest(cr);
}
@Override
protected OperationHandle<CompareRequest, CompareResponse> performOperation(final Connection conn) {
return conn.operation(getRequest());
}
@Override
public String toString() {
return "[" + super.toString() + "]";
}
/**
* Compare validator builder.
*/
public static class Builder extends
AbstractOperationConnectionValidator.AbstractBuilder<
CompareRequest, CompareResponse, CompareConnectionValidator.Builder, CompareConnectionValidator> {
/**
* Creates a new builder.
*/
protected Builder() {
super(new CompareConnectionValidator());
}
@Override
protected Builder self() {
return this;
}
}
}

View file

@ -0,0 +1,199 @@
package org.xbib.net.ldap;
import java.util.Arrays;
import org.xbib.net.ldap.handler.CompareValueHandler;
/**
* Executes an ldap compare operation.
*
*/
public class CompareOperation extends AbstractOperation<CompareRequest, CompareResponse> {
/**
* Functions to handle the compare result.
*/
private CompareValueHandler[] compareValueHandlers;
/**
* Default constructor.
*/
public CompareOperation() {
}
/**
* Creates a new compare operation.
*
* @param factory connection factory
*/
public CompareOperation(final ConnectionFactory factory) {
super(factory);
}
/**
* Sends a compare request. See {@link OperationHandle#send()}.
*
* @param factory connection factory
* @param request compare request
* @return operation handle
* @throws LdapException if the connection cannot be opened
*/
public static CompareOperationHandle send(final ConnectionFactory factory, final CompareRequest request)
throws LdapException {
final Connection conn = factory.getConnection();
try {
conn.open();
} catch (Exception e) {
conn.close();
throw e;
}
return conn.operation(request).onComplete(conn::close).send();
}
/**
* Executes a compare request. See {@link OperationHandle#execute()}.
*
* @param factory connection factory
* @param request compare request
* @return compare result
* @throws LdapException if the connection cannot be opened
*/
public static CompareResponse execute(final ConnectionFactory factory, final CompareRequest request)
throws LdapException {
try (Connection conn = factory.getConnection()) {
conn.open();
return conn.operation(request).execute();
}
}
/**
* Returns a new compare operation with the same properties as the supplied operation.
*
* @param operation to copy
* @return copy of the supplied compare operation
*/
public static CompareOperation copy(final CompareOperation operation) {
final CompareOperation op = new CompareOperation();
op.setRequestHandlers(operation.getRequestHandlers());
op.setResultHandlers(operation.getResultHandlers());
op.setControlHandlers(operation.getControlHandlers());
op.setReferralHandlers(operation.getReferralHandlers());
op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers());
op.setExceptionHandler(operation.getExceptionHandler());
op.setThrowCondition(operation.getThrowCondition());
op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers());
op.setConnectionFactory(operation.getConnectionFactory());
op.setCompareValueHandlers(operation.getCompareValueHandlers());
return op;
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
public CompareValueHandler[] getCompareValueHandlers() {
return compareValueHandlers;
}
public void setCompareValueHandlers(final CompareValueHandler... handlers) {
compareValueHandlers = handlers;
}
/**
* Sends a compare request. See {@link OperationHandle#send()}.
*
* @param request compare request
* @return operation handle
* @throws LdapException if the connection cannot be opened
*/
@Override
public CompareOperationHandle send(final CompareRequest request)
throws LdapException {
final Connection conn = getConnectionFactory().getConnection();
try {
conn.open();
} catch (Exception e) {
conn.close();
throw e;
}
return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send();
}
/**
* Executes a compare request. See {@link OperationHandle#execute()}.
*
* @param request compare request
* @return compare result
* @throws LdapException if the connection cannot be opened
*/
@Override
public CompareResponse execute(final CompareRequest request)
throws LdapException {
try (Connection conn = getConnectionFactory().getConnection()) {
conn.open();
return configureHandle(conn.operation(configureRequest(request))).execute();
}
}
/**
* Adds configured functions to the supplied handle.
*
* @param handle to configure
* @return configured handle
*/
protected CompareOperationHandle configureHandle(final CompareOperationHandle handle) {
return handle
.onCompare(getCompareValueHandlers())
.onControl(getControlHandlers())
.onReferral(getReferralHandlers())
.onIntermediate(getIntermediateResponseHandlers())
.onException(getExceptionHandler())
.throwIf(getThrowCondition())
.onUnsolicitedNotification(getUnsolicitedNotificationHandlers())
.onResult(getResultHandlers());
}
@Override
public String toString() {
return super.toString() + ", " + "compareValueHandlers=" + Arrays.toString(compareValueHandlers);
}
/**
* Compare operation builder.
*/
public static class Builder extends AbstractOperation.AbstractBuilder<CompareOperation.Builder, CompareOperation> {
/**
* Creates a new builder.
*/
protected Builder() {
super(new CompareOperation());
}
@Override
protected Builder self() {
return this;
}
/**
* Sets the functions to execute when a compare result is complete.
*
* @param handlers to execute on a compare result
* @return this builder
*/
public Builder onCompare(final CompareValueHandler... handlers) {
object.setCompareValueHandlers(handlers);
return self();
}
}
}

View file

@ -0,0 +1,75 @@
package org.xbib.net.ldap;
import org.xbib.net.ldap.handler.CompareValueHandler;
import org.xbib.net.ldap.handler.CompleteHandler;
import org.xbib.net.ldap.handler.ExceptionHandler;
import org.xbib.net.ldap.handler.IntermediateResponseHandler;
import org.xbib.net.ldap.handler.ReferralHandler;
import org.xbib.net.ldap.handler.ResponseControlHandler;
import org.xbib.net.ldap.handler.ResultHandler;
import org.xbib.net.ldap.handler.ResultPredicate;
import org.xbib.net.ldap.handler.UnsolicitedNotificationHandler;
/**
* Handle that notifies on the components of a compare request.
*
*/
public interface CompareOperationHandle extends OperationHandle<CompareRequest, CompareResponse> {
@Override
CompareOperationHandle send();
@Override
CompareResponse await() throws LdapException;
@Override
default CompareResponse execute()
throws LdapException {
return send().await();
}
@Override
CompareOperationHandle onResult(ResultHandler... function);
@Override
CompareOperationHandle onControl(ResponseControlHandler... function);
@Override
CompareOperationHandle onReferral(ReferralHandler... function);
@Override
CompareOperationHandle onIntermediate(IntermediateResponseHandler... function);
@Override
CompareOperationHandle onUnsolicitedNotification(UnsolicitedNotificationHandler... function);
@Override
CompareOperationHandle onException(ExceptionHandler function);
@Override
CompareOperationHandle throwIf(ResultPredicate function);
@Override
CompareOperationHandle onComplete(CompleteHandler function);
/**
* Sets the function to execute when a compare result is received.
*
* @param function to execute on a compare result
* @return this handle
*/
CompareOperationHandle onCompare(CompareValueHandler... function);
}

View file

@ -0,0 +1,204 @@
package org.xbib.net.ldap;
import org.xbib.asn1.ApplicationDERTag;
import org.xbib.asn1.ConstructedDEREncoder;
import org.xbib.asn1.DEREncoder;
import org.xbib.asn1.IntegerType;
import org.xbib.asn1.OctetStringType;
import org.xbib.asn1.UniversalDERTag;
/**
* LDAP compare request defined as:
*
* <pre>
* CompareRequest ::= [APPLICATION 14] SEQUENCE {
* entry LDAPDN,
* ava AttributeValueAssertion }
* </pre>
*
*/
public class CompareRequest extends AbstractRequestMessage {
/**
* BER protocol number.
*/
public static final int PROTOCOL_OP = 14;
/**
* LDAP DN to compare.
*/
private String ldapDn;
/**
* Attribute description
*/
private String attributeDesc;
/**
* Assertion value.
*/
private String assertionValue;
/**
* Default constructor.
*/
public CompareRequest() {
}
/**
* Creates a new compare request.
*
* @param dn to compare
* @param name attribute description
* @param value assertion value
*/
public CompareRequest(final String dn, final String name, final String value) {
ldapDn = dn;
attributeDesc = name;
assertionValue = value;
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns the DN.
*
* @return DN
*/
public String getDn() {
return ldapDn;
}
/**
* Sets the DN.
*
* @param dn ldapDn to set
*/
public void setDn(final String dn) {
ldapDn = dn;
}
/**
* Returns the name.
*
* @return name
*/
public String getName() {
return attributeDesc;
}
/**
* Sets the name.
*
* @param name attributeDesc to set
*/
public void setName(final String name) {
attributeDesc = name;
}
/**
* Returns the value.
*
* @return value
*/
public String getValue() {
return assertionValue;
}
/**
* Sets the value.
*
* @param value assertionValue to set
*/
public void setValue(final String value) {
assertionValue = value;
}
@Override
protected DEREncoder[] getRequestEncoders(final int id) {
return new DEREncoder[]{
new IntegerType(id),
new ConstructedDEREncoder(
new ApplicationDERTag(PROTOCOL_OP, true),
new OctetStringType(ldapDn),
new ConstructedDEREncoder(
UniversalDERTag.SEQ,
new OctetStringType(attributeDesc),
new OctetStringType(assertionValue))),
};
}
@Override
public String toString() {
return super.toString() + ", " +
"dn=" + ldapDn + ", " +
"attributeDesc=" + attributeDesc + ", " +
"assertionValue=" + ("userPassword".equalsIgnoreCase(attributeDesc) ? "<suppressed>" : assertionValue);
}
/**
* Compare request builder.
*/
public static class Builder extends AbstractRequestMessage.AbstractBuilder<CompareRequest.Builder, CompareRequest> {
/**
* Default constructor.
*/
protected Builder() {
super(new CompareRequest());
}
@Override
protected Builder self() {
return this;
}
/**
* Sets the ldap DN.
*
* @param dn ldap DN
* @return this builder
*/
public Builder dn(final String dn) {
object.ldapDn = dn;
return self();
}
/**
* Sets the attribute description.
*
* @param name attribute description
* @return this builder
*/
public Builder name(final String name) {
object.attributeDesc = name;
return self();
}
/**
* Sets the assertion value.
*
* @param value assertion value
* @return this builder
*/
public Builder value(final String value) {
object.assertionValue = value;
return self();
}
}
}

View file

@ -0,0 +1,135 @@
package org.xbib.net.ldap;
import org.xbib.asn1.DERBuffer;
import org.xbib.asn1.DERParser;
import org.xbib.asn1.DERPath;
/**
* LDAP compare response defined as:
*
* <pre>
* CompareResponse ::= [APPLICATION 15] LDAPResult
* </pre>
*
*/
public class CompareResponse extends AbstractResult {
/**
* BER protocol number.
*/
public static final int PROTOCOL_OP = 15;
/**
* hash code seed.
*/
private static final int HASH_CODE_SEED = 10247;
/**
* DER path to result code.
*/
private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(15)/ENUM[0]");
/**
* DER path to matched DN.
*/
private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(15)/OCTSTR[1]");
/**
* DER path to diagnostic message.
*/
private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(15)/OCTSTR[2]");
/**
* DER path to referral.
*/
private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(15)/CTX(3)/OCTSTR[0]");
/**
* Default constructor.
*/
private CompareResponse() {
}
/**
* Creates a new compare response.
*
* @param buffer to decode
*/
public CompareResponse(final DERBuffer buffer) {
final DERParser parser = new DERParser();
parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this));
parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this));
parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this));
parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this));
parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this));
parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this));
parser.parse(buffer);
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns whether the result code in this result is {@link ResultCode#COMPARE_TRUE}.
*
* @return whether this result is compare true
*/
public boolean isTrue() {
return ResultCode.COMPARE_TRUE == getResultCode();
}
/**
* Returns whether the result code in this result is {@link ResultCode#COMPARE_FALSE}.
*
* @return whether this result is compare false
*/
public boolean isFalse() {
return ResultCode.COMPARE_FALSE == getResultCode();
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
return o instanceof CompareResponse && super.equals(o);
}
@Override
public int hashCode() {
return
LdapUtils.computeHashCode(
HASH_CODE_SEED,
getMessageID(),
getControls(),
getResultCode(),
getMatchedDN(),
getDiagnosticMessage(),
getReferralURLs());
}
// CheckStyle:OFF
public static class Builder extends AbstractResult.AbstractBuilder<Builder, CompareResponse> {
protected Builder() {
super(new CompareResponse());
}
@Override
protected Builder self() {
return this;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,48 @@
package org.xbib.net.ldap;
/**
* Exception that indicates a connection attempt failed.
*
*/
public class ConnectException extends LdapException {
/**
* serialVersionUID.
*/
private static final long serialVersionUID = -5935483002226156942L;
/**
* Creates a new connect exception.
*
* @param code result code describing this exception
* @param msg describing this exception
*/
public ConnectException(final ResultCode code, final String msg) {
super(code, msg);
}
/**
* Creates a new connect exception.
*
* @param code result code describing this exception
* @param e underlying exception
*/
public ConnectException(final ResultCode code, final Throwable e) {
super(code, e);
}
/**
* Creates a new connect exception.
*
* @param code result code describing this exception
* @param msg describing this exception
* @param e underlying exception
*/
public ConnectException(final ResultCode code, final String msg, final Throwable e) {
super(code, msg, e);
}
}

View file

@ -0,0 +1,156 @@
package org.xbib.net.ldap;
import org.xbib.net.ldap.control.RequestControl;
import org.xbib.net.ldap.extended.ExtendedOperationHandle;
import org.xbib.net.ldap.extended.ExtendedRequest;
import org.xbib.net.ldap.sasl.DefaultSaslClientRequest;
import org.xbib.net.ldap.sasl.SaslClientRequest;
/**
* Interface for connection implementations.
*
*/
public interface Connection extends AutoCloseable {
/**
* Executes an abandon operation. Clients should execute abandons using {@link OperationHandle#abandon()}.
*
* @param request abandon request
*/
void operation(AbandonRequest request);
/**
* Creates a handle for an add operation.
*
* @param request add request
* @return operation handle
*/
OperationHandle<AddRequest, AddResponse> operation(AddRequest request);
/**
* Creates a handle for a bind operation. Since clients must not send requests while a bind is in progress, some
* methods may not be supported on the operation handle.
*
* @param request bind request
* @return operation handle
*/
OperationHandle<BindRequest, BindResponse> operation(BindRequest request);
/**
* Creates a handle for a compare operation.
*
* @param request compare request
* @return compare operation handle
*/
CompareOperationHandle operation(CompareRequest request);
/**
* Creates a handle for a delete operation.
*
* @param request delete request
* @return operation handle
*/
OperationHandle<DeleteRequest, DeleteResponse> operation(DeleteRequest request);
/**
* Creates a handle for an extended operation.
*
* @param request extended request
* @return extended operation handle
*/
ExtendedOperationHandle operation(ExtendedRequest request);
/**
* Creates a handle for a modify operation.
*
* @param request modify request
* @return operation handle
*/
OperationHandle<ModifyRequest, ModifyResponse> operation(ModifyRequest request);
/**
* Creates a handle for a modify dn operation.
*
* @param request modify dn request
* @return operation handle
*/
OperationHandle<ModifyDnRequest, ModifyDnResponse> operation(ModifyDnRequest request);
/**
* Creates a handle for a search operation.
*
* @param request search request
* @return search operation handle
*/
SearchOperationHandle operation(SearchRequest request);
/**
* Returns the result of a SASL request that requires use of a generic SASL client.
*
* @param request SASL client request
* @return operation result
* @throws LdapException if the operation fails or another bind is in progress
*/
BindResponse operation(SaslClientRequest request) throws LdapException;
/**
* Returns the result of a SASL request that requires use of the default SASL client. This includes CRAM-MD5,
* DIGEST-MD5, and GSS-API.
*
* @param request default SASL client request
* @return operation result
* @throws LdapException if the operation fails or another bind is in progress
*/
BindResponse operation(DefaultSaslClientRequest request) throws LdapException;
/**
* Returns the URL that was selected for this connection. The existence of this value does not indicate a current
* established connection.
*
* @return LDAP URL
*/
LdapURL getLdapURL();
/**
* Returns whether this connection is open.
*
* @return whether this connection is open
*/
boolean isOpen();
/**
* Opens the connection.
*
* @throws LdapException if an error occurs opening the connection
*/
void open() throws LdapException;
@Override
default void close() {
close((RequestControl[]) null);
}
/**
* Closes the connection.
*
* @param controls to send when closing the connection
*/
void close(RequestControl... controls);
}

View file

@ -0,0 +1,601 @@
package org.xbib.net.ldap;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;
import org.xbib.net.ldap.ssl.SslConfig;
/**
* Contains all the configuration data needed to control connections.
*
*/
public class ConnectionConfig extends AbstractConfig {
/**
* Predicate that attempts a single reconnect.
*/
public static final Predicate<RetryMetadata> ONE_RECONNECT_ATTEMPT =
new Predicate<>() {
@Override
public boolean test(final RetryMetadata metadata) {
return metadata instanceof ClosedRetryMetadata && metadata.getAttempts() == 0;
}
@Override
public String toString() {
return "ONE_RECONNECT_ATTEMPT";
}
};
/**
* Predicate that attempts to reconnect forever, waiting for 5 seconds after the first attempt.
*/
public static final Predicate<RetryMetadata> INFINITE_RECONNECT_ATTEMPTS =
new Predicate<>() {
@Override
public boolean test(final RetryMetadata metadata) {
if (metadata instanceof ClosedRetryMetadata) {
if (metadata.getAttempts() > 0) {
try {
// CheckStyle:MagicNumber OFF
Thread.sleep(Duration.ofSeconds(5).toMillis());
// CheckStyle:MagicNumber ON
} catch (InterruptedException ignored) {
}
}
return true;
}
return false;
}
@Override
public String toString() {
return "INFINITE_RECONNECT_ATTEMPTS";
}
};
/**
* Predicate that attempts to reconnect forever, backing off in 5 second intervals after the first attempt.
*/
public static final Predicate<RetryMetadata> INFINITE_RECONNECT_ATTEMPTS_WITH_BACKOFF =
new Predicate<>() {
@Override
public boolean test(final RetryMetadata metadata) {
if (metadata instanceof ClosedRetryMetadata) {
if (metadata.getAttempts() > 0) {
try {
// CheckStyle:MagicNumber OFF
Thread.sleep(Duration.ofSeconds(5).multipliedBy(metadata.getAttempts()).toMillis());
// CheckStyle:MagicNumber ON
} catch (InterruptedException ignored) {
}
}
return true;
}
return false;
}
@Override
public String toString() {
return "INFINITE_RECONNECT_ATTEMPTS_WITH_BACKOFF";
}
};
/**
* Transport options.
*/
private final Map<String, Object> transportOptions = new HashMap<>();
/**
* URL to the LDAP(s).
*/
private String ldapUrl;
/**
* Duration of time that connects will block.
*/
private Duration connectTimeout = Duration.ofMinutes(1);
/**
* Duration of time to wait for startTLS responses.
*/
private Duration startTLSTimeout = Duration.ofMinutes(1);
/**
* Duration of time to wait for responses.
*/
private Duration responseTimeout = Duration.ofMinutes(1);
/**
* Duration of time that operations will block on reconnects, should generally be longer than {@link
* #connectTimeout}.
*/
private Duration reconnectTimeout = Duration.ofMinutes(2);
/**
* Whether to automatically reconnect to the server when a connection is lost. Default is true.
*/
private boolean autoReconnect = true;
/**
* Condition used to determine whether another reconnect attempt should be made. Default makes a single attempt only
* if the connection was previously opened.
*/
private Predicate<RetryMetadata> autoReconnectCondition = ONE_RECONNECT_ATTEMPT;
/**
* Whether pending operations should be replayed after a reconnect. Default is false.
*/
private boolean autoReplay;
/**
* Configuration for SSL and startTLS connections.
*/
private SslConfig sslConfig;
/**
* Connect to LDAP using startTLS.
*/
private boolean useStartTLS;
/**
* Connection initializers to execute on {@link Connection#open()}.
*/
private ConnectionInitializer[] connectionInitializers;
/**
* Connection strategy.
*/
private ConnectionStrategy connectionStrategy = new ActivePassiveConnectionStrategy();
/**
* Connection validator.
*/
private ConnectionValidator connectionValidator;
/**
* Default constructor.
*/
public ConnectionConfig() {
}
/**
* Creates a new connection config.
*
* @param url to connect to
*/
public ConnectionConfig(final String url) {
setLdapUrl(url);
}
/**
* Returns a new connection config initialized with the supplied config.
*
* @param config connection config to read properties from
* @return connection config
*/
public static ConnectionConfig copy(final ConnectionConfig config) {
final ConnectionConfig cc = new ConnectionConfig();
cc.setLdapUrl(config.getLdapUrl());
cc.setConnectTimeout(config.getConnectTimeout());
cc.setStartTLSTimeout(config.getStartTLSTimeout());
cc.setResponseTimeout(config.getResponseTimeout());
cc.setReconnectTimeout(config.getReconnectTimeout());
cc.setAutoReconnect(config.getAutoReconnect());
cc.setAutoReconnectCondition(config.getAutoReconnectCondition());
cc.setAutoReplay(config.getAutoReplay());
cc.setSslConfig(config.getSslConfig() != null ? SslConfig.copy(config.getSslConfig()) : null);
cc.setUseStartTLS(config.getUseStartTLS());
cc.setConnectionInitializers(config.getConnectionInitializers());
cc.setConnectionStrategy(
config.getConnectionStrategy() != null ? config.getConnectionStrategy().newInstance() : null);
cc.setConnectionValidator(config.getConnectionValidator());
cc.setTransportOptions(config.getTransportOptions());
return cc;
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns the ldap url.
*
* @return ldap url
*/
public String getLdapUrl() {
return ldapUrl;
}
/**
* Sets the ldap url.
*
* @param url of the ldap
*/
public void setLdapUrl(final String url) {
checkStringInput(url, true);
ldapUrl = url;
}
/**
* Returns the connect timeout.
*
* @return timeout
*/
public Duration getConnectTimeout() {
return connectTimeout;
}
/**
* Sets the maximum amount of time that connects will block.
*
* @param time timeout for connects
*/
public void setConnectTimeout(final Duration time) {
if (time == null || time.isNegative()) {
throw new IllegalArgumentException("Connect timeout cannot be null or negative");
}
connectTimeout = time;
}
/**
* Returns the startTLS timeout.
*
* @return timeout
*/
public Duration getStartTLSTimeout() {
return startTLSTimeout;
}
/**
* Sets the maximum amount of time that startTLS operations will wait for a response.
*
* @param time timeout for responses
*/
public void setStartTLSTimeout(final Duration time) {
if (time == null || time.isNegative()) {
throw new IllegalArgumentException("StartTLS timeout cannot be null or negative");
}
startTLSTimeout = time;
}
/**
* Returns the response timeout.
*
* @return timeout
*/
public Duration getResponseTimeout() {
return responseTimeout;
}
/**
* Sets the maximum amount of time that operations will wait for a response.
*
* @param time timeout for responses
*/
public void setResponseTimeout(final Duration time) {
if (time == null || time.isNegative()) {
throw new IllegalArgumentException("Response timeout cannot be null or negative");
}
responseTimeout = time;
}
/**
* Returns the reconnect timeout.
*
* @return timeout
*/
public Duration getReconnectTimeout() {
return reconnectTimeout;
}
/**
* Sets the maximum amount of time that operations will block waiting for a reconnect.
*
* @param time timeout for reconnects
*/
public void setReconnectTimeout(final Duration time) {
if (time == null || time.isNegative()) {
throw new IllegalArgumentException("Reconnect timeout cannot be null or negative");
}
reconnectTimeout = time;
}
/**
* Returns whether connections will attempt to reconnect.
*
* @return whether to automatically reconnect when a connection is lost
*/
public boolean getAutoReconnect() {
return autoReconnect;
}
/**
* Sets whether connections will attempt to reconnect when unexpectedly closed.
*
* @param b whether to automatically reconnect when a connection is lost
*/
public void setAutoReconnect(final boolean b) {
autoReconnect = b;
}
/**
* Returns the auto reconnect condition.
*
* @return auto reconnect condition
*/
public Predicate<RetryMetadata> getAutoReconnectCondition() {
return autoReconnectCondition;
}
/**
* Sets the auto reconnect condition.
*
* @param predicate to determine whether to attempt a reconnect
*/
public void setAutoReconnectCondition(final Predicate<RetryMetadata> predicate) {
autoReconnectCondition = predicate;
}
/**
* Returns whether operations should be replayed after a reconnect.
*
* @return whether to auto replay
*/
public boolean getAutoReplay() {
return autoReplay;
}
/**
* Sets whether operations will be replayed after a reconnect.
*
* @param b whether to replay operations
*/
public void setAutoReplay(final boolean b) {
autoReplay = b;
}
/**
* Returns the ssl config.
*
* @return ssl config
*/
public SslConfig getSslConfig() {
return sslConfig;
}
/**
* Sets the ssl config.
*
* @param config ssl config
*/
public void setSslConfig(final SslConfig config) {
sslConfig = config;
}
/**
* Returns whether startTLS will be used for connections.
*
* @return whether startTLS will be used
*/
public boolean getUseStartTLS() {
return useStartTLS;
}
/**
* Sets whether startTLS will be used for connections.
*
* @param b whether startTLS will be used
*/
public void setUseStartTLS(final boolean b) {
useStartTLS = b;
}
/**
* Returns the connection initializers.
*
* @return connection initializers
*/
public ConnectionInitializer[] getConnectionInitializers() {
return connectionInitializers;
}
/**
* Sets the connection initializers.
*
* @param initializers connection initializers
*/
public void setConnectionInitializers(final ConnectionInitializer... initializers) {
checkArrayContainsNull(initializers);
connectionInitializers = initializers;
}
/**
* Returns the connection strategy.
*
* @return strategy for making connections
*/
public ConnectionStrategy getConnectionStrategy() {
return connectionStrategy;
}
/**
* Sets the connection strategy.
*
* @param strategy for making new connections
*/
public void setConnectionStrategy(final ConnectionStrategy strategy) {
connectionStrategy = strategy;
}
/**
* Returns the connection validator.
*
* @return connection validator
*/
public ConnectionValidator getConnectionValidator() {
return connectionValidator;
}
/**
* Sets the connection validator.
*
* @param validator for validating connections
*/
public void setConnectionValidator(final ConnectionValidator validator) {
connectionValidator = validator;
}
/**
* Returns transport options.
*
* @return transport options
*/
public Map<String, ?> getTransportOptions() {
return transportOptions;
}
/**
* Sets transport options.
*
* @param options to set
*/
public void setTransportOptions(final Map<String, ?> options) {
transportOptions.putAll(options);
}
/**
* Returns a transport option.
*
* @param id transport option id
* @return transport option
*/
public Object getTransportOption(final String id) {
return transportOptions.get(id);
}
/**
* Sets a transport option.
*
* @param id of the transport option
* @param value of the transport option
*/
public void setTransportOption(final String id, final Object value) {
transportOptions.put(id, value);
}
@Override
public String toString() {
return "[" +
getClass().getName() + "@" + hashCode() + "::" +
"ldapUrl=" + ldapUrl + ", " +
"connectTimeout=" + connectTimeout + ", " +
"startTLSTimeout=" + startTLSTimeout + ", " +
"responseTimeout=" + responseTimeout + ", " +
"reconnectTimeout=" + reconnectTimeout + ", " +
"autoReconnect=" + autoReconnect + ", " +
"autoReconnectCondition=" + autoReconnectCondition + ", " +
"autoReplay=" + autoReplay + ", " +
"sslConfig=" + sslConfig + ", " +
"useStartTLS=" + useStartTLS + ", " +
"connectionInitializers=" + Arrays.toString(connectionInitializers) + ", " +
"connectionStrategy=" + connectionStrategy + ", " +
"connectionValidator=" + connectionValidator + ", " +
"transportOptions=" + transportOptions + "]";
}
public static class Builder {
private final ConnectionConfig object = new ConnectionConfig();
protected Builder() {
}
public Builder url(final String url) {
object.setLdapUrl(url);
return this;
}
public Builder connectTimeout(final Duration timeout) {
object.setConnectTimeout(timeout);
return this;
}
public Builder responseTimeout(final Duration timeout) {
object.setResponseTimeout(timeout);
return this;
}
public Builder startTLSTimeout(final Duration timeout) {
object.setStartTLSTimeout(timeout);
return this;
}
public Builder reconnectTimeout(final Duration timeout) {
object.setReconnectTimeout(timeout);
return this;
}
public Builder autoReconnect(final boolean b) {
object.setAutoReconnect(b);
return this;
}
public Builder autoReconnectCondition(final Predicate<RetryMetadata> predicate) {
object.setAutoReconnectCondition(predicate);
return this;
}
public Builder autoReplay(final boolean b) {
object.setAutoReplay(b);
return this;
}
public Builder sslConfig(final SslConfig config) {
object.setSslConfig(config);
return this;
}
public Builder useStartTLS(final boolean b) {
object.setUseStartTLS(b);
return this;
}
public Builder connectionInitializers(final ConnectionInitializer... initializers) {
object.setConnectionInitializers(initializers);
return this;
}
public Builder connectionStrategy(final ConnectionStrategy strategy) {
object.setConnectionStrategy(strategy);
return this;
}
public Builder connectionValidator(final ConnectionValidator validator) {
object.setConnectionValidator(validator);
return this;
}
public Builder transportOption(final String id, final Object value) {
object.setTransportOption(id, value);
return this;
}
public ConnectionConfig build() {
return object;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,33 @@
package org.xbib.net.ldap;
/**
* Interface for connection factories.
*
*/
public interface ConnectionFactory {
/**
* Creates a new connection.
*
* @return connection
* @throws LdapException if a connection cannot be returned
*/
Connection getConnection()
throws LdapException;
/**
* Returns the connection configuration used to create connections.
*
* @return connection config
*/
ConnectionConfig getConnectionConfig();
/**
* Free any resources associated with this factory.
*/
void close();
}

View file

@ -0,0 +1,25 @@
package org.xbib.net.ldap;
/**
* Interface for objects that manage an instance of connection factory.
*
*/
public interface ConnectionFactoryManager {
/**
* Returns the connection factory.
*
* @return connection factory
*/
ConnectionFactory getConnectionFactory();
/**
* Sets the connection factory.
*
* @param cf connection factory
*/
void setConnectionFactory(ConnectionFactory cf);
}

View file

@ -0,0 +1,25 @@
package org.xbib.net.ldap;
/**
* Interface to describe the state of the connection factory. Used by {@link ConnectionStrategy} to produce LDAP URLs.
*
*/
public interface ConnectionFactoryMetadata {
/**
* Returns the LDAP URL the connection factory is using. May be space delimited for multiple URLs.
*
* @return ldap url
*/
String getLdapUrl();
/**
* Returns the number of times the connection factory has created a connection.
*
* @return connection count
*/
int getConnectionCount();
}

View file

@ -0,0 +1,20 @@
package org.xbib.net.ldap;
/**
* Provides an interface for initializing connections after they are opened.
*
*/
public interface ConnectionInitializer {
/**
* Initialize the supplied connection.
*
* @param conn connection to initialize
* @return result associated with the initialization or an empty result
* @throws LdapException if initialization fails
*/
Result initialize(Connection conn)
throws LdapException;
}

View file

@ -0,0 +1,81 @@
package org.xbib.net.ldap;
import java.util.function.Predicate;
/**
* Interface to describe various connection strategies. Each strategy returns an ordered list of LDAP URLs to attempt
* when opening a connection.
*
*/
public interface ConnectionStrategy extends Iterable<LdapURL> {
/**
* Populates a {@link LdapURLSet} from the URL string provided at configuration time.
*
* @param urls Space-delimited string of URLs describing the LDAP hosts to connect to. The URLs in the string
* are commonly {@code ldap://} or {@code ldaps://} URLs that directly describe the hosts to connect to,
* but may also describe a resource from which to obtain LDAP connection URLs as is the case for
* {@link DnsSrvConnectionStrategy} that use URLs with the scheme {@code dns:}.
* @param urlSet LDAP URL set to populate.
*/
void populate(String urls, LdapURLSet urlSet);
/**
* Prepare this strategy for use.
*
* @param urls LDAP URLs for this strategy
* @param activateCondition predicate to determine whether a connection is active
*/
void initialize(String urls, Predicate<LdapURL> activateCondition);
/**
* Whether this strategy is ready for use.
*
* @return whether this strategy is ready for use
*/
boolean isInitialized();
/**
* Returns the condition used to activate connections.
*
* @return activate condition
*/
Predicate<LdapURL> getActivateCondition();
/**
* Returns the condition used to determine whether to attempt to activate a connection.
*
* @return retry condition
*/
Predicate<LdapURL> getRetryCondition();
/**
* Indicates the supplied URL was successfully connected to.
*
* @param url which was successfully connected to
*/
void success(LdapURL url);
/**
* Indicates the supplied URL could not be connected to.
*
* @param url which was could not be connected to
*/
void failure(LdapURL url);
/**
* Create a deep copy of this strategy.
*
* @return new instance of this connection strategy
*/
ConnectionStrategy newInstance();
}

View file

@ -0,0 +1,50 @@
package org.xbib.net.ldap;
import java.time.Duration;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Provides an interface for defining connection validation.
*
*/
public interface ConnectionValidator extends Function<Connection, Boolean> {
/**
* Provides an asynchronous implementation of {@link #apply(Object)}. The supplied consumer will be invoked with the
* validation result. {@link #getValidateTimeout()} must be enforced by the caller.
*
* @param conn to validate
* @param function to consume the validation result
*/
void applyAsync(Connection conn, Consumer<Boolean> function);
/**
* Provides an asynchronous implementation of {@link #apply(Object)}. The returned supplier will block until a
* validation result is received respecting {@link #getValidateTimeout()}.
*
* @param conn to validate
* @return supplier to retrieve the validation result
*/
Supplier<Boolean> applyAsync(Connection conn);
/**
* Returns the interval at which the validation task will be executed.
*
* @return validation period
*/
Duration getValidatePeriod();
/**
* Returns the duration at which a validate operation should be abandoned.
*
* @return validation timeout
*/
Duration getValidateTimeout();
}

View file

@ -0,0 +1,89 @@
package org.xbib.net.ldap;
/**
* Provides convenience methods for converting the various types of passwords into a byte array.
*
*/
public class Credential {
/**
* Credential stored as a byte array.
*/
private final byte[] bytes;
/**
* Creates a new credential.
*
* @param password converted from UTF-8 to a byte array
*/
public Credential(final String password) {
if (password == null) {
throw new NullPointerException("Password cannot be null");
}
bytes = LdapUtils.utf8Encode(password, false);
}
/**
* Creates a new credential.
*
* @param password converted from UTF-8 to a byte array
*/
public Credential(final char[] password) {
if (password == null) {
throw new NullPointerException("Password cannot be null");
}
bytes = LdapUtils.utf8Encode(new String(password), false);
}
/**
* Creates a new credential.
*
* @param password to store
*/
public Credential(final byte[] password) {
if (password == null) {
throw new NullPointerException("Password cannot be null");
}
bytes = password;
}
/**
* Returns this credential as a byte array.
*
* @return credential bytes
*/
public byte[] getBytes() {
return bytes;
}
/**
* Returns this credential as a string.
*
* @return credential string
*/
public String getString() {
return LdapUtils.utf8Encode(bytes, false);
}
/**
* Returns this credential as a character array.
*
* @return credential characters
*/
public char[] getChars() {
return getString().toCharArray();
}
@Override
public String toString() {
return "[" + getClass().getName() + "@" + hashCode() + "::" + "bytes=" + LdapUtils.utf8Encode(bytes) + "]";
}
}

View file

@ -0,0 +1,181 @@
package org.xbib.net.ldap;
import org.xbib.net.ldap.transport.Transport;
import org.xbib.net.ldap.transport.TransportFactory;
/**
* Creates connections for performing ldap operations.
*
*/
public class DefaultConnectionFactory implements ConnectionFactory {
/**
* Transport used by this factory.
*/
private final Transport transport;
/**
* Connection configuration used by this factory.
*/
private ConnectionConfig config;
/**
* Default constructor.
*/
public DefaultConnectionFactory() {
this(TransportFactory.getTransport(DefaultConnectionFactory.class));
}
/**
* Creates a new default connection factory. Be sure to invoke {@link #close()} if the supplied transport has
* resources to cleanup.
*
* @param t transport
*/
public DefaultConnectionFactory(final Transport t) {
transport = t;
}
/**
* Creates a new default connection factory.
*
* @param ldapUrl to connect to
*/
public DefaultConnectionFactory(final String ldapUrl) {
this(new ConnectionConfig(ldapUrl));
}
/**
* Creates a new default connection factory. Be sure to invoke {@link #close()} if the supplied transport has
* resources to cleanup.
*
* @param ldapUrl to connect to
* @param t transport
*/
public DefaultConnectionFactory(final String ldapUrl, final Transport t) {
this(new ConnectionConfig(ldapUrl), t);
}
/**
* Creates a new default connection factory.
*
* @param cc connection configuration
*/
public DefaultConnectionFactory(final ConnectionConfig cc) {
this(cc, TransportFactory.getTransport(DefaultConnectionFactory.class));
}
/**
* Creates a new default connection factory. Be sure to invoke {@link #close()} if the supplied transport has
* resources to cleanup.
*
* @param cc connection configuration
* @param t transport
*/
public DefaultConnectionFactory(final ConnectionConfig cc, final Transport t) {
transport = t;
setConnectionConfig(cc);
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Creates a builder for this class.
*
* @param t transport
* @return new builder
*/
public static Builder builder(final Transport t) {
return new Builder(t);
}
@Override
public ConnectionConfig getConnectionConfig() {
return config;
}
/**
* Sets the connection config.
*
* @param cc connection config
*/
public void setConnectionConfig(final ConnectionConfig cc) {
config = cc;
}
/**
* Returns the ldap transport.
*
* @return ldap transport
*/
public Transport getTransport() {
return transport;
}
/**
* Creates a new connection. Connections returned from this method must be opened before they can perform ldap
* operations.
*
* @return connection
*/
@Override
public Connection getConnection() {
return transport.create(config);
}
@Override
public void close() {
transport.close();
}
@Override
public String toString() {
return "[" +
getClass().getName() + "@" + hashCode() + "::" +
"transport=" + transport + ", " +
"config=" + config + "]";
}
// CheckStyle:OFF
public static class Builder {
private final DefaultConnectionFactory object;
protected Builder() {
object = new DefaultConnectionFactory();
}
protected Builder(final Transport t) {
object = new DefaultConnectionFactory(t);
}
public Builder config(final ConnectionConfig cc) {
object.setConnectionConfig(cc);
return this;
}
public DefaultConnectionFactory build() {
return object;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,149 @@
package org.xbib.net.ldap;
/**
* Executes an ldap delete operation.
*
*/
public class DeleteOperation extends AbstractOperation<DeleteRequest, DeleteResponse> {
/**
* Default constructor.
*/
public DeleteOperation() {
}
/**
* Creates a new delete operation.
*
* @param factory connection factory
*/
public DeleteOperation(final ConnectionFactory factory) {
super(factory);
}
/**
* Sends a delete request. See {@link OperationHandle#send()}.
*
* @param factory connection factory
* @param request delete request
* @return operation handle
* @throws LdapException if the connection cannot be opened
*/
public static OperationHandle<DeleteRequest, DeleteResponse> send(
final ConnectionFactory factory,
final DeleteRequest request)
throws LdapException {
final Connection conn = factory.getConnection();
try {
conn.open();
} catch (Exception e) {
conn.close();
throw e;
}
return conn.operation(request).onComplete(conn::close).send();
}
/**
* Executes a delete request. See {@link OperationHandle#execute()}.
*
* @param factory connection factory
* @param request delete request
* @return delete result
* @throws LdapException if the connection cannot be opened
*/
public static DeleteResponse execute(final ConnectionFactory factory, final DeleteRequest request)
throws LdapException {
try (Connection conn = factory.getConnection()) {
conn.open();
return conn.operation(request).execute();
}
}
/**
* Returns a new delete operation with the same properties as the supplied operation.
*
* @param operation to copy
* @return copy of the supplied delete operation
*/
public static DeleteOperation copy(final DeleteOperation operation) {
final DeleteOperation op = new DeleteOperation();
op.setRequestHandlers(operation.getRequestHandlers());
op.setResultHandlers(operation.getResultHandlers());
op.setControlHandlers(operation.getControlHandlers());
op.setReferralHandlers(operation.getReferralHandlers());
op.setIntermediateResponseHandlers(operation.getIntermediateResponseHandlers());
op.setExceptionHandler(operation.getExceptionHandler());
op.setThrowCondition(operation.getThrowCondition());
op.setUnsolicitedNotificationHandlers(operation.getUnsolicitedNotificationHandlers());
op.setConnectionFactory(operation.getConnectionFactory());
return op;
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Sends a delete request. See {@link OperationHandle#send()}.
*
* @param request delete request
* @return operation handle
* @throws LdapException if the connection cannot be opened
*/
@Override
public OperationHandle<DeleteRequest, DeleteResponse> send(final DeleteRequest request)
throws LdapException {
final Connection conn = getConnectionFactory().getConnection();
try {
conn.open();
} catch (Exception e) {
conn.close();
throw e;
}
return configureHandle(conn.operation(configureRequest(request))).onComplete(conn::close).send();
}
/**
* Executes a delete request. See {@link OperationHandle#execute()}.
*
* @param request delete request
* @return delete result
* @throws LdapException if the connection cannot be opened
*/
@Override
public DeleteResponse execute(final DeleteRequest request)
throws LdapException {
try (Connection conn = getConnectionFactory().getConnection()) {
conn.open();
return configureHandle(conn.operation(configureRequest(request))).execute();
}
}
/**
* Delete operation builder.
*/
public static class Builder extends AbstractOperation.AbstractBuilder<DeleteOperation.Builder, DeleteOperation> {
/**
* Creates a new builder.
*/
protected Builder() {
super(new DeleteOperation());
}
@Override
protected Builder self() {
return this;
}
}
}

View file

@ -0,0 +1,108 @@
package org.xbib.net.ldap;
import org.xbib.asn1.ApplicationDERTag;
import org.xbib.asn1.DEREncoder;
import org.xbib.asn1.IntegerType;
import org.xbib.asn1.OctetStringType;
/**
* LDAP delete request defined as:
*
* <pre>
* DelRequest ::= [APPLICATION 10] LDAPDN
* </pre>
*
*/
public class DeleteRequest extends AbstractRequestMessage {
/**
* BER protocol number.
*/
public static final int PROTOCOL_OP = 10;
/**
* LDAP DN to delete.
*/
private String ldapDn;
/**
* Default constructor.
*/
private DeleteRequest() {
}
/**
* Creates a new delete request.
*
* @param dn DN to delete
*/
public DeleteRequest(final String dn) {
ldapDn = dn;
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns the DN.
*
* @return DN
*/
public String getDn() {
return ldapDn;
}
@Override
protected DEREncoder[] getRequestEncoders(final int id) {
return new DEREncoder[]{
new IntegerType(id),
new OctetStringType(new ApplicationDERTag(PROTOCOL_OP, false), ldapDn),
};
}
@Override
public String toString() {
return super.toString() + ", " + "dn=" + ldapDn;
}
/**
* Delete request builder.
*/
public static class Builder extends AbstractRequestMessage.AbstractBuilder<DeleteRequest.Builder, DeleteRequest> {
/**
* Default constructor.
*/
protected Builder() {
super(new DeleteRequest());
}
@Override
protected Builder self() {
return this;
}
/**
* Sets the ldap DN.
*
* @param dn ldap DN
* @return this builder
*/
public Builder dn(final String dn) {
object.ldapDn = dn;
return self();
}
}
}

View file

@ -0,0 +1,117 @@
package org.xbib.net.ldap;
import org.xbib.asn1.DERBuffer;
import org.xbib.asn1.DERParser;
import org.xbib.asn1.DERPath;
/**
* LDAP delete response defined as:
*
* <pre>
* DelResponse ::= [APPLICATION 11] LDAPResult
* </pre>
*
*/
public class DeleteResponse extends AbstractResult {
/**
* BER protocol number.
*/
public static final int PROTOCOL_OP = 11;
/**
* hash code seed.
*/
private static final int HASH_CODE_SEED = 10253;
/**
* DER path to result code.
*/
private static final DERPath RESULT_CODE_PATH = new DERPath("/SEQ/APP(11)/ENUM[0]");
/**
* DER path to matched DN.
*/
private static final DERPath MATCHED_DN_PATH = new DERPath("/SEQ/APP(11)/OCTSTR[1]");
/**
* DER path to diagnostic message.
*/
private static final DERPath DIAGNOSTIC_MESSAGE_PATH = new DERPath("/SEQ/APP(11)/OCTSTR[2]");
/**
* DER path to referral.
*/
private static final DERPath REFERRAL_PATH = new DERPath("/SEQ/APP(11)/CTX(3)/OCTSTR[0]");
/**
* Default constructor.
*/
private DeleteResponse() {
}
/**
* Creates a new delete response.
*
* @param buffer to decode
*/
public DeleteResponse(final DERBuffer buffer) {
final DERParser parser = new DERParser();
parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this));
parser.registerHandler(RESULT_CODE_PATH, new ResultCodeHandler(this));
parser.registerHandler(MATCHED_DN_PATH, new MatchedDNHandler(this));
parser.registerHandler(DIAGNOSTIC_MESSAGE_PATH, new DiagnosticMessageHandler(this));
parser.registerHandler(REFERRAL_PATH, new ReferralHandler(this));
parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this));
parser.parse(buffer);
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
return o instanceof DeleteResponse && super.equals(o);
}
@Override
public int hashCode() {
return
LdapUtils.computeHashCode(
HASH_CODE_SEED,
getMessageID(),
getControls(),
getResultCode(),
getMatchedDN(),
getDiagnosticMessage(),
getReferralURLs());
}
// CheckStyle:OFF
public static class Builder extends AbstractResult.AbstractBuilder<Builder, DeleteResponse> {
protected Builder() {
super(new DeleteResponse());
}
@Override
protected Builder self() {
return this;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,29 @@
package org.xbib.net.ldap;
/**
* Enum to define how aliases are dereferenced.
*
*/
public enum DerefAliases {
/**
* never dereference aliases.
*/
NEVER,
/**
* dereference when searching the entries beneath the starting point but not when searching for the starting entry.
*/
SEARCHING,
/**
* dereference when searching for the starting entry but not when searching the entries beneath the starting point.
*/
FINDING,
/**
* dereference when searching for the starting entry and when searching the entries beneath the starting point.
*/
ALWAYS
}

View file

@ -0,0 +1,175 @@
package org.xbib.net.ldap;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Connection strategy that tries all IP addresses resolved from DNS. The order of IP addressees returned can be
* controlled via the java.net.preferIPv4Stack or java.net.preferIPv6Addresses system property flags. This strategy
* operates in an active/passive fashion.
*
*/
public class DnsResolverConnectionStrategy extends AbstractConnectionStrategy {
/**
* Default time to live for DNS results.
*/
protected static final Duration DEFAULT_TTL = Duration.ofHours(6);
/**
* Custom iterator function.
*/
private final Function<List<LdapURL>, Iterator<LdapURL>> iterFunction;
/**
* Time to live for DNS records.
*/
private final Duration dnsTtl;
/**
* Name resolver function.
*/
private Function<String, InetAddress[]> resolverFunction = name -> {
try {
return InetAddress.getAllByName(name);
} catch (UnknownHostException e) {
throw new IllegalStateException("Could not resolve IP address for " + name, e);
}
};
/**
* LDAP URL string used to initialize this strategy.
*/
private String ldapUrls;
/**
* DNS expiration time.
*/
private Instant expirationTime;
/**
* Default constructor.
*/
public DnsResolverConnectionStrategy() {
this(DEFAULT_TTL);
}
/**
* Creates a new DNS resolver connection strategy.
*
* @param ttl time to live for DNS records
*/
public DnsResolverConnectionStrategy(final Duration ttl) {
this(null, ttl);
}
/**
* Creates a new DNS connection strategy.
*
* @param function that produces a custom iterator
*/
public DnsResolverConnectionStrategy(final Function<List<LdapURL>, Iterator<LdapURL>> function) {
this(function, DEFAULT_TTL);
}
/**
* Creates a new DNS resolver connection strategy.
*
* @param function that produces a custom iterator
* @param ttl time to live for DNS records
*/
public DnsResolverConnectionStrategy(final Function<List<LdapURL>, Iterator<LdapURL>> function, final Duration ttl) {
iterFunction = function;
dnsTtl = ttl;
}
/**
* Returns the name resolution function.
*
* @return name resolution function
*/
public Function<String, InetAddress[]> getResolverFunction() {
return resolverFunction;
}
/**
* Sets the function used to resolve names.
*
* @param func to set
*/
public void setResolverFunction(final Function<String, InetAddress[]> func) {
resolverFunction = func;
}
@Override
public Iterator<LdapURL> iterator() {
if (!isInitialized()) {
throw new IllegalStateException("Strategy is not initialized");
}
if (Instant.now().isAfter(expirationTime)) {
populate(ldapUrls, ldapURLSet);
}
if (iterFunction != null) {
return iterFunction.apply(ldapURLSet.getUrls());
}
return new DefaultLdapURLIterator(ldapURLSet.getUrls());
}
@Override
public void populate(final String urls, final LdapURLSet urlSet) {
if (urls == null || urls.isEmpty()) {
throw new IllegalArgumentException("urls cannot be empty or null");
}
ldapUrls = urls;
if (urls.contains(" ")) {
urlSet.populate(Stream.of(urls.split(" "))
.flatMap(s -> {
final List<LdapURL> l = new ArrayList<>(2);
final LdapURL parsedUrl = new LdapURL(s);
for (InetAddress address : resolverFunction.apply(parsedUrl.getHostname())) {
final LdapURL url = LdapURL.copy(parsedUrl);
url.setRetryMetadata(new LdapURLRetryMetadata(this));
url.setInetAddress(address);
l.add(url);
}
return l.stream();
}).collect(Collectors.toList()));
} else {
final LdapURL parsedUrl = new LdapURL(urls);
urlSet.populate(Stream.of(resolverFunction.apply(parsedUrl.getHostname()))
.map(ip -> {
final LdapURL url = LdapURL.copy(parsedUrl);
url.setRetryMetadata(new LdapURLRetryMetadata(this));
url.setInetAddress(ip);
return url;
}).collect(Collectors.toList()));
}
expirationTime = Instant.now().plus(dnsTtl);
}
@Override
public DnsResolverConnectionStrategy newInstance() {
final DnsResolverConnectionStrategy strategy = new DnsResolverConnectionStrategy(iterFunction, dnsTtl);
strategy.setResolverFunction(resolverFunction);
strategy.setRetryCondition(getRetryCondition());
return strategy;
}
}

View file

@ -0,0 +1,253 @@
package org.xbib.net.ldap;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.xbib.net.ldap.dn.Dn;
import org.xbib.net.ldap.dns.DNSContextFactory;
import org.xbib.net.ldap.dns.DNSDomainFunction;
import org.xbib.net.ldap.dns.DefaultDNSContextFactory;
import org.xbib.net.ldap.dns.SRVDNSResolver;
import org.xbib.net.ldap.dns.SRVRecord;
/**
* DNS SRV connection strategy. Queries a DNS server for SRV records and uses those records to construct a list of URLs.
* A time to live can be set to control how often the DNS server is consulted. See http://www.ietf.org/rfc/rfc2782.txt.
*
*/
public class DnsSrvConnectionStrategy extends AbstractConnectionStrategy {
/**
* Default time to live for DNS results.
*/
protected static final Duration DEFAULT_TTL = Duration.ofHours(6);
/**
* DNS context factory to override initialization parameters.
*/
private final DNSContextFactory dnsContextFactory;
/**
* Time to live for SRV records.
*/
private final Duration srvTtl;
/**
* Connect to LDAP using LDAPS.
*/
private final boolean useSSL;
/**
* LDAP URL string used to initialize this strategy.
*/
private String ldapUrls;
/**
* Resolver(s) for SRV DNS records.
*/
private Map<SRVDNSResolver, String> dnsResolvers;
/**
* SRV records expiration time.
*/
private Instant expirationTime;
/**
* Default constructor.
*/
public DnsSrvConnectionStrategy() {
this(DEFAULT_TTL);
}
/**
* Creates a new DNS SRV connection strategy.
*
* @param ttl time to live for SRV records
*/
public DnsSrvConnectionStrategy(final Duration ttl) {
this(null, ttl);
}
/**
* Creates a new DNS SRV connection strategy.
*
* @param factory DNS context factory
*/
public DnsSrvConnectionStrategy(final DNSContextFactory factory) {
this(factory, DEFAULT_TTL);
}
/**
* Creates a new DNS SRV connection strategy.
*
* @param factory DNS context factory
* @param ttl time to live for SRV records
*/
public DnsSrvConnectionStrategy(final DNSContextFactory factory, final Duration ttl) {
this(factory, ttl, false);
}
/**
* Creates a new DNS SRV connection strategy.
*
* @param factory DNS context factory
* @param ttl time to live for SRV records
* @param ssl whether SRV records should produce LDAPS URLs
*/
public DnsSrvConnectionStrategy(final DNSContextFactory factory, final Duration ttl, final boolean ssl) {
dnsContextFactory = factory;
srvTtl = ttl;
useSSL = ssl;
}
@Override
public void populate(final String urls, final LdapURLSet urlSet) {
ldapUrls = urls;
// SRV records are ordered by priority then weight.
// Thus LdapURLSet will be organized by decreasing precedence.
final List<LdapURL> list = readSrvRecords(ldapUrls)
.stream()
.map(srv -> {
final LdapURL url = srv.getLdapURL();
url.setRetryMetadata(new LdapURLRetryMetadata(this));
return url;
})
.collect(Collectors.toList());
urlSet.populate(list);
}
/**
* Parses the supplied DNS URL string and reads SRV records from DNS.
*
* @param urls to parse
* @return Set of DNS SRV records ordered first by priority and then by weight.
*/
protected Set<SRVRecord> readSrvRecords(final String urls) {
if (urls == null) {
dnsResolvers = Collections.singletonMap(
new SRVDNSResolver(
Objects.requireNonNullElseGet(dnsContextFactory, DefaultDNSContextFactory::new), useSSL), null);
} else if (urls.contains(" ")) {
dnsResolvers = new HashMap<>();
for (String url : urls.split(" ")) {
final String[] dnsUrl = parseUrl(url);
dnsResolvers.put(
new SRVDNSResolver(
Objects.requireNonNullElseGet(dnsContextFactory, () -> new DefaultDNSContextFactory(dnsUrl[0])), useSSL),
dnsUrl[1]);
}
} else {
final String[] dnsUrl = parseUrl(urls);
dnsResolvers = Collections.singletonMap(
new SRVDNSResolver(
Objects.requireNonNullElseGet(
dnsContextFactory, () -> new DefaultDNSContextFactory(dnsUrl[0])), useSSL), dnsUrl[1]);
}
final Set<SRVRecord> srvRecords = retrieveDNSRecords();
if (srvRecords.isEmpty()) {
expirationTime = Instant.now();
} else {
expirationTime = Instant.now().plus(srvTtl);
}
return srvRecords;
}
/**
* Parses the supplied URL. If the URL has an ldap scheme, it is inspected for a baseDN which will be used as the
* domain. Otherwise, the URL is assumed to have a dns scheme.
*
* @param url to parse
* @return array containing the DNS URL and the record name in that order
*/
protected String[] parseUrl(final String url) {
final LdapURL ldapURL;
try {
ldapURL = new LdapURL(url);
} catch (Exception e) {
return parseDnsUrl(url);
}
if (ldapURL.getBaseDn() == null || ldapURL.getBaseDn().isEmpty()) {
throw new IllegalArgumentException("LDAP URL " + url + " must contain a base DN");
}
final String domain = new DNSDomainFunction().apply(new Dn(ldapURL.getBaseDn()));
if (domain.isEmpty()) {
throw new IllegalArgumentException("Base DN " + ldapURL.getBaseDn() + " could not be converted to a domain");
}
return new String[]{null, "_ldap._tcp.".concat(domain)};
}
/**
* Parses a DNS URL of the form dns://hostname/domain?record. Where record is the DNS record to retrieve.
*
* @param url to parse
* @return array containing the DNS URL and the record name in that order
*/
protected String[] parseDnsUrl(final String url) {
if (!url.contains("?")) {
return new String[]{url, null};
}
return url.split("\\?");
}
/**
* Returns a list of URLs retrieved from DNS SRV records.
*
* @return list of URLs to attempt connections to
*/
@Override
public synchronized Iterator<LdapURL> iterator() {
if (!isInitialized()) {
throw new IllegalStateException("Strategy is not initialized");
}
if (Instant.now().isAfter(expirationTime)) {
populate(ldapUrls, ldapURLSet);
}
return new DefaultLdapURLIterator(ldapURLSet.getUrls());
}
/**
* Invoke {@link org.xbib.net.ldap.dns.DNSResolver#resolve(String)} for each resolver until results are found.
*
* @return set of LDAP URLs
*/
protected Set<SRVRecord> retrieveDNSRecords() {
for (Map.Entry<SRVDNSResolver, String> entry : dnsResolvers.entrySet()) {
try {
final Set<SRVRecord> records = entry.getKey().resolve(entry.getValue());
if (records != null && !records.isEmpty()) {
return records;
}
} catch (Exception e) {
//
}
}
return Collections.emptySet();
}
@Override
public DnsSrvConnectionStrategy newInstance() {
final DnsSrvConnectionStrategy strategy = new DnsSrvConnectionStrategy(dnsContextFactory, srvTtl, useSSL);
strategy.setRetryCondition(getRetryCondition());
return strategy;
}
}

View file

@ -0,0 +1,277 @@
package org.xbib.net.ldap;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.xbib.net.ldap.filter.FilterUtils;
/**
* Class for producing an LDAP search filter from a filter template. Templates can use either index based parameters or
* name based parameters for substitutions. Parameters are encoded according to RFC 4515.
*
*/
public class FilterTemplate {
/**
* hash code seed.
*/
private static final int HASH_CODE_SEED = 311;
/**
* filter parameters.
*/
private final Map<String, Object> parameters = new HashMap<>();
/**
* filter.
*/
private String searchFilter;
/**
* Default constructor.
*/
public FilterTemplate() {
}
/**
* Creates a new search filter with the supplied filter.
*
* @param filter to set
*/
public FilterTemplate(final String filter) {
setFilter(filter);
}
/**
* Creates a new search filter with the supplied filter and parameters.
*
* @param filter to set
* @param params to set
*/
public FilterTemplate(final String filter, final Object[] params) {
setFilter(filter);
setParameters(params);
}
/**
* Hex encodes the supplied byte array for use in a search filter.
*
* @param value to encode
* @return encoded value or null if supplied value is null
*/
public static String encodeValue(final byte[] value) {
if (value == null) {
return null;
}
final char[] c = LdapUtils.hexEncode(value);
final StringBuilder sb = new StringBuilder(c.length + c.length / 2);
for (int i = 0; i < c.length; i += 2) {
sb.append('\\').append(c[i]).append(c[i + 1]);
}
return sb.toString();
}
/**
* Encodes the supplied attribute value for use in a search filter. See {@link FilterUtils#escape(String)}.
*
* @param value to encode
* @return encoded value or null if supplied value is null
*/
public static String encodeValue(final String value) {
if (value == null) {
return null;
}
return FilterUtils.escape(value);
}
/**
* Hex encodes the supplied object if it is of type byte[], otherwise the string format of the object is escaped. See
* {@link FilterUtils#escape(String)}.
*
* @param obj to encode
* @return encoded object
*/
protected static String encode(final Object obj) {
if (obj == null) {
return null;
}
final String str;
if (obj instanceof String) {
str = encodeValue((String) obj);
} else if (obj instanceof byte[]) {
str = encodeValue((byte[]) obj);
} else {
str = encodeValue(obj.toString());
}
return str;
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Gets the filter.
*
* @return filter
*/
public String getFilter() {
return searchFilter;
}
/**
* Sets the filter.
*
* @param filter to set
*/
public void setFilter(final String filter) {
searchFilter = filter;
}
/**
* Gets the filter parameters.
*
* @return unmodifiable map of filter parameters
*/
public Map<String, Object> getParameters() {
return Collections.unmodifiableMap(parameters);
}
/**
* Sets positional filter parameters.
*
* @param values to set
*/
public void setParameters(final Object[] values) {
int i = 0;
for (Object o : values) {
parameters.put(Integer.toString(i++), o);
}
}
/**
* Sets a positional filter parameter.
*
* @param position of the parameter in the filter
* @param value to set
*/
public void setParameter(final int position, final Object value) {
parameters.put(Integer.toString(position), value);
}
/**
* Sets a named filter parameter.
*
* @param name of the parameter in the filter
* @param value to set
*/
public void setParameter(final String name, final Object value) {
parameters.put(name, value);
}
/**
* Returns this filter with its parameters encoded and replaced. See {@link #encode(Object)}.
*
* @return formatted and encoded filter
*/
public String format() {
String s = searchFilter;
if (!parameters.isEmpty()) {
for (Map.Entry<String, Object> e : parameters.entrySet()) {
final String encoded = encode(e.getValue());
if (encoded != null) {
s = s.replace("{" + e.getKey() + "}", encoded);
}
}
}
return s;
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (o instanceof FilterTemplate v) {
return LdapUtils.areEqual(searchFilter, v.searchFilter) &&
LdapUtils.areEqual(parameters, v.parameters);
}
return false;
}
@Override
public int hashCode() {
return LdapUtils.computeHashCode(HASH_CODE_SEED, searchFilter, parameters);
}
@Override
public String toString() {
return "[" +
getClass().getName() + "@" + hashCode() + "::" +
"filter=" + searchFilter + ", " +
"parameters=" + parameters + "]";
}
// CheckStyle:OFF
public static class Builder {
private final FilterTemplate object = new FilterTemplate();
protected Builder() {
}
public Builder filter(final String filter) {
object.setFilter(filter);
return this;
}
public Builder parameter(final String name, final String value) {
object.setParameter(name, value);
return this;
}
public Builder parameter(final String name, final Object value) {
object.setParameter(name, value);
return this;
}
public Builder parameter(final int pos, final String value) {
object.setParameter(pos, value);
return this;
}
public Builder parameter(final int pos, final Object value) {
object.setParameter(pos, value);
return this;
}
public Builder parameters(final Object... values) {
object.setParameters(values);
return this;
}
public FilterTemplate build() {
return object;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,21 @@
package org.xbib.net.ldap;
import java.time.Instant;
/**
* Retry metadata used when a connection is opened.
*
*/
public class InitialRetryMetadata extends AbstractRetryMetadata {
/**
* Creates a new initial retry metadata.
*
* @param time of last successful connection
*/
public InitialRetryMetadata(final Instant time) {
successTime = time;
}
}

View file

@ -0,0 +1,654 @@
package org.xbib.net.ldap;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* LDAP attribute defined as:
*
* <pre>
* Attribute ::= PartialAttribute(WITH COMPONENTS {
* ...,
* vals (SIZE(1..MAX))})
* </pre>
*
*/
public class LdapAttribute {
/**
* hash code seed.
*/
private static final int HASH_CODE_SEED = 10223;
/**
* List of attribute names known to use binary syntax.
*/
private static final String[] DEFAULT_BINARY_ATTRIBUTES = new String[]{
"photo",
"personalSignature",
"audio",
"jpegPhoto",
"javaSerializedData",
"thumbnailPhoto",
"thumbnailLogo",
"userCertificate",
"cACertificate",
"authorityRevocationList",
"certificateRevocationList",
"crossCertificatePair",
"x500UniqueIdentifier",
};
/**
* List of custom binary attribute names.
*/
private static final String[] BINARY_ATTRIBUTES;
static {
// Configure custom binary attribute names
final String[] split = System.getProperty("org.xbib.net.ldap.attribute.binary", "").split(",");
BINARY_ATTRIBUTES = LdapUtils.concatArrays(DEFAULT_BINARY_ATTRIBUTES, split);
}
/**
* Attribute name.
*/
private String attributeName;
/**
* Attribute values.
*/
private Set<ByteBuffer> attributeValues = new LinkedHashSet<>();
/**
* Whether this attribute is binary and string representations should be base64 encoded.
*/
private boolean binary;
/**
* Default constructor.
*/
public LdapAttribute() {
}
/**
* Creates a new attribute.
*
* @param type attribute description
*/
public LdapAttribute(final String type) {
setName(type);
}
/**
* Creates a new attribute.
*
* @param type attribute description
* @param value attribute values
*/
public LdapAttribute(final String type, final byte[]... value) {
setName(type);
addBinaryValues(value);
}
/**
* Creates a new attribute.
*
* @param type attribute description
* @param value attribute values
*/
public LdapAttribute(final String type, final String... value) {
setName(type);
addStringValues(value);
}
/**
* Returns a new attribute whose values are sorted. String values are sorted naturally. Binary values are sorted using
* {@link ByteBuffer#compareTo(ByteBuffer)}.
*
* @param la attribute to sort
* @return sorted attribute
*/
public static LdapAttribute sort(final LdapAttribute la) {
final LdapAttribute sorted = new LdapAttribute(la.getName());
if (la.isBinary()) {
sorted.setBinary(true);
final Set<byte[]> newValues = la.getBinaryValues().stream().sorted(
(o1, o2) -> {
final ByteBuffer bb1 = ByteBuffer.wrap(o1);
final ByteBuffer bb2 = ByteBuffer.wrap(o2);
return bb1.compareTo(bb2);
}).collect(Collectors.toCollection(LinkedHashSet::new));
sorted.addBinaryValues(newValues);
} else {
final Set<String> newValues = la.getStringValues().stream()
.sorted(Comparator.comparing(String::toString)).collect(Collectors.toCollection(LinkedHashSet::new));
sorted.addStringValues(newValues);
}
return sorted;
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns whether this ldap attribute is binary.
*
* @return whether this ldap attribute is binary
*/
public boolean isBinary() {
return binary;
}
/**
* Sets whether this ldap attribute is binary.
*
* @param b whether this ldap attribute is binary
*/
public void setBinary(final boolean b) {
binary = b;
}
/**
* Checks whether attrNames matches the name of this attribute. If a match is found this attribute is set as binary.
*
* @param attrNames custom binary attribute names
*/
public void configureBinary(final String... attrNames) {
if (binary) {
return;
}
if (attrNames != null && attrNames.length > 0) {
for (String s : attrNames) {
if (attributeName.equals(s)) {
binary = true;
break;
}
}
}
}
/**
* Returns the attribute description with options.
*
* @return attribute description
*/
public String getName() {
return attributeName;
}
/**
* Sets the name. This method has the side effect of setting this attribute as binary if the name has an option of
* 'binary' or the name matches one of {@link #BINARY_ATTRIBUTES}.
*
* @param type attribute name
*/
public void setName(final String type) {
attributeName = type;
if (getOptions().contains("binary") || Stream.of(BINARY_ATTRIBUTES).anyMatch(attributeName::equals)) {
setBinary(true);
}
}
/**
* Returns the attribute description with or without options.
*
* @param withOptions whether the attribute description should include options
* @return attribute description
*/
public String getName(final boolean withOptions) {
if (withOptions) {
return attributeName;
} else {
final int optionIndex = attributeName.indexOf(";");
return optionIndex > 0 ? attributeName.substring(0, optionIndex) : attributeName;
}
}
/**
* Returns any options that may exist on the attribute description.
*
* @return attribute description options
*/
public List<String> getOptions() {
if (attributeName.indexOf(";") > 0) {
final String[] split = attributeName.split(";");
if (split.length > 1) {
return IntStream.range(1, split.length).mapToObj(i -> split[i]).collect(Collectors.toUnmodifiableList());
}
}
return Collections.emptyList();
}
/**
* Returns a single byte array value of this attribute.
*
* @return single byte array attribute value or null if this attribute is empty
*/
public byte[] getBinaryValue() {
return attributeValues.isEmpty() ? null : attributeValues.iterator().next().array();
}
/**
* Returns the values of this attribute as byte arrays. The return collection cannot be modified.
*
* @return collection of string attribute values
*/
public Collection<byte[]> getBinaryValues() {
if (attributeValues.isEmpty()) {
return Collections.emptySet();
}
return attributeValues.stream().map(ByteBuffer::array).collect(Collectors.toUnmodifiableList());
}
/**
* Returns a single string value of this attribute.
*
* @return single string attribute value or null if this attribute is empty
*/
public String getStringValue() {
if (attributeValues.isEmpty()) {
return null;
}
final ByteBuffer val = attributeValues.iterator().next();
return binary ? LdapUtils.base64Encode(val.array()) : LdapUtils.utf8Encode(val.array());
}
/**
* Returns the values of this attribute as strings. Binary data is base64 encoded. The return collection cannot be
* modified.
*
* @return collection of string attribute values
*/
public Collection<String> getStringValues() {
if (attributeValues.isEmpty()) {
return Collections.emptySet();
}
return attributeValues.stream().map(v -> {
if (binary) {
return LdapUtils.base64Encode(v.array());
}
return LdapUtils.utf8Encode(v.array(), false);
}).collect(Collectors.toUnmodifiableList());
}
/**
* Returns a single decoded value of this attribute.
*
* @param <T> type of decoded attribute
* @param func to decode attribute value with
* @return single decoded attribute value or null if this attribute is empty
*/
public <T> T getValue(final Function<byte[], T> func) {
return attributeValues.isEmpty() ? null : func.apply(attributeValues.iterator().next().array());
}
/**
* Returns the values of this attribute decoded by the supplied function.
*
* @param <T> type of decoded attributes
* @param func to decode attribute values with
* @return collection of decoded attribute values, null values are discarded
*/
public <T> Collection<T> getValues(final Function<byte[], T> func) {
return attributeValues.stream()
.filter(Objects::nonNull)
.map(ByteBuffer::array)
.map(func).collect(Collectors.toUnmodifiableList());
}
/**
* Adds the supplied byte array as a value for this attribute.
*
* @param value to add, null values are discarded
*/
public void addBinaryValues(final byte[]... value) {
Stream.of(value).filter(Objects::nonNull).map(ByteBuffer::wrap).forEach(attributeValues::add);
}
/**
* Adds all the byte arrays in the supplied collection as values for this attribute.
*
* @param values to add, null values are discarded
*/
public void addBinaryValues(final Collection<byte[]> values) {
values.stream().filter(Objects::nonNull).map(ByteBuffer::wrap).forEach(attributeValues::add);
}
/**
* Adds the supplied string as a value for this attribute.
*
* @param value to add, null values are discarded
*/
public void addStringValues(final String... value) {
Stream.of(value)
.filter(Objects::nonNull)
.map(s -> toByteArray(s, true))
.filter(Objects::nonNull)
.map(ByteBuffer::wrap)
.forEach(attributeValues::add);
}
/**
* Adds all the strings in the supplied collection as values for this attribute.
*
* @param values to add, null values are discarded
*/
public void addStringValues(final Collection<String> values) {
values.stream()
.filter(Objects::nonNull)
.map(s -> toByteArray(s, true))
.filter(Objects::nonNull)
.map(ByteBuffer::wrap)
.forEach(attributeValues::add);
}
/**
* Adds all the buffers in the supplied collection as values for this attribute.
*
* @param values to add, null values are discarded
*/
public void addBufferValues(final ByteBuffer... values) {
Stream.of(values).filter(Objects::nonNull).forEach(attributeValues::add);
}
/**
* Adds all the buffers in the supplied collection as values for this attribute.
*
* @param values to add, null values are discarded
*/
public void addBufferValues(final Collection<ByteBuffer> values) {
values.stream().filter(Objects::nonNull).forEach(attributeValues::add);
}
/**
* Adds the supplied values for this attribute by encoding them with the supplied function.
*
* @param <T> type attribute to encode
* @param func to encode value with
* @param value to encode and add, null values are discarded
*/
@SuppressWarnings("unchecked")
public <T> void addValues(final Function<T, byte[]> func, final T... value) {
Stream.of(value)
.filter(Objects::nonNull)
.map(func)
.filter(Objects::nonNull)
.map(ByteBuffer::wrap)
.forEach(attributeValues::add);
}
/**
* Adds all the values in the supplied collection for this attribute by encoding them with the supplied function.
* See {@link #addValues(Function, Object...)}.
*
* @param <T> type attribute to encode
* @param func to encode value with
* @param values to encode and add, null values are discarded
*/
public <T> void addValues(final Function<T, byte[]> func, final Collection<T> values) {
values.stream()
.filter(Objects::nonNull)
.map(func)
.filter(Objects::nonNull)
.map(ByteBuffer::wrap)
.forEach(attributeValues::add);
}
/**
* Removes the supplied byte array as a value from this attribute.
*
* @param value to remove, null values are discarded
*/
public void removeBinaryValues(final byte[]... value) {
Stream.of(value).filter(Objects::nonNull).map(ByteBuffer::wrap).forEach(attributeValues::remove);
}
/**
* Removes all the byte arrays in the supplied collection as values from this attribute.
*
* @param values to remove, null values are discarded
*/
public void removeBinaryValues(final Collection<byte[]> values) {
values.stream().filter(Objects::nonNull).map(ByteBuffer::wrap).forEach(attributeValues::remove);
}
/**
* Removes the supplied string as a value from this attribute.
*
* @param value to remove, null values are discarded
*/
public void removeStringValues(final String... value) {
Stream.of(value)
.filter(Objects::nonNull)
.map(s -> toByteArray(s, true))
.filter(Objects::nonNull)
.map(ByteBuffer::wrap)
.forEach(attributeValues::remove);
}
/**
* Removes all the strings in the supplied collection as values from this attribute.
*
* @param values to remove, null values are discarded
*/
public void removeStringValues(final Collection<String> values) {
values.stream()
.filter(Objects::nonNull)
.map(s -> toByteArray(s, true))
.filter(Objects::nonNull)
.map(ByteBuffer::wrap)
.forEach(attributeValues::remove);
}
/**
* Removes all the buffers in the supplied collection as values from this attribute.
*
* @param values to remove, null values are discarded
*/
public void removeBufferValues(final ByteBuffer... values) {
Stream.of(values).filter(Objects::nonNull).forEach(attributeValues::remove);
}
/**
* Removes all the buffers in the supplied collection as values from this attribute.
*
* @param values to remove, null values are discarded
*/
public void removeBufferValues(final Collection<ByteBuffer> values) {
values.stream().filter(Objects::nonNull).forEach(attributeValues::remove);
}
/**
* Returns whether the supplied value exists in this attribute.
*
* @param value to find
* @return whether value exists
*/
public boolean hasValue(final byte[] value) {
return attributeValues.stream().anyMatch(bb -> Arrays.equals(bb.array(), value));
}
/**
* Returns whether the supplied value exists in this attribute.
*
* @param value to find
* @return whether value exists
*/
public boolean hasValue(final String value) {
return attributeValues.stream().anyMatch(bb -> Arrays.equals(bb.array(), toByteArray(value, false)));
}
/**
* Returns whether the supplied value exists in this attribute.
*
* @param <T> type attribute to encode
* @param func to encode value with
* @param value to find
* @return whether value exists
*/
public <T> boolean hasValue(final Function<T, byte[]> func, final T value) {
return attributeValues.stream().anyMatch(bb -> Arrays.equals(bb.array(), func.apply(value)));
}
/**
* Returns the number of values in this ldap attribute.
*
* @return number of values in this ldap attribute
*/
public int size() {
return attributeValues.size();
}
/**
* Removes all the values in this ldap attribute.
*/
public void clear() {
attributeValues.clear();
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (o instanceof LdapAttribute) {
final LdapAttribute v = (LdapAttribute) o;
return LdapUtils.areEqual(LdapUtils.toLowerCase(attributeName), LdapUtils.toLowerCase(v.attributeName)) &&
LdapUtils.areEqual(attributeValues, v.attributeValues);
}
return false;
}
@Override
public int hashCode() {
return
LdapUtils.computeHashCode(
HASH_CODE_SEED,
LdapUtils.toLowerCase(attributeName),
attributeValues);
}
@Override
public String toString() {
return getClass().getName() + "@" + hashCode() + "::" +
"name=" + attributeName + ", " +
"values=" + getStringValues() + ", " +
"binary=" + binary;
}
/**
* Converts the supplied string value to a byte array respecting the {@link #binary} flag. If this attribute is
* binary, value is expected to be in base64 format. Otherwise, value is UTF-8 encoded.
*
* @param value to convert
* @param throwOnError whether to throw if a base64 decode error occurs
* @return binary value
* @throws IllegalArgumentException if attribute is binary, value cannot be base64 decoded and throwOnError is true
*/
private byte[] toByteArray(final String value, final boolean throwOnError) {
if (binary) {
try {
return LdapUtils.base64Decode(value);
} catch (IllegalArgumentException e) {
if (throwOnError) {
throw new IllegalArgumentException("Error decoding " + value + " for " + attributeName, e);
}
return null;
}
}
return LdapUtils.utf8Encode(value, false);
}
// CheckStyle:OFF
public static class Builder {
private final LdapAttribute object = new LdapAttribute();
protected Builder() {
}
public Builder name(final String name) {
object.setName(name);
return this;
}
@SuppressWarnings("unchecked")
public <T> Builder values(final Function<T, byte[]> func, final T... value) {
object.addValues(func, value);
return this;
}
public Builder values(final byte[]... values) {
object.addBinaryValues(values);
return this;
}
public Builder binaryValues(final Collection<byte[]> values) {
object.addBinaryValues(values);
return this;
}
public Builder values(final String... values) {
object.addStringValues(values);
return this;
}
public Builder stringValues(final Collection<String> values) {
object.addStringValues(values);
return this;
}
public Builder values(final ByteBuffer... values) {
object.addBufferValues(values);
return this;
}
public Builder bufferValues(final Collection<ByteBuffer> values) {
object.addBufferValues(values);
return this;
}
public Builder binary(final boolean b) {
object.setBinary(b);
return this;
}
public LdapAttribute build() {
return object;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,542 @@
package org.xbib.net.ldap;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.xbib.asn1.AbstractParseHandler;
import org.xbib.asn1.DERBuffer;
import org.xbib.asn1.DERParser;
import org.xbib.asn1.DERPath;
import org.xbib.asn1.OctetStringType;
import org.xbib.net.ldap.dn.Dn;
/**
* LDAP search result entry defined as:
*
* <pre>
* SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
* objectName LDAPDN,
* attributes PartialAttributeList }
*
* PartialAttributeList ::= SEQUENCE OF
* partialAttribute PartialAttribute
*
* PartialAttribute ::= SEQUENCE {
* type AttributeDescription,
* vals SET OF value AttributeValue }
* </pre>
*
*/
public class LdapEntry extends AbstractMessage {
/**
* BER protocol number.
*/
public static final int PROTOCOL_OP = 4;
/**
* hash code seed.
*/
private static final int HASH_CODE_SEED = 10303;
/**
* DER path to LDAP DN.
*/
private static final DERPath LDAP_DN_PATH = new DERPath("/SEQ/APP(4)/OCTSTR[0]");
/**
* DER path to attributes.
*/
private static final DERPath ATTRIBUTES_PATH = new DERPath("/SEQ/APP(4)/SEQ/SEQ");
/**
* LDAP DN of the entry.
*/
private String ldapDn;
/**
* Parsed LDAP DN.
*/
private Dn parsedDn;
/**
* Normalized LDAP DN.
*/
private String normalizedDn;
/**
* LDAP attributes on the entry.
*/
private Map<String, LdapAttribute> attributes = new LinkedHashMap<>();
/**
* Default constructor.
*/
public LdapEntry() {
}
/**
* Creates a new search result entry.
*
* @param buffer to decode
*/
public LdapEntry(final DERBuffer buffer) {
final DERParser parser = new DERParser();
parser.registerHandler(MessageIDHandler.PATH, new MessageIDHandler(this));
parser.registerHandler(LDAP_DN_PATH, new LdapDnHandler(this));
parser.registerHandler(ATTRIBUTES_PATH, new AttributesHandler(this));
parser.registerHandler(ControlsHandler.PATH, new ControlsHandler(this));
parser.parse(buffer);
}
/**
* Returns a new entry whose attributes are sorted naturally by name without options.
*
* @param le entry to sort
* @return sorted entry
*/
public static LdapEntry sort(final LdapEntry le) {
final LdapEntry sorted = new LdapEntry();
sorted.copyValues(le);
sorted.setDn(le.getDn());
sorted.addAttributes(
le.getAttributes().stream()
.map(LdapAttribute::sort)
.sorted(Comparator.comparing(o -> o.getName(false))).collect(Collectors.toCollection(LinkedHashSet::new)));
return sorted;
}
/**
* Returns the list of attribute modifications needed to change the supplied target entry into the supplied source
* entry. See {@link #computeModifications(LdapEntry, LdapEntry, boolean)}.
*
* @param source ldap entry containing new data
* @param target ldap entry containing existing data
* @return attribute modifications needed to change target into source or an empty array
*/
public static AttributeModification[] computeModifications(final LdapEntry source, final LdapEntry target) {
return computeModifications(source, target, true);
}
/**
* Returns the list of attribute modifications needed to change the supplied target entry into the supplied source
* entry. This implementation performs a byte comparison on the attribute values to determine changes.
*
* @param source ldap entry containing new data
* @param target ldap entry containing existing data
* @param useReplace whether to use a single REPLACE modification or individual ADD/DELETE for attribute values
* @return attribute modifications needed to change target into source or an empty array
*/
public static AttributeModification[] computeModifications(
final LdapEntry source, final LdapEntry target, final boolean useReplace) {
final List<AttributeModification> mods = new ArrayList<>();
for (LdapAttribute sourceAttr : source.getAttributes()) {
final LdapAttribute targetAttr = target.getAttribute(sourceAttr.getName());
if (targetAttr == null) {
if (sourceAttr.size() > 0) {
mods.add(new AttributeModification(AttributeModification.Type.ADD, sourceAttr));
} else {
// perform a replace if attribute has no values to avoid potential schema issues
mods.add(new AttributeModification(AttributeModification.Type.REPLACE, sourceAttr));
}
} else if (!targetAttr.equals(sourceAttr)) {
if (useReplace) {
mods.add(new AttributeModification(AttributeModification.Type.REPLACE, sourceAttr));
} else {
final LdapAttribute toAdd = new LdapAttribute(sourceAttr.getName());
sourceAttr.getBinaryValues().stream()
.filter(sv -> !targetAttr.hasValue(sv))
.forEach(toAdd::addBinaryValues);
if (toAdd.size() > 0) {
mods.add(new AttributeModification(AttributeModification.Type.ADD, toAdd));
}
final LdapAttribute toDelete = new LdapAttribute(sourceAttr.getName());
targetAttr.getBinaryValues().stream()
.filter(tv -> !sourceAttr.hasValue(tv))
.forEach(toDelete::addBinaryValues);
if (toDelete.size() > 0) {
mods.add(new AttributeModification(AttributeModification.Type.DELETE, toDelete));
}
}
}
}
for (LdapAttribute targetAttr : target.getAttributes()) {
final LdapAttribute sourceAttr = source.getAttribute(targetAttr.getName());
if (sourceAttr == null) {
mods.add(
new AttributeModification(
AttributeModification.Type.DELETE,
LdapAttribute.builder().name(targetAttr.getName()).build()));
}
}
return mods.toArray(AttributeModification[]::new);
}
/**
* Creates a builder for this class.
*
* @return new builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns the ldap DN.
*
* @return ldap DN
*/
public String getDn() {
return ldapDn;
}
/**
* Sets the ldap DN.
*
* @param dn ldap DN
*/
public void setDn(final String dn) {
ldapDn = dn;
if (ldapDn != null) {
try {
parsedDn = new Dn(ldapDn);
} catch (Exception e) {
parsedDn = null;
}
if (parsedDn != null) {
normalizedDn = parsedDn.format();
}
}
}
/**
* Returns the parsed ldap DN. Parsing is performed using {@link org.xbib.net.ldap.dn.DefaultDnParser}.
*
* @return parsed ldap DN or null if {@link #ldapDn} is null or could not be parsed
*/
public Dn getParsedDn() {
return parsedDn;
}
/**
* Returns the normalized ldap DN. Normalization is performed using {@link org.xbib.net.ldap.dn.DefaultRDnNormalizer}.
*
* @return normalized ldap DN or null if {@link #ldapDn} is null or could not be parsed
*/
public String getNormalizedDn() {
return normalizedDn;
}
/**
* Returns the ldap attributes.
*
* @return ldap attributes
*/
public Collection<LdapAttribute> getAttributes() {
return attributes.values();
}
/**
* Returns a single attribute of this entry. If multiple attributes exist the first attribute returned by the
* underlying iterator is used. If no attributes exist null is returned.
*
* @return single attribute
*/
public LdapAttribute getAttribute() {
if (attributes.isEmpty()) {
return null;
}
return attributes.values().iterator().next();
}
/**
* Returns the attribute with the supplied name.
*
* @param name of the attribute to return
* @return ldap attribute
*/
public LdapAttribute getAttribute(final String name) {
if (name != null) {
return attributes.get(LdapUtils.toLowerCase(name));
}
return null;
}
/**
* Returns the attribute names in this entry.
*
* @return string array of attribute names
*/
public String[] getAttributeNames() {
return attributes.values().stream().map(LdapAttribute::getName).toArray(String[]::new);
}
/**
* Adds attributes to the entry.
*
* @param attrs attributes to add
*/
public void addAttributes(final LdapAttribute... attrs) {
for (LdapAttribute a : attrs) {
attributes.put(LdapUtils.toLowerCase(a.getName()), a);
}
}
/**
* Adds attributes to the entry.
*
* @param attrs attributes to add
*/
public void addAttributes(final Collection<LdapAttribute> attrs) {
attrs.forEach(a -> attributes.put(LdapUtils.toLowerCase(a.getName()), a));
}
/**
* Removes the attribute with the supplied name.
*
* @param name of attribute to remove
*/
public void removeAttribute(final String name) {
attributes.remove(LdapUtils.toLowerCase(name));
}
/**
* Removes an attribute from this ldap attributes.
*
* @param attrs attribute to remove
*/
public void removeAttributes(final LdapAttribute... attrs) {
for (LdapAttribute a : attrs) {
attributes.remove(LdapUtils.toLowerCase(a.getName()));
}
}
/**
* Removes the attribute(s) from this ldap attributes.
*
* @param attrs collection of ldap attributes to remove
*/
public void removeAttributes(final Collection<LdapAttribute> attrs) {
attrs.forEach(a -> attributes.remove(LdapUtils.toLowerCase(a.getName())));
}
/**
* Returns the number of attributes.
*
* @return number of attributes
*/
public int size() {
return attributes.size();
}
/**
* Removes all the attributes.
*/
public void clear() {
attributes.clear();
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (o instanceof LdapEntry && super.equals(o)) {
final LdapEntry v = (LdapEntry) o;
// compare normalizedDn if not null, else compare Dn
return LdapUtils.areEqual(
normalizedDn != null ? normalizedDn : ldapDn,
normalizedDn != null ? v.normalizedDn : v.normalizedDn != null ? v.normalizedDn : v.ldapDn) &&
LdapUtils.areEqual(attributes, v.attributes);
}
return false;
}
@Override
public int hashCode() {
return
LdapUtils.computeHashCode(
HASH_CODE_SEED,
getMessageID(),
getControls(),
normalizedDn != null ? normalizedDn : ldapDn,
attributes);
}
@Override
public String toString() {
return super.toString() + ", " +
"dn=" + ldapDn + ", " +
"attributes=" + (attributes != null ? attributes.values() : null);
}
/**
* Parse handler implementation for the LDAP DN.
*/
protected static class LdapDnHandler extends AbstractParseHandler<LdapEntry> {
/**
* Creates a new ldap dn handler.
*
* @param response to configure
*/
LdapDnHandler(final LdapEntry response) {
super(response);
}
@Override
public void handle(final DERParser parser, final DERBuffer encoded) {
getObject().setDn(OctetStringType.decode(encoded));
}
}
/**
* Parse handler implementation for the attributes.
*/
protected static class AttributesHandler extends AbstractParseHandler<LdapEntry> {
/**
* Creates a new attributes handler.
*
* @param response to configure
*/
AttributesHandler(final LdapEntry response) {
super(response);
}
@Override
public void handle(final DERParser parser, final DERBuffer encoded) {
final AttributeParser p = new AttributeParser();
p.parse(encoded);
if (p.getName().isEmpty()) {
throw new IllegalArgumentException("Could not parse attribute");
}
if (p.getValues().isEmpty()) {
getObject().addAttributes(LdapAttribute.builder().name(p.getName().get()).build());
} else {
getObject().addAttributes(
LdapAttribute.builder().name(p.getName().get()).bufferValues(p.getValues().get()).build());
}
}
}
/**
* Parses a buffer containing an attribute name and its values.
*/
protected static class AttributeParser {
/**
* DER path to name.
*/
private static final DERPath NAME_PATH = new DERPath("/OCTSTR");
/**
* DER path to values.
*/
private static final DERPath VALUES_PATH = new DERPath("/SET/OCTSTR");
/**
* Parser for decoding LDAP attributes.
*/
private final DERParser parser = new DERParser();
/**
* Attribute name.
*/
private String name;
/**
* Attribute values.
*/
private final List<ByteBuffer> values = new ArrayList<>();
/**
* Creates a new attribute parser.
*/
public AttributeParser() {
parser.registerHandler(NAME_PATH, (p, e) -> name = OctetStringType.decode(e));
parser.registerHandler(VALUES_PATH, (p, e) -> values.add(ByteBuffer.wrap(e.getRemainingBytes())));
}
/**
* Examines the supplied buffer and parses an LDAP attribute if one is found.
*
* @param buffer to parse
*/
public void parse(final DERBuffer buffer) {
parser.parse(buffer);
}
/**
* Returns the attribute name.
*
* @return attribute name or empty
*/
public Optional<String> getName() {
return Optional.ofNullable(name);
}
/**
* Returns the attribute values.
*
* @return attribute values or empty
*/
public Optional<List<ByteBuffer>> getValues() {
return values.isEmpty() ? Optional.empty() : Optional.of(values);
}
}
// CheckStyle:OFF
public static class Builder extends AbstractMessage.AbstractBuilder<Builder, LdapEntry> {
protected Builder() {
super(new LdapEntry());
}
@Override
protected Builder self() {
return this;
}
public Builder dn(final String dn) {
object.setDn(dn);
return this;
}
public Builder attributes(final LdapAttribute... attrs) {
object.addAttributes(attrs);
return this;
}
public Builder attributes(final Collection<LdapAttribute> attrs) {
object.addAttributes(attrs);
return this;
}
}
// CheckStyle:ON
}

View file

@ -0,0 +1,116 @@
package org.xbib.net.ldap;
/**
* Base exception for all ldap related exceptions.
*
*/
public class LdapException extends Exception {
/**
* serialVersionUID.
*/
private static final long serialVersionUID = 6812614366508784841L;
/**
* Optional result code associated with this exception.
*/
private final ResultCode resultCode;
/**
* Creates a new ldap exception based on the supplied result.
*
* @param result that produced this exception
*/
public LdapException(final Result result) {
this(result.getResultCode(), formatResult(result));
}
/**
* Creates a new ldap exception.
*
* @param msg describing this exception
*/
public LdapException(final String msg) {
this(null, msg);
}
/**
* Creates a new ldap exception.
*
* @param code result code describing this exception
* @param msg describing this exception
*/
public LdapException(final ResultCode code, final String msg) {
super(msg);
resultCode = code;
}
/**
* Creates a new ldap exception.
*
* @param e underlying exception
*/
public LdapException(final Throwable e) {
this((ResultCode) null, e);
}
/**
* Creates a new ldap exception.
*
* @param code result code describing this exception
* @param e underlying exception
*/
public LdapException(final ResultCode code, final Throwable e) {
super(e);
resultCode = code;
}
/**
* Creates a new ldap exception.
*
* @param msg describing this exception
* @param e underlying exception
*/
public LdapException(final String msg, final Throwable e) {
this(null, msg, e);
}
/**
* Creates a new ldap exception.
*
* @param code result code describing this exception
* @param msg describing this exception
* @param e underlying exception
*/
public LdapException(final ResultCode code, final String msg, final Throwable e) {
super(msg, e);
resultCode = code;
}
/**
* Formats the supplied result for use as an exception message.
*
* @param result to format
* @return formatted result
*/
protected static String formatResult(final Result result) {
return "resultCode=" + result.getResultCode() + ", " + "diagnosticMessage=" + result.getEncodedDiagnosticMessage();
}
/**
* Returns the result code.
*
* @return result code or null
*/
public ResultCode getResultCode() {
return resultCode;
}
}

View file

@ -0,0 +1,516 @@
package org.xbib.net.ldap;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Class for parsing LDAP URLs. See RFC 4516. Expects URLs of the form scheme://hostname:port/baseDn?attrs?scope?filter.
* This implementation does not support URL extensions.
*
*/
public class LdapURL {
/**
* Pattern to match LDAP URL.
*/
protected static final Pattern URL_PATTERN = Pattern.compile(
"([lL][dD][aA][pP][sSiI]?)://(\\[[0-9A-Fa-f:]+\\]|[^:/]+)?" +
"(?::(\\d+))?" +
"(?:/(?:([^?]+))?" +
"(?:\\?([^?]*))?" +
"(?:\\?([^?]*))?" +
"(?:\\?(.*))?)?");
/**
* Default LDAP port, value is {@value}.
*/
protected static final int DEFAULT_LDAP_PORT = 389;
/**
* Default LDAPS port, value is {@value}.
*/
protected static final int DEFAULT_LDAPS_PORT = 636;
/**
* Default base DN, value is {@value}.
*/
protected static final String DEFAULT_BASE_DN = "";
/**
* Default search filter value is '(objectClass=*)'.
*/
protected static final String DEFAULT_FILTER = "(objectClass=*)";
/**
* Default scope, value is {@link SearchScope#OBJECT}.
*/
protected static final SearchScope DEFAULT_SCOPE = SearchScope.OBJECT;
/**
* Default return attributes, value is all user attributes.
*/
protected static final String[] DEFAULT_ATTRIBUTES = ReturnAttributes.ALL_USER.value();
/**
* hash code seed.
*/
private static final int HASH_CODE_SEED = 10333;
/**
* Scheme of the ldap url.
*/
private String scheme;
/**
* Hostname of the ldap url.
*/
private String hostname;
/**
* Port of the ldap url.
*/
private int port;
/**
* Base DN of the ldap url.
*/
private String baseDn;
/**
* Attributes of the ldap url.
*/
private String[] attributes;
/**
* Search scope of the ldap url.
*/
private SearchScope scope;
/**
* Search filter of the ldap url.
*/
private String filter;
/**
* Metadata that describes connection failures on this URL.
*/
private LdapURLRetryMetadata retryMetadata;
/**
* False if the last connection attempt to this URL failed, which should result in updating {@link #retryMetadata},
* otherwise true.
*/
private boolean active = true;
/**
* IP address resolved for this URL.
*/
private InetAddress inetAddress;
/**
* Private constructor.
*/
private LdapURL() {
}
/**
* Creates a new ldap url.
*
* @param hostname LDAP server hostname
* @param port TCP port the LDAP server is listening on
*/
public LdapURL(final String hostname, final int port) {
this("ldap://" + hostname + ":" + port);
}
/**
* Creates a new ldap url.
*
* @param url LDAP url
*/
public LdapURL(final String url) {
parseURL(url);
}
/**
* Creates a new ldap url.
*
* @param scheme url scheme
* @param hostname url hostname
* @param port url port
* @param baseDn base DN
* @param attributes attributes
* @param scope search scope
* @param filter search filter
*/
protected LdapURL(
final String scheme,
final String hostname,
final int port,
final String baseDn,
final String[] attributes,
final SearchScope scope,
final String filter) {
if (scheme == null) {
throw new IllegalArgumentException("Scheme cannot be null");
}
this.scheme = scheme;
this.hostname = hostname;
this.port = port;
this.baseDn = baseDn;
this.attributes = attributes;
this.scope = scope;
this.filter = filter;
}
/**
* Returns a new ldap URL initialized with the supplied URL.
*
* @param ldapURL ldap URL to read properties from
* @return ldap URL
*/
public static LdapURL copy(final LdapURL ldapURL) {
final LdapURL url = new LdapURL();
url.scheme = ldapURL.scheme;
url.hostname = ldapURL.hostname;
url.port = ldapURL.port;
url.baseDn = ldapURL.baseDn;
url.attributes = ldapURL.attributes;
url.scope = ldapURL.scope;
url.filter = ldapURL.filter;
url.retryMetadata = ldapURL.retryMetadata;
url.active = ldapURL.active;
url.inetAddress = ldapURL.inetAddress;
return url;
}
/**
* Returns the scheme.
*
* @return scheme
*/
public String getScheme() {
return scheme;
}
/**
* Returns the hostname.
*
* @return hostname
*/
public String getHostname() {
return hostname;
}
/**
* Returns the port. If no port was supplied, returns the default port for the scheme.
*
* @return port
*/
public int getPort() {
if (port == -1) {
return "ldaps".equals(scheme) ? DEFAULT_LDAPS_PORT : DEFAULT_LDAP_PORT;
}
return port;
}
/**
* Returns false if a port was supplied in this url.
*
* @return false if a port was supplied in this url
*/
public boolean isDefaultPort() {
return port == -1;
}
/**
* Returns the base DN.
*
* @return baseDn
*/
public String getBaseDn() {
return baseDn == null ? DEFAULT_BASE_DN : baseDn;
}
/**
* Returns whether a base DN was supplied in this url.
*
* @return whether a base DN was supplied in this url
*/
public boolean isDefaultBaseDn() {
return baseDn == null;
}
/**
* Returns the attributes.
*
* @return attributes
*/
public String[] getAttributes() {
return attributes == null ? DEFAULT_ATTRIBUTES : attributes;
}
/**
* Returns whether attributes were supplied in this url.
*
* @return whether an attributes were supplied in this url
*/
public boolean isDefaultAttributes() {
return attributes == null;
}
/**
* Returns the scope.
*
* @return scope
*/
public SearchScope getScope() {
return scope == null ? DEFAULT_SCOPE : scope;
}
/**
* Returns whether a scope was supplied in this url.
*
* @return whether a scope was supplied in this url
*/
public boolean isDefaultScope() {
return scope == null;
}
/**
* Returns the filter.
*
* @return filter
*/
public String getFilter() {
return filter == null ? DEFAULT_FILTER : filter;
}
/**
* Returns whether a filter was supplied in this url.
*
* @return whether a filter was supplied in this url
*/
public boolean isDefaultFilter() {
return filter == null;
}
/**
* Returns the formatted URL as scheme://hostname:port/baseDn?attrs?scope?filter.
*
* @return url
*/
public String getUrl() {
final StringBuilder sb = new StringBuilder(scheme).append("://");
final String hostname = getHostname();
if (hostname != null) {
// ipv6 address
if (hostname.contains(":")) {
sb.append("[").append(hostname).append("]");
} else {
sb.append(hostname);
}
}
sb.append(":").append(getPort());
sb.append("/").append(LdapUtils.percentEncode(getBaseDn()));
sb.append("?");
final String[] attrs = getAttributes();
for (int i = 0; i < attrs.length; i++) {
sb.append(attrs[i]);
if (i + 1 < attrs.length) {
sb.append(",");
}
}
sb.append("?");
final SearchScope scope = getScope();
if (SearchScope.OBJECT == scope) {
sb.append("base");
} else if (SearchScope.ONELEVEL == scope) {
sb.append("one");
} else if (SearchScope.SUBTREE == scope) {
sb.append("sub");
}
sb.append("?").append(LdapUtils.percentEncode(getFilter()));
return sb.toString();
}
/**
* Returns the hostname:port.
*
* @return hostname:port
*/
public String getHostnameWithPort() {
return (getHostname() != null ? getHostname() : "null") + ":" + getPort();
}
/**
* Returns the scheme://hostname:port.
*
* @return scheme://hostname:port
*/
public String getHostnameWithSchemeAndPort() {
return getScheme() + "://" + (getHostname() != null ? getHostname() : "null") + ":" + getPort();
}
/**
* Returns the retry metadata.
*
* @return metadata describing retry attempts for connections made this URL.
*/
LdapURLRetryMetadata getRetryMetadata() {
return retryMetadata;
}
/**
* Sets the retry metadata.
*
* @param metadata retry metadata
*/
void setRetryMetadata(final LdapURLRetryMetadata metadata) {
retryMetadata = metadata;
}
/**
* Returns whether this URL is currently active.
*
* @return true if this URL can be connected to, false otherwise.
*/
boolean isActive() {
return active;
}
/**
* Marks this URL as active.
*/
void activate() {
active = true;
}
/**
* Marks this URL as inactive.
*/
void deactivate() {
active = false;
}
/**
* Returns the resolved IP address.
*
* @return resolved IP address for this URL.
*/
public InetAddress getInetAddress() {
return inetAddress;
}
/**
* Sets the resolved IP address.
*
* @param address IP address for this URL
*/
void setInetAddress(final InetAddress address) {
inetAddress = address;
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (o instanceof LdapURL) {
final LdapURL v = (LdapURL) o;
return LdapUtils.areEqual(scheme, v.scheme) &&
LdapUtils.areEqual(hostname, v.hostname) &&
LdapUtils.areEqual(port, v.port) &&
LdapUtils.areEqual(baseDn, v.baseDn) &&
LdapUtils.areEqual(attributes, v.attributes) &&
LdapUtils.areEqual(scope, v.scope) &&
LdapUtils.areEqual(filter, v.filter) &&
LdapUtils.areEqual(inetAddress, v.inetAddress);
}
return false;
}
@Override
public int hashCode() {
return LdapUtils.computeHashCode(
HASH_CODE_SEED,
scheme,
hostname,
port,
baseDn,
attributes,
scope,
filter,
inetAddress);
}
@Override
public String toString() {
return "[" +
getClass().getName() + "@" + hashCode() + "::" +
"scheme=" + scheme + ", " +
"hostname=" + hostname + ", " +
"port=" + port + ", " +
"baseDn=" + baseDn + ", " +
"attributes=" + Arrays.toString(attributes) + ", " +
"scope=" + scope + ", " +
"filter=" + filter + ", " +
"inetAddress=" + inetAddress + "]";
}
/**
* Matches the supplied url against a pattern and reads its components.
*
* @param url to parse
*/
protected void parseURL(final String url) {
final Matcher m = URL_PATTERN.matcher(url);
if (!m.matches()) {
throw new IllegalArgumentException("Invalid LDAP URL: " + url);
}
// CheckStyle:MagicNumber OFF
scheme = LdapUtils.toLowerCaseAscii(m.group(1));
hostname = m.group(2);
if (hostname != null) {
// check for ipv6 address
if (hostname.startsWith("[") && hostname.endsWith("]")) {
hostname = hostname.substring(1, hostname.length() - 1).trim();
}
}
port = m.group(3) != null ? Integer.parseInt(m.group(3)) : -1;
baseDn = m.group(4) != null ? LdapUtils.percentDecode(m.group(4)) : null;
attributes = m.group(5) != null ? m.group(5).length() > 0 ? m.group(5).split(",") : null : null;
final String scope = m.group(6);
if (scope != null && scope.length() > 0) {
if ("base".equalsIgnoreCase(scope)) {
this.scope = SearchScope.OBJECT;
} else if ("one".equalsIgnoreCase(scope)) {
this.scope = SearchScope.ONELEVEL;
} else if ("sub".equalsIgnoreCase(scope)) {
this.scope = SearchScope.SUBTREE;
} else {
throw new IllegalArgumentException("Invalid scope: " + scope);
}
}
filter = m.group(7) != null
? m.group(7).length() > 0 ? LdapUtils.percentDecode(m.group(7)) : null : null;
// CheckStyle:MagicNumber ON
}
}
// CheckStyle:HiddenField ON

Some files were not shown because too many files have changed in this diff Show more