LdaptiveEntryMapper.java

/*
 * Copyright 2019 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;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.ldaptive.AttributeModification;
import org.ldaptive.AttributeModification.Type;
import org.ldaptive.LdapAttribute;
import org.ldaptive.LdapEntry;
import org.ldaptive.ModifyRequest;
import org.ldaptive.beans.LdapEntryMapper;
import org.ldaptive.dn.Dn;
import org.ldaptive.dn.NameValue;
import org.ldaptive.dn.RDn;
import org.ldaptive.transcode.ValueTranscoder;

/**
 * The ldap entry mapper.
 *
 * @param <T> the type of the domain object
 * @author Christian Bremer
 */
public interface LdaptiveEntryMapper<T> extends LdapEntryMapper<T> {

  /**
   * Get object classes of the ldap entry. The object classes are only required, if a new ldap entry
   * should be persisted.
   *
   * @return the object classes of the ldap entry
   */
  String[] getObjectClasses();

  /**
   * Get mapped attribute names.
   *
   * @return the mapped attribute names
   */
  default String[] getMappedAttributeNames() {
    return new String[0];
  }

  /**
   * Get binary attribute names.
   *
   * @return the binary attribute names
   */
  default String[] getBinaryAttributeNames() {
    return new String[0];
  }

  @Override
  String mapDn(T domainObject);

  /**
   * Map a ldap entry into a domain object.
   *
   * @param ldapEntry the ldap entry
   * @return the domain object
   */
  T map(LdapEntry ldapEntry);

  @Override
  void map(LdapEntry source, T destination);

  @Override
  default void map(T source, LdapEntry destination) {
    mapAndComputeModifications(source, destination);
  }

  /**
   * Map and compute attribute modifications (see
   * {@link LdapEntry#computeModifications(LdapEntry, LdapEntry)}**).
   *
   * @param source the source (domain object); required
   * @param destination the destination (ldap entry); required
   * @return the attribute modifications
   */
  AttributeModification[] mapAndComputeModifications(
      T source,
      LdapEntry destination);

  /**
   * Map and compute modify request.
   *
   * @param source the source (domain object); required
   * @param destination the destination (ldap entry); required
   * @return the modify request
   */
  default Optional<ModifyRequest> mapAndComputeModifyRequest(
      T source,
      LdapEntry destination) {

    return Optional.ofNullable(mapAndComputeModifications(source, destination))
        .filter(mods -> mods.length > 0)
        .map(mods -> new ModifyRequest(destination.getDn(), mods));
  }

  /**
   * Gets attribute value.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry; required
   * @param name the name; required
   * @param valueTranscoder the value transcoder; required
   * @param defaultValue the default value
   * @return the attribute value
   */
  static <T> T getAttributeValue(
      LdapEntry ldapEntry,
      String name,
      ValueTranscoder<T> valueTranscoder,
      T defaultValue) {
    LdapAttribute attr = ldapEntry == null ? null : ldapEntry.getAttribute(name);
    T value = attr != null ? attr.getValue(valueTranscoder.decoder()) : null;
    return value != null ? value : defaultValue;
  }

  /**
   * Gets attribute value.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry
   * @param attribute the attribute
   * @param defaultValue the default value
   * @return the attribute value
   */
  static <T> T getAttributeValue(
      LdapEntry ldapEntry,
      LdaptiveAttribute<T> attribute,
      T defaultValue) {
    return getAttributeValue(
        ldapEntry,
        attribute.getName(),
        attribute.getValueTranscoder(),
        defaultValue);
  }

  /**
   * Gets attribute values.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry; required
   * @param name the name; required
   * @param valueTranscoder the value transcoder; required
   * @return the attribute values
   */
  static <T> Collection<T> getAttributeValues(
      LdapEntry ldapEntry,
      String name,
      ValueTranscoder<T> valueTranscoder) {
    LdapAttribute attr = ldapEntry == null ? null : ldapEntry.getAttribute(name);
    Collection<T> values = attr != null ? attr.getValues(valueTranscoder.decoder()) : null;
    return values != null ? values : new ArrayList<>();
  }

  /**
   * Gets attribute values.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry
   * @param attribute the attribute
   * @return the attribute values
   */
  static <T> Collection<T> getAttributeValues(
      LdapEntry ldapEntry,
      LdaptiveAttribute<T> attribute) {
    return getAttributeValues(ldapEntry, attribute.getName(), attribute.getValueTranscoder());
  }

