/*
 * Copyright 2015 Google Inc.
 *
 * Licensed 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.
 */

package com.google.template.soy.jbcsrc;

import com.google.common.base.Throwables;
import com.google.template.soy.jbcsrc.shared.Names;

import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;

import javax.annotation.Nullable;

/**
 * Base class to share code between our custom memory based classloader implementations.
 */
abstract class AbstractMemoryClassLoader extends ClassLoader {
  private static final ProtectionDomain DEFAULT_PROTECTION_DOMAIN;

  static {
    ClassLoader.registerAsParallelCapable();

    DEFAULT_PROTECTION_DOMAIN =
        AccessController.doPrivileged(
            new PrivilegedAction<ProtectionDomain>() {
              @Override
              public ProtectionDomain run() {
                return MemoryClassLoader.class.getProtectionDomain();
              }
            });
  }

  AbstractMemoryClassLoader() {
    // We want our loaded classes to be a child classloader of ours to make sure they have access
    // to the same classes that we do.
    this(AbstractMemoryClassLoader.class.getClassLoader());
  }

  AbstractMemoryClassLoader(ClassLoader classLoader) {
    super(classLoader);
  }

  /** Returns a data object for a class with the given name or {@code null} if it doesn't exist. */
  @Nullable
  abstract ClassData getClassData(String name);

  @Override
  public final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // we need to override parent delegation if we are loading a generated class, since the parent
    // may contain a reference to the same class (if it is running with precompiled soy templates),
    // but we don't want to use it in this case.
    // This replicates part of super.loadClass.
    if (name.startsWith(Names.CLASS_PREFIX)) {
      synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        // Unlike super.loadClass we don't call parent.loadClass here
        if (c == null) {
          c = findClass(name);
        }
        if (resolve) {
          resolveClass(c);
        }
        return c;
      }
    }
    // otherwise use normal parent delegation
    return super.loadClass(name, resolve);
  }
  
  @Override
  protected final Class<?> findClass(String name) throws ClassNotFoundException {
    ClassData classDef = getClassData(name);
    if (classDef == null) {
      throw new ClassNotFoundException(name);
    }
    try {
      return super.defineClass(
          name, classDef.data(), 0, classDef.data().length, DEFAULT_PROTECTION_DOMAIN);
    } catch (Throwable t) {
      // Attach additional information in a suppressed exception to make debugging easier.
      t.addSuppressed(new RuntimeException("Failed to load generated class:\n" + classDef));
      Throwables.propagateIfInstanceOf(t, ClassNotFoundException.class);
      throw Throwables.propagate(t);
    }
  }

  @Override
  protected final URL findResource(final String name) {
    if (!name.endsWith(".class")) {
      return null;
    }
    String className = name.substring(0, name.length() - ".class".length()).replace('/', '.');
    ClassData classDef = getClassData(className);
    if (classDef == null) {
      return null;
    }
    return classDef.asUrl();
  }
}

