ClassWorld.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;

/*
 * 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.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

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

/**
 * A collection of <code>ClassRealm</code>s, indexed by id.
 *
 * @author <a href="mailto:bob@eng.werken.com">bob mcwhirter</a>
 */
public class ClassWorld implements Closeable {

    private static final System.Logger LOGGER = System.getLogger(ClassWorld.class.getName());

    private Map<String, ClassRealm> realms;

    private final List<ClassWorldListener> listeners = new ArrayList<>();

    private ModuleLayer moduleLayer;

    private ModuleLayer.Controller moduleLayerController;

    public ClassWorld(String realmId, ClassLoader classLoader) {
        this();
        try {
            newRealm(realmId, classLoader);
        } catch (DuplicateRealmException e) {
            // Will never happen as we are just creating the world.
        }
    }

    public ClassWorld() {
        this.realms = new LinkedHashMap<>();
    }

    public ClassRealm newRealm(String id) throws DuplicateRealmException {
        return newRealm(id, getClass().getClassLoader());
    }

    public ClassRealm newRealm(String id, ClassLoader classLoader) throws DuplicateRealmException {
        return newRealm(id, classLoader, null);
    }

    /**
     * Adds a class realm with filtering.
     * Only resources/classes whose name matches a given predicate are exposed.
     * @param id The identifier for this realm, must not be <code>null</code>.
     * @param classLoader The base class loader for this realm, may be <code>null</code> to use the bootstrap class
     *            loader.
     * @param filter a predicate to apply to each resource name to determine if it should be loaded through this class loader
     * @return the created class realm
     * @throws DuplicateRealmException in case a realm with the given id does already exist
     * @since 2.7.0
     * @see FilteredClassRealm
     */
    public synchronized ClassRealm newRealm(String id, ClassLoader classLoader, Predicate<String> filter)
            throws DuplicateRealmException {
        if (realms.containsKey(id)) {
            throw new DuplicateRealmException(this, id);
        }
        ClassRealm realm;
        if (filter == null) {
            realm = new ClassRealm(this, id, classLoader);
        } else {
            realm = new FilteredClassRealm(filter, this, id, classLoader);
        }
        realms.put(id, realm);
        for (ClassWorldListener listener : listeners) {
            listener.realmCreated(realm);
        }
        return realm;
    }

    /**
     * Closes all contained class realms.
     * @since 2.7.0
     */
    @Override
    public synchronized void close() throws IOException {
        realms.values().forEach(this::disposeRealm);
        realms.clear();
    }

    public synchronized void disposeRealm(String id) throws NoSuchRealmException {
        ClassRealm realm = realms.remove(id);
        if (realm != null) {
            disposeRealm(realm);
        } else {
            throw new NoSuchRealmException(this, id);
        }
    }

    private void disposeRealm(ClassRealm realm) {
        try {
            realm.close();
        } catch (IOException ignore) {
        }
        for (ClassWorldListener listener : listeners) {
            listener.realmDisposed(realm);
        }
    }

    public synchronized ClassRealm getRealm(String id) throws NoSuchRealmException {
        if (realms.containsKey(id)) {
            return realms.get(id);
        }
        throw new NoSuchRealmException(this, id);
    }

    public synchronized Collection<ClassRealm> getRealms() {
        return Collections.unmodifiableList(new ArrayList<>(realms.values()));
    }

    public synchronized void setModuleLayer(ModuleLayer moduleLayer, ModuleLayer.Controller controller) {
        this.moduleLayer = moduleLayer;
        this.moduleLayerController = controller;
    }

    public ModuleLayer getModuleLayer() {
        return moduleLayer;
    }

    public ModuleLayer.Controller getModuleLayerController() {
        return moduleLayerController;
    }