  /**
   * Gets attribute values as set.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry; required
   * @param name the name; required
   * @param valueTranscoder the value transcoder; required
   * @return the attribute values as set
   */
  static <T> Set<T> getAttributeValuesAsSet(
      LdapEntry ldapEntry,
      String name,
      ValueTranscoder<T> valueTranscoder) {
    return new LinkedHashSet<>(getAttributeValues(ldapEntry, name, valueTranscoder));
  }

  /**
   * Gets attribute values as set.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry
   * @param attribute the attribute
   * @return the attribute values as set
   */
  static <T> Set<T> getAttributeValuesAsSet(
      LdapEntry ldapEntry,
      LdaptiveAttribute<T> attribute) {
    return getAttributeValuesAsSet(ldapEntry, attribute.getName(), attribute.getValueTranscoder());
  }

  /**
   * Gets attribute values as list.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry; required
   * @param name the name; required
   * @param valueTranscoder the value transcoder; required
   * @return the attribute values as list
   */
  static <T> List<T> getAttributeValuesAsList(
      LdapEntry ldapEntry,
      String name,
      ValueTranscoder<T> valueTranscoder) {
    return new ArrayList<>(getAttributeValues(ldapEntry, name, valueTranscoder));
  }

  /**
   * Gets attribute values as list.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry
   * @param attribute the attribute
   * @return the attribute values as list
   */
  static <T> List<T> getAttributeValuesAsList(
      LdapEntry ldapEntry,
      LdaptiveAttribute<T> attribute) {
    return getAttributeValuesAsList(ldapEntry, attribute.getName(), attribute.getValueTranscoder());
  }

  /**
   * Replaces the value of the attribute with the specified value.
   *
   * @param <T> the type of the domain object
   * @param ldapEntry the ldap entry; required
   * @param name the attribute name; required
   * @param value the attribute value; can be null
   * @param isBinary specifies whether the attribute value is binary or not
   * @param valueTranscoder the value transcoder (can be null if value is also null)
   * @param modifications the list of modifications; required
   */
  static <T> void setAttribute(
      LdapEntry ldapEntry,
      String name,
      T value,
      boolean isBinary,
      ValueTranscoder<T> valueTranscoder,
      List<AttributeModification> modifications) {

    setAttributes(
        ldapEntry,
        name,
        value != null ? Collections.singleton(value) : null,
        isBinary,
        valueTranscoder,
        modifications);
  }

  /**
   * Sets attribute.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry
   * @param attribute the attribute
   * @param value the value
   * @param modifications the modifications
   */
  static <T> void setAttribute(
      LdapEntry ldapEntry,
      LdaptiveAttribute<T> attribute,
      T value,
      List<AttributeModification> modifications) {

    setAttributes(
        ldapEntry,
        attribute.getName(),
        value != null ? Collections.singleton(value) : null,
        attribute.isBinary(),
        attribute.getValueTranscoder(),
        modifications);
  }

  /**
   * Replaces the values of the attribute with the specified values.
   *
   * @param <T> the type of the domain object
   * @param ldapEntry the ldap entry; required
   * @param name the attribute name; required
   * @param values the values of the attribute
   * @param isBinary specifies whether the attribute value is binary or not
   * @param valueTranscoder the value transcoder (can be null if values is also null)
   * @param modifications the list of modifications; required
   */
  static <T> void setAttributes(
      LdapEntry ldapEntry,
      String name,
      Collection<T> values,
      boolean isBinary,
      ValueTranscoder<T> valueTranscoder,
      List<AttributeModification> modifications) {

    Collection<T> realValues = Stream.ofNullable(values)
        .flatMap(Collection::stream)
        .filter(value -> {
          if (value instanceof CharSequence cs) {
            return !cs.isEmpty();
          }
          return value != null;
        })
        .toList();
    LdapAttribute attr = ldapEntry.getAttribute(name);
    if (attr == null && !realValues.isEmpty()) {
      addAttributes(ldapEntry, name, realValues, isBinary, valueTranscoder, modifications);
    } else if (attr != null) {
      if (realValues.isEmpty()) {
        ldapEntry.removeAttribute(name);
        modifications.add(
            new AttributeModification(
                Type.DELETE,
                attr));
      } else if (areNotEqual(realValues, attr.getValues(valueTranscoder.decoder()))) {
        LdapAttribute newAttr = new LdapAttribute();
        newAttr.setBinary(isBinary);
        newAttr.setName(name);
        newAttr.addValues(valueTranscoder.encoder(), realValues);
        ldapEntry.addAttributes(newAttr);
        modifications.add(
            new AttributeModification(
                Type.REPLACE,
                newAttr));
      }
    }
  }

