/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "jsvc.h"

#ifdef OS_CYGWIN
typedef long long __int64;
#endif
#include <unistd.h>
#include <jni.h>

#ifdef CHARSET_EBCDIC
#ifdef OSD_POSIX
#include <ascii_ebcdic.h>
#define jsvc_xlate_to_ascii(b) _e2a(b)
#define jsvc_xlate_from_ascii(b) _a2e(b)
#endif
#else
#define jsvc_xlate_to_ascii(b)         /* NOOP */
#define jsvc_xlate_from_ascii(b)       /* NOOP */
#endif

static JavaVM *jvm = NULL;
static JNIEnv *env = NULL;
static jclass cls = NULL;

#define FALSE 0
#define TRUE !FALSE

static void shutdown(JNIEnv * env, jobject source, jboolean reload)
{
    log_debug("Shutdown requested (reload is %d)", reload);
    if (reload == TRUE)
        main_reload();
    else
        main_shutdown();
}

static void failed(JNIEnv * env, jobject source, jstring message)
{
    if (message) {
        const char *msg = (*env)->GetStringUTFChars(env, message, NULL);
        log_error("Failed %s", msg ? msg : "(null)");
        if (msg)
            (*env)->ReleaseStringUTFChars(env, message, msg);
    }
    else
        log_error("Failed requested");
    main_shutdown();
}

/* Automatically restart when the JVM crashes */
static void java_abort123(void)
{
    exit(123);
}

char *java_library(arg_data *args, home_data *data)
{
    char *libf = NULL;

    /* Did we find ANY virtual machine? */
    if (data->jnum == 0) {
        log_error("Cannot find any VM in Java Home %s", data->path);
        return NULL;
    }

    /* Select the VM */
    if (args->name == NULL) {
        libf = data->jvms[0]->libr;
        log_debug("Using default JVM in %s", libf);
    }
    else {
        int x;
        for (x = 0; x < data->jnum; x++) {
            if (data->jvms[x]->name == NULL)
                continue;
            if (strcmp(args->name, data->jvms[x]->name) == 0) {
                libf = data->jvms[x]->libr;
                log_debug("Using specific JVM in %s", libf);
                break;
            }
        }
        if (libf == NULL) {
            log_error("Invalid JVM name specified %s", args->name);
            return NULL;
        }
    }
    return libf;
}

typedef jint(*jvm_create_t) (JavaVM **, JNIEnv **, JavaVMInitArgs *);

bool java_signal(void)
{
    jmethodID method;
    jboolean ret;
    char start[] = "signal";
    char startparams[] = "()Z";

    jsvc_xlate_to_ascii(start);
    jsvc_xlate_to_ascii(startparams);
    method = (*env)->GetStaticMethodID(env, cls, start, startparams);
    if (method == NULL) {
        (*env)->ExceptionClear(env);
        log_error("Cannot find DaemonLoader \"signal\" method");
        return false;
    }

    ret = (*env)->CallStaticBooleanMethod(env, cls, method);
    /* Clear any pending exception
     * so we can continue calling native methods
     */
    (*env)->ExceptionClear(env);
    log_debug("Daemon signal method returned %s", ret ? "true" : "false");
    return ret;
}

