/**
 * 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
 *
 *     http://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 <jni.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <grp.h>
#include <stdio.h>
#include <pwd.h>
#include <string.h>

#include "exception.h"
#include "org_apache_hadoop_security_JniBasedUnixGroupsMapping.h"
#include "org_apache_hadoop.h"
#include "hadoop_group_info.h"
#include "hadoop_user_info.h"

static jmethodID g_log_error_method;

static jclass g_string_clazz;

extern jobject pw_lock_object;

JNIEXPORT void JNICALL
Java_org_apache_hadoop_security_JniBasedUnixGroupsMapping_anchorNative(
JNIEnv *env, jclass clazz)
{
  jobject string_clazz;

  g_log_error_method = (*env)->GetStaticMethodID(env, clazz, "logError",
        "(ILjava/lang/String;)V");
  if (!g_log_error_method) {
    return; // an exception has been raised
  }
  string_clazz = (*env)->FindClass(env, "java/lang/String");
  if (!string_clazz) {
    return; // an exception has been raised
  }
  g_string_clazz = (*env)->NewGlobalRef(env, string_clazz);
  if (!g_string_clazz) {
    jthrowable jthr = newRuntimeException(env,
        "JniBasedUnixGroupsMapping#anchorNative: failed to make "
        "a global reference to the java.lang.String class\n");
    (*env)->Throw(env, jthr);
    return;
  }
}

/**
 * Log an error about a failure to look up a group ID
 *
 * @param env   The JNI environment
 * @param clazz JniBasedUnixGroupsMapping class
 * @param gid   The gid we failed to look up
 * @param ret   Failure code
 */
static void logError(JNIEnv *env, jclass clazz, jint gid, int ret)
{
  jstring error_msg;

  error_msg = (*env)->NewStringUTF(env, terror(ret));
  if (!error_msg) {
    (*env)->ExceptionClear(env);
    return;
  }
  (*env)->CallStaticVoidMethod(env, clazz, g_log_error_method, gid, error_msg);
  if ((*env)->ExceptionCheck(env)) {
    (*env)->ExceptionClear(env);
    return;
  }
  (*env)->DeleteLocalRef(env, error_msg);
}

JNIEXPORT jobjectArray JNICALL 
Java_org_apache_hadoop_security_JniBasedUnixGroupsMapping_getGroupsForUser 
(JNIEnv *env, jclass clazz, jstring jusername)
{
  const char *username = NULL;
  struct hadoop_user_info *uinfo = NULL;
  struct hadoop_group_info *ginfo = NULL;
  jstring jgroupname = NULL;
  int i, ret, nvalid;
  int pw_lock_locked = 0;
  jobjectArray jgroups = NULL, jnewgroups = NULL;

  if (pw_lock_object != NULL) {
    if ((*env)->MonitorEnter(env, pw_lock_object) != JNI_OK) {
      goto done; // exception thrown
    }
    pw_lock_locked = 1;
  }
  username = (*env)->GetStringUTFChars(env, jusername, NULL);
  if (username == NULL) {
    goto done; // exception thrown
  }
  uinfo = hadoop_user_info_alloc();
  if (!uinfo) {
    THROW(env, "java/lang/OutOfMemoryError", NULL);
    goto done;
  }
  ret = hadoop_user_info_fetch(uinfo, username);
  if (ret) {
    if (ret == ENOENT) {
      jgroups = (*env)->NewObjectArray(env, 0, g_string_clazz, NULL);
    } else { // handle other errors
      (*env)->Throw(env, newRuntimeException(env,
          "getgrouplist: error looking up user. %d (%s)", ret, terror(ret)));
    }
    goto done;
  }

  ginfo = hadoop_group_info_alloc();
  if (!ginfo) {
    THROW(env, "java/lang/OutOfMemoryError", NULL);
    goto done;
  }
  ret = hadoop_user_info_getgroups(uinfo);
  if (ret) {
    if (ret == ENOMEM) {
      THROW(env, "java/lang/OutOfMemoryError", NULL);
    } else {
      (*env)->Throw(env, newRuntimeException(env,
          "getgrouplist: error looking up group. %d (%s)", ret, terror(ret)));
    }
    goto done;
  }
  jgroups = (jobjectArray)(*env)->NewObjectArray(env, uinfo->num_gids,
                                                 g_string_clazz, NULL);
  for (nvalid = 0, i = 0; i < uinfo->num_gids; i++) {
    ret = hadoop_group_info_fetch(ginfo, uinfo->gids[i]);
    if (ret) {
      logError(env, clazz, uinfo->gids[i], ret);
    } else {
      jgroupname = (*env)->NewStringUTF(env, ginfo->group.gr_name);
      if (!jgroupname) { // exception raised
        (*env)->DeleteLocalRef(env, jgroups);
        jgroups = NULL;
        goto done;
      }
      (*env)->SetObjectArrayElement(env, jgroups, nvalid++, jgroupname);
      // We delete the local reference once the element is in the array.
      // This is OK because the array has a reference to it.
      // Technically JNI only mandates that the JVM allow up to 16 local
      // references at a time  (though many JVMs allow more than that.)
      (*env)->DeleteLocalRef(env, jgroupname);
    }
  }
  if (nvalid != uinfo->num_gids) {
    // If some group names could not be looked up, allocate a smaller array
    // with just the entries that could be resolved.  Java has no equivalent to
    // realloc, so we have to do this manually.
    jnewgroups = (jobjectArray)(*env)->NewObjectArray(env, nvalid,
            (*env)->FindClass(env, "java/lang/String"), NULL);
    if (!jnewgroups) { // exception raised
      (*env)->DeleteLocalRef(env, jgroups);
      jgroups = NULL;
      goto done;
    }
    for (i = 0; i < nvalid; i++) {
      jgroupname = (*env)->GetObjectArrayElement(env, jgroups, i);
      (*env)->SetObjectArrayElement(env, jnewgroups, i, jgroupname);
      (*env)->DeleteLocalRef(env, jgroupname);
    }
    (*env)->DeleteLocalRef(env, jgroups);
    jgroups = jnewgroups;
  }

done:
  if (pw_lock_locked) {
    (*env)->MonitorExit(env, pw_lock_object);
  }
  if (username) {
    (*env)->ReleaseStringUTFChars(env, jusername, username);
  }
  if (uinfo) {
    hadoop_user_info_free(uinfo);
  }
  if (ginfo) {
    hadoop_group_info_free(ginfo);
  }
  if (jgroupname) {
    (*env)->DeleteLocalRef(env, jgroupname);
  }
  return jgroups;
}
