blob: 58f10359cf012627556815a9288a8bfd8b02de02 [file] [log] [blame]
/*
* Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// 08/10/2009-2.0 Guy Pelletier
// - 267391: JPA 2.0 implement/extend/use an APT tooling library for MetaModel API canonical classes
// 04/27/2010-2.1 Guy Pelletier
// - 309856: MappedSuperclasses from XML are not being initialized properly
// 06/14/2010-2.2 Guy Pelletier
// - 264417: Table generation is incorrect for JoinTables in AssociationOverrides
// 08/12/2010-2.2 Guy Pelletier
// - 298118: canonical metamodel generation with untyped Map throws NPE
// 08/25/2010-2.2 Guy Pelletier
// - 309445: CannonicalModelProcessor process all files
// 10/18/2010-2.2 Guy Pelletier
// - 322921: OutOfMemory in annotation processor
// 11/23/2010-2.2 Guy Pelletier
// - 330660: Canonical model generator throws ClassCastException when using package-info.java
package org.eclipse.persistence.internal.jpa.modelgen;
import java.io.IOException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.PrimitiveType;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Entity;
import jakarta.persistence.MappedSuperclass;
import javax.tools.JavaFileObject;
import org.eclipse.persistence.Version;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.internal.jpa.metadata.MetadataLogger;
import org.eclipse.persistence.internal.jpa.metadata.MetadataProject;
import org.eclipse.persistence.internal.jpa.metadata.accessors.classes.ClassAccessor;
import org.eclipse.persistence.internal.jpa.metadata.accessors.mappings.MappedKeyMapAccessor;
import org.eclipse.persistence.internal.jpa.metadata.accessors.mappings.MappingAccessor;
import org.eclipse.persistence.internal.jpa.metadata.accessors.objects.MetadataAnnotatedElement;
import org.eclipse.persistence.internal.jpa.metadata.accessors.objects.MetadataClass;
import org.eclipse.persistence.internal.jpa.modelgen.objects.PersistenceUnit;
import org.eclipse.persistence.internal.jpa.modelgen.objects.PersistenceUnitReader;
import org.eclipse.persistence.internal.jpa.modelgen.visitors.TypeVisitor;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.logging.AbstractSessionLog;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.sessions.DatabaseLogin;
import org.eclipse.persistence.sessions.Project;
import org.eclipse.persistence.sessions.server.ServerSession;
/**
* The main APT processor to generate the JPA 2.0 Canonical model.
*
* @author Guy Pelletier
* @since EclipseLink 1.2
*/
public class CanonicalModelProcessor extends AbstractProcessor {
protected enum AttributeType {CollectionAttribute, ListAttribute, MapAttribute, SetAttribute, SingularAttribute }
protected MetadataMirrorFactory nonStaticFactory;
protected static MetadataMirrorFactory staticFactory;
private SessionLog log;
private boolean useStaticFactory;
private boolean generateComments;
private boolean generateTimestamp;
private boolean generateGenerated;
private static final Set<String> SUPPORTED_ANNOTATIONS = Collections.unmodifiableSet(new HashSet<String>() {{
if (SourceVersion.latest().compareTo(SourceVersion.RELEASE_8) > 0) {
add("java.persistence/jakarta.persistence.*");
}
add("jakarta.persistence.*");
add("org.eclipse.persistence.annotations.*");
}});
private static final Set<String> SUPPORTED_OPTIONS = Collections.unmodifiableSet(new HashSet<String>() {{
add(CanonicalModelProperties.CANONICAL_MODEL_PREFIX);
add(CanonicalModelProperties.CANONICAL_MODEL_SUFFIX);
add(CanonicalModelProperties.CANONICAL_MODEL_SUB_PACKAGE);
add(CanonicalModelProperties.CANONICAL_MODEL_LOAD_XML);
add(CanonicalModelProperties.CANONICAL_MODEL_USE_STATIC_FACTORY);
add(CanonicalModelProperties.CANONICAL_MODEL_GENERATE_GENERATED);
add(CanonicalModelProperties.CANONICAL_MODEL_GENERATE_TIMESTAMP);
add(CanonicalModelProperties.CANONICAL_MODEL_GENERATE_COMMENTS);
add(CanonicalModelProperties.CANONICAL_MODEL_PROCESSOR_LOG_LEVEL);
add(CanonicalModelProperties.CANONICAL_MODEL_GLOBAL_LOG_LEVEL);
add(PersistenceUnitProperties.ECLIPSELINK_PERSISTENCE_XML);
add("verbose"); //shortcut to enable FINER logging
}});
/**
* Default constructor.
*/
public CanonicalModelProcessor() {
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return SUPPORTED_ANNOTATIONS;
}
@Override
public Set<String> getSupportedOptions() {
return SUPPORTED_OPTIONS;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
Map<String, String> options = processingEnv.getOptions();
log = new MessagerLog(processingEnv.getMessager(), options);
if (Boolean.parseBoolean(options.get("verbose")) && log.getLevel() > SessionLog.FINER) {
log.setLevel(SessionLog.FINER);
}
AbstractSessionLog.setLog(log);
// Log processing environment options
for (Map.Entry<String, String> option : options.entrySet()) {
log(SessionLog.CONFIG, "Found Option: {0}, with value: {1}",
option.getKey(), option.getValue());
}
useStaticFactory = Boolean.parseBoolean(CanonicalModelProperties.getOption(
CanonicalModelProperties.CANONICAL_MODEL_USE_STATIC_FACTORY,
CanonicalModelProperties.CANONICAL_MODEL_USE_STATIC_FACTORY_DEFAULT,
options));
generateGenerated = Boolean.parseBoolean(CanonicalModelProperties.getOption(
CanonicalModelProperties.CANONICAL_MODEL_GENERATE_GENERATED,
CanonicalModelProperties.CANONICAL_MODEL_GENERATE_GENERATED_DEFAULT,
options));
if (generateGenerated) {
generateTimestamp = Boolean.parseBoolean(CanonicalModelProperties.getOption(
CanonicalModelProperties.CANONICAL_MODEL_GENERATE_TIMESTAMP,
CanonicalModelProperties.CANONICAL_MODEL_GENERATE_TIMESTAMP_DEFAULT,
options));
generateComments = Boolean.parseBoolean(CanonicalModelProperties.getOption(
CanonicalModelProperties.CANONICAL_MODEL_GENERATE_COMMENTS,
CanonicalModelProperties.CANONICAL_MODEL_GENERATE_COMMENTS_DEFAULT,
options));
}
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (! roundEnv.processingOver() && ! roundEnv.errorRaised()) {
MetadataMirrorFactory factory = null;
try {
if (useStaticFactory) {
if (staticFactory == null) {
// We must remember some state from one round to another.
// In some rounds, the user may only change one class
// meaning we only have one root element from the round.
// If it is a child class to an existing already generated
// parent class we need to know about this class, so the
// factory will also hang onto static projects for each
// persistence unit. Doing this is going to need careful
// cleanup thoughts though. Adding classes ok, but what
// about removing some?
AbstractSession session = new ServerSession(new Project(new DatabaseLogin()));
session.setSessionLog(log);
final MetadataLogger logger = new MetadataLogger(session);
staticFactory = new MetadataMirrorFactory(logger,
Thread.currentThread().getContextClassLoader());
log(SessionLog.INFO, "Creating static metadata factory ...");
}
factory = staticFactory;
} else {
if (nonStaticFactory == null) {
AbstractSession session = new ServerSession(new Project(new DatabaseLogin()));
session.setSessionLog(log);
final MetadataLogger logger = new MetadataLogger(session);
nonStaticFactory = new MetadataMirrorFactory(logger,
Thread.currentThread().getContextClassLoader());
log(SessionLog.INFO, "Creating non-static metadata factory ...");
}
factory = nonStaticFactory;
}
final MetadataLogger logger = factory.getLogger();
// Step 1 - The factory is passed around so those who want the
// processing or round env can get it off the factory. This
// saves us from having to pass around multiple objects.
factory.setEnvironments(processingEnv, roundEnv);
// Step 2 - read the persistence xml classes (gives us extra
// classes and mapping files. From them we get transients and
// access). Metadata read from XML causes new accessors to be
// created and override existing ones (causing them to be un-
// pre-processed. We can never tell what changes in XML so we
// have to do this.
final PersistenceUnitReader puReader = new PersistenceUnitReader(logger, processingEnv);
puReader.initPersistenceUnits(factory);
// Step 3 - iterate over all the persistence units and generate
// their canonical model classes.
for (PersistenceUnit persistenceUnit : factory.getPersistenceUnits()) {
// Step 3a - add the Entities not defined in XML that are
// being compiled.
for (Element element : roundEnv.getElementsAnnotatedWith(Entity.class)) {
persistenceUnit.addEntityAccessor(element);
}
// Step 3b - add the Embeddables not defined in XML that are
// being compiled.
for (Element element : roundEnv.getElementsAnnotatedWith(Embeddable.class)) {
persistenceUnit.addEmbeddableAccessor(element);
}
// Step 3c - add the MappedSuperclasses not defined in XML
// that are being compiled.
for (Element element : roundEnv.getElementsAnnotatedWith(MappedSuperclass.class)) {
persistenceUnit.addMappedSuperclassAccessor(element);
}
// Step 3d - tell the persistence unit to pre-process itself.
persistenceUnit.preProcessForCanonicalModel();
// Step 3e - We're set, generate the canonical model classes.
generateCanonicalModelClasses(factory, persistenceUnit);
}
} catch (Exception e) {
log.logThrowable(SessionLog.SEVERE, SessionLog.PROCESSOR, e);
throw new RuntimeException(e);
}
}
return false; // Don't claim any annotations
}
/**
* INTERNAL:
*/
protected void generateCanonicalModelClass(MetadataClass metadataClass, Element element, PersistenceUnit persistenceUnit) throws IOException {
Writer writer = null;
try {
ClassAccessor accessor = persistenceUnit.getClassAccessor(metadataClass);
String qualifiedName = accessor.getAccessibleObjectName();
String className = getName(qualifiedName);
String classPackage = getPackage(qualifiedName);
String qualifiedCanonicalName = persistenceUnit.getQualifiedCanonicalName(qualifiedName);
String canonicalName = getName(qualifiedCanonicalName);
String canonicalpackage = getPackage(qualifiedCanonicalName);
boolean isNewJava = SourceVersion.RELEASE_8.compareTo(processingEnv.getSourceVersion()) < 0;
JavaFileObject file = processingEnv.getFiler().createSourceFile(qualifiedCanonicalName, element);
writer = file.openWriter();
// Print the package if we have one.
if (! canonicalpackage.equals("")) {
writer.append("package " + canonicalpackage + ";\n\n");
}
// Go through the accessor list, ignoring any transient accessors
// to build our attributes and import list.
ArrayList<String> attributes = new ArrayList<>();
HashMap<String, String> imports = new HashMap<>();
if (generateGenerated) {
if (isNewJava) {
imports.put("Generated", "javax.annotation.processing.Generated");
} else {
imports.put("Generated", "jakarta.annotation.Generated");
}
}
// Import the model class if the canonical class is generated elsewhere.
if (! classPackage.equals(canonicalpackage)) {
imports.put(className, qualifiedName);
}
for (MappingAccessor mappingAccessor : accessor.getDescriptor().getMappingAccessors()) {
if (! mappingAccessor.isTransient()) {
MetadataAnnotatedElement annotatedElement = mappingAccessor.getAnnotatedElement();
// Must go through the mapping accessor for the raw class
// since it may be a virtual mapping accessor with an
// attribute type.
MetadataClass rawClass = mappingAccessor.getRawClass();
// NOTE: order of checking is important.
String attributeType;
String types = className;
if (mappingAccessor.isBasic()) {
types = types + ", " + getUnqualifiedType(getBoxedType(annotatedElement, rawClass), imports);
attributeType = AttributeType.SingularAttribute.name();
imports.put(attributeType, "jakarta.persistence.metamodel.SingularAttribute");
} else {
if (rawClass.isList()) {
attributeType = AttributeType.ListAttribute.name();
imports.put(attributeType, "jakarta.persistence.metamodel.ListAttribute");
} else if (rawClass.isSet()) {
attributeType = AttributeType.SetAttribute.name();
imports.put(attributeType, "jakarta.persistence.metamodel.SetAttribute");
} else if (rawClass.isMap()) {
attributeType = AttributeType.MapAttribute.name();
imports.put(attributeType, "jakarta.persistence.metamodel.MapAttribute");
} else if (rawClass.isCollection()) {
attributeType = AttributeType.CollectionAttribute.name();
imports.put(attributeType, "jakarta.persistence.metamodel.CollectionAttribute");
} else {
attributeType = AttributeType.SingularAttribute.name();
imports.put(attributeType, "jakarta.persistence.metamodel.SingularAttribute");
}
if (mappingAccessor.isMapAccessor()) {
if (mappingAccessor.isMappedKeyMapAccessor()) {
MetadataClass mapKeyClass = ((MappedKeyMapAccessor) mappingAccessor).getMapKeyClass();
types = types + ", " + getUnqualifiedType(mapKeyClass.getName(), imports) + ", " + getUnqualifiedType(mappingAccessor.getReferenceClassName(), imports);
} else {
String mapKeyType;
if (annotatedElement.isGenericCollectionType()) {
// Grab the map key class from the generic.
mapKeyType = annotatedElement.getGenericType().get(1);
} else {
if (mappingAccessor.getReferenceDescriptor().hasIdAccessor()) {
// Grab the id type from the reference descriptor, now there's a handle!
MappingAccessor idAccessor = mappingAccessor.getReferenceDescriptor().getIdAccessors().values().iterator().next();
mapKeyType = idAccessor.getReferenceClassName();
} else {
// We don't know at this point so just use the catch all default.
mapKeyType = TypeVisitor.GENERIC_TYPE;
}
}
types = types + ", " + getUnqualifiedType(mapKeyType, imports) + ", " + getUnqualifiedType(mappingAccessor.getReferenceClassName(), imports);
}
} else {
types = types + ", " + getUnqualifiedType(mappingAccessor.getReferenceClassName(), imports);
}
}
// Add the mapping attribute to the list of attributes for this class.
attributes.add(" public static volatile " + attributeType + "<" + types + "> " + annotatedElement.getAttributeName() + ";\n");
}
}
// Will import the parent as well if needed.
String parent = writeImportStatements(imports, accessor, writer, persistenceUnit, canonicalpackage);
if (generateGenerated) {
// Write out the generation annotations.
String elVersion = "EclipseLink-" + Version.getVersion() + ".v" + Version.getBuildDate() + "-r" + Version.getBuildRevision();
writer.append("@Generated(value=\"");
if (isNewJava) {
writer.append(CanonicalModelProcessor.class.getName());
} else {
writer.append(elVersion);
}
writer.append("\"");
if (generateTimestamp) {
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
writer.append(", date=\"" + sdf.format(date) + "\"");
}
if (isNewJava && generateComments) {
writer.append(", comments=\"");
writer.append(elVersion);
writer.append("\"");
}
writer.append(")\n");
}
writer.append("@StaticMetamodel(" + className + ".class)\n");
int modifier = accessor.getAccessibleObject().getModifiers();
writer.append(java.lang.reflect.Modifier.toString(modifier) + " class " + canonicalName);
if (parent == null) {
writer.append(" { \n\n");
} else {
writer.append(" extends " + parent + " {\n\n");
}
// Go through the attributes and write them out.
for (String str : attributes) {
writer.append(str);
}
writer.append("\n}");
} finally {
if (writer != null) {
writer.flush();
writer.close();
writer = null;
}
}
}
/**
* INTERNAL:
*/
protected void generateCanonicalModelClasses(MetadataMirrorFactory factory, PersistenceUnit persistenceUnit) throws IOException {
Map<Element, MetadataClass> roundElements = factory.getRoundElements();
for (Element roundElement : roundElements.keySet()) {
MetadataClass roundClass = roundElements.get(roundElement);
if (persistenceUnit.containsClass(roundClass)) {
log(SessionLog.FINEST, "Generating class: {0}", roundClass.getName());
generateCanonicalModelClass(roundClass, roundElement, persistenceUnit);
}
}
}
/**
* INTERNAL:
*/
protected String getBoxedType(MetadataAnnotatedElement annotatedElement, MetadataClass rawClass) {
PrimitiveType primitiveType = (PrimitiveType) annotatedElement.getPrimitiveType();
if (primitiveType != null) {
return processingEnv.getTypeUtils().boxedClass(primitiveType).toString();
}
String type = annotatedElement.getType();
return (type == null) ? rawClass.getType() : type;
}
/**
* INTERNAL:
*/
protected String getName(String qualifiedName) {
if (qualifiedName.indexOf('.') > -1) {
return qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
}
return qualifiedName;
}
/**
* INTERNAL:
*/
protected String getPackage(String qualifiedName) {
if (qualifiedName.indexOf('.') > -1) {
return qualifiedName.substring(0, qualifiedName.lastIndexOf('.'));
}
return "";
}
/**
* INTERNAL: This method will hack off any package qualification. It will
* add that type to the import list unless it is a known JDK type that does
* not need to be imported (java.lang). This method also trims the type
* from leading and trailing white spaces.
*/
protected String getUnqualifiedType(String type, HashMap<String, String> imports) {
// Remove any leading and trailing white spaces.
type = type.trim();
// Convert any $ (enums, inner classes to valid dot notation for import statement)
// org.eclipse.persistence.testing.models.jpa.xml.advanced.ShovelSections$MaterialType
type = type.replace("$", ".");
if (type.contains("void")) {
// This case hits when the user defines something like:
// @BasicCollection public Collection responsibilities;
return TypeVisitor.GENERIC_TYPE;
} else if (type.startsWith("java.lang")) {
return type.substring(type.lastIndexOf('.') + 1);
} else {
if (type.indexOf('<') > -1) {
String raw = type.substring(0, type.indexOf('>'));
String generic = type.substring(type.indexOf('<') + 1, type.length() - 1);
if (raw.contains("Map")) {
String key = generic.substring(0, generic.indexOf(','));
String value = generic.substring(generic.indexOf(',') + 1);
return getUnqualifiedType(raw, imports) + "<" + getUnqualifiedType(key, imports) + ", " + getUnqualifiedType(value, imports) + ">";
}
return getUnqualifiedType(raw, imports) + "<" + getUnqualifiedType(generic, imports) + ">";
} else if (type.indexOf('.') > -1) {
String shortClassName = type.substring(type.lastIndexOf('.') + 1);
// We already have an import for this class, look at it further.
if (imports.containsKey(shortClassName)) {
if (imports.get(shortClassName).equals(type)) {
// We're hitting the same class from the same package,
// return the short name for this class.
return type.substring(type.lastIndexOf('.') + 1);
} else {
// Same class name different package. Don't hack off the
// qualification and don't add it to the import list.
return type;
}
} else {
// Add it to the import list. If the type is used in an array
// hack off the [].
if (shortClassName.indexOf('[') > 1) {
imports.put(shortClassName, type.substring(0, type.indexOf('[')));
} else {
imports.put(shortClassName, type);
}
return shortClassName;
}
} else {
return type;
}
}
}
/**
* INTERNAL:
*/
protected String writeImportStatements(HashMap<String, String> typeImports, ClassAccessor accessor, Writer writer, PersistenceUnit persistenceUnit, String childCanonicalpackage) throws IOException {
String parentCanonicalName = null;
// Get the import list ready to be sorted.
ArrayList<String> imps = new ArrayList<>();
imps.addAll(typeImports.values());
// Add the standard canonical model generator imports.
imps.add("jakarta.persistence.metamodel.StaticMetamodel");
// Import the parent canonical class if need be.
MetadataClass parentCls = accessor.getJavaClass().getSuperclass();
MetadataProject project = accessor.getProject();
if (project.hasEntity(parentCls) || project.hasEmbeddable(parentCls) || project.hasMappedSuperclass(parentCls)) {
String qualifiedParentCanonicalName = persistenceUnit.getQualifiedCanonicalName(parentCls.getName());
parentCanonicalName = getName(qualifiedParentCanonicalName);
String parentCanonicalPackage = getPackage(qualifiedParentCanonicalName);
if (! parentCanonicalPackage.equals(childCanonicalpackage)) {
imps.add(qualifiedParentCanonicalName);
}
}
// Sort the list of imports before writing them.
Collections.sort(imps);
// Write out the imports.
for (String typeImport : imps) {
writer.append("import " + typeImport + ";\n");
}
writer.append("\n");
return parentCanonicalName;
}
private void log(int level, String msg, Object... args) {
log.log(level, SessionLog.PROCESSOR, msg, args, false);
}
}