001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2024–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.bean;
015
016import java.lang.System.Logger;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Comparator;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Set;
024
025import javax.lang.model.AnnotatedConstruct;
026
027import javax.lang.model.element.AnnotationMirror;
028import javax.lang.model.element.Element;
029import javax.lang.model.element.QualifiedNameable;
030import javax.lang.model.element.TypeElement;
031
032import javax.lang.model.type.DeclaredType;
033import javax.lang.model.type.TypeMirror;
034
035import org.microbean.assign.Annotated;
036import org.microbean.assign.Matcher;
037import org.microbean.assign.Selectable;
038
039import org.microbean.construct.Domain;
040
041import org.microbean.construct.element.SyntheticAnnotationMirror;
042import org.microbean.construct.element.SyntheticAnnotationTypeElement;
043import org.microbean.construct.element.SyntheticAnnotationTypeElement.SyntheticAnnotationElement;
044import org.microbean.construct.element.SyntheticAnnotationValue;
045import org.microbean.construct.element.SyntheticName;
046
047import static java.lang.System.getLogger;
048
049import static java.util.Collections.sort;
050import static java.util.Objects.requireNonNull;
051
052import static javax.lang.model.type.TypeKind.BOOLEAN;
053import static javax.lang.model.type.TypeKind.DECLARED;
054import static javax.lang.model.type.TypeKind.INT;
055
056import static org.microbean.construct.element.AnnotationMirrors.sameAnnotation;
057import static org.microbean.construct.element.AnnotationMirrors.get;
058
059/**
060 * Utility methods for working with {@link Bean}s.
061 *
062 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
063 *
064 * @see #normalize(Collection)
065 *
066 * @see Bean
067 */
068public final class Beans {
069
070
071  /*
072   * Static fields.
073   */
074
075
076  private static final Logger LOGGER = getLogger(Beans.class.getName());
077
078
079  /*
080   * Instance fields.
081   */
082
083
084  private final AnnotationMirror alternate;
085
086  private final AnnotationMirror rank;
087
088  private final Comparator<Bean<?>> byAlternateThenByRankComparator;
089
090
091  /*
092   * Constructors.
093   */
094
095  /**
096   * Creates a new {@link Beans}.
097   *
098   * @param d a non-{@code null} {@link Domain}
099   *
100   * @see #Beans(Domain, AnnotationMirror, AnnotationMirror)
101   */
102  public Beans(final Domain d) {
103    this(d, null, null);
104  }
105  
106  /**
107   * Creates a new {@link Beans}.
108   *
109   * @param alternate a non-{@code null} {@link AnnotationMirror} that will designate a {@link Bean} as being an
110   * alternate
111   *
112   * @param rank a non-{@code null} {@link AnnotationMirror} that can be examined to determine the <dfn>rank</dfn> of a {@link Bean}
113   * that has been deemed to be an alternate
114   *
115   * @exception NullPointerException if any argument is {@code null}
116   *
117   * @see #Beans(Domain, AnnotationMirror, AnnotationMirror)
118   */
119  public Beans(final AnnotationMirror alternate, final AnnotationMirror rank) {
120    this(null, alternate, rank);
121  }
122  
123  /**
124   * Creates a new {@link Beans}.
125   *
126   * @param d a non-{@code null} {@link Domain}
127   *
128   * @param alternate an {@link AnnotationMirror} that will designate a {@link Bean} as being an alternate; may be
129   * {@code null}; if non-{@code null} must have a {@code boolean}-typed {@code value} element
130   *
131   * @param rank an {@link AnnotationMirror} that can be examined to determine the <dfn>rank</dfn> of a {@link Bean}
132   * that has been deemed to be an alternate; may be {@code null}; if non-{@code null} must have an {@code int}-typed
133   * {@code value} element
134   */
135  public Beans(final Domain d,
136               final AnnotationMirror alternate,
137               final AnnotationMirror rank) {
138    super();
139    if (alternate == null) {
140      if (d == null) {
141        this.alternate = null;
142      } else {
143        this.alternate =
144          new SyntheticAnnotationMirror(new SyntheticAnnotationTypeElement(List.of(),
145                                                                           new SyntheticName("Alternate"),
146                                                                           List.of(new SyntheticAnnotationElement(d.primitiveType(BOOLEAN),
147                                                                                                                  new SyntheticName("value"),
148                                                                                                                  new SyntheticAnnotationValue(false)))));
149      }
150    } else {
151      this.alternate = alternate;
152    }
153    if (rank == null) {
154      if (d == null || this.alternate == null) {
155        this.rank = null;
156      } else {
157        this.rank =
158          new SyntheticAnnotationMirror(new SyntheticAnnotationTypeElement(List.of(),
159                                                                           new SyntheticName("Rank"),
160                                                                           List.of(new SyntheticAnnotationElement(d.primitiveType(INT),
161                                                                                                                  new SyntheticName("value"),
162                                                                                                                  new SyntheticAnnotationValue(0)))));
163      }
164    } else {
165      this.rank = rank;
166    }
167    this.byAlternateThenByRankComparator =
168      Comparator.<Bean<?>, Boolean>comparing(this::alternate).reversed()
169      .thenComparing(Comparator.<Bean<?>, Integer>comparing(this::rank).reversed());
170  }
171
172  /**
173   * Returns a non-{@code null}, determinate, immutable {@link List} containing a subset of {@linkplain
174   * Bean#equals(Object) distinct elements} contained in the supplied {@link Collection}, sorted in a deliberately
175   * unspecified fashion.
176   *
177   * @param beans a {@link Collection} of {@link Bean}s; must not be {@code null}
178   *
179   * @return a non-{@code null}, determinate, immutable {@link List}
180   *
181   * @exception NullPointerException if {@code beans} is {@code null}
182   *
183   * @see Bean#equals(Object)
184   */
185  public final List<Bean<?>> normalize(final Collection<? extends Bean<?>> beans) {
186    return beans.isEmpty() ? List.of() : switch (beans.size()) {
187    case 1 -> List.copyOf(beans);
188    default -> {
189      final List<Bean<?>> newBeans = new ArrayList<>(beans instanceof Set<? extends Bean<?>> s ? s : new HashSet<>(beans));
190      yield newBeans.isEmpty() ? List.of() : switch (newBeans.size()) {
191      case 1 -> List.of(newBeans.get(0));
192      default -> {
193        sort(newBeans, this.byAlternateThenByRankComparator);
194        yield List.copyOf(newBeans);
195      }
196      };
197    }
198    };
199  }
200  
201  /**
202   * {@linkplain #normalize(Collection) Normalizes} the supplied {@link Collection} of {@link Bean}s and returns a
203   * {@link Selectable} suitable for it and the supplied {@link Matcher}, using the {@link
204   * org.microbean.assign.Selectables#filtering(Collection, BiPredicate)} method internally.
205   *
206   * <p>The returned {@link Selectable} does not cache its results or resolve ambiguities.</p>
207   *
208   * @param beanCollection a {@link Collection} of {@link Bean}s; must not be {@code null}
209   *
210   * @param m a {@link Matcher}; must not be {@code null}
211   *
212   * @return a non-{@code null}, determinate {@link Selectable}
213   *
214   * @exception NullPointerException if any argument is {@code null}
215   *
216   * @see Matcher#test(Object, Object)
217   *
218   * @see org.microbean.assign.Selectables#filtering(Collection, java.util.function.BiPredicate)
219   *
220   * @see Selectables#ambiguityReducing(Selectable, Predicate, ToIntFunction)
221   *
222   * @see #normalize(Collection)
223   */
224  public final Selectable<Annotated<? extends AnnotatedConstruct>, Bean<?>> typesafeFilteringSelectable(final Collection<? extends Bean<?>> beanCollection,
225                                                                                                        final Matcher<? super Annotated<? extends AnnotatedConstruct>, ? super Id> m) {
226    requireNonNull(m, "m"); // often an IdMatcher; need to overhaul this
227    return
228      beanCollection.isEmpty() ?
229      org.microbean.assign.Selectables.empty() :
230      org.microbean.assign.Selectables.filtering(this.normalize(beanCollection), (b, aac) -> m.test(aac, b.id()));
231  }
232
233  /**
234   * Returns {@code true} if and only if the supplied {@link Bean} {@linkplain Bean#id() has an <code>Id</code>} for which
235   * {@link #alternate(Id)} returns {@code true}.
236   *
237   * @param b a non-{@code null} {@link Bean}
238   *
239   * @return {@code true} if and only if the supplied {@link Bean} {@linkplain Bean#id() has an <code>Id</code>} for which
240   * {@link #alternate(Id)} returns {@code true}
241   *
242   * @exception NullPointerException if {@code b} is {@code null}
243   *
244   * @see #alternate(Id)
245   */
246  public final boolean alternate(final Bean<?> b) {
247    return this.alternate(b.id());
248  }
249
250  /**
251   * Returns {@code true} if and only if the supplied {@link Id} {@linkplain Id#annotations() has annotations} for which
252   * {@link #alternate(Collection)} returns {@code true}.
253   *
254   * @param id a non-{@code null} {@link Id}
255   *
256   * @return {@code true} if and only if the supplied {@link Id} {@linkplain Id#annotations() has annotations} for which
257   * {@link #alternate(Collection)} returns {@code true}
258   *
259   * @exception NullPointerException if {@code id} is {@code null}
260   *
261   * @see #alternate(Collection)
262   */
263  public final boolean alternate(final Id id) {
264    return this.alternate(id.annotations());
265  }
266
267  /**
268   * Returns {@code true} if and only if the supplied {@link Collection} of {@link AnnotationMirror}s contains at least
269   * one {@link AnnotationMirror} for which {@link #alternate(AnnotationMirror)} returns {@code true}.
270   *
271   * @param as a non-{@code null} {@link Collection} of {@link AnnotationMirror}s
272   *
273   * @return {@code true} if and only if the supplied {@link Collection} of {@link AnnotationMirror}s contains at least
274   * one {@link AnnotationMirror} for which {@link #alternate(AnnotationMirror)} returns {@code true}
275   *
276   * @exception NullPointerException if {@code as} is {@code null}
277   *
278   * @see #alternate(AnnotationMirror)
279   */
280  public final boolean alternate(final Collection<? extends AnnotationMirror> as) {
281    for (final AnnotationMirror a : as) {
282      if (this.alternate(a)) {
283        return true;
284      }
285    }
286    return false;
287  }
288
289  /**
290   * Returns {@code true} if and only if the supplied {@link AnnotationMirror} designates whatever it annotates as being
291   * an <dfn>alternate</dfn>.
292   *
293   * @param a a non-{@code null} {@link AnnotationMirror}
294   *
295   * @return {@code true} if and only if the supplied {@link AnnotationMirror} designates whatever it annotates as being
296   * an <dfn>alternate</dfn>
297   *
298   * @exception NullPointerException if {@code a} is {@code null}
299   *
300   * @see #Beans(Domain, AnnotationMirror, AnnotationMirror)
301   */
302  public final boolean alternate(final AnnotationMirror a) {
303    return
304      this.alternate != null &&
305      ((QualifiedNameable)this.alternate.getAnnotationType().asElement()).getQualifiedName().contentEquals(((QualifiedNameable)a.getAnnotationType().asElement()).getQualifiedName()) &&
306      get(a, "value") instanceof Boolean b &&
307      b.booleanValue();
308  }
309
310  /**
311   * Returns the result of invoking the {@link #rank(Id)} method with the supplied {@link Bean}'s {@link Bean#id() Id}.
312   *
313   * @param b a non-{@code null} {@link Bean}
314   *
315   * @return a rank, which may be {@code 0}
316   *
317   * @exception NullPointerException if {@code b} is {@code null}
318   *
319   * @see #rank(Id)
320   */
321  public final int rank(final Bean<?> b) {
322    return this.rank(b.id());
323  }
324
325  /**
326   * Returns the result of invoking the {@link #rank(Collection)} method with the supplied {@link Id}'s {@linkplain
327   * Id#annotations() annotations}.
328   *
329   * @param id a non-{@code null} {@link Id}
330   *
331   * @return a rank, which may be {@code 0}
332   *
333   * @exception NullPointerException if {@code id} is {@code null}
334   *
335   * @see #rank(Collection)
336   */
337  public final int rank(final Id id) {
338    return this.alternate(id) ? this.rank(id.annotations()) : 0;
339  }
340
341  /**
342   * Iterates over the supplied {@link Collection} of {@link AnnotationMirror}s and invokes the {@link
343   * #rank(AnnotationMirror)} method on each one until a non-zero result is found and returns that result, or {@code 0}
344   * in all other cases.
345   *
346   * @param as a non-{@code null} {@link Collection} of {@link AnnotationMirror}s
347   *
348   * @return a rank, which may be {@code 0}
349   *
350   * @exception NullPointerException if {@code as} is {@code null}
351   *
352   * @see #rank(AnnotationMirror)
353   */
354  public final int rank(final Collection<? extends AnnotationMirror> as) {
355    for (final AnnotationMirror a : as) {
356      final int rank = this.rank(a);
357      if (rank != 0) {
358        return rank;
359      }
360    }
361    return 0;
362  }
363
364  /**
365   * Returns an {@code int} representing the <dfn>rank</dfn> derivable from the supplied {@link AnnotationMirror}, which
366   * may be {@code 0}, or {@code 0} if the supplied {@link AnnotationMirror} does not or cannot supply a rank.
367   *
368   * @param a a non-{@code null} {@link AnnotationMirror}
369   *
370   * @return an {@code int} representing the <dfn>rank</dfn> derivable from the supplied {@link AnnotationMirror}, which
371   * may be {@code 0}, or {@code 0} if the supplied {@link AnnotationMirror} does not or cannot supply a rank
372   *
373   * @exception NullPointerException if {@code a} is {@code null}
374   *
375   * @see #Beans(Domain, AnnotationMirror, AnnotationMirror)
376   */
377  public final int rank(final AnnotationMirror a) {
378    return
379      this.rank != null &&
380      ((QualifiedNameable)this.rank.getAnnotationType().asElement()).getQualifiedName().contentEquals(((QualifiedNameable)a.getAnnotationType().asElement()).getQualifiedName()) &&
381      get(a, "value") instanceof Integer i ?
382      i.intValue() :
383      0;
384  }
385
386}