Configurator.java

/*
 * 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.
 */
package org.codehaus.plexus.classworlds.launcher;

/*
 * Copyright 2001-2006 Codehaus Foundation.
 *
 * 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.
 */

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.module.Configuration;
import java.lang.module.FindException;
import java.lang.module.ModuleFinder;
import java.lang.module.ResolutionException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.codehaus.plexus.classworlds.ClassWorld;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.classworlds.realm.DuplicateRealmException;
import org.codehaus.plexus.classworlds.realm.NoSuchRealmException;

/**
 * <code>Launcher</code> configurator.
 *
 * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
 * @author Jason van Zyl
 */
public class Configurator implements ConfigurationHandler {
    private static final System.Logger LOGGER = System.getLogger(Configurator.class.getName());

    /**
     * The launcher to configure.
     */
    private Launcher launcher;

    private ClassWorld world;

    /**
     * Processed Realms.
     */
    private Map<String, ClassRealm> configuredRealms;

    /**
     * Current Realm.
     */
    private ClassRealm curRealm;

    private ClassLoader foreignClassLoader = null;

    private List<Path> modulePaths = new ArrayList<>();

    /**
     * Module JAR paths that could not be loaded into a JPMS layer and must be added to the
     * first realm that gets created instead (used when the {@code module} directive in the
     * configuration file precedes any {@code [section]} header, so no realm exists yet when
     * the fallback fires).
     */
    private List<Path> pendingModuleClasspathFallback = new ArrayList<>();

    private List<String[]> exportDirectives = new ArrayList<>();

    private List<String[]> openDirectives = new ArrayList<>();

    private List<String[]> readDirectives = new ArrayList<>();

    /**
     * Construct.
     *
     * @param launcher The launcher to configure.
     */
    public Configurator(Launcher launcher) {
        this.launcher = launcher;
        configuredRealms = new HashMap<>();
        if (launcher != null) {
            this.foreignClassLoader = launcher.getSystemClassLoader();
        }
    }

    /**
     * Construct.
     *
     * @param world The classWorld to configure.
     */
    public Configurator(ClassWorld world) {
        setClassWorld(world);
    }

    /**
     * set world.
     * this setter is provided so you can use the same configurator to configure several "worlds"
     *
     * @param world The classWorld to configure.
     */
    public void setClassWorld(ClassWorld world) {
        this.world = world;
        configuredRealms = new HashMap<>();
    }

    /**
     * Configure from a file.
     *
     * @param is The config input stream
     * @throws IOException             If an error occurs reading the config file.
     * @throws MalformedURLException   If the config file contains invalid URLs.
     * @throws ConfigurationException  If the config file is corrupt.
     * @throws DuplicateRealmException If the config file defines two realms with the same id.
     * @throws NoSuchRealmException    If the config file defines a main entry point in
     *                                 a non-existent realm.
     */
    public void configure(InputStream is)
            throws IOException, ConfigurationException, DuplicateRealmException, NoSuchRealmException {
        if (world == null) {
            world = new ClassWorld();
        }
        curRealm = null;
        foreignClassLoader = null;
        modulePaths = new ArrayList<>();
        exportDirectives = new ArrayList<>();
        openDirectives = new ArrayList<>();
        readDirectives = new ArrayList<>();
        if (this.launcher != null) {
            foreignClassLoader = this.launcher.getSystemClassLoader();
        }
        ConfigurationParser parser = new ConfigurationParser(this, System.getProperties());
        parser.parse(is);
        createModuleLayer();
        // Associate child realms to their parents.
        associateRealms();
        if (this.launcher != null) {
            this.launcher.setWorld(world);
        }
    }

    // TODO return this to protected when the legacy wrappers can be removed.
    /**
     * Associate parent realms with their children.
     */
    public void associateRealms() {
        List<String> sortRealmNames = new ArrayList<>(configuredRealms.keySet());
        // sort by name
        sortRealmNames.sort(String::compareTo);
        // So now we have something like the following for defined
        // realms:
        //
        // root
        // root.maven
        // root.maven.plugin
        //
        // Now if the name of a realm is a superset of an existing realm
        // the we want to make child/parent associations.
        for (String realmName : sortRealmNames) {
            int j = realmName.lastIndexOf('.');
            if (j > 0) {
                String parentRealmName = realmName.substring(0, j);
                ClassRealm parentRealm = configuredRealms.get(parentRealmName);
                if (parentRealm != null) {
                    ClassRealm realm = configuredRealms.get(realmName);
                    realm.setParentRealm(parentRealm);
                }
            }
        }
    }

    public void addImportFrom(String relamName, String importSpec) throws NoSuchRealmException {
        curRealm.importFrom(relamName, importSpec);
    }

    public void addLoadFile(File file) {
        try {
            curRealm.addURL(file.toURI().toURL());
        } catch (MalformedURLException e) {
            // can't really happen... or can it?
        }
    }

    public void addLoadURL(URL url) {
        curRealm.addURL(url);
    }

