Skip to content

Commit 63c82a8

Browse files
dgarzonjetersen
authored andcommitted
Describable will give a warning on multiple implementations of a symbol (#674)
1 parent fb895cc commit 63c82a8

File tree

6 files changed

+276
-95
lines changed

6 files changed

+276
-95
lines changed

plugin/src/main/java/io/jenkins/plugins/casc/ConfigurationContext.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ public RootElementConfigurator lookupRootElement(String name) {
6767

6868
@Override
6969
@NonNull
70-
public Configurator lookupOrFail(Type type) throws ConfiguratorException {
70+
public <T> Configurator<T> lookupOrFail(Type type) throws ConfiguratorException {
7171
return registry.lookupOrFail(type);
7272
}
7373

7474
@Override
7575
@CheckForNull
76-
public Configurator lookup(Type type) {
76+
public <T> Configurator<T> lookup(Type type) {
7777
return registry.lookup(type);
7878
}
7979

plugin/src/main/java/io/jenkins/plugins/casc/Configurator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,11 @@ default Class getImplementedAPI() {
9696

9797

9898
/**
99-
* @return list of {@link }Configurator}s to be considered so one can fully configure this component.
99+
* @return list of {@link Configurator<T>}s to be considered so one can fully configure this component.
100100
* Typically, configurator for an abstract extension point will return Configurators for available implementations.
101101
*/
102102
@NonNull
103-
default List<Configurator> getConfigurators(ConfigurationContext context) {
103+
default List<Configurator<T>> getConfigurators(ConfigurationContext context) {
104104
return Collections.singletonList(this);
105105
}
106106

Lines changed: 144 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package io.jenkins.plugins.casc.impl.configurators;
22

3+
import static io.jenkins.plugins.casc.model.CNode.Type.MAPPING;
4+
import static io.vavr.API.unchecked;
5+
36
import edu.umd.cs.findbugs.annotations.CheckForNull;
47
import edu.umd.cs.findbugs.annotations.NonNull;
58
import hudson.model.Describable;
@@ -9,12 +12,16 @@
912
import io.jenkins.plugins.casc.Attribute;
1013
import io.jenkins.plugins.casc.ConfigurationContext;
1114
import io.jenkins.plugins.casc.Configurator;
12-
import io.jenkins.plugins.casc.ConfiguratorException;
1315
import io.jenkins.plugins.casc.ObsoleteConfigurationMonitor;
1416
import io.jenkins.plugins.casc.impl.attributes.DescribableAttribute;
1517
import io.jenkins.plugins.casc.model.CNode;
1618
import io.jenkins.plugins.casc.model.Mapping;
1719
import io.jenkins.plugins.casc.model.Scalar;
20+
import io.vavr.Tuple;
21+
import io.vavr.Tuple2;
22+
import io.vavr.collection.HashMap;
23+
import io.vavr.collection.Stream;
24+
import io.vavr.control.Option;
1825
import jenkins.model.Jenkins;
1926
import org.jenkinsci.Symbol;
2027
import org.kohsuke.accmod.Restricted;
@@ -24,8 +31,8 @@
2431
import java.util.List;
2532
import java.util.Map;
2633
import java.util.Set;
34+
import java.util.function.Predicate;
2735
import java.util.logging.Logger;
28-
import java.util.stream.Collectors;
2936

3037
/**
3138
* {@link Configurator} that works with {@link Describable} subtype as a {@link #target}.
@@ -42,11 +49,10 @@
4249
* @author <a href="mailto:[email protected]">Nicolas De Loof</a>
4350
*/
4451
@Restricted(NoExternalUse.class)
45-
public class HeteroDescribableConfigurator<T extends Describable> implements Configurator<T> {
52+
public class HeteroDescribableConfigurator<T extends Describable<T>> implements Configurator<T> {
4653

4754
private static final Logger LOGGER = Logger.getLogger(HeteroDescribableConfigurator.class.getName());
4855

49-
5056
private final Class<T> target;
5157

5258
public HeteroDescribableConfigurator(Class<T> clazz) {
@@ -59,119 +65,167 @@ public Class<T> getTarget() {
5965
}
6066

6167
@NonNull
62-
public List<Configurator> getConfigurators(ConfigurationContext context) {
63-
final List<Descriptor> candidates = Jenkins.getInstance().getDescriptorList(target);
64-
final List<Configurator> configurators = candidates.stream()
65-
.map(d -> context.lookup(d.getKlass().toJavaClass()))
66-
.filter(c -> c != null)
67-
.collect(Collectors.toList());
68-
configurators.add(this);
69-
return configurators;
68+
@Override
69+
public List<Configurator<T>> getConfigurators(ConfigurationContext context) {
70+
return getDescriptors()
71+
.flatMap(d -> lookupConfigurator(context, descriptorClass(d)))
72+
.append(this)
73+
.toJavaList();
7074
}
7175

7276
@NonNull
7377
@Override
74-
public T configure(CNode config, ConfigurationContext context) throws ConfiguratorException {
75-
String shortname;
76-
CNode subconfig = null;
77-
switch (config.getType()) {
78-
case SCALAR:
79-
shortname = config.asScalar().getValue();
80-
break;
81-
case MAPPING:
82-
Mapping map = config.asMapping();
83-
if (map.size() != 1) {
84-
throw new IllegalArgumentException("single entry map expected to configure a "+target.getName());
85-
}
86-
final Map.Entry<String, CNode> next = map.entrySet().iterator().next();
87-
shortname = next.getKey();
88-
subconfig = next.getValue();
89-
break;
90-
default:
91-
throw new IllegalArgumentException("Unexpected configuration type "+config);
92-
}
78+
public T configure(CNode config, ConfigurationContext context) {
79+
return preConfigure(config).apply((shortName, subConfig) ->
80+
lookupDescriptor(shortName, config)
81+
.map(descriptor -> forceLookupConfigurator(context, descriptor))
82+
.map(configurator -> doConfigure(context, configurator, subConfig.getOrNull())))
83+
.getOrNull();
84+
}
9385

94-
final List<Descriptor> candidates = Jenkins.getInstance().getDescriptorList(target);
86+
@Override
87+
public T check(CNode config, ConfigurationContext context) {
88+
return configure(config, context);
89+
}
9590

96-
Class<? extends T> k = findDescribableBySymbol(config, shortname, candidates);
97-
final Configurator<T> configurator = context.lookup(k);
98-
if (configurator == null) throw new IllegalStateException("No configurator implementation to manage "+k);
99-
return configurator.configure(subconfig, context);
91+
@NonNull
92+
@Override
93+
public Set<Attribute<T, ?>> describe() {
94+
return Collections.emptySet();
10095
}
10196

97+
@CheckForNull
10298
@Override
103-
public T check(CNode config, ConfigurationContext context) throws ConfiguratorException {
104-
return configure(config, context);
99+
public CNode describe(T instance, ConfigurationContext context) {
100+
Predicate<CNode> isScalar = node -> node.getType().equals(MAPPING) && unchecked(node::asMapping).apply().size() == 0;
101+
return lookupConfigurator(context, instance.getClass())
102+
.map(configurator -> convertToNode(context, configurator, instance))
103+
.map(node -> {
104+
if (isScalar.test(node)) {
105+
return new Scalar(preferredSymbol(instance.getDescriptor()));
106+
} else {
107+
return new Mapping().put(preferredSymbol(instance.getDescriptor()), node);
108+
}
109+
}).getOrNull();
110+
}
111+
112+
@SuppressWarnings("unused")
113+
public Map<String, Class<T>> getImplementors() {
114+
return getDescriptors()
115+
.map(descriptor -> Tuple.of(preferredSymbol(descriptor), descriptor))
116+
.foldLeft(HashMap.empty(), this::handleDuplicateSymbols)
117+
.mapValues(this::descriptorClass)
118+
.toJavaMap();
119+
}
120+
121+
@SuppressWarnings("unchecked")
122+
private Option<Configurator<T>> lookupConfigurator(ConfigurationContext context, Class<?> descriptor) {
123+
return Option.of(context.lookup(descriptor));
105124
}
106125

107-
public Map<String, Class> getImplementors() {
108-
final Class api = getImplementedAPI();
109-
final List<Descriptor> descriptors = Jenkins.getInstance().getDescriptorList(target);
110-
return descriptors.stream()
111-
.collect(Collectors.toMap(
112-
d -> DescribableAttribute.getSymbols(d, api, target).get(0),
113-
d -> d.getKlass().toJavaClass()));
126+
private Configurator<T> forceLookupConfigurator(ConfigurationContext context, Descriptor<T> descriptor) {
127+
Class<T> klazz = descriptorClass(descriptor);
128+
return lookupConfigurator(context, klazz)
129+
.getOrElseThrow(() -> new IllegalStateException("No configurator implementation to manage " + klazz));
114130
}
115131

132+
private Stream<Descriptor<T>> getDescriptors() {
133+
return Stream.ofAll(Jenkins.getInstance().getDescriptorList(target));
134+
}
116135

117-
/**
118-
*
119-
* @return true if the support plugin is installed, false otherwise.
120-
*/
121-
private boolean _hasSupportPluginInstalled() {
122-
return Jenkins.getInstance().getPlugin("configuration-as-code-support") != null;
136+
@SuppressWarnings("unchecked")
137+
private Class<T> descriptorClass(Descriptor<T> descriptor) {
138+
return descriptor.getKlass().toJavaClass();
123139
}
124140

125-
private Class<T> findDescribableBySymbol(CNode node, String shortname, List<Descriptor> candidates) {
141+
private Option<Descriptor<T>> lookupDescriptor(String symbol, CNode config) {
142+
return getDescriptors()
143+
.filter(descriptor -> findByPreferredSymbol(descriptor, symbol) || findBySymbols(descriptor, symbol, config))
144+
.map(descriptor -> Tuple.of(preferredSymbol(descriptor), descriptor))
145+
.foldLeft(HashMap.empty(), this::handleDuplicateSymbols)
146+
.values()
147+
.headOption()
148+
.orElse(() -> {
149+
String message = "No " + target.getName() + " implementation found for " + symbol;
150+
String possible = lookupPlugin("configuration-as-code-support") ? "" : System.lineSeparator() + "Possible solution: Try to install 'configuration-as-code-support' plugin";
151+
throw new IllegalArgumentException(Stream.of(message, possible)
152+
.intersperse("")
153+
.foldLeft(new StringBuilder(), StringBuilder::append)
154+
.toString());
155+
});
156+
}
126157

127-
final Class api = getImplementedAPI();
128-
// Search for @Symbol annotation on Descriptor to match shortName
158+
private Boolean findByPreferredSymbol(Descriptor<T> descriptor, String symbol) {
159+
return preferredSymbol(descriptor).equalsIgnoreCase(symbol);
160+
}
129161

130-
for (Descriptor d : candidates) {
131-
final List<String> symbols = DescribableAttribute.getSymbols(d, api, target);
132-
final String preferred = symbols.get(0);
133-
if (preferred.equalsIgnoreCase(shortname)) {
134-
return d.getKlass().toJavaClass();
135-
}
162+
private Boolean findBySymbols(Descriptor<T> descriptor, String symbol, CNode node) {
163+
return getSymbols(descriptor)
164+
.find(actual -> actual.equalsIgnoreCase(symbol))
165+
.map(actual -> {
166+
ObsoleteConfigurationMonitor.get().record(node, "'" + symbol + "' is obsolete, please use '" + preferredSymbol(descriptor) + "'");
167+
return descriptorClass(descriptor);
168+
}).isDefined();
169+
}
170+
171+
private Stream<String> getSymbols(Descriptor<T> descriptor) {
172+
return Stream.ofAll(DescribableAttribute.getSymbols(descriptor, target, target));
173+
}
174+
175+
private String preferredSymbol(Descriptor<?> descriptor) {
176+
return DescribableAttribute.getPreferredSymbol(descriptor, target, target);
177+
}
178+
179+
private Boolean lookupPlugin(String name) {
180+
return Option.of(Jenkins.getInstance().getPlugin(name)).isDefined();
181+
}
182+
183+
private HashMap<String, Descriptor<T>> handleDuplicateSymbols(HashMap<String, Descriptor<T>> r, Tuple2<String, Descriptor<T>> t) {
184+
if (r.containsKey(t._1)) {
185+
String message = String.format("Found multiple implementations for symbol = %s: [%s, %s]. Please report to plugin maintainer.", t._1, r.get(t._1).get(), t._2);
186+
LOGGER.warning(message);
187+
return r;
188+
} else {
189+
return r.put(t);
136190
}
191+
}
137192

138-
// Not found by preferred symbol, try other ones
139-
for (Descriptor d : candidates) {
140-
final List<String> symbols = DescribableAttribute.getSymbols(d, api, target);
141-
for (String symbol : symbols) {
142-
if (symbol.equalsIgnoreCase(shortname)) {
143-
ObsoleteConfigurationMonitor.get().record(node, "'"+shortname+"' is obsolete, please use '" + symbols.get(0) + "'");
144-
return d.getKlass().toJavaClass();
145-
}
146-
}
193+
private Tuple2<String, Option<CNode>> preConfigure(CNode config) {
194+
switch (config.getType()) {
195+
case SCALAR:
196+
return configureScalar(config);
197+
case MAPPING:
198+
return configureMapping(config);
199+
default:
200+
return configureUnexpected(config);
147201
}
202+
}
148203

149-
final String errSupport = !_hasSupportPluginInstalled() ? "\nPossible solution: Try to install 'configuration-as-code-support' plugin" : "";
150-
final String msg = "No "+target.getName()+ " implementation found for "+shortname;
151-
throw new IllegalArgumentException(String.format("%s%s", msg, errSupport));
204+
private Tuple2<String, Option<CNode>> configureUnexpected(CNode config) {
205+
throw new IllegalArgumentException("Unexpected configuration type " + config);
152206
}
153207

154-
@NonNull
155-
@Override
156-
public Set<Attribute<T,?>> describe() {
157-
return Collections.emptySet();
208+
private Tuple2<String, Option<CNode>> configureScalar(CNode config) {
209+
Scalar scalar = unchecked(config::asScalar).apply();
210+
return Tuple.of(scalar.getValue(), Option.none());
158211
}
159212

160-
@CheckForNull
161-
@Override
162-
public CNode describe(Describable instance, ConfigurationContext context) throws Exception {
163-
final String symbol = DescribableAttribute.getPreferredSymbol(instance.getDescriptor(), getTarget(), instance.getClass());
164-
final Configurator c = context.lookupOrFail(instance.getClass());
165-
final CNode describe = c.describe(instance, context);
166-
if (describe == null) {
167-
return null;
168-
}
169-
if (describe.getType() == CNode.Type.MAPPING && describe.asMapping().size() == 0) {
170-
return new Scalar(symbol);
213+
private Tuple2<String, Option<CNode>> configureMapping(CNode config) {
214+
Mapping mapping = unchecked(config::asMapping).apply();
215+
if (mapping.size() != 1) {
216+
throw new IllegalArgumentException("Single entry map expected to configure a " + target.getName());
217+
} else {
218+
Map.Entry<String, CNode> next = mapping.entrySet().iterator().next();
219+
return Tuple.of(next.getKey(), Option.some(next.getValue()));
171220
}
221+
}
222+
223+
private T doConfigure(ConfigurationContext context, Configurator<T> configurator, CNode subConfig) {
224+
return unchecked(() -> configurator.configure(subConfig, context)).apply();
225+
}
172226

173-
Mapping mapping = new Mapping();
174-
mapping.put(symbol, describe);
175-
return mapping;
227+
@SuppressWarnings("unchecked")
228+
private CNode convertToNode(ConfigurationContext context, Configurator configurator, Describable instance) {
229+
return unchecked(() -> configurator.describe(instance, context)).apply();
176230
}
177231
}

0 commit comments

Comments
 (0)