+ public final int size() {
+ return this.types.size();
+ }
+
+ }
+ */
+
}
diff --git a/src/main/java/org/microbean/event/Events.java b/src/main/java/org/microbean/event/Events.java
new file mode 100644
index 0000000..3b313d5
--- /dev/null
+++ b/src/main/java/org/microbean/event/Events.java
@@ -0,0 +1,158 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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.microbean.event;
+
+import java.util.Iterator;
+import java.util.List;
+
+import javax.lang.model.type.TypeMirror;
+
+import org.microbean.assign.AttributedElement;
+import org.microbean.assign.AttributedType;
+
+import org.microbean.attributes.Attributes;
+
+import org.microbean.bean.ReferencesSelector;
+
+import org.microbean.construct.Domain;
+
+import static java.util.Objects.requireNonNull;
+
+import static org.microbean.assign.Qualifiers.anyQualifiers;
+import static org.microbean.assign.Qualifiers.qualifiers;
+
+/**
+ * A utility class for working with events .
+ *
+ * @author Laird Nelson
+ */
+// Deliberately not final.
+public class Events {
+
+ private final EventTypes eventTypes;
+
+ private final EventTypeMatcher eventTypeMatcher;
+
+ private final EventQualifiersMatcher eventQualifiersMatcher;
+
+ private final AttributedType eventListenerAttributedType;
+
+ /**
+ * Creates a new {@link Events}.
+ *
+ * @param eventTypes an {@link EventTypes}; must not be {@code null}
+ *
+ * @param eventTypeMatcher an {@link EventTypeMatcher}; must not be {@code null}
+ *
+ * @param eventQualifiersMatcher an {@link EventQualifiersMatcher}; must not be {@code null}
+ *
+ * @exception NullPointerException if any argument is {@code null}
+ */
+ public Events(final EventTypes eventTypes,
+ final EventTypeMatcher eventTypeMatcher,
+ final EventQualifiersMatcher eventQualifiersMatcher) {
+ super();
+ this.eventTypes = requireNonNull(eventTypes, "eventTypes");
+ this.eventTypeMatcher = requireNonNull(eventTypeMatcher, "eventTypeMatcher");
+ this.eventQualifiersMatcher = requireNonNull(eventQualifiersMatcher, "eventQualifiersMatcher");
+ final Domain d = eventTypes.domain();
+ this.eventListenerAttributedType =
+ new AttributedType(d.declaredType(d.typeElement(EventListener.class.getCanonicalName()),
+ d.wildcardType(),
+ d.wildcardType(null, d.javaLangObjectType())),
+ anyQualifiers());
+ }
+
+ /**
+ * Delivers ("fires") the supplied {@code event} to suitable {@link EventListener}s.
+ *
+ * A suitable {@link EventListener} is one whose {@link EventListener#attributedType()} method returns an {@link
+ * AttributedType}
+ *
+ * @param typeArgumentSource handwave here about the specified type and type argument substitution
+ *
+ * @param attributes a {@link List} of {@link Attributes} qualifying the event; must not be {@code null}
+ *
+ * @param event the event; must not be {@code null}
+ *
+ * @param rs a {@link ReferencesSelector}; used to find {@link EventListener EventListener<?, ?>} references; must not be
+ * {@code null}
+ *
+ * @exception NullPointerException if any argument is {@code null}
+ *
+ * @exception IllegalArgumentException if {@code typeArgumentSource} is unsuitable
+ *
+ * @see #fire(EventListener, Object, ReferencesSelector)
+ *
+ * @see EventTypes#eventTypes(TypeMirror, Object)
+ */
+ // Deliberately final.
+ public final void fire(final TypeMirror typeArgumentSource,
+ final List attributes,
+ final Object event,
+ final ReferencesSelector rs) {
+ final EventTypeList eventTypes = this.eventTypes.eventTypes(typeArgumentSource, event);
+ final List eventQualifiers = qualifiers(attributes);
+ final Iterator extends EventListener, ? super Object>> i =
+ rs.>references(this.eventListenerAttributedType).iterator();
+ while (i.hasNext()) {
+ final EventListener, ? super Object> el = i.next();
+ try {
+ final AttributedType slot = el.attributedType();
+ if (slot == null || !this.eventQualifiersMatcher.test(qualifiers(slot.attributes()), eventQualifiers)) {
+ continue;
+ }
+ final TypeMirror slotType = slot.type();
+ for (final TypeMirror eventType : eventTypes) {
+ if (this.eventTypeMatcher.test(slotType, eventType)) {
+ // This level of indirection permits asynchronous notification.
+ this.fire(el, event, rs);
+ break;
+ }
+ }
+ } finally {
+ i.remove(); // if the EventListener is in a scope where it can be removed, do so, otherwise no-op
+ }
+ }
+ }
+
+ /**
+ * Delivers ("Fires") the supplied {@code event} to the supplied {@link EventListener} via its {@link
+ * EventListener#eventReceived(Object, ReferencesSelector)} method.
+ *
+ * The default implementation of this method behaves as if its body were exactly {@link EventListener
+ * el}.{@link EventListener#eventReceived(Object, ReferencesSelector) eventReceived(event, rs)}.
+ *
+ * When this method is invoked by the {@link #fire(TypeMirror, List, Object, ReferencesSelector)} method, it is
+ * guaranteed that the supplied {@link EventListener} is a suitable one for the supplied {@code event}.
+ *
+ * Overrides of this method must not call the {@link #fire(TypeMirror, List, Object, ReferencesSelector)} method,
+ * or an infinite loop may result.
+ *
+ * @param el an {@link EventListener} that has been determined to be suitable for the supplied {@code event}; must not
+ * be {@code null}
+ *
+ * @param event the event to deliver; must not be {@code null}
+ *
+ * @param rs a {@link ReferencesSelector}; must not be {@code null}
+ *
+ * @exception NullPointerException if any argument is {@code null}
+ */
+ protected void fire(final EventListener, ? super Object> el,
+ final Object event,
+ final ReferencesSelector rs) {
+ el.eventReceived(event, rs);
+ }
+
+}
diff --git a/src/site/site.xml b/src/site/site.xml
index fb69fe9..b8967ea 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -13,7 +13,7 @@
org.apache.maven.skins
maven-fluido-skin
- 2.0.0
+ 2.1.0
diff --git a/src/test/java/org/microbean/event/TestCDIEventParameterizedType.java b/src/test/java/org/microbean/event/TestCDIEventParameterizedType.java
new file mode 100644
index 0000000..43c00bd
--- /dev/null
+++ b/src/test/java/org/microbean/event/TestCDIEventParameterizedType.java
@@ -0,0 +1,158 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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.microbean.event;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.enterprise.event.Event;
+
+import jakarta.enterprise.inject.Instance;
+import jakarta.enterprise.inject.UnsatisfiedResolutionException;
+
+import jakarta.enterprise.inject.se.SeContainer;
+import jakarta.enterprise.inject.se.SeContainerInitializer;
+
+import jakarta.enterprise.util.TypeLiteral;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class TestCDIEventParameterizedType {
+
+ private SeContainer c;
+
+ private TestCDIEventParameterizedType() {
+ super();
+ }
+
+ @BeforeEach
+ final void setup() {
+ this.c = SeContainerInitializer.newInstance()
+ .disableDiscovery()
+ .addBeanClasses(DummyBean.class)
+ .initialize();
+ }
+
+ @AfterEach
+ final void teardown() {
+ if (this.c != null) {
+ this.c.close();
+ }
+ }
+
+ @Test
+ final void testFireParameterizedSubtype() {
+ final Event> e = this.c.select(new TypeLiteral>>() {}).get();
+ final ArrayList l = new ArrayListWithTwoTypeVariables();
+ // l's raw type is ArrayListWithTwoTypeVariables.
+ // The specified type is ArrayList, so we can infer one of two type arguments. We can't infer the second
+ // one, so this fails.
+ assertThrows(IllegalArgumentException.class, () -> e.fire(l));
+ }
+
+ @Test
+ final void testFireArrayListString() {
+ final Event> e = this.c.select(new TypeLiteral>>() {}).get();
+ // The CDI specification says that the event types of an object serving as an event "include all superclasses and
+ // interfaces of the runtime class of the event object". Somewhat interestingly, the "runtime class" of the event
+ // being fired here is, of course, simply, java.util.ArrayList. Nevertheless, in Weld, one of the event types of the
+ // event is java.util.ArrayList (set a breakpoint on the fire() call below and find out for
+ // yourself). java.util.ArrayList is a ParameterizedType and not a "runtime class" so this is kind of
+ // interesting. See also https://github.com/jakartaee/cdi/issues/884.
+ e.fire(new ArrayList());
+ }
+
+ @Test
+ final void testFireSubtypeWithWildcards0() {
+ final Event super List> e = this.c.select(new TypeLiteral>>() {}).get();
+ e.fire(new ArrayList());
+ }
+
+ @Test
+ final void testFireSubtypeWithWildcards1() {
+ final Event super List> e = this.c.select(new TypeLiteral>>() {}).get();
+ // Weld really should permit this
+ assertThrows(IllegalArgumentException.class, () -> e.fire(new ArrayList()));
+ }
+
+ @Test
+ final void testFireSubtypeWithWildcards2() {
+ final Event super List extends String>> e = this.c.select(new TypeLiteral>>() {}).get();
+ // Weld really should permit this
+ assertThrows(IllegalArgumentException.class, () -> e.fire(new ArrayList()));
+ }
+
+ @Test
+ final void testFireOuterInner() {
+ final Event.Inner> e = this.c.select(new TypeLiteral.Inner>>() {}).get();
+ e.fire(new Outer().new Inner());
+ }
+
+ @Test
+ final void testInstanceWithWildcards() {
+ // Obvious.
+ assertNotNull(this.c.select(DummyBean.class).get());
+ // Same formulation, just using TypeLiteral. Obvious.
+ assertNotNull(this.c.select(new TypeLiteral() {}).get());
+ // Maybe less obvious at first glance, but still obvious.
+ assertNotNull(this.c.select(new TypeLiteral>() {}).get().get());
+ // Kind of weird until you remember that wildcards are illegal bean types. Still kind of weird. It is true that if
+ // this *did* return an unknown-type-that-extends-DummyBean it's not clear you could call destroy(Object) or
+ // remove(Object) on the iterator.
+ assertThrows(UnsatisfiedResolutionException.class, this.c.select(new TypeLiteral>() {}).get()::get);
+ assertThrows(UnsatisfiedResolutionException.class, this.c.select(new TypeLiteral>() {}).get()::get);
+ // See above. Here you wouldn't be able to iterate.
+ assertThrows(UnsatisfiedResolutionException.class, this.c.select(new TypeLiteral>() {}).get()::get);
+ }
+
+ @Test
+ final void testInference() throws ReflectiveOperationException {
+ final Event> e = this.c.select(new TypeLiteral>>() {}).get();
+ final Sub sub = new Sub<>();
+ final Method m = e.getClass().getDeclaredMethod("getEventType", Class.class);
+ assertTrue(m.trySetAccessible());
+ final ParameterizedType p = (ParameterizedType)m.invoke(e, sub.getClass());
+ assertSame(p.getRawType(), Sub.class);
+ assertSame(Object.class, p.getActualTypeArguments()[0]);
+ assertSame(String.class, p.getActualTypeArguments()[1]);
+ }
+
+ private static class Sup {}
+
+ private static class Sub extends Sup {}
+
+ private static final class DummyBean {}
+
+ private static final class Outer {
+ private final class Inner extends ArrayList {
+ private static final long serialVersionUID = 1L;
+ }
+ }
+
+ private static final class ArrayListWithTwoTypeVariables extends ArrayList {
+ private static final long serialVersionUID = 1L;
+ };
+
+}
diff --git a/src/test/java/org/microbean/event/TestEventTypes.java b/src/test/java/org/microbean/event/TestEventTypes.java
new file mode 100644
index 0000000..d0b5ef5
--- /dev/null
+++ b/src/test/java/org/microbean/event/TestEventTypes.java
@@ -0,0 +1,140 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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.microbean.event;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeMirror;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.microbean.construct.DefaultDomain;
+import org.microbean.construct.Domain;
+
+import static javax.lang.model.type.TypeKind.ARRAY;
+import static javax.lang.model.type.TypeKind.DECLARED;
+import static javax.lang.model.type.TypeKind.INT;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class TestEventTypes {
+
+ private Domain domain;
+
+ private EventTypes eventTypes;
+
+ private TestEventTypes() {
+ super();
+ }
+
+ @BeforeEach
+ final void setup() {
+ this.domain = new DefaultDomain();
+ this.eventTypes = new EventTypes(this.domain);
+ }
+
+ @Test
+ final void testEventTypeCase1() {
+ final TypeMirror na = domain.declaredType(domain.javaLangObject());
+ final TypeMirror rv = eventTypes.eventType(na, int.class);
+ assertTrue(rv instanceof PrimitiveType);
+ assertSame(INT, rv.getKind());
+ }
+
+ @Test
+ final void testEventTypeCase2() {
+ final TypeMirror na = domain.declaredType(domain.javaLangObject());
+ final TypeMirror rv = eventTypes.eventType(na, int[].class);
+ assertSame(ARRAY, rv.getKind());
+ assertSame(INT, ((ArrayType)rv).getComponentType().getKind());
+ }
+
+ @Test
+ final void testEventTypeCase3() {
+ final TypeMirror na = domain.declaredType(domain.javaLangObject());
+ final TypeMirror rv = eventTypes.eventType(na, Object.class);
+ assertSame(DECLARED, rv.getKind());
+ assertTrue(domain.javaLangObject(rv));
+ }
+
+ @Test
+ final void testEventTypeCase4() {
+ final TypeMirror na = domain.declaredType(domain.javaLangObject());
+ final TypeMirror rv = eventTypes.eventType(na, Object[].class);
+ assertSame(ARRAY, rv.getKind());
+ assertTrue(domain.javaLangObject(((ArrayType)rv).getComponentType()));
+ }
+
+ @Test
+ final void testEventTypeCase5() {
+ final TypeMirror intScalar = domain.primitiveType(INT);
+ assertThrows(IllegalArgumentException.class, () -> eventTypes.eventType(intScalar, ArrayList.class));
+ }
+
+ @Test
+ final void testEventTypeCase6() {
+ final TypeMirror intArray = domain.arrayTypeOf(domain.primitiveType(INT));
+ assertThrows(IllegalArgumentException.class, () -> eventTypes.eventType(intArray, ArrayList.class));
+ }
+
+ @Test
+ final void testEventTypeCase7() {
+ final TypeMirror list = domain.declaredType(domain.typeElement(List.class.getCanonicalName()));
+ assertThrows(IllegalArgumentException.class, () -> eventTypes.eventType(list, ArrayList.class));
+ }
+
+ @Test
+ final void testEventTypeCase8() {
+ final TypeMirror listArray = domain.arrayTypeOf(domain.declaredType(domain.typeElement(List.class.getCanonicalName())));
+ assertThrows(IllegalArgumentException.class, () -> eventTypes.eventType(listArray, ArrayList.class));
+ }
+
+ @Test
+ final void testEventTypeCase9() {
+ final TypeMirror string = domain.declaredType(domain.typeElement(String.class.getCanonicalName()));
+ final TypeMirror abstractListString = domain.declaredType(domain.typeElement(AbstractList.class.getCanonicalName()), string);
+ assertTrue(domain.sameType(domain.declaredType(domain.typeElement(ArrayList.class.getCanonicalName()), string),
+ eventTypes.eventType(abstractListString, ArrayList.class)));
+ }
+
+ @Test
+ final void testEventTypeCase10() {
+ final TypeMirror string = domain.declaredType(domain.typeElement(String.class.getCanonicalName()));
+ final TypeMirror listStringArray = domain.arrayTypeOf(domain.declaredType(domain.typeElement(List.class.getCanonicalName()),
+ string));
+ assertTrue(domain.sameType(domain.arrayTypeOf(domain.declaredType(domain.typeElement(ArrayList.class.getCanonicalName()),
+ string)),
+ eventTypes.eventType(listStringArray, ArrayList[].class)));
+ }
+
+ @Test
+ final void testEventTypeCase11() {
+ final TypeMirror string = domain.declaredType(domain.typeElement(String.class.getCanonicalName()));
+ final TypeMirror listStringArray = domain.arrayTypeOf(domain.declaredType(domain.typeElement(List.class.getCanonicalName()),
+ string));
+ assertTrue(domain.sameType(domain.declaredType(domain.typeElement(ArrayList.class.getCanonicalName()), string),
+ eventTypes.eventType(listStringArray, ArrayList.class)));
+ }
+
+
+}
diff --git a/src/test/java/org/microbean/event/TestJDKFacts.java b/src/test/java/org/microbean/event/TestJDKFacts.java
new file mode 100644
index 0000000..4337014
--- /dev/null
+++ b/src/test/java/org/microbean/event/TestJDKFacts.java
@@ -0,0 +1,115 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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.microbean.event;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+
+import java.util.ArrayList;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class TestJDKFacts {
+
+ private TestJDKFacts() {
+ super();
+ }
+
+ @Test
+ final void testCanonicalNames() {
+ final Outer.Inner0 i = new Outer().new Inner0();
+ assertEquals(this.getClass().getName() + "." +
+ Outer.class.getSimpleName() + "." +
+ Outer.Inner0.class.getSimpleName(),
+ i.getClass().getCanonicalName());
+ assertEquals("int", int.class.getCanonicalName());
+ assertEquals("int[]", int[].class.getCanonicalName());
+ }
+
+ @Test
+ final void testClassKinds() {
+ assertTrue(Outer.Inner0.class.isMemberClass());
+ assertTrue(Outer.InnerStatic.class.isMemberClass());
+ }
+
+ @Test
+ final void testReflectionAssumptionsAboutRawArrayList() {
+ // Generic superclass is a Class (specifically ArrayList.class); no type arguments
+ assertSame(ArrayList.class, RawArrayList.class.getGenericSuperclass());
+ assertEquals(1, ArrayList.class.getTypeParameters().length);
+ }
+
+ @Test
+ final void testReflectionAssumptionsAboutCookedArrayList() {
+ // Generic superclass is a ParameterizedType (specifically ArrayList); one type argument
+ final ParameterizedType pt = (ParameterizedType)CookedArrayList.class.getGenericSuperclass();
+ assertSame(ArrayList.class, pt.getRawType());
+ final Type[] tas = pt.getActualTypeArguments();
+ assertEquals(1, tas.length);
+ assertSame(String.class, tas[0]);
+ }
+
+ @Test
+ final void testOuterInner() {
+ final Outer.Inner0 i = new Outer().new Inner0();
+ assertSame(Outer.Inner0.class, i.getClass());
+ assertSame(Outer.class, Outer.Inner0.class.getEnclosingClass());
+ assertSame(this.getClass(), Outer.class.getEnclosingClass());
+
+ // Get ArrayList. Can't get ArrayList.
+ final ParameterizedType inner0Supertype = (ParameterizedType)Outer.Inner0.class.getGenericSuperclass();
+ assertSame(ArrayList.class, inner0Supertype.getRawType());
+ final Type[] inner0SupertypeTypeArguments = inner0Supertype.getActualTypeArguments();
+ assertEquals(1, inner0SupertypeTypeArguments.length);
+ assertTrue(inner0SupertypeTypeArguments[0] instanceof TypeVariable>);
+
+ final OuterSubclass os = new OuterSubclass();
+ OuterSubclass.Inner1 x0 = os.new Inner1();
+ Outer.Inner1 x1 = new OuterSubclass().new Inner1();
+ OuterSubclass.Inner1 x2 = new OuterSubclass().new Inner1();
+
+
+
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static final class RawArrayList extends ArrayList {
+ private static final long serialVersionUID = 1L;
+ }
+
+ private static final class CookedArrayList extends ArrayList {
+ private static final long serialVersionUID = 1L;
+ }
+
+ private static class Outer {
+ private class Inner0 extends ArrayList {
+ private static final long serialVersionUID = 1L;
+ }
+ class Inner1 {}
+ private static class InnerStatic {}
+ }
+
+ private static class OuterSubclass extends Outer {}
+
+ private static class ParameterizedSubtype {}
+
+}
diff --git a/src/test/java/org/microbean/event/TestLanguageModelFacts.java b/src/test/java/org/microbean/event/TestLanguageModelFacts.java
new file mode 100644
index 0000000..d755e8e
--- /dev/null
+++ b/src/test/java/org/microbean/event/TestLanguageModelFacts.java
@@ -0,0 +1,363 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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.microbean.event;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.TypeParameterElement;
+
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+
+import org.junit.jupiter.api.Test;
+
+import org.microbean.construct.DefaultDomain;
+import org.microbean.construct.Domain;
+
+import org.microbean.construct.element.UniversalElement;
+
+import org.microbean.construct.type.UniversalType;
+
+import static javax.lang.model.element.ElementKind.ANNOTATION_TYPE;
+import static javax.lang.model.element.ElementKind.CLASS;
+import static javax.lang.model.element.ElementKind.METHOD;
+
+import static javax.lang.model.type.TypeKind.DECLARED;
+import static javax.lang.model.type.TypeKind.TYPEVAR;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class TestLanguageModelFacts {
+
+ private static final Domain domain = new DefaultDomain();
+
+ private TestLanguageModelFacts() {
+ super();
+ }
+
+ @Test
+ final void testAnnotationMirrorEnclosingAndEnclosedElements() {
+ final TypeElement e = domain.typeElement("java.security.AccessControlException"); // has @Deprecated on it
+ final List extends AnnotationMirror> as = e.getAnnotationMirrors();
+ assertEquals(1, as.size());
+ final AnnotationMirror deprecated = as.get(0);
+ final Map extends ExecutableElement, ? extends AnnotationValue> m = deprecated.getElementValues();
+ assertEquals(2, m.size(), String.valueOf(m)); // since, forRemoval
+ // Just get whichever one happens to come up
+ final ExecutableElement element = m.keySet().iterator().next();
+ final TypeElement deprecatedElement = (TypeElement)element.getEnclosingElement();
+ assertSame(ANNOTATION_TYPE, deprecatedElement.getKind());
+ final List extends Element> enclosedElements = deprecatedElement.getEnclosedElements();
+ assertEquals(2, enclosedElements.size());
+ enclosedElements.forEach(ee -> assertSame(METHOD, ee.getKind()));
+ System.out.println("*** modifiers: " + deprecatedElement.getModifiers());
+ enclosedElements.forEach(ee -> System.out.println(" modifiers for " + ee + ": " + ee.getModifiers()));
+
+ }
+
+ @Test
+ final void testPrototypicalTypeAndDeclaredType() {
+ final TypeElement string = domain.typeElement("java.lang.String");
+ final DeclaredType prototypicalStringType = (DeclaredType)string.asType();
+ final DeclaredType stringTypeUsage = domain.declaredType(string);
+ assertTrue(string instanceof UniversalElement);
+ assertTrue(prototypicalStringType instanceof UniversalType);
+ assertTrue(stringTypeUsage instanceof UniversalType);
+ assertNotSame(prototypicalStringType, stringTypeUsage);
+ // This surprises me, but it ends up being true because types in the compiler are always compared by identity, and
+ // UniversalType delegates to its delegate's equals() implementation. So the prototype type is indeed not the same
+ // as and therefore not equal to the type usage.
+ assertNotEquals(prototypicalStringType, stringTypeUsage);
+ assertTrue(domain.sameType(prototypicalStringType, stringTypeUsage));
+ }
+
+ @Test
+ final void testRawPropagation() {
+ final DeclaredType arrayListTU = domain.declaredType(domain.typeElement("java.util.ArrayList"));
+ assertTrue(arrayListTU.getTypeArguments().isEmpty());
+
+ // Hypothesis: if you use directSupertypes, it will sub in the type arguments. (For inferring event type arguments
+ // that's not what we want.) If you supply it a raw type, then all the supertypes will be raw as well. Again, not
+ // what we want.
+ assertEquals(List.of(), ((DeclaredType)domain.directSupertypes(arrayListTU).get(0)).getTypeArguments());
+ }
+
+ @Test
+ final void testNoTypeVariablePropagationInTypeUsages() {
+ final TypeElement arrayList = domain.typeElement("java.util.ArrayList");
+ final DeclaredType abstractListSC = (DeclaredType)arrayList.getSuperclass();
+ final TypeElement abstractList = (TypeElement)abstractListSC.asElement();
+ assertEquals(domain.typeElement("java.util.AbstractList"), abstractList);
+ final TypeParameterElement abstractListTPE = abstractList.getTypeParameters().get(0);
+ final TypeVariable abstractListTV = (TypeVariable)abstractListTPE.asType();
+ assertEquals(abstractListTPE, abstractListTV.asElement()); // note that the arraylist E died
+ }
+
+ @Test
+ final void testJLSViolation() {
+ final TypeElement arrayList = domain.typeElement("java.util.ArrayList");
+ final TypeElement string = domain.typeElement("java.lang.String");
+ final DeclaredType arrayListTU = domain.declaredType(arrayList, string.asType());
+ final List extends TypeMirror> directSupertypes = domain.directSupertypes(arrayListTU);
+ // The first type in the list/set is AbstractList.
+ final DeclaredType abstractListST = (DeclaredType)directSupertypes.get(0);
+ assertEquals(1, abstractListST.getTypeArguments().size());
+ // The next type in the list/set is List. Interface types are guaranteed to appear after non-interface types
+ // so this shows that the raw type represented by, simply, ArrayList does not appear in the set. The JLS says it
+ // should, among other types. See also https://docs.oracle.com/javase/specs/jls/se25/html/jls-4.html#jls-4.10.2 and
+ // https://bugs.openjdk.org/browse/JDK-8055219 and
+ // https://stackoverflow.com/questions/79817198/why-does-directsupertypes-not-return-a-raw-type-as-required-by-jls-4-10.
+ //
+ // My guess is that Types#directSupertypes(TypeMirror) should *really* be specified to return direct supertypes
+ // *that can be declared in the Java language*. This would make sense given that the annotation processing model was
+ // partially reverse engineered out of the existing guts of javac. So, for example, since you can write neither:
+ //
+ // // Invalid Java
+ // public class ArrayList extends ArrayList, AbstractList...
+ //
+ // ...nor:
+ //
+ // // Invalid Java
+ // public class ArrayLIst extends ArrayList extends WTF>...
+ //
+ // ...nor anything analogous, the types represented by ArrayList and ArrayList extends WTF> will not appear in the
+ // return value.
+ assertTrue(((DeclaredType)directSupertypes.get(1)).asElement().getKind().isInterface());
+ }
+
+ @Test
+ final void testPrototypicalTypeStructure() {
+
+ // ArrayList in ArrayList. The declaring type element.
+ //
+ // <>
+ // (arrayList)
+ // +---------------+
+ // | ArrayList |
+ // +---------------+
+ //
+ final TypeElement arrayList = domain.typeElement("java.util.ArrayList");
+
+ // in ArrayList. The declaring type parameter element.
+ //
+ // <> <>
+ // (arrayList) (arrayListE)
+ // +---------------+ +------------------------+
+ // | ArrayList <---> E |
+ // +---------------+ +------------------------+
+ //
+ final TypeParameterElement arrayListE = (TypeParameterElement)arrayList.getTypeParameters().get(0);
+ assertEquals(arrayList, arrayListE.getGenericElement());
+
+ // The type variable declared by .
+ //
+ // <> <>
+ // (arrayList) (arrayListE)
+ // +---------------+ +------------------------+
+ // | ArrayList <---> E |
+ // +---------------+ +------------^-----------+
+ // |
+ // |
+ // V
+ // <>
+ // (arrayListETV)
+ // +----------------+
+ // | (E) |
+ // +----------------+
+ //
+ final TypeVariable arrayListETV = (TypeVariable)arrayListE.asType();
+ assertEquals(arrayListE, arrayListETV.asElement());
+ assertEquals(arrayListETV, arrayListE.asType());
+
+ // The prototypical type declared by ArrayList.
+ //
+ // <> <>
+ // (arrayList) (arrayListE)
+ // +---------------+ +------------------------+
+ // | ArrayList <---> E |
+ // +--------^------+ +------------^-----------+
+ // | |
+ // V |
+ // <> V
+ // <> <>
+ // (arrayListPT) (arrayListETV)
+ // +---------------------+ +----------------+
+ // | (ArrayList) | | (E) |
+ // +---------------------+ +----------------+
+ final DeclaredType arrayListPT = (DeclaredType)arrayList.asType();
+ assertEquals(arrayList, arrayListPT.asElement());
+
+ // Its type argument is the type variable underlying . This completes a cycle:
+ //
+ // <> <>
+ // (arrayList) (arrayListE)
+ // +---------------+ +------------------------+
+ // | ArrayList <---> E |
+ // +--------^------+ +------------^-----------+
+ // | |
+ // V |
+ // <> V
+ // <> <>
+ // (arrayListPT) (arrayListETV)
+ // +---------------------+ +----------------+
+ // | (ArrayList) +---> (E) |
+ // +---------------------+ +----------------+
+ //
+ // The prototypical type is not raw, because it _does_ have type arguments. The type argument is a type variable,
+ // not nothing.
+ assertEquals(arrayListETV, arrayListPT.getTypeArguments().get(0));
+
+ }
+
+ @Test
+ final void testAsMemberOf() {
+ final TypeElement arrayList = domain.typeElement("java.util.ArrayList");
+ final TypeElement list = domain.typeElement("java.util.List");
+ final TypeElement string = domain.typeElement("java.lang.String");
+
+ final DeclaredType stringTypeUsage = domain.declaredType(string);
+ final DeclaredType listStringTypeUsage = domain.declaredType(list, stringTypeUsage);
+
+ final DeclaredType arrayListRawTypeUsage = domain.declaredType(arrayList);
+
+ // Can we do this? No.
+ assertThrows(IllegalArgumentException.class, () -> domain.asMemberOf(listStringTypeUsage, arrayList));
+
+ // OK, can we do this? No.
+ assertThrows(IllegalArgumentException.class, () -> domain.asMemberOf(listStringTypeUsage, domain.typeParameterElement(arrayList, "E")));
+ }
+
+ @Test
+ final void testRawTypeRepresentation() {
+ final List l = new ArrayList<>();
+
+ // Here's a TypeElement representing ArrayList (not ArrayList, not ArrayList>, not really ArrayList).
+ final TypeElement list = domain.typeElement(l.getClass().getCanonicalName());
+
+ // rawArrayListTypeUsage here is a *usage* of the type declared by the java.util.ArrayList TypeElement. This usage has no
+ // type arguments. This makes the declared type "in" this usage a raw type.
+ final DeclaredType rawArrayListTypeUsage = domain.declaredType(list); // note: no type arguments supplied
+ assertSame(DECLARED, rawArrayListTypeUsage.getKind());
+
+ // rawArrayListTypeUsage has no type arguments. (It can't; none were supplied. See above.)
+ List extends TypeMirror> typeArguments = rawArrayListTypeUsage.getTypeArguments();
+ assertEquals(0, typeArguments.size());
+
+ // The declared type's type element has one type parameter element (E). The fact that the declared type's type
+ // argument count is zero and its delcaring type element's type parameter count is one is what makes the declared
+ // type raw.
+ final TypeElement e = (TypeElement)rawArrayListTypeUsage.asElement();
+ assertSame(CLASS, e.getKind());
+ assertEquals(list, e); // basically identical, but always call equals() to allow delegates and wrappers to work
+
+ // java.util.ArrayList's sole type parameter element is "E" (as in public class java.util.ArrayList...).
+ final List extends TypeParameterElement> typeParameterElements = list.getTypeParameters();
+ assertEquals(1, typeParameterElements.size());
+ final TypeParameterElement typeParameterElement = typeParameterElements.get(0);
+ assertTrue(typeParameterElement.getSimpleName().contentEquals("E"));
+
+ // This type parameter element declares a type variable (definitionally unnamed).
+ TypeVariable tv = (TypeVariable)typeParameterElement.asType();
+
+ // (The type variable can get its declaring type parameter element.)
+ assertEquals(typeParameterElement, (TypeParameterElement)tv.asElement());
+
+ // Here is the type as it was *declared* by the java.util.ArrayList TypeElement. Note that it *does* have type
+ // arguments. This is distinct from rawArrayListTypeUsage above, which represents a declared type *usage*.
+ final DeclaredType listDeclarationType = (DeclaredType)list.asType();
+ assertSame(DECLARED, listDeclarationType.getKind());
+ assertEquals(list, listDeclarationType.asElement());
+
+ // So what *are* the type arguments in a type declaration created/implied by a TypeElement? The TypeVariables
+ // declared by the TypeParameterElements.
+ typeArguments = listDeclarationType.getTypeArguments();
+ assertEquals(1, typeArguments.size());
+ assertEquals(tv, typeArguments.get(0));
+
+ // To recap so far:
+ //
+ // In this partial code snippet:
+ //
+ // public class ArrayList ...
+ //
+ // * The TypeElement is named java.util.ArrayList
+ // * It has one TypeParameterElement whose name is E
+ // * That TypeParameterElement's asType() method returns a TypeVariable "backing" E
+ // * The TypeVariable's asElement() method returns the TypeParameterElement (so mutual dependency)
+ // * The TypeElement's asType() method returns a DeclaredType representing the actual declaration type
+ // * This DeclaredType has the same number of type arguments as its declaring TypeElement has TypeParameterElements
+ // * The sole type argument is the TypeVariable mentioned above
+ //
+ // In this partial code snippet:
+ //
+ // DeclaredType rawArrayListTypeUsage = domain.declaredType(domain.typeElement("java.util.ArrayList"));
+ //
+ // * rawArrayListTypeUsage is a type *usage*
+ // * It has no type arguments at all
+ // * Therefore it is a raw type (a raw type usage)
+ // * Its asElement() returns the TypeElement declared by "public class ArrayList"
+ // * Note that that TypeElement's asType() method does NOT return this type usage, but returns the type
+ // declaration "backing" the TypeElement
+ // * As previously noted it DOES have type arguments
+ //
+ // +---------------------+ +---------------------------------+
+ // | TypeElement (named) <1--declares--1> DeclaredType (type declaration) +-----+
+ // +-----^------------^--+ +---------------------------------+ |type arguments
+ // 1 1 (equal to type parameter count) *
+ // ^ | | +------------------------------+ +-------v------+
+ // uses | +-----------*> TypeParameterElement (named) <1---1> TypeVariable |
+ // | +------------------------------+ +--------------+
+ // +-----+---------------------+ +------------------------------------------------+
+ // | DeclaredType (type usage) +--*> TypeMirror (type argument) |
+ // +---------------------------+ | |
+ // | (one of DeclaredType, ArrayType, TypeVariable) |
+ // | (TypeVariable may be surprising, but consider |
+ // | a type usage in an "extends" or "implements" |
+ // | clause) |
+ // +------------------------------------------------+
+
+ }
+
+ private static class Sup {
+
+ private Sup() {
+ super();
+ }
+
+ }
+
+ private static final class Sub extends Sup {
+
+ private Sub() {
+ super();
+ }
+
+ }
+
+}
diff --git a/src/test/java/org/microbean/event/TestRuntimeClassDivination.java b/src/test/java/org/microbean/event/TestRuntimeClassDivination.java
new file mode 100644
index 0000000..7359e98
--- /dev/null
+++ b/src/test/java/org/microbean/event/TestRuntimeClassDivination.java
@@ -0,0 +1,84 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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.microbean.event;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+
+import java.util.ArrayList;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class TestRuntimeClassDivination {
+
+ private TestRuntimeClassDivination() {
+ super();
+ }
+
+ @Test
+ final void testReflectionAssumptionsAboutRawArrayList() {
+ // Generic superclass is a Class (specifically ArrayList.class); no type arguments
+ assertSame(ArrayList.class, RawArrayList.class.getGenericSuperclass());
+ assertEquals(1, ArrayList.class.getTypeParameters().length);
+ }
+
+ @Test
+ final void testReflectionAssumptionsAboutCookedArrayList() {
+ // Generic superclass is a ParameterizedType (specifically ArrayList); one type argument
+ final ParameterizedType pt = (ParameterizedType)CookedArrayList.class.getGenericSuperclass();
+ assertSame(ArrayList.class, pt.getRawType());
+ final Type[] tas = pt.getActualTypeArguments();
+ assertEquals(1, tas.length);
+ assertSame(String.class, tas[0]);
+ }
+
+ @Test
+ final void testOuterInner() {
+ final Outer.Inner i = new Outer().new Inner();
+ assertSame(Outer.Inner.class, i.getClass());
+ assertSame(Outer.class, Outer.Inner.class.getEnclosingClass());
+ assertSame(this.getClass(), Outer.class.getEnclosingClass());
+
+ // Get ArrayList. Can't get ArrayList.
+ final ParameterizedType innerSupertype = (ParameterizedType)Outer.Inner.class.getGenericSuperclass();
+ assertSame(ArrayList.class, innerSupertype.getRawType());
+ final Type[] innerSupertypeTypeArguments = innerSupertype.getActualTypeArguments();
+ assertEquals(1, innerSupertypeTypeArguments.length);
+ assertTrue(innerSupertypeTypeArguments[0] instanceof TypeVariable>);
+
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static final class RawArrayList extends ArrayList {
+ private static final long serialVersionUID = 1L;
+ }
+
+ private static final class CookedArrayList extends ArrayList {
+ private static final long serialVersionUID = 1L;
+ }
+
+ private static final class Outer {
+ private final class Inner extends ArrayList {
+ private static final long serialVersionUID = 1L;
+ }
+ }
+
+}