Using map of maps as Maven plugin parameters

前端 未结 2 1577
既然无缘
既然无缘 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:16

    One solution is quite simple and works for 1-level nesting. A more sophisticated approach can be found in the alternative answer which possibly also allows for deeper nesting of Maps.

    Instead of using an interface as type parameter, simply use a concrete class like TreeMap

     @Parameter
     private Map<String, TreeMap> converters.
    

    The reason is this check in MapConverter which fails for an interface but suceeds for a concrete class:

       private static Class<?> findElementType( final Type[] typeArguments )
       {
           if ( null != typeArguments && typeArguments.length > 1 
                && typeArguments[1] instanceof Class<?> )
           {
               return (Class<?>) typeArguments[1];
           }
           return Object.class;
       }
    

    As a side-note, an as it is also related to this answer for Maven > 3.3.x it also works to install a custom converter by subclassing BasicComponentConfigurator and using it as a Plexus component. BasicComponentConfigurator has the DefaultConverterLookup as a protected member variable and is hence easily accessible for registering custom converters.

    0 讨论(0)
  • 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<String, String>). 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<String, String> as value but use a custom object:

    @Parameter
    private Map<String, Converter> converters;
    

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

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

    You can then configure your Mojo with:

    <converters>
      <json>
        <properties>
          <indent>true</indent>
          <strict>true</strict>
        </properties>
      </json>
      <yaml>
        <properties>
          <stripComments>false</stripComments>
        </properties>
      </yaml>
    </converters>
    

    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<String, Map<String, String>> 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:

      <?xml version="1.0" encoding="UTF-8"?>
      <component-set>
        <components>
          <component>
            <role>org.codehaus.plexus.component.configurator.ComponentConfigurator</role>
            <role-hint>custom-basic</role-hint>
            <implementation>package.to.CustomBasicComponentConfigurator</implementation>
          </component>
        </components>
      </component-set>
      

      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 <implementation> 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<String, Map<String, String>> 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<Object, Object> 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<Object, Object> 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<Object, Object>();
            }
    
            final Object impl = instantiateObject(implType);
            failIfNotTypeCompatible(impl, type, configuration);
            return (Map<Object, Object>) 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;
        }
    
    }
    
    0 讨论(0)
提交回复
热议问题