All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cedarsoftware.ncube.util.CdnClassLoader.groovy Maven / Gradle / Ivy

There is a newer version: 5.6.9
Show newest version
package com.cedarsoftware.ncube.util

import com.cedarsoftware.ncube.NCubeRuntime
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.codehaus.groovy.GroovyBugError
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.ClassNodeResolver
import org.codehaus.groovy.control.CompilationFailedException
import org.codehaus.groovy.control.CompilationUnit
import org.codehaus.groovy.control.SourceUnit

import java.util.concurrent.ConcurrentHashMap

import static com.cedarsoftware.ncube.NCubeAppContext.ncubeRuntime

/**
 *  @author Ken Partlow ([email protected])
 *          Greg Morefield ([email protected])
 *          John DeRegnaucourt ([email protected])
 *         
* Copyright (c) Cedar Software LLC *

* 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. */ @Slf4j @CompileStatic class CdnClassLoader extends GroovyClassLoader { private final boolean _preventRemoteBeanInfo private final boolean _preventRemoteCustomizer private final Map resourceCache = new ConcurrentHashMap<>() private final Map> resourcesCache = new ConcurrentHashMap<>() private final URL nullUrl = new URL('http://null.com:8080') private final List whiteList private ClassNodeResolver classNodeResolver = new PreferClassNodeResolver() private static String generatedClassesDir = '' /** * Create a GroovyClassLoader using the given ClassLoader as parent */ CdnClassLoader(ClassLoader loader, boolean preventRemoteBeanInfo = true, boolean preventRemoteCustomizer = true, List acceptedDomains = null) { super(configureParentClassLoader(loader), null) _preventRemoteBeanInfo = preventRemoteBeanInfo _preventRemoteCustomizer = preventRemoteCustomizer if (acceptedDomains == null) { String domains = (ncubeRuntime as NCubeRuntime).acceptedDomains if (domains) { whiteList = Arrays.asList(domains.split(';')) } else { whiteList = null } } else { whiteList = acceptedDomains } } /** * Injects URLClassLoader as parent to pickup generated classes directory, if configured */ private static ClassLoader configureParentClassLoader(ClassLoader parent) { String classesDir = generatedClassesDirectory if (classesDir) { File classesFile = new File(classesDir) return new URLClassLoader([classesFile.toURI().toURL()] as URL [], parent) } return parent } /** * Create a class loader that will have the additional URLs added to the classpath. * @param urlList List of String URLs to be added to the classpath. * @param acceptedDomains List of String prefixes of white-list domains which are * allowed to be searched for dynamic code or resources. */ CdnClassLoader(List urlList, List acceptedDomains = null) { this(CdnClassLoader.class.classLoader, true, true, acceptedDomains) addURLs(urlList) } /** * Caches the class, if name is supplied and caching is configured, * then delegates to super class to defineClass from raw bytes * * @param name String name of class to define, or null, if unknown * @param byteCode byte [] of raw Class bytes * @return generated Class definition */ Class defineClass(String name, byte[] byteCode) { if (name) { Class existing = this.findLoadedClass(name) if (existing) { log.warn("Attempting to define duplicate class with name: ${name}") return existing } } Class definedClass = super.defineClass(name, byteCode) if (name && generatedClassesDir) { dumpGeneratedClass(name,byteCode) } return definedClass } /** * Writes the generated Groovy class to the directory identified by the NCUBE_PARAM:genClsDir * @param name String of fully qualified name of the class * @param byteCode byte [] of Class to write */ private void dumpGeneratedClass(String name, byte [] byteCode) { File classFile = null try { classFile = new File("${generatedClassesDir}/${name.replace('.',File.separator)}.class") if (ensureDirectoryExists(classFile.parentFile)) { classFile.bytes = byteCode } } catch (Exception e) { log.warn("Failed to write class file with path=${classFile?.path}",e) } } /** * Finds and loads the class with the specified name from the URL search * path. Any URLs referring to JAR files are loaded and opened as needed * until the class is found. * * @param name the name of the class * @return the resulting class * @exception ClassNotFoundException if the class could not be found, * or if the loader is closed. * @exception NullPointerException if {@code name} is {@code null}. */ protected Class findClass(final String name) throws ClassNotFoundException { // println "findClass(${name})" if (_preventRemoteBeanInfo && name.endsWith('BeanInfo')) { throw new ClassNotFoundException(name) } if (_preventRemoteCustomizer && name.endsWith('Customizer')) { throw new ClassNotFoundException(name) } if (whiteList) { for (item in whiteList) { if (name.startsWith(item)) { Class clazz = super.findClass(name) return clazz } } } throw new ClassNotFoundException(name) } private void addURLs(List list) { for (url in list) { addURL(url) } } /** * Add the passed in String URL to the classpath. * @param url String url to add to the classpath. */ void addURL(String url) { if (url) { if (!url.endsWith("/")) { url += '/' } addURL(new URL(url)) } } /** * Prevent dynamic code from certain packages, like java, javax, groovy, com/cedarsoftware, com/google. * @param name Name of resource * @return true if we should only look locally. */ protected boolean isLocalOnlyResource(String name) { // println "isLocalOnlyResource(${name})" if (!whiteList) { // If there is no whiteList, then we can skip the HTTP HEAD check for ASTTransformation if (name.endsWith('.class')) { return true } if (name == 'META-INF/services/org.codehaus.groovy.transform.ASTTransformation') { return true } } // NOTE: This list needs to match (weed out) imports automatically brought in by Groovy as well as // those GroovyExpression adds to the source file. Must be in 'path' form (using slashes) if (name.contains('ncube/grv/') || name.startsWith('java/') || name.startsWith('javax/') || name.startsWith('groovy/') || name.startsWith('com/google/common/') || name.startsWith('com/cedarsoftware/')) { if (name.startsWith('ncube/grv/closure/')) { return false } return true } if (_preventRemoteBeanInfo && name.endsWith('BeanInfo.groovy')) { return true } if (_preventRemoteCustomizer && name.endsWith('Customizer.groovy')) { return true } return false } /** * Finds all the resources with the given name. A resource is some data * (images, audio, text, code, etc) that can be accessed by class code in a * way that is independent of the location of the code. * *

The name of a resource is a /-separated path name that * identifies the resource. * *

The search order is described in the documentation for * #getResource(String).

* * @apiNote When overriding this method it is recommended that an * implementation ensures that any delegation is consistent with the * getResource(String) method. This should ensure that the first element * returned by the Enumeration's {@code nextElement} method is the same * resource that the {@code getResource(String)} method would return. * * @param name * The resource name * * @return An enumeration of {@link java.net.URL URL} objects for * the resource. If no resources could be found, the enumeration * will be empty. Resources that the class loader doesn't have * access to will not be in the enumeration. * * @throws IOException * If I/O errors occur * * @see #findResources(String) */ Enumeration getResources(String name) throws IOException { // println "getResources(${name})" if (!resourcesCache.containsKey(name)) { if (isLocalOnlyResource(name)) { resourcesCache[name] = [] } else { resourcesCache[name] = super.getResources(name).toList() } } return Collections.enumeration(resourcesCache[name]) } /** * Finds the resource with the given name. A resource is some data * (images, audio, text, etc) that can be accessed by class code in a way * that is independent of the location of the code. * *

The name of a resource is a '/'-separated path name that * identifies the resource. * * This implementation caches local resource paths to URLs so that multiple * requests for the same relative resource will be answered without any * network traffic. * * @param name * The resource name * * @return A URL object for reading the resource, or * null if the resource could not be found or the invoker * doesn't have adequate privileges to get the resource. */ URL getResource(String name) { // println "getResource(${name})" if (resourceCache.containsKey(name)) { URL url = resourceCache[name] return nullUrl.is(url) ? null : url } if (isLocalOnlyResource(name)) { resourceCache.put(name, nullUrl) return null } URL res = super.getResource(name) resourceCache[name] = res ?: nullUrl return res } /** * Returns an Enumeration of URLs representing all of the resources * on the URL search path having the specified name. * * @param name the resource name * @exception IOException if an I/O exception occurs * @return an {@code Enumeration} of {@code URL}s * If the loader is closed, the Enumeration will be empty. */ Enumeration findResources(String name) throws IOException { // println "findResources(${name})" if (!resourcesCache.containsKey(name)) { if (isLocalOnlyResource(name)) { resourcesCache[name] = [] } else { resourcesCache[name] = super.findResources(name).toList() } } return Collections.enumeration(resourcesCache[name]) } /** * Finds the resource with the specified name on the URL search path. * * @param name the name of the resource * @return a {@code URL} for the resource, or {@code null} * if the resource could not be found, or if the loader is closed. */ URL findResource(String name) { // println "findResource(${name})" if (resourceCache.containsKey(name)) { URL url = resourceCache[name] return nullUrl.is(url) ? null : url } if (isLocalOnlyResource(name)) { resourceCache.put(name, nullUrl) return null } URL res = super.findResource(name) resourceCache[name] = res ?: nullUrl return res } /** * Clear any internal caches. The resource caches which map relative paths to * fully qualified URLs are cleared, as well as the parent class loader is told * to clear its internal class cache. */ void clearCache() { resourceCache.clear() resourcesCache.clear() super.clearCache() } /** * Specifies directory to use for caching Class files generated during Groovy compiles. * If the directory doesn't exist and can't be created, the Class caching will be disabled * @param classesDir String containing relative or absolute path to use for Class caching. A null or empty string will disable caching */ static void setGeneratedClassesDirectory(String classesDir) { try { if (classesDir) { generatedClassesDir = ensureDirectoryExists(new File(classesDir)) ? classesDir : '' } else { generatedClassesDir = '' } if (generatedClassesDir) { log.info("Generated classes configured to use path=${generatedClassesDir}") } } catch (Exception e) { log.warn("Unable to set classes directory to ${classesDir}", e) generatedClassesDir = '' } } /** * Returns directory to use for Class caching. * @return String path to directory to use for Class caching, if enabled; otherwise, empty string */ static String getGeneratedClassesDirectory() { return generatedClassesDir } /** * Tries to validate the directory specified. * * @param dir File containing directory path to validate/create * @return true if directory exists or can be created; otherwise, false * @throws SecurityException from mkdirs invocation */ private static boolean ensureDirectoryExists(File dir) { if (!dir.exists()) { dir.mkdirs() } boolean valid = dir.directory if (!valid) { log.warn("Failed to locate or create generated classes directory with path=${dir.path}") } return valid } ClassNodeResolver getClassNodeResolver() { return classNodeResolver } static class PreferClassNodeResolver extends ClassNodeResolver { // Map to store cached classes private Map cachedClasses = new ConcurrentHashMap<>() /** * Method adapted from ClassNodeResolver.tryAsLoaderClassOrScript, only looking up source if class doesn't exist * @param name * @param compilationUnit * @return */ @Override LookupResult findClassNode(String name, CompilationUnit compilationUnit) { GroovyClassLoader loader = compilationUnit.classLoader Class cls try { // NOTE: it's important to do no lookup against script files // here since the GroovyClassLoader would create a new CompilationUnit cls = loader.loadClass(name, false, true) } catch (ClassNotFoundException ignore) { return tryAsScript(name, compilationUnit) } catch (CompilationFailedException cfe) { throw new GroovyBugError("The lookup for "+name+" caused a failed compilaton. There should not have been any compilation from this call.", cfe) } ClassNode cn = ClassHelper.make(cls) return new LookupResult(null,cn) } /** * Method adapted from ClassNodeResolver.tryAsScript, only doing script lookup */ private static LookupResult tryAsScript(String name, CompilationUnit compilationUnit) { LookupResult lr = null if (name.startsWith("java.")) { return lr } //TODO: don't ignore inner static classes completely if (name.indexOf('$') != -1) { return lr } // try to find a script from classpath*/ GroovyClassLoader gcl = compilationUnit.classLoader URL url = null try { url = gcl.resourceLoader.loadGroovySource(name) } catch (MalformedURLException e) { // fall through and let the URL be null } if (url != null) { SourceUnit su = compilationUnit.addSource(url) return new LookupResult(su,null) } return lr } /** * caches a ClassNode (taken from ClassNodeResolver.cacheClass) * @param name - the name of the class * @param res - the ClassNode for that name */ void cacheClass(String name, ClassNode res) { cachedClasses.put(name, res) } /** * returns whatever is stored in the class cache for the given name (taken from ClassNodeResolver.getFromClassCache) * @param name - the name of the class * @return the result of the lookup, which may be null */ ClassNode getFromClassCache(String name) { // We use here the class cache cachedClasses to prevent // calls to ClassLoader#loadClass. Disabling this cache will // cause a major performance hit. ClassNode cached = cachedClasses.get(name) return cached } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy