com.querydsl.codegen.GenericExporter Maven / Gradle / Ivy
/*
* Copyright 2015, The Querydsl Team (http://www.querydsl.com/team)
*
* Licensed 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 com.querydsl.codegen;
import com.querydsl.codegen.utils.JavaWriter;
import com.querydsl.codegen.utils.ScalaWriter;
import com.querydsl.codegen.utils.model.Parameter;
import com.querydsl.codegen.utils.model.Type;
import com.querydsl.codegen.utils.model.TypeCategory;
import com.querydsl.codegen.utils.support.ClassUtils;
import com.querydsl.core.QueryException;
import com.querydsl.core.annotations.Config;
import com.querydsl.core.annotations.PropertyType;
import com.querydsl.core.annotations.QueryEmbeddable;
import com.querydsl.core.annotations.QueryEmbedded;
import com.querydsl.core.annotations.QueryEntity;
import com.querydsl.core.annotations.QueryExclude;
import com.querydsl.core.annotations.QueryInit;
import com.querydsl.core.annotations.QueryProjection;
import com.querydsl.core.annotations.QuerySupertype;
import com.querydsl.core.annotations.QueryTransient;
import com.querydsl.core.annotations.QueryType;
import com.querydsl.core.util.Annotations;
import com.querydsl.core.util.BeanUtils;
import com.querydsl.core.util.ReflectionUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
/**
* {@code GenericExporter} provides query type serialization logic for cases where APT annotation
* processors can't be used. {@code GenericExporter} scans the classpath for classes annotated with
* specified annotations in specific packages and mirrors them into Querydsl expression types.
*
* Example with Querydsl annotations:
*
*
{@code
* GenericExporter exporter = new GenericExporter();
* exporter.setTargetFolder(new File("target/generated-sources/java"));
* exporter.export(com.example.domain.Entity.class.getPackage());
* }
*
* Example with JPA annotations:
*
*
{@code
* GenericExporter exporter = new GenericExporter();
* exporter.setKeywords(Keywords.JPA);
* exporter.setEntityAnnotation(Entity.class);
* exporter.setEmbeddableAnnotation(Embeddable.class);
* exporter.setEmbeddedAnnotation(Embedded.class);
* exporter.setSupertypeAnnotation(MappedSuperclass.class);
* exporter.setSkipAnnotation(Transient.class);
* exporter.setTargetFolder(new File("target/generated-sources/java"));
* exporter.export(com.example.domain.Entity.class.getPackage());
* }
*
* @author tiwe
*/
public class GenericExporter {
private Class extends Annotation> entityAnnotation = QueryEntity.class;
private Class extends Annotation> supertypeAnnotation = QuerySupertype.class;
private Class extends Annotation> embeddableAnnotation = QueryEmbeddable.class;
private Class extends Annotation> embeddedAnnotation = QueryEmbedded.class;
private Class extends Annotation> skipAnnotation = QueryTransient.class;
private boolean createScalaSources = false;
private final Set> stopClasses = new HashSet<>();
private final Map allTypes = new HashMap<>();
private final Map, EntityType> entityTypes = new HashMap<>();
private final Map, EntityType> superTypes = new HashMap<>();
private final Map, EntityType> embeddableTypes = new HashMap<>();
private final Map, EntityType> projectionTypes = new HashMap<>();
private final CodegenModule codegenModule = new CodegenModule();
private SerializerConfig serializerConfig = SimpleSerializerConfig.DEFAULT;
@Deprecated private boolean handleFields = true, handleMethods = true;
private PropertyHandling propertyHandling = PropertyHandling.ALL;
private boolean useFieldTypes = false;
@Nullable private File targetFolder;
@Nullable private TypeFactory typeFactory;
private final List annotationHelpers = new ArrayList<>();
@Nullable private TypeMappings typeMappings;
@Nullable private QueryTypeFactory queryTypeFactory;
@Nullable private Class extends Serializer> serializerClass;
private final Charset charset;
private final ClassLoader classLoader;
private Set generatedFiles = new HashSet<>();
private boolean strictMode;
/**
* Create a GenericExporter instance using the given classloader and charset for serializing
* source files
*
* @param classLoader classloader to use
* @param charset charset of target sources
*/
public GenericExporter(ClassLoader classLoader, Charset charset) {
this.classLoader = classLoader;
this.charset = charset;
stopClasses.add(Object.class);
stopClasses.add(Enum.class);
}
/**
* Create a GenericExporter instance using the given classloader and default charset
*
* @param classLoader classloader to use
*/
public GenericExporter(ClassLoader classLoader) {
this(classLoader, Charset.defaultCharset());
}
/**
* Create a GenericExporter instance using the context classloader and the given charset
*
* @param charset charset of target sources
*/
public GenericExporter(Charset charset) {
this(Thread.currentThread().getContextClassLoader(), charset);
}
/** Create a GenericExporter instance using the context classloader and default charset */
public GenericExporter() {
this(Thread.currentThread().getContextClassLoader(), Charset.defaultCharset());
}
/**
* Export the given packages
*
* @param packages packages to be scanned
*/
public void export(Package... packages) {
var pkgs = new String[packages.length];
for (var i = 0; i < packages.length; i++) {
pkgs[i] = packages[i].getName();
}
export(pkgs);
}
/**
* Export the given packages
*
* @param packages packages to be scanned
*/
public void export(String... packages) {
scanPackages(packages);
innerExport();
}
/**
* Export the given classes
*
* @param classes classes to be scanned
*/
public void export(Class>... classes) {
for (Class> cl : classes) {
handleClass(cl);
}
innerExport();
}
@SuppressWarnings("unchecked")
private void innerExport() {
typeMappings = codegenModule.get(TypeMappings.class);
queryTypeFactory = codegenModule.get(QueryTypeFactory.class);
typeFactory =
new TypeFactory(
Arrays.asList(entityAnnotation, supertypeAnnotation, embeddableAnnotation),
codegenModule.get(Function.class, CodegenModule.VARIABLE_NAME_FUNCTION_CLASS));
// copy annotations helpers to typeFactory
for (AnnotationHelper helper : annotationHelpers) {
typeFactory.addAnnotationHelper(helper);
}
// process supertypes
for (Class> cl : superTypes.keySet()) {
createEntityType(cl, superTypes);
}
// process embeddables
for (Class> cl : embeddableTypes.keySet()) {
createEntityType(cl, embeddableTypes);
}
// process entities
for (Class> cl : entityTypes.keySet()) {
createEntityType(cl, entityTypes);
}
// process projections
for (Class> cl : projectionTypes.keySet()) {
createEntityType(cl, projectionTypes);
}
// add constructors and properties
for (Map, EntityType> entries :
Arrays.asList(superTypes, embeddableTypes, entityTypes, projectionTypes)) {
for (Map.Entry, EntityType> entry : new HashSet<>(entries.entrySet())) {
addConstructors(entry.getKey(), entry.getValue());
addProperties(entry.getKey(), entry.getValue());
}
}
// merge supertype fields into subtypes
Set handled = new HashSet<>();
for (EntityType type : superTypes.values()) {
addSupertypeFields(type, allTypes, handled);
}
for (EntityType type : entityTypes.values()) {
addSupertypeFields(type, allTypes, handled);
}
for (EntityType type : embeddableTypes.values()) {
addSupertypeFields(type, allTypes, handled);
}
// extend types
typeFactory.extendTypes();
try {
Serializer supertypeSerializer, entitySerializer, embeddableSerializer, projectionSerializer;
if (serializerClass != null) {
Serializer serializer = codegenModule.get(serializerClass);
supertypeSerializer = serializer;
entitySerializer = serializer;
embeddableSerializer = serializer;
projectionSerializer = serializer;
} else {
supertypeSerializer = codegenModule.get(SupertypeSerializer.class);
entitySerializer = codegenModule.get(EntitySerializer.class);
embeddableSerializer = codegenModule.get(EmbeddableSerializer.class);
projectionSerializer = codegenModule.get(ProjectionSerializer.class);
}
// serialize super types
serialize(supertypeSerializer, superTypes);
// serialize entity types
serialize(entitySerializer, entityTypes);
// serialize embeddable types
serialize(embeddableSerializer, embeddableTypes);
// serialize projection types
serialize(projectionSerializer, projectionTypes);
} catch (IOException e) {
throw new QueryException(e);
}
}
private void addSupertypeFields(
EntityType model, Map superTypes, Set handled) {
if (handled.add(model)) {
for (Supertype supertype : model.getSuperTypes()) {
var entityType = superTypes.get(supertype.getType().getFullName());
if (entityType == null) {
if (supertype.getType().getPackageName().startsWith("java.")) {
// skip internal supertypes
continue;
}
// FIXME this misses the generics
Class> cl = supertype.getType().getJavaClass();
typeFactory.addEmbeddableType(cl);
entityType = createEntityType(cl, new HashMap<>());
addProperties(cl, entityType);
}
addSupertypeFields(entityType, superTypes, handled);
supertype.setEntityType(entityType);
model.include(supertype);
}
}
}
private boolean containsAny(Class> clazz, Class extends Annotation>... annotations) {
for (Class extends Annotation> annType : annotations) {
if (clazz.isAnnotationPresent(annType)) {
return true;
}
}
return false;
}
private EntityType createEntityType(Class> cl, Map, EntityType> types) {
if (types.get(cl) != null) {
return types.get(cl);
} else {
var type = allTypes.get(ClassUtils.getFullName(cl));
if (type == null) {
type = typeFactory.getEntityType(cl);
}
types.put(cl, type);
var fullName = ClassUtils.getFullName(cl);
if (!allTypes.containsKey(fullName)) {
allTypes.put(fullName, type);
}
typeMappings.register(type, queryTypeFactory.create(type));
if (strictMode && cl.getSuperclass() != null) {
@SuppressWarnings("unchecked")
var annotations =
(Class extends Annotation>[])
new Class>[] {entityAnnotation, supertypeAnnotation, embeddableAnnotation};
if (!containsAny(cl.getSuperclass(), annotations)) {
// skip supertype handling
return type;
}
}
if (cl.getSuperclass() != null
&& !stopClasses.contains(cl.getSuperclass())
&& !cl.getSuperclass().isAnnotationPresent(QueryExclude.class)) {
type.addSupertype(
new Supertype(typeFactory.get(cl.getSuperclass(), cl.getGenericSuperclass())));
}
if (cl.isInterface()) {
for (Class> iface : cl.getInterfaces()) {
if (!stopClasses.contains(iface) && !iface.isAnnotationPresent(QueryExclude.class)) {
type.addSupertype(new Supertype(typeFactory.get(iface)));
}
}
}
return type;
}
}
private void addConstructors(Class> cl, EntityType type) {
final var constructors = cl.getConstructors();
Arrays.sort(constructors, Comparator.comparing(Constructor::toString));
for (Constructor> constructor : constructors) {
if (constructor.isAnnotationPresent(QueryProjection.class)) {
List parameters = new ArrayList<>();
for (var i = 0; i < constructor.getParameterTypes().length; i++) {
var parameterType =
typeFactory.get(
constructor.getParameterTypes()[i], constructor.getGenericParameterTypes()[i]);
for (Annotation annotation : constructor.getParameterAnnotations()[i]) {
if (annotation.annotationType().equals(QueryType.class)) {
var queryType = (QueryType) annotation;
parameterType = parameterType.as(TypeCategory.valueOf(queryType.value().name()));
}
}
parameters.add(new Parameter("param" + i, parameterType));
}
type.addConstructor(new com.querydsl.codegen.utils.model.Constructor(parameters));
}
}
}
private void addProperties(Class> cl, EntityType type) {
Map types = new HashMap<>();
Map annotations = new HashMap<>();
var config = propertyHandling.getConfig(cl);
// fields
if (config.isFields()) {
for (Field field : cl.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers())) {
if (Modifier.isTransient(field.getModifiers())
&& !field.isAnnotationPresent(QueryType.class)) {
continue;
}
var annotated = ReflectionUtils.getAnnotatedElement(cl, field.getName(), field.getType());
var propertyType =
getPropertyType(cl, annotated, field.getType(), field.getGenericType());
var ann = new Annotations(field);
types.put(field.getName(), propertyType);
annotations.put(field.getName(), ann);
}
}
}
// getters
if (config.isMethods()) {
for (Method method : cl.getDeclaredMethods()) {
var name = method.getName();
if (method.getParameterTypes().length == 0
&& !Modifier.isStatic(method.getModifiers())
&& !method.isBridge()
&& ((name.startsWith("get") && name.length() > 3)
|| (name.startsWith("is") && name.length() > 2))) {
String propertyName;
if (name.startsWith("get")) {
propertyName = BeanUtils.uncapitalize(name.substring(3));
} else {
propertyName = BeanUtils.uncapitalize(name.substring(2));
}
var propertyType =
getPropertyType(cl, method, method.getReturnType(), method.getGenericReturnType());
if (!types.containsKey(propertyName) || !useFieldTypes) {
types.put(propertyName, propertyType);
}
var ann = annotations.get(propertyName);
if (ann == null) {
ann = new Annotations();
annotations.put(propertyName, ann);
}
ann.addAnnotations(method);
}
}
}
for (Map.Entry entry : types.entrySet()) {
var ann = annotations.get(entry.getKey());
Property property = createProperty(type, entry.getKey(), entry.getValue(), ann);
if (property != null) {
type.addProperty(property);
}
}
}
private Type getPropertyType(
Class> cl, AnnotatedElement annotated, Class> type, java.lang.reflect.Type genericType) {
Type propertyType = null;
if (annotated.isAnnotationPresent(embeddedAnnotation)) {
Class> embeddableType = type;
if (Collection.class.isAssignableFrom(type)) {
embeddableType = ReflectionUtils.getTypeParameterAsClass(genericType, 0);
} else if (Map.class.isAssignableFrom(type)) {
embeddableType = ReflectionUtils.getTypeParameterAsClass(genericType, 1);
}
if (!embeddableType.getName().startsWith("java.")) {
typeFactory.addEmbeddableType(embeddableType);
if (!embeddableTypes.containsKey(embeddableType)
&& !entityTypes.containsKey(embeddableType)
&& !superTypes.containsKey(embeddableType)) {
var entityType = createEntityType(embeddableType, embeddableTypes);
addProperties(embeddableType, entityType);
if (embeddableType == type) {
propertyType = entityType;
}
}
}
}
if (propertyType == null) {
propertyType = typeFactory.get(type, annotated, genericType);
if (propertyType instanceof EntityType
&& !allTypes.containsKey(ClassUtils.getFullName(type))) {
var fullName = ClassUtils.getFullName(type);
if (!allTypes.containsKey(fullName)) {
allTypes.put(fullName, (EntityType) propertyType);
}
}
}
return propertyType;
}
@Nullable
private Property createProperty(
EntityType entityType, String propertyName, Type propertyType, AnnotatedElement annotated) {
List inits = Collections.emptyList();
if (annotated.isAnnotationPresent(skipAnnotation)
&& !annotated.isAnnotationPresent(QueryType.class)) {
return null;
}
if (annotated.isAnnotationPresent(QueryInit.class)) {
inits = Arrays.asList(annotated.getAnnotation(QueryInit.class).value());
}
if (annotated.isAnnotationPresent(QueryType.class)) {
var queryType = annotated.getAnnotation(QueryType.class);
if (queryType.value().equals(PropertyType.NONE)) {
return null;
}
propertyType = propertyType.as(TypeCategory.valueOf(queryType.value().name()));
}
return new Property(entityType, propertyName, propertyType, inits);
}
private void scanPackages(String... packages) {
if (packages == null) {
return;
}
for (String pkg : packages) {
try {
for (Class> cl : ClassPathUtils.scanPackage(classLoader, pkg)) {
handleClass(cl);
}
} catch (IOException e) {
throw new QueryException(e);
}
}
}
private void handleClass(Class> cl) {
if (stopClasses.contains(cl) || cl.isAnnotationPresent(QueryExclude.class)) {
return;
} else if (cl.isAnnotationPresent(entityAnnotation)) {
entityTypes.put(cl, null);
} else if (cl.isAnnotationPresent(embeddableAnnotation)) {
embeddableTypes.put(cl, null);
} else if (cl.isAnnotationPresent(supertypeAnnotation)) {
superTypes.put(cl, null);
} else {
for (Constructor> constructor : cl.getConstructors()) {
if (constructor.isAnnotationPresent(QueryProjection.class)) {
projectionTypes.put(cl, null);
break;
}
}
}
}
private void serialize(Serializer serializer, Map, EntityType> types)
throws IOException {
for (Map.Entry, EntityType> entityType : types.entrySet()) {
var type = typeMappings.getPathType(entityType.getValue(), entityType.getValue(), true);
var packageName = type.getPackageName();
var className =
packageName.length() > 0
? (packageName + "." + type.getSimpleName())
: type.getSimpleName();
var config = serializerConfig;
if (entityType.getKey().isAnnotationPresent(Config.class)) {
config = SimpleSerializerConfig.getConfig(entityType.getKey().getAnnotation(Config.class));
}
var fileSuffix = createScalaSources ? ".scala" : ".java";
write(serializer, className.replace('.', '/') + fileSuffix, config, entityType.getValue());
}
}
private void write(
Serializer serializer, String path, SerializerConfig serializerConfig, EntityType type)
throws IOException {
var targetFile = new File(targetFolder, path);
generatedFiles.add(targetFile);
try (var w = writerFor(targetFile)) {
var writer = createScalaSources ? new ScalaWriter(w) : new JavaWriter(w);
serializer.serialize(type, serializerConfig, writer);
}
}
private Writer writerFor(File file) {
if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
System.err.println("Folder " + file.getParent() + " could not be created");
}
try {
return new OutputStreamWriter(new FileOutputStream(file), charset);
} catch (FileNotFoundException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* Return the set of generated files
*
* @return a set of generated files
*/
public Set getGeneratedFiles() {
return generatedFiles;
}
/**
* Set the entity annotation
*
* @param entityAnnotation entity annotation
*/
public void setEntityAnnotation(Class extends Annotation> entityAnnotation) {
this.entityAnnotation = entityAnnotation;
}
/**
* Set the supertype annotation
*
* @param supertypeAnnotation supertype annotation
*/
public void setSupertypeAnnotation(Class extends Annotation> supertypeAnnotation) {
this.supertypeAnnotation = supertypeAnnotation;
}
/**
* Set the embeddable annotation
*
* @param embeddableAnnotation embeddable annotation
*/
public void setEmbeddableAnnotation(Class extends Annotation> embeddableAnnotation) {
this.embeddableAnnotation = embeddableAnnotation;
}
/**
* Set the embedded annotation
*
* @param embeddedAnnotation embedded annotation
*/
public void setEmbeddedAnnotation(Class extends Annotation> embeddedAnnotation) {
this.embeddedAnnotation = embeddedAnnotation;
}
/**
* Set the skip annotation
*
* @param skipAnnotation skip annotation
*/
public void setSkipAnnotation(Class extends Annotation> skipAnnotation) {
this.skipAnnotation = skipAnnotation;
}
/**
* Set the target folder for generated sources
*
* @param targetFolder
*/
public void setTargetFolder(File targetFolder) {
this.targetFolder = targetFolder;
}
/**
* Set the serializer class to be used
*
* @param serializerClass
*/
public void setSerializerClass(Class extends Serializer> serializerClass) {
codegenModule.bind(serializerClass);
this.serializerClass = serializerClass;
}
/**
* Set the typemappings class to be used
*
* @param typeMappingsClass
*/
public void setTypeMappingsClass(Class extends TypeMappings> typeMappingsClass) {
codegenModule.bind(TypeMappings.class, typeMappingsClass);
}
/**
* Set whether Scala sources are generated
*
* @param createScalaSources
*/
public void setCreateScalaSources(boolean createScalaSources) {
this.createScalaSources = createScalaSources;
}
/**
* Set the keywords to be used
*
* @param keywords
*/
public void setKeywords(Collection keywords) {
codegenModule.bind(CodegenModule.KEYWORDS, keywords);
}
/**
* Set the name prefix
*
* @param prefix
*/
public void setNamePrefix(String prefix) {
codegenModule.bind(CodegenModule.PREFIX, prefix);
}
/**
* Set the name suffix
*
* @param suffix
*/
public void setNameSuffix(String suffix) {
codegenModule.bind(CodegenModule.SUFFIX, suffix);
}
/**
* Set the package suffix
*
* @param suffix
*/
public void setPackageSuffix(String suffix) {
codegenModule.bind(CodegenModule.PACKAGE_SUFFIX, suffix);
}
/**
* Set whether fields are handled (default true)
*
* @param b
* @deprecated Use {@link #setPropertyHandling(PropertyHandling)} instead
*/
@Deprecated
public void setHandleFields(boolean b) {
handleFields = b;
setPropertyHandling();
}
/**
* Set whether methods are handled (default true)
*
* @param b
* @deprecated Use {@link #setPropertyHandling(PropertyHandling)} instead
*/
@Deprecated
public void setHandleMethods(boolean b) {
handleMethods = b;
setPropertyHandling();
}
private void setPropertyHandling() {
if (handleFields) {
propertyHandling = handleMethods ? PropertyHandling.ALL : PropertyHandling.FIELDS;
} else if (handleMethods) {
propertyHandling = PropertyHandling.METHODS;
} else {
propertyHandling = PropertyHandling.NONE;
}
}
/**
* Set the property handling mode
*
* @param propertyHandling
*/
public void setPropertyHandling(PropertyHandling propertyHandling) {
this.propertyHandling = propertyHandling;
}
/**
* Set whether field types should be used instead of getter return types (default false)
*
* @param b
*/
public void setUseFieldTypes(boolean b) {
useFieldTypes = b;
}
/**
* Add a stop class to be used (default Object.class and Enum.class)
*
* @param cl
*/
public void addStopClass(Class> cl) {
stopClasses.add(cl);
}
/**
* Set whether annotationless superclasses are handled or not (default: true)
*
* @param s
*/
public void setStrictMode(boolean s) {
strictMode = s;
}
/**
* Set the serializer configuration to use
*
* @param serializerConfig
*/
public void setSerializerConfig(SerializerConfig serializerConfig) {
this.serializerConfig = serializerConfig;
}
/**
* Add a annotation helper object to process custom annotations
*
* @param annotationHelper
*/
public void addAnnotationHelper(AnnotationHelper annotationHelper) {
annotationHelpers.add(annotationHelper);
}
/**
* Set the Generated annotation class. Will default to java {@code @Generated}
*
* @param generatedAnnotationClass the fully qualified class name of the Single-Element
* Annotation (with {@code String} element) to be used on the generated sources, or
* {@code null} (defaulting to {@code javax.annotation.Generated} or {@code
* javax.annotation.processing.Generated} depending on the java version).
* @see Single-Element
* Annotation
*/
public void setGeneratedAnnotationClass(@Nullable String generatedAnnotationClass) {
codegenModule.bindInstance(
CodegenModule.GENERATED_ANNOTATION_CLASS,
GeneratedAnnotationResolver.resolve(generatedAnnotationClass));
}
}