FqcnResolver.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.apache.commons.jexl3.internal;

import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.introspection.JexlUberspect;

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Helper resolving a simple class name into a fully-qualified class name (hence FqcnResolver) using
 * package names as roots of import.
 * <p>This only keeps names of classes to avoid any class loading/reloading/permissions issue.</p>
 */
 class FqcnResolver implements JexlContext.ClassNameResolver {
    /**
     * The class loader.
     */
    private final JexlUberspect uberspect;
    /**
     * A lock for RW concurrent ops.
     */
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    /**
     * The set of packages to be used as import roots.
     */
    private final Set<String> imports = new LinkedHashSet<>();
    /**
     * The map of solved fqcns based on imports keyed on (simple) name,
     * valued as fully-qualified class name.
     */
    private final Map<String, String> fqcns = new HashMap<>();
    /**
     * Optional parent solver.
     */
    private final FqcnResolver parent;

    /**
     * Adds a collection of packages as import root, checks the names are one of a package.
     * @param names the package names
     */
    private void importCheck(final Iterable<String> names) {
        if (names != null) {
            names.forEach(this::importCheck);
        }
    }

    /**
     * Adds a package as import root, checks the name if one of a package.
     * @param name the package name
     */
    private void importCheck(final String name) {
        // check the package name actually points to a package to avoid clutter
        if (name != null && Package.getPackage(name) != null) {
            imports.add(name);
        }
    }

    @Override
    public String resolveClassName(final String name) {
        return getQualifiedName(name);
    }

    /**
     * Creates a class name solver.
     *
     * @param uber   the optional class loader
     * @param packages the optional package names
     */
    FqcnResolver(final JexlUberspect uber, final Iterable<String> packages) {
        this.uberspect = uber;
        this.parent = null;
        importCheck(packages);
    }

    /**
     * Creates a class name solver.
     *
     * @param solver the parent solver
     * @throws NullPointerException if parent solver is null
     */
    FqcnResolver(final FqcnResolver solver) {
        if (solver == null) {
            throw new NullPointerException("parent solver can not be null");
        }
        this.parent = solver;
        this.uberspect = solver.uberspect;
    }

    /**
     * Checks is a package is imported by this solver of one of its ascendants.
     *
     * @param pkg the package name
     * @return true if an import exists for this package, false otherwise
     */
    boolean isImporting(final String pkg) {
        if (parent != null && parent.isImporting(pkg)) {
            return true;
        }
        lock.readLock().lock();
        try {
            return imports.contains(pkg);
        } finally {
            lock.readLock().unlock();
        }
    }

    /**
     * Imports a list of packages as solving roots.
     *
     * @param packages the packages
     * @return this solver
     */
    FqcnResolver importPackages(final Iterable<String> packages) {
        if (packages != null) {
            lock.writeLock().lock();
            try {
                if (parent == null) {
                    importCheck(packages);
                } else {
                    packages.forEach(pkg ->{ if (!parent.isImporting(pkg)) { importCheck(pkg); }});
                }
            } finally {
                lock.writeLock().unlock();
            }
        }
        return this;
    }

    /**
     * Gets a fully qualified class name from a simple class name and imports.
     *
     * @param name the simple name
     * @return the fqcn
     */
    String getQualifiedName(final String name) {
        String fqcn;
        if (parent != null && (fqcn = parent.getQualifiedName(name)) != null) {
            return  fqcn;
        }
        lock.readLock().lock();
        try {
            fqcn = fqcns.get(name);
        } finally {
            lock.readLock().unlock();
        }
        if (fqcn == null) {
            final ClassLoader loader = uberspect.getClassLoader();
            for (final String pkg : imports) {
                Class<?> clazz;
                try {
                    clazz = loader.loadClass(pkg + "." + name);
                } catch (final ClassNotFoundException e) {
                    // not in this package
                    continue;
                }
                // solved it, insert in map and return
                if (clazz != null) {
                    fqcn = clazz.getName();
                    lock.writeLock().lock();
                    try {
                        fqcns.put(name, fqcn);
                    } finally {
                        lock.writeLock().unlock();
                    }
                    break;
                }
            }
        }
        return fqcn;
    }
}