001/*
002 * Copyright 2017-2021 original authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.micronaut.maven.openapi;
017
018import io.micronaut.openapi.generator.AbstractMicronautJavaCodegen;
019import io.micronaut.openapi.generator.GeneratorOptionsBuilder;
020import io.micronaut.openapi.generator.MicronautCodeGenerator;
021import io.micronaut.openapi.generator.MicronautCodeGeneratorBuilder;
022import org.apache.commons.lang3.StringUtils;
023import org.apache.maven.plugin.MojoExecutionException;
024import org.apache.maven.plugins.annotations.LifecyclePhase;
025import org.apache.maven.plugins.annotations.Mojo;
026import org.apache.maven.plugins.annotations.Parameter;
027
028import java.lang.reflect.InvocationTargetException;
029import java.lang.reflect.Method;
030import java.util.Locale;
031import java.util.Map;
032
033/**
034 * A generic OpenAPI mojo that will be used for configuring custom Micronaut OpenAPI generator extensions.
035 */
036@Mojo(name = OpenApiGenericMojo.MOJO_NAME, defaultPhase = LifecyclePhase.GENERATE_SOURCES)
037public class OpenApiGenericMojo extends AbstractOpenApiMojo {
038
039    public static final String MOJO_NAME = "generate-openapi-generic";
040    public static final String CONFIGURATION_PROPERTIES = MICRONAUT_OPENAPI_PREFIX + ".generator.properties";
041
042    /**
043     * The classname of the generator to be used for code generation.
044     *
045     * <p>The generator must property overwrite the {@link AbstractMicronautJavaCodegen#optionsBuilder()} method
046     * and the builder should have setters or withers for the properties to be used in maven configuration.</p>
047     */
048    @Parameter(property = MICRONAUT_OPENAPI_PREFIX + ".generator.builder.classname")
049    protected String generatorClassName;
050
051    /**
052     * The configuration properties that will be passed on to the custom generator options builder.
053     *
054     * <p>Any configuration parameters with key {@code micronaut.openapi.generator.properties.[PROPERTY_NAME]}
055     * will be passed on to the generator options builder. String, integer and boolean value types are supported
056     * for additional properties.</p>
057     */
058    @Parameter(property = CONFIGURATION_PROPERTIES)
059    protected Map<String, String> properties;
060
061    @Override
062    protected boolean isEnabled() {
063        return generatorClassName != null;
064    }
065
066    @Override
067    protected void configureBuilder(MicronautCodeGeneratorBuilder builder) throws MojoExecutionException {
068        MicronautCodeGenerator<? extends GeneratorOptionsBuilder> generator = instantiateGenerator();
069
070        try {
071            builder.forCodeGenerator(generator, config -> {
072                for (Map.Entry<String, String> entry : properties.entrySet()) {
073                    String name = entry.getKey().substring(CONFIGURATION_PROPERTIES.length() + 1);
074                    String value = entry.getValue();
075                    invokeMethod(name, config, value);
076                }
077            });
078        } catch (OpenAPIInvocationException e) {
079            throw new MojoExecutionException(e);
080        }
081    }
082
083    private MicronautCodeGenerator<? extends GeneratorOptionsBuilder> instantiateGenerator() {
084        MicronautCodeGenerator<? extends GeneratorOptionsBuilder> generator;
085        try {
086            generator = (MicronautCodeGenerator<? extends GeneratorOptionsBuilder>) this.getClass()
087                .getClassLoader()
088                .loadClass(generatorClassName)
089                .getDeclaredConstructor()
090                .newInstance();
091        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) {
092            throw new RuntimeException(e);
093        }
094        return generator;
095    }
096
097    private static void invokeMethod(String name, GeneratorOptionsBuilder builder, String value) {
098        try {
099            String witherName = "with" + StringUtils.capitalize(name);
100            String setterName = "set" + StringUtils.capitalize(name);
101            Class<? extends GeneratorOptionsBuilder> builderClazz = builder.getClass();
102            var methods = builderClazz.getDeclaredMethods();
103            for (Method method : methods) {
104                if (invokeIfMatches(name, builder, value, witherName, setterName, method)) {
105                    return;
106                }
107            }
108            throw new OpenAPIInvocationException("Unable to find a method on builder " + builderClazz + " with name '" + name + "' which accepts argument '" + value + "'");
109        } catch (IllegalAccessException | InvocationTargetException ex) {
110            throw new OpenAPIInvocationException(ex);
111        }
112    }
113
114    private static boolean invokeIfMatches(String name, GeneratorOptionsBuilder builder, String value, String witherName, String setterName, Method method) throws IllegalAccessException, InvocationTargetException {
115        var methodName = method.getName();
116        if ((methodName.equals(name) || methodName.equals(witherName) || methodName.equals(setterName)) && method.getParameterCount() == 1) {
117            Class<?> parameterType = method.getParameterTypes()[0];
118            if (parameterType.equals(String.class)) {
119                method.invoke(builder, value);
120                return true;
121            } else if (parameterType.equals(Boolean.TYPE)) {
122                var coerced = value.toLowerCase(Locale.ENGLISH);
123                if ("true".equals(coerced) || "false".equals(coerced)) {
124                    method.invoke(builder, Boolean.parseBoolean(coerced));
125                    return true;
126                }
127            } else if (parameterType.equals(Integer.TYPE) && (value.matches("[0-9]+"))) {
128                method.invoke(builder, Integer.parseInt(value));
129                return true;
130
131            }
132        }
133        return false;
134    }
135
136    /**
137     * Exception to be thrown when OpenAPI generator configuration fails.
138     */
139    static class OpenAPIInvocationException extends RuntimeException {
140
141        public OpenAPIInvocationException(String message) {
142            super(message);
143        }
144
145        public OpenAPIInvocationException(Throwable throwable) {
146            super(throwable);
147        }
148    }
149}