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}