diff --git a/documentation/documentation.gradle b/documentation/documentation.gradle index ec22779e5f..15ee582734 100644 --- a/documentation/documentation.gradle +++ b/documentation/documentation.gradle @@ -17,6 +17,7 @@ apply from: rootProject.file( 'gradle/releasable.gradle' ) apply plugin: 'org.hibernate.matrix-test' apply plugin: 'org.hibernate.orm.build.reports' +apply plugin: 'org.hibernate.orm.build.properties' tasks.build.dependsOn 'buildDocs' defaultTasks 'buildDocs' @@ -105,6 +106,23 @@ asciidoctorj { options logDocuments: true } +// Collect config properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +task collectConfigProperties { task -> + group 'Documentation' + description 'Collect config properties' + + // make sure that the javadocs are generated prior to collecting properties. + dependsOn ':hibernate-core:javadoc' + dependsOn ':hibernate-envers:javadoc' + dependsOn ':hibernate-jcache:javadoc' + + dependsOn tasks.generateConfigPropertiesMap + dependsOn tasks.writeConfigPropertiesMap + + tasks.buildDocs.dependsOn task + tasks.buildDocsForPublishing.dependsOn task + +} // Topical Guides ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -160,6 +178,8 @@ task renderUserGuide(type: AsciidoctorTask, group: 'Documentation') {task-> tasks.buildDocs.dependsOn task tasks.buildDocsForPublishing.dependsOn task + dependsOn tasks.collectConfigProperties + sourceDir = file( 'src/main/asciidoc/userguide' ) sources { include 'Hibernate_User_Guide.adoc' diff --git a/documentation/src/main/asciidoc/userguide/ConfigPropertyList.adoc b/documentation/src/main/asciidoc/userguide/ConfigPropertyList.adoc new file mode 100644 index 0000000000..0cdee70fc7 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/ConfigPropertyList.adoc @@ -0,0 +1,3 @@ +== List of all available configuration properties + +include::../../../../target/configs.asciidoc[opts=optional] \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc b/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc index 3034b49f0c..a781d8199f 100644 --- a/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc +++ b/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc @@ -42,5 +42,6 @@ include::appendices/Legacy_DomainModel.adoc[] include::appendices/LegacyBasicTypeResolution.adoc[] include::appendices/Legacy_Native_Queries.adoc[] +include::ConfigPropertyList.adoc[] include::Bibliography.adoc[] diff --git a/local-build-plugins/build.gradle b/local-build-plugins/build.gradle index 2497bfc35e..445de243db 100644 --- a/local-build-plugins/build.gradle +++ b/local-build-plugins/build.gradle @@ -21,6 +21,7 @@ dependencies { implementation 'jakarta.json.bind:jakarta.json.bind-api:2.0.0' implementation 'jakarta.json:jakarta.json-api:2.0.1' implementation 'org.eclipse:yasson:2.0.4' + implementation 'org.jsoup:jsoup:1.15.3' } tasks.compileJava { @@ -63,6 +64,10 @@ gradlePlugin { id = 'org.hibernate.orm.build.env-project' implementationClass = 'org.hibernate.orm.env.EnvironmentProjectPlugin' } + configPropertiesCollectorPlugin { + id = 'org.hibernate.orm.build.properties' + implementationClass = 'org.hibernate.orm.properties.ConfigPropertyCollectorPlugin' + } } } diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/ConfigPropertyCollectorPlugin.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/ConfigPropertyCollectorPlugin.java new file mode 100644 index 0000000000..f596805bcb --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/ConfigPropertyCollectorPlugin.java @@ -0,0 +1,39 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties; + +import org.hibernate.orm.properties.processor.ConfigPropertyHolder; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; + +public class ConfigPropertyCollectorPlugin implements Plugin { + public static final String TASK_GROUP_NAME = "hibernate-properties"; + + @Override + public void apply(Project project) { + final Task groupingTask = project.getTasks().maybeCreate( "generateHibernateConfigProperties" ); + groupingTask.setGroup( TASK_GROUP_NAME ); + + ConfigPropertyHolder propertyHolder = new ConfigPropertyHolder(); + final ConfigPropertyCollectorTask configPropertyCollectorTask = project.getTasks().create( + "generateConfigPropertiesMap", + ConfigPropertyCollectorTask.class, + propertyHolder + ); + groupingTask.dependsOn( configPropertyCollectorTask ); + + final ConfigPropertyWriterTask configPropertyWriterTask = project.getTasks().create( + "writeConfigPropertiesMap", + ConfigPropertyWriterTask.class, + propertyHolder + ); + groupingTask.dependsOn( configPropertyWriterTask ); + configPropertyWriterTask.dependsOn( configPropertyCollectorTask ); + } +} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/ConfigPropertyCollectorTask.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/ConfigPropertyCollectorTask.java new file mode 100644 index 0000000000..f502b7ff37 --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/ConfigPropertyCollectorTask.java @@ -0,0 +1,177 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import javax.inject.Inject; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import org.hibernate.orm.properties.processor.ConfigPropertyHolder; +import org.hibernate.orm.properties.processor.Configuration; +import org.hibernate.orm.properties.processor.ConfigurationPropertyProcessor; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.UnknownDomainObjectException; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskAction; + +/** + * Task that goes to the root project and then sends all the sources from children projects trough annotation processing + * collecting all the config properties into a map. See {@link ConfigurationPropertyProcessor} + */ +public class ConfigPropertyCollectorTask extends DefaultTask { + + private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + private final Project project; + private final ConfigPropertyHolder properties; + private final Path target; + + @Inject + public ConfigPropertyCollectorTask(ConfigPropertyHolder properties, Project project) { + this.project = project; + this.properties = properties; + this.target = project.getBuildDir().toPath(); + } + + + @TaskAction + public void collectProperties() { + for ( Map.Entry projectEntry : project.getRootProject().getChildProjects().entrySet() ) { + try { + // we don't need to look at testing projects as these aren't for public configurations. + if ( projectEntry.getKey().contains( "test" ) ) { + continue; + } + SourceSetContainer sources = projectEntry.getValue().getExtensions().getByType( + SourceSetContainer.class ); + + sources.all( s -> { + // no need to compile/process test sources: + if ( !"test".equals( s.getName() ) ) { + compile( + projectEntry.getValue(), + s.getAllJava().getSourceDirectories().getFiles(), + s.getCompileClasspath().getFiles() + ); + } + } ); + } + catch (UnknownDomainObjectException e) { + getLogger().info( "Ignoring " + projectEntry.getKey() + " because of " + e.getMessage(), e ); + } + } + } + + public boolean compile(Project project, Collection sources, Collection classpath) { + List classes = new ArrayList<>(); + for ( File sourceFile : sources ) { + try { + // need to find all java files to be later converted to compilation units: + Files.walkFileTree( sourceFile.toPath(), new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if ( file.getFileName().toString().endsWith( ".java" ) ) { + classes.add( file.toFile() ); + } + return FileVisitResult.CONTINUE; + } + } ); + } + catch (IOException e) { + getLogger().debug( "Failed to process " + sourceFile.getAbsolutePath(), e ); + } + } + + if ( classes.isEmpty() ) { + return true; + } + + StandardJavaFileManager fileManager = compiler.getStandardFileManager( null, null, null ); + Iterable compilationUnits = fileManager.getJavaFileObjects( + classes.stream().toArray( File[]::new ) ); + + try { + // we don't really need the compiled classes, so we just dump them somewhere to not mess with the rest of the + // classes: + fileManager.setLocation( + StandardLocation.CLASS_OUTPUT, + Arrays.asList( + Files.createDirectories( target.resolve( "config-property-collector-compiled-classes" ) ) + .toFile() ) + ); + fileManager.setLocation( + StandardLocation.CLASS_PATH, + classpath + ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + + List options = new ArrayList<>(); + options.add( + String.format( + Locale.ROOT, + "-A%s=%s", + Configuration.MODULE_TITLE, + project.getDescription() + ) + ); + options.add( + String.format( + Locale.ROOT, + "-A%s=%s", + Configuration.MODULE_LINK_ANCHOR, + project.getName() + "-" + ) + ); + // todo: this should come from some plugin/task config rather than be hardcoded: + options.add( + String.format( + Locale.ROOT, + "-A%s=%s", + Configuration.JAVADOC_LINK, + "https://docs.jboss.org/hibernate/orm/6.2/javadocs/" + ) + ); + + JavaCompiler.CompilationTask task = compiler.getTask( + null, + fileManager, + null, + options, + null, + compilationUnits + ); + + task.setProcessors( Arrays.asList( new ConfigurationPropertyProcessor( + project.getBuildDir().toPath().resolve( "docs/javadoc" ), + properties + ) ) ); + + return task.call(); + } + + +} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/ConfigPropertyWriterTask.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/ConfigPropertyWriterTask.java new file mode 100644 index 0000000000..167255fa86 --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/ConfigPropertyWriterTask.java @@ -0,0 +1,78 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import javax.inject.Inject; + +import org.hibernate.orm.properties.processor.AsciiDocWriter; +import org.hibernate.orm.properties.processor.ConfigPropertyHolder; +import org.hibernate.orm.properties.processor.ConfigurationProperty; +import org.hibernate.orm.properties.processor.HibernateOrmConfiguration; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.tasks.TaskAction; + +/** + * Task that writes two asciidoc files from the collected config properties. One is for public configurations, another - for SPI. + */ +public class ConfigPropertyWriterTask extends DefaultTask { + + private static final Predicate> API_FILTER = entry -> HibernateOrmConfiguration.Type.API.equals( + entry.getValue().type() ); + private static final Predicate> SPI_FILTER = entry -> HibernateOrmConfiguration.Type.SPI.equals( + entry.getValue().type() ); + + private final Project project; + private final ConfigPropertyHolder properties; + private final String fileName = "configs"; + + @Inject + public ConfigPropertyWriterTask(Project project, ConfigPropertyHolder properties) { + this.project = project; + this.properties = properties; + } + + @TaskAction + public void writeProperties() { + if ( properties.hasProperties() ) { + if ( properties.hasProperties( API_FILTER ) ) { + writeProperties( + fileName + ".asciidoc", + new AsciiDocWriter( + API_FILTER + ) + ); + } + if ( properties.hasProperties( SPI_FILTER ) ) { + writeProperties( + fileName + "-spi.asciidoc", + new AsciiDocWriter( + SPI_FILTER + ) + ); + } + } + } + + private void writeProperties(String fileName, BiConsumer, Writer> transformer) { + try ( Writer writer = new FileWriter( project.getBuildDir().toPath().resolve( fileName ).toFile() ) + ) { + properties.write( transformer, writer ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + +} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/AnnotationUtils.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/AnnotationUtils.java new file mode 100644 index 0000000000..04990bb08f --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/AnnotationUtils.java @@ -0,0 +1,62 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties.processor; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; + + +public final class AnnotationUtils { + + private AnnotationUtils() { + } + + public static boolean isIgnored(Element element) { + return findAnnotation( element, HibernateOrmConfiguration.class ) + .flatMap( a -> a.attribute( "ignore", Boolean.class ) ) + .orElse( false ); + } + + public static Optional findAnnotation(Element element, Class annotation) { + for ( AnnotationMirror mirror : element.getAnnotationMirrors() ) { + if ( mirror.getAnnotationType().toString().equals( annotation.getName() ) ) { + return Optional.of( new AnnotationAttributeHolder( mirror ) ); + } + } + return Optional.empty(); + } + + public static class AnnotationAttributeHolder { + private final AnnotationMirror annotationMirror; + + private AnnotationAttributeHolder(AnnotationMirror annotationMirror) { + this.annotationMirror = annotationMirror; + } + + public Optional attribute(String name, Class klass) { + return annotationMirror.getElementValues().entrySet().stream() + .filter( entry -> entry.getKey().getSimpleName().contentEquals( name ) ) + .map( entry -> klass.cast( entry.getValue().getValue() ) ) + .findAny(); + } + + public Optional> multiAttribute(String name, Class klass) { + return annotationMirror.getElementValues().entrySet().stream() + .filter( entry -> entry.getKey().getSimpleName().contentEquals( name ) ) + .map( entry -> entry.getValue().getValue() ) + .map( obj -> ( (List) List.class.cast( obj ) ) ) + .map( list -> list.stream().map( AnnotationValue.class::cast ).map( AnnotationValue::getValue ) + .map( klass::cast ).collect( Collectors.toList() ) ) + .findAny(); + } + } + +} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/AsciiDocWriter.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/AsciiDocWriter.java new file mode 100644 index 0000000000..268616ec8b --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/AsciiDocWriter.java @@ -0,0 +1,128 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties.processor; + + +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class AsciiDocWriter implements BiConsumer, Writer> { + + private final Predicate> filter; + + public AsciiDocWriter(Predicate> filter) { + this.filter = filter; + } + + @Override + public void accept(Map propertyMap, Writer writer) { + Map> groups = propertyMap.entrySet().stream() + .filter( filter ) + .map( Map.Entry::getValue ) + .collect( + Collectors.groupingBy( + ConfigurationProperty::moduleName, + TreeMap::new, + Collectors.toCollection( TreeSet::new ) + ) + ); + + if ( groups.isEmpty() ) { + // nothing to write - return fast. + return; + } + + try { + for ( Map.Entry> entry : groups.entrySet() ) { + tryToWriteLine( writer, "[[configuration-properties-aggregated-", entry.getValue().iterator().next().anchorPrefix(), "]]" ); + tryToWriteLine( writer, "=== ", entry.getKey() ); + writer.write( '\n' ); + for ( ConfigurationProperty el : entry.getValue() ) { + Iterator keys = el.key().resolvedKeys().iterator(); + String firstKey = keys.next(); + writer.write( "[[" ); + writer.write( "configuration-properties-aggregated-" ); + writer.write( el.anchorPrefix() ); + writer.write( firstKey.replaceAll( "[^\\w-.]", "_" ) ); + writer.write( "]] " ); + + writer.write( '`' ); + writer.write( firstKey ); + writer.write( '`' ); + writer.write( "::\n" ); + + // using inline passthrough for javadocs to not render HTML. + writer.write( "+++ " ); + writer.write( el.javadoc() ); + writer.write( " +++ " ); + + String defaultValue = Objects.toString( el.defaultValue(), "" ); + if ( !defaultValue.trim().isEmpty() ) { + writer.write( "\n+\n" ); + writer.write( "Default value: `" ); + writer.write( defaultValue ); + writer.write( '`' ); + } + + writer.write( '\n' ); + + printOtherKeyVariants( writer, keys ); + } + } + writer.write( '\n' ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + + private void printOtherKeyVariants(Writer writer, Iterator keys) throws IOException { + boolean hasMultipleKeys = false; + if ( keys.hasNext() ) { + hasMultipleKeys = true; + writer.write( "+\n" ); + writer.write( ".Variants of this configuration property (Click here):\n" ); + writer.write( "[%collapsible]\n" ); + writer.write( "====\n" ); + } + while ( keys.hasNext() ) { + writer.write( '`' ); + writer.write( keys.next() ); + writer.write( '`' ); + if ( keys.hasNext() ) { + writer.write( ", " ); + } + } + + if ( hasMultipleKeys ) { + writer.write( "\n====\n" ); + } + } + + private void tryToWriteLine(Writer writer, String prefix, String value, String... other) { + try { + writer.write( prefix ); + writer.write( value ); + for ( String s : other ) { + writer.write( s ); + } + writer.write( "\n" ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } +} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigPropertyHolder.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigPropertyHolder.java new file mode 100644 index 0000000000..16a9685c17 --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigPropertyHolder.java @@ -0,0 +1,39 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties.processor; + +import java.io.Writer; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +public class ConfigPropertyHolder { + + private final Map properties = new TreeMap<>(); + + + public boolean isEmpty() { + return properties.isEmpty(); + } + + public void write(BiConsumer, Writer> transformer, Writer writer) { + transformer.accept( this.properties, writer ); + } + + public void put(String key, ConfigurationProperty property) { + properties.put( key, property ); + } + + public boolean hasProperties() { + return !properties.isEmpty(); + } + + public boolean hasProperties(Predicate> filter) { + return properties.entrySet().stream().anyMatch( filter ); + } +} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/Configuration.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/Configuration.java new file mode 100644 index 0000000000..6e634a89b3 --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/Configuration.java @@ -0,0 +1,41 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties.processor; + +/** + * Lists the config parameters that can be passed to this annotation processor via {@code -A.....}. + */ +public final class Configuration { + private Configuration() { + } + + private static final String HIBERNATE_ORM_CPCAP_PREFIX = "org.hibernate.orm.cpcap."; + + /** + * Use to define a base URL for Hibernate ORM Javadoc. As we are getting parts of Javadoc links in it should + * be adjusted to point to somewhere where the docs actually live. + */ + public static final String JAVADOC_LINK = HIBERNATE_ORM_CPCAP_PREFIX + "javadoc.link"; + /** + * Use to define a pattern for classes to be ignored by this collector. We can have some {@code *Settings} classes + * in {@code impl} packages. And we don't need to collect properties from those. + */ + public static final String IGNORE_PATTERN = HIBERNATE_ORM_CPCAP_PREFIX + "ignore.pattern"; + /** + * Use to define a pattern for property key values that should be ignored. By default, we will ignore keys that end + * with a dot {@code '.'}. + */ + public static final String IGNORE_KEY_VALUE_PATTERN = HIBERNATE_ORM_CPCAP_PREFIX + "ignore.key.value.pattern"; + /** + * Used to group properties in sections and as a title of that section. + */ + public static final String MODULE_TITLE = HIBERNATE_ORM_CPCAP_PREFIX + "module.title"; + /** + * Used to group properties in sections and as a title of that section. + */ + public static final String MODULE_LINK_ANCHOR = HIBERNATE_ORM_CPCAP_PREFIX + "module.link.anchor"; +} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigurationProperty.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigurationProperty.java new file mode 100644 index 0000000000..e8f976ab73 --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigurationProperty.java @@ -0,0 +1,185 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties.processor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class ConfigurationProperty implements Comparable { + + private static final Comparator CONFIGURATION_PROPERTY_COMPARATOR = Comparator.comparing( + c -> c.key().key ); + private Key key; + private String javadoc; + private String sourceClass; + + private HibernateOrmConfiguration.Type type; + + private Object defaultValue; + + private String anchorPrefix; + private String moduleName; + + public Key key() { + return key; + } + + public ConfigurationProperty key(Key key) { + this.key = key; + return this; + } + + public String javadoc() { + return javadoc; + } + + public ConfigurationProperty javadoc(String javadoc) { + this.javadoc = javadoc == null ? "" : javadoc; + return this; + } + + public String sourceClass() { + return sourceClass; + } + + public ConfigurationProperty sourceClass(String sourceClass) { + this.sourceClass = sourceClass; + return this; + } + + public HibernateOrmConfiguration.Type type() { + return type; + } + + public ConfigurationProperty type(HibernateOrmConfiguration.Type type) { + this.type = type; + return this; + } + + public Object defaultValue() { + return defaultValue; + } + + public ConfigurationProperty defaultValue(Object defaultValue) { + this.defaultValue = defaultValue == null ? "" : defaultValue; + return this; + } + + public String anchorPrefix() { + return anchorPrefix; + } + + public ConfigurationProperty withAnchorPrefix(String anchorPrefix) { + this.anchorPrefix = anchorPrefix.replaceAll( "[^\\w-.]", "_" ); + return this; + } + + public String moduleName() { + return moduleName; + } + + public ConfigurationProperty withModuleName(String moduleName) { + this.moduleName = moduleName; + return this; + } + + @Override + public String toString() { + return "ConfigurationProperty{" + + "key='" + key + '\'' + + ", javadoc='" + javadoc + '\'' + + ", sourceClass='" + sourceClass + '\'' + + ", type='" + type + '\'' + + ", default='" + defaultValue + '\'' + + ", anchorPrefix='" + anchorPrefix + '\'' + + ", moduleName='" + moduleName + '\'' + + '}'; + } + + @Override + public int compareTo(ConfigurationProperty o) { + return CONFIGURATION_PROPERTY_COMPARATOR.compare( this, o ); + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + ConfigurationProperty that = (ConfigurationProperty) o; + return Objects.equals( key, that.key ) && + Objects.equals( javadoc, that.javadoc ) && + Objects.equals( sourceClass, that.sourceClass ) && + type == that.type && + Objects.equals( defaultValue, that.defaultValue ) && + Objects.equals( anchorPrefix, that.anchorPrefix ) && + Objects.equals( moduleName, that.moduleName ); + } + + @Override + public int hashCode() { + return Objects.hash( key, javadoc, sourceClass, type, defaultValue, anchorPrefix, moduleName ); + } + + public static class Key { + private final List prefixes; + private final String key; + + public Key(List prefixes, String key) { + this.key = key; + this.prefixes = prefixes; + } + + public void overridePrefixes(String... prefixes) { + overridePrefixes( Arrays.asList( prefixes ) ); + } + + public void overridePrefixes(List prefixes) { + this.prefixes.clear(); + this.prefixes.addAll( prefixes ); + } + + public boolean matches(Pattern pattern) { + return pattern.matcher( key ).matches(); + } + + public List resolvedKeys() { + if ( prefixes.isEmpty() ) { + return Collections.singletonList( key ); + } + else { + return prefixes.stream() + .map( p -> p + key ) + .collect( Collectors.toList() ); + } + } + + @Override + public String toString() { + return toString( "/" ); + } + + private String toString(String delimiter) { + if ( prefixes.isEmpty() ) { + return key; + } + else { + return prefixes.stream() + .map( p -> p + key ) + .collect( Collectors.joining( delimiter ) ); + } + } + } +} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigurationPropertyCollector.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigurationPropertyCollector.java new file mode 100644 index 0000000000..cf0a60bc7a --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigurationPropertyCollector.java @@ -0,0 +1,226 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties.processor; + +import static org.hibernate.orm.properties.processor.AnnotationUtils.findAnnotation; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Name; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.util.Elements; +import javax.tools.Diagnostic; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +public class ConfigurationPropertyCollector { + + // assume that spi/impl/internal packages are not for public use and consider all of them as SPI: + private static final Pattern SPI_PATTERN = Pattern.compile( + "(.*\\.spi$)|(.*\\.spi\\..*)|(.*\\.impl$)|(.*\\.impl\\..*)|(.*\\.internal$)|(.*\\.internal\\..*)" ); + + private final Set processedTypes = new HashSet<>(); + private final ConfigPropertyHolder properties; + private final Elements elementUtils; + private final String title; + private final String anchor; + private final Path javadocsLocation; + private final String javadocsBaseLink; + private final Pattern ignoreKeys; + private final Messager messager; + + public ConfigurationPropertyCollector(ConfigPropertyHolder properties, ProcessingEnvironment processingEnvironment, + String title, + String anchor, Path javadocsLocation, Pattern ignoreKeys, + String javadocsBaseLink) { + this.properties = properties; + this.elementUtils = processingEnvironment.getElementUtils(); + this.title = title; + this.anchor = anchor; + this.javadocsLocation = javadocsLocation; + this.javadocsBaseLink = javadocsBaseLink; + this.ignoreKeys = ignoreKeys; + this.messager = processingEnvironment.getMessager(); + } + + public void visitType(TypeElement element) { + Name qualifiedName = element.getQualifiedName(); + if ( !processedTypes.contains( qualifiedName ) ) { + processedTypes.add( qualifiedName ); + + Optional annotation = findAnnotation( + element, HibernateOrmConfiguration.class ); + Optional> classPrefix = annotation + .flatMap( a -> a.multiAttribute( "prefix", String.class ) ); + Optional title = annotation.flatMap( a -> a.attribute( "title", String.class ) ); + Optional anchorPrefix = annotation.flatMap( a -> a.attribute( "anchorPrefix", String.class ) ); + + for ( Element inner : elementUtils.getAllMembers( element ) ) { + if ( inner.getKind().equals( ElementKind.FIELD ) && inner instanceof VariableElement ) { + processConstant( ( (VariableElement) inner ), classPrefix, title, anchorPrefix ); + } + } + } + } + + private void processConstant(VariableElement constant, Optional> classPrefix, + Optional classTitle, + Optional classAnchorPrefix) { + Optional annotation = findAnnotation( + constant, HibernateOrmConfiguration.class ); + if ( annotation.flatMap( a -> a.attribute( "ignore", Boolean.class ) ).orElse( false ) ) { + return; + } + + Optional title = annotation.flatMap( a -> a.attribute( "title", String.class ) ); + Optional anchorPrefix = annotation.flatMap( a -> a.attribute( "anchorPrefix", String.class ) ); + + ConfigurationProperty.Key key = extractKey( + constant, + classPrefix, + annotation.flatMap( a -> a.multiAttribute( "prefix", String.class ) ) + ); + if ( !key.matches( ignoreKeys ) ) { + // Try to find a default value. Assumption is that the settings class has an inner class called "Defaults" and + // the key for the default value is exactly the same as the config constant name: + Object value = findDefault( constant ); + + properties.put( + constant.getEnclosingElement().toString() + "#" + constant.getSimpleName().toString(), + new ConfigurationProperty() + .javadoc( extractJavadoc( constant ) ) + .key( key ) + .sourceClass( constant.getEnclosingElement().toString() ) + .type( extractType( constant ) ) + .defaultValue( value ) + .withModuleName( title.orElse( classTitle.orElse( this.title ) ) ) + .withAnchorPrefix( anchorPrefix.orElse( classAnchorPrefix.orElse( this.anchor ) ) ) + ); + } + } + + + private ConfigurationProperty.Key extractKey(VariableElement constant, Optional> classPrefix, + Optional> constantPrefix) { + List prefix; + if ( constantPrefix.isPresent() ) { + prefix = constantPrefix.get(); + } + else if ( classPrefix.isPresent() ) { + prefix = classPrefix.get(); + } + else { + prefix = Collections.emptyList(); + } + + return new ConfigurationProperty.Key( + prefix, + Objects.toString( constant.getConstantValue(), "NOT_FOUND#" + constant.getSimpleName() ) + ); + } + + private HibernateOrmConfiguration.Type extractType(VariableElement constant) { + String packageName = packageElement( constant ).getQualifiedName().toString(); + return SPI_PATTERN.matcher( packageName ).matches() ? + HibernateOrmConfiguration.Type.SPI : + HibernateOrmConfiguration.Type.API; + } + + private String extractJavadoc(VariableElement constant) { + try { + Element enclosingClass = constant.getEnclosingElement(); + Path docs = javadocsLocation.resolve( + enclosingClass.toString().replace( ".", File.separator ) + ".html" + ); + + String packagePath = packageElement( enclosingClass ).getQualifiedName().toString().replace( ".", File.separator ); + + Document javadoc = Jsoup.parse( docs.toFile() ); + + org.jsoup.nodes.Element block = javadoc.selectFirst( "#" + constant.getSimpleName() + " + ul li.blockList"); + if ( block != null ) { + for ( org.jsoup.nodes.Element link : block.getElementsByTag( "a" ) ) { + String href = link.attr( "href" ); + // only update links if they are not external: + if ( !link.hasClass( "external-link" ) ) { + if ( href.startsWith( "#" ) ) { + href = enclosingClass.getSimpleName().toString() + ".html" + href; + } + href = javadocsBaseLink + packagePath + "/" + href; + } + else if ( href.contains( "/build/parents/" ) && href.contains( "/apidocs" ) ) { + // means a link was to a class from other module and javadoc plugin generated some external link + // that won't work. So we replace it: + href = javadocsBaseLink + href.substring( href.indexOf( "/apidocs" ) + "/apidocs".length() ); + } + link.attr( "href", href ); + } + + org.jsoup.nodes.Element result = new org.jsoup.nodes.Element( "div" ); + for ( org.jsoup.nodes.Element child : block.children() ) { + if ( "h4".equalsIgnoreCase( child.tagName() ) || "pre".equalsIgnoreCase( child.tagName() ) ) { + continue; + } + result.appendChild( child ); + } + + return result.toString(); + } + else { + return elementUtils.getDocComment( constant ); + } + } + catch (IOException e) { + messager.printMessage( Diagnostic.Kind.NOTE, "Wasn't able to find rendered javadocs for " + constant + ". Trying to read plain javadoc comment." ); + return elementUtils.getDocComment( constant ); + } + } + + /** + * This really works only for string/primitive constants ... other types would just get null returned. + */ + private Object findDefault(VariableElement constant) { + if ( constant.getEnclosingElement() instanceof TypeElement ) { + for ( Element element : elementUtils.getAllMembers( (TypeElement) constant.getEnclosingElement() ) ) { + if ( ElementKind.CLASS.equals( element.getKind() ) + && element.getSimpleName().contentEquals( "Defaults" ) ) { + for ( Element enclosedElement : element.getEnclosedElements() ) { + if ( enclosedElement.getSimpleName().equals( constant.getSimpleName() ) ) { + return ( (VariableElement) enclosedElement ).getConstantValue(); + } + } + } + } + } + return null; + } + + private PackageElement packageElement(Element element) { + Element packageElement = element; + while ( !( packageElement instanceof PackageElement ) && packageElement.getEnclosingElement() != null ) { + packageElement = packageElement.getEnclosingElement(); + } + + return packageElement instanceof PackageElement ? (PackageElement) packageElement : null; + } + +} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigurationPropertyProcessor.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigurationPropertyProcessor.java new file mode 100644 index 0000000000..367fd89ca8 --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/ConfigurationPropertyProcessor.java @@ -0,0 +1,126 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties.processor; + + +import static org.hibernate.orm.properties.processor.AnnotationUtils.isIgnored; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; + + +@SupportedAnnotationTypes("*") +@SupportedSourceVersion(SourceVersion.RELEASE_8) +@SupportedOptions({ + Configuration.JAVADOC_LINK, + Configuration.IGNORE_PATTERN, + Configuration.IGNORE_KEY_VALUE_PATTERN, + Configuration.MODULE_TITLE, + Configuration.MODULE_LINK_ANCHOR +}) +public class ConfigurationPropertyProcessor extends AbstractProcessor { + + private ConfigurationPropertyCollector propertyCollector; + private Optional ignore; + private final Path javadocFolder; + private final ConfigPropertyHolder properties; + + public ConfigurationPropertyProcessor(Path javadocFolder, ConfigPropertyHolder properties) { + this.javadocFolder = javadocFolder; + this.properties = properties; + } + + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init( processingEnv ); + + String pattern = processingEnv.getOptions().get( Configuration.IGNORE_PATTERN ); + this.ignore = Optional.ofNullable( pattern ).map( Pattern::compile ); + String title = processingEnv.getOptions().getOrDefault( Configuration.MODULE_TITLE, "Unknown" ); + String anchor = processingEnv.getOptions().getOrDefault( Configuration.MODULE_LINK_ANCHOR, "hibernate-orm-" ); + + String javadocsBaseLink = processingEnv.getOptions().getOrDefault( Configuration.JAVADOC_LINK, "" ); + + String keyPattern = processingEnv.getOptions().getOrDefault( Configuration.IGNORE_KEY_VALUE_PATTERN, ".*\\.$" ); + Pattern ignoreKeys = Pattern.compile( keyPattern ); + + this.propertyCollector = new ConfigurationPropertyCollector( + properties, + processingEnv, + title, + anchor, + javadocFolder, + ignoreKeys, + javadocsBaseLink + ); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + Set rootElements = roundEnv.getRootElements(); + + // first let's go through all root elements and see if we can find *Settings classes: + for ( Element element : rootElements ) { + if ( isSettingsClass( element ) ) { + process( propertyCollector, element ); + } + } + + // means we might have some inner classes that we also wanted to consider for config property processing + // so let's see if we need to process any: + for ( TypeElement annotation : annotations ) { + if ( annotation.getQualifiedName().contentEquals( HibernateOrmConfiguration.class.getName() ) ) { + Set elements = roundEnv.getElementsAnnotatedWith( annotation ); + for ( Element element : elements ) { + if ( isTypeElement( element ) ) { + process( propertyCollector, element ); + } + } + } + } + + if ( roundEnv.processingOver() ) { + beforeExit(); + } + + return true; + } + + private void beforeExit() { + // processor won't generate anything another gradle task would create an asciidoc file. + } + + private void process(ConfigurationPropertyCollector propertyCollector, Element element) { + if ( !isIgnored( element ) && !ignore.map( p -> p.matcher( element.toString() ).matches() ).orElse( + Boolean.FALSE ) ) { + propertyCollector.visitType( (TypeElement) element ); + } + } + + private boolean isSettingsClass(Element element) { + return ( element.getKind().equals( ElementKind.CLASS ) || element.getKind().equals( ElementKind.INTERFACE ) ) && + element.getSimpleName().toString().endsWith( "Settings" ); + } + + private boolean isTypeElement(Element element) { + return element instanceof TypeElement; + } + +} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/HibernateOrmConfiguration.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/HibernateOrmConfiguration.java new file mode 100644 index 0000000000..62deaa933e --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/processor/HibernateOrmConfiguration.java @@ -0,0 +1,56 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.properties.processor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation to define overrides configured by annotation processor configuration. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.TYPE, ElementType.FIELD }) +public @interface HibernateOrmConfiguration { + + /** + * Describes to which type the configuration property belongs to - API/SPI. + */ + enum Type { + /** + * Configuration property type API/SPI will be determined by inspecting the package in which a class is located. + * In case package contains {@code spi} package at any upper levels the type will be {@code SPI}, otherwise - {@code API} + */ + API, + SPI + } + + /** + * Set to {@code true} in case we have a {@code *Settings} class that we want to ignore in config processing. + * Also works on a field leve. Setting it to {@code true} on field level will not include that particular constant. + * Can be useful to skip prefix definitions etc. + */ + boolean ignore() default false; + + /** + * Overrides a prefix provided by annotation processor configuration. If set on class level - all constants from that class will + * use this prefix. If set on field level - that particular constant will use the configured prefix and will ignore the + * one set by annotation processor configuration or at class level. + */ + String[] prefix() default ""; + + /** + * Used to group properties in sections and as a title of that grouped section. + */ + String title() default ""; + + /** + * Used as part of generated anchor links to provide uniqueness. + */ + String anchorPrefix() default ""; +}