/************ Javaconn C++ Functions Source Code File (.CPP) ***********/
/*  Name: JAVAConn.CPP  Version 1.1                                    */
/*                                                                     */
/*  (C) Copyright to the author Olivier BERTRAND          2017 - 2021  */
/*                                                                     */
/*  This file contains the JAVA connection classes functions.          */
/***********************************************************************/

#if defined(_WIN32)
// This is needed for RegGetValue
#define _WINVER 0x0601
#undef  _WIN32_WINNT
#define _WIN32_WINNT 0x0601
#endif   // _WIN32

/***********************************************************************/
/*  Include relevant MariaDB header file.                              */
/***********************************************************************/
#include <my_global.h>
//#include <m_string.h>
#if defined(_WIN32)
#include <direct.h>                      // for getcwd
#if defined(__BORLANDC__)
#define __MFC_COMPAT__                   // To define min/max as macro
#endif   // __BORLANDC__
#else   // !_WIN32
#if defined(UNIX)
#include <errno.h>
#else   // !UNIX
#endif  // !UNIX
#include <stdio.h>
#include <stdlib.h>                      // for getenv
#define NODW
#endif  // !_WIN32

#include <m_string.h>

/***********************************************************************/
/*  Required objects includes.                                         */
/***********************************************************************/
#include "global.h"
#include "plgdbsem.h"
#include "colblk.h"
#include "xobject.h"
#include "xtable.h"
#include "tabext.h"
#include "javaconn.h"
#include "resource.h"
#include "valblk.h"
#include "osutil.h"

#if defined(_WIN32)
extern "C" HINSTANCE s_hModule;           // Saved module handle
#endif   // _WIN32
#define nullptr 0

//TYPCONV GetTypeConv();
//int GetConvSize();
extern char *JvmPath;   // The connect_jvm_path global variable value
extern char *ClassPath; // The connect_class_path global variable value

char *GetPluginDir(void);
char *GetMessageDir(void);
char *GetJavaWrapper(void);		// The connect_java_wrapper variable value
extern MYSQL_PLUGIN_IMPORT char lc_messages_dir[FN_REFLEN];

/***********************************************************************/
/*  Static JAVAConn objects.                                           */
/***********************************************************************/
void  *JAVAConn::LibJvm = NULL;
CRTJVM JAVAConn::CreateJavaVM = NULL;
GETJVM JAVAConn::GetCreatedJavaVMs = NULL;
#if defined(_DEBUG)
GETDEF JAVAConn::GetDefaultJavaVMInitArgs = NULL;
#endif   // _DEBUG

/***********************************************************************/
/*  Some macro's (should be defined elsewhere to be more accessible)   */
/***********************************************************************/
#if defined(_DEBUG)
#define ASSERT(f)          assert(f)
#define DEBUG_ONLY(f)      (f)
#else   // !_DEBUG
#define ASSERT(f)          ((void)0)
#define DEBUG_ONLY(f)      ((void)0)
#endif  // !_DEBUG

/***********************************************************************/
/*  JAVAConn construction/destruction.                                 */
/***********************************************************************/
JAVAConn::JAVAConn(PGLOBAL g, PCSZ wrapper)
{
	m_G = g;
	jvm = nullptr;            // Pointer to the JVM (Java Virtual Machine)
	env = nullptr;            // Pointer to native interface
	jdi = nullptr;						// Pointer to the java wrapper class
	job = nullptr;						// The java wrapper class object
	errid = nullptr;
	DiscFunc = "Disconnect";
	Msg = NULL;
	m_Wrap = (wrapper) ? wrapper : GetJavaWrapper();

	if (!strchr(m_Wrap, '/')) {
		// Add the wrapper package name
		char *wn = (char*)PlugSubAlloc(g, NULL, strlen(m_Wrap) + 10);
		m_Wrap = strcat(strcpy(wn, "wrappers/"), m_Wrap);
	} // endif m_Wrap

	fp = NULL;
	m_Opened = false;
	m_Connected = false;
	m_Rows = 0;
//*m_ErrMsg = '\0';
} // end of JAVAConn

//JAVAConn::~JAVAConn()
//  {
//if (Connected())
//  EndCom();

//  } // end of ~JAVAConn
char *JAVAConn::GetUTFString(jstring s)
{
	char *str;
	const char *utf = env->GetStringUTFChars(s, nullptr);

	str = PlugDup(m_G, utf);
	env->ReleaseStringUTFChars(s, utf);
	env->DeleteLocalRef(s);
	return str;
}	// end of GetUTFString

