This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 174dbdf3af Moving schema annotations into juneau-common
174dbdf3af is described below
commit 174dbdf3afd0ff89712bb9ba80fab433d4ee7b14
Author: James Bognar <[email protected]>
AuthorDate: Mon Apr 13 20:49:59 2026 -0400
Moving schema annotations into juneau-common
---
.../juneau/commons/reflect/AnnotationProvider.java | 20 +-
.../java/org/apache/juneau/annotation/Schema.java | 102 ---------
.../apache/juneau/annotation/SchemaAnnotation.java | 105 +---------
.../org/apache/juneau/annotation/SchemaApply.java | 99 +++++++++
.../juneau/annotation/SchemaApplyAnnotation.java | 231 +++++++++++++++++++++
.../juneau/jsonschema/JsonSchemaGeneratorTest.java | 16 +-
.../annotation/SchemaAnnotation_Test.java | 37 +---
.../annotation/SchemaApplyAnnotation_Test.java | 131 ++++++++++++
todo/decouple-rest-common-from-marshall.md | 32 +--
todo/xapply-annotation-split.md | 230 ++++++++++++++++++++
10 files changed, 740 insertions(+), 263 deletions(-)
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AnnotationProvider.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AnnotationProvider.java
index 8d869155e9..5a10f5c726 100644
---
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AnnotationProvider.java
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AnnotationProvider.java
@@ -345,18 +345,22 @@ public class AnnotationProvider {
try {
var ci = ClassInfo.of(a.getClass());
+ // XApply pattern: if annotation has a
value() returning a single Annotation,
+ // unwrap and store the inner
annotation instead of the wrapper.
+ var annotationToStore =
unwrapXApply(ci, a);
+
ci.getPublicMethod(x ->
x.hasName("onClass")).ifPresent(mi -> {
if (!
mi.getReturnType().is(Class[].class))
throw bex("Invalid
annotation @{0} used in runtime annotations. Annotation must define an
onClass() method that returns a Class array.", cns(a));
for (var c :
(Class<?>[])mi.accessible().invoke(a))
-
runtimeAnnotations.append(c.getName(), a);
+
runtimeAnnotations.append(c.getName(), annotationToStore);
});
ci.getPublicMethod(x ->
x.hasName("on")).ifPresent(mi -> {
if (!
mi.getReturnType().is(String[].class))
throw bex("Invalid
annotation @{0} used in runtime annotations. Annotation must define an on()
method that returns a String array.", cns(a));
for (var s :
(String[])mi.accessible().invoke(a))
-
runtimeAnnotations.append(s, a);
+
runtimeAnnotations.append(s, annotationToStore);
});
} catch (BeanRuntimeException e) {
@@ -368,6 +372,18 @@ public class AnnotationProvider {
return this;
}
+ private static Annotation unwrapXApply(ClassInfo ci, Annotation
a) {
+ var valueMethod = ci.getPublicMethod(x ->
x.hasName("value"));
+ if (valueMethod.isEmpty())
+ return a;
+ var mi = valueMethod.get();
+ var rt = mi.getReturnType().inner();
+ if (! Annotation.class.isAssignableFrom(rt) ||
rt.isArray())
+ return a;
+ var inner = (Annotation) mi.accessible().invoke(a);
+ return inner != null ? inner : a;
+ }
+
/**
* Builds a new {@link AnnotationProvider} instance with the
configured settings.
*
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
index db8539b76b..5151df5dee 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
@@ -96,7 +96,6 @@ import org.apache.juneau.oapi.*;
@Target({ PARAMETER, METHOD, TYPE, FIELD })
@Retention(RUNTIME)
@Repeatable(SchemaAnnotation.Array.class)
-@ContextApply(SchemaAnnotation.Apply.class)
@SuppressWarnings({
"java:S100" // Annotation methods use underscore suffix or $ prefix to
match JSON Schema keywords (e.g., default_, enum_, $ref)
})
@@ -1344,107 +1343,6 @@ public @interface Schema {
*/
String multipleOf() default "";
- /**
- * Dynamically apply this annotation to the specified
classes/methods/fields.
- *
- * <p>
- * Used in conjunction with {@link
org.apache.juneau.BeanContext.Builder#applyAnnotations(Class...)} to
dynamically apply an annotation to an existing class/method/field.
- * It is ignored when the annotation is applied directly to
classes/methods/fields.
- *
- * <h5 class='section'>Valid patterns:</h5>
- * <ul class='spaced-list'>
- * <li>Classes:
- * <ul>
- * <li>Fully qualified:
- * <ul>
- * <li><js>"com.foo.MyClass"</js>
- * </ul>
- * <li>Fully qualified inner class:
- * <ul>
- *
<li><js>"com.foo.MyClass$Inner1$Inner2"</js>
- * </ul>
- * <li>Simple:
- * <ul>
- * <li><js>"MyClass"</js>
- * </ul>
- * <li>Simple inner:
- * <ul>
- *
<li><js>"MyClass$Inner1$Inner2"</js>
- * <li><js>"Inner1$Inner2"</js>
- * <li><js>"Inner2"</js>
- * </ul>
- * </ul>
- * <li>Methods:
- * <ul>
- * <li>Fully qualified with args:
- * <ul>
- *
<li><js>"com.foo.MyClass.myMethod(String,int)"</js>
- *
<li><js>"com.foo.MyClass.myMethod(java.lang.String,int)"</js>
- *
<li><js>"com.foo.MyClass.myMethod()"</js>
- * </ul>
- * <li>Fully qualified:
- * <ul>
- *
<li><js>"com.foo.MyClass.myMethod"</js>
- * </ul>
- * <li>Simple with args:
- * <ul>
- *
<li><js>"MyClass.myMethod(String,int)"</js>
- *
<li><js>"MyClass.myMethod(java.lang.String,int)"</js>
- *
<li><js>"MyClass.myMethod()"</js>
- * </ul>
- * <li>Simple:
- * <ul>
- * <li><js>"MyClass.myMethod"</js>
- * </ul>
- * <li>Simple inner class:
- * <ul>
- *
<li><js>"MyClass$Inner1$Inner2.myMethod"</js>
- *
<li><js>"Inner1$Inner2.myMethod"</js>
- * <li><js>"Inner2.myMethod"</js>
- * </ul>
- * </ul>
- * <li>Fields:
- * <ul>
- * <li>Fully qualified:
- * <ul>
- *
<li><js>"com.foo.MyClass.myField"</js>
- * </ul>
- * <li>Simple:
- * <ul>
- * <li><js>"MyClass.myField"</js>
- * </ul>
- * <li>Simple inner class:
- * <ul>
- *
<li><js>"MyClass$Inner1$Inner2.myField"</js>
- *
<li><js>"Inner1$Inner2.myField"</js>
- * <li><js>"Inner2.myField"</js>
- * </ul>
- * </ul>
- * <li>A comma-delimited list of anything on this list.
- * </ul>
- *
- * <h5 class='section'>See Also:</h5><ul>
- * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations">Dynamically
Applied Annotations</a>
- * </ul>
- *
- * @return The annotation value.
- */
- String[] on() default {};
-
- /**
- * Dynamically apply this annotation to the specified classes.
- *
- * <p>
- * Identical to {@link #on()} except allows you to specify class
objects instead of a strings.
- *
- * <h5 class='section'>See Also:</h5><ul>
- * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations">Dynamically
Applied Annotations</a>
- * </ul>
- *
- * @return The annotation value.
- */
- Class<?>[] onClass() default {};
-
/**
* Synonym for {@link #pattern()}.
*
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
index 8118764641..9f28d7caee 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
@@ -22,17 +22,13 @@ import static
org.apache.juneau.commons.utils.CollectionUtils.*;
import static org.apache.juneau.jsonschema.SchemaUtils.*;
import java.lang.annotation.*;
-import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;
-import org.apache.juneau.*;
import org.apache.juneau.collections.*;
import org.apache.juneau.commons.annotation.*;
-import org.apache.juneau.commons.reflect.*;
import org.apache.juneau.commons.utils.*;
import org.apache.juneau.parser.*;
-import org.apache.juneau.svl.*;
/**
* Utility classes and methods for the {@link Schema @Schema} annotation.
@@ -97,29 +93,6 @@ public class SchemaAnnotation {
*/
private SchemaAnnotation() {}
- /**
- * Applies targeted {@link Schema} annotations to a {@link
org.apache.juneau.Context.Builder}.
- */
- public static class Apply extends
AnnotationApplier<Schema,Context.Builder> {
-
- /**
- * Constructor.
- *
- * @param vr The resolver for resolving values in annotations.
- */
- public Apply(VarResolverSession vr) {
- super(Schema.class, Context.Builder.class, vr);
- }
-
- @Override
- public void apply(AnnotationInfo<Schema> ai, Context.Builder b)
{
- Schema a = ai.inner();
- if (isEmptyArray(a.on()) && isEmptyArray(a.onClass()))
- return;
- b.annotations(a);
- }
- }
-
/**
* A collection of {@link Schema @Schema annotations}.
*/
@@ -147,7 +120,7 @@ public class SchemaAnnotation {
@SuppressWarnings({
"java:S116" // Field names intentionally match JSON property
names
})
- public static class Builder extends AppliedAnnotationObject.BuilderTMF {
+ public static class Builder extends AnnotationObject.Builder {
private boolean aev;
private boolean allowEmptyValue;
@@ -1088,67 +1061,13 @@ public class SchemaAnnotation {
return this;
}
- @Override /* Overridden from AppliedAnnotationObject.Builder */
- public Builder on(String...value) {
- super.on(value);
- return this;
- }
-
- @Override /* Overridden from AppliedAnnotationObject.BuilderT */
- public Builder on(Class<?>...value) {
- super.on(value);
- return this;
- }
-
- @Override /* Overridden from
AppliedOnClassAnnotationObject.Builder */
- public Builder onClass(Class<?>...value) {
- super.onClass(value);
- return this;
- }
-
- @Override /* Overridden from AppliedAnnotationObject.BuilderM */
- public Builder on(Method...value) {
- super.on(value);
- return this;
- }
-
- @Override /* Overridden from AppliedAnnotationObject.BuilderMF
*/
- public Builder on(Field...value) {
- super.on(value);
- return this;
- }
-
- @Override /* Overridden from AppliedAnnotationObject.BuilderT */
- public Builder on(ClassInfo...value) {
- super.on(value);
- return this;
- }
-
- @Override /* Overridden from AppliedAnnotationObject.BuilderT */
- public Builder onClass(ClassInfo...value) {
- super.onClass(value);
- return this;
- }
-
- @Override /* Overridden from AppliedAnnotationObject.BuilderTMF
*/
- public Builder on(FieldInfo...value) {
- super.on(value);
- return this;
- }
-
- @Override /* Overridden from AppliedAnnotationObject.BuilderTMF
*/
- public Builder on(MethodInfo...value) {
- super.on(value);
- return this;
- }
-
}
@SuppressWarnings({
"java:S116", // Field names intentionally match JSON property
names
"java:S2160" // equals() inherited from AnnotationObject
compares all annotation interface methods; subclass fields are accessed via
those methods
})
- private static class Object extends AppliedOnClassAnnotationObject
implements Schema {
+ private static class Object extends AnnotationObject implements Schema {
private final String[] description;
private final boolean aev;
@@ -1787,26 +1706,6 @@ public class SchemaAnnotation {
return new Builder();
}
- /**
- * Instantiates a new builder for this class.
- *
- * @param on The targets this annotation applies to.
- * @return A new builder object.
- */
- public static Builder create(Class<?>...on) {
- return create().on(on);
- }
-
- /**
- * Instantiates a new builder for this class.
- *
- * @param on The targets this annotation applies to.
- * @return A new builder object.
- */
- public static Builder create(String...on) {
- return create().on(on);
- }
-
/**
* Returns <jk>true</jk> if the specified annotation contains all
default values.
*
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApply.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApply.java
new file mode 100644
index 0000000000..446fb4b91d
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApply.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.juneau.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+/**
+ * Dynamically applies a {@link Schema @Schema} annotation to specified
classes, methods, or fields.
+ *
+ * <p>
+ * This annotation separates the <b>targeting</b> concern ({@link
#on()}/{@link #onClass()}) from the
+ * <b>content</b> concern ({@link #value()}), enabling {@link Schema @Schema}
to be a pure data annotation
+ * without marshall-specific application machinery.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <ja>@SchemaApply</ja>(on=<js>"com.example.Foo"</js>,
value=<ja>@Schema</ja>(format=<js>"date-time"</js>))
+ * <jk>public class</jk> MyConfig {}
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations">Dynamically
Applied Annotations</a>
+ * </ul>
+ */
+@Documented
+@Target(TYPE)
+@Retention(RUNTIME)
+@Repeatable(SchemaApply.Array.class)
+@ContextApply(SchemaApplyAnnotation.Applier.class)
+public @interface SchemaApply {
+
+ /**
+ * The {@link Schema @Schema} annotation to apply.
+ *
+ * @return The annotation value.
+ */
+ Schema value();
+
+ /**
+ * Dynamically apply this annotation to the specified
classes/methods/fields.
+ *
+ * <p>
+ * Identifies the targets this annotation applies to using
fully-qualified names.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations">Dynamically
Applied Annotations</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ String[] on() default {};
+
+ /**
+ * Dynamically apply this annotation to the specified classes.
+ *
+ * <p>
+ * Identical to {@link #on()} except allows you to specify class
objects instead of strings.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations">Dynamically
Applied Annotations</a>
+ * </ul>
+ *
+ * @return The annotation value.
+ */
+ Class<?>[] onClass() default {};
+
+ /**
+ * A collection of {@link SchemaApply @SchemaApply annotations}.
+ */
+ @Documented
+ @Target(TYPE)
+ @Retention(RUNTIME)
+ public @interface Array {
+
+ /**
+ * The child annotations.
+ *
+ * @return The annotation value.
+ */
+ SchemaApply[] value();
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApplyAnnotation.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApplyAnnotation.java
new file mode 100644
index 0000000000..7394e8340b
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApplyAnnotation.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.juneau.annotation;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+
+import java.lang.annotation.*;
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.annotation.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Utility classes and methods for the {@link SchemaApply @SchemaApply}
annotation.
+ *
+ */
+public class SchemaApplyAnnotation {
+
+ /**
+ * Prevents instantiation.
+ */
+ private SchemaApplyAnnotation() {}
+
+ /**
+ * Applies targeted {@link SchemaApply} annotations to a {@link
org.apache.juneau.Context.Builder}.
+ *
+ * <p>
+ * Passes the {@link SchemaApply @SchemaApply} annotation through to
the builder's annotation list.
+ * The {@link
org.apache.juneau.commons.reflect.AnnotationProvider.Builder#addRuntimeAnnotations(java.util.List)}
+ * method handles unwrapping the nested {@link Schema @Schema} from
{@link SchemaApply#value()} and registering it
+ * under the targets specified by {@link SchemaApply#on()} and {@link
SchemaApply#onClass()}.
+ */
+ public static class Applier extends
AnnotationApplier<SchemaApply,Context.Builder> {
+
+ /**
+ * Constructor.
+ *
+ * @param vr The resolver for resolving values in annotations.
+ */
+ public Applier(VarResolverSession vr) {
+ super(SchemaApply.class, Context.Builder.class, vr);
+ }
+
+ @Override
+ public void apply(AnnotationInfo<SchemaApply> ai,
Context.Builder b) {
+ SchemaApply a = ai.inner();
+ if (isEmptyArray(a.on()) && isEmptyArray(a.onClass()))
+ return;
+ b.annotations(a);
+ }
+ }
+
+ /**
+ * Builder class.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='jm'>{@link
org.apache.juneau.BeanContext.Builder#annotations(Annotation...)}
+ * </ul>
+ */
+ public static class Builder extends AppliedAnnotationObject.BuilderTMF {
+
+ Schema value = SchemaAnnotation.DEFAULT;
+
+ /**
+ * Constructor.
+ */
+ protected Builder() {
+ super(SchemaApply.class);
+ }
+
+ /**
+ * Sets the {@link SchemaApply#value()} property on this
annotation.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public Builder value(Schema value) {
+ this.value = value;
+ return this;
+ }
+
+ @Override /* Overridden from AppliedAnnotationObject.Builder */
+ public Builder on(String...value) {
+ super.on(value);
+ return this;
+ }
+
+ @Override /* Overridden from AppliedAnnotationObject.BuilderT */
+ public Builder on(Class<?>...value) {
+ super.on(value);
+ return this;
+ }
+
+ @Override /* Overridden from
AppliedOnClassAnnotationObject.Builder */
+ public Builder onClass(Class<?>...value) {
+ super.onClass(value);
+ return this;
+ }
+
+ @Override /* Overridden from AppliedAnnotationObject.BuilderM */
+ public Builder on(Method...value) {
+ super.on(value);
+ return this;
+ }
+
+ @Override /* Overridden from AppliedAnnotationObject.BuilderMF
*/
+ public Builder on(Field...value) {
+ super.on(value);
+ return this;
+ }
+
+ @Override /* Overridden from AppliedAnnotationObject.BuilderT */
+ public Builder on(ClassInfo...value) {
+ super.on(value);
+ return this;
+ }
+
+ @Override /* Overridden from AppliedAnnotationObject.BuilderT */
+ public Builder onClass(ClassInfo...value) {
+ super.onClass(value);
+ return this;
+ }
+
+ @Override /* Overridden from AppliedAnnotationObject.BuilderTMF
*/
+ public Builder on(FieldInfo...value) {
+ super.on(value);
+ return this;
+ }
+
+ @Override /* Overridden from AppliedAnnotationObject.BuilderTMF
*/
+ public Builder on(MethodInfo...value) {
+ super.on(value);
+ return this;
+ }
+
+ /**
+ * Instantiates a new {@link SchemaApply @SchemaApply} object
initialized with this builder.
+ *
+ * @return A new {@link SchemaApply} object.
+ */
+ public SchemaApply build() {
+ return new Object(this);
+ }
+ }
+
+ @SuppressWarnings({
+ "java:S2160" // equals() inherited from AnnotationObject
compares all annotation interface methods; subclass fields are accessed via
those methods
+ })
+ private static class Object extends AppliedOnClassAnnotationObject
implements SchemaApply {
+
+ private final Schema value;
+
+ Object(SchemaApplyAnnotation.Builder b) {
+ super(b);
+ value = b.value;
+ }
+
+ @Override /* Overridden from SchemaApply */
+ public Schema value() {
+ return value;
+ }
+
+ @Override /* Overridden from SchemaApply */
+ public String[] on() {
+ return super.on();
+ }
+
+ @Override /* Overridden from SchemaApply */
+ public Class<?>[] onClass() {
+ return super.onClass();
+ }
+ }
+
+ /** Default value */
+ public static final SchemaApply DEFAULT = create().build();
+
+ /**
+ * Instantiates a new builder for this class.
+ *
+ * @return A new builder object.
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ /**
+ * Instantiates a new builder for this class.
+ *
+ * @param on The targets this annotation applies to.
+ * @return A new builder object.
+ */
+ public static Builder create(Class<?>...on) {
+ return create().on(on);
+ }
+
+ /**
+ * Instantiates a new builder for this class.
+ *
+ * @param on The targets this annotation applies to.
+ * @return A new builder object.
+ */
+ public static Builder create(String...on) {
+ return create().on(on);
+ }
+
+ /**
+ * Returns <jk>true</jk> if the specified annotation contains all
default values.
+ *
+ * @param a The annotation to check.
+ * @return <jk>true</jk> if the specified annotation contains all
default values.
+ */
+ public static boolean empty(SchemaApply a) {
+ return a == null || DEFAULT.equals(a);
+ }
+}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
index 44f7174a0f..c9383722a1 100755
---
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
@@ -1168,9 +1168,9 @@ class JsonSchemaGeneratorTest extends TestBase {
assertJson("{description:'baz',format:'bar',type:'foo',properties:{f1:{type:'integer',format:'int32'}}}",
s.getSchema(A1a.class));
}
- @Schema(on="Dummy1",type="foo",format="bar",description="baz")
- @Schema(on="A1a",type="foo",format="bar",description="baz")
- @Schema(on="Dummy2",type="foo",format="bar",description="baz")
+
@SchemaApply(on="Dummy1",value=@Schema(type="foo",format="bar",description="baz"))
+
@SchemaApply(on="A1a",value=@Schema(type="foo",format="bar",description="baz"))
+
@SchemaApply(on="Dummy2",value=@Schema(type="foo",format="bar",description="baz"))
private static class A1aConfig {}
public static class A1a {
@@ -1192,7 +1192,7 @@ class JsonSchemaGeneratorTest extends TestBase {
assertJson("{type:'object',properties:{f1:{description:'baz',format:'bar',type:'foo'}}}",
s.getSchema(A2a.class));
}
- @Schema(on="A2a.f1",type="foo",format="bar",description="baz")
+
@SchemaApply(on="A2a.f1",value=@Schema(type="foo",format="bar",description="baz"))
private static class A2aConfig {}
public static class A2a {
@@ -1214,7 +1214,7 @@ class JsonSchemaGeneratorTest extends TestBase {
assertJson("{type:'object',properties:{f1:{description:'baz',format:'bar',type:'foo'}}}",
s.getSchema(A3a.class));
}
- @Schema(on="A3a.getF1",type="foo",format="bar",description="baz")
+
@SchemaApply(on="A3a.getF1",value=@Schema(type="foo",format="bar",description="baz"))
private static class A3aConfig {}
public static class A3a {
@@ -1238,7 +1238,7 @@ class JsonSchemaGeneratorTest extends TestBase {
assertJson("{type:'object',properties:{f1:{description:'baz',format:'bar',type:'foo'}}}",
s.getSchema(A4a.class));
}
- @Schema(on="A4a.setF1",type="foo",format="bar",description="baz")
+
@SchemaApply(on="A4a.setF1",value=@Schema(type="foo",format="bar",description="baz"))
private static class A4aConfig {}
public static class A4a {
@@ -1272,7 +1272,7 @@ class JsonSchemaGeneratorTest extends TestBase {
assertJson("{type:'array',items:{type:'array',items:{description:'baz',format:'bar',type:'foo'}}}",
s.getSchema(SimpleBean[][].class));
}
- @Schema(on="SwapWithAnnotation2",
type="foo",format="bar",description="baz")
+ @SchemaApply(on="SwapWithAnnotation2",
value=@Schema(type="foo",format="bar",description="baz"))
private static class SwapWithAnnotation2Config {}
public static class SwapWithAnnotation2 extends
ObjectSwap<SimpleBean,Integer> {}
@@ -1281,7 +1281,7 @@ class JsonSchemaGeneratorTest extends TestBase {
// @JsonSchema on ObjectSwap
//====================================================================================================
- @Schema(onClass=B.class,$ref="ref")
+ @SchemaApply(onClass=B.class, value=@Schema($ref="ref"))
static class BConfig {}
static class B {}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
index 2f7f62702f..2bd1fb55ac 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
@@ -30,10 +30,6 @@ import org.junit.jupiter.api.*;
})
class SchemaAnnotation_Test extends TestBase {
- private static final String CNAME =
SchemaAnnotation_Test.class.getName();
-
- private static class X1 {}
-
//------------------------------------------------------------------------------------------------------------------
// Basic tests
//------------------------------------------------------------------------------------------------------------------
@@ -78,8 +74,6 @@ class SchemaAnnotation_Test extends TestBase {
.minProperties(12)
.mo("s")
.multipleOf("t")
- .on("u")
- .onClass(X1.class)
.p("v")
.pattern("w")
.properties("x")
@@ -135,8 +129,6 @@ class SchemaAnnotation_Test extends TestBase {
.minProperties(12)
.mo("s")
.multipleOf("t")
- .on("u")
- .onClass(X1.class)
.p("v")
.pattern("w")
.properties("x")
@@ -154,8 +146,8 @@ class SchemaAnnotation_Test extends TestBase {
@Test void a01_basic() {
assertBean(a1,
-
"$ref,default_,enum_,additionalProperties,aev,allOf,allowEmptyValue,cf,collectionFormat,d,description,df,discriminator,e,emax,emin,exclusiveMaximum,exclusiveMinimum,externalDocs{description,url},f,format,ignore,items{$ref,default_,enum_,cf,collectionFormat,description,df,e,emax,emin,exclusiveMaximum,exclusiveMinimum,f,format,items{$ref,default_,enum_,cf,collectionFormat,description,df,e,emax,emin,exclusiveMaximum,exclusiveMinimum,f,format,items,max,maxItems,maxLength,maxi,maximum,maxl
[...]
-
"c,[a],[b],[d],false,[e],false,f,g,[h],[i],[j],k,[l],true,true,true,true,{[],},m,n,true,{,[],[],,,[],[],[],false,false,false,false,,,{,[],[],,,[],[],[],false,false,false,false,,,[],,-1,-1,-1,,-1,,-1,-1,-1,,-1,,,,,,,false,false},,-1,-1,-1,,-1,,-1,-1,-1,,-1,,,,,,,false,false},o,2,4,6,1,p,3,5,q,8,10,12,7,r,9,11,s,t,[u],[X1],v,w,[x],true,true,true,true,false,false,z,aa,bb,true,true,[cc]");
+
"$ref,default_,enum_,additionalProperties,aev,allOf,allowEmptyValue,cf,collectionFormat,d,description,df,discriminator,e,emax,emin,exclusiveMaximum,exclusiveMinimum,externalDocs{description,url},f,format,ignore,items{$ref,default_,enum_,cf,collectionFormat,description,df,e,emax,emin,exclusiveMaximum,exclusiveMinimum,f,format,items{$ref,default_,enum_,cf,collectionFormat,description,df,e,emax,emin,exclusiveMaximum,exclusiveMinimum,f,format,items,max,maxItems,maxLength,maxi,maximum,maxl
[...]
+
"c,[a],[b],[d],false,[e],false,f,g,[h],[i],[j],k,[l],true,true,true,true,{[],},m,n,true,{,[],[],,,[],[],[],false,false,false,false,,,{,[],[],,,[],[],[],false,false,false,false,,,[],,-1,-1,-1,,-1,,-1,-1,-1,,-1,,,,,,,false,false},,-1,-1,-1,,-1,,-1,-1,-1,,-1,,,,,,,false,false},o,2,4,6,1,p,3,5,q,8,10,12,7,r,9,11,s,t,v,w,[x],true,true,true,true,false,false,z,aa,bb,true,true,[cc]");
}
@Test void a02_testEquivalency() {
@@ -178,27 +170,6 @@ class SchemaAnnotation_Test extends TestBase {
// Other methods.
//------------------------------------------------------------------------------------------------------------------
- public static class C1 {
- public int f1;
- public void m1() {}
- }
- public static class C2 {
- public int f2;
- public void m2() {}
- }
-
- @Test void c01_otherMethods() throws Exception {
- var c1 = SchemaAnnotation.create(C1.class).on(C2.class).build();
- var c2 = SchemaAnnotation.create("a").on("b").build();
- var c3 =
SchemaAnnotation.create().on(C1.class.getField("f1")).on(C2.class.getField("f2")).build();
- var c4 =
SchemaAnnotation.create().on(C1.class.getMethod("m1")).on(C2.class.getMethod("m2")).build();
-
- assertBean(c1, "on", "["+CNAME+"$C1,"+CNAME+"$C2]");
- assertBean(c2, "on", "[a,b]");
- assertBean(c3, "on", "["+CNAME+"$C1.f1,"+CNAME+"$C2.f2]");
- assertBean(c4, "on", "["+CNAME+"$C1.m1(),"+CNAME+"$C2.m2()]");
- }
-
//------------------------------------------------------------------------------------------------------------------
// Comparison with declared annotations.
//------------------------------------------------------------------------------------------------------------------
@@ -243,8 +214,6 @@ class SchemaAnnotation_Test extends TestBase {
minProperties=12,
mo="s",
multipleOf="t",
- on="u",
- onClass=X1.class,
p="v",
pattern="w",
properties="x",
@@ -302,8 +271,6 @@ class SchemaAnnotation_Test extends TestBase {
minProperties=12,
mo="s",
multipleOf="t",
- on="u",
- onClass=X1.class,
p="v",
pattern="w",
properties="x",
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaApplyAnnotation_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaApplyAnnotation_Test.java
new file mode 100644
index 0000000000..a710d6a0a4
--- /dev/null
+++
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaApplyAnnotation_Test.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.juneau.jsonschema.annotation;
+
+import static org.apache.juneau.TestUtils.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.junit.jupiter.api.*;
+
+@SuppressWarnings({
+ "java:S1186" // Empty test method intentional for framework testing
+})
+class SchemaApplyAnnotation_Test extends TestBase {
+
+ private static final String CNAME =
SchemaApplyAnnotation_Test.class.getName();
+
+ private static class X1 {}
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Basic tests
+
//------------------------------------------------------------------------------------------------------------------
+
+ SchemaApply a1 = SchemaApplyAnnotation.create()
+ .on("u")
+ .onClass(X1.class)
+ .value(SchemaAnnotation.create().format("date-time").build())
+ .build();
+
+ SchemaApply a2 = SchemaApplyAnnotation.create()
+ .on("u")
+ .onClass(X1.class)
+ .value(SchemaAnnotation.create().format("date-time").build())
+ .build();
+
+ @Test void a01_basic() {
+ assertBean(a1, "on,onClass,value{format}",
"[u],[X1],{date-time}");
+ }
+
+ @Test void a02_testEquivalency() {
+ assertEquals(a2, a1);
+ assertNotEqualsAny(a1.hashCode(), 0, -1);
+ assertEquals(a1.hashCode(), a2.hashCode());
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // PropertyStore equivalency.
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void b01_testEquivalencyInPropertyStores() {
+ var bc1 = BeanContext.create().annotations(a1).build();
+ var bc2 = BeanContext.create().annotations(a2).build();
+ assertSame(bc1, bc2);
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Targeting methods
+
//------------------------------------------------------------------------------------------------------------------
+
+ public static class C1 {
+ public int f1;
+ public void m1() {}
+ }
+ public static class C2 {
+ public int f2;
+ public void m2() {}
+ }
+
+ @Test void c01_targetingMethods() throws Exception {
+ var c1 =
SchemaApplyAnnotation.create(C1.class).on(C2.class).build();
+ var c2 = SchemaApplyAnnotation.create("a").on("b").build();
+ var c3 =
SchemaApplyAnnotation.create().on(C1.class.getField("f1")).on(C2.class.getField("f2")).build();
+ var c4 =
SchemaApplyAnnotation.create().on(C1.class.getMethod("m1")).on(C2.class.getMethod("m2")).build();
+
+ assertBean(c1, "on", "["+CNAME+"$C1,"+CNAME+"$C2]");
+ assertBean(c2, "on", "[a,b]");
+ assertBean(c3, "on", "["+CNAME+"$C1.f1,"+CNAME+"$C2.f2]");
+ assertBean(c4, "on", "["+CNAME+"$C1.m1(),"+CNAME+"$C2.m2()]");
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Comparison with declared annotations.
+
//------------------------------------------------------------------------------------------------------------------
+
+ @SchemaApply(on="u", onClass=X1.class,
value=@Schema(format="date-time"))
+ public static class D1 {}
+ SchemaApply d1 = D1.class.getAnnotationsByType(SchemaApply.class)[0];
+
+ @SchemaApply(on="u", onClass=X1.class,
value=@Schema(format="date-time"))
+ public static class D2 {}
+ SchemaApply d2 = D2.class.getAnnotationsByType(SchemaApply.class)[0];
+
+ @Test void d01_comparisonWithDeclarativeAnnotations() {
+ assertEqualsAll(a1, d1, d2);
+ assertNotEqualsAny(a1.hashCode(), 0, -1);
+ assertEqualsAll(a1.hashCode(), d1.hashCode(), d2.hashCode());
+ }
+
+
//------------------------------------------------------------------------------------------------------------------
+ // Dynamic application via BeanContext
+
//------------------------------------------------------------------------------------------------------------------
+
+ @Test void e01_dynamicApplication() {
+ var schema =
SchemaAnnotation.create().type("string").format("date-time").build();
+ var apply =
SchemaApplyAnnotation.create("com.example.Foo").value(schema).build();
+
+ assertBean(apply, "on,value{type,format}",
"[com.example.Foo],{string,date-time}");
+ }
+
+ @Test void e02_emptyCheck() {
+ assertTrue(SchemaApplyAnnotation.empty(null));
+
assertTrue(SchemaApplyAnnotation.empty(SchemaApplyAnnotation.DEFAULT));
+ assertFalse(SchemaApplyAnnotation.empty(a1));
+ }
+}
diff --git a/todo/decouple-rest-common-from-marshall.md
b/todo/decouple-rest-common-from-marshall.md
index 2ea57815fe..3af7ed1347 100644
--- a/todo/decouple-rest-common-from-marshall.md
+++ b/todo/decouple-rest-common-from-marshall.md
@@ -33,21 +33,26 @@ juneau-commons -> juneau-rest-common -> juneau-marshall ->
juneau-rest-client
## Phase 2 — Move `@Schema` and related annotations to `juneau-commons`
**Difficulty**: Hard
-**Impact**: 83 files (7 HTTP annotations + Swagger model classes)
+**Impact**: Large (HTTP annotations, Swagger models)
+
+**Prerequisite**: The global `XApply` annotation split must be completed
first. See [`xapply-annotation-split.md`](xapply-annotation-split.md) for the
full plan covering the removal of `on`/`onClass` from all 30 context-appliable
annotations into companion `@XApply` annotations.
`@Schema` is the #1 blocker. Every HTTP annotation references it via `Schema
schema() default @Schema`. `InvalidAnnotationException` is also used.
-### Classes to move
-- `org.apache.juneau.annotation.Schema` →
`org.apache.juneau.commons.annotation.Schema`
-- `org.apache.juneau.annotation.Items` →
`org.apache.juneau.commons.annotation.Items`
-- `org.apache.juneau.annotation.SubItems` →
`org.apache.juneau.commons.annotation.SubItems`
-- `org.apache.juneau.annotation.ExternalDocs` →
`org.apache.juneau.commons.annotation.ExternalDocs`
-- `InvalidAnnotationException`
+### Classes to move (commons)
+
+- `org.apache.juneau.annotation.Schema` ->
`org.apache.juneau.commons.annotation.Schema` (no
`on`/`onClass`/`@ContextApply` after XApply split)
+- `SchemaAnnotation` -> commons **minus** apply-only / marshall-only inner
types
+- `org.apache.juneau.annotation.Items` ->
`org.apache.juneau.commons.annotation.Items`
+- `org.apache.juneau.annotation.SubItems` ->
`org.apache.juneau.commons.annotation.SubItems`
+- `org.apache.juneau.annotation.ExternalDocs` ->
`org.apache.juneau.commons.annotation.ExternalDocs`
+- `InvalidAnnotationException` (and any other schema-adjacent types that must
compile without marshall)
### Considerations
-- `SchemaAnnotation` (empty(), DEFAULT, builder utilities) must also move or
be split
-- `@Schema` references `JsonSchemaSerializer` in javadoc — javadoc link would
break
-- `HttpPartSchema` reads `@Schema` attributes — must check if `HttpPartSchema`
can also move or if reflective access suffices
+
+- **`@Schema` Javadoc** references marshall-only types (e.g.
`JsonSchemaSerializer`) -- replace with neutral text or doclinks that do not
create a compile dependency from commons to marshall.
+- **`HttpPartSchema`** reads `@Schema` -- after the move, references
**`org.apache.juneau.commons.annotation.Schema`** (Phase 3 may move
`HttpPartSchema` itself to commons).
+- **Repeatability**: `@Schema` keeps its own `@Repeatable(Schema.Array.class)`
for inline use; `@SchemaApply` has its own `@Repeatable` container.
---
@@ -70,7 +75,7 @@ juneau-commons -> juneau-rest-common -> juneau-marshall ->
juneau-rest-client
- `HttpPartMarshalling` (references serializer/parser types)
### Considerations
-- `HttpPartSchema.Builder` currently has `apply(HttpPartMarshalling)` which
reads `serializer()`/`parser()` — this creates a dependency on marshall. Could
be split: schema model in commons, marshalling-aware builder extension in
marshall.
+- `HttpPartSchema.Builder` currently has `apply(HttpPartMarshalling)` which
reads `serializer()`/`parser()` -- this creates a dependency on marshall. Could
be split: schema model in commons, marshalling-aware builder extension in
marshall.
---
@@ -79,7 +84,7 @@ juneau-commons -> juneau-rest-common -> juneau-marshall ->
juneau-rest-client
**Difficulty**: Medium
**Impact**: 3 files (SerializedHeader, SerializedPart, SerializedEntity) + 3
factory helpers
-These classes have the heaviest marshall dependencies — they use `httppart`,
`oapi`, `serializer`, and `urlencoding` packages. They bridge the gap between
REST-common HTTP types and marshall serialization.
+These classes have the heaviest marshall dependencies -- they use `httppart`,
`oapi`, `serializer`, and `urlencoding` packages. They bridge the gap between
REST-common HTTP types and marshall serialization.
### Options
1. **Move to marshall**: These classes logically belong in the serialization
layer
@@ -114,7 +119,7 @@ Move `SerializedHeader`, `SerializedPart`,
`SerializedEntity` and their factory
| Priority | Package | Files | Target module | Status |
|----------|---------|-------|----------------|--------|
| 1 | `MediaType`, `MediaRanges`, `StringRanges`, etc. | many | juneau-commons
(`org.apache.juneau.commons.http`) | Done |
-| 2 | `@Schema`, `@Items`, `@SubItems` | 83 | juneau-commons | Pending |
+| 2 | `@Schema`, `@Items`, etc. + `@XApply` in marshall | 83+ | commons +
marshall (`@SchemaApply`, etc.) | Pending |
| 3 | `HttpPartSchema`, `HttpPartType` | 12 | juneau-commons | Pending |
| 4 | `Serialized*` bridge classes | 6 | juneau-marshall or bridge | Pending |
| 5 | `VarResolverSession`, `BeanCreator` | 5 | juneau-commons or optional |
Pending |
@@ -124,4 +129,5 @@ Move `SerializedHeader`, `SerializedPart`,
`SerializedEntity` and their factory
- **Split-package**: HTTP media types moved out of `org.apache.juneau` into
`org.apache.juneau.commons.http`
- **Binary compatibility**: All moves require recompilation of downstream
modules
- **Schema complexity**: `@Schema` has ~50 attributes and deep integration
with serializers
+- **Breaking `XApply` migration**: Replacing `on`/`onClass` on
context-appliable annotations touches many call sites; plan for a single major
release boundary and migration notes
- **Incremental approach**: Each phase should be self-contained and buildable
independently
diff --git a/todo/xapply-annotation-split.md b/todo/xapply-annotation-split.md
new file mode 100644
index 0000000000..1e4e4290e8
--- /dev/null
+++ b/todo/xapply-annotation-split.md
@@ -0,0 +1,230 @@
+# Split `on`/`onClass` into separate `@XApply` annotations
+
+## Goal
+
+Remove `on()` and `onClass()` from all context-appliable annotations and move
them into companion `@XApply` annotations. This separates annotation
**content** (what to configure) from **targeting** (where to apply it),
enabling annotations like `@Schema` to live in `juneau-commons` while dynamic
application machinery stays in `juneau-marshall`.
+
+## Background
+
+Today, annotations like `@Schema`, `@Bean`, `@Json`, etc. serve two roles:
+
+1. **Inline declaration** — placed directly on a class/method/field, no
`on`/`onClass` set.
+2. **Dynamic application** — placed on a config class with `on`/`onClass` set,
applied later via `BeanContext.Builder.applyAnnotations()`.
+
+The `@ContextApply` meta-annotation and `AnnotationApplier` machinery in
marshall handles role 2. The applier checks `on`/`onClass`; if populated, it
stores the annotation for later target matching.
+
+**Problem:** `on`/`onClass` and `@ContextApply` are **marshall-only**
concepts, but they live on annotations that should be usable in
`juneau-commons` or `juneau-rest-common` without pulling in marshall.
+
+## Design
+
+### Before (current)
+
+```java
+@Schema(on = "com.example.Foo", format = "date-time")
+@Bean(onClass = Foo.class, sort = true)
+public class MyConfig {}
+```
+
+### After (proposed)
+
+```java
+@SchemaApply(on = "com.example.Foo", value = @Schema(format = "date-time"))
+@BeanApply(onClass = Foo.class, value = @Bean(sort = true))
+public class MyConfig {}
+```
+
+- `@Schema` has **no** `on`, `onClass`, or `@ContextApply` — it is a pure data
annotation.
+- `@SchemaApply` lives in marshall, carries `on`/`onClass` and
`@ContextApply`, and wraps the annotation via a `value()` member.
+- Breaking change: existing `@Schema(on = ...)` usages must be rewritten.
+
+---
+
+## Annotations in scope
+
+### Group A: `org.apache.juneau.annotation` — both `on()` and `onClass()`
+
+These have the `AppliedOnClassAnnotationObject` builder base and target types
(classes).
+
+| Annotation | Builder base | Applier target | Companion file |
+|---|---|---|---|
+| `@Schema` | `BuilderTMF` | `Context.Builder` | `SchemaAnnotation.java` |
+| `@Swap` | `BuilderTMF` | `BeanContext.Builder` | `SwapAnnotation.java` |
+| `@Bean` | `BuilderT` | `BeanContext.Builder` | `BeanAnnotation.java` |
+| `@BeanIgnore` | `BuilderTMFC` | `BeanContext.Builder` |
`BeanIgnoreAnnotation.java` |
+| `@Uri` | `BuilderTMF` | `BeanContext.Builder` | `UriAnnotation.java` |
+| `@Marshalled` | `BuilderT` | `BeanContext.Builder` |
`MarshalledAnnotation.java` |
+| `@Example` | `BuilderTMF` | `BeanContext.Builder` | `ExampleAnnotation.java`
|
+
+### Group B: `org.apache.juneau.annotation` — `on()` only (no `onClass()`)
+
+These have the `AppliedAnnotationObject` builder base and target
methods/fields/constructors only.
+
+| Annotation | Builder base | Companion file |
+|---|---|---|
+| `@Beanp` | `BuilderMF` | `BeanpAnnotation.java` |
+| `@Beanc` | `BuilderC` | `BeancAnnotation.java` |
+| `@ParentProperty` | `BuilderMF` | `ParentPropertyAnnotation.java` |
+| `@NameProperty` | `BuilderMF` | `NamePropertyAnnotation.java` |
+
+### Group C: Format-specific annotations (marshall subpackages) — both `on()`
and `onClass()`
+
+| Annotation | Package |
+|---|---|
+| `@Json` | `json/annotation` |
+| `@Xml` | `xml/annotation` |
+| `@Html` | `html/annotation` |
+| `@HtmlLink` | `html/annotation` |
+| `@Csv` | `csv/annotation` |
+| `@Uon` | `uon/annotation` |
+| `@UrlEncoding` | `urlencoding/annotation` |
+| `@MsgPack` | `msgpack/annotation` |
+| `@OpenApi` | `oapi/annotation` |
+| `@PlainText` | `plaintext/annotation` |
+| `@SoapXml` | `soap/annotation` |
+| `@Cbor` | `cbor/annotation` |
+| `@Bson` | `bson/annotation` |
+| `@Proto` | `proto/annotation` |
+| `@Hjson` | `hjson/annotation` |
+| `@Hocon` | `hocon/annotation` |
+| `@Ini` | `ini/annotation` |
+| `@Markdown` | `markdown/annotation` |
+| `@Parquet` | `parquet/annotation` |
+
+### Group D: marshall-rdf
+
+| Annotation | Package |
+|---|---|
+| `@Rdf` | `jena/annotation` |
+
+### Group E: REST annotations (juneau-rest-server)
+
+These annotations also have `on()`/`onClass()` but use a different application
mechanism (RestContext, not BeanContext). They are **out of scope** for this
plan — their `on`/`onClass` serve a different purpose (REST method/class
targeting) and do not need the `XApply` split.
+
+| Annotation |
+|---|
+| `@Rest`, `@RestGet`, `@RestPut`, `@RestPost`, `@RestDelete`, `@RestPatch`,
`@RestOptions`, `@RestOp` |
+| `@RestStartCall`, `@RestPreCall`, `@RestPostCall`, `@RestEndCall`,
`@RestInit`, `@RestPostInit`, `@RestDestroy` |
+
+Note: REST operation annotations (`@Rest` through `@RestOp`) have
`@ContextApply` but their appliers configure the REST context, not
`BeanContext`. Their `on()` identifies which REST class/method the annotation
applies to. REST lifecycle annotations (`@RestStartCall` through
`@RestDestroy`) have `on()` but no `@ContextApply`.
+
+---
+
+## Implementation plan
+
+### Step 1: Create the `@XApply` template pattern
+
+Define the structural pattern that all `@XApply` annotations will follow.
+
+For an annotation `@X` with both `on` and `onClass`:
+
+```java
+// In the same package as the original @X (stays in marshall)
+@Documented
+@Target(TYPE)
+@Retention(RUNTIME)
+@Repeatable(XApply.Array.class)
+@ContextApply(XApplyAnnotation.Applier.class)
+public @interface XApply {
+ X value(); // the wrapped annotation
+ String[] on() default {};
+ Class<?>[] onClass() default {};
+
+ @Documented
+ @Target(TYPE)
+ @Retention(RUNTIME)
+ public @interface Array {
+ XApply[] value();
+ }
+}
+```
+
+For `@X` with `on()` only (no `onClass`):
+
+```java
+@Documented
+@Target(TYPE)
+@Retention(RUNTIME)
+@Repeatable(XApply.Array.class)
+@ContextApply(XApplyAnnotation.Applier.class)
+public @interface XApply {
+ X value();
+ String[] on() default {};
+
+ @Documented @Target(TYPE) @Retention(RUNTIME)
+ public @interface Array { XApply[] value(); }
+}
+```
+
+### Step 2: Create `XApplyAnnotation` companion classes
+
+Each `@XApply` gets a companion `XApplyAnnotation.java` with:
+
+- Builder class extending the appropriate `AppliedAnnotationObject` or
`AppliedOnClassAnnotationObject` builder base
+- `Applier` inner class that:
+ 1. Reads `on`/`onClass` from the `@XApply`
+ 2. Reads the nested `@X` annotation
+ 3. Calls `b.annotations(...)` to register the nested annotation against the
targets
+- `copy()` method for VarResolver resolution
+- `DEFAULT`, `create()`, `Array` container
+
+The applier logic is essentially the same as today's applier, just relocated
from `XAnnotation.Apply` to `XApplyAnnotation.Applier`.
+
+### Step 3: Strip `on`/`onClass`/`@ContextApply` from `@X` annotations
+
+For each annotation in Groups A-D:
+
+1. Remove `String[] on() default {}` attribute
+2. Remove `Class<?>[] onClass() default {}` attribute (where present)
+3. Remove `@ContextApply(XAnnotation.Applier.class)` meta-annotation
+4. Remove the `Applier`/`Apply` inner class from `XAnnotation.java`
+5. Remove `on`/`onClass` from the `XAnnotation.java` Builder class
+6. Change builder base from `AppliedOnClassAnnotationObject.BuilderTMF` to a
non-targeting base
+7. Change private Object class from `AppliedOnClassAnnotationObject` to base
`AnnotationObject` or similar
+8. Keep `@Repeatable(XAnnotation.Array.class)` on `@X` itself (repeatability
for inline use is still valid)
+
+### Step 4: Migrate all call sites
+
+For each usage of `@X(on = ..., ...)` or `@X(onClass = ..., ...)` in the
codebase:
+
+1. **Declarative (annotation on class):** Rewrite to `@XApply(on = ..., value
= @X(...))`
+2. **Programmatic (builder API):** Rewrite from
`XAnnotation.create().on(...).build()` to
`XApplyAnnotation.create().on(...).value(XAnnotation.create()...build()).build()`
+
+### Step 5: Update tests
+
+Every `*Annotation_Test.java` file needs updates for the new structure. Also
update integration tests that use `on`/`onClass` declaratively.
+
+### Step 6: Update `AnnotationWorkList` and `Context.Builder`
+
+The annotation discovery machinery (`CONTEXT_APPLY_FILTER`, `traverse()`,
`applyAnnotation()`) should work unchanged because `@XApply` carries
`@ContextApply` and its applier handles the nesting. Verify this with tests.
+
+### Step 7: Release notes
+
+Document the breaking change with before/after examples for common patterns.
+
+---
+
+## Ordering
+
+This work is a **prerequisite** for Phase 2 of
`decouple-rest-common-from-marshall.md` (moving `@Schema` to commons). However,
it can be done incrementally:
+
+1. **Start with one annotation** (e.g. `@Schema`) as a proof-of-concept
+2. **Sweep remaining Group A** annotations
+3. **Sweep Group B** (`on()`-only annotations)
+4. **Sweep Group C** (format-specific annotations)
+5. **Sweep Group D** (`@Rdf`)
+
+Each step should compile and pass tests independently.
+
+---
+
+## Files affected (estimated)
+
+| Category | Count |
+|---|---|
+| New `@XApply` annotation files | ~30 |
+| New `XApplyAnnotation.java` companion files | ~30 |
+| Modified `@X` annotation files (strip `on`/`onClass`) | ~30 |
+| Modified `XAnnotation.java` files (remove Applier) | ~30 |
+| Migrated call sites (declarative `on`/`onClass` usage) | ~80 |
+| Updated test files | ~40 |
+| Release notes | 1 |