    /**
     * Exports a package from a named module to the given target module.
     * Looks up the source module in the runtime layer first, then the boot layer.
     * Only runtime layer modules can be modified via the Controller; boot layer
     * modules require {@code --add-exports} in the launcher script.
     *
     * @param moduleName the source module name
     * @param packageName the package to export
     * @param target the target module to export to
     */
    public synchronized void addExports(String moduleName, String packageName, Module target) {
        applyModuleAccess(moduleName, packageName, target, false);
    }

    /**
     * Opens a package from a named module to the given target module for deep reflection.
     * Looks up the source module in the runtime layer first, then the boot layer.
     * Only runtime layer modules can be modified via the Controller; boot layer
     * modules require {@code --add-opens} in the launcher script.
     *
     * @param moduleName the source module name
     * @param packageName the package to open
     * @param target the target module to open to
     */
    public synchronized void addOpens(String moduleName, String packageName, Module target) {
        applyModuleAccess(moduleName, packageName, target, true);
    }

    /**
     * Adds a reads edge from the named source module to the given target module.
     * Only runtime layer modules can be modified via the Controller.
     *
     * @param sourceModuleName the source module name
     * @param target the target module to read
     */
    public synchronized void addReads(String sourceModuleName, Module target) {
        Module source = findModule(sourceModuleName);
        if (source == null) {
            LOGGER.log(
                    System.Logger.Level.WARNING,
                    "module-access: module ''{0}'' not found, ignoring add-reads",
                    sourceModuleName);
            return;
        }
        if (moduleLayerController == null) {
            LOGGER.log(
                    System.Logger.Level.WARNING,
                    "module-access: no ModuleLayer controller available, cannot add-reads {0}",
                    sourceModuleName);
            return;
        }
        if (isBootLayerModule(source)) {
            LOGGER.log(
                    System.Logger.Level.WARNING,
                    "module-access: module ''{0}'' is in the boot layer and cannot be modified at runtime;"
                            + " use --add-reads {0}=ALL-UNNAMED on the JVM command line instead",
                    sourceModuleName);
            return;
        }
        moduleLayerController.addReads(source, target);
    }

    private void applyModuleAccess(String moduleName, String packageName, Module target, boolean open) {
        String directive = open ? "add-opens" : "add-exports";
        Module source = findModule(moduleName);
        if (source == null) {
            LOGGER.log(
                    System.Logger.Level.WARNING,
                    "module-access: module ''{0}'' not found, ignoring {1} {0}/{2}",
                    moduleName,
                    directive,
                    packageName);
            return;
        }
        if (moduleLayerController == null) {
            LOGGER.log(
                    System.Logger.Level.WARNING,
                    "module-access: no ModuleLayer controller available, cannot {0} {1}/{2}",
                    directive,
                    moduleName,
                    packageName);
            return;
        }
        if (isBootLayerModule(source)) {
            LOGGER.log(
                    System.Logger.Level.WARNING,
                    "module-access: module ''{0}'' is in the boot layer and cannot be modified at runtime;"
                            + " use --{1} {0}/{2}=ALL-UNNAMED on the JVM command line instead",
                    moduleName,
                    directive,
                    packageName);
            return;
        }
        if (open) {
            moduleLayerController.addOpens(source, packageName, target);
        } else {
            moduleLayerController.addExports(source, packageName, target);
        }
    }

    private Module findModule(String moduleName) {
        if (moduleLayer != null) {
            Module m = moduleLayer.findModule(moduleName).orElse(null);
            if (m != null) {
                return m;
            }
        }
        return ModuleLayer.boot().findModule(moduleName).orElse(null);
    }

    private boolean isBootLayerModule(Module module) {
        return module.getLayer() == ModuleLayer.boot();
    }

    // from exports branch
    public synchronized ClassRealm getClassRealm(String id) {
        if (realms.containsKey(id)) {
            return realms.get(id);
        }
        return null;
    }

    public synchronized void addListener(ClassWorldListener listener) {
        // TODO ideally, use object identity, not equals
        if (!listeners.contains(listener)) {
            listeners.add(listener);
        }
    }

    public synchronized void removeListener(ClassWorldListener listener) {
        listeners.remove(listener);
    }
}