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 a633d2c..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 @@ -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; @@ -50,6 +51,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 +401,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 +458,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,30 +468,55 @@ 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; + List ambiguousNames = possibleClazzes.stream().map(Class::getName).sorted().collect(Collectors.toList()); + if (ambiguousNames.size() == 2) { + errorString = StringUtils.join(ambiguousNames, " or "); + } else { + errorString = StringUtils.join(ambiguousNames, ", "); + } + throw new UnsupportedOperationException(name + " as a " + base + " could mean any of " + errorString); + } + return possibleClazzes.get(0); } } 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/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/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/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..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; @@ -63,7 +64,13 @@ public void setLegacyTwo(String two) { this.two = two; } + @Override + public String toString() { + return "SharedName[one[" + one + "], [two[" + two + "]]"; + } + @Extension + @Symbol(value = "sharedName", context = {AmbiguousContainer.class}) 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..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 @@ -38,6 +38,11 @@ public SharedName(String two) { this.two = two; } + @Override + public String toString() { + return "SharedName[two[" + two + "]]"; + } + @Extension public static class DescriptorImpl extends Descriptor { @Override 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"; + } + } +}