001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2025 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.bean.model;
015
016import java.util.Collection;
017import java.util.List;
018import java.util.Map;
019import java.util.Set;
020
021import java.util.function.BiFunction;
022import java.util.function.Function;
023
024import org.microbean.assign.Aggregate;
025import org.microbean.assign.AttributedElement;
026import org.microbean.assign.AttributedType;
027import org.microbean.assign.Selectable;
028
029import org.microbean.bean.Bean;
030
031import static java.util.HashMap.newHashMap;
032
033import static java.util.HashSet.newHashSet;
034
035import static java.util.Objects.requireNonNull;
036
037import static java.util.stream.Collectors.toUnmodifiableSet;
038
039public final class Model {
040
041  private final Set<DependencyResolution> dependencyResolutions;
042
043  private volatile boolean valid;
044
045  private Model() {
046    this(Set.of());
047  }
048  
049  private Model(Set<? extends DependencyResolution> dependencyResolutions) {
050    super();
051    if (dependencyResolutions == null || dependencyResolutions.isEmpty()) {
052      this.dependencyResolutions = Set.of();
053      this.valid = true;
054    } else {
055      this.dependencyResolutions = Set.copyOf(dependencyResolutions);
056    }
057  }
058
059  @Override // Object
060  public final boolean equals(final Object other) {
061    return switch (other) {
062    case null -> false;
063    case Model m when this.getClass() == m.getClass() -> this.dependencyResolutions.equals(m.dependencyResolutions);
064    default -> false;
065    };
066  }
067
068  @Override // Object
069  public final int hashCode() {
070    return this.dependencyResolutions.hashCode();
071  }
072
073  public final boolean valid() {
074    if (this.valid) { // volatile read
075      return true;
076    }
077    for (final DependencyResolution dependencyResolution : this.dependencyResolutions) {
078      if (dependencyResolution.beans().size() != 1) {
079        return false;
080      }
081    }
082    this.valid = true; // volatile write
083    return true;
084  }
085
086  public final Set<DependencyResolution> ambiguousDependencyResolutions() {
087    return this.dependencyResolutions.stream().filter(DependencyResolution::ambiguous).collect(toUnmodifiableSet());
088  }
089
090  public final Set<DependencyResolution> unsatisfiedDependencyResolutions() {
091    return this.dependencyResolutions.stream().filter(DependencyResolution::unsatisfied).collect(toUnmodifiableSet());
092  }
093
094  // So you can do:
095  // org.microbean.assign.Selectables.caching(selectable, model.toSelectionCache());
096  public final BiFunction<? super AttributedType, Function<? super AttributedType, ? extends List<Bean<?>>>, ? extends List<Bean<?>>> toSelectionCache() {
097    if (!this.valid()) {
098      throw new IllegalStateException("not valid");
099    }
100    final Map<AttributedType, List<Bean<?>>> m = newHashMap(this.dependencyResolutions.size()); // too big but whatever
101    for (final DependencyResolution r : this.dependencyResolutions) {
102      m.putIfAbsent(r.attributedType(), r.beans());
103    }
104    return m::computeIfAbsent; // m is mutable on purpose; we can revisit if we decide that a Model is the absolute source of truth
105  }
106
107  // s would normally be typesafeFiltering and ambiguityReducing but not necessarily cached
108  @SuppressWarnings("unchecked")
109  public static Model of(final Selectable<? super AttributedType, ? extends Bean<?>> s) {
110    final Collection<? extends Aggregate> allBeans = s.select(null);
111    if (allBeans.isEmpty()) {
112      return new Model(Set.of());
113    }
114    final Map<AttributedType, List<Bean<?>>> m2 = newHashMap(allBeans.size() * 5); // estimate;
115    final Set<DependencyResolution> dependencyResolutions = newHashSet(allBeans.size() * 5);
116    for (final Aggregate bean : allBeans) {
117      for (final AttributedElement dependency : bean.dependencies()) {
118        dependencyResolutions.add(new DependencyResolution(dependency,
119                                 m2.computeIfAbsent(dependency.attributedType(),
120                                                    at -> (List<Bean<?>>)s.select(at))));
121      }
122    }
123    return new Model(dependencyResolutions);
124  }
125
126  public static final record DependencyResolution(AttributedElement attributedElement, List<Bean<?>> beans) {
127
128    public DependencyResolution {
129      requireNonNull(attributedElement, "attributedElement");
130      beans = beans == null ? List.of() : List.copyOf(beans);
131    }
132
133    public final AttributedType attributedType() {
134      return this.attributedElement.attributedType();
135    }
136    
137    public final boolean ambiguous() {
138      return this.beans().size() > 1;
139    }
140
141    public final boolean unsatisfied() {
142      return this.beans().isEmpty();
143    }
144
145  }
146
147}