Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
384 changes: 149 additions & 235 deletions build-plugins/src/main/kotlin/util/AnnotationNodeExtensions.kt

Large diffs are not rendered by default.

39 changes: 14 additions & 25 deletions critter/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,35 +36,10 @@
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
</dependency>
<dependency>
<groupId>com.google.devtools.ksp</groupId>
<artifactId>symbol-processing-api</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>com.google.devtools.ksp</groupId>
<artifactId>symbol-processing-common-deps</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>com.google.devtools.ksp</groupId>
<artifactId>symbol-processing-aa-embeddable</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.jboss.forge.roaster</groupId>
<artifactId>roaster-jdt</artifactId>
Expand Down Expand Up @@ -103,6 +78,20 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>none</phase>
</execution>
<execution>
<id>test-compile</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
47 changes: 47 additions & 0 deletions critter/core/src/main/java/dev/morphia/critter/Critter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package dev.morphia.critter;

import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Property;
import dev.morphia.annotations.Transient;

import org.objectweb.asm.Type;

public class Critter {
public static final List<Type> propertyAnnotations = new ArrayList<>(List.of(Type.getType(Property.class)));
public static final List<Type> transientAnnotations = new ArrayList<>(List.of(Type.getType(Transient.class)));

public static String critterPackage(Class<?> entity) {
return entity.getPackageName() + ".__morphia." + entity.getSimpleName().toLowerCase();
}

public static String titleCase(String s) {
if (s == null || s.isEmpty())
return s;
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}

public static String identifierCase(String s) {
if (s == null || s.isEmpty())
return s;
return Character.toLowerCase(s.charAt(0)) + s.substring(1);
}

private final File root;
private final File outputDir;
private final File ksp;

public Critter(File root) {
this.root = root;
this.outputDir = new File(root, "target");
this.ksp = new File(outputDir, "ksp");
}

private URI loadPath() throws Exception {
return Entity.class.getProtectionDomain().getCodeSource().getLocation().toURI();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package dev.morphia.critter;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import dev.morphia.mapping.codec.pojo.PropertyModel;

import net.bytebuddy.dynamic.loading.ByteArrayClassLoader;

public class CritterClassLoader extends ByteArrayClassLoader.ChildFirst {

public CritterClassLoader() {
super(PropertyModel.class.getClassLoader(), Collections.emptyMap());
}

public void register(String name, byte[] bytes) {
typeDefinitions.put(name, bytes);
}

byte[] bytes(String name) throws ClassNotFoundException {
// If already registered, return it
byte[] existing = typeDefinitions.get(name);
if (existing != null) {
return existing;
}

// Try to load from resources if it's a project class
if (shouldRegister(name)) {
String resourceName = name.replace('.', '/') + ".class";
// Try both this classloader and parent classloader
java.io.InputStream stream = getResourceAsStream(resourceName);
if (stream == null && getParent() != null) {
stream = getParent().getResourceAsStream(resourceName);
}
if (stream != null) {
try (java.io.InputStream in = stream) {
byte[] data = in.readAllBytes();
register(name, data);
return data;
} catch (java.io.IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}

throw new ClassNotFoundException(name);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// Try to register from resources first if not already registered
// Only register project classes to avoid LinkageError with third-party libraries
if (!typeDefinitions.containsKey(name) && shouldRegister(name)) {
java.net.URL resource = getResource(name.replace('.', '/') + ".class");
if (resource != null) {
try {
register(name, resource.openStream().readAllBytes());
} catch (java.io.IOException ignored) {
}
}
}
return super.findClass(name);
}

private boolean shouldRegister(String className) {
// Only register classes from the dev.morphia.critter package
// This avoids SecurityException (java.*, javax.*) and LinkageError (third-party libs)
return className.startsWith("dev.morphia.critter.");
}

public Map<String, byte[]> getTypeDefinitions() {
return new HashMap<>(typeDefinitions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dev.morphia.critter.conventions;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;

import dev.morphia.annotations.Id;
import dev.morphia.annotations.Property;
import dev.morphia.annotations.Reference;
import dev.morphia.annotations.Transient;
import dev.morphia.annotations.Version;
import dev.morphia.annotations.internal.MorphiaInternal;
import dev.morphia.config.MorphiaConfig;
import dev.morphia.mapping.Mapper;

/**
* @since 3.0
* @hidden @morphia.internal
*/
@MorphiaInternal
public class PropertyConvention {

public static List<Class<? extends Annotation>> transientAnnotations() {
return List.of(
Transient.class,
java.beans.Transient.class);
}

@MorphiaInternal
public static String mappedName(MorphiaConfig config, Map<String, Annotation> annotations, String modelName) {
Property property = (Property) annotations.get(Property.class.getName());
Reference reference = (Reference) annotations.get(Reference.class.getName());
Version version = (Version) annotations.get(Version.class.getName());
Id id = (Id) annotations.get(Id.class.getName());

if (id != null) {
return "_id";
} else if (property != null && !Mapper.IGNORED_FIELDNAME.equals(property.value())) {
return property.value();
} else if (reference != null && !Mapper.IGNORED_FIELDNAME.equals(reference.value())) {
return reference.value();
} else if (version != null && !Mapper.IGNORED_FIELDNAME.equals(version.value())) {
return version.value();
} else {
return config.propertyNaming().apply(modelName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package dev.morphia.critter.parser;

import java.util.Locale;
import java.util.regex.Pattern;

import org.objectweb.asm.Type;
import org.objectweb.asm.tree.MethodNode;

public class ExtensionFunctions {

private static final Pattern SNAKE_CASE_REGEX = Pattern.compile("(?<=.)[A-Z]");

public static String titleCase(String s) {
if (s == null || s.isEmpty())
return s;
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}

public static String methodCase(String s) {
if (s == null || s.isEmpty())
return s;
return Character.toLowerCase(s.charAt(0)) + s.substring(1);
}

public static String snakeCase(String s) {
return SNAKE_CASE_REGEX.matcher(s).replaceAll(m -> "_" + m.group().toLowerCase(Locale.getDefault()));
}

/**
* Converts a getter method to a property name. Examples:
* - "getName" -> "name"
* - "isActive" -> "active"
* - "getX" -> "x"
* - "name()" with matching field "name" -> "name"
*
* @param method the method node
* @param entity the class to check for matching fields when method name doesn't follow standard
* getter naming
*/
public static String getterToPropertyName(MethodNode method, Class<?> entity) {
String methodName = method.name;

// Standard getter patterns
if (methodName.startsWith("get") && methodName.length() > 3) {
return methodCase(methodName.substring(3));
}
if (methodName.startsWith("is") && methodName.length() > 2) {
return methodCase(methodName.substring(2));
}

// Check if method name matches a field: no parameters and return type matches field type
Type[] argTypes = Type.getArgumentTypes(method.desc);
Type returnType = Type.getReturnType(method.desc);
if (argTypes.length == 0) {
for (java.lang.reflect.Field field : entity.getDeclaredFields()) {
if (field.getName().equals(methodName) && Type.getType(field.getType()).equals(returnType)) {
return methodName;
}
}
}

return methodCase(methodName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package dev.morphia.critter.parser;

import dev.morphia.config.MorphiaConfig;
import dev.morphia.config.MorphiaConfigHelper;
import dev.morphia.mapping.Mapper;
import dev.morphia.mapping.ReflectiveMapper;
import dev.morphia.mapping.conventions.MorphiaDefaultsConvention;

import org.objectweb.asm.Type;

import static org.objectweb.asm.Type.ARRAY;

public class Generators {
public static final Generators INSTANCE = new Generators();

public String configFile = MorphiaConfigHelper.MORPHIA_CONFIG_PROPERTIES;

private MorphiaConfig config;
private Mapper mapper;
public MorphiaDefaultsConvention convention = new MorphiaDefaultsConvention();

private Generators() {
}

public synchronized MorphiaConfig getConfig() {
if (config == null) {
config = MorphiaConfig.load(configFile);
}
return config;
}

public synchronized Mapper getMapper() {
if (mapper == null) {
mapper = new ReflectiveMapper(getConfig());
}
return mapper;
}

public static Type wrap(Type fieldType) {
if (fieldType.equals(Type.VOID_TYPE))
return Type.getType(Void.class);
if (fieldType.equals(Type.BOOLEAN_TYPE))
return Type.getType(Boolean.class);
if (fieldType.equals(Type.CHAR_TYPE))
return Type.getType(Character.class);
if (fieldType.equals(Type.BYTE_TYPE))
return Type.getType(Byte.class);
if (fieldType.equals(Type.SHORT_TYPE))
return Type.getType(Short.class);
if (fieldType.equals(Type.INT_TYPE))
return Type.getType(Integer.class);
if (fieldType.equals(Type.FLOAT_TYPE))
return Type.getType(Float.class);
if (fieldType.equals(Type.LONG_TYPE))
return Type.getType(Long.class);
if (fieldType.equals(Type.DOUBLE_TYPE))
return Type.getType(Double.class);
return fieldType;
}

public static boolean isArray(Type type) {
return type.getSort() == ARRAY;
}

public static Class<?> asClass(Type type) {
return asClass(type, Thread.currentThread().getContextClassLoader());
}

public static Class<?> asClass(Type type, ClassLoader classLoader) {
if (type.equals(Type.VOID_TYPE))
return void.class;
if (type.equals(Type.BOOLEAN_TYPE))
return boolean.class;
if (type.equals(Type.CHAR_TYPE))
return char.class;
if (type.equals(Type.BYTE_TYPE))
return byte.class;
if (type.equals(Type.SHORT_TYPE))
return short.class;
if (type.equals(Type.INT_TYPE))
return int.class;
if (type.equals(Type.FLOAT_TYPE))
return float.class;
if (type.equals(Type.LONG_TYPE))
return long.class;
if (type.equals(Type.DOUBLE_TYPE))
return double.class;
String className = type.getSort() == ARRAY
? type.getDescriptor().replace('/', '.')
: type.getClassName();
try {
return Class.forName(className, false, classLoader);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Could not find class: " + className, e);
}
}
}
Loading
Loading