Statistics
| Revision:

svn-gvsig-desktop / trunk / org.gvsig.desktop / org.gvsig.desktop.framework / org.gvsig.andami / src / main / java / org / gvsig / andami / plugins / PluginClassLoader.java @ 47429

History | View | Annotate | Download (24.1 KB)

1
/**
2
 * gvSIG. Desktop Geographic Information System.
3
 *
4
 * Copyright (C) 2007-2013 gvSIG Association.
5
 *
6
 * This program is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License as published by the Free Software
8
 * Foundation; either version 3 of the License, or (at your option) any later
9
 * version.
10
 *
11
 * This program is distributed in the hope that it will be useful, but WITHOUT
12
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14
 * details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with
17
 * this program; if not, write to the Free Software Foundation, Inc., 51
18
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
 *
20
 * For any additional information, do not hesitate to contact us at info AT
21
 * gvsig.com, or visit our website www.gvsig.com.
22
 */
23
package org.gvsig.andami.plugins;
24

    
25
import java.io.DataInputStream;
26
import java.io.File;
27
import java.io.FileInputStream;
28
import java.io.IOException;
29
import java.io.InputStream;
30
import java.net.MalformedURLException;
31
import java.net.URL;
32
import java.net.URLClassLoader;
33
import java.security.AllPermission;
34
import java.security.CodeSource;
35
import java.security.PermissionCollection;
36
import java.util.ArrayList;
37
import java.util.Arrays;
38
import java.util.Collections;
39
import java.util.Enumeration;
40
import java.util.HashMap;
41
import java.util.HashSet;
42
import java.util.Hashtable;
43
import java.util.Iterator;
44
import java.util.List;
45
import java.util.Map;
46
import java.util.Map.Entry;
47
import java.util.Set;
48
import java.util.StringTokenizer;
49
import java.util.zip.ZipEntry;
50
import java.util.zip.ZipException;
51
import java.util.zip.ZipFile;
52
import org.apache.commons.lang3.StringUtils;
53

    
54
import org.gvsig.andami.messages.Messages;
55
import org.slf4j.Logger;
56
import org.slf4j.LoggerFactory;
57

    
58
/**
59
 * <p>
60
 * Class loader which loads the classes requested by the plugins. It first tries
61
 * to search in the classpath, then it requests the class to the parent
62
 * classloader, then it searches in the owns plugins' library dir, and if all
63
 * these methods fail, it tries to load the class from any of the depended
64
 * plugins. Finally, if this also fails, the other classloaders provided in the
65
 * <code>addLoaders</code> method are requested to load the class.</p>
66
 *
67
 * <p>
68
 * The class loader can also be used to load resources from the plugin's
69
 * directory by using the <code>getResource()</code> method.</p>
70
 *
71
 * @author Fernando Gonz�lez Cort�s
72
 */