/***********************************************************************/
/*  Screen for errors.                                                 */
/***********************************************************************/
bool JAVAConn::Check(jint rc)
{
	jstring s;

	if (env->ExceptionCheck()) {
		jthrowable exc = env->ExceptionOccurred();
		jmethodID tid = env->GetMethodID(env->FindClass("java/lang/Object"),
			"toString", "()Ljava/lang/String;");

		if (exc != nullptr && tid != nullptr) {
			s = (jstring)env->CallObjectMethod(exc, tid);
			Msg = GetUTFString(s);
		} else
			Msg = "Exception occurred";

		env->ExceptionClear();
	} else if (rc < 0) {
		s = (jstring)env->CallObjectMethod(job, errid);
		Msg = GetUTFString(s);
	} else
		Msg = NULL;

	return (Msg != NULL);
} // end of Check

/***********************************************************************/
/*  Get MethodID if not exists yet.                                    */
/***********************************************************************/
bool JAVAConn::gmID(PGLOBAL g, jmethodID& mid, const char *name, const char *sig)
{
	if (mid == nullptr) {
		mid = env->GetMethodID(jdi, name, sig);

		if (Check()) {
			snprintf(g->Message, sizeof(g->Message), "%s", Msg);
			return true;
		} else
			return false;

	} else
		return false;

} // end of gmID

#if 0
/***********************************************************************/
/*  Utility routine.                                                   */
/***********************************************************************/
int JAVAConn::GetMaxValue(int n)
{
	jint      m;
	jmethodID maxid = nullptr;

	if (gmID(m_G, maxid, "GetMaxValue", "(I)I"))
		return -1;

	// call method
	if (Check(m = env->CallIntMethod(job, maxid, n)))
		htrc("GetMaxValue: %s", Msg);

	return (int)m;
} // end of GetMaxValue
#endif // 0

/***********************************************************************/
/*  Reset the JVM library.                                             */
/***********************************************************************/
void JAVAConn::ResetJVM(void)
{
	if (LibJvm) {
#if defined(_WIN32)
		FreeLibrary((HMODULE)LibJvm);
#else   // !_WIN32
		dlclose(LibJvm);
#endif  // !_WIN32
		LibJvm = NULL;
		CreateJavaVM = NULL;
		GetCreatedJavaVMs = NULL;
#if defined(_DEBUG)
		GetDefaultJavaVMInitArgs = NULL;
#endif   // _DEBUG
	} // endif LibJvm

} // end of ResetJVM

