001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019
020package icy.plugin.classloader;
021
022import icy.plugin.classloader.exception.JclException;
023import icy.plugin.classloader.exception.ResourceNotFoundException;
024import icy.system.IcyExceptionHandler;
025
026import java.io.ByteArrayInputStream;
027import java.io.IOException;
028import java.io.InputStream;
029import java.net.URL;
030import java.util.Collections;
031import java.util.Enumeration;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035import java.util.Set;
036import java.util.logging.Level;
037import java.util.logging.Logger;
038
039/**
040 * Reads the class bytes from jar files and other resources using
041 * ClasspathResources
042 * 
043 * @author Kamran Zafar
044 * @author Stephane Dallongeville
045 */
046@SuppressWarnings("rawtypes")
047public class JarClassLoader extends AbstractClassLoader
048{
049    /**
050     * Class cache
051     */
052    protected final Map<String, Class> loadedClasses;
053
054    protected final ClasspathResources classpathResources;
055    private char classNameReplacementChar;
056    private final ProxyClassLoader localLoader = new LocalLoader();
057
058    private static Logger logger = Logger.getLogger(JarClassLoader.class.getName());
059
060    public JarClassLoader(ClassLoader parent)
061    {
062        super(parent);
063
064        classpathResources = new ClasspathResources();
065        loadedClasses = Collections.synchronizedMap(new HashMap<String, Class>());
066
067        addLoader(localLoader);
068    }
069
070    public JarClassLoader()
071    {
072        this(getSystemClassLoader());
073    }
074
075    /**
076     * Loads classes from different sources
077     * 
078     * @param sources
079     */
080    public JarClassLoader(Object[] sources)
081    {
082        this();
083
084        addAll(sources);
085    }
086
087    /**
088     * Loads classes from different sources
089     * 
090     * @param sources
091     */
092    public JarClassLoader(List sources)
093    {
094        this();
095
096        addAll(sources);
097    }
098
099    /**
100     * Add all jar/class sources
101     * 
102     * @param sources
103     */
104    public void addAll(Object[] sources)
105    {
106        for (Object source : sources)
107            add(source);
108    }
109
110    /**
111     * Add all jar/class sources
112     * 
113     * @param sources
114     */
115    public void addAll(List sources)
116    {
117        for (Object source : sources)
118            add(source);
119    }
120
121    /**
122     * Loads local/remote source
123     * 
124     * @param source
125     */
126    public void add(Object source)
127    {
128        if (source instanceof InputStream)
129            throw new JclException("Unsupported resource type");
130        else if (source instanceof URL)
131            add((URL) source);
132        else if (source instanceof String)
133            add((String) source);
134        else
135            throw new JclException("Unknown Resource type");
136
137    }
138
139    /**
140     * Loads local/remote resource
141     * 
142     * @param resourceName
143     */
144    public void add(String resourceName)
145    {
146        classpathResources.loadResource(resourceName);
147    }
148
149    /**
150     * Loads classes from InputStream.
151     * 
152     * @deprecated Not anymore supported (we need URL for getResource(..) method)
153     */
154    @Deprecated
155    public void add(InputStream jarStream)
156    {
157        // classpathResources.loadJar(jarStream);
158    }
159
160    /**
161     * Loads local/remote resource
162     * 
163     * @param url
164     */
165    public void add(URL url)
166    {
167        classpathResources.loadResource(url);
168    }
169
170    /**
171     * Release all loaded resources and classes.
172     * The ClassLoader cannot be used anymore to load any new resource.
173     */
174    public void unloadAll()
175    {
176        // unload resources
177        classpathResources.entryContents.clear();
178        // unload classes
179        loadedClasses.clear();
180    }
181
182    /**
183     * Reads the class bytes from different local and remote resources using
184     * ClasspathResources
185     * 
186     * @param className
187     * @return byte[]
188     * @throws IOException
189     */
190    protected byte[] getClassBytes(String className) throws IOException
191    {
192        return classpathResources.getResourceContent(formatClassName(className));
193    }
194
195    /**
196     * Attempts to unload class, it only unloads the locally loaded classes by
197     * JCL
198     * 
199     * @param className
200     */
201    public void unloadClass(String className)
202    {
203        if (logger.isLoggable(Level.FINEST))
204            logger.finest("Unloading class " + className);
205
206        if (loadedClasses.containsKey(className))
207        {
208            if (logger.isLoggable(Level.FINEST))
209                logger.finest("Removing loaded class " + className);
210            loadedClasses.remove(className);
211            try
212            {
213                classpathResources.unload(formatClassName(className));
214            }
215            catch (ResourceNotFoundException e)
216            {
217                throw new JclException("Something is very wrong!!!"
218                        + "The locally loaded classes must be in synch with ClasspathResources", e);
219            }
220        }
221        else
222        {
223            try
224            {
225                classpathResources.unload(formatClassName(className));
226            }
227            catch (ResourceNotFoundException e)
228            {
229                throw new JclException("Class could not be unloaded "
230                        + "[Possible reason: Class belongs to the system]", e);
231            }
232        }
233    }
234
235    /**
236     * @param className
237     * @return String
238     */
239    protected String formatClassName(String className)
240    {
241        String cname = className.replace('/', '~');
242
243        if (classNameReplacementChar == '\u0000')
244            // '/' is used to map the package to the path
245            cname = cname.replace('.', '/') + ".class";
246        else
247            // Replace '.' with custom char, such as '_'
248            cname = cname.replace('.', classNameReplacementChar) + ".class";
249
250        return cname.replace('~', '/');
251    }
252
253    /**
254     * Local class loader
255     */
256    class LocalLoader extends ProxyClassLoader
257    {
258        private final Logger logger = Logger.getLogger(LocalLoader.class.getName());
259
260        public LocalLoader()
261        {
262            super(50);
263
264            enabled = Configuration.isLocalLoaderEnabled();
265        }
266
267        @Override
268        public ClassLoader getLoader()
269        {
270            return JarClassLoader.this;
271        }
272
273        @Override
274        public Class loadClass(String className, boolean resolveIt) throws ClassNotFoundException, ClassFormatError
275        {
276            Class result = null;
277            byte[] classBytes;
278
279            result = loadedClasses.get(className);
280            if (result != null)
281            {
282                if (logger.isLoggable(Level.FINEST))
283                    logger.finest("Returning local loaded class [" + className + "] from cache");
284                return result;
285            }
286
287            // try to find from already loaded class (by other method)
288            result = findLoadedClass(className);
289            // not loaded ?
290            if (result == null)
291            {
292                try
293                {
294                    classBytes = getClassBytes(className);
295                }
296                catch (IOException e)
297                {
298                    // we got a severe error here --> throw an exception
299                    throw new ClassNotFoundException(className, e);
300                }
301
302                if (classBytes == null)
303                    return null;
304
305                result = defineClass(className, classBytes, 0, classBytes.length);
306
307                if (result == null)
308                    return null;
309            }
310
311            /*
312             * Preserve package name.
313             */
314            if (result.getPackage() == null)
315            {
316                int lastDotIndex = className.lastIndexOf('.');
317                String packageName = (lastDotIndex >= 0) ? className.substring(0, lastDotIndex) : "";
318                definePackage(packageName, null, null, null, null, null, null, null);
319            }
320
321            if (resolveIt)
322                resolveClass(result);
323
324            loadedClasses.put(className, result);
325            if (logger.isLoggable(Level.FINEST))
326                logger.finest("Return new local loaded class " + className);
327
328            return result;
329        }
330
331        @Override
332        public InputStream getResourceAsStream(String name)
333        {
334            try
335            {
336                byte[] arr = classpathResources.getResourceContent(name);
337
338                if (arr != null)
339                {
340                    if (logger.isLoggable(Level.FINEST))
341                        logger.finest("Returning newly loaded resource " + name);
342
343                    return new ByteArrayInputStream(arr);
344                }
345            }
346            catch (IOException e)
347            {
348                IcyExceptionHandler.showErrorMessage(e, false, true);
349            }
350
351            return null;
352        }
353
354        @Override
355        public URL getResource(String name)
356        {
357            URL url = classpathResources.getResource(name);
358
359            if (url != null)
360            {
361                if (logger.isLoggable(Level.FINEST))
362                    logger.finest("Returning newly loaded resource " + name);
363
364                return url;
365            }
366
367            return null;
368        }
369
370        @Override
371        public Enumeration<URL> getResources(String name) throws IOException
372        {
373            final URL url = getResource(name);
374
375            return new Enumeration<URL>()
376            {
377                boolean hasMore = (url != null);
378
379                @Override
380                public boolean hasMoreElements()
381                {
382                    return hasMore;
383                }
384
385                @Override
386                public URL nextElement()
387                {
388                    if (hasMore)
389                    {
390                        hasMore = false;
391                        return url;
392                    }
393
394                    return null;
395                }
396            };
397        }
398    }
399
400    public char getClassNameReplacementChar()
401    {
402        return classNameReplacementChar;
403    }
404
405    public void setClassNameReplacementChar(char classNameReplacementChar)
406    {
407        this.classNameReplacementChar = classNameReplacementChar;
408    }
409
410    /**
411     * Returns an immutable Set of all resources name
412     */
413    public Set<String> getResourcesName()
414    {
415        return classpathResources.getResourcesName();
416    }
417
418    /**
419     * Returns an immutable Map of all resources
420     */
421    public Map<String, URL> getResources()
422    {
423        return classpathResources.getResources();
424    }
425
426    /**
427     * Returns all currently loaded classes and resources.
428     */
429    public Map<String, byte[]> getLoadedResources()
430    {
431        return classpathResources.getLoadedResources();
432    }
433
434    /**
435     * @return Local JCL ProxyClassLoader
436     */
437    public ProxyClassLoader getLocalLoader()
438    {
439        return localLoader;
440    }
441
442    /**
443     * Returns all JCL-loaded classes as an immutable Map
444     * 
445     * @return Map
446     */
447    public Map<String, Class> getLoadedClasses()
448    {
449        return Collections.unmodifiableMap(loadedClasses);
450    }
451}