/* See LICENSE for licensing and NOTICE for copyright. */ package org.ldaptive; import java.io.Serializable; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.TreeSet; import java.util.stream.Collectors; import org.ldaptive.io.ValueTranscoder; /** * Simple bean representing an ldap attribute. Contains a name and a collection of values. * * @author Middleware Services */ public class LdapAttribute extends AbstractLdapBean { /** hash code seed. */ private static final int HASH_CODE_SEED = 313; /** serial version uid. */ private static final long serialVersionUID = -3902233717232754155L; /** Name for this attribute. */ private String attributeName; /** Values for this attribute. */ private final LdapAttributeValues<?> attributeValues; /** Default constructor. */ public LdapAttribute() { this(SortBehavior.getDefaultSortBehavior(), false); } /** * Creates a new ldap attribute. * * @param sb sort behavior of this attribute */ public LdapAttribute(final SortBehavior sb) { this(sb, false); } /** * Creates a new ldap attribute. * * @param binary whether this attribute contains binary values */ public LdapAttribute(final boolean binary) { this(SortBehavior.getDefaultSortBehavior(), binary); } /** * Creates a new ldap attribute. * * @param sb sort behavior of this attribute * @param binary whether this attribute contains binary values */ public LdapAttribute(final SortBehavior sb, final boolean binary) { super(sb); if (binary) { attributeValues = new LdapAttributeValues<>(byte[].class); } else { attributeValues = new LdapAttributeValues<>(String.class); } } /** * Creates a new ldap attribute. * * @param name of this attribute */ public LdapAttribute(final String name) { this(); setName(name); } /** * Creates a new ldap attribute. * * @param name of this attribute * @param values of this attribute */ public LdapAttribute(final String name, final String... values) { this(false); setName(name); addStringValue(values); } /** * Creates a new ldap attribute. * * @param name of this attribute * @param values of this attribute */ public LdapAttribute(final String name, final byte[]... values) { this(true); setName(name); addBinaryValue(values); } /** * Returns the name of this attribute. Includes options if they exist. * * @return attribute name */ public String getName() { return getName(true); } /** * Returns the name of this attribute with or without options. * * @param withOptions whether options should be included in the name * * @return attribute name */ public String getName(final boolean withOptions) { if (withOptions) { return attributeName; } else { final int optionIndex = attributeName.indexOf(";"); return optionIndex > 0 ? attributeName.substring(0, optionIndex) : attributeName; } } /** * Sets the name of this attribute. * * @param name to set */ public void setName(final String name) { attributeName = name; } /** * Returns the options for this attribute. Returns an empty array if attribute contains no options. * * @return options parsed from the attribute name */ public String[] getOptions() { String[] options = null; if (attributeName.indexOf(";") > 0) { final String[] split = attributeName.split(";"); if (split.length > 1) { options = new String[split.length - 1]; System.arraycopy(split, 1, options, 0, options.length); } } return options != null ? options : new String[0]; } /** * 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() { return attributeValues.getStringValues(); } /** * Returns a single string value of this attribute. See {@link #getStringValues()}. * * @return single string attribute value */ public String getStringValue() { final Collection<String> values = getStringValues(); if (values.isEmpty()) { return null; } return values.iterator().next(); } /** * Returns the values of this attribute as byte arrays. String data is UTF-8 encoded. The return collection cannot be * modified. * * @return collection of byte array attribute values */ public Collection<byte[]> getBinaryValues() { return attributeValues.getBinaryValues(); } /** * Returns a single byte array value of this attribute. See {@link #getBinaryValues()}. * * @return single byte array attribute value */ public byte[] getBinaryValue() { final Collection<byte[]> values = getBinaryValues(); if (values.isEmpty()) { return null; } return values.iterator().next(); } /** * Returns whether this ldap attribute contains a value of type byte[]. * * @return whether this ldap attribute contains a value of type byte[] */ public boolean isBinary() { return attributeValues.isType(byte[].class); } /** * Returns the values of this attribute decoded by the supplied transcoder. * * @param <T> type of decoded attributes * @param transcoder to decode attribute values with * * @return collection of decoded attribute values */ public <T> Collection<T> getValues(final ValueTranscoder<T> transcoder) { final Collection<T> values = createSortBehaviorCollection(transcoder.getType()); if (isBinary()) { values.addAll(getBinaryValues().stream().map(transcoder::decodeBinaryValue).collect(Collectors.toList())); } else { values.addAll(getStringValues().stream().map(transcoder::decodeStringValue).collect(Collectors.toList())); } return values; } /** * Returns a single decoded value of this attribute. See {@link #getValues(ValueTranscoder)}. * * @param <T> type of decoded attributes * @param transcoder to decode attribute values with * * @return single decoded attribute value */ public <T> T getValue(final ValueTranscoder<T> transcoder) { final Collection<T> t = getValues(transcoder); if (t.isEmpty()) { return null; } return t.iterator().next(); } /** * Adds the supplied string as a value for this attribute. * * @param value to add * * @throws NullPointerException if value is null */ public void addStringValue(final String... value) { for (String s : value) { attributeValues.add(s); } } /** * Adds all the strings in the supplied collection as values for this attribute. See {@link * #addStringValue(String...)}. * * @param values to add */ public void addStringValues(final Collection<String> values) { values.forEach(this::addStringValue); } /** * Adds the supplied byte array as a value for this attribute. * * @param value to add * * @throws NullPointerException if value is null */ public void addBinaryValue(final byte[]... value) { for (byte[] b : value) { attributeValues.add(b); } } /** * Adds all the byte arrays in the supplied collection as values for this attribute. See {@link * #addBinaryValue(byte[][])}. * * @param values to add */ public void addBinaryValues(final Collection<byte[]> values) { values.forEach(this::addBinaryValue); } /** * Adds the supplied values for this attribute by encoding them with the supplied transcoder. * * @param <T> type attribute to encode * @param transcoder to encode value with * @param value to encode and add * * @throws NullPointerException if value is null */ @SuppressWarnings("unchecked") public <T> void addValue(final ValueTranscoder<T> transcoder, final T... value) { for (T t : value) { if (isBinary()) { attributeValues.add(transcoder.encodeBinaryValue(t)); } else { attributeValues.add(transcoder.encodeStringValue(t)); } } } /** * Adds all the values in the supplied collection for this attribute by encoding them with the supplied transcoder. * See {@link #addValue(ValueTranscoder, Object...)}. * * @param <T> type attribute to encode * @param transcoder to encode value with * @param values to encode and add */ @SuppressWarnings("unchecked") public <T> void addValues(final ValueTranscoder<T> transcoder, final Collection<T> values) { for (T value : values) { addValue(transcoder, value); } } /** * Removes the supplied value from the attribute values if it exists. * * @param value to remove */ public void removeStringValue(final String... value) { for (String s : value) { attributeValues.remove(s); } } /** * Removes the supplied values from the attribute values if they exists. See {@link #removeStringValue(String...)}. * * @param values to remove */ public void removeStringValues(final Collection<String> values) { values.forEach(this::removeStringValue); } /** * Removes the supplied value from the attribute values if it exists. * * @param value to remove */ public void removeBinaryValue(final byte[]... value) { for (byte[] b : value) { attributeValues.remove(b); } } /** * Removes the supplied values from the attribute values if they exists. See {@link #removeBinaryValue(byte[][])}. * * @param values to remove */ public void removeBinaryValues(final Collection<byte[]> values) { values.forEach(this::removeBinaryValue); } /** * 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( attributeName != null ? attributeName.toLowerCase() : null, v.attributeName != null ? v.attributeName.toLowerCase() : null) && LdapUtils.areEqual(attributeValues, v.attributeValues); } return false; } @Override public int hashCode() { return LdapUtils.computeHashCode( HASH_CODE_SEED, attributeName != null ? attributeName.toLowerCase() : null, attributeValues); } @Override public String toString() { return String.format("[%s%s]", attributeName, attributeValues); } /** * Returns an implementation of collection for the sort behavior of this bean. This implementation returns HashSet for * {@link SortBehavior#UNORDERED}, LinkedHashSet for {@link SortBehavior#ORDERED}, and TreeSet for {@link * SortBehavior#SORTED}. * * @param <E> contained in the collection * @param c type contained in the collection * * @return collection corresponding to the sort behavior */ protected <E> Collection<E> createSortBehaviorCollection(final Class<E> c) { Collection<E> values = null; if (SortBehavior.UNORDERED == getSortBehavior()) { values = new HashSet<>(); } else if (SortBehavior.ORDERED == getSortBehavior()) { values = new LinkedHashSet<>(); } else if (SortBehavior.SORTED == getSortBehavior()) { if (!c.isAssignableFrom(Comparable.class)) { values = new TreeSet<>(getComparator(c)); } else { values = new TreeSet<>(); } } return values; } /** * Returns a comparator for the supplied class type. Should not be invoked for classes that have a natural ordering. * Returns a comparator that uses {@link Object#toString()} for unknown types. * * @param <E> type of class * @param c type to compare * * @return comparator for use with the supplied type */ private static <E> Comparator<E> getComparator(final Class<E> c) { if (c.isAssignableFrom(byte[].class)) { return (o1, o2) -> { final ByteBuffer bb1 = ByteBuffer.wrap((byte[]) o1); final ByteBuffer bb2 = ByteBuffer.wrap((byte[]) o2); return bb1.compareTo(bb2); }; } else { return (o1, o2) -> o1.toString().compareTo(o2.toString()); } } /** * Creates a new ldap attribute. The collection of values is inspected for either String or byte[] and the appropriate * attribute is created. * * @param sb sort behavior * @param name of this attribute * @param values of this attribute * * @return ldap attribute * * @throws IllegalArgumentException if values contains something other than String or byte[] */ public static LdapAttribute createLdapAttribute( final SortBehavior sb, final String name, final Collection<Object> values) { final Collection<String> stringValues = new ArrayList<>(); final Collection<byte[]> binaryValues = new ArrayList<>(); for (Object value : values) { if (value instanceof byte[]) { binaryValues.add((byte[]) value); } else if (value instanceof String) { stringValues.add((String) value); } else { throw new IllegalArgumentException("Values must contain either String or byte[]"); } } LdapAttribute la; if (!binaryValues.isEmpty()) { la = new LdapAttribute(sb, true); la.setName(name); la.addBinaryValues(binaryValues); } else { la = new LdapAttribute(sb, false); la.setName(name); la.addStringValues(stringValues); } return la; } /** * Escapes the supplied string value per RFC 4514 section 2.4. * * @param value to escape * * @return escaped value */ public static String escapeValue(final String value) { final int len = value.length(); final StringBuilder sb = new StringBuilder(len); char ch; for (int i = 0; i < len; i++) { ch = value.charAt(i); switch (ch) { case '"': case '#': case '+': case ',': case ';': case '<': case '=': case '>': case '\\': sb.append('\\').append(ch); break; case ' ': // escape first space and last space if (i == 0 || i + 1 == len) { sb.append('\\').append(ch); } else { sb.append(ch); } break; case 0: // escape null sb.append("\\00"); break; default: // escape non-printable ASCII characters // CheckStyle:MagicNumber OFF if (ch < ' ' || ch == 127) { sb.append(LdapUtils.hexEncode(ch)); } else { sb.append(ch); } // CheckStyle:MagicNumber ON break; } } return sb.toString(); } /** * Simple bean for ldap attribute values. * * @param <T> type of values * * @author Middleware Services */ private class LdapAttributeValues<T> implements Serializable { /** hash code seed. */ private static final int HASH_CODE_SEED = 317; /** serial version uid. */ private static final long serialVersionUID = 8075255677989836494L; /** Type of values. */ private final Class<T> type; /** Collection of values. */ private final Collection<T> values; /** * Creates a new ldap attribute values. * * @param t type of values * * @throws IllegalArgumentException if t is not a String or byte[] */ LdapAttributeValues(final Class<T> t) { if (!(t.isAssignableFrom(String.class) || t.isAssignableFrom(byte[].class))) { throw new IllegalArgumentException("Only String and byte[] values are supported"); } type = t; values = createSortBehaviorCollection(type); } /** * Returns whether this ldap attribute values is of the supplied type. * * @param c type to check * * @return whether this ldap attribute values is of the supplied type */ public boolean isType(final Class<?> c) { return type.isAssignableFrom(c); } /** * Returns the values in string format. If the type of this values is String, values are returned as is. If the type * of this values is byte[], values are base64 encoded. See {@link #convertValuesToString(Collection)}. * * @return unmodifiable collection */ @SuppressWarnings("unchecked") public Collection<String> getStringValues() { if (isType(String.class)) { return Collections.unmodifiableCollection((Collection<String>) values); } return Collections.unmodifiableCollection(convertValuesToString((Collection<byte[]>) values)); } /** * Returns the values in binary format. If the type of this values is byte[], values are returned as is. If the type * of this values is String, values are UTF-8 encoded. See {@link #convertValuesToByteArray(Collection)}. * * @return unmodifiable collection */ @SuppressWarnings("unchecked") public Collection<byte[]> getBinaryValues() { if (isType(byte[].class)) { return Collections.unmodifiableCollection((Collection<byte[]>) values); } return Collections.unmodifiableCollection(convertValuesToByteArray((Collection<String>) values)); } /** * Adds the supplied object to this values. * * @param o to add * * @throws IllegalArgumentException if o is null or if o is not the correct type */ public void add(final Object o) { checkValue(o); values.add(type.cast(o)); } /** * Removes the supplied object from this values if it exists. * * @param o to remove * * @throws IllegalArgumentException if o is null or if o is not the correct type */ public void remove(final Object o) { checkValue(o); values.remove(type.cast(o)); } /** * Determines if the supplied object is acceptable to use in this values. * * @param o object to check * * @throws IllegalArgumentException if o is null or if o is not the correct type */ private void checkValue(final Object o) { if (o == null) { throw new IllegalArgumentException("Value cannot be null"); } if (!isType(o.getClass())) { throw new IllegalArgumentException( String.format( "Attribute %s does not support values of type %s", attributeName, o.getClass().isArray() ? o.getClass().getComponentType() : o.getClass().getName())); } } /** * Returns the number of values. * * @return number of values */ public int size() { return values.size(); } /** Removes all the values. */ public void clear() { values.clear(); } @Override @SuppressWarnings("unchecked") public boolean equals(final Object o) { if (o == this) { return true; } if (o instanceof LdapAttributeValues) { final LdapAttributeValues v = (LdapAttributeValues) o; if (type != v.type) { return false; } if (isType(byte[].class)) { return LdapUtils.areEqual( convertValuesToString((Collection<byte[]>) values), convertValuesToString((Collection<byte[]>) v.values)); } else { return LdapUtils.areEqual(values, v.values); } } return false; } @Override public int hashCode() { return LdapUtils.computeHashCode(HASH_CODE_SEED, values); } @Override public String toString() { return getStringValues().toString(); } /** * Base64 encodes the supplied collection of values. * * @param v values to encode * * @return collection of string values */ protected Collection<String> convertValuesToString(final Collection<byte[]> v) { final Collection<String> c = createSortBehaviorCollection(String.class); c.addAll(v.stream().map(LdapUtils::base64Encode).collect(Collectors.toList())); return c; } /** * UTF-8 encodes the supplied collection of values. * * @param v values to encode * * @return collection of byte array values */ protected Collection<byte[]> convertValuesToByteArray(final Collection<String> v) { final Collection<byte[]> c = createSortBehaviorCollection(byte[].class); c.addAll(v.stream().map(LdapUtils::utf8Encode).collect(Collectors.toList())); return c; } } }