changeset 65:156e942b136e

added support for custom methods
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 20 Aug 2025 13:01:39 +0200
parents 9817ca245a49
children b87ce7874ddc
files .reuse/dep5 CodeGeneration/CodeModel.py CodeGeneration/CppCodeGeneration.py CodeGeneration/CppNativeSDK.mustache CodeGeneration/CustomFunctions.json CodeGeneration/CustomMethods.json CodeGeneration/JavaCodeGeneration.py Plugin/JavaEnvironment.cpp Plugin/JavaEnvironment.h Plugin/Plugin.cpp
diffstat 10 files changed, 200 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/.reuse/dep5	Thu Aug 14 18:18:19 2025 +0200
+++ b/.reuse/dep5	Wed Aug 20 13:01:39 2025 +0200
@@ -11,6 +11,8 @@
   Samples/FHIR/NOTES.txt
   Samples/MammographyDeepLearning/NOTES.txt
   CodeGeneration/README.txt
+  CodeGeneration/CustomFunctions.json
+  CodeGeneration/CustomMethods.json
 Copyright: 2023-2025 Sebastien Jodogne, ICTEAM UCLouvain (Belgium)
 License: CC0-1.0
 
--- a/CodeGeneration/CodeModel.py	Thu Aug 14 18:18:19 2025 +0200
+++ b/CodeGeneration/CodeModel.py	Wed Aug 20 13:01:39 2025 +0200
@@ -97,15 +97,35 @@
     return s
 
 
+def RemovePrefix(prefix, s, isCamelCase):
+    assert(s.startswith(prefix))
+    t = s[len(prefix):]
+    if isCamelCase:
+        return t[0].lower() + t[1:]
+    else:
+        return t
+
+
 def RemoveOrthancPluginPrefix(s, isCamelCase):
+    CUSTOM_PREFIX = 'OrthancPluginCustom_'
+    if s.startswith(CUSTOM_PREFIX):
+        return RemovePrefix(CUSTOM_PREFIX, s, isCamelCase)
+
     PREFIX = 'OrthancPlugin'
     if s.startswith(PREFIX):
-        t = s[len(PREFIX):]
-        if isCamelCase:
-            t = t[0].lower() + t[1:]
-        return t
+        return RemovePrefix(PREFIX, s, isCamelCase)
+
+    raise Exception('Incorrect prefix: %s' % s)
+
+
+def GetJavaMethodName(className, f):
+    if 'short_name' in f:
+        return f['short_name']
+    elif (className != None and
+          f['c_function'].startswith(className)):
+        return RemovePrefix(className, f['c_function'], True)
     else:
-        raise Exception('Incorrect prefix: %s' % s)
+        return RemoveOrthancPluginPrefix(f['c_function'], True)
 
 
 def ConvertReturnType(f):
@@ -122,15 +142,15 @@
         result = {
             'c_type' : 'jint',
             'default_value' : '0',
-            'is_number' : True,
+            'is_basic_type' : True,
             'java_signature' : 'I',
             'java_type' : 'int',
             }
-    elif f['return_sdk_type'] in [ 'int64_t' ]:
+    elif f['return_sdk_type'] in [ 'int64_t', 'uint64_t' ]:
         result = {
             'c_type' : 'jlong',
             'default_value' : '0',
-            'is_number' : True,
+            'is_basic_type' : True,
             'java_signature' : 'J',
             'java_type' : 'long',
             }
@@ -185,6 +205,14 @@
             'java_signature' : 'Ljava/lang/String;',
             'java_type' : 'String',
             }
+    elif f['return_sdk_type'] == 'bool':
+        result = {
+            'c_type' : 'jboolean',
+            'default_value' : 'false',
+            'is_basic_type' : True,
+            'java_signature' : 'Z',
+            'java_type' : 'boolean',
+            }
     else:
         raise Exception('Unsupported return type: %s' % json.dumps(f, indent=4))
 
@@ -196,6 +224,11 @@
 def ConvertArgument(arg):
     result = None
 
