Processor.java
/*
* SPDX-FileCopyrightText: 2025 kaumei.io
* SPDX-License-Identifier: Apache-2.0
*/
package io.kaumei.jdbc.anno;
import com.palantir.javapoet.AnnotationSpec;
import io.kaumei.jdbc.anno.ctx.Context;
import io.kaumei.jdbc.anno.ctx.JavaMessenger;
import org.jspecify.annotations.Nullable;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
import java.util.Set;
@SupportedAnnotationTypes({
Processor.JDBC_NATIVE_NAME,
Processor.JDBC_SELECT_NAME,
Processor.JDBC_BATCH_UPDATE_NAME,
Processor.JDBC_UPDATE_NAME,
Processor.JDBC_CONFIG_NAME
})
@SupportedOptions({
Processor.OPTION_KEY_DEBUG_FOLDER,
Processor.OPTION_KEY_CONFIG,
Processor.OPTION_KEY_LOG_LEVEL,
})
public final class Processor extends AbstractProcessor {
// @formatter:off
public static final String JDBC_CONFIG_NAME = "io.kaumei.jdbc.annotation.config.JdbcConfig";
public static final String JDBC_BATCH_UPDATE_NAME = "io.kaumei.jdbc.annotation.JdbcBatchUpdate";
public static final String JDBC_NATIVE_NAME = "io.kaumei.jdbc.annotation.JdbcNative";
public static final String JDBC_SELECT_NAME = "io.kaumei.jdbc.annotation.JdbcSelect";
public static final String JDBC_UPDATE_NAME = "io.kaumei.jdbc.annotation.JdbcUpdate";
public static final String OPTION_KEY_CONFIG = "io.kaumei.jdbc.processor.config";
public static final String OPTION_KEY_DEBUG_FOLDER = "io.kaumei.jdbc.processor.debugfolder";
public static final String OPTION_KEY_LOG_LEVEL = "io.kaumei.jdbc.processor.loglevel";
// @formatter:on
// ------------------------------------------------------------------------
public static final String FILE_COMMENT = """
Generated by Kaumei JDBC annotation processor.
This code is generated and is not licensed under the Apache License 2.0.
You may use, modify, and distribute this file without restriction.
SPDX-License-Identifier: CC0-1.0
""";
public static final AnnotationSpec GENERATED = AnnotationSpec.builder(Generated.class)
.addMember("value", "$S", Processor.class.getCanonicalName())
.addMember("date", "$S", OffsetDateTime.now().toString())
.build();
// ---------------------------------------------------------------
// ----- services
@Nullable
private Context ctx;
@Nullable
private JavaMessenger logger;
// ----- state
private int roundCount = 0;
private long timeInMillis;
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
this.ctx = new Context(env);
this.logger = this.ctx.logger;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
roundCount++;
return process0(annotations, roundEnv);
} catch (Exception e) {
if (this.logger != null) {
this.logger.error(e);
} else {
e.printStackTrace();
}
return true;
}
}
private boolean process0(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (this.ctx == null || this.logger == null) {
throw new ProcessorException("Illegal state: logger is null");
} else if (roundEnv.processingOver()) {
// this.logger.always("Processing is over. Skip JDBC annotation processing.");
return false;
} else if (roundEnv.errorRaised()) {
this.logger.always("Processing raised error. Skip JDBC annotation processing.");
return false;
} else if (roundEnv.getRootElements() == null || roundEnv.getRootElements().isEmpty()) {
this.logger.always("No root elements found. Skip JDBC annotation processing.");
return false;
} else if (annotations.isEmpty()) {
this.logger.always("No annotations found. Skip JDBC annotation processing.", "roundCount", roundCount);
return false;
}
this.logger.debug("Start Kaumei JDBC annotation processing");
var startTime = System.currentTimeMillis();
var jdbcRoundEnv = new ProcessorEnvironment(this.ctx, annotations, roundEnv);
String dumpState = "ok";
try {
this.ctx.kaumeiConfig.process(jdbcRoundEnv);
this.ctx.sourceMethodService.process(jdbcRoundEnv);
this.ctx.kaumeiJdbc2Java.process(jdbcRoundEnv);
this.ctx.kaumeiJava2Jdbc.process(jdbcRoundEnv);
this.ctx.kaumeiJdbcGenerator.process(jdbcRoundEnv);
} catch (RuntimeException e) {
dumpState = e.getMessage();
throw e;
} finally {
timeInMillis += (System.currentTimeMillis() - startTime);
this.logger.always("Kaumei JDBC annotation processor finished in " + timeInMillis + "ms");
dumpState(jdbcRoundEnv, dumpState);
}
return true;
}
private void dumpState(ProcessorEnvironment jdbcRoundEnv, String state) {
if (this.ctx == null || !this.ctx.kaumeiConfig.dump()) {
return;
}
var folder = this.ctx.kaumeiConfig.dumpFolder();
try {
if (!Files.exists(folder)) {
Files.createDirectories(folder);
}
var runId = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
// ----- create run data
var sb = new StringBuilder();
this.ctx.kaumeiJdbc2Java.dump(sb);
this.ctx.kaumeiJava2Jdbc.dump(sb);
var dataString = sb.toString();
var currentHash = sha256Short(dataString); // use a hash, so we can check if there are changes
var lastDataFile = search(folder);
if (!lastDataFile.contains(currentHash)) {
var data = folder.resolve("data-" + runId + "-" + currentHash + ".txt");
Files.writeString(data, dataString, StandardOpenOption.CREATE_NEW);
}
// ----- add run stats
var runPath = folder.resolve("./run.csv");
if (!Files.exists(runPath)) {
var header = "timestamp, millis, jdbcInterfaces,jdbcToJava, basicJdbc,globalJdbc,localJdbc, basicJava,globalJava,localJava, hashJdbc, state\n";
Files.writeString(runPath, header, StandardOpenOption.CREATE_NEW);
}
var row = runId
+ ", " + timeInMillis
+ ", " + jdbcRoundEnv.jdbcInterfaces().size()
+ ", " + this.ctx.kaumeiJdbc2Java.csvStats()
+ ", " + this.ctx.kaumeiJava2Jdbc.csvStats()
+ ", " + currentHash
+ ", " + state
+ "\n";
Files.writeString(runPath, row, StandardOpenOption.APPEND);
} catch (Exception e) {
if (this.logger != null) {
this.logger.warn("Dumping state failed. ", "folder", folder, "msg", e);
} else {
System.out.println("Dumping state failed. folder=" + folder + ", msg=" + e.getMessage()); // Acceptable in this context
}
}
}
private static String search(Path folder) throws IOException {
try (var stream = Files.list(folder)) {
Optional<Path> last = stream.filter(p -> p.getFileName().toString().startsWith("data-"))
.sorted() // lexicographically ascending
.reduce((first, second) -> second); // keep only the last
return last.map(path -> path.getFileName().toString()).orElse("");
}
}
private static String sha256Short(String s) throws NoSuchAlgorithmException {
MessageDigest d = MessageDigest.getInstance("SHA-256");
byte[] hash = d.digest(s.getBytes(StandardCharsets.UTF_8));
// take first 12 hex chars (48 bits)
StringBuilder sb = new StringBuilder(12);
for (int i = 0; i < 6; i++) {
sb.append(String.format("%02X", hash[i]));
}
return sb.toString();
}
}