11package 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+
36import edu .umd .cs .findbugs .annotations .CheckForNull ;
47import edu .umd .cs .findbugs .annotations .NonNull ;
58import hudson .model .Describable ;
912import io .jenkins .plugins .casc .Attribute ;
1013import io .jenkins .plugins .casc .ConfigurationContext ;
1114import io .jenkins .plugins .casc .Configurator ;
12- import io .jenkins .plugins .casc .ConfiguratorException ;
1315import io .jenkins .plugins .casc .ObsoleteConfigurationMonitor ;
1416import io .jenkins .plugins .casc .impl .attributes .DescribableAttribute ;
1517import io .jenkins .plugins .casc .model .CNode ;
1618import io .jenkins .plugins .casc .model .Mapping ;
1719import 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 ;
1825import jenkins .model .Jenkins ;
1926import org .jenkinsci .Symbol ;
2027import org .kohsuke .accmod .Restricted ;
2431import java .util .List ;
2532import java .util .Map ;
2633import java .util .Set ;
34+ import java .util .function .Predicate ;
2735import 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}.
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 () ? "\n Possible 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