  /**
   * Sets attributes.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry
   * @param attribute the attribute
   * @param values the values
   * @param modifications the modifications
   */
  static <T> void setAttributes(
      LdapEntry ldapEntry,
      LdaptiveAttribute<T> attribute,
      Collection<T> values,
      List<AttributeModification> modifications) {
    setAttributes(ldapEntry, attribute.getName(), values, attribute.isBinary(),
        attribute.getValueTranscoder(), modifications);
  }

  private static boolean areNotEqual(Collection<?> newValues, Collection<?> existingValues) {
    if (newValues.size() != existingValues.size()) {
      return true;
    }
    List<?> newValueList = new ArrayList<>(newValues);
    List<?> existingValueList = new ArrayList<>(existingValues);
    for (int i = 0; i < newValueList.size(); i++) {
      Object newValue = newValueList.get(i);
      Object existingValue = existingValueList.get(i);
      if (newValue instanceof byte[] n && existingValue instanceof byte[] e) {
        if (!Arrays.equals(n, e)) {
          return true;
        }
      } else if (!Objects.equals(newValue, existingValue)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Adds the specified value to the attribute with the specified name.
   *
   * @param <T> the type of the domain object
   * @param ldapEntry the ldap entry; required
   * @param name the attribute name; required
   * @param value the attribute value; can be null
   * @param isBinary specifies whether the attribute value is binary or not
   * @param valueTranscoder the value transcoder; required
   * @param modifications the list of modifications; required
   */
  static <T> void addAttribute(
      LdapEntry ldapEntry,
      String name,
      T value,
      boolean isBinary,
      ValueTranscoder<T> valueTranscoder,
      List<AttributeModification> modifications) {
    addAttributes(
        ldapEntry,
        name,
        value != null ? Collections.singleton(value) : null,
        isBinary,
        valueTranscoder,
        modifications);
  }

  /**
   * Add attribute.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry
   * @param attribute the attribute
   * @param value the value
   * @param modifications the modifications
   */
  static <T> void addAttribute(
      LdapEntry ldapEntry,
      LdaptiveAttribute<T> attribute,
      T value,
      List<AttributeModification> modifications) {
    addAttributes(
        ldapEntry,
        attribute.getName(),
        value != null ? Collections.singleton(value) : null,
        attribute.isBinary(),
        attribute.getValueTranscoder(),
        modifications);
  }

  /**
   * Adds the specified values to the attribute with the specified name.
   *
   * @param <T> the type of the domain object
   * @param ldapEntry the ldap entry; required
   * @param name the attribute name; required
   * @param values the attribute values; can be null
   * @param isBinary specifies whether the attribute value is binary or not
   * @param valueTranscoder the value transcoder; required
   * @param modifications the list of modifications; required
   */
  static <T> void addAttributes(
      LdapEntry ldapEntry,
      String name,
      Collection<T> values,
      boolean isBinary,
      ValueTranscoder<T> valueTranscoder,
      List<AttributeModification> modifications) {
    Collection<T> realValues = Stream.ofNullable(values)
        .flatMap(Collection::stream)
        .filter(value -> {
          if (value instanceof CharSequence cs) {
            return !cs.isEmpty();
          }
          return value != null;
        })
        .toList();
    if (realValues.isEmpty()) {
      return;
    }
    LdapAttribute attr = ldapEntry.getAttribute(name);
    if (attr == null) {
      LdapAttribute newAttr = new LdapAttribute();
      newAttr.setBinary(isBinary);
      newAttr.setName(name);
      newAttr.addValues(valueTranscoder.encoder(), realValues);
      ldapEntry.addAttributes(newAttr);
      modifications.add(
          new AttributeModification(
              Type.ADD,
              newAttr));
    } else {
      List<T> newValues = new ArrayList<>(
          getAttributeValues(ldapEntry, name, valueTranscoder));
      newValues.addAll(realValues);
      setAttributes(ldapEntry, name, newValues, attr.isBinary(), valueTranscoder, modifications);
    }
  }

  /**
   * Add attributes.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry
   * @param attribute the attribute
   * @param values the values
   * @param modifications the modifications
   */
  static <T> void addAttributes(
      LdapEntry ldapEntry,
      LdaptiveAttribute<T> attribute,
      Collection<T> values,
      List<AttributeModification> modifications) {
    addAttributes(ldapEntry, attribute.getName(), values, attribute.isBinary(),
        attribute.getValueTranscoder(), modifications);
  }

  /**
   * Removes an attribute with the specified name.
   *
   * @param ldapEntry the ldap entry; required
   * @param name the name; required
   * @param modifications the modifications; required
   */
  static void removeAttribute(
      LdapEntry ldapEntry,
      String name,
      List<AttributeModification> modifications) {
    LdapAttribute attr = ldapEntry.getAttribute(name);
    if (attr == null) {
      return;
    }
    ldapEntry.removeAttributes(attr);
    modifications.add(
        new AttributeModification(
            Type.DELETE,
            attr));
  }

  /**
   * Removes an attribute with the specified value. If the value is {@code null}, the whole
   * attribute will be removed.
   *
   * @param <T> the type of the domain object
   * @param ldapEntry the ldap entry; required
   * @param name the name; required
   * @param value the value; can be null
   * @param valueTranscoder the value transcoder; required
   * @param modifications the modifications; required
   */
  static <T> void removeAttribute(
      LdapEntry ldapEntry,
      String name,
      T value,
      ValueTranscoder<T> valueTranscoder,
      List<AttributeModification> modifications) {
    LdapAttribute attr = ldapEntry.getAttribute(name);
    if (attr == null) {
      return;
    }
    if (value == null) {
      removeAttribute(ldapEntry, name, modifications);
    } else {
      removeAttributes(ldapEntry, name, Collections.singleton(value), valueTranscoder,
          modifications);
    }
  }

  /**
   * Remove attribute.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry
   * @param attribute the attribute
   * @param value the value
   * @param modifications the modifications
   */
  static <T> void removeAttribute(
      LdapEntry ldapEntry,
      LdaptiveAttribute<T> attribute,
      T value,
      List<AttributeModification> modifications) {
    removeAttribute(ldapEntry, attribute.getName(), value, attribute.getValueTranscoder(),
        modifications);
  }

  /**
   * Remove attributes with the specified values. If values are empty or {@code null}, no attributes
   * will be removed.
   *
   * @param <T> the type of the domain object
   * @param ldapEntry the ldap entry; required
   * @param name the name; required
   * @param values the values
   * @param valueTranscoder the value transcoder; required
   * @param modifications the modifications; required
   */
  static <T> void removeAttributes(
      LdapEntry ldapEntry,
      String name,
      Collection<T> values,
      ValueTranscoder<T> valueTranscoder,
      List<AttributeModification> modifications) {

    LdapAttribute attr = ldapEntry.getAttribute(name);
    if (attr == null || values == null || values.isEmpty()) {
      return;
    }
    List<T> newValues = new ArrayList<>(getAttributeValues(ldapEntry, name, valueTranscoder));
    newValues.removeAll(values);
    setAttributes(ldapEntry, name, newValues, attr.isBinary(), valueTranscoder, modifications);
  }

  /**
   * Remove attributes.
   *
   * @param <T> the type parameter
   * @param ldapEntry the ldap entry
   * @param attribute the attribute
   * @param values the values
   * @param modifications the modifications
   */
  static <T> void removeAttributes(
      LdapEntry ldapEntry,
      LdaptiveAttribute<T> attribute,
      Collection<T> values,
      List<AttributeModification> modifications) {
    removeAttributes(ldapEntry, attribute.getName(), values, attribute.getValueTranscoder(),
        modifications);
  }

  /**
   * Create dn string.
   *
   * @param rdn the rdn; required
   * @param rdnValue the rdn value; required
   * @param baseDn the base dn; required
   * @return the string
   */
  static String createDn(
      String rdn,
      String rdnValue,
      String baseDn) {
    return Dn.builder()
        .add(new RDn(new NameValue(rdn, rdnValue)))
        .add(new Dn(baseDn))
        .build()
        .format();
  }

  /**
   * Gets rdn value (not the rdn attribute name).
   *
   * @param dn the dn
   * @return the rdn
   */
  static String getRdn(String dn) {
    if (dn == null) {
      return null;
    }
    try {
      Dn parsedDn = new Dn(dn);
      if (parsedDn.isEmpty()) {
        return dn;
      }
      return parsedDn.getRDn().getNameValue().getStringValue();

    } catch (IllegalArgumentException ignored) {
      return dn;
    }
  }

}