001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2026 microBean™.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
006 * the License. You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
012 * specific language governing permissions and limitations under the License.
013 */
014package org.microbean.assign;
015
016import java.util.Arrays;
017import java.util.Map;
018import java.util.Objects;
019
020import java.util.concurrent.ConcurrentHashMap;
021import java.util.concurrent.ConcurrentMap;
022
023/**
024 * An <strong>experimental</strong>, simple, mutable, concurrent cache of objects.
025 *
026 * @param <T> the type of object to be normalized
027 *
028 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
029 *
030 * @see #normalize(Object, Object)
031 */
032public final class Normalizer<T> {
033
034  private final Map<Object, T> cache;
035
036  /**
037   * Creates a new {@link Normalizer}.
038   *
039   * @see #Normalizer(ConcurrentMap)
040   */
041  public Normalizer() {
042    this(null);
043  }
044
045  /**
046   * Creates a new {@link Normalizer}.
047   *
048   * @param cache a {@link ConcurrentMap} that will be used as the internal cache; may be {@code null} in which case a
049   * default, unbounded, initially empty implementation will be used instead
050   */
051  public Normalizer(final ConcurrentMap<Object, T> cache) {
052    super();
053    this.cache = cache == null ? new ConcurrentHashMap<>() : cache;
054  }
055
056  /**
057   * <dfn>Normalizes</dfn> the supplied {@code element} such that if this {@link Normalizer} already has an {@linkplain
058   * Object#equals(Object) equivalent} cached element, the cached element is returned, and, if it does not, the supplied
059   * {@code element} is cached indefinitely and returned.
060   *
061   * @param element the element to normalize; may be {@code null} in which case {@code null} is returned
062   *
063   * @return the supplied {@code element} or a previously cached {@linkplain Object#equals(Object) equivalent} element
064   *
065   * @see #normalize(Object, Object)
066   */
067  public final T normalize(final T element) {
068    return this.normalize(element, null);
069  }
070
071  /**
072   * <dfn>Normalizes</dfn> the supplied {@code element} such that if this {@link Normalizer} already has an {@linkplain
073   * Object#equals(Object) equivalent} cached element, the cached element is returned, and, if it does not, the supplied
074   * {@code element} is cached indefinitely and returned.
075   *
076   * @param element the element to normalize; may be {@code null} in which case {@code null} is returned
077   *
078   * @param extra additional data to include in equality comparisons; may be {@code null}; ignored if {@code element} is
079   * {@code null}
080   *
081   * @return the supplied {@code element} or a previously cached {@linkplain Object#equals(Object) equivalent} element
082   */
083  @SuppressWarnings("unchecked")
084  public final T normalize(final T element, final Object extra) {
085    if (element == null) {
086      return null;
087    } else if (extra == null) {
088      return this.cache.computeIfAbsent(element, k -> (T)k); // don't bother to create a Key
089    }
090    return this.cache.computeIfAbsent(new Key<>(element, extra), k -> ((Key<T>)k).element());
091  }
092
093
094  /*
095   * Inner and nested classes.
096   */
097
098
099  private static final record Key<T>(T element, Object extra) {
100
101    @Override // Record
102    public final int hashCode() {
103      if (this.element == null) {
104        if (this.extra == null) {
105          return 0;
106        }
107        return this.extra instanceof Object[] a ? Arrays.deepHashCode(a) : this.extra.hashCode();
108      } else if (this.extra == null) {
109        return this.element instanceof Object[] a ? Arrays.deepHashCode(a) : this.element.hashCode();
110      }
111      int hashCode = 17;
112      int c = this.element instanceof Object[] a ? Arrays.deepHashCode(a) : this.element.hashCode();
113      hashCode = 31 * hashCode + c;
114      c = this.extra instanceof Object[] a ? Arrays.deepHashCode(a) : this.extra.hashCode();
115      return 31 * hashCode + c;
116    }
117
118    @Override // Record
119    public final boolean equals(final Object other) {
120      if (other == this) {
121        return true;
122      } else if (other != null && this.getClass() == other.getClass()) {
123        final Key<?> her = (Key<?>)other;
124        return Objects.deepEquals(this.element, her.element) && Objects.deepEquals(this.extra, her.extra);
125      } else {
126        return false;
127      }
128    }
129
130  }
131
132}