From 8dfa082a542b78127b6b95e2f63f7d475519361a Mon Sep 17 00:00:00 2001
From: Simone Baffelli <simone.baffelli@empa.ch>
Date: Tue, 15 Aug 2023 22:12:53 +0200
Subject: [PATCH] Added custom extension to extract method signatures to ts.

---
 api-openbis-java/build.gradle                 |  57 +++---
 api-openbis-java/settings.gradle              |   2 +-
 .../ch/empa/tsprocessor/MethodExtension.java  | 176 ++++++++++++++++++
 build/build.gradle                            |   5 +-
 build/repository.gradle                       |   3 +
 build/settings.gradle                         |   2 +-
 6 files changed, 220 insertions(+), 25 deletions(-)
 create mode 100644 api-openbis-java/source/java/ch/empa/tsprocessor/MethodExtension.java

diff --git a/api-openbis-java/build.gradle b/api-openbis-java/build.gradle
index d99b470e16c..91855a760cb 100644
--- a/api-openbis-java/build.gradle
+++ b/api-openbis-java/build.gradle
@@ -1,5 +1,8 @@
+import groovy.json.JsonOutput
+import cz.habarta.typescript.generator.Settings.ConfiguredExtension
 evaluationDependsOn(':lib-commonbase')
 evaluationDependsOn(':lib-common')
+evaluationDependsOn(':lib-typescriptprocessor')
 
 apply from: '../build/javaproject.gradle'
 
@@ -16,27 +19,33 @@ buildscript {
 
 }
 