/* Initialize the JVM and its environment, loading libraries and all */
bool java_init(arg_data *args, home_data *data)
{
#ifdef OS_DARWIN
    dso_handle apph = NULL;
    char appf[1024];
    struct stat sb;
#endif /* ifdef OS_DARWIN */
    jvm_create_t symb = NULL;
    JNINativeMethod nativemethods[2];
    JavaVMOption *opt = NULL;
    dso_handle libh = NULL;
    JavaVMInitArgs arg;
    char *libf = NULL;
    jint ret;
    int x;
    char loaderclass[] = LOADER;
    char shutdownmethod[] = "shutdown";
    char shutdownparams[] = "(Z)V";
    char failedmethod[] = "failed";
    char failedparams[] = "(Ljava/lang/String;)V";
    char daemonprocid[64];
    /* Decide WHAT virtual machine we need to use */
    libf = java_library(args, data);
    if (libf == NULL) {
        log_error("Cannot locate JVM library file");
        return false;
    }

    /* Some Java options require environment variables to be set before loading
     * Java when using JNI
     */
    for (x = 0; x < args->onum; x++) {
        if (!strncmp(args->opts[x], "-XX:NativeMemoryTracking=", 25)) {
            fprintf(stdout, "Found [%s]\n", args->opts[x]);
            char *value = args->opts[x] + 25;
            fprintf(stdout, "Found value [%s]\n", value);
            if (strcmp(value, "off")) {
                snprintf(daemonprocid, sizeof(daemonprocid), "NMT_LEVEL_%d=%s", (int)getpid(), value);
                fprintf(stdout, "Setting environment entry [%s]\n", daemonprocid);
                putenv(daemonprocid);
            }
        }
    }

    /* Initialize the DSO library */
    if (dso_init() != true) {
        log_error("Cannot initialize the dynamic library loader");
        return false;
    }

    /* Load the JVM library */
#if !defined(OSD_POSIX)
    libh = dso_link(libf);
    if (libh == NULL) {
        log_error("Cannot dynamically link to %s", libf);
        log_error("%s", dso_error());
        return false;
    }
    log_debug("JVM library %s loaded", libf);
#endif

#ifdef OS_DARWIN
    /*
       MacOS/X actually has two libraries, one with the REAL vm, and one for
       the VM startup.
       - JVM 1.6, the library name is libverify.dylib
       - JVM 1.7 onwards, the library name is libjli.dylib
     */
    if (replace(appf, 1024, "$JAVA_HOME/../Libraries/libverify.dylib",
                "$JAVA_HOME", data->path) != 0) {
        log_error("Cannot replace values in loader library");
        return false;
    }
    if (stat(appf, &sb)) {
        if (replace(appf, 1024, "$JAVA_HOME/../MacOS/libjli.dylib", "$JAVA_HOME", data->path) != 0) {
            log_error("Cannot replace values in loader library");
            return false;
        }
    }
    // DAEMON-331 Alternative path for custom OpenJDK builds
    if (stat(appf, &sb)) {
        if (replace(appf, 1024, "$JAVA_HOME/lib/jli/libjli.dylib", "$JAVA_HOME", data->path) != 0) {
            log_error("Cannot replace values in loader library");
            return false;
        }
    }
    apph = dso_link(appf);
    if (apph == NULL) {
        log_error("Cannot load required shell library %s", appf);
        return false;
    }
    log_debug("Shell library %s loaded", appf);
#endif /* ifdef OS_DARWIN */
#if defined(OSD_POSIX)
    /* BS2000 does not allow to call JNI_CreateJavaVM indirectly */
#else
    symb = (jvm_create_t) dso_symbol(libh, "JNI_CreateJavaVM");
    if (symb == NULL) {
#ifdef OS_DARWIN
        symb = (jvm_create_t) dso_symbol(apph, "JNI_CreateJavaVM");
        if (symb == NULL) {
#endif /* ifdef OS_DARWIN */
            log_error("Cannot find JVM library entry point");
            return false;
#ifdef OS_DARWIN
        }
#endif /* ifdef OS_DARWIN */
    }
    log_debug("JVM library entry point found (0x%08X)", symb);
#endif

    /* Prepare the VM initialization arguments */

    /* Minimum Java version is Java 6 */
    arg.version = JNI_VERSION_1_6;

#if defined(OSD_POSIX)
    if (JNI_GetDefaultJavaVMInitArgs(&arg) < 0) {
        log_error("Cannot init default JVM default args");
        return false;
    }
#endif
    arg.ignoreUnrecognized = FALSE;
    arg.nOptions = args->onum + 5;     /* pid, ppid, version, class and abort */
    opt = (JavaVMOption *) malloc(arg.nOptions * sizeof(JavaVMOption));
    for (x = 0; x < args->onum; x++) {
        opt[x].optionString = strdup(args->opts[x]);
        jsvc_xlate_to_ascii(opt[x].optionString);
        opt[x].extraInfo = NULL;
    }
    /* Add our daemon process id */
    snprintf(daemonprocid, sizeof(daemonprocid), "-Dcommons.daemon.process.id=%d", (int)getpid());
    opt[x].optionString = strdup(daemonprocid);
    jsvc_xlate_to_ascii(opt[x].optionString);
    opt[x++].extraInfo = NULL;

    snprintf(daemonprocid, sizeof(daemonprocid),
             "-Dcommons.daemon.process.parent=%d", (int)getppid());
    opt[x].optionString = strdup(daemonprocid);
    jsvc_xlate_to_ascii(opt[x].optionString);
    opt[x++].extraInfo = NULL;

    snprintf(daemonprocid, sizeof(daemonprocid),
             "-Dcommons.daemon.version=%s", JSVC_VERSION_STRING);
    opt[x].optionString = strdup(daemonprocid);
    jsvc_xlate_to_ascii(opt[x].optionString);
    opt[x++].extraInfo = NULL;

    /* DBCP-388. For the benefit of jconsole. */
    snprintf(daemonprocid, sizeof(daemonprocid), "-Dsun.java.command=%s", args->clas);
    opt[x].optionString = strdup(daemonprocid);
    jsvc_xlate_to_ascii(opt[x].optionString);
    opt[x++].extraInfo = NULL;

    opt[x].optionString = strdup("abort");
    jsvc_xlate_to_ascii(opt[x].optionString);
    opt[x].extraInfo = (void *)java_abort123;

    arg.options = opt;

    /* Do some debugging */
    if (log_debug_flag == true) {
        log_debug("+-- DUMPING JAVA VM CREATION ARGUMENTS -----------------");
        log_debug("| Version:                       %#08x", arg.version);
        log_debug("| Ignore Unrecognized Arguments: %s",
                  arg.ignoreUnrecognized == TRUE ? "True" : "False");
        log_debug("| Extra options:                 %d", args->onum);

        for (x = 0; x < args->onum; x++) {
            jsvc_xlate_from_ascii(opt[x].optionString);
            log_debug("|   \"%s\" (0x%08x)", opt[x].optionString, opt[x].extraInfo);
            jsvc_xlate_to_ascii(opt[x].optionString);
        }
        log_debug("+-------------------------------------------------------");
        log_debug("| Internal options:              %d", arg.nOptions - args->onum);

        for (; x < arg.nOptions; x++) {
            jsvc_xlate_from_ascii(opt[x].optionString);
            log_debug("|   \"%s\" (0x%08x)", opt[x].optionString, opt[x].extraInfo);
            jsvc_xlate_to_ascii(opt[x].optionString);
        }
        log_debug("+-------------------------------------------------------");
    }

    /* And finally create the Java VM */
#if defined(OSD_POSIX)
    ret = JNI_CreateJavaVM(&jvm, &env, &arg);
#else
    ret = (*symb) (&jvm, &env, &arg);
#endif
    if (ret < 0) {
        log_error("Cannot create Java VM");
        return false;
    }
    log_debug("Java VM created successfully");

    jsvc_xlate_to_ascii(loaderclass);
    cls = (*env)->FindClass(env, loaderclass);
    jsvc_xlate_from_ascii(loaderclass);
    if (cls == NULL) {
        log_error("Cannot find daemon loader %s", loaderclass);
        return false;
    }
    log_debug("Class %s found", loaderclass);

    jsvc_xlate_to_ascii(shutdownmethod);
    nativemethods[0].name = shutdownmethod;
    jsvc_xlate_to_ascii(shutdownparams);
    nativemethods[0].signature = shutdownparams;
    nativemethods[0].fnPtr = (void *)shutdown;
    jsvc_xlate_to_ascii(failedmethod);
    nativemethods[1].name = failedmethod;
    jsvc_xlate_to_ascii(failedparams);
    nativemethods[1].signature = failedparams;
    nativemethods[1].fnPtr = (void *)failed;

    if ((*env)->RegisterNatives(env, cls, nativemethods, 2) != 0) {
        log_error("Cannot register native methods");
        return false;
    }
    log_debug("Native methods registered");

    return true;
}

