/*
 * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 *
 */

import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.net.URL;
import java.net.URLClassLoader;
import java.io.File;
import java.security.CodeSigner;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import sun.hotspot.WhiteBox;

public class RedefineClassApp {
    static WhiteBox wb = WhiteBox.getWhiteBox();

    public static interface Intf {            // Loaded from Boot class loader (-Xbootclasspath/a).
        public String get();
    }
    public static class Bar implements Intf { // Loaded from Boot class loader.
        public String get() {
            return "buzz";
        }
    }
    public static class Foo implements Intf { // Loaded from AppClassLoader
        public String get() {
            return "buzz";
        }
    }

    static int numTests = 0;
    static int failed = 0;
    static Instrumentation instrumentation;

    public static void main(String args[]) throws Throwable {
        if (wb.areSharedStringsIgnored()) {
          System.out.println("Shared strings are ignored.");
          return;
        }

        File bootJar = new File(args[0]);
        File appJar  = new File(args[1]);

        instrumentation = InstrumentationRegisterClassFileTransformer.getInstrumentation();
        System.out.println("INFO: instrumentation = " + instrumentation);

        testBootstrapCDS("Bootstrap Loader", bootJar);
        testAppCDSv1("Application Loader", appJar);

        if (failed > 0) {
            throw new RuntimeException("FINAL RESULT: " + failed + " out of " + numTests + " test case(s) have failed");
        } else {
            System.out.println("FINAL RESULT: All " + numTests + " test case(s) have passed!");
        }

        // Full GC. The cached objects in adjustable archive heap regions are
        // scanned. The archive regions are verified. No error should be
        // reported.
        wb.fullGC();
    }

    static void testBootstrapCDS(String group, File jar) throws Throwable {
        doTest(group, new Bar(), jar);
    }

    static void testAppCDSv1(String group, File jar) throws Throwable {
        doTest(group, new Foo(), jar);
    }

    static void checkArchivedMirrorObject(Class klass) {
        if (wb.areOpenArchiveHeapObjectsMapped()) {
            if (!wb.isShared(klass)) {
                failed ++;
                System.out.println("FAILED. " + klass + " mirror object is not archived");
                return;
            }
        }
    }

    static void doTest(String group, Intf object, File jar) throws Throwable {
        numTests ++;

        Class klass = object.getClass();
        System.out.println();
        System.out.println("++++++++++++++++++++++++++");
        System.out.println("Test group: " + group);
        System.out.println("Testing with classloader = " + klass.getClassLoader());
        System.out.println("Testing with class       = " + klass);
        System.out.println("Test is shared           = " + wb.isSharedClass(klass));
        System.out.println("++++++++++++++++++++++++++");

        // Check archived mirror object before redefine
        checkArchivedMirrorObject(klass);

        // Call get() before redefine. All strings in archived classes are shared.
        String res = object.get();
        System.out.println("get() returns " + res);
        if (res.equals("buzz") && wb.isShared(res)) {
            System.out.println("get() returns " + res + ", string is shared");
        } else {
            if (!res.equals("buzz")) {
                System.out.println("FAILED. buzz is expected but got " + res);
            } else {
                System.out.println("FAILED. " + res + " is not shared");
            }
            failed ++;
            return;
        }
        res = null; // release the local reference to the string

        // Run GC
        System.gc();
        System.gc();
        System.gc();

        // Redefine the shared class
        byte[] buff = Util.getClassFileFromJar(jar, klass.getName());
        Util.replace(buff, "buzz", "huzz");
        String f = "(failed)";
        try {
            instrumentation.redefineClasses(new ClassDefinition(klass, buff));
            f = object.get();
        } catch (UnmodifiableClassException|UnsupportedOperationException e) {
            e.printStackTrace();
        }
        if (f.equals("huzz")) {
            System.out.println("PASSED: object.get() after redefinition returns " + f);
        } else {
            System.out.println("FAILED: object.get() after redefinition returns " + f);
            failed ++;
        }

        // Run GC. Should not crash.
        System.gc();
        System.gc();
        System.gc();

        // Check archived mirror object after redefine and GC
        checkArchivedMirrorObject(klass);

        System.out.println("++++++++++++++++++++++++++++++++++++++++++++++++ (done)\n\n");
    }
}