    public void addRealm(String realmName) throws DuplicateRealmException {
        curRealm = world.newRealm(realmName, foreignClassLoader);
        // If the 'module' directive failed to create a JPMS layer before any section was
        // declared, drain the deferred classpath fallback into this first realm.
        if (!pendingModuleClasspathFallback.isEmpty()) {
            for (Path path : pendingModuleClasspathFallback) {
                try {
                    curRealm.addURL(path.toUri().toURL());
                } catch (MalformedURLException e) {
                    // ignore
                }
            }
            pendingModuleClasspathFallback.clear();
        }
        // Stash the configured realm for subsequent association processing.
        configuredRealms.put(realmName, curRealm);
    }

    public void addModuleFile(File file) {
        modulePaths.add(file.toPath());
    }

    public List<Path> getModulePaths() {
        return modulePaths;
    }

    public void addExports(String module, String pkg, String target) {
        exportDirectives.add(new String[] {module, pkg, target});
    }

    public void addOpens(String module, String pkg, String target) {
        openDirectives.add(new String[] {module, pkg, target});
    }

    public void addReads(String source, String target) {
        readDirectives.add(new String[] {source, target});
    }

    private void createModuleLayer() {
        if (modulePaths.isEmpty()
                && exportDirectives.isEmpty()
                && openDirectives.isEmpty()
                && readDirectives.isEmpty()) {
            return;
        }
        if (!modulePaths.isEmpty()) {
            try {
                Set<String> bootModuleNames = ModuleLayer.boot().modules().stream()
                        .map(Module::getName)
                        .collect(Collectors.toSet());
                ModuleFinder finder = ModuleFinder.of(modulePaths.toArray(new Path[0]));
                Set<String> newModules = finder.findAll().stream()
                        .map(ref -> ref.descriptor().name())
                        .filter(name -> !bootModuleNames.contains(name))
                        .collect(Collectors.toSet());
                if (!newModules.isEmpty()) {
                    Configuration cf =
                            ModuleLayer.boot().configuration().resolve(finder, ModuleFinder.of(), newModules);
                    ClassLoader parent =
                            foreignClassLoader != null ? foreignClassLoader : ClassLoader.getSystemClassLoader();
                    ModuleLayer.Controller controller =
                            ModuleLayer.defineModulesWithOneLoader(cf, List.of(ModuleLayer.boot()), parent);
                    ModuleLayer layer = controller.layer();
                    ClassLoader oldForeignClassLoader = foreignClassLoader;
                    foreignClassLoader = layer.findLoader(newModules.iterator().next());
                    world.setModuleLayer(layer, controller);
                    // Realms created during parsing (before this module layer existed) used the
                    // old foreignClassLoader as their parent. Retroactively update them so they
                    // delegate to the new module layer loader, making module-layer classes (e.g.
                    // JLine) accessible from the classrealm without duplicating jars on the
                    // classpath.
                    for (ClassRealm realm : configuredRealms.values()) {
                        if (realm.getParentClassLoader() == oldForeignClassLoader) {
                            realm.setParentClassLoader(foreignClassLoader);
                        }
                    }
                }
            } catch (FindException | ResolutionException | LayerInstantiationException e) {
                LOGGER.log(
                        System.Logger.Level.DEBUG,
                        "Failed to create JPMS module layer, falling back to classpath mode: " + e.getMessage());
                addModulePathsToClasspath();
            }
        }
        Module unnamedTarget = getClass().getModule();
        for (String[] directive : exportDirectives) {
            Module target = resolveTargetModule(directive[2], unnamedTarget);
            if (target != null) {
                world.addExports(directive[0], directive[1], target);
            }
        }
        for (String[] directive : openDirectives) {
            Module target = resolveTargetModule(directive[2], unnamedTarget);
            if (target != null) {
                world.addOpens(directive[0], directive[1], target);
            }
        }
        for (String[] directive : readDirectives) {
            Module target = resolveTargetModule(directive[1], unnamedTarget);
            if (target != null) {
                world.addReads(directive[0], target);
            }
        }
    }

    private void addModulePathsToClasspath() {
        if (curRealm != null) {
            for (Path path : modulePaths) {
                try {
                    curRealm.addURL(path.toUri().toURL());
                } catch (MalformedURLException e) {
                    // ignore
                }
            }
        } else {
            // The 'module' directive appeared before any [section]: no realm exists yet.
            // Defer adding these JARs to the classpath of the first realm to be created.
            pendingModuleClasspathFallback.addAll(modulePaths);
        }
    }

    private Module resolveTargetModule(String targetName, Module unnamedTarget) {
        if ("ALL-UNNAMED".equals(targetName)) {
            return unnamedTarget;
        }
        ModuleLayer layer = world.getModuleLayer();
        if (layer != null) {
            Module m = layer.findModule(targetName).orElse(null);
            if (m != null) {
                return m;
            }
        }
        return ModuleLayer.boot().findModule(targetName).orElse(null);
    }

    public void setAppMain(String mainClassName, String mainRealmName) {
        if (this.launcher != null) {
            this.launcher.setAppMain(mainClassName, mainRealmName);
        }
    }
}