/***********************************************************************/
/*  Dynamically link the JVM library.                                  */
/*  The purpose of this function is to allow using the CONNECT plugin  */
/*  for other table types when the Java JDK is not installed.          */
/***********************************************************************/
bool JAVAConn::GetJVM(PGLOBAL g)
{
	int ntry;

	if (!LibJvm) {
		char soname[512];

#if defined(_WIN32)
		for (ntry = 0; !LibJvm && ntry < 3; ntry++) {
			if (!ntry && JvmPath) {
				snprintf(soname, sizeof(soname), "%s\\jvm.dll", JvmPath);

				ntry = 3;		 // No other try
			} else if (ntry < 2 && getenv("JAVA_HOME")) {
				safe_strcpy(soname, sizeof(soname), getenv("JAVA_HOME"));

				if (ntry == 1)
					safe_strcat(soname, sizeof(soname), "\\jre");

				safe_strcat(soname, sizeof(soname), "\\bin\\client\\jvm.dll");
			} else {
				// Try to find it through the registry
				char version[16];
				char javaKey[64] = "SOFTWARE\\JavaSoft\\Java Runtime Environment";
				LONG  rc;
				DWORD BufferSize = 16;

				safe_strcpy(soname, sizeof(soname), "jvm.dll");		// In case it fails

				if ((rc = RegGetValue(HKEY_LOCAL_MACHINE, javaKey, "CurrentVersion",
					RRF_RT_ANY, NULL, (PVOID)&version, &BufferSize)) == ERROR_SUCCESS) {
					safe_strcat(javaKey, sizeof(javaKey), "\\");
					safe_strcat(javaKey, sizeof(javaKey), version);
					BufferSize = sizeof(soname);

					if ((rc = RegGetValue(HKEY_LOCAL_MACHINE, javaKey, "RuntimeLib",
						RRF_RT_ANY, NULL, (PVOID)&soname, &BufferSize)) != ERROR_SUCCESS)
						printf("RegGetValue: rc=%ld\n", rc);

				} // endif rc

				ntry = 3;		 // Try this only once
			} // endelse

			// Load the desired shared library
			LibJvm = LoadLibrary(soname);
		}	// endfor ntry

		// Get the needed entries
		if (!LibJvm) {
			char  buf[256];
			DWORD rc = GetLastError();

			FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM |
										FORMAT_MESSAGE_IGNORE_INSERTS, NULL, rc, 0,
				            (LPTSTR)buf, sizeof(buf), NULL);
			snprintf(g->Message, sizeof(g->Message), MSG(DLL_LOAD_ERROR)": %s", rc,
							 soname, buf);
		} else if (!(CreateJavaVM = (CRTJVM)GetProcAddress((HINSTANCE)LibJvm,
			                                       "JNI_CreateJavaVM"))) {
			snprintf(g->Message, sizeof(g->Message), MSG(PROCADD_ERROR), GetLastError(), "JNI_CreateJavaVM");
			FreeLibrary((HMODULE)LibJvm);
			LibJvm = NULL;
		} else if (!(GetCreatedJavaVMs = (GETJVM)GetProcAddress((HINSTANCE)LibJvm,
			                                       "JNI_GetCreatedJavaVMs"))) {
			snprintf(g->Message, sizeof(g->Message), MSG(PROCADD_ERROR), GetLastError(), "JNI_GetCreatedJavaVMs");
			FreeLibrary((HMODULE)LibJvm);
			LibJvm = NULL;
#if defined(_DEBUG)
		} else if (!(GetDefaultJavaVMInitArgs = (GETDEF)GetProcAddress((HINSTANCE)LibJvm,
			                                       "JNI_GetDefaultJavaVMInitArgs"))) {
			snprintf(g->Message, sizeof(g->Message), MSG(PROCADD_ERROR), GetLastError(),
				"JNI_GetDefaultJavaVMInitArgs");
			FreeLibrary((HMODULE)LibJvm);
			LibJvm = NULL;
#endif   // _DEBUG
		} // endif LibJvm
#else   // !_WIN32
		const char *error = NULL;

		for (ntry = 0; !LibJvm && ntry < 2; ntry++) {
			if (!ntry && JvmPath) {
				snprintf(soname, sizeof(soname), "%s/libjvm.so", JvmPath);
				ntry = 2;
			} else if (!ntry && getenv("JAVA_HOME")) {
				// TODO: Replace i386 by a better guess
				snprintf(soname, sizeof(soname), "%s/jre/lib/i386/client/libjvm.so",
								 getenv("JAVA_HOME"));
			} else {	 // Will need LD_LIBRARY_PATH to be set
				safe_strcpy(soname, sizeof(soname), "libjvm.so");
				ntry = 2;
			} // endelse

			LibJvm = dlopen(soname, RTLD_LAZY);
		} // endfor ntry

			// Load the desired shared library
		if (!LibJvm) {
			error = dlerror();
			snprintf(g->Message, sizeof(g->Message), MSG(SHARED_LIB_ERR), soname, SVP(error));
		} else if (!(CreateJavaVM = (CRTJVM)dlsym(LibJvm, "JNI_CreateJavaVM"))) {
			error = dlerror();
			snprintf(g->Message, sizeof(g->Message), MSG(GET_FUNC_ERR), "JNI_CreateJavaVM", SVP(error));
			dlclose(LibJvm);
			LibJvm = NULL;
		} else if (!(GetCreatedJavaVMs = (GETJVM)dlsym(LibJvm, "JNI_GetCreatedJavaVMs"))) {
			error = dlerror();
			snprintf(g->Message, sizeof(g->Message), MSG(GET_FUNC_ERR), "JNI_GetCreatedJavaVMs", SVP(error));
			dlclose(LibJvm);
			LibJvm = NULL;
#if defined(_DEBUG)
		} else if (!(GetDefaultJavaVMInitArgs = (GETDEF)dlsym(LibJvm,
			"JNI_GetDefaultJavaVMInitArgs"))) {
			error = dlerror();
			snprintf(g->Message, sizeof(g->Message), MSG(GET_FUNC_ERR), "JNI_GetDefaultJavaVMInitArgs", SVP(error));
			dlclose(LibJvm);
			LibJvm = NULL;
#endif   // _DEBUG
		} // endif LibJvm
#endif  // !_WIN32

	} // endif LibJvm

	return LibJvm == NULL;
} // end of GetJVM

