Using map of maps as Maven plugin parameters

前端 未结 2 1575
既然无缘
既然无缘 2021-01-06 11:48

Is it possible to use a map of maps as a Maven plugin parameter?, e.g.

@Parameter
private Map

        
2条回答
  •  小鲜肉
    小鲜肉 (楼主)
    2021-01-06 12:26

    This is apparently a limitation of the sisu.plexus project internally used by the Mojo API. If you peek inside the MapConverter source, you'll find out that it first tries to fetch the value of the map by trying to interpret the configuration as a String (invoking fromExpression), and when this fails, looks up the expected type of the value. However this method doesn't check for parameterized types, which is our case here (since the type of the map value is Map). I filed the bug 498757 on the Bugzilla of this project to track this.

    Using a custom wrapper object

    One workaround would be to not use a Map as value but use a custom object:

    @Parameter
    private Map converters;
    

    with a class Converter, located in the same package as the Mojo, being:

    public class Converter {
    
        @Parameter
        private Map properties;
    
        @Override
        public String toString() { return properties.toString(); } // to test
    
    }
    

    You can then configure your Mojo with:

    
      
        
          true
          true
        
      
      
        
          false
        
      
    
    

    This configuration will correctly inject the values in the inner-maps. It also keeps the variable aspect: the object is only introduced as a wrapper around the inner-map. I tested this with a simple test mojo having

    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info(converters.toString());
    }
    

    and the output was the expected {json={indent=true, strict=true}, yaml={stripComments=false}}.

    Using a custom configurator

    I also found a way to keep a Map> by using a custom ComponentConfigurator.

    So we want to fix MapConverter by inhering it, the trouble is how to register this new FixedMapConverter. By default, Maven uses a BasicComponentConfigurator to configure the Mojo and it relies on a DefaultConverterLookup to look-up for converters to use for a specific class. In this case, we want to provide a custom converted for Map that will return our fixed version. Therefore, we need to extend this basic configurator and register our new converter.

    import org.codehaus.plexus.classworlds.realm.ClassRealm;
    import org.codehaus.plexus.component.configurator.BasicComponentConfigurator;
    import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
    import org.codehaus.plexus.component.configurator.ConfigurationListener;
    import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
    import org.codehaus.plexus.configuration.PlexusConfiguration;
    
    public class CustomBasicComponentConfigurator extends BasicComponentConfigurator {
        @Override
        public void configureComponent(final Object component, final PlexusConfiguration configuration,
                final ExpressionEvaluator evaluator, final ClassRealm realm, final ConfigurationListener listener)
                throws ComponentConfigurationException {
            converterLookup.registerConverter(new FixedMapConverter());
            super.configureComponent(component, configuration, evaluator, realm, listener);
        }
    }
    

    Then we need to tell Maven to use this new configurator instead of the basic one. This is a 2-step process:

    1. Inside your Maven plugin, create a file src/main/resources/META-INF/plexus/components.xml registering the new component:

      
      
        
          
            org.codehaus.plexus.component.configurator.ComponentConfigurator
            custom-basic
            package.to.CustomBasicComponentConfigurator
          
        
      
      

      Note a few things: we declare a new component having the hint "custom-basic", this will serve as an id to refer to it and the refers to the fully qualified class name of our configurator.

    2. Tell our Mojo to use this configurator with the configurator attribute of the @Mojo annotation:

      @Mojo(name = "test", configurator = "custom-basic")
      

      The configurator passed here corresponds to the role-hint specified in the components.xml above.

    With such a set-up, you can finally declare

    @Parameter
    private Map> converters;
    

    and everything will be injected properly: Maven will use our custom configurator, that will register our fixed version of the map converter and will correctly convert the inner-maps.


    Full code of FixedMapConverter (which pretty much copy-pastes MapConverter because we can't override the faulty method):

    public class FixedMapConverter extends MapConverter {
    
        public Object fromConfiguration(final ConverterLookup lookup, final PlexusConfiguration configuration,
                final Class type, final Type[] typeArguments, final Class enclosingType, final ClassLoader loader,
                final ExpressionEvaluator evaluator, final ConfigurationListener listener)
                throws ComponentConfigurationException {
            final Object value = fromExpression(configuration, evaluator, type);
            if (null != value) {
                return value;
            }
            try {
                final Map map = instantiateMap(configuration, type, loader);
                final Class elementType = findElementType(typeArguments);
                if (Object.class == elementType || String.class == elementType) {
                    for (int i = 0, size = configuration.getChildCount(); i < size; i++) {
                        final PlexusConfiguration element = configuration.getChild(i);
                        map.put(element.getName(), fromExpression(element, evaluator));
                    }
                    return map;
                }
                // handle maps with complex element types...
                final ConfigurationConverter converter = lookup.lookupConverterForType(elementType);
                for (int i = 0, size = configuration.getChildCount(); i < size; i++) {
                    Object elementValue;
                    final PlexusConfiguration element = configuration.getChild(i);
                    try {
                        elementValue = converter.fromConfiguration(lookup, element, elementType, enclosingType, //
                                loader, evaluator, listener);
                    }
                    // TEMP: remove when http://jira.codehaus.org/browse/MSHADE-168
                    // is fixed
                    catch (final ComponentConfigurationException e) {
                        elementValue = fromExpression(element, evaluator);
    
                        Logs.warn("Map in " + enclosingType + " declares value type as: {} but saw: {} at runtime",
                                elementType, null != elementValue ? elementValue.getClass() : null);
                    }
                    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    map.put(element.getName(), elementValue);
                }
                return map;
            } catch (final ComponentConfigurationException e) {
                if (null == e.getFailedConfiguration()) {
                    e.setFailedConfiguration(configuration);
                }
                throw e;
            }
        }
    
        @SuppressWarnings("unchecked")
        private Map instantiateMap(final PlexusConfiguration configuration, final Class type,
                final ClassLoader loader) throws ComponentConfigurationException {
            final Class implType = getClassForImplementationHint(type, configuration, loader);
            if (null == implType || Modifier.isAbstract(implType.getModifiers())) {
                return new TreeMap();
            }
    
            final Object impl = instantiateObject(implType);
            failIfNotTypeCompatible(impl, type, configuration);
            return (Map) impl;
        }
    
        private static Class findElementType( final Type[] typeArguments )
        {
            if ( null != typeArguments && typeArguments.length > 1 )
            {
                if ( typeArguments[1] instanceof Class )
                {
                    return (Class) typeArguments[1];
                }
                // begin fix here
                if ( typeArguments[1] instanceof ParameterizedType )
                {
                    return (Class) ((ParameterizedType) typeArguments[1]).getRawType();
                }
                // end fix here
            }
            return Object.class;
        }
    
    }
    

提交回复
热议问题