/* Destroy the Java VM */
bool JVM_destroy(int exit)
{
    jclass system = NULL;
    jmethodID method;
    char System[] = "java/lang/System";
    char exitclass[] = "exit";
    char exitparams[] = "(I)V";

    jsvc_xlate_to_ascii(System);
    system = (*env)->FindClass(env, System);
    jsvc_xlate_from_ascii(System);
    if (system == NULL) {
        log_error("Cannot find class %s", System);
        return false;
    }

    jsvc_xlate_to_ascii(exitclass);
    jsvc_xlate_to_ascii(exitparams);
    method = (*env)->GetStaticMethodID(env, system, exitclass, exitparams);
    if (method == NULL) {
        log_error("Cannot find \"System.exit(int)\" entry point");
        return false;
    }

    log_debug("Calling System.exit(%d)", exit);
    (*env)->CallStaticVoidMethod(env, system, method, (jint) exit);

    /* We shouldn't get here, but just in case... */
    log_debug("Destroying the Java VM");
    if ((*jvm)->DestroyJavaVM(jvm) != 0)
        return false;
    log_debug("Java VM destroyed");
    return true;
}

/* Call the load method in our DaemonLoader class */
bool java_load(arg_data *args)
{
    jclass stringClass = NULL;
    jstring className = NULL;
    jstring currentArgument = NULL;
    jobjectArray stringArray = NULL;
    jmethodID method = NULL;
    jboolean ret = FALSE;
    int x;
    char lang[] = "java/lang/String";
    char load[] = "load";
    char loadparams[] = "(Ljava/lang/String;[Ljava/lang/String;)Z";

    jsvc_xlate_to_ascii(args->clas);
    className = (*env)->NewStringUTF(env, args->clas);
    jsvc_xlate_from_ascii(args->clas);
    if (className == NULL) {
        log_error("Cannot create string for class name");
        return false;
    }

    jsvc_xlate_to_ascii(lang);
    stringClass = (*env)->FindClass(env, lang);
    if (stringClass == NULL) {
        log_error("Cannot find class java/lang/String");
        return false;
    }

    stringArray = (*env)->NewObjectArray(env, args->anum, stringClass, NULL);
    if (stringArray == NULL) {
        log_error("Cannot create arguments array");
        return false;
    }

    for (x = 0; x < args->anum; x++) {
        jsvc_xlate_to_ascii(args->args[x]);
        currentArgument = (*env)->NewStringUTF(env, args->args[x]);
        if (currentArgument == NULL) {
            jsvc_xlate_from_ascii(args->args[x]);
            log_error("Cannot create string for argument %s", args->args[x]);
            return false;
        }
        (*env)->SetObjectArrayElement(env, stringArray, x, currentArgument);
    }

    jsvc_xlate_to_ascii(load);
    jsvc_xlate_to_ascii(loadparams);
    method = (*env)->GetStaticMethodID(env, cls, load, loadparams);
    if (method == NULL) {
        log_error("Cannot find Daemon Loader \"load\" entry point");
        return false;
    }

    log_debug("Daemon loading...");
    ret = (*env)->CallStaticBooleanMethod(env, cls, method, className, stringArray);
    if (ret == FALSE) {
        log_error("Cannot load daemon");
        return false;
    }

    log_debug("Daemon loaded successfully");
    return true;
}