/***********************************************************************/
/*  Open: connect to a data source.                                    */
/***********************************************************************/
bool JAVAConn::Open(PGLOBAL g)
{
         bool		 brc = true;
	jboolean jt = (trace(1));

	// Link or check whether jvm library was linked
	if (GetJVM(g))
		return true;

	// Firstly check whether the jvm was already created
	JavaVM* jvms[1];
	jsize   jsz;
	jint    rc = GetCreatedJavaVMs(jvms, 1, &jsz);

	if (rc == JNI_OK && jsz == 1) {
		// jvm already existing
		jvm = jvms[0];
		rc = jvm->AttachCurrentThread((void**)&env, nullptr);

		if (rc != JNI_OK) {
			snprintf(g->Message, sizeof(g->Message), "Cannot attach jvm to the current thread");
			return true;
		} // endif rc

	} else {
		/*******************************************************************/
		/*  Create a new jvm																							 */
		/*******************************************************************/
		PSTRG    jpop = new(g)STRING(g, 512, "-Djava.class.path=.");
		char    *cp = NULL;
		char     sep;

#if defined(_WIN32)
		sep = ';';
#define N 1
		//#define N 2
		//#define N 3
#else
		sep = ':';
#define N 1
#endif

		// Add wrappers jar files 
		AddJars(jpop, sep);

		//================== prepare loading of Java VM ============================
		JavaVMInitArgs vm_args;                        // Initialization arguments
		JavaVMOption* options = new JavaVMOption[N];   // JVM invocation options

		// where to find java .class
		if (ClassPath && *ClassPath) {
			jpop->Append(sep);
			jpop->Append(ClassPath);
		}	// endif ClassPath

		// All wrappers are pre-compiled in JavaWrappers.jar in the share dir
		jpop->Append(sep);
		jpop->Append(GetMessageDir());
		jpop->Append("JavaWrappers.jar");

#if defined(MONGO_SUPPORT)
		jpop->Append(sep);
		jpop->Append(GetMessageDir());
		jpop->Append("Mongo3.jar");
		jpop->Append(sep);
		jpop->Append(GetMessageDir());
		jpop->Append("Mongo2.jar");
#endif   // MONGO_SUPPORT

		if ((cp = getenv("CLASSPATH"))) {
			jpop->Append(sep);
			jpop->Append(cp);
		} // endif cp

		if (trace(1)) {
			htrc("ClassPath=%s\n", ClassPath ? ClassPath : "null");
			htrc("CLASSPATH=%s\n", cp ? cp : "null");
			htrc("%s\n", jpop->GetStr());
		} // endif trace

		options[0].optionString = jpop->GetStr();
#if N == 2
		options[1].optionString = "-Xcheck:jni";
#endif
#if N == 3
		options[1].optionString = "-Xms256M";
		options[2].optionString = "-Xmx512M";
#endif
#if defined(_DEBUG)
		vm_args.version = JNI_VERSION_1_2;             // minimum Java version
		rc = GetDefaultJavaVMInitArgs(&vm_args);
#else
		vm_args.version = JNI_VERSION_1_6;             // minimum Java version
#endif   // _DEBUG
		vm_args.nOptions = N;                          // number of options
		vm_args.options = options;
		vm_args.ignoreUnrecognized = false; // invalid options make the JVM init fail

		//=============== load and initialize Java VM and JNI interface =============
		rc = CreateJavaVM(&jvm, (void**)&env, &vm_args);  // YES !!
		delete[] options;    // we then no longer need the initialisation options.

		switch (rc) {
			case JNI_OK:
				snprintf(g->Message, sizeof(g->Message), "VM successfully created");
				brc = false;
				break;
			case JNI_ERR:
				snprintf(g->Message, sizeof(g->Message), "Initialising JVM failed: unknown error");
				break;
			case JNI_EDETACHED:
				snprintf(g->Message, sizeof(g->Message), "Thread detached from the VM");
				break;
			case JNI_EVERSION:
				snprintf(g->Message, sizeof(g->Message), "JNI version error");
				break;
			case JNI_ENOMEM:
				snprintf(g->Message, sizeof(g->Message), "Not enough memory");
				break;
			case JNI_EEXIST:
				snprintf(g->Message, sizeof(g->Message), "VM already created");
				break;
			case JNI_EINVAL:
				snprintf(g->Message, sizeof(g->Message), "Invalid arguments");
				break;
			default:
				snprintf(g->Message, sizeof(g->Message), "Unknown return code %d", (int)rc);
				break;
		} // endswitch rc

		if (trace(1))
			htrc("%s\n", g->Message);

		if (brc)
			return true;

		//=============== Display JVM version ===============
		jint ver = env->GetVersion();
		printf("JVM Version %d.%d\n", ((ver >> 16) & 0x0f), (ver & 0x0f));
	} // endif rc

	// try to find the java wrapper class
	jdi = env->FindClass(m_Wrap);

	if (jdi == nullptr) {
		snprintf(g->Message, sizeof(g->Message), "ERROR: class %s not found!", m_Wrap);
		return true;
	} // endif jdi

#if 0		// Suppressed because it does not make any usable change
	if (b && jpath && *jpath) {
		// Try to add that path the the jvm class path
		jmethodID alp = env->GetStaticMethodID(jdi, "addLibraryPath",
			"(Ljava/lang/String;)I");

		if (alp == nullptr) {
			env->ExceptionDescribe();
			env->ExceptionClear();
		} else {
			char *msg;
			jstring path = env->NewStringUTF(jpath);
			rc = env->CallStaticIntMethod(jdi, alp, path);

			if ((msg = Check(rc))) {
				snprintf(g->Message, sizeof(g->Message), msg);
				env->DeleteLocalRef(path);
				return RC_FX;
			} else switch (rc) {
				case JNI_OK:
					printf("jpath added\n");
					break;
				case JNI_EEXIST:
					printf("jpath already exist\n");
					break;
				case JNI_ERR:
				default:
					snprintf(g->Message, sizeof(g->Message), "Error adding jpath");
					env->DeleteLocalRef(path);
					return RC_FX;
			}	// endswitch rc

			env->DeleteLocalRef(path);
		}	// endif alp

	}	// endif jpath
#endif // 0

	// if class found, continue
	jmethodID ctor = env->GetMethodID(jdi, "<init>", "(Z)V");

	if (ctor == nullptr) {
		snprintf(g->Message, sizeof(g->Message), "ERROR: %s constructor not found!", m_Wrap);
		return true;
	} else
		job = env->NewObject(jdi, ctor, jt);

	if (job == nullptr) {
		snprintf(g->Message, sizeof(g->Message), "%s class object not constructed!", m_Wrap);
		return true;
	} // endif job

	// If the object is successfully constructed, 
	// we can then search for the method we want to call, 
	// and invoke it for the object:
	errid = env->GetMethodID(jdi, "GetErrmsg", "()Ljava/lang/String;");

	if (env->ExceptionCheck()) {
		snprintf(g->Message, sizeof(g->Message), "ERROR: method GetErrmsg() not found!");
		env->ExceptionDescribe();
		env->ExceptionClear();
		return true;
	} // endif Check

	/*********************************************************************/
	/*  Link a Fblock. This make possible to automatically close it      */
	/*  in case of error (throw).                                        */
	/*********************************************************************/
	PDBUSER dbuserp = (PDBUSER)g->Activityp->Aptr;

	fp = (PFBLOCK)PlugSubAlloc(g, NULL, sizeof(FBLOCK));
	fp->Type = TYPE_FB_JAVA;
	fp->Fname = NULL;
	fp->Next = dbuserp->Openlist;
	dbuserp->Openlist = fp;
	fp->Count = 1;
	fp->Length = 0;
	fp->Memory = NULL;
	fp->Mode = MODE_ANY;
	fp->File = this;
	fp->Handle = 0;

	m_Opened = true;
	return false;
} // end of Open

/***********************************************************************/
/*  Disconnect connection                                              */
/***********************************************************************/
void JAVAConn::Close()
{
	jint rc;

	if (m_Connected) {
		jmethodID did = nullptr;

		// Could have been detached in case of join
		rc = jvm->AttachCurrentThread((void**)&env, nullptr);

		if (gmID(m_G, did, DiscFunc, "()I"))
			printf("%s\n", Msg);
		else if (Check(env->CallIntMethod(job, did)))
			printf("%s: %s\n", DiscFunc, Msg);

		m_Connected = false;
	}	// endif m_Connected

	if ((rc = jvm->DetachCurrentThread()) != JNI_OK)
		printf("DetachCurrentThread: rc=%d\n", (int)rc);

	if (fp)
		fp->Count = 0;

	m_Opened = false;
} // end of Close