73
public class PluginClassLoader extends URLClassLoader {
74

    
75
    /**
76
     * DOCUMENT ME!
77
     */
78
    private static Logger logger = LoggerFactory.getLogger(PluginClassLoader.class.getName());
79

    
80

    
81
    private Map<String, ZipFile> clasesJar = new HashMap<>();
82

    
83

    
84
    private File baseDir;
85
    private List<PluginClassLoader> pluginLoaders;
86
    private static List<ClassLoader> otherLoaders = new ArrayList<>();
87
    private boolean isOtherLoader = false;
88

    
89
    /**
90
     * Creates a new PluginClassLoader object.
91
     *
92
     * @param jars Array with the search paths where classes will be searched
93
     * @param baseDir Base directory for this plugin. This is the directory
94
     * which will be used as basedir in the <code>getResources</code> method.
95
     * @param cl The parent classloader of this classloader. It will be used to
96
     * search classes before trying to search in the plugin's directory
97
     * @param pluginLoaders The classloaders of the depended plugins.
98
     *
99
     * @throws IOException
100
     */
101
    public PluginClassLoader(URL[] jars, String baseDir, ClassLoader cl,
102
            PluginClassLoader[] pluginLoaders) throws IOException {
103
        this(jars, baseDir, cl, Arrays.asList(pluginLoaders));
104
    }
105

    
106
    public PluginClassLoader(URL[] jars, String baseDir, ClassLoader cl,
107
            List<PluginClassLoader> pluginLoaders) throws IOException {
108
        super(jars, cl);
109
        this.baseDir = new File(new File(baseDir).getAbsolutePath());
110
        try {
111
            logger.debug("Creating PluginClassLoader for {}.", this.getPluginName());
112
            this.pluginLoaders = new ArrayList();
113
            this.pluginLoaders.addAll(pluginLoaders);
114

    
115
            if (jars == null || jars.length < 1) {
116
                debugDump();
117
                return;
118
            }
119
            ZipFile[] jarFiles = new ZipFile[jars.length];
120

    
121
            for (int i = 0; i < jars.length; i++) {
122
                try {
123
                    logger.debug("Classloader {}, loading jar {}", this.getPluginName(), jars[i].getPath());
124
                    jarFiles[i] = new ZipFile(jars[i].getPath());
125

    
126
                    Enumeration<? extends ZipEntry> entradas = jarFiles[i].entries();
127

    
128
                    while (entradas.hasMoreElements()) {
129
                        ZipEntry file = (ZipEntry) entradas.nextElement();
130
                        String fileName = file.getName();
131

    
132
                        if (!fileName.toLowerCase().endsWith(".class")) { //$NON-NLS-1$
133

    
134
                            continue;
135
                        }
136

    
137
                        fileName = fileName.substring(0, fileName.length() - 6)
138
                                .replace('/', '.');
139

    
140
                        if (clasesJar.get(fileName) != null) {
141
                            logger.warn("Classloader {}, duplicated class {} in {} and {}",
142
                                    new Object[]{
143
                                        this.getPluginName(),
144
                                        fileName,
145
                                        jarFiles[i].getName(),
146
                                        clasesJar.get(fileName).getName()
147
                                    }
148
                            );
149
                        } else {
150
                            clasesJar.put(fileName, jarFiles[i]);
151
                        }
152
                    }
153
                } catch (ZipException e) {
154
                    throw new IOException(e.getMessage() + " Jar: "
155
                            + jars[i].getPath() + ": " + jarFiles[i]);
156
                } catch (IOException e) {
157
                    throw e;
158
                }
159
            }
160
        } finally {
161
            debugDump();
162
        }
163
    }
164

    
165
    private void debugDump() {
166
        if (!logger.isDebugEnabled()) {
167
            return;
168
        }
169
        logger.debug(this.toString());
170
        logger.debug("  baseDir: " + this.baseDir);
171
        logger.debug("  parent: " + this.getParent());
172
        logger.debug("  depends:");
173
        for (PluginClassLoader pluginLoader : this.pluginLoaders) {
174
            logger.debug("    {}", pluginLoader.toString());
175
        }
176
        logger.debug("  urls:");
177
        URL[] urls = this.getURLs();
178
        for (URL url : urls) {
179
            logger.debug("    " + url.toString());
180
        }
181
//        logger.debug("  classes:");
182
//        Iterator<Map.Entry<String, ZipFile>> it = this.clasesJar.entrySet().iterator();
183
//        while (it.hasNext()) {
184
//            Entry<String, ZipFile> entry = it.next();
185
//            logger.debug("    " + entry.getKey() + "(" + entry.getValue().getName() + ")");
186
//        }
187
    }
188

    
189
    protected Class singleLoadClass(String name) throws ClassNotFoundException {
190
        Class<?> c;
191
        Set<String> pluginsVisiteds = new HashSet<>();
192
        c = this.singleLoadClass(pluginsVisiteds, name);
193
        return c;
194
    }
195

    
196
    /**
197
     * Carga la clase
198
     *
199
     * @param name Nombre de la clase
200
     * @param resolve Si se ha de resolver la clase o no
201
     *
202
     * @return Clase cargada
203
     *
204
     * @throws ClassNotFoundException Si no se pudo encontrar la clase
205
     */
206
    @Override
207
    protected Class loadClass(String name, boolean resolve)
208
            throws ClassNotFoundException {
209
        Class<?> c = null;
210

    
211
        if(StringUtils.equals(name, "java.nio.file.SimpleFileVisitor")){
212
            logger.info("Loading SimpleFileVisitor: isOtherLoader = "+isOtherLoader+" PluginName = "+this.getPluginName());
213
        }
214
        // Intentamos cargar con el URLClassLoader
215
        for( int retry=0; c==null && retry<3; retry++) {
216
            try {
217
                if (!isOtherLoader) {
218
                    c = super.loadClass(name, resolve);
219
                    logger.debug("Classloader {}, found class {}.", this.getPluginName(), name);
220
                }
221
            } catch (ClassNotFoundException e1) {
222
                if(StringUtils.equals(name, "java.nio.file.SimpleFileVisitor")){
223
                    logger.info("ClassNotFoundException SimpleFileVisitor in classLoader = "+super.getClass().getName(),e1);
224
                }
225
            }
226
            if (c == null) {
227
                try {
228
                    c = singleLoadClass(name);
229
                } catch (ClassNotFoundException e2) {
230
                }
231
            }
232
            if (c == null) {
233
                try {
234
                    isOtherLoader = true;
235
                    c = loadOtherClass(name);
236
                } catch (ClassNotFoundException e3) {
237
                    throw new ClassNotFoundException("Class " + name
238
                            + " not found through the plugin " + baseDir, e3);
239
                } finally {
240
                    isOtherLoader = false;
241
                }
242
            }
243
            if( c==null ) {
244
                logger.info("class "+name+", retry "+retry);
245
            }
246
        }
247
        if (c == null) {
248
            if( StringUtils.startsWith(name,"java.") || StringUtils.startsWith(name,"javax.") ) {
249
                logger.warn("Can't locate class '"+name+"'. It can be a serious problem. It is advisable that you close the application.");
250
            }
251
            throw new ClassNotFoundException(Messages.getString(
252
                    "PluginClassLoader.Error_reading_file") + name);
253
        }
254
        if (resolve) {
255
            resolveClass(c);
256
        }
257
        return c;
258
    }
259

    
260
    private Class<?> loadOtherClass(String name)
261
            throws ClassNotFoundException {
262
        ClassLoader[] ocls = (ClassLoader[]) otherLoaders.toArray(new ClassLoader[0]);
263
        Class<?> c;
264
        for (ClassLoader ocl : ocls) {
265
            c = ocl.loadClass(name);
266
            if (c != null) {
267
                logger.debug("Classloader {}, found class {} in classloader {}", 
268
                        new Object[]{this, getPluginName(), name, ocl.toString()}
269
                );
270
                return c;
271
            }
272
        }
273
        throw new ClassNotFoundException(name);
274
    }
275

    
276
    /**
277
     * obtiene el array de bytes de la clase
278
     *
279
     * @param classFile Entrada dentro del jar contiene los bytecodes de la
280
     * clase (el .class)
281
     * @param is InputStream para leer la entrada del jar
282
     *
283
     * @return Bytes de la clase
284
     *
285
     * @throws IOException Si no se puede obtener el .class del jar
286
     */
287
    private byte[] loadClassData(ZipEntry classFile, InputStream is)
288
            throws IOException {
289
        // Get size of class file
290
        int size = (int) classFile.getSize();
291

    
292
        // Reserve space to read
293
        byte[] buff = new byte[size];
294

    
295
        // Get stream to read from
296
        DataInputStream dis = new DataInputStream(is);
297

    
298
        // Read in data
299
        dis.readFully(buff);
300

    
301
        // close stream
302
        dis.close();
303

    
304
        // return data
305
        return buff;
306
    }
307

    
308
    /**
309
     * Gets the bytes of a File
310
     *
311
     * @param file File
312
     *
313
     * @return bytes of file
314
     *
315
     * @throws IOException If the operation fails
316
     */
317
    private byte[] loadClassData(File file) throws IOException {
318
        InputStream is = new FileInputStream(file);
319

    
320
        // Get the size of the file
321
        long length = file.length();
322

    
323
        // You cannot create an array using a long type.
324
        // It needs to be an int type.
325
        // Before converting to an int type, check
326
        // to ensure that file is not larger than Integer.MAX_VALUE.
327
        if (length > Integer.MAX_VALUE) {
328
            // File is too large
329
        }
330

    
331
        // Create the byte array to hold the data
332
        byte[] bytes = new byte[(int) length];
333

    
334
        // Read in the bytes
335
        int offset = 0;
336
        int numRead = 0;
337

    
338
        while ((offset < bytes.length)
339
                && ((numRead = is.read(bytes, offset, bytes.length - offset)) >= 0)) {
340
            offset += numRead;
341
        }
342

    
343
        // Close the input stream and return bytes
344
        is.close();
345

    
346
        // Ensure all the bytes have been read in
347
        if (offset < bytes.length) {
348
            throw new IOException("Could not completely read file "
349
                    + file.getName());
350
        }
351

    
352
        return bytes;
353
    }
354

    
355
    /**
356
     * Gets the requested resource. If the path is relative, its base directory
357
     * will be the one provided in the PluginClassLoader's constructor. If the
358
     * resource is not found, search in dependents plugins, otherwise the parent
359
     * classloader will be invoked to try to get it. If it is not found, it will
360
     * return null.
361
     *
362
     * @param res An absolute or relative path to the requested resource.
363
     *
364
     * @return Resource's URL if it was found, nul otherwise.
365
     */
366
    @Override
367
    public URL getResource(String res) {
368
        URL ret ;
369

    
370
        Set<String> pluginsVisiteds = new HashSet<>();
371
        ret = this.getResource(pluginsVisiteds, res);
372
        return ret;
373
    }
374

    
375
    /**
376
     * Gets the requested resource. If the path is relative, its base directory
377
     * will be the one provided in the PluginClassLoader's constructor. If the
378
     * resource is not found, the parent classloader will be invoked to try to
379
     * get it. If it is not found, it will return null.
380
     *
381
     * @param res An absolute or relative path to the requested resource.
382
     *
383
     * @return Resource's URL if it was found, nul otherwise.
384
     */
385
    private URL getResource(File base, List<String> res) {
386
        File[] files = base.listFiles();
387

    
388
        String parte = res.get(0);
389

    
390
        for (File file : files) {
391
            if (file.getName().compareTo(parte) == 0) {
392
                if (res.size() == 1) {
393
                    try {
394
                        return new URL("file:" + file.toString());
395
                    }catch (MalformedURLException e) {
396
                        return null;
397
                    }
398
                } else {
399
                    return getResource(file, res.subList(1, res.size()));
400
                }
401
            }
402
        }
403

    
404
        return null;
405
    }
406
    
407
    @Override
408
    public Enumeration<URL> getResources(String name) throws IOException {
409
            HashSet visitedPlugins = new HashSet();
410
            return getResources(name, visitedPlugins);
411
    }
412
    
413
    protected Enumeration<URL> getResources(String name, HashSet<PluginClassLoader> visitedPlugins) throws IOException {        
414
            List<URL> resources = new ArrayList<>();
415
            Enumeration<URL> aux = super.getResources(name);
416
            while(aux.hasMoreElements()){
417
                    URL url = aux.nextElement();
418
                    resources.add(url);
419
            }
420
        visitedPlugins.add(this);
421
            for(PluginClassLoader loader: this.pluginLoaders){
422
                    if (!visitedPlugins.contains(loader)) {
423
                            aux = loader.getResources(name, visitedPlugins);
424
                        while(aux.hasMoreElements()){
425
                                URL url = aux.nextElement();
426
                                resources.add(url);
427
                        }
428
                    }
429
            }
430
            return Collections.enumeration(resources);
431
    }
432

    
433
    /**
434
     * Returns the name of the plugin (the name of the directory containing the
435
     * plugin).
436
     *
437
     * @return An String containing the plugin's name.
438
     */
439
    public String getPluginName() {
440
        return baseDir.getName();
441
    }
442

    
443
    /*
444
     * @see java.security.SecureClassLoader#getPermissions(java.security.CodeSource)
445
     */
446
    @Override
447
    protected PermissionCollection getPermissions(CodeSource codesource) {
448
        PermissionCollection perms = super.getPermissions(codesource);
449
        perms.add(new AllPermission());
450

    
451
        return perms;
452
    }
453

    
454
    /**
455
     * Gets the plugin's base Iterator<Map.Entry<Integer, Integer>>dir, the
456
     * directory which will be used to search resources.
457
     *
458
     * @return Returns the baseDir.
459
     */
460
    public String getBaseDir() {
461
        return baseDir.getAbsolutePath();
462
    }
463

    
464
    /**
465
     * Adds other classloader to use when all the normal methods fail.
466
     *
467
     * @param classLoaders An ArrayList of ClassLoaders which will be used to
468
     * load classes when all the normal methods fail.
469
     */
470
    public static void addLoaders(ArrayList classLoaders) {
471
        otherLoaders.addAll(classLoaders);
472
    }
473

    
474
    @Override
475
    public String toString() {
476
        return super.toString() + " (" + getPluginName() + ")";
477
    }
478

    
479
    public void addPluginClassLoader(PluginClassLoader pluginClassLoader) {
480
        if (!this.pluginLoaders.contains(pluginClassLoader)) {
481
            this.pluginLoaders.add(pluginClassLoader);
482
        }
483
    }
484

    
485
    private Class singleLoadClass(Set<String> pluginsVisiteds, String name) throws ClassNotFoundException {
486

    
487
        if (pluginsVisiteds.contains(this.getPluginName())) {
488
            return null;
489
        }
490
        pluginsVisiteds.add(this.getPluginName());
491

    
492
        // Buscamos en las clases de las librer�as del plugin
493
        Class<?> c = findLoadedClass(name);
494

    
495
        if (c != null) {
496
            return c;
497
        }
498

    
499
        logger.debug("Classloader {}, searching class '{}'",
500
                new Object[]{this.getPluginName(), name}
501
        );
502
        try {
503
            ZipFile jar = (ZipFile) clasesJar.get(name);
504

    
505
            //No esta en ningun jar
506
            if (jar == null) {
507
                //Buscamos en el directorio de clases
508
                String classFileName = baseDir + "/classes/"
509
                        + name.replace('.', '/') + ".class";
510
                File f = new File(classFileName);
511
                if (f.exists()) {
512
                    byte[] data = loadClassData(f);
513
                    c = defineClass(name, data, 0, data.length);
514
                    logger.debug("Classloader {}/classes-folder, found class {}",
515
                            new Object[]{this.getPluginName(), name}
516
                    );
517

    
518
                } else {
519
                    for (PluginClassLoader pluginLoader1 : pluginLoaders) {
520
                        c = null;
521
                        PluginClassLoader pluginLoader = pluginLoader1;
522
                        if (pluginLoader != null) {
523
                            try {
524
                                c = pluginLoader.singleLoadClass(pluginsVisiteds, name);
525
                            } catch (ClassNotFoundException e) {
526
                                // Si no la encontramos en el primer plugin, capturamos la exceptcion
527
                                // porque es probable que la encontremos en el resto de plugins.
528
                            }
529
                            if (c != null) {
530
                                logger.debug("Classloader {}, found class {} in plugin {}",
531
                                        new Object[]{
532
                                            this.getPluginName(),
533
                                            name,
534
                                            pluginLoader.getPluginName()
535
                                        }
536
                                );
537
                                return c;
538
                            }
539
                        }
540
                    }
541
                    
542
                    
543
                }
544
            } else {
545
                logger.debug("Classloader {}, found class {} in jar {}",
546
                        new Object[]{
547
                            this.getPluginName(),
548
                            name,
549
                            jar.getName()
550
                        }
551
                );
552
                String fileName = name.replace('.', '/') + ".class";
553
                ZipEntry classFile = jar.getEntry(fileName);
554
                byte[] data = loadClassData(classFile,
555
                        jar.getInputStream(classFile));
556

    
557
                c = defineClass(name, data, 0, data.length);
558
                if (c == null) {
559
                    logger.debug("Classloader {}, can't load class {} from jar {}",
560
                            new Object[]{
561
                                this.getPluginName(),
562
                                name,
563
                                jar.getName()
564
                            }
565
                    );
566
                }
567

    
568
            }
569

    
570
            if (c == null) {
571
                logger.debug("Classloader {}, class not found {}",
572
                        new Object[]{
573
                            this.getPluginName(),
574
                            name
575
                        }
576
                );
577
                debugDump();
578
                throw new ClassNotFoundException(name);
579
            }
580

    
581
            return c;
582
        } catch (IOException e) {
583
            logger.debug("Classloader {}, error loading class {}",
584
                    new Object[]{
585
                        this.getPluginName(),
586
                        name
587
                    },
588
                    e
589
            );
590
            throw new ClassNotFoundException(Messages.getString(
591
                    "PluginClassLoader.Error_reading_file") + name);
592
        }
593
    }
594

    
595
    /**
596
     * Este metodo busca en este class loader y en el de los plugins de los que
597
     * depende. Se cerciora de que no se queda bucleado cuando hay una relacion
598
     * recursiva entre los plugins.
599
     *
600
     * @param pluginsVisiteds, set con los plugins que va visitando para evitar
601
     * que se quede bucleado cuando hay referencia ciclicas entre plugins.
602
     * @param res
603
     *
604
     * @return
605
     */
606
    private URL getResource(Set<String> pluginsVisiteds, String res) {
607
        URL ret = null;
608

    
609
        if (pluginsVisiteds.contains(this.getPluginName())) {
610
            return null;
611
        }
612
        pluginsVisiteds.add(this.getPluginName());
613

    
614
        //
615
        // Primero buscamos en el directorio del plugin.
616
        try {
617
//            if( res.contains("cosa") ) {
618
//                logger.info("Classloader {}, searching resource '{}'", this.getPluginName(), res);
619
//            }
620
            logger.debug("Classloader {}, searching resource '{}'", this.getPluginName(), res);
621
            List<String> resource = new ArrayList<>();
622
            StringTokenizer st = new StringTokenizer(res, "\\/");
623
            while (st.hasMoreTokens()) {
624
                String token = st.nextToken();
625
                resource.add(token);
626
            }
627
            ret = getResource(baseDir, resource);
628
            if (ret != null) {
629
                return ret;
630
            }
631
        } catch (Exception e) {
632
            logger.warn("Classloader {}, Error getting resource '{}'.",
633
                    new Object[]{
634
                        this.getPluginName(), res
635
                    },
636
                    e
637
            );
638
        }
639

    
640
//        if( res.contains("cosa") ) {
641
//            logger.info("Classloader {}, searching in depends pluginLoaders", this.getPluginName());
642
//        }
643
        logger.debug("Classloader {}, searching in depends pluginLoaders", this.getPluginName());
644
        for (PluginClassLoader pluginClassLoader : this.pluginLoaders) {
645
            if (pluginClassLoader != null) {
646
                try {
647
                    ret = pluginClassLoader.getResource(pluginsVisiteds, res);
648
                    if (ret != null) {
649
                        logger.debug("Classloader {}, Found resource '{}' in plugin '{}'.",
650
                                new Object[]{
651
                                    this.getPluginName(), res, pluginClassLoader.getPluginName(),});
652
                        return ret;
653
                    }
654
                } catch (Exception e) {
655
                    // Ignore, try in next classloader
656
                }  finally {
657
                    pluginsVisiteds.add(pluginClassLoader.getPluginName());
658
                }
659
            }
660
        }
661

    
662
        if (ret == null) {
663
            //
664
            // Por ultimo en el class loader padre, se supone que es el del sistema.
665
            try {
666
                ret = super.getResource(res);
667
            } catch (Exception e) {
668
                logger.warn("Classloader {}, error getting resource '{}' in parent classloader'",
669
                        new Object[]{this.getPluginName(), res},
670
                        e
671
                );
672
            }
673
            if (ret == null) {
674
                logger.debug("Classloader {}, Resource '{}' not found.",
675
                        new Object[]{this.getPluginName(), res}
676
                );
677
            }
678
        }
679
        return ret;
680
    }
681

    
682
    @Override
683
    public void addURL(URL url) {
684
        super.addURL(url);
685
    }
686

    
687
    
688
}