Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automate attribute converter #1325

Merged
merged 11 commits into from
Mar 27, 2021
3 changes: 3 additions & 0 deletions src/main/java/run/halo/app/config/HaloConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
Expand All @@ -19,6 +20,7 @@
import run.halo.app.cache.AbstractStringCacheStore;
import run.halo.app.cache.InMemoryCacheStore;
import run.halo.app.cache.LevelCacheStore;
import run.halo.app.config.attributeconverter.AttributeConverterAutoGenerateConfiguration;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.repository.base.BaseRepositoryImpl;
import run.halo.app.utils.HttpClientUtils;
Expand All @@ -35,6 +37,7 @@
@EnableConfigurationProperties(HaloProperties.class)
@EnableJpaRepositories(basePackages = "run.halo.app.repository", repositoryBaseClass =
BaseRepositoryImpl.class)
@Import(AttributeConverterAutoGenerateConfiguration.class)
public class HaloConfiguration {

private final HaloProperties haloProperties;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package run.halo.app.config.attributeconverter;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer;
import org.springframework.context.annotation.Bean;

/**
* Jpa configuration.
*
* @author johnniang
*/
public class AttributeConverterAutoGenerateConfiguration {

@Bean
EntityManagerFactoryBuilderCustomizer entityManagerFactoryBuilderCustomizer(
ConfigurableListableBeanFactory factory) {
return builder -> builder.setPersistenceUnitPostProcessors(
new AutoGenerateConverterPersistenceUnitPostProcessor(factory));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package run.halo.app.config.attributeconverter;

import static net.bytebuddy.description.annotation.AnnotationDescription.Builder.ofType;
import static net.bytebuddy.description.type.TypeDescription.Generic.Builder.parameterizedType;
import static net.bytebuddy.implementation.FieldAccessor.ofField;
import static net.bytebuddy.implementation.MethodDelegation.to;
import static net.bytebuddy.matcher.ElementMatchers.isDefaultConstructor;
import static net.bytebuddy.matcher.ElementMatchers.named;

import java.lang.reflect.Modifier;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.NamingStrategy;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodCall;
import org.apache.commons.lang3.StringUtils;

/**
* Attribute converter auto generator.
*
* @author johnniang
*/
class AttributeConverterAutoGenerator {

/**
* Auto generation suffix.
*/
public static final String AUTO_GENERATION_SUFFIX = "$AttributeConverterGeneratedByByteBuddy";

private final ClassLoader classLoader;

public AttributeConverterAutoGenerator(ClassLoader classLoader) {
this.classLoader = classLoader;
}

public <T> Class<?> generate(Class<T> clazz) {
try {
return new ByteBuddy()
.with(new NamingStrategy.AbstractBase() {
@Override
protected String name(TypeDescription superClass) {
return clazz.getName() + AUTO_GENERATION_SUFFIX;
}
})
.subclass(
parameterizedType(AttributeConverter.class, clazz, Integer.class).build())
.annotateType(ofType(Converter.class).define("autoApply", true).build())
.constructor(isDefaultConstructor())
.intercept(MethodCall.invoke(Object.class.getDeclaredConstructor())
.andThen(ofField("enumType").setsValue(clazz)))
.defineField("enumType", Class.class, Modifier.PRIVATE | Modifier.FINAL)
.method(named("convertToDatabaseColumn"))
.intercept(to(AttributeConverterInterceptor.class))
.method(named("convertToEntityAttribute"))
.intercept(to(AttributeConverterInterceptor.class))
.make()
.load(this.classLoader, ClassLoadingStrategy.Default.INJECTION.allowExistingTypes())
.getLoaded();
} catch (NoSuchMethodException e) {
// should never happen
throw new RuntimeException("Failed to get declared constructor.", e);
}
}

public static boolean isGeneratedByByteBuddy(String className) {
return StringUtils.endsWith(className, AUTO_GENERATION_SUFFIX);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package run.halo.app.config.attributeconverter;

import net.bytebuddy.implementation.bind.annotation.FieldValue;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import run.halo.app.model.enums.ValueEnum;

/**
* Attribute Converter Interceptor.
*
* @author johnniang
*/
public class AttributeConverterInterceptor {

private AttributeConverterInterceptor() {
}

@RuntimeType
public static <T extends Enum<T> & ValueEnum<V>, V> V convertToDatabaseColumn(T attribute) {
return attribute == null ? null : attribute.getValue();
}

@RuntimeType
public static <T extends Enum<T> & ValueEnum<V>, V> T convertToEntityAttribute(V dbData,
@FieldValue("enumType") Class<T> enumType) {
return dbData == null ? null : ValueEnum.valueToEnum(enumType, dbData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package run.halo.app.config.attributeconverter;

import static java.util.stream.Collectors.toUnmodifiableSet;

import java.util.Set;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor;
import org.springframework.util.ClassUtils;
import run.halo.app.model.enums.ValueEnum;
import run.halo.app.model.properties.PropertyEnum;

/**
* Attribute converter persistence unit post processor.
*
* @author johnniang
*/
class AutoGenerateConverterPersistenceUnitPostProcessor implements PersistenceUnitPostProcessor {

private static final String PACKAGE_TO_SCAN = "run.halo.app";

private final ConfigurableListableBeanFactory factory;

public AutoGenerateConverterPersistenceUnitPostProcessor(
ConfigurableListableBeanFactory factory) {
this.factory = factory;
}

@Override
public void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) {
var generator = new AttributeConverterAutoGenerator(factory.getBeanClassLoader());

findValueEnumClasses()
.stream()
.map(generator::generate)
.map(Class::getName)
.forEach(pui::addManagedClassName);
}

private Set<Class<?>> findValueEnumClasses() {
var scanner = new ClassPathScanningCandidateComponentProvider(false);
// include ValueEnum class
scanner.addIncludeFilter(new AssignableTypeFilter(ValueEnum.class));
// exclude PropertyEnum class
scanner.addExcludeFilter(new AssignableTypeFilter(PropertyEnum.class));

return scanner.findCandidateComponents(PACKAGE_TO_SCAN)
.stream()
.filter(bd -> bd.getBeanClassName() != null)
.map(bd -> ClassUtils.resolveClassName(bd.getBeanClassName(), null))
.collect(toUnmodifiableSet());
}
}
2 changes: 1 addition & 1 deletion src/main/java/run/halo/app/model/entity/Journal.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class Journal extends BaseEntity {
private Long likes;

@Column(name = "type")
@ColumnDefault("1")
@ColumnDefault("0")
private JournalType type;

@Override
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/run/halo/app/model/enums/JournalType.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ public enum JournalType implements ValueEnum<Integer> {
/**
* Public type.
*/
PUBLIC(1),
PUBLIC(0),

/**
* Intimate type.
*/
INTIMATE(0);
INTIMATE(1);

private final int value;

Expand Down
36 changes: 0 additions & 36 deletions src/main/java/run/halo/app/model/enums/PostType.java

This file was deleted.

17 changes: 8 additions & 9 deletions src/main/java/run/halo/app/model/enums/ValueEnum.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
*/
public interface ValueEnum<T> {

/**
* Gets enum value.
*
* @return enum value
*/
T getValue();

/**
* Converts value to corresponding enum.
*
Expand All @@ -20,7 +27,7 @@ public interface ValueEnum<T> {
* @param <E> enum generic
* @return corresponding enum
*/
static <V, E extends ValueEnum<V>> E valueToEnum(Class<E> enumType, V value) {
static <V, E extends Enum<E> & ValueEnum<V>> E valueToEnum(Class<E> enumType, V value) {
Assert.notNull(enumType, "enum type must not be null");
Assert.notNull(value, "value must not be null");
Assert.isTrue(enumType.isEnum(), "type must be an enum type");
Expand All @@ -30,12 +37,4 @@ static <V, E extends ValueEnum<V>> E valueToEnum(Class<E> enumType, V value) {
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("unknown database value: " + value));
}

/**
* Gets enum value.
*
* @return enum value
*/
T getValue();

}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading