From f1184131128ca82a535e642b753d6b7021d85e4e Mon Sep 17 00:00:00 2001 From: Andrew Bayer Date: Wed, 3 Oct 2018 15:23:26 -0400 Subject: [PATCH 1/3] [JENKINS-53825] Choose from ambiguous class names via a simple heuristic If there are multiple possible classes matching a name on resolution, filter for just classes that are either inner classes of the model class, are in the same package as the model class, or are in subpackages of the model class's package. If more than one class passes those filters, we'll still fail. Also starts reporting all possible clases when there are more than two of them. --- .../structs/describable/DescribableModel.java | 44 ++++++-- .../UninstantiatedDescribable.java | 2 +- .../describable/AbstractSecondSharedName.java | 30 +++++ .../describable/AbstractThirdSharedName.java | 30 +++++ .../describable/DescribableModelTest.java | 101 ++++++++++++++++- .../describable/UnambiguousClassName.java | 5 + .../first/NarrowAmbiguousArrayContainer.java | 59 ++++++++++ .../first/NarrowAmbiguousContainer.java | 103 ++++++++++++++++++ .../first/NarrowAmbiguousListContainer.java | 56 ++++++++++ .../structs/describable/first/SharedName.java | 7 +- .../first/second/SecondSharedName.java | 78 +++++++++++++ .../describable/second/SharedName.java | 7 +- .../structs/third/SecondSharedName.java | 53 +++++++++ .../structs/third/ThirdSharedName.java | 53 +++++++++ 14 files changed, 611 insertions(+), 17 deletions(-) create mode 100644 plugin/src/test/java/org/jenkinsci/plugins/structs/describable/AbstractSecondSharedName.java create mode 100644 plugin/src/test/java/org/jenkinsci/plugins/structs/describable/AbstractThirdSharedName.java create mode 100644 plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousArrayContainer.java create mode 100644 plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousContainer.java create mode 100644 plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousListContainer.java create mode 100644 plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/second/SecondSharedName.java create mode 100644 plugin/src/test/java/org/jenkinsci/plugins/structs/third/SecondSharedName.java create mode 100644 plugin/src/test/java/org/jenkinsci/plugins/structs/third/ThirdSharedName.java diff --git a/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java b/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java index a633d2c..5a8a332 100644 --- a/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java +++ b/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java @@ -50,6 +50,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import static org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable.*; @@ -399,7 +400,7 @@ private Object coerce(String context, Type type, Object o) throws Exception { m.put((String) entry.getKey(), entry.getValue()); } - Class clazz = resolveClass(erased, (String) m.remove(CLAZZ), null); + Class clazz = resolveClass(erased, (String) m.remove(CLAZZ), null, getType()); return new DescribableModel(clazz).instantiate(m); } else if (o instanceof String && erased.isEnum()) { return Enum.valueOf(erased.asSubclass(Enum.class), (String) o); @@ -456,7 +457,8 @@ private Object coerceStringToNumber(@Nonnull String context, @Nonnull Class numb * @param base * Signature of the type that the resolved class should be assignable to. */ - /*package*/ static Class resolveClass(Class base, @Nullable String name, @Nullable String symbol) throws ClassNotFoundException { + /*package*/ static Class resolveClass(Class base, @Nullable String name, @Nullable String symbol, + @Nullable Class contextClass) throws ClassNotFoundException { // TODO: if both name & symbol are present, should we verify its consistency? if (name != null) { @@ -465,19 +467,43 @@ private Object coerceStringToNumber(@Nonnull String context, @Nonnull Class numb ClassLoader loader = j != null ? j.getPluginManager().uberClassLoader : Thread.currentThread().getContextClassLoader(); return Class.forName(name, true, loader); } else { - Class clazz = null; + List> possibleClazzes = new ArrayList<>(); for (Class c : findSubtypes(base)) { if (c.getSimpleName().equals(name)) { - if (clazz != null) { - throw new UnsupportedOperationException(name + " as a " + base + " could mean either " + clazz.getName() + " or " + c.getName()); - } - clazz = c; + possibleClazzes.add(c); } } - if (clazz == null) { + if (possibleClazzes.isEmpty()) { throw new UnsupportedOperationException("no known implementation of " + base + " is named " + name); } - return clazz; + if (possibleClazzes.size() != 1) { + // Try to heuristically determine the correct class. + List> narrowedClazzes = new ArrayList<>(); + for (Class possible : possibleClazzes) { + if (contextClass != null) { + if (contextClass.equals(possible.getEnclosingClass()) + || contextClass.getPackage().equals(possible.getPackage()) + || possible.getPackage().getName().startsWith(contextClass.getPackage().getName())) { + narrowedClazzes.add(possible); + } + } + } + + // We found just one that was eligible, return that. + if (narrowedClazzes.size() == 1) { + return narrowedClazzes.get(0); + } + + // Couldn't heuristically determine the correct class, error out. + String errorString; + if (possibleClazzes.size() == 2) { + errorString = possibleClazzes.stream().map(Class::getName).collect(Collectors.joining(" or ")); + } else { + errorString = possibleClazzes.stream().map(Class::getName).collect(Collectors.joining(", ")); + } + throw new UnsupportedOperationException(name + " as a " + base + " could mean any of " + errorString); + } + return possibleClazzes.get(0); } } diff --git a/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/UninstantiatedDescribable.java b/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/UninstantiatedDescribable.java index 68da488..45f0d88 100644 --- a/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/UninstantiatedDescribable.java +++ b/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/UninstantiatedDescribable.java @@ -164,7 +164,7 @@ public Object instantiate() throws Exception { * depends on this parameter. */ public T instantiate(Class base) throws Exception { - Class c = DescribableModel.resolveClass(base, klass, symbol); + Class c = DescribableModel.resolveClass(base, klass, symbol, null); return base.cast(new DescribableModel(c).instantiate(arguments)); } diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/AbstractSecondSharedName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/AbstractSecondSharedName.java new file mode 100644 index 0000000..7f968c1 --- /dev/null +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/AbstractSecondSharedName.java @@ -0,0 +1,30 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.structs.describable; + +import hudson.model.AbstractDescribableImpl; + +public abstract class AbstractSecondSharedName extends AbstractDescribableImpl { +} diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/AbstractThirdSharedName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/AbstractThirdSharedName.java new file mode 100644 index 0000000..94a65d4 --- /dev/null +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/AbstractThirdSharedName.java @@ -0,0 +1,30 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.structs.describable; + +import hudson.model.AbstractDescribableImpl; + +public abstract class AbstractThirdSharedName extends AbstractDescribableImpl { +} diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/DescribableModelTest.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/DescribableModelTest.java index 6c8699a..23cef9f 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/DescribableModelTest.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/DescribableModelTest.java @@ -42,9 +42,15 @@ import org.jenkinsci.plugins.structs.FishingNet; import org.jenkinsci.plugins.structs.Internet; import org.jenkinsci.plugins.structs.Tech; +import org.jenkinsci.plugins.structs.describable.first.NarrowAmbiguousArrayContainer; +import org.jenkinsci.plugins.structs.describable.first.NarrowAmbiguousContainer; +import org.jenkinsci.plugins.structs.describable.first.NarrowAmbiguousListContainer; import org.jenkinsci.plugins.structs.describable.first.SharedName; +import org.jenkinsci.plugins.structs.describable.first.second.SecondSharedName; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.kohsuke.stapler.DataBoundConstructor; @@ -61,6 +67,7 @@ import java.util.logging.Level; import static org.apache.commons.lang3.SerializationUtils.roundtrip; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.jenkinsci.plugins.structs.describable.DescribableModel.*; import static org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable.ANONYMOUS_KEY; @@ -74,6 +81,9 @@ public class DescribableModelTest { @ClassRule public static LoggerRule logging = new LoggerRule().record(DescribableModel.class, Level.ALL); + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Test public void instantiate() throws Exception { Map args = map("text", "hello", "flag", true, "ignored", "!"); @@ -669,17 +679,17 @@ public void setFoo(Recursion r) {} */ @Test public void resolveClass() throws Exception { - assertEquals(FishingNet.class, DescribableModel.resolveClass(Fishing.class, null, "net")); - assertEquals(FishingNet.class, DescribableModel.resolveClass(Fishing.class, "FishingNet", null)); - assertEquals(Internet.class, DescribableModel.resolveClass(Tech.class, null, "net")); - assertEquals(Internet.class, DescribableModel.resolveClass(Tech.class, "Internet", null)); + assertEquals(FishingNet.class, DescribableModel.resolveClass(Fishing.class, null, "net", null)); + assertEquals(FishingNet.class, DescribableModel.resolveClass(Fishing.class, "FishingNet", null, null)); + assertEquals(Internet.class, DescribableModel.resolveClass(Tech.class, null, "net", null)); + assertEquals(Internet.class, DescribableModel.resolveClass(Tech.class, "Internet", null, null)); } @Issue("JENKINS-46122") @Test public void resolveSymbolOnWrongBaseClass() throws Exception { try { - DescribableModel.resolveClass(Tech.class, null, "rod"); + DescribableModel.resolveClass(Tech.class, null, "rod", null); fail("No symbol for Tech should exist."); } catch (UnsupportedOperationException e) { assertEquals("no known implementation of " + Tech.class + " is using symbol ‘rod’", e.getMessage()); @@ -870,6 +880,87 @@ public void ambiguousTopLevelSimpleNameInArray() throws Exception { assertThat(roundtrip.getArray()[1], instanceOf(UnambiguousClassName.class)); } + @Issue("JENKINS-53825") + @Test + public void heuristicSharedNameSamePackage() throws Exception { + NarrowAmbiguousContainer container = new NarrowAmbiguousContainer(new SharedName("first"), + new UnambiguousClassName("second")); + + NarrowAmbiguousContainer fromInstantiate = instantiate(NarrowAmbiguousContainer.class, + map("ambiguous", map("$class", "SharedName", "one", "first"), + "unambiguous", map("$class", "UnambiguousClassName", "one", "second"))); + + assertEquals(container.toString(), fromInstantiate.toString()); + } + + @Issue("JENKINS-53825") + @Test + public void heuristicSharedNameInnerClass() throws Exception { + NarrowAmbiguousContainer container = new NarrowAmbiguousContainer(new NarrowAmbiguousContainer.ThirdSharedName("first"), + new UnambiguousClassName("second")); + + NarrowAmbiguousContainer fromInstantiate = instantiate(NarrowAmbiguousContainer.class, + map("ambiguous", map("$class", "ThirdSharedName", "one", "first"), + "unambiguous", map("$class", "UnambiguousClassName", "one", "second"))); + + assertEquals(container.toString(), fromInstantiate.toString()); + } + + @Issue("JENKINS-53825") + @Test + public void heuristicSharedNameChildPackage() throws Exception { + NarrowAmbiguousContainer container = new NarrowAmbiguousContainer(new SecondSharedName("first"), + new UnambiguousClassName("second")); + + NarrowAmbiguousContainer fromInstantiate = instantiate(NarrowAmbiguousContainer.class, + map("ambiguous", map("$class", "SecondSharedName", "one", "first"), + "unambiguous", map("$class", "UnambiguousClassName", "one", "second"))); + + assertEquals(container.toString(), fromInstantiate.toString()); + } + + @Issue("JENKINS-53825") + @Test + public void heuristicSharedNameFailToDistinguish() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage(containsString("SharedName as a interface hudson.model.Describable could mean any of org.jenkinsci.plugins.structs.describable.first.SharedName or org.jenkinsci.plugins.structs.describable.second.SharedName")); + instantiate(AmbiguousContainer.class, + map("ambiguous", map("$class", "SharedName", "one", "first"), + "unambiguous", map("$class", "UnambiguousClassName", "one", "second"))); + } + + @Issue("JENKINS-53825") + @Test + public void heuristicSharedNameList() throws Exception { + SharedName first = new SharedName("first"); + first.setTwo("something"); + NarrowAmbiguousListContainer container = new NarrowAmbiguousListContainer(Arrays.>asList(first, + new UnambiguousClassName("second"))); + + NarrowAmbiguousListContainer fromInstantiate = instantiate(NarrowAmbiguousListContainer.class, + map("list", + Arrays.asList(map("$class", "SharedName", "one", "first", "two", "something"), + map("$class", "UnambiguousClassName", "one", "second")))); + + assertEquals(container.toString(), fromInstantiate.toString()); + } + + @Issue("JENKINS-53825") + @Test + public void heuristicSharedNameInArray() throws Exception { + SharedName first = new SharedName("first"); + first.setTwo("something"); + NarrowAmbiguousArrayContainer container = new NarrowAmbiguousArrayContainer(first, + new UnambiguousClassName("second")); + + NarrowAmbiguousArrayContainer fromInstantiate = instantiate(NarrowAmbiguousArrayContainer.class, + map("array", + Arrays.asList(map("$class", "SharedName", "one", "first", "two", "something"), + map("$class", "UnambiguousClassName", "one", "second")))); + + assertEquals(container.toString(), fromInstantiate.toString()); + } + private static Map map(Object... keysAndValues) { if (keysAndValues.length % 2 != 0) { throw new IllegalArgumentException(); diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/UnambiguousClassName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/UnambiguousClassName.java index 9fb9c5e..01e90d2 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/UnambiguousClassName.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/UnambiguousClassName.java @@ -37,6 +37,11 @@ public UnambiguousClassName(String one) { this.one = one; } + @Override + public String toString() { + return "UnambiguousClassName[one[" + one + "]]"; + } + @Extension public static class DescriptorImpl extends Descriptor { @Override diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousArrayContainer.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousArrayContainer.java new file mode 100644 index 0000000..fea57fd --- /dev/null +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousArrayContainer.java @@ -0,0 +1,59 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.structs.describable.first; + +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Describable; +import hudson.model.Descriptor; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.util.Arrays; + +public class NarrowAmbiguousArrayContainer extends AbstractDescribableImpl { + private final Describable[] array; + + @DataBoundConstructor + public NarrowAmbiguousArrayContainer(Describable... array) { + this.array = array.clone(); + } + + public Describable[] getArray() { + return array.clone(); + } + + @Override + public String toString() { + return "NarrowAmbiguousArrayContainer[array[" + Arrays.asList(array).toString() + "]]"; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + @Override + public String getDisplayName() { + return "ambiguous array container"; + } + } +} diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousContainer.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousContainer.java new file mode 100644 index 0000000..58807bf --- /dev/null +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousContainer.java @@ -0,0 +1,103 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.structs.describable.first; + +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Describable; +import hudson.model.Descriptor; +import org.jenkinsci.plugins.structs.describable.AbstractSharedName; +import org.jenkinsci.plugins.structs.describable.AbstractThirdSharedName; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +public class NarrowAmbiguousContainer extends AbstractDescribableImpl { + public final Describable ambiguous; + public final Describable unambiguous; + + @DataBoundConstructor + public NarrowAmbiguousContainer(Describable ambiguous, Describable unambiguous) { + this.ambiguous = ambiguous; + this.unambiguous = unambiguous; + } + + @Override + public String toString() { + return "NarrowAmbiguousContainer[ambiguous[" + ambiguous.toString() + "], unambiguous[" + unambiguous.toString() + "]]"; + } + @Extension + public static class DescriptorImpl extends Descriptor { + @Override + public String getDisplayName() { + return "narrow ambiguous container"; + } + } + + public static class ThirdSharedName extends AbstractThirdSharedName { + private final String one; + private String two; + + @DataBoundConstructor + public ThirdSharedName(String one) { + this.one = one; + } + + public String getOne() { + return one; + } + + public String getTwo() { + return two; + } + + @DataBoundSetter + public void setTwo(String two) { + this.two = two; + } + + public String getLegacyTwo() { + return two; + } + + @Deprecated + @DataBoundSetter + public void setLegacyTwo(String two) { + this.two = two; + } + + @Override + public String toString() { + return "ThirdSharedName[one[" + one + "], [two[" + two + "]]"; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + @Override + public String getDisplayName() { + return "inner.ThirdSharedName"; + } + } + } +} diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousListContainer.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousListContainer.java new file mode 100644 index 0000000..3a057d6 --- /dev/null +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/NarrowAmbiguousListContainer.java @@ -0,0 +1,56 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.structs.describable.first; + +import hudson.Extension; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Describable; +import hudson.model.Descriptor; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.util.ArrayList; +import java.util.List; + +public class NarrowAmbiguousListContainer extends AbstractDescribableImpl { + public final List> list; + + @DataBoundConstructor + public NarrowAmbiguousListContainer(List> list) { + this.list = new ArrayList<>(list); + } + + @Override + public String toString() { + return "NarrowAmbiguousListContainer[list[" + list + "]]"; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + @Override + public String getDisplayName() { + return "ambiguous list container"; + } + } +} diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java index 777ec35..1d99375 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java @@ -63,7 +63,12 @@ public void setLegacyTwo(String two) { this.two = two; } - @Extension + @Override + public String toString() { + return "SharedName[one[" + one + "], [two[" + two + "]]"; + } + + @Extension(ordinal = 0) public static class DescriptorImpl extends Descriptor { @Override public String getDisplayName() { diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/second/SecondSharedName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/second/SecondSharedName.java new file mode 100644 index 0000000..d964b10 --- /dev/null +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/second/SecondSharedName.java @@ -0,0 +1,78 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.structs.describable.first.second; + +import hudson.Extension; +import hudson.model.Descriptor; +import org.jenkinsci.plugins.structs.describable.AbstractSecondSharedName; +import org.jenkinsci.plugins.structs.describable.AbstractSharedName; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +public class SecondSharedName extends AbstractSecondSharedName { + private final String one; + private String two; + + @DataBoundConstructor + public SecondSharedName(String one) { + this.one = one; + } + + public String getOne() { + return one; + } + + public String getTwo() { + return two; + } + + @DataBoundSetter + public void setTwo(String two) { + this.two = two; + } + + public String getLegacyTwo() { + return two; + } + + @Deprecated + @DataBoundSetter + public void setLegacyTwo(String two) { + this.two = two; + } + + @Override + public String toString() { + return "SecondSharedName[one[" + one + "], [two[" + two + "]]"; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + @Override + public String getDisplayName() { + return "first.SecondSharedName"; + } + } +} diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/second/SharedName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/second/SharedName.java index bfd9e39..c60a0a4 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/second/SharedName.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/second/SharedName.java @@ -38,7 +38,12 @@ public SharedName(String two) { this.two = two; } - @Extension + @Override + public String toString() { + return "SharedName[two[" + two + "]]"; + } + + @Extension(ordinal = 1) public static class DescriptorImpl extends Descriptor { @Override public String getDisplayName() { diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/third/SecondSharedName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/third/SecondSharedName.java new file mode 100644 index 0000000..13c9f6f --- /dev/null +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/third/SecondSharedName.java @@ -0,0 +1,53 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.structs.third; + +import hudson.Extension; +import hudson.model.Descriptor; +import org.jenkinsci.plugins.structs.describable.AbstractSecondSharedName; +import org.jenkinsci.plugins.structs.describable.AbstractSharedName; +import org.kohsuke.stapler.DataBoundConstructor; + +public class SecondSharedName extends AbstractSecondSharedName { + public final String two; + + @DataBoundConstructor + public SecondSharedName(String two) { + this.two = two; + } + + @Override + public String toString() { + return "SecondSharedName[two[" + two + "]]"; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + @Override + public String getDisplayName() { + return "third.SecondSharedName"; + } + } +} diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/third/ThirdSharedName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/third/ThirdSharedName.java new file mode 100644 index 0000000..aff6697 --- /dev/null +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/third/ThirdSharedName.java @@ -0,0 +1,53 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.structs.third; + +import hudson.Extension; +import hudson.model.Descriptor; +import org.jenkinsci.plugins.structs.describable.AbstractThirdSharedName; +import org.jenkinsci.plugins.structs.describable.AbstractSharedName; +import org.kohsuke.stapler.DataBoundConstructor; + +public class ThirdSharedName extends AbstractThirdSharedName { + public final String two; + + @DataBoundConstructor + public ThirdSharedName(String two) { + this.two = two; + } + + @Override + public String toString() { + return "ThirdSharedName[two[" + two + "]]"; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + @Override + public String getDisplayName() { + return "third.ThirdSharedName"; + } + } +} From 5b54f48c84bce8a4e99369f0b0352c236cc22867 Mon Sep 17 00:00:00 2001 From: Andrew Bayer Date: Wed, 3 Oct 2018 15:42:58 -0400 Subject: [PATCH 2/3] Ok, this actually makes the error message order deterministic --- .../plugins/structs/describable/DescribableModel.java | 8 +++++--- .../plugins/structs/describable/first/SharedName.java | 2 +- .../plugins/structs/describable/second/SharedName.java | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java b/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java index 5a8a332..1150019 100644 --- a/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java +++ b/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java @@ -14,6 +14,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang.ClassUtils; import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang.StringUtils; import org.codehaus.groovy.reflection.ReflectionCache; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.structs.SymbolLookup; @@ -496,10 +497,11 @@ private Object coerceStringToNumber(@Nonnull String context, @Nonnull Class numb // Couldn't heuristically determine the correct class, error out. String errorString; - if (possibleClazzes.size() == 2) { - errorString = possibleClazzes.stream().map(Class::getName).collect(Collectors.joining(" or ")); + List ambiguousNames = possibleClazzes.stream().map(Class::getName).sorted().collect(Collectors.toList()); + if (ambiguousNames.size() == 2) { + errorString = StringUtils.join(ambiguousNames, " or "); } else { - errorString = possibleClazzes.stream().map(Class::getName).collect(Collectors.joining(", ")); + errorString = StringUtils.join(ambiguousNames, ", "); } throw new UnsupportedOperationException(name + " as a " + base + " could mean any of " + errorString); } diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java index 1d99375..29e4f77 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java @@ -68,7 +68,7 @@ public String toString() { return "SharedName[one[" + one + "], [two[" + two + "]]"; } - @Extension(ordinal = 0) + @Extension public static class DescriptorImpl extends Descriptor { @Override public String getDisplayName() { diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/second/SharedName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/second/SharedName.java index c60a0a4..6977f98 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/second/SharedName.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/second/SharedName.java @@ -43,7 +43,7 @@ public String toString() { return "SharedName[two[" + two + "]]"; } - @Extension(ordinal = 1) + @Extension public static class DescriptorImpl extends Descriptor { @Override public String getDisplayName() { From fe96e5f8f693e05e9eb6eccab45dd53764157182 Mon Sep 17 00:00:00 2001 From: Andrew Bayer Date: Wed, 3 Oct 2018 18:07:10 -0400 Subject: [PATCH 3/3] Initial work on adding "context" to Symbol. --- .../src/main/java/org/jenkinsci/Symbol.java | 1 + .../plugins/structs/SymbolLookup.java | 95 +++++++++++++++---- .../structs/describable/DescribableModel.java | 4 +- .../plugins/structs/SymbolLookupTest.java | 37 ++++++++ .../structs/describable/first/SharedName.java | 2 + 5 files changed, 121 insertions(+), 18 deletions(-) diff --git a/annotation/src/main/java/org/jenkinsci/Symbol.java b/annotation/src/main/java/org/jenkinsci/Symbol.java index 7f007e2..09854a0 100644 --- a/annotation/src/main/java/org/jenkinsci/Symbol.java +++ b/annotation/src/main/java/org/jenkinsci/Symbol.java @@ -45,4 +45,5 @@ @Documented public @interface Symbol { String[] value(); + Class[] context() default {}; } diff --git a/plugin/src/main/java/org/jenkinsci/plugins/structs/SymbolLookup.java b/plugin/src/main/java/org/jenkinsci/plugins/structs/SymbolLookup.java index 4771f36..652cdf3 100644 --- a/plugin/src/main/java/org/jenkinsci/plugins/structs/SymbolLookup.java +++ b/plugin/src/main/java/org/jenkinsci/plugins/structs/SymbolLookup.java @@ -15,6 +15,8 @@ import javax.annotation.Nonnull; import javax.inject.Inject; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -33,9 +35,9 @@ */ @Extension public class SymbolLookup { - private final ConcurrentMap cache = new ConcurrentHashMap(); + private final ConcurrentMap cache = new ConcurrentHashMap(); - private final ConcurrentMap noHitCache = new ConcurrentHashMap(); + private final ConcurrentMap noHitCache = new ConcurrentHashMap(); static final Object NO_HIT = new Object(); @@ -55,7 +57,8 @@ private static HashSet pluginsToNames(List plugins) { return pluginNames; } - /** Update list of plugins used and purge the noHit cache if plugins have been added + /** + * Update list of plugins used and purge the noHit cache if plugins have been added */ private synchronized void checkPluginsForChangeAndRefresh() { List wrap = pluginManager.getPlugins(); @@ -69,14 +72,22 @@ private synchronized void checkPluginsForChangeAndRefresh() { } /** - * @param type - * Restrict the search to a subset of extensions. + * @param type Restrict the search to a subset of extensions. */ public T find(Class type, String symbol) { + return find(type, symbol, null); + } + + /** + * @param type Restrict the search to a subset of extensions. + * @param context + * Prefer classes with a matching context on their {@link Symbol} + */ + public T find(Class type, String symbol, Class context) { try { - Key k = new Key("find",type,symbol); + Key k = new Key("find", type, symbol, context); Object i = cache.get(k); - if (i!=null) return type.cast(i); + if (i != null) return type.cast(i); // not allowing @Symbol to use an invalid identifier. // TODO: compile time check @@ -90,25 +101,42 @@ public T find(Class type, String symbol) { return null; } + List> candidates = new ArrayList<>(); for (Class e : Index.list(Symbol.class, pluginManager.uberClassLoader, Class.class)) { if (type.isAssignableFrom(e)) { Symbol s = e.getAnnotation(Symbol.class); if (s != null) { for (String t : s.value()) { if (t.equals(symbol)) { - i = jenkins.getInjector().getInstance(e); - cache.put(k, i); - return type.cast(i); + candidates.add(e); } } } } } + if (!candidates.isEmpty()) { + for (Class e : candidates) { + Symbol s = e.getAnnotation(Symbol.class); + if (context != null && Arrays.stream(s.context()).anyMatch(c -> context.isAssignableFrom(c))) { + i = jenkins.getInjector().getInstance(e); + break; + } + } + + if (i == null) { + // If we didn't find a context-aware match, just use the first general match. + i = jenkins.getInjector().getInstance(candidates.get(0)); + } + + cache.put(k, i); + return type.cast(i); + } + noHitCache.put(k, NO_HIT); return null; } catch (IOException e) { - LOGGER.log(Level.WARNING, "Unable to find @Symbol",e); + LOGGER.log(Level.WARNING, "Unable to find @Symbol", e); return null; } } @@ -120,8 +148,20 @@ public T find(Class type, String symbol) { * Restrict the search to a subset of {@link Describable} */ public Descriptor findDescriptor(Class type, String symbol) { + return findDescriptor(type, symbol, null); + } + + /** + * Looks for a {@link Descriptor} that has the given symbol + * + * @param type + * Restrict the search to a subset of {@link Describable} + * @param context + * Prefer classes with a matching context on their {@link Symbol} + */ + public Descriptor findDescriptor(Class type, String symbol, Class context) { try { - Key k = new Key("findDescriptor",type,symbol); + Key k = new Key("findDescriptor",type,symbol, context); Object i = cache.get(k); if (i!=null) return (Descriptor)i; @@ -137,6 +177,7 @@ public Descriptor findDescriptor(Class type, String symbol) { return null; } + List> candidates = new ArrayList<>(); for (Class e : Index.list(Symbol.class, pluginManager.uberClassLoader, Class.class)) { if (Descriptor.class.isAssignableFrom(e)) { Symbol s = e.getAnnotation(Symbol.class); @@ -145,8 +186,7 @@ public Descriptor findDescriptor(Class type, String symbol) { if (t.equals(symbol)) { Descriptor d = (Descriptor) jenkins.getInjector().getInstance(e); if (type.isAssignableFrom(d.clazz)) { - cache.put(k, d); - return d; + candidates.add(e); } } } @@ -154,6 +194,25 @@ public Descriptor findDescriptor(Class type, String symbol) { } } + Descriptor descriptor = null; + + if (!candidates.isEmpty()) { + for (Class e : candidates) { + Symbol s = e.getAnnotation(Symbol.class); + if (context != null && Arrays.stream(s.context()).anyMatch(c -> context.isAssignableFrom(c))) { + descriptor = (Descriptor) jenkins.getInjector().getInstance(e); + break; + } + } + + if (descriptor == null) { + descriptor = (Descriptor) jenkins.getInjector().getInstance(candidates.get(0)); + } + + cache.put(k, descriptor); + return descriptor; + } + noHitCache.put(k, NO_HIT); return null; } catch (IOException e) { @@ -166,11 +225,13 @@ private static class Key { private final String tag; private final Class type; private final String name; + private final Class context; - public Key(String tag, Class type, String name) { + public Key(String tag, Class type, String name, Class context) { this.tag = tag; this.type = type; this.name = name; + this.context = context; } @Override @@ -178,7 +239,8 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Key key = (Key) o; - return type==key.type && tag.equals(key.tag) && name.equals(key.name); + return type==key.type && tag.equals(key.tag) && name.equals(key.name) + && (context != null ? context.equals(key.context) : key.context == null); } @Override @@ -186,6 +248,7 @@ public int hashCode() { int h = type.hashCode(); h = h*31 + tag.hashCode(); h = h*31 + name.hashCode(); + h = h*31 + (context != null ? context.hashCode() : 0); return h; } } diff --git a/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java b/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java index 1150019..a4f9968 100644 --- a/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java +++ b/plugin/src/main/java/org/jenkinsci/plugins/structs/describable/DescribableModel.java @@ -511,12 +511,12 @@ private Object coerceStringToNumber(@Nonnull String context, @Nonnull Class numb if (symbol != null) { // The normal case: the Descriptor is marked, but the name applies to its Describable. - Descriptor d = SymbolLookup.get().findDescriptor(base, symbol); + Descriptor d = SymbolLookup.get().findDescriptor(base, symbol, contextClass); if (d != null) { return d.clazz; } if (base == ParameterValue.class) { // TODO JENKINS-26093 workaround - d = SymbolLookup.get().findDescriptor(ParameterDefinition.class, symbol); + d = SymbolLookup.get().findDescriptor(ParameterDefinition.class, symbol, contextClass); if (d != null) { Class c = parameterValueClass(d.clazz); if (c != null) { diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/SymbolLookupTest.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/SymbolLookupTest.java index 41fd75e..75d60fd 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/structs/SymbolLookupTest.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/SymbolLookupTest.java @@ -23,6 +23,12 @@ public static class Foo {} @TestExtension @Symbol("bar") public static class Bar {} + @TestExtension @Symbol("ambiguous") + public static class FirstAmbiguous {} + + @TestExtension @Symbol(value = "ambiguous", context = Bar.class) + public static class SecondAmbiguous {} + @Rule public JenkinsRule rule = new JenkinsRule(); @@ -35,6 +41,12 @@ public static class Bar {} @Inject Bar bar; + @Inject + FirstAmbiguous firstAmbiguous; + + @Inject + SecondAmbiguous secondAmbiguous; + @Inject FishingNet.DescriptorImpl fishingNetDescriptor; @@ -104,4 +116,29 @@ public void descriptorIsDescribable() { @Symbol("whatever") public static class SomeConfiguration extends GlobalConfiguration {} + @Issue("JENKINS-53825") + @Test + public void context() { + assertThat(lookup.find(Object.class, "ambiguous"), is(sameInstance(this.firstAmbiguous))); + assertThat(lookup.find(Object.class, "ambiguous", Bar.class), is(sameInstance(this.secondAmbiguous))); + } + + @TestExtension("descriptorContext") + @Symbol("ambiguousDescriptor") + public static class FirstAmbiguousConfiguration extends GlobalConfiguration {} + + @TestExtension("descriptorContext") + @Symbol(value = "ambiguousDescriptor", context = Bar.class) + public static class SecondAmbiguousConfiguration extends GlobalConfiguration {} + + @Issue("JENKINS-53825") + @Test + public void descriptorContext() { + FirstAmbiguousConfiguration first = rule.jenkins.getDescriptorByType(FirstAmbiguousConfiguration.class); + SecondAmbiguousConfiguration second = rule.jenkins.getDescriptorByType(SecondAmbiguousConfiguration.class); + + assertThat(lookup.find(Object.class, "ambiguousDescriptor"), is(sameInstance(first))); + assertThat(lookup.find(Object.class, "ambiguousDescriptor", Bar.class), is(sameInstance(second))); + } + } diff --git a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java index 29e4f77..ce6799f 100644 --- a/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java +++ b/plugin/src/test/java/org/jenkinsci/plugins/structs/describable/first/SharedName.java @@ -28,6 +28,7 @@ import hudson.model.Descriptor; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.structs.describable.AbstractSharedName; +import org.jenkinsci.plugins.structs.describable.AmbiguousContainer; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -69,6 +70,7 @@ public String toString() { } @Extension + @Symbol(value = "sharedName", context = {AmbiguousContainer.class}) public static class DescriptorImpl extends Descriptor { @Override public String getDisplayName() {