org.apache.openjpa.persistence.meta.AnnotationProcessor6 Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.persistence.meta;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
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.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import jakarta.persistence.metamodel.StaticMetamodel;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.MetaDataFactory;
import org.apache.openjpa.persistence.PersistenceMetaDataFactory;
import org.apache.openjpa.persistence.PersistentCollection;
import org.apache.openjpa.persistence.util.SourceCode;
/**
* Annotation processing tool generates source code for a meta-model class given
* the annotated source code of persistent entity.
*
* This tool is invoked during compilation for JDK6 compiler if
*
* - OpenJPA and JPA libraries are available in the compiler classpath
* and
- Annotation Processor option
-Aopenjpa.metamodel=true
is specified.
*
*
* Usage
* $ javac -classpath path/to/openjpa-all.jar -Aopenjpa.metamodel=true mypackage/MyEntity.java
* will generate source code for canonical meta-model class mypackage.MyEntity_.java
.
* The source code is generated relative to the directory specified in -s
option
* of javac
compiler and defaulted to the current directory.
*
* The Annotation Processor also recognizes the following options (none of them are mandatory):
*
* -Aopenjpa.log={log level} The logging level. Default is WARN
. Permissible values are
* TRACE
, INFO
, WARN
or ERROR
.
* -Aopenjpa.source={n} Java source version of the generated code. Default is 6
.
* -Aopenjpa.naming={class name} fully-qualified name of a class implementing
* org.apache.openjpa.meta.MetaDataFactory
that determines
* the name of a meta-class given the name of the original persistent Java entity class. Defaults to
* org.apache.openjpa.persistence.PersistenceMetaDataFactory
which appends a underscore character
* (_
) to the original Java class name.
* -Aopenjpa.header={url}
* A url whose content will appear as comment header to the generated file(s). Recognizes special value
* ASL
for Apache Source License header as comment. By default adds a OpenJPA proprietary
* text.
*
*
*
* @author Pinaki Poddar
*
* @since 2.0.0
*
*/
@SupportedAnnotationTypes({
"jakarta.persistence.Entity",
"jakarta.persistence.Embeddable",
"jakarta.persistence.MappedSuperclass" })
@SupportedOptions({ "openjpa.log",
"openjpa.source",
"openjpa.naming",
"openjpa.header",
"openjpa.metamodel",
"openjpa.addGeneratedAnnotation"
})
public class AnnotationProcessor6 extends AbstractProcessor {
private SourceAnnotationHandler handler;
private MetaDataFactory factory;
private int generatedSourceVersion = 6;
private CompileTimeLogger logger;
private List header = new ArrayList<>();
private boolean active;
private static Localizer _loc = Localizer.forPackage(AnnotationProcessor6.class);
private SourceVersion supportedSourceVersion;
private String addGeneratedOption;
private Class> generatedAnnotation;
private Date generationDate;
/**
* Category of members as per JPA 2.0 type system.
*
*/
private static enum TypeCategory {
ATTRIBUTE("jakarta.persistence.metamodel.SingularAttribute"),
COLLECTION("jakarta.persistence.metamodel.CollectionAttribute"),
SET("jakarta.persistence.metamodel.SetAttribute"),
LIST("jakarta.persistence.metamodel.ListAttribute"),
MAP("jakarta.persistence.metamodel.MapAttribute");
private String type;
private TypeCategory(String type) {
this.type = type;
}
public String getMetaModelType() {
return type;
}
}
/**
* Enumerates available java.util.* collection classes to categorize them
* into corresponding JPA meta-model member type.
*/
private static List CLASSNAMES_LIST = Arrays.asList(
new String[]{
"java.util.List", "java.util.AbstractList",
"java.util.AbstractSequentialList", "java.util.ArrayList",
"java.util.Stack", "java.util.Vector"});
private static List CLASSNAMES_SET = Arrays.asList(
new String[]{
"java.util.Set", "java.util.AbstractSet", "java.util.EnumSet",
"java.util.HashSet", "java.util.LinkedList", "java.util.LinkedHashSet",
"java.util.SortedSet", "java.util.TreeSet"});
private static List CLASSNAMES_MAP = Arrays.asList(
new String[]{
"java.util.Map", "java.util.AbstractMap", "java.util.EnumMap",
"java.util.HashMap", "java.util.Hashtable",
"java.util.IdentityHashMap", "java.util.LinkedHashMap",
"java.util.Properties", "java.util.SortedMap",
"java.util.TreeMap"});
private static List CLASSNAMES_COLLECTION = Arrays.asList(
new String[]{
"java.util.Collection", "java.util.AbstractCollection",
"java.util.AbstractQueue", "java.util.Queue",
"java.util.PriorityQueue"});
/**
* Gets the fully-qualified name of member class in JPA 2.0 type system,
* given the fully-qualified name of a Java class.
*
*/
private TypeCategory toMetaModelTypeCategory(TypeMirror mirror,
String name, boolean persistentCollection) {
if (mirror.getKind() == TypeKind.ARRAY && persistentCollection ) {
return TypeCategory.LIST;
}
if (CLASSNAMES_COLLECTION.contains(name))
return TypeCategory.COLLECTION;
if (CLASSNAMES_LIST.contains(name))
return TypeCategory.LIST;
if (CLASSNAMES_SET.contains(name))
return TypeCategory.SET;
if (CLASSNAMES_MAP.contains(name))
return TypeCategory.MAP;
return TypeCategory.ATTRIBUTE;
}
@Override
public SourceVersion getSupportedSourceVersion() {
if (supportedSourceVersion != null) {
return supportedSourceVersion;
}
return SourceVersion.latestSupported();
}
/**
* Initialization.
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
active = "true".equalsIgnoreCase(getOptionValue("openjpa.metamodel"));
if (!active)
return;
final String supported = getOptionValue("openjpa.processor.supportedversion");
if (supported != null) {
supportedSourceVersion = SourceVersion.valueOf(supported);
} else { // default to ensure we don't log a false warning for every compilation, see OPENJPA-2300
supportedSourceVersion = SourceVersion.latestSupported();
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, _loc.get("mmg-tool-banner").toString());
logger = new CompileTimeLogger(processingEnv, getOptionValue("openjpa.log"));
setSourceVersion();
setNamingPolicy();
setHeader();
handler = new SourceAnnotationHandler(processingEnv, logger);
setAddGeneratedAnnotation();
this.generationDate = new Date();
}
/**
* The entry point for java compiler.
*/
@Override
public boolean process(Set extends TypeElement> annos, RoundEnvironment roundEnv) {
if (active && !roundEnv.processingOver()) {
Set extends Element> elements = roundEnv.getRootElements();
for (Element e : elements) {
if (e instanceof TypeElement) {
process((TypeElement) e);
}
}
}
return true;
}
/**
* Generate meta-model source code for the given type.
*
* @return true if code is generated for the given element. false otherwise.
*/
private boolean process(TypeElement e) {
if (!handler.isAnnotatedAsEntity(e)) {
return false;
}
Elements eUtils = processingEnv.getElementUtils();
String originalClass = eUtils.getBinaryName((TypeElement) e).toString();
String originalSimpleClass = e.getSimpleName().toString();
String metaClass = factory.getMetaModelClassName(originalClass);
SourceCode source = new SourceCode(metaClass);
comment(source);
annotate(source, originalClass);
TypeElement supCls = handler.getPersistentSupertype(e);
if (supCls != null) {
String superName = factory.getMetaModelClassName(supCls.toString());
source.getTopLevelClass().setSuper(superName);
}
try {
PrintWriter writer = createSourceFile(originalClass, metaClass, e);
SourceCode.Class modelClass = source.getTopLevelClass();
Set extends Element> members = handler.getPersistentMembers(e);
for (Element m : members) {
boolean isPersistentCollection = m.getAnnotation(PersistentCollection.class) != null;
TypeMirror decl = handler.getDeclaredType(m);
String fieldName = handler.getPersistentMemberName(m);
String fieldType = handler.getDeclaredTypeName(decl, true, isPersistentCollection);
TypeCategory typeCategory =
toMetaModelTypeCategory(decl, fieldType, isPersistentCollection);
String metaModelType = typeCategory.getMetaModelType();
SourceCode.Field modelField = null;
switch (typeCategory) {
case ATTRIBUTE:
modelField = modelClass.addField(fieldName, metaModelType);
modelField.addParameter(originalSimpleClass)
.addParameter(fieldType);
break;
case COLLECTION:
case LIST:
case SET:
TypeMirror param = handler.getTypeParameter(m, decl, 0, true);
String elementType = handler.getDeclaredTypeName(param);
modelField = modelClass.addField(fieldName, metaModelType);
modelField.addParameter(originalSimpleClass)
.addParameter(elementType);
break;
case MAP:
TypeMirror key = handler.getTypeParameter(m, decl, 0, false);
TypeMirror value = handler.getTypeParameter(m, decl, 1, true);
String keyType = handler.getDeclaredTypeName(key);
String valueType = handler.getDeclaredTypeName(value);
modelField = modelClass.addField(fieldName, metaModelType);
modelField.addParameter(originalSimpleClass)
.addParameter(keyType)
.addParameter(valueType);
break;
}
modelField.makePublic().makeStatic().makeVolatile();
}
source.write(writer);
writer.flush();
writer.close();
return true;
} catch (Exception e1) {
logger.error(_loc.get("mmg-process-error", e.getQualifiedName()), e1);
return false;
}
}
private void annotate(SourceCode source, String originalClass) {
SourceCode.Class cls = source.getTopLevelClass();
cls.addAnnotation(StaticMetamodel.class.getName())
.addArgument("value", originalClass + ".class", false);
switch (this.addGeneratedOption) {
case "false":
return;
case "force":
cls.addAnnotation(jakarta.annotation.Generated.class.getName())
.addArgument("value", this.getClass().getName())
.addArgument("date", this.generationDate.toString());
break;
case "auto":
// fall through
default:
// only add the annotation if it is on the classpath for Java 6+.
if (generatedAnnotation != null && generatedSourceVersion >= 6) {
cls.addAnnotation(generatedAnnotation.getName())
.addArgument("value", this.getClass().getName())
.addArgument("date", this.generationDate.toString());
}
break;
}
}
private void comment(SourceCode source) {
if (header.size() != 0)
source.addComment(false, header.toArray(new String[header.size()]));
String defaultHeader = _loc.get("mmg-tool-sign").getMessage();
source.addComment(false, defaultHeader);
}
/**
* Parse annotation processor option -Aopenjpa.source=n
to detect
* the source version for the generated classes.
* n must be a integer. Default or wrong specification returns 6.
*/
private void setSourceVersion() {
String version = getOptionValue("openjpa.source");
if (version != null) {
try {
generatedSourceVersion = Integer.parseInt(version);
} catch (NumberFormatException e) {
logger.warn(_loc.get("mmg-bad-source", version, 6));
generatedSourceVersion = 6;
}
} else {
generatedSourceVersion = 6;
}
}
private void setNamingPolicy() {
String policy = getOptionValue("openjpa.naming");
if (policy != null) {
try {
factory = (MetaDataFactory)Class.forName(policy).newInstance();
} catch (Throwable e) {
logger.warn(_loc.get("mmg-bad-naming", policy, e));
factory = new PersistenceMetaDataFactory();
}
} else {
factory = new PersistenceMetaDataFactory();
}
}
private void setHeader() {
String headerOption = getOptionValue("openjpa.header");
if (headerOption == null) {
return;
}
if ("ASL".equalsIgnoreCase(headerOption)) {
header.add(_loc.get("mmg-asl-header").getMessage());
} else {
try {
URL url = new URL(headerOption);
InputStream is = url.openStream();
Scanner s = new Scanner(is);
while (s.hasNextLine()) {
header.add(s.nextLine());
}
} catch (Throwable t) {
}
}
}
private void setAddGeneratedAnnotation() {
this.addGeneratedOption = getOptionValue("openjpa.addGeneratedAnnotation");
if (this.addGeneratedOption == null) {
this.addGeneratedOption = "auto";
}
// only add the annotation if it is on the classpath for Java 6+.
try {
this.generatedAnnotation = Class.forName("jakarta.annotation.Generated", false, null);
} catch (ClassNotFoundException generatedNotFoundEx) {
logger.trace(_loc.get("mmg-annotation-not-found"));
}
}
/**
* Creates a file where source code of the given metaClass will be written.
*
*/
private PrintWriter createSourceFile(String originalClass, String metaClass, TypeElement e)
throws IOException {
JavaFileObject javaFile = processingEnv.getFiler().createSourceFile(metaClass, e);
logger.info(_loc.get("mmg-process", javaFile.toUri().normalize()));
return new PrintWriter(javaFile.openWriter());
}
/**
* Get the value for the given keys, whoever matches first, in the current available options.
*/
private String getOptionValue(String... keys) {
Map options = processingEnv.getOptions();
for (String key : keys) {
if (options.containsKey(key))
return options.get(key);
}
return null;
}
}