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

Add support for Kotlin data model classes #21

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ optional values:
- `useHollowPrimitiveTypes` - specify to use Hollow Primitive Types instead of generating them per project. defaults to `true`
- `restrictApiToFieldType` - api code only generates `get<FieldName>` with return type as per schema. defaults to `true`
- `useVerboseToString` - will implement `toString()` method for hollow objects with `HollowRecordStringifier().stringify(this)`. defaults to `true`
- `sourcesExtension` - the extension of your data classes. defaults to `".java"`
- `filesToExclude` - files to exclude when scanning for data classes. defaults to `["package-info.java", "module-info.java"]`
- `relativeCompileClassPaths` - the relative classpath of your compiled classes. defaults to `["/build/classes/main/", "/build/classes/java/main/"]`
- `relativeDestinationPath` - the relative path to the api-related sources will be generated. defaults to `"/src/main/java/"`
- `relativeSourcesPath` - the relative path to your data classes. defaults to `"/src/main/java/"`

For more information, please refer to [`AbstractHollowAPIGeneratorBuilder`](https://github.com/Netflix/hollow/blob/master/hollow/src/main/java/com/netflix/hollow/api/codegen/AbstractHollowAPIGeneratorBuilder.java)
launch task:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.netflix.nebula.hollow;

import java.util.Arrays;
import java.util.List;

public class ApiGeneratorExtension {
Expand All @@ -24,7 +25,11 @@ public class ApiGeneratorExtension {
public String apiPackageName;
public String getterPrefix;
public String classPostfix;
public String destinationPath;
public String sourcesExtension = ".java";
public List<String> filesToExclude = Arrays.asList("package-info.java", "module-info.java");
public List<String> relativeCompileClassPaths = Arrays.asList("/build/classes/main/", "/build/classes/java/main/");
public String relativeDestinationPath = "/src/main/java/";
public String relativeSourcesPath = "/src/main/java/";
public boolean parameterizeAllClassNames = false;
public boolean useAggressiveSubstitutions = false;
public boolean useErgonomicShortcuts = true;
Expand Down
51 changes: 25 additions & 26 deletions src/main/java/com/netflix/nebula/hollow/ApiGeneratorTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ public class ApiGeneratorTask extends DefaultTask {

private final File projectDirFile = getProject().getProjectDir();
private final String projectDirPath = projectDirFile.getAbsolutePath();
private final String relativeJavaSourcesPath = "/src/main/java/";
private final String javaSourcesPath = projectDirPath + relativeJavaSourcesPath;
//TODO: perhaps allow the users to configure these paths?
private final String[] compiledClassesPaths = {
projectDirPath + "/build/classes/main/",
projectDirPath + "/build/classes/java/main/",
};

private URLClassLoader urlClassLoader;

Expand All @@ -48,18 +41,22 @@ public void generateApi() throws IOException {
ApiGeneratorExtension extension = getProject().getExtensions().getByType(ApiGeneratorExtension.class);
validatePluginConfiguration(extension);

initClassLoader();
initClassLoader(extension.relativeCompileClassPaths);

String absoluteSourcesPath = projectDirFile.getAbsolutePath() + extension.relativeSourcesPath;
String absoluteDestinationPath = projectDirFile.getAbsolutePath() + extension.relativeDestinationPath;

HollowWriteStateEngine writeEngine = new HollowWriteStateEngine();
HollowObjectMapper mapper = new HollowObjectMapper(writeEngine);

Collection<Class<?>> datamodelClasses = extractClasses(extension.packagesToScan);
Collection<Class<?>> datamodelClasses = extractClasses(extension.packagesToScan, absoluteSourcesPath,
extension.sourcesExtension, extension.filesToExclude);
for (Class<?> clazz : datamodelClasses) {
getLogger().debug("Initialize schema for class {}", clazz.getName());
mapper.initializeTypeState(clazz);
}

String apiTargetPath = extension.destinationPath != null && extension.destinationPath.isEmpty() ? extension.destinationPath : buildPathToApiTargetFolder(extension.apiPackageName);
String apiTargetPath = buildPathToApiTargetFolder(absoluteDestinationPath, extension.apiPackageName);

HollowAPIGenerator generator = buildHollowAPIGenerator(extension, writeEngine, apiTargetPath);

Expand Down Expand Up @@ -103,26 +100,28 @@ private HollowAPIGenerator buildHollowAPIGenerator(ApiGeneratorExtension extensi
return builder.build();
}

private Collection<Class<?>> extractClasses(List<String> packagesToScan) {
private Collection<Class<?>> extractClasses(List<String> packagesToScan, String absoluteSourcesPath,
String sourcesExtension, List<String> filesToExclude) {
Set<Class<?>> classes = new HashSet<>();

for (String packageToScan : packagesToScan) {
File packageFile = buildPackageFile(packageToScan);
File packageFile = buildPackageFile(absoluteSourcesPath, packageToScan);

List<File> allFilesInPackage = findFilesRecursively(packageFile);
List<String> classNames = new ArrayList<>();
for (File file : allFilesInPackage) {
String filePath = file.getAbsolutePath();
getLogger().debug("Candidate for schema initialization {}", filePath);
if (filePath.endsWith(".java") &&
!filePath.endsWith("package-info.java") &&
!filePath.endsWith("module-info.java")
) {
String relativeFilePath = removeSubstrings(filePath, projectDirPath, relativeJavaSourcesPath);
classNames.add(convertFolderPathToPackageName(removeSubstrings(relativeFilePath, ".java")));
if (filePath.endsWith(sourcesExtension) && filesToExclude.stream().noneMatch(filePath::endsWith)) {
String relativeFilePath = removeSubstrings(filePath, absoluteSourcesPath);
classNames.add(convertFolderPathToPackageName(removeSubstrings(relativeFilePath, sourcesExtension)));
}
}

if (classNames.isEmpty()) {
getLogger().warn("No data model classes have been found.");
}

for (String fqdn : classNames) {
try {
Class<?> clazz = urlClassLoader.loadClass(fqdn);
Expand All @@ -149,12 +148,12 @@ private List<File> findFilesRecursively(File packageFile) {
return foundFiles;
}

private File buildPackageFile(String packageName) {
return new File(javaSourcesPath + convertPackageNameToFolderPath(packageName));
private File buildPackageFile(String absoluteSourcesPath, String packageName) {
return new File(absoluteSourcesPath + convertPackageNameToFolderPath(packageName));
}

private String buildPathToApiTargetFolder(String apiPackageName) {
return javaSourcesPath + convertPackageNameToFolderPath(apiPackageName);
private String buildPathToApiTargetFolder(String destinationPath, String apiPackageName) {
return destinationPath + convertPackageNameToFolderPath(apiPackageName);
}

private String convertPackageNameToFolderPath(String packageName) {
Expand All @@ -180,10 +179,10 @@ private void cleanupAndCreateFolders(String generatedApiTarget) {
}
}

private void initClassLoader() throws MalformedURLException {
URL[] urls = new URL[compiledClassesPaths.length];
for (int i=0; i < compiledClassesPaths.length; i++){
urls[i]= new File(compiledClassesPaths[i]).toURI().toURL() ;
private void initClassLoader(List<String> relativeCompileClassPaths) throws MalformedURLException {
URL[] urls = new URL[relativeCompileClassPaths.size()];
for (int i=0; i < relativeCompileClassPaths.size(); i++){
urls[i]= new File(projectDirPath + relativeCompileClassPaths.get(i)).toURI().toURL() ;
}
urlClassLoader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,103 @@ public class Actor {
this.name = name;
}
}
""".stripIndent()

when:
ExecutionResult result = runTasks('generateHollowConsumerApi')

then:
result.success

and:
[
'/Movie.java',
'/Actor.java',
'/MovieAPI.java',
'/accessor/ActorDataAccessor.java',
'/accessor/MovieDataAccessor.java',
'/collections/SetOfActor.java',
'/core/ActorDelegate.java',
'/core/ActorDelegateCachedImpl.java',
'/core/ActorHollowFactory.java',
'/core/ActorTypeAPI.java',
'/core/MovieAPIFactory.java',
'/core/MovieDelegate.java',
'/core/MovieDelegateCachedImpl.java',
'/core/MovieDelegateLookupImpl.java',
'/core/MovieHollowFactory.java',
'/core/MovieTypeAPI.java',
'/core/SetOfActorHollowFactory.java',
'/core/SetOfActorTypeAPI.java',
'/core/ActorDelegateLookupImpl.java',
'/core/MovieAPIFactory.java',
'/index/MovieAPIHashIndex.java',
'/index/MoviePrimaryKeyIndex.java',
'/index/MovieUniqueKeyIndex.java'
].forEach { fileName ->
assert getFile(destinationSrcFolder, fileName).exists()
}
}

def 'execution of generator task is successful for Kotlin model classes'() {
given:
String destinationSrcFolder = '/src/main/java/com/netflix/nebula/hollow/test/api'
buildFile << """
buildscript {
ext.kotlin_version = '1.3.61'

repositories {
mavenCentral()
}

dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:\$kotlin_version"
}
}

apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'nebula.hollow'

sourceCompatibility = 1.8

hollow {
packagesToScan = ['com.netflix.nebula.hollow.test']
apiClassName = 'MovieAPI'
apiPackageName = 'com.netflix.nebula.hollow.test.api'

sourcesExtension = '.kt'
filesToExclude = []
relativeCompileClassPaths = ['/build/classes/main/', '/build/classes/kotlin/main/']
relativeSourcesPath = '/src/main/kotlin/'
}

repositories {
jcenter()
}

dependencies {
compile "com.netflix.hollow:hollow:3.+"
compile "org.jetbrains.kotlin:kotlin-stdlib:\$kotlin_version"
}
""".stripIndent()

def moviefile = createFile('src/main/kotlin/com/netflix/nebula/hollow/test/Movie.kt')
moviefile << """package com.netflix.nebula.hollow.test;

import com.netflix.hollow.core.write.objectmapper.HollowPrimaryKey;

@HollowPrimaryKey(fields=["id"])
data class Movie(val id: Long,
val title: String,
val releaseYear: Int,
val actors: Set<Actor>)
""".stripIndent()

def authorFile = createFile('src/main/kotlin/com/netflix/nebula/hollow/test/Actor.kt')
authorFile << """package com.netflix.nebula.hollow.test;

data class Actor(val name: String)
""".stripIndent()

when:
Expand Down