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}