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 */
019package icy.plugin.abstract_;
020
021import java.awt.image.BufferedImage;
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.URL;
026import java.util.ArrayList;
027import java.util.Enumeration;
028import java.util.List;
029
030import javax.swing.ImageIcon;
031
032import icy.file.FileUtil;
033import icy.gui.frame.IcyFrame;
034import icy.gui.viewer.Viewer;
035import icy.image.IcyBufferedImage;
036import icy.image.ImageUtil;
037import icy.main.Icy;
038import icy.network.NetworkUtil;
039import icy.plugin.PluginDescriptor;
040import icy.plugin.PluginLauncher;
041import icy.plugin.PluginLoader;
042import icy.plugin.interface_.PluginBundled;
043import icy.plugin.interface_.PluginThreaded;
044import icy.preferences.PluginsPreferences;
045import icy.preferences.XMLPreferences;
046import icy.resource.ResourceUtil;
047import icy.sequence.Sequence;
048import icy.system.IcyExceptionHandler;
049import icy.system.SystemUtil;
050import icy.system.audit.Audit;
051import icy.util.ClassUtil;
052
053/**
054 * Base class for Plugin, provide some helper methods.<br>
055 * By default the constructor of a Plugin class is called in the EDT (Event Dispatch Thread).<br>
056 * If the plugin implements the {@link PluginThreaded} there is no more guarantee that is the case.
057 * 
058 * @author Fabrice de Chaumont & Stephane
059 */
060public abstract class Plugin
061{
062    public static Plugin getPlugin(List<Plugin> list, String className)
063    {
064        for (Plugin plugin : list)
065            if (plugin.getClass().getName().equals(className))
066                return plugin;
067
068        return null;
069    }
070
071    private PluginDescriptor descriptor;
072
073    /**
074     * Default Plugin constructor.<br>
075     * The {@link PluginLauncher} is normally responsible of Plugin class instantiation.
076     */
077    public Plugin()
078    {
079        super();
080
081        // get descriptor from loader
082        descriptor = PluginLoader.getPlugin(getClass().getName());
083
084        if (descriptor == null)
085        {
086            // descriptor not found (don't check for anonymous plugin class) ?
087            if (!getClass().isAnonymousClass())
088            {
089                System.out.println(
090                        "Warning : Plugin '" + getClass().getName() + "' started but not found in PluginLoader !");
091                System.out.println("Local XML plugin description file is probably incorrect.");
092            }
093
094            // create dummy descriptor
095            descriptor = new PluginDescriptor(this.getClass());
096            descriptor.setName(getClass().getSimpleName());
097        }
098
099        // audit
100        Audit.pluginInstanced(this);
101    }
102
103    @Override
104    protected void finalize() throws Throwable
105    {
106        // unregister plugin (weak reference so we can do it here)
107        Icy.getMainInterface().unRegisterPlugin(this);
108
109        super.finalize();
110    }
111
112    /**
113     * @return the descriptor
114     */
115    public PluginDescriptor getDescriptor()
116    {
117        return descriptor;
118    }
119
120    /**
121     * @return the plugin name (from its descriptor)
122     */
123    public String getName()
124    {
125        return descriptor.getName();
126    }
127
128    /**
129     * @return <code>true</code> if this is a bundled plugin (see {@link PluginBundled}).
130     */
131    public boolean isBundled()
132    {
133        return this instanceof PluginBundled;
134    }
135
136    /**
137     * @return the class name of the plugin owner.<br>
138     *         If this Plugin is not bundled (see {@link PluginBundled}) then it just returns the
139     *         current class name otherwise it will returns the plugin owner class name.
140     */
141    public String getOwnerClassName()
142    {
143        if (isBundled())
144            return ((PluginBundled) this).getMainPluginClassName();
145
146        return getClass().getName();
147    }
148
149    /**
150     * @return the folder where the plugin is installed (or should be installed).
151     */
152    public String getInstallFolder()
153    {
154        return ClassUtil.getPathFromQualifiedName(ClassUtil.getPackageName(getClass().getName()));
155    }
156
157    public Viewer getActiveViewer()
158    {
159        return Icy.getMainInterface().getActiveViewer();
160    }
161
162    public Sequence getActiveSequence()
163    {
164        return Icy.getMainInterface().getActiveSequence();
165    }
166
167    public IcyBufferedImage getActiveImage()
168    {
169        return Icy.getMainInterface().getActiveImage();
170    }
171
172    /**
173     * @deprecated Use {@link #getActiveViewer()} instead
174     */
175    @Deprecated
176    public Viewer getFocusedViewer()
177    {
178        return getActiveViewer();
179    }
180
181    /**
182     * @deprecated Use {@link #getActiveSequence()} instead
183     */
184    @Deprecated
185    public Sequence getFocusedSequence()
186    {
187        return getActiveSequence();
188    }
189
190    /**
191     * @deprecated Use {@link #getActiveImage()} instead
192     */
193    @Deprecated
194    public IcyBufferedImage getFocusedImage()
195    {
196        return getActiveImage();
197    }
198
199    public void addIcyFrame(final IcyFrame frame)
200    {
201        frame.addToDesktopPane();
202    }
203
204    public void addSequence(final Sequence sequence)
205    {
206        Icy.getMainInterface().addSequence(sequence);
207    }
208
209    public void removeSequence(final Sequence sequence)
210    {
211        sequence.close();
212    }
213
214    public ArrayList<Sequence> getSequences()
215    {
216        return Icy.getMainInterface().getSequences();
217    }
218
219    /**
220     * Return the resource URL from given resource name.<br>
221     * Ex: <code>getResource("plugins/author/resources/def.xml");</code>
222     * 
223     * @param name
224     *        resource name
225     */
226    public URL getResource(String name)
227    {
228        return getClass().getClassLoader().getResource(name);
229    }
230
231    /**
232     * Return resources corresponding to given resource name.<br>
233     * Ex: <code>getResources("plugins/author/resources/def.xml");</code>
234     * 
235     * @param name
236     *        resource name
237     * @throws IOException
238     */
239    public Enumeration<URL> getResources(String name) throws IOException
240    {
241        return getClass().getClassLoader().getResources(name);
242    }
243
244    /**
245     * Return the resource as data stream from given resource name.<br>
246     * Ex: <code>getResourceAsStream("plugins/author/resources/def.xml");</code>
247     * 
248     * @param name
249     *        resource name
250     */
251    public InputStream getResourceAsStream(String name)
252    {
253        return getClass().getClassLoader().getResourceAsStream(name);
254    }
255
256    /**
257     * Return the image resource from given resource name
258     * Ex: <code>getResourceAsStream("plugins/author/resources/image.png");</code>
259     * 
260     * @param resourceName
261     *        resource name
262     */
263    public BufferedImage getImageResource(String resourceName)
264    {
265        return ImageUtil.load(getResourceAsStream(resourceName));
266    }
267
268    /**
269     * Return the icon resource from given resource name
270     * Ex: <code>getResourceAsStream("plugins/author/resources/icon.png");</code>
271     * 
272     * @param resourceName
273     *        resource name
274     */
275    public ImageIcon getIconResource(String resourceName)
276    {
277        return ResourceUtil.getImageIcon(getImageResource(resourceName));
278    }
279
280    /**
281     * Retrieve the preferences root for this plugin.<br>
282     */
283    public XMLPreferences getPreferencesRoot()
284    {
285        return PluginsPreferences.root(this);
286    }
287
288    /**
289     * Retrieve the plugin preferences node for specified name.<br>
290     * i.e : getPreferences("window") will return node
291     * "plugins.[authorPackage].[pluginClass].window"
292     */
293    public XMLPreferences getPreferences(String name)
294    {
295        return getPreferencesRoot().node(name);
296    }
297
298    /**
299     * Returns the base resource path for plugin native libraries.<br/>
300     * Depending the Operating System it can returns these values:
301     * <ul>
302     * <li>lib/unix32</li>
303     * <li>lib/unix64</li>
304     * <li>lib/mac32</li>
305     * <li>lib/mac64</li>
306     * <li>lib/win32</li>
307     * <li>lib/win64</li>
308     * </ul>
309     */
310    protected String getResourceLibraryPath()
311    {
312        return "lib" + FileUtil.separator + SystemUtil.getOSArchIdString();
313    }
314
315    /**
316     * Load a packed native library from the JAR file.<br/>
317     * Native libraries should be packaged with the following directory & file structure:
318     * 
319     * <pre>
320     * /lib/unix32
321     *   libxxx.so
322     * /lib/unix64
323     *   libxxx.so
324     * /lib/mac32
325     *   libxxx.dylib
326     * /lib/mac64
327     *   libxxx.dylib
328     * /lib/win32
329     *   xxx.dll
330     * /lib/win64
331     *   xxx.dll
332     * /plugins/myname/mypackage    
333     *   MyPlugin.class
334     *   ....
335     * </pre>
336     * 
337     * Here "xxx" is the name of the native library.<br/>
338     * Current approach is to unpack the native library into a temporary file and load from there.
339     * 
340     * @param libName
341     * @return true if the library was correctly loaded.
342     * @see #prepareLibrary(String)
343     */
344    public boolean loadLibrary(String libName)
345    {
346        final File file = prepareLibrary(libName);
347
348        if (file == null)
349            return false;
350
351        // and load it
352        System.load(file.getPath());
353
354        return true;
355    }
356
357    /**
358     * Extract a packed native library from the JAR file to a temporary native library folder so it can be easily loaded
359     * later.<br/>
360     * Native libraries should be packaged with the following directory & file structure:
361     * 
362     * <pre>
363     * /lib/unix32
364     *   libxxx.so
365     * /lib/unix64
366     *   libxxx.so
367     * /lib/mac32
368     *   libxxx.dylib
369     * /lib/mac64
370     *   libxxx.dylib
371     * /lib/win32
372     *   xxx.dll
373     * /lib/win64
374     *   xxx.dll
375     * /plugins/myname/mypackage    
376     *   MyPlugin.class
377     *   ....
378     * </pre>
379     * 
380     * Here "xxx" is the name of the native library.<br/>
381     * 
382     * @param libName
383     * @return the extracted native library file.
384     * @see #loadLibrary(String)
385     */
386    public File prepareLibrary(String libName)
387    {
388        try
389        {
390            // get mapped library name
391            String mappedlibName = System.mapLibraryName(libName);
392            // get base resource path for native library
393            final String basePath = getResourceLibraryPath() + FileUtil.separator;
394
395            // search for library in resource
396            URL libUrl = getResource(basePath + mappedlibName);
397
398            // not found ?
399            if (libUrl == null)
400            {
401                // jnilib extension may not work, try with "dylib" extension instead
402                if (mappedlibName.endsWith(".jnilib"))
403                {
404                    mappedlibName = mappedlibName.substring(0, mappedlibName.length() - 7) + ".dylib";
405                    libUrl = getResource(basePath + mappedlibName);
406                }
407                // do the contrary in case we have an old "jnilib" file and system use "dylib" by default
408                else if (mappedlibName.endsWith(".dylib"))
409                {
410                    mappedlibName = mappedlibName.substring(0, mappedlibName.length() - 6) + ".jnilib";
411                    libUrl = getResource(basePath + mappedlibName);
412                }
413            }
414
415            // resource not found --> error
416            if (libUrl == null)
417                throw new IOException("Couldn't find resource " + basePath + mappedlibName);
418
419            // extract resource
420            final File extractedFile = extractResource(
421                    SystemUtil.getTempLibraryDirectory() + FileUtil.separator + mappedlibName, libUrl);
422
423            return extractedFile;
424        }
425        catch (IOException e)
426        {
427            System.err.println("Error while extracting packed library " + libName + ": " + e);
428        }
429
430        return null;
431    }
432
433    /**
434     * Extract a resource to the specified path
435     * 
436     * @param outputPath
437     *        the file to extract the resource to
438     * @param resource
439     *        the resource URL
440     * @return the extracted file
441     * @throws IOException
442     */
443    protected File extractResource(String outputPath, URL resource) throws IOException
444    {
445        // open resource stream
446        final InputStream in = resource.openStream();
447        // create output file
448        final File result = new File(outputPath);
449        final byte data[];
450
451        try
452        {
453            // load resource
454            data = NetworkUtil.download(in);
455        }
456        finally
457        {
458            in.close();
459        }
460
461        // file already exist ??
462        if (result.exists())
463        {
464            // same size --> assume it's the same
465            if (result.length() == data.length)
466                return result;
467
468            if (!FileUtil.delete(result, false))
469                throw new IOException("Cannot overwrite " + result + " file !");
470        }
471
472        // save resource to file
473        FileUtil.save(result, data, true);
474
475        return result;
476    }
477
478    /**
479     * Report an error log for this plugin (reported to Icy web site which report then to the
480     * author of the plugin).
481     * 
482     * @see IcyExceptionHandler#report(PluginDescriptor, String)
483     */
484    public void report(String errorLog)
485    {
486        IcyExceptionHandler.report(descriptor, errorLog);
487    }
488
489    @Override
490    public String toString()
491    {
492        return getDescriptor().getName();
493    }
494}