+    if 'name' in arg:
+        name = arg['name']
+    else:
+        name = arg['sdk_name']
+
     if arg['sdk_type'] in [ 'int', 'int32_t', 'uint32_t' ]:
         result = {
             'c_type' : 'jint',
@@ -228,7 +261,7 @@
         }
     elif arg['sdk_type'] == 'const char *':
         result = {
-            'c_accessor' : 'c_%s.GetValue()' % arg['name'],
+            'c_accessor' : 'c_%s.GetValue()' % name,
             'c_type' : 'jstring',
             'convert_string' : True,
             'java_signature' : 'Ljava/lang/String;',
@@ -240,7 +273,7 @@
         #   - argument "body" of "OrthancPluginSendHttpStatus()" in 1.11.1
 
         result = {
-            'c_accessor' : 'reinterpret_cast<const char*>(c_%s.GetData()), c_%s.GetSize()' % (arg['name'], arg['name']),
+            'c_accessor' : 'reinterpret_cast<const char*>(c_%s.GetData()), c_%s.GetSize()' % (name, name),
             'c_type' : 'jbyteArray',
             'convert_bytes' : True,
             'java_signature' : '[B',
@@ -248,7 +281,7 @@
         }
     elif arg['sdk_type'] == 'enumeration':
         result = {
-            'c_accessor' : 'static_cast<%s>(%s)' % (arg['sdk_enumeration'], arg['name']),
+            'c_accessor' : 'static_cast<%s>(%s)' % (arg['sdk_enumeration'], name),
             'c_type' : 'jint',
             'java_wrapper_accessor' : '%s.getValue()' % arg['sdk_name'],
             'java_wrapper_type' : RemoveOrthancPluginPrefix(arg['sdk_enumeration'], False),
@@ -257,7 +290,7 @@
             }
     elif arg['sdk_type'] == 'const void *':
         result = {
-            'c_accessor' : 'c_%s.GetData()' % arg['name'],
+            'c_accessor' : 'c_%s.GetData()' % name,
             'c_type' : 'jbyteArray',
             'convert_bytes' : True,
             'java_signature' : '[B',
@@ -265,7 +298,7 @@
         }
     elif arg['sdk_type'] in [ 'object', 'const_object' ]:
         result = {
-            'c_accessor' : 'reinterpret_cast<%s*>(static_cast<intptr_t>(%s))' % (arg['sdk_class'], arg['name']),
+            'c_accessor' : 'reinterpret_cast<%s*>(static_cast<intptr_t>(%s))' % (arg['sdk_class'], name),
             'c_type' : 'jlong',
             'java_signature' : 'J',
             'java_type' : 'long',
@@ -275,8 +308,9 @@
     else:
         raise Exception('Unsupported argument type: %s' % json.dumps(arg, indent=4))
 
-    result['name'] = arg['name']
+    result['name'] = name
     result['sdk_name'] = arg['sdk_name']
+    result['sdk_type'] = arg['sdk_type']
 
     if not 'java_wrapper_type' in result:
         result['java_wrapper_type'] = result['java_type']
@@ -285,7 +319,7 @@
         result['java_wrapper_accessor'] = arg['sdk_name']
 
     if not 'c_accessor' in result:
-        result['c_accessor'] = arg['name']
+        result['c_accessor'] = name
 
     return result
 
@@ -352,7 +386,12 @@
     return list(map(lambda x: { 'line' : x }, lines))
 
 
-def WrapFunction(className, f):
+def WrapFunction(cls, f, is_custom = False):
+    if cls == None:
+        className = None
+    else:
+        className = cls['name']
+
     args = []
     for a in f['args']:
         args.append(ConvertArgument(a))
@@ -361,10 +400,15 @@
         args[-1]['last'] = True
 
     returnType = ConvertReturnType(f)
-    signature = '(%s%s)%s' % ('J' if className != None else '',
+    signature = '(%s%s)%s' % ('J' if cls != None else '',
                               ''.join(map(lambda x: x['java_signature'], args)),
                               returnType['java_signature'])
 
+    since_sdk = f.get('since_sdk')
+    if (since_sdk == None and
+        cls != None):
+        since_sdk = cls.get('since_sdk')
+
     result = {
         'args' : args,
         'c_function' : f['c_function'],
@@ -372,15 +416,17 @@
         'has_args' : len(args) > 0,
         'java_signature' : signature,
         'return' : returnType,
-        'java_name' : RemoveOrthancPluginPrefix(f['c_function'], True),
-        'since_sdk' : f.get('since_sdk', None),
+        'java_name' : GetJavaMethodName(className, f),
+        'since_sdk' : since_sdk,
+        'is_custom' : is_custom,
+        'return_sdk_type' : f['return_sdk_type'],
     }
 
     if 'documentation' in f:
         result['has_documentation'] = True
         result['documentation'] = EncodeFunctionDocumentation(f)
 
-    if (returnType.get('is_number') == True or
+    if (returnType.get('is_basic_type') == True or
         returnType.get('is_bytes') == True or
         returnType.get('is_dynamic_string') == True or
         returnType.get('is_static_string') == True):
@@ -397,10 +443,22 @@
     return result
 
 
-def Load(path):
+def Load(path, customFunctionsPath = None, customMethodsPath = None):
     with open(path, 'r') as f:
         model = json.loads(f.read())
 
+    if customMethodsPath == None:
+        customMethods = {}
+    else:
+        with open(customMethodsPath, 'r') as f:
+            customMethods = json.loads(f.read())
+
+    if customFunctionsPath == None:
+        customFunctions = {}
+    else:
+        with open(customFunctionsPath, 'r') as f:
+            customFunctions = json.loads(f.read())
+
     for enum in model['enumerations']:
         if not enum['name'].startswith('OrthancPlugin'):
             raise Exception()
@@ -425,11 +483,14 @@
     for f in model['global_functions']:
         nativeFunctions.append(WrapFunction(None, f))
 
+    for f in customFunctions:
+        nativeFunctions.append(WrapFunction(None, f, is_custom = True))
+
     for c in model['classes']:
         c['short_name'] = RemoveOrthancPluginPrefix(c['name'], False)
 
         if 'destructor' in c:
-            nativeFunctions.append(WrapFunction(c['name'], {
+            nativeFunctions.append(WrapFunction(c, {
                 'args' : [],
                 'c_function' : c['destructor'],
                 'return_sdk_type' : 'void',
@@ -439,8 +500,12 @@
         wrappedMethods = []
 
         for m in c['methods']:
-            nativeFunctions.append(WrapFunction(c['name'], m))
-            wrappedMethods.append(WrapFunction(c['short_name'], m))
+            nativeFunctions.append(WrapFunction(c, m))
+            wrappedMethods.append(WrapFunction(c, m))
+
+        for customMethod in customMethods.get(c['name'], []):
+            nativeFunctions.append(WrapFunction(c, customMethod, is_custom = True))
+            wrappedMethods.append(WrapFunction(c, customMethod, is_custom = True))
 
         c['wrapped_methods'] = wrappedMethods
 
--- a/CodeGeneration/CppCodeGeneration.py	Thu Aug 14 18:18:19 2025 +0200
+++ b/CodeGeneration/CppCodeGeneration.py	Wed Aug 20 13:01:39 2025 +0200
@@ -50,6 +50,12 @@
 parser.add_argument('--target',
                     default = 'NativeSDK.cpp',
                     help = 'Target file')
+parser.add_argument('--custom-functions',
+                    default = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'CustomFunctions.json'),
+                    help = 'Input list of custom global functions')
+parser.add_argument('--custom-methods',
+                    default = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'CustomMethods.json'),
+                    help = 'Input list of custom methods')
 
 args = parser.parse_args()
 
@@ -63,7 +69,9 @@
 
 print('** Generating the C++ native functions to wrap Orthanc SDK %d.%d.%d **' % (SDK_VERSION[0], SDK_VERSION[1], SDK_VERSION[2]))
 
-model = CodeModel.Load(args.model)
+model = CodeModel.Load(args.model,
+                       customFunctionsPath = args.custom_functions,
+                       customMethodsPath = args.custom_methods)
 
 with open(os.path.join(ROOT, 'CppNativeSDK.mustache'), 'r') as f:
     template = f.read()
--- a/CodeGeneration/CppNativeSDK.mustache	Thu Aug 14 18:18:19 2025 +0200
+++ b/CodeGeneration/CppNativeSDK.mustache	Wed Aug 20 13:01:39 2025 +0200
@@ -37,6 +37,14 @@
 extern OrthancPluginContext* context_;
 
 {{#functions}}
+{{#is_custom}}
+{{#return.is_basic_type}}
+extern {{return_sdk_type}} {{c_function}}(OrthancPluginContext* context{{#class_name}}, {{class_name}}* self{{/class_name}}{{#args}}, {{sdk_type}} {{name}}{{/args}});
+{{/return.is_basic_type}}
+{{/is_custom}}
+{{/functions}}
+
+{{#functions}}
 
 JNIEXPORT {{return.c_type}} JNI_{{c_function}}(JNIEnv* env, jobject sdkObject{{#class_name}}, jlong self{{/class_name}}{{#args}}, {{c_type}} {{name}}{{/args}})
 {
@@ -67,11 +75,11 @@
     }
 {{/return.is_exception}}
   
-{{#return.is_number}}
+{{#return.is_basic_type}}
     return {{c_function}}(context_
       {{#class_name}}, reinterpret_cast<{{class_name}}*>(static_cast<intptr_t>(self)){{/class_name}}
       {{#args}}, {{c_accessor}}{{/args}});
-{{/return.is_number}}
+{{/return.is_basic_type}}
   
 {{#return.is_static_string}}
     const char* s = {{c_function}}(context_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CodeGeneration/CustomFunctions.json	Wed Aug 20 13:01:39 2025 +0200
@@ -0,0 +1,20 @@
+[
+  {
+    "c_function" : "OrthancPluginCustom_GetQueueSize",
+    "documentation" : {
+      "description" : [ "Get the number of elements in a queue." ],
+      "args" : {
+        "queueId" : "The identifier of the queue."
+      },
+      "return" : "The value, or None."
+    },
+    "args" : [
+      {
+        "sdk_name" : "queueId",
+        "sdk_type" : "const char *"
+      }
+    ],
+    "return_sdk_type" : "uint64_t",
+    "since_sdk" : [ 1, 12, 8 ]
+  }
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CodeGeneration/CustomMethods.json	Wed Aug 20 13:01:39 2025 +0200
@@ -0,0 +1,15 @@
+{
+  "OrthancPluginKeysValuesIterator" : [
+    {
+      "short_name" : "next",
+      "c_function" : "OrthancPluginCustom_KeysValuesIteratorNext",
+      "documentation" : {
+        "description" : [ "Advance to the next element in the iterator." ],
+        "return" : "Whether the iterator is done."
+      },
+      "args" : [
+      ],
+      "return_sdk_type" : "bool"
+    }
+  ]
+}
--- a/CodeGeneration/JavaCodeGeneration.py	Thu Aug 14 18:18:19 2025 +0200
+++ b/CodeGeneration/JavaCodeGeneration.py	Wed Aug 20 13:01:39 2025 +0200
@@ -50,6 +50,12 @@
 parser.add_argument('--target',
                     default = '/tmp/OrthancJavaSDK',
                     help = 'Target folder')
+parser.add_argument('--custom-functions',
+                    default = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'CustomFunctions.json'),
+                    help = 'Input list of custom global functions')
+parser.add_argument('--custom-methods',
+                    default = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'CustomMethods.json'),
+                    help = 'Input list of custom methods')
 
 args = parser.parse_args()
 
@@ -68,7 +74,9 @@
     escape = lambda u: u,  # No escaping
 )
 
-model = CodeModel.Load(args.model)
+model = CodeModel.Load(args.model,
+                       customFunctionsPath = args.custom_functions,
+                       customMethodsPath = args.custom_methods)
 
 
 print('** Generating the Java classes to wrap Orthanc SDK %d.%d.%d **' % (SDK_VERSION[0], SDK_VERSION[1], SDK_VERSION[2]))
--- a/Plugin/JavaEnvironment.cpp	Thu Aug 14 18:18:19 2025 +0200
+++ b/Plugin/JavaEnvironment.cpp	Wed Aug 20 13:01:39 2025 +0200
@@ -296,3 +296,10 @@
     throw std::runtime_error("Cannot create enumeration value: " + fqn + " " + buf);
   }
 }
+
+
+std::string JavaEnvironment::GetRuntimeErrorMessage(OrthancPluginContext* context,
+                                                    OrthancPluginErrorCode code)
+{
+  return "An exception has occurred in Java: " + std::string(OrthancPluginGetErrorDescription(context, code));
+}
--- a/Plugin/JavaEnvironment.h	Thu Aug 14 18:18:19 2025 +0200
+++ b/Plugin/JavaEnvironment.h	Wed Aug 20 13:01:39 2025 +0200
@@ -87,4 +87,7 @@
 
   jobject ConstructEnumValue(const std::string& fqn,
                              int value);
+
+  static std::string GetRuntimeErrorMessage(OrthancPluginContext* context,
+                                            OrthancPluginErrorCode code);
 };
--- a/Plugin/Plugin.cpp	Thu Aug 14 18:18:19 2025 +0200
+++ b/Plugin/Plugin.cpp	Wed Aug 20 13:01:39 2025 +0200
@@ -435,6 +435,42 @@
 }
 
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+uint64_t OrthancPluginCustom_GetQueueSize(OrthancPluginContext* context, const char *queueId)
+{
+  uint64_t size = 0;
+  OrthancPluginErrorCode code = OrthancPluginGetQueueSize(context, queueId, &size);
+
+  if (code == OrthancPluginErrorCode_Success)
+  {
+    return size;
+  }
+  else
+  {
+    throw std::runtime_error(JavaEnvironment::GetRuntimeErrorMessage(context, code));
+  }
+}
+#endif
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 8)
+bool OrthancPluginCustom_KeysValuesIteratorNext(OrthancPluginContext* context, OrthancPluginKeysValuesIterator* self)
+{
+  uint8_t done = true;
+  OrthancPluginErrorCode code = OrthancPluginKeysValuesIteratorNext(context, &done, self);
+
+  if (code == OrthancPluginErrorCode_Success)
+  {
+    return done;
+  }
+  else
+  {
+    throw std::runtime_error(JavaEnvironment::GetRuntimeErrorMessage(context, code));
+  }
+}
+#endif
+
+
 extern "C"
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)