/* Call the start method in our daemon loader */
bool java_start(void)
{
    jmethodID method;
    jboolean ret;
    char start[] = "start";
    char startparams[] = "()Z";

    jsvc_xlate_to_ascii(start);
    jsvc_xlate_to_ascii(startparams);
    method = (*env)->GetStaticMethodID(env, cls, start, startparams);
    if (method == NULL) {
        log_error("Cannot find Daemon Loader \"start\" entry point");
        return false;
    }

    ret = (*env)->CallStaticBooleanMethod(env, cls, method);
    if (ret == FALSE) {
        log_error("Cannot start daemon");
        return false;
    }

    log_debug("Daemon started successfully");
    return true;
}

/*
 * call the java sleep to prevent problems with threads
 */
void java_sleep(int wait)
{
    jclass clsThread;
    jmethodID method;
    char jsleep[] = "sleep";
    char jsleepparams[] = "(J)V";
    char jthread[] = "java/lang/Thread";

    jsvc_xlate_to_ascii(jsleep);
    jsvc_xlate_to_ascii(jsleepparams);
    jsvc_xlate_to_ascii(jthread);

    clsThread = (*env)->FindClass(env, jthread);
    if (clsThread == NULL) {
        log_error("Cannot find java/lang/Thread class");
        return;
    }
    method = (*env)->GetStaticMethodID(env, clsThread, jsleep, jsleepparams);
    if (method == NULL) {
        log_error("Cannot found the sleep entry point");
        return;
    }

    (*env)->CallStaticVoidMethod(env, clsThread, method, (jlong) wait * 1000);
}

