SerLdapAttr.java

/*
 * Copyright 2024 the original author or 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
 *
 *      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.
 */

package org.bremersee.ldaptive.serializable;

import static java.util.Objects.nonNull;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serial;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import lombok.Getter;
import lombok.ToString;
import org.ldaptive.LdapAttribute;
import org.ldaptive.LdapUtils;

/**
 * A serializable ldap attribute.
 *
 * @author Christian Bremer
 */
@ToString(onlyExplicitlyIncluded = true)
public class SerLdapAttr implements Serializable {

  @Serial
  private static final long serialVersionUID = 1L;

  /**
   * Attribute name.
   */
  @JsonProperty(value = "name", required = true)
  @ToString.Include
  @Getter
  private final String attributeName;

  /**
   * Attribute values.
   */
  @JsonIgnore
  private final Collection<byte[]> attributeValues;

  /**
   * Whether this attribute is binary and string representations should be base64 encoded.
   */
  @JsonProperty(value = "binary", required = true)
  @ToString.Include
  @Getter
  private final boolean binary;

  /**
   * Instantiates a new serializable ldap attribute.
   *
   * @param ldapAttribute the ldap attribute
   */
  public SerLdapAttr(LdapAttribute ldapAttribute) {
    this.attributeName = ldapAttribute.getName();
    this.attributeValues = ldapAttribute.getBinaryValues();
    this.binary = ldapAttribute.isBinary();
  }

  /**
   * Instantiates a new Ser ldap attr.
   *
   * @param name the name
   * @param binary the binary
   * @param values the values
   */
  @JsonCreator
  public SerLdapAttr(
      @JsonProperty(value = "name", required = true) String name,
      @JsonProperty(value = "binary", required = true) boolean binary,
      @JsonProperty(value = "values") List<String> values) {
    this.attributeName = name;
    this.binary = binary;
    this.attributeValues = Stream.ofNullable(values)
        .flatMap(Collection::stream)
        .filter(Objects::nonNull)
        .map(this::toByteArray)
        .filter(bytes -> bytes.length > 0)
        .toList();
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    SerLdapAttr that = (SerLdapAttr) o;
    return binary == that.binary
        && Objects.equals(attributeName.toLowerCase(), that.attributeName.toLowerCase())
        && attributeValues.size() == that.attributeValues.size()
        && attributeValues.stream().allMatch(that::hasValue);
  }

  @Override
  public int hashCode() {
    int hash = 10223;
    int index = 1;
    for (byte[] b : attributeValues) {
      hash = hash * 113 + Arrays.hashCode(b) + index++;
    }
    return hash * 113 + Objects.hash(attributeName, binary);
  }

  /**
   * Returns the number of values in this ldap attribute.
   *
   * @return number of values in this ldap attribute
   */
  public int size() {
    return attributeValues.size();
  }

  /**
   * Returns a single string value of this attribute.
   *
   * @return single string attribute value or null if this attribute is empty
   */
  @JsonIgnore
  public String getStringValue() {
    if (attributeValues.isEmpty()) {
      return null;
    }
    final byte[] val = attributeValues.iterator().next();
    return binary ? LdapUtils.base64Encode(val) : LdapUtils.utf8Encode(val);
  }


  /**
   * 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
   */
  @JsonProperty(value = "values")
  @ToString.Include
  public Collection<String> getStringValues() {
    return attributeValues.stream()
        .map(v -> {
          if (binary) {
            return LdapUtils.base64Encode(v);
          }
          return LdapUtils.utf8Encode(v, false);
        })
        .toList();
  }

  /**
   * Returns a single byte array value of this attribute.
   *
   * @return single byte array attribute value or null if this attribute is empty
   */
  @JsonIgnore
  public byte[] getBinaryValue() {
    return attributeValues.isEmpty() ? null : attributeValues.iterator().next();
  }

  /**
   * Returns the values of this attribute as byte arrays. The return collection cannot be modified.
   *
   * @return collection of string attribute values
   */
  @JsonIgnore
  public Collection<byte[]> getBinaryValues() {
    return attributeValues.stream().toList();
  }

  /**
   * Returns whether the supplied value exists in this attribute.
   *
   * @param value to find
   * @return whether value exists
   */
  public boolean hasValue(byte[] value) {
    return attributeValues.stream()
        .anyMatch(attrValueBytes -> Arrays.equals(attrValueBytes, value));
  }

  /**
   * Returns whether the supplied value exists in this attribute.
   *
   * @param value to find
   * @return whether value exists
   */
  public boolean hasValue(String value) {
    return attributeValues.stream()
        .anyMatch(attrValueBytes -> {
          byte[] valueBytes = toByteArray(value);
          return valueBytes.length > 0 && Arrays.equals(valueBytes, attrValueBytes);
        });
  }

  /**
   * To ldap attribute.
   *
   * @return the ldap attribute
   */
  public LdapAttribute toLdapAttribute() {
    LdapAttribute ldapAttribute = new LdapAttribute();
    if (nonNull(attributeName)) {
      ldapAttribute.setName(attributeName);
    }
    ldapAttribute.setBinary(binary);
    if (nonNull(attributeValues) && !attributeValues.isEmpty()) {
      ldapAttribute.addBinaryValues(attributeValues);
    }
    return ldapAttribute;
  }

  private byte[] toByteArray(String value) {
    if (binary) {
      try {
        return LdapUtils.base64Decode(value);
      } catch (IllegalArgumentException e) {
        return new byte[0];
      }
    }
    return LdapUtils.utf8Encode(value, false);
  }

}