-task('generateModuleNames'){
-    doLast{
-        def file = fileTree(dir: '../api-openbis-javascript/src/v3', include: 'as/**/*.js')
+
+task('generateModuleNames') {
+    doLast {
+        def file = fileTree(dir: '../api-openbis-javascript/src/v3', include: ['as/**/*.js', 'ds/**/*.js', 'openbis.js'])
         def modules = file.collect { File f ->
             def name = f.name
             def moduleName = name.substring(0, name.lastIndexOf('.'))
-            return "\"${moduleName}\""
+            return "${moduleName}"
         }
-        def moduleNames = modules.unique().join(', \n')
+        def names = [names: modules]
+        def json = JsonOutput.prettyPrint(JsonOutput.toJson(names))
         def moduleNamesFile = new File('../api-openbis-javascript/src/v3/moduleNames.json')
-        moduleNamesFile.write("{\"names\":[$moduleNames]}")
+        moduleNamesFile.write(json)
     }
 }
 
 
 dependencies {
-    api project(':lib-common'),
+    api project(':lib-common'), project(":lib-typescriptprocessor"),
             'sis:sis-file-transfer:19.03.1',
             'fasterxml:jackson-core:2.9.10',
-            'fasterxml:jackson-annotations:2.9.10'
+            'fasterxml:jackson-annotations:2.9.10',
+            "cz.habarta.typescript-generator:typescript-generator-core:3.2.1263"
+
+    api "cz.habarta.typescript-generator:typescript-generator-core:3.2.1263"
 
+    api 'org.bsc.processor:java2ts-processor:1.3.1'
 
     testImplementation project(path: ':lib-commonbase', configuration: 'tests'),
             project(path: ':lib-common', configuration: 'tests'),
@@ -44,32 +53,36 @@ dependencies {
             'testng:testng:6.8-CISD',
             'reflections:reflections:0.9.10'
 
-
+    api(project(":lib-typescriptprocessor"))
 }
 
 
 apply plugin: "cz.habarta.typescript-generator"
 
 
-
-
-
 generateTypeScript {
     jsonLibrary = 'jackson2'
     classPatterns = [
-            'ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.**',
-            'ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.**',
-            'ch.ethz.sis.openbis.generic.asapi.v3.dto.property.**',
-            'ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.**',
-            'ch.ethz.sis.openbis.generic.asapi.v3.dto.datastore.**',
-            'ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.**',
-            'ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi',
-            'ch.ethz.sis.openbis.generic.OpenBIS',
+//            'ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.**',
+//            'ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.**',
+//            'ch.ethz.sis.openbis.generic.asapi.v3.dto.property.**',
+//            'ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.**',
+//            'ch.ethz.sis.openbis.generic.asapi.v3.dto.datastore.**',
+//            'ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.**',
+            'ch.ethz.sis.openbis.generic.OpenBIS'
     ]
-    outputKind = 'ambientModule'
+    excludeClassPatterns = ["**.v1.**",
+                            "**.generic.shared.**",
+                            "ch.ethz.sis.openbis.generic.asapi.v3.dto.session.search.PersonalAccessTokenSessionNameSearchCriteria",
+                            "ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.search.ExternalDmsSearchCriteria",
+    "ch.ethz.sis.openbis.generic.asapi.v3.dto.externaldms.search.ExternalDmsSearchCriteria"]
+    outputKind = 'module'
+    outputFileType = 'declarationFile'
+    //customTypeProcessor = "ch.empa.tsprocessor.CustomTypeProcessor"
+    extensionsWithConfiguration = [  new ConfiguredExtension(className:'ch.empa.tsprocessor.MethodExtension',  configuration:  ['asnycClasses':['ch.ethz.sis.openbis.generic.OpenBIS']])]
     jackson2ModuleDiscovery = true
     outputFile = file('../api-openbis-javascript/src/v3/openbis.d.ts')
-    module = 'dto'
+    //module = 'dto'
 
 }
 
diff --git a/api-openbis-java/settings.gradle b/api-openbis-java/settings.gradle
index d1561187ade..aafaece0ea9 100644
--- a/api-openbis-java/settings.gradle
+++ b/api-openbis-java/settings.gradle
@@ -1 +1 @@
-includeFlat 'lib-commonbase', 'lib-common'
+includeFlat 'lib-commonbase', 'lib-common', 'lib-typescriptprocessor'
diff --git a/api-openbis-java/source/java/ch/empa/tsprocessor/MethodExtension.java b/api-openbis-java/source/java/ch/empa/tsprocessor/MethodExtension.java
new file mode 100644
index 00000000000..1d048fd094a
--- /dev/null
+++ b/api-openbis-java/source/java/ch/empa/tsprocessor/MethodExtension.java
@@ -0,0 +1,176 @@
+package ch.empa.tsprocessor;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import cz.habarta.typescript.generator.*;
+import cz.habarta.typescript.generator.compiler.ModelCompiler;
+import cz.habarta.typescript.generator.compiler.SymbolTable;
+import cz.habarta.typescript.generator.compiler.TsModelTransformer;
+import cz.habarta.typescript.generator.emitter.*;
+import cz.habarta.typescript.generator.parser.BeanModel;
+import cz.habarta.typescript.generator.util.Utils;
+
+import java.io.IOException;
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.ObjectMapper;// in play 2.3
+
+interface MethodProcessor {
+    TsPropertyModel makeFunction(Method method, TsModelTransformer.Context context, SymbolTable symbolTable);
+}
+
+public class MethodExtension extends Extension {
+
+    static final ObjectMapper mapper = new ObjectMapper();
+
+    public static final String CFG_ASYNC_CLASSES = "asyncClasses";
+
+    private List<String> asnycClasses = new ArrayList<>();
+
+    public MethodExtension() {
+    }
+    public MethodExtension(List<String> asyncClasses) {
+        this.asnycClasses = asyncClasses;
+    }
+
+
+
+    @Override
+    public void setConfiguration(Map<String, String> configuration) throws RuntimeException {
+        if (configuration.containsKey(CFG_ASYNC_CLASSES)) {
+            String classString = configuration.get(CFG_ASYNC_CLASSES);
+            try {
+                String[] classes = mapper.readValue(classString, String[].class);
+                asnycClasses = Arrays.asList(classes);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+            System.out.println(classString);
+
+        }
+    }
+
+    private static String formatAllMethods(TsBeanModel bean) {
+        System.out.println("MethodExtension.formatAllMethos");
+        Class<?> origin = bean.getOrigin();
+        return Arrays.stream(origin.getMethods()).map(method -> method.getName()).collect(Collectors.joining("\n"));
+
+    }
+
+    private static boolean filterMethods(Method method) {
+        System.out.println("MethodExtension.filterMethods");
+        return !((method.getDeclaringClass() == Object.class) || (method.getName().matches("^(set|get|is).*|hashCode|toString|equals")));
+    }
+
+    private static List<TsParameter> getMethodParameters(Method method, SymbolTable symbolTable) {
+        System.out.println("MethodExtension.getMethodParameters");
+        return Arrays.stream(method.getParameters()).map(parameter -> new TsParameter(method.getName(), ResolveGenericType(parameter.getParameterizedType(), symbolTable))).collect(Collectors.toList());
+    }
+
+    private static TsType ResolveGenericType(Type type, SymbolTable symbolTable) {
+        if (type instanceof ParameterizedType) {
+            ParameterizedType parameterizedType = (ParameterizedType) type;
+            Class<?> rawType = (Class<?>) parameterizedType.getRawType();
+
+            if (List.class.isAssignableFrom(rawType)) {
+                // Manually map List<T> to Array<T>
+                TsType elementType = ResolveGenericType(parameterizedType.getActualTypeArguments()[0], symbolTable);
+                return new TsType.GenericReferenceType(symbolTable.getSymbol(Array.class), Collections.singletonList(elementType));
+            } else {
+                Class<?> rawClass = (Class<?>) parameterizedType.getRawType();
+                TsType[] typeArguments = Arrays.stream(parameterizedType.getActualTypeArguments()).map(typeArgument -> ResolveGenericType(typeArgument, symbolTable)).toArray(TsType[]::new);
+                return new TsType.GenericReferenceType(symbolTable.getSymbol(rawClass), Arrays.asList(typeArguments));
+            }
+        } else if (type instanceof GenericArrayType) {
+            GenericArrayType genericArrayType = (GenericArrayType) type;
+            return new TsType.BasicArrayType(ResolveGenericType(genericArrayType.getGenericComponentType(), symbolTable));
+        } else if (type instanceof Class) {
+            Class<?> clz = (Class<?>) type;
+            return new TsType.ReferenceType(symbolTable.getSymbol(clz));
+        } else if (type instanceof WildcardType) {
+            WildcardType wildcardType = (WildcardType) type;
+            final Type[] upperBounds = wildcardType.getUpperBounds();
+            return upperBounds.length > 0 ? new TsType.ReferenceType(symbolTable.getSymbol(upperBounds[0].getClass())) : TsType.Any;
+        } else {
+            // Handle TypeVariable case
+            return TsType.Any;
+        }
+    }
+
+    private static TsType getReturnType(Method method, TsModelTransformer.Context context, SymbolTable symbolTable) {
+        System.out.println("MethodExtension.getReturnType");
+        TsType typeParams = ResolveGenericType(method.getGenericReturnType(), symbolTable);
+        return typeParams;
+
+    }
+
+
+    private static TsPropertyModel makeFunction(Method method, TsModelTransformer.Context context, SymbolTable symbolTable) {
+        //System.out.println("MethodExtension.makeFunction");
+        List<TsParameter> params = getMethodParameters(method, symbolTable);
+        TsType returnType = getReturnType(method, context, symbolTable);
+        return new TsPropertyModel(method.getName(), new TsType.FunctionType(params, returnType), TsModifierFlags.None, false, null);
+    }
+
+    private static TsPropertyModel makeCallBackFunction(Method method, TsModelTransformer.Context context, SymbolTable symbolTable) {
+        //System.out.println("MethodExtension.makeCallBackFunction");
+        List<TsParameter> params = getMethodParameters(method, symbolTable);
+        TsType returnType = getReturnType(method, context, symbolTable);
+        TsParameter callbackParam = new TsParameter("callback", returnType);
+        TsType.FunctionType arrowFunction = new TsType.FunctionType(List.of(callbackParam), TsType.Null);
+        TsParameter callback = new TsParameter("callback", arrowFunction);
+        List<TsParameter> paramsWithCallback = Stream.concat(params.stream(), Stream.of(callback)).collect(Collectors.toList());
+        return new TsPropertyModel(method.getName(), new TsType.FunctionType(paramsWithCallback, TsType.Null), TsModifierFlags.None, false, null);
+    }
+
+
+    private static TsBeanModel addFunctions(TsBeanModel bean, TsModelTransformer.Context context, SymbolTable symbolTable, MethodProcessor processor) {
+        //System.out.println("MethodExtension.addFunction");
+        Class<?> origin = bean.getOrigin();
+        Stream<TsPropertyModel> params = Arrays.stream(origin.getMethods()).filter(mt -> filterMethods(mt)).map(method -> processor.makeFunction(method, context, symbolTable));
+        List<TsPropertyModel> allProps = Stream.concat(params, bean.getProperties().stream()).collect(Collectors.toList());
+        return bean.withProperties(allProps);
+    }
+
+    @Override
+    public EmitterExtensionFeatures getFeatures() {
+        final EmitterExtensionFeatures features = new EmitterExtensionFeatures();
+        features.generatesRuntimeCode = false;
+        features.generatesModuleCode = true;
+        features.worksWithPackagesMappedToNamespaces = true;
+        features.generatesJaxrsApplicationClient = false;
+        return features;
+    }
+
+    @Override
+    public List<TransformerDefinition> getTransformers() {
+        return List.of(new TransformerDefinition(ModelCompiler.TransformationPhase.BeforeSymbolResolution, (TsModelTransformer) (context, model) -> {
+            //System.out.println("MethodExtension.getTransformers");
+            Stream<TsBeanModel>  processedBeans =  model.getBeans().stream().map(bean -> {
+                System.out.printf("bean: %s\n", (bean.getOrigin().getName()));
+                if (asnycClasses.contains(bean.getOrigin().getName())){
+                    return addFunctions(bean, context, context.getSymbolTable(), MethodExtension::makeCallBackFunction);
+                } else {
+                    return  addFunctions(bean, context, context.getSymbolTable(), MethodExtension::makeFunction);
+                }
+            } );
+
+            return model.withBeans(processedBeans.collect(Collectors.toList()));
+        }));
+    }
+
+    public void emitElements(Writer writer, Settings settings, boolean exportKeyword, TsModel model) {
+        System.out.println("MethodExtension.emitElements");
+        for (TsBeanModel bean : model.getBeans()) {
+            //System.out.printf("bean: %s\n", formatAllMethods(bean));
+            if (bean.isJaxrsApplicationClientBean()) {
+                final String clientName = bean.getName().getSimpleName();
+                final String clientFullName = settings.mapPackagesToNamespaces ? bean.getName().getFullName() : bean.getName().getSimpleName();
+            }
+        }
+    }
+
+}
diff --git a/build/build.gradle b/build/build.gradle
index 7fdf2e012a2..92df344b5cb 100644
--- a/build/build.gradle
+++ b/build/build.gradle
@@ -14,5 +14,8 @@ evaluationDependsOn(':test-ui-core')
 evaluationDependsOn(':test-api-openbis-javascript')
 evaluationDependsOn(':lib-image-readers')
 evaluationDependsOn(':server-external-data-store')
-
+evaluationDependsOn(':lib-typescriptprocessor')
 apply from: '../build/javaproject.gradle'
+
+
+
diff --git a/build/repository.gradle b/build/repository.gradle
index 1672a54cfb7..bedda7cc8d1 100644
--- a/build/repository.gradle
+++ b/build/repository.gradle
@@ -3,4 +3,7 @@ ext.repositoryConfig = {
         ivyPattern "https://sissource.ethz.ch/openbis/openbis-public/openbis-ivy/-/raw/main/[organisation]/[module]/[revision]/ivy.xml"
         artifactPattern "https://sissource.ethz.ch/openbis/openbis-public/openbis-ivy/-/raw/main/[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier]).[ext]"
     }
+        maven {
+            url "https://plugins.gradle.org/m2/"
+        }
 }
diff --git a/build/settings.gradle b/build/settings.gradle
index 89b7a93c4d7..6432614e0bb 100644
--- a/build/settings.gradle
+++ b/build/settings.gradle
@@ -3,4 +3,4 @@ includeFlat 'lib-commonbase', 'lib-common', 'api-openbis-java', 'lib-openbis-com
         'core-plugin-openbis', 'app-openbis-installer', 'lib-image-readers', 'test-ui-core',
         'test-api-openbis-javascript', 'server-external-data-store', 'ui-admin', 'lib-microservice-server',
         'lib-transactional-file-system', 'server-data-store', 'ui-eln-lims', 'api-openbis-javascript',
-        'lib-json', 'api-data-store-server-java', 'api-data-store-server-javascript'
+        'lib-json', 'api-data-store-server-java', 'api-data-store-server-javascript', 'lib-typescriptprocessor'
\ No newline at end of file
-- 
GitLab