/* Call the stop method in our daemon loader */
bool java_stop(void)
{
    jmethodID method;
    jboolean ret;
    char stop[] = "stop";
    char stopparams[] = "()Z";

    jsvc_xlate_to_ascii(stop);
    jsvc_xlate_to_ascii(stopparams);
    method = (*env)->GetStaticMethodID(env, cls, stop, stopparams);
    if (method == NULL) {
        log_error("Cannot found Daemon Loader \"stop\" entry point");
        return false;
    }

    ret = (*env)->CallStaticBooleanMethod(env, cls, method);
    if (ret == FALSE) {
        log_error("Cannot stop daemon");
        return false;
    }

    log_debug("Daemon stopped successfully");
    return true;
}

/* Call the version method in our daemon loader */
bool java_version(void)
{
    jmethodID method;
    char version[] = "version";
    char versionparams[] = "()V";

    jsvc_xlate_to_ascii(version);
    jsvc_xlate_to_ascii(versionparams);
    method = (*env)->GetStaticMethodID(env, cls, version, versionparams);
    if (method == NULL) {
        log_error("Cannot found Daemon Loader \"version\" entry point");
        return false;
    }

    (*env)->CallStaticVoidMethod(env, cls, method);
    return true;
}

/* Call the check method in our DaemonLoader class */
bool java_check(arg_data *args)
{
    jstring className = NULL;
    jmethodID method = NULL;
    jboolean ret = FALSE;
    char check[] = "check";
    char checkparams[] = "(Ljava/lang/String;)Z";

    log_debug("Checking daemon");

    jsvc_xlate_to_ascii(args->clas);
    className = (*env)->NewStringUTF(env, args->clas);
    jsvc_xlate_from_ascii(args->clas);
    if (className == NULL) {
        log_error("Cannot create string for class name");
        return false;
    }

    jsvc_xlate_to_ascii(check);
    jsvc_xlate_to_ascii(checkparams);
    method = (*env)->GetStaticMethodID(env, cls, check, checkparams);
    if (method == NULL) {
        log_error("Cannot found Daemon Loader \"check\" entry point");
        return false;
    }

    ret = (*env)->CallStaticBooleanMethod(env, cls, method, className);
    if (ret == FALSE) {
        log_error("An error was detected checking the %s daemon", args->clas);
        return false;
    }

    log_debug("Daemon checked successfully");
    return true;
}

/* Call the destroy method in our daemon loader */
bool java_destroy(void)
{
    jmethodID method;
    jboolean ret;
    char destroy[] = "destroy";
    char destroyparams[] = "()Z";

    jsvc_xlate_to_ascii(destroy);
    jsvc_xlate_to_ascii(destroyparams);
    method = (*env)->GetStaticMethodID(env, cls, destroy, destroyparams);
    if (method == NULL) {
        log_error("Cannot found Daemon Loader \"destroy\" entry point");
        return false;
    }

    ret = (*env)->CallStaticBooleanMethod(env, cls, method);
    if (ret == FALSE) {
        log_error("Cannot destroy daemon");
        return false;
    }

    log_debug("Daemon destroyed successfully");
    return true;
}
