001/*
002 * Copyright 2012-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.cli.compiler;
018
019import java.io.ByteArrayInputStream;
020import java.io.File;
021import java.io.InputStream;
022import java.net.MalformedURLException;
023import java.net.URL;
024import java.net.URLClassLoader;
025import java.security.AccessController;
026import java.security.PrivilegedAction;
027import java.util.ArrayList;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.Map;
031import java.util.Set;
032
033import groovy.lang.GroovyClassLoader;
034import org.codehaus.groovy.ast.ClassNode;
035import org.codehaus.groovy.control.CompilationUnit;
036import org.codehaus.groovy.control.CompilerConfiguration;
037import org.codehaus.groovy.control.SourceUnit;
038
039import org.springframework.util.Assert;
040import org.springframework.util.FileCopyUtils;
041import org.springframework.util.StringUtils;
042
043/**
044 * Extension of the {@link GroovyClassLoader} with support for obtaining '.class' files as
045 * resources.
046 *
047 * @author Phillip Webb
048 * @author Dave Syer
049 */
050public class ExtendedGroovyClassLoader extends GroovyClassLoader {
051
052        private static final String SHARED_PACKAGE = "org.springframework.boot.groovy";
053
054        private static final URL[] NO_URLS = new URL[] {};
055
056        private final Map<String, byte[]> classResources = new HashMap<String, byte[]>();
057
058        private final GroovyCompilerScope scope;
059
060        private final CompilerConfiguration configuration;
061
062        public ExtendedGroovyClassLoader(GroovyCompilerScope scope) {
063                this(scope, createParentClassLoader(scope), new CompilerConfiguration());
064        }
065
066        private static ClassLoader createParentClassLoader(GroovyCompilerScope scope) {
067                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
068                if (scope == GroovyCompilerScope.DEFAULT) {
069                        classLoader = new DefaultScopeParentClassLoader(classLoader);
070                }
071                return classLoader;
072        }
073
074        private ExtendedGroovyClassLoader(GroovyCompilerScope scope, ClassLoader parent,
075                        CompilerConfiguration configuration) {
076                super(parent, configuration);
077                this.configuration = configuration;
078                this.scope = scope;
079        }
080
081        @Override
082        protected Class<?> findClass(String name) throws ClassNotFoundException {
083                try {
084                        return super.findClass(name);
085                }
086                catch (ClassNotFoundException ex) {
087                        if (this.scope == GroovyCompilerScope.DEFAULT
088                                        && name.startsWith(SHARED_PACKAGE)) {
089                                Class<?> sharedClass = findSharedClass(name);
090                                if (sharedClass != null) {
091                                        return sharedClass;
092                                }
093                        }
094                        throw ex;
095                }
096        }
097
098        private Class<?> findSharedClass(String name) {
099                try {
100                        String path = name.replace('.', '/').concat(".class");
101                        InputStream inputStream = getParent().getResourceAsStream(path);
102                        if (inputStream != null) {
103                                try {
104                                        return defineClass(name, FileCopyUtils.copyToByteArray(inputStream));
105                                }
106                                finally {
107                                        inputStream.close();
108                                }
109                        }
110                        return null;
111                }
112                catch (Exception ex) {
113                        return null;
114                }
115        }
116
117        @Override
118        public InputStream getResourceAsStream(String name) {
119                InputStream resourceStream = super.getResourceAsStream(name);
120                if (resourceStream == null) {
121                        byte[] bytes = this.classResources.get(name);
122                        resourceStream = bytes == null ? null : new ByteArrayInputStream(bytes);
123                }
124                return resourceStream;
125        }
126
127        @Override
128        public ClassCollector createCollector(CompilationUnit unit, SourceUnit su) {
129                InnerLoader loader = AccessController
130                                .doPrivileged(new PrivilegedAction<InnerLoader>() {
131                                        @Override
132                                        public InnerLoader run() {
133                                                return new InnerLoader(ExtendedGroovyClassLoader.this) {
134                                                        // Don't return URLs from the inner loader so that Tomcat only
135                                                        // searches the parent. Fixes 'TLD skipped' issues
136                                                        @Override
137                                                        public URL[] getURLs() {
138                                                                return NO_URLS;
139                                                        }
140                                                };
141                                        }
142                                });
143                return new ExtendedClassCollector(loader, unit, su);
144        }
145
146        public CompilerConfiguration getConfiguration() {
147                return this.configuration;
148        }
149
150        /**
151         * Inner collector class used to track as classes are added.
152         */
153        protected class ExtendedClassCollector extends ClassCollector {
154
155                protected ExtendedClassCollector(InnerLoader loader, CompilationUnit unit,
156                                SourceUnit su) {
157                        super(loader, unit, su);
158                }
159
160                @Override
161                protected Class<?> createClass(byte[] code, ClassNode classNode) {
162                        Class<?> createdClass = super.createClass(code, classNode);
163                        ExtendedGroovyClassLoader.this.classResources
164                                        .put(classNode.getName().replace('.', '/') + ".class", code);
165                        return createdClass;
166                }
167
168        }
169
170        /**
171         * ClassLoader used for a parent that filters so that only classes from groovy-all.jar
172         * are exposed.
173         */
174        private static class DefaultScopeParentClassLoader extends ClassLoader {
175
176                private static final String[] GROOVY_JARS_PREFIXES = { "groovy", "antlr", "asm" };
177
178                private final URLClassLoader groovyOnlyClassLoader;
179
180                DefaultScopeParentClassLoader(ClassLoader parent) {
181                        super(parent);
182                        this.groovyOnlyClassLoader = new URLClassLoader(getGroovyJars(parent), null);
183                }
184
185                private URL[] getGroovyJars(final ClassLoader parent) {
186                        Set<URL> urls = new HashSet<URL>();
187                        findGroovyJarsDirectly(parent, urls);
188                        if (urls.isEmpty()) {
189                                findGroovyJarsFromClassPath(parent, urls);
190                        }
191                        Assert.state(!urls.isEmpty(), "Unable to find groovy JAR");
192                        return new ArrayList<URL>(urls).toArray(new URL[urls.size()]);
193                }
194
195                private void findGroovyJarsDirectly(ClassLoader classLoader, Set<URL> urls) {
196                        while (classLoader != null) {
197                                if (classLoader instanceof URLClassLoader) {
198                                        for (URL url : ((URLClassLoader) classLoader).getURLs()) {
199                                                if (isGroovyJar(url.toString())) {
200                                                        urls.add(url);
201                                                }
202                                        }
203                                }
204                                classLoader = classLoader.getParent();
205                        }
206                }
207
208                private void findGroovyJarsFromClassPath(ClassLoader parent, Set<URL> urls) {
209                        String classpath = System.getProperty("java.class.path");
210                        String[] entries = classpath.split(System.getProperty("path.separator"));
211                        for (String entry : entries) {
212                                if (isGroovyJar(entry)) {
213                                        File file = new File(entry);
214                                        if (file.canRead()) {
215                                                try {
216                                                        urls.add(file.toURI().toURL());
217                                                }
218                                                catch (MalformedURLException ex) {
219                                                        // Swallow and continue
220                                                }
221                                        }
222                                }
223                        }
224                }
225
226                private boolean isGroovyJar(String entry) {
227                        entry = StringUtils.cleanPath(entry);
228                        for (String jarPrefix : GROOVY_JARS_PREFIXES) {
229                                if (entry.contains("/" + jarPrefix + "-")) {
230                                        return true;
231                                }
232                        }
233                        return false;
234                }
235
236                @Override
237                protected Class<?> loadClass(String name, boolean resolve)
238                                throws ClassNotFoundException {
239                        this.groovyOnlyClassLoader.loadClass(name);
240                        return super.loadClass(name, resolve);
241                }
242
243        }
244
245}