001/*
002 * $Id: Compiler.java,v 1.111 2005/10/27 16:44:49 jponge Exp $
003 * IzPack - Copyright 2001-2005 Julien Ponge, All Rights Reserved.
004 * 
005 * http://www.izforge.com/izpack/
006 * http://developer.berlios.de/projects/izpack/
007 * 
008 * Copyright 2001 Johannes Lehtinen
009 * Copyright 2002 Paul Wilkinson
010 * Copyright 2004 Gaganis Giorgos
011 *
012 * 
013 * Licensed under the Apache License, Version 2.0 (the "License");
014 * you may not use this file except in compliance with the License.
015 * You may obtain a copy of the License at
016 * 
017 *     http://www.apache.org/licenses/LICENSE-2.0
018 *     
019 * Unless required by applicable law or agreed to in writing, software
020 * distributed under the License is distributed on an "AS IS" BASIS,
021 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
022 * See the License for the specific language governing permissions and
023 * limitations under the License.
024 */
025
026package com.izforge.izpack.compiler;
027
028import java.io.File;
029import java.io.IOException;
030import java.net.MalformedURLException;
031import java.net.URL;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.HashMap;
035import java.util.Iterator;
036import java.util.List;
037import java.util.Map;
038import java.util.Properties;
039import java.util.Set;
040import java.util.jar.JarInputStream;
041import java.util.zip.ZipEntry;
042
043import com.izforge.izpack.CustomData;
044import com.izforge.izpack.GUIPrefs;
045import com.izforge.izpack.Info;
046import com.izforge.izpack.Panel;
047import com.izforge.izpack.util.Debug;
048import com.izforge.izpack.util.VariableSubstitutor;
049
050/**
051 * The IzPack compiler class. This is now a java bean style class that can be
052 * configured using the object representations of the install.xml
053 * configuration. The install.xml configuration is now handled by the
054 * CompilerConfig class.
055 * 
056 * @see CompilerConfig
057 * 
058 * @author Julien Ponge
059 * @author Tino Schwarze
060 * @author Chadwick McHenry
061 */
062public class Compiler extends Thread
063{
064    /** The IzPack version. */
065    public final static String IZPACK_VERSION = "3.8.0";
066
067    /** The IzPack home directory. */
068    public static String IZPACK_HOME = ".";
069
070    /** The base directory. */
071    protected String basedir;
072
073    /** The installer kind. */
074    protected String kind;
075
076    /** The output jar filename. */
077    protected String output;
078
079    /** Collects and packs files into installation jars, as told. */
080    private Packager packager = null;
081
082    /** Error code, set to true if compilation succeeded. */
083    private boolean compileFailed = true;
084
085    /** Key/values which are substituted at compile time in the install data */
086    private Properties properties;
087
088    /** Replaces the properties in the install.xml file prior to compiling */
089    private VariableSubstitutor propertySubstitutor;
090
091    /**
092     * Set the IzPack home directory
093     * @param izHome - the izpack home directory
094     */
095    public static void setIzpackHome(String izHome)
096    {
097        IZPACK_HOME = izHome;
098    }
099
100    /**
101     * The constructor.
102     * 
103     * @param basedir The base directory.
104     * @param kind The installer kind.
105     * @param output The installer filename.
106     * @throws CompilerException
107     */
108    public Compiler(String basedir, String kind, String output) throws CompilerException
109    {
110        this(basedir,kind,output,"default");
111    }
112
113    /**
114     * The constructor.
115     * 
116     * @param basedir The base directory.
117     * @param kind The installer kind.
118     * @param output The installer filename.
119     * @param compr_format The format which should be used for the packs.
120     * @throws CompilerException
121     */
122    public Compiler(String basedir, String kind, String output, String compr_format) throws CompilerException
123    {
124        this(basedir,kind,output, compr_format, -1);
125    }
126
127    /**
128     * The constructor.
129     * 
130     * @param basedir The base directory.
131     * @param kind The installer kind.
132     * @param output The installer filename.
133     * @param compr_format The format which should be used for the packs.
134     * @param compr_level Compression level to be used if supported.
135     * @throws CompilerException
136     */
137    public Compiler(String basedir, String kind, String output, 
138            String compr_format, int compr_level) throws CompilerException
139    {
140        // Default initialisation
141        this.basedir = basedir;
142        this.kind = kind;
143        this.output = output;
144
145        // initialize backed by system properties
146        properties = new Properties(System.getProperties());
147        propertySubstitutor = new VariableSubstitutor(properties);
148
149        // add izpack built in property
150        setProperty("izpack.version", IZPACK_VERSION);
151        setProperty("basedir", basedir);
152
153        packager = new Packager(compr_format, compr_level);
154        packager.getCompressor().setCompiler(this);
155    }
156
157    
158    /**
159     * Retrieves the packager listener
160     */
161    public PackagerListener getPackagerListener()
162    {
163        return packager.getPackagerListener();
164    }
165    /**
166     * Sets the packager listener.
167     * 
168     * @param listener The listener.
169     */
170    public void setPackagerListener(PackagerListener listener)
171    {
172        packager.setPackagerListener(listener);
173    }
174
175    /**
176     * Access the installation kind.
177     * @return the installation kind.
178     */
179    public String getKind()
180    {
181        return kind;
182    }
183    /**
184     * Get the packager variables.
185     * @return the packager variables
186     */
187    public Properties getVariables()
188    {
189        return packager.getVariables();
190    }
191
192    /** Compiles. */
193    public void compile()
194    {
195        start();
196    }
197
198    /** The run() method. */
199    public void run()
200    {
201        try
202        {
203            createInstaller(); // Execute the compiler - may send info to
204            // System.out
205        }
206        catch (CompilerException ce)
207        {
208            System.out.println(ce.getMessage() + "\n");
209        }
210        catch (Exception e)
211        {
212            if (Debug.stackTracing())
213            {
214                e.printStackTrace();
215            }
216            else
217            {
218                System.out.println("ERROR: " + e.getMessage());
219            }
220        }
221    }
222
223    /**
224     * Compiles the installation.
225     * 
226     * @exception Exception Description of the Exception
227     */
228    public void createInstaller() throws Exception
229    {
230        // Add the class files from the chosen compressor.
231        if( packager.getCompressor().getContainerPaths() != null )
232        {
233            String [] containerPaths = packager.getCompressor().getContainerPaths();
234            String [][] decoderClassNames = packager.getCompressor().getDecoderClassNames();
235            for( int i = 0; i < containerPaths.length; ++i)
236            {
237                URL compressorURL = null;
238                if( containerPaths[i] != null )
239                    compressorURL = findIzPackResource(containerPaths[i],"pack compression Jar file");
240                if( decoderClassNames[i] != null && decoderClassNames[i].length > 0)
241                    addJarContent(compressorURL, Arrays.asList(decoderClassNames[i]));
242            }
243            
244            
245        }
246  
247        // We ask the packager to create the installer
248        packager.createInstaller(new File(output));
249        this.compileFailed = false;
250    }
251
252    public boolean wasSuccessful()
253    {
254        return !this.compileFailed;
255    }
256
257    public String replaceProperties(String value) throws CompilerException
258    {
259        return propertySubstitutor.substitute(value, "at");
260    }
261
262    public void setGUIPrefs(GUIPrefs prefs)
263    {
264        packager.setGUIPrefs(prefs);
265    }
266    public void setInfo(Info info) throws Exception
267    {
268        packager.setInfo(info);
269    }
270
271    /**
272     * Get the install packager.
273     * @return the install packager.
274     */
275    public Packager getPackager()
276    {
277        return packager;
278    }
279    /**
280     * Get the properties currently known to the compileer.
281     */
282    public Properties getProperties()
283    {
284        return properties;
285    }
286
287    /**
288     * Get the value of a property currerntly known to izpack.
289     * 
290     * @param name the name of the property
291     * @return the value of the property, or null
292     */
293    public String getProperty(String name)
294    {
295        return properties.getProperty(name);
296    }
297
298    /**
299     * Add a name value pair to the project property set. Overwriting any existing value.
300     * 
301     * @param name the name of the property
302     * @param value the value to set
303     * @return true
304     */
305    public boolean setProperty(String name, String value)
306    {
307        // TODO: don't allow overwriting of system properties
308        properties.put(name, value);
309        return true;
310    }
311
312    /**
313     * Add a name value pair to the project property set. It is <i>not</i> replaced it is already
314     * in the set of properties.
315     * 
316     * @param name the name of the property
317     * @param value the value to set
318     * @return true if the property was not already set
319     */
320    public boolean addProperty(String name, String value)
321    {
322        String old = properties.getProperty(name);
323        if (old == null)
324        {
325            properties.put(name, value);
326            return true;
327        }
328        return false;
329    }
330
331    /**
332     * Add jar content to the installation.
333     * @param content 
334     */
335    public void addJarContent(URL content)
336    {
337        packager.addJarContent(content);
338    }
339    /**
340     * Add jar content to the installation.
341     * @param content 
342     */
343    public void addJarContent(URL content, List files)
344    {
345        packager.addJarContent(content, files);
346    }
347    /**
348     * Add a custom jar to the installation.
349     * @param ca
350     * @param url
351     */
352    public void addCustomJar(CustomData ca, URL url)
353    {
354        packager.addCustomJar(ca, url);
355    }
356    /**
357     * Add a lang pack to the installation.
358     * @param iso3
359     * @param iso3xmlURL
360     * @param iso3FlagURL
361     */
362    public void addLangPack(String iso3, URL iso3xmlURL, URL iso3FlagURL)
363    {
364        packager.addLangPack(iso3, iso3xmlURL, iso3FlagURL);
365    }
366    /**
367     * Add a native library to the installation.
368     * @param name
369     * @param url
370     * @throws Exception
371     */
372    public void addNativeLibrary(String name, URL url) throws Exception
373    {
374        packager.addNativeLibrary(name, url);
375    }
376    /**
377     * Add an unistaller library.
378     * @param data
379     */
380    public void addNativeUninstallerLibrary(CustomData data)
381    {
382        packager.addNativeUninstallerLibrary(data);
383    }
384    /**
385     * Add a pack to the installation.
386     * @param pack
387     */
388    public void addPack(PackInfo pack)
389    {
390        packager.addPack(pack);
391    }
392    /**
393     * Add a panel jar to the installation.
394     * @param panel
395     * @param url
396     */
397    public void addPanelJar(Panel panel, URL url)
398    {
399        packager.addPanelJar(panel, url);        
400    }
401    /**
402     * Add a resource to the installation.
403     * @param name
404     * @param url
405     */
406    public void addResource(String name, URL url)
407    {
408        packager.addResource(name, url);
409    }
410
411    /**
412     * Checks whether the dependencies stated in the configuration file are correct. Specifically it
413     * checks that no pack point to a non existent pack and also that there are no circular
414     * dependencies in the packs.
415     */
416    public void checkDependencies() throws CompilerException
417    {
418        checkDependencies(packager.getPacksList());
419    }
420    /**
421     * Checks whether the dependencies among the given Packs. Specifically it
422     * checks that no pack point to a non existent pack and also that there are no circular
423     * dependencies in the packs.
424     * @param packs - List<Pack> representing the packs in the installation
425     */
426    public void checkDependencies(List packs) throws CompilerException
427    {
428        // Because we use package names in the configuration file we assosiate
429        // the names with the objects
430        Map names = new HashMap();
431        for (int i = 0; i < packs.size(); i++)
432        {
433            PackInfo pack = (PackInfo) packs.get(i);
434            names.put(pack.getPack().name, pack);
435        }
436        int result = dfs(packs, names);
437        // @todo More informative messages to include the source of the error
438        if (result == -2)
439            parseError("Circular dependency detected");
440        else if (result == -1) parseError("A dependency doesn't exist");
441    }
442
443    /**
444     * We use the dfs graph search algorithm to check whether the graph is acyclic as described in:
445     * Thomas H. Cormen, Charles Leiserson, Ronald Rivest and Clifford Stein. Introduction to
446     * algorithms 2nd Edition 540-549,MIT Press, 2001
447     * 
448     * @param packs The graph
449     * @param names The name map
450     */
451    private int dfs(List packs, Map names)
452    {
453        Map edges = new HashMap();
454        for (int i = 0; i < packs.size(); i++)
455        {
456            PackInfo pack = (PackInfo) packs.get(i);
457            if (pack.colour == PackInfo.WHITE)
458            {
459                if (dfsVisit(pack, names, edges) != 0) return -1;
460            }
461
462        }
463        return checkBackEdges(edges);
464    }
465
466    /**
467     * This function checks for the existence of back edges.
468     */
469    private int checkBackEdges(Map edges)
470    {
471        Set keys = edges.keySet();
472        for (Iterator iterator = keys.iterator(); iterator.hasNext();)
473        {
474            final Object key = iterator.next();
475            int color = ((Integer) edges.get(key)).intValue();
476            if (color == PackInfo.GREY) { return -2; }
477        }
478        return 0;
479
480    }
481
482    /**
483     * This class is used for the classification of the edges
484     */
485    private class Edge
486    {
487
488        PackInfo u;
489
490        PackInfo v;
491
492        Edge(PackInfo u, PackInfo v)
493        {
494            this.u = u;
495            this.v = v;
496        }
497    }
498
499    private int dfsVisit(PackInfo u, Map names, Map edges)
500    {
501        u.colour = PackInfo.GREY;
502        List deps = u.getDependencies();
503        if (deps != null)
504        {
505            for (int i = 0; i < deps.size(); i++)
506            {
507                String name = (String) deps.get(i);
508                PackInfo v = (PackInfo) names.get(name);
509                if (v == null)
510                {
511                    System.out.println("Failed to find dependency: "+name);
512                    return -1;
513                }
514                Edge edge = new Edge(u, v);
515                if (edges.get(edge) == null) edges.put(edge, new Integer(v.colour));
516
517                if (v.colour == PackInfo.WHITE)
518                {
519
520                    final int result = dfsVisit(v, names, edges);
521                    if (result != 0) return result;
522                }
523            }
524        }
525        u.colour = PackInfo.BLACK;
526        return 0;
527    }
528
529    /**
530     * Recursive method to add files in a pack.
531     * 
532     * @param file The file to add.
533     * @param targetdir The relative path to the parent.
534     * @param osList The target OS constraints.
535     * @param override Overriding behaviour.
536     * @param pack Pack to be packed into
537     * @param additionals Map which contains additional data
538     * @exception FileNotFoundException if the file does not exist
539     */
540    protected void addRecursively(File file, String targetdir, List osList, int override,
541            PackInfo pack, Map additionals) throws IOException
542    {
543        String targetfile = targetdir + "/" + file.getName();
544        if (!file.isDirectory())
545            pack.addFile(file, targetfile, osList, override, additionals);
546        else
547        {
548            File[] files = file.listFiles();
549            if (files.length == 0) // The directory is empty so must be added
550                pack.addFile(file, targetfile, osList, override, additionals);
551            else
552            {
553                // new targetdir = targetfile;
554                for (int i = 0; i < files.length; i++)
555                    addRecursively(files[i], targetfile, osList, override, pack, additionals);
556            }
557        }
558    }
559
560    /**
561     * Look for an IzPack resource either in the compiler jar, or within IZPACK_HOME. The path must
562     * not be absolute. The path must use '/' as the fileSeparator (it's used to access the jar
563     * file). If the resource is not found, a CompilerException is thrown indicating fault in the
564     * parent element.
565     * 
566     * @param path the relative path (using '/' as separator) to the resource.
567     * @param desc the description of the resource used to report errors
568     * @return a URL to the resource.
569     */
570    public URL findIzPackResource(String path, String desc)
571            throws CompilerException
572    {
573        URL url = getClass().getResource("/" + path);
574        if (url == null)
575        {
576            File resource = new File(path);
577            if (!resource.isAbsolute()) resource = new File(IZPACK_HOME, path);
578
579            if (!resource.exists()) // fatal
580                parseError(desc + " not found: " + resource);
581
582            try
583            {
584                url = resource.toURL();
585            }
586            catch (MalformedURLException how)
587            {
588                parseError(desc + "(" + resource + ")", how);
589            }
590        }
591
592        return url;
593    }
594
595    /**
596     * Create parse error with consistent messages. Includes file name. For use When parent is
597     * unknown.
598     * 
599     * @param message Brief message explaining error
600     */
601    public void parseError(String message) throws CompilerException
602    {
603        this.compileFailed = true;
604        throw new CompilerException(message);
605    }
606    public void parseError(String message, Throwable how) throws CompilerException
607    {
608        this.compileFailed = true;
609        throw new CompilerException(message, how);
610    }
611
612    /**
613     * The main method if the compiler is invoked by a command-line call.
614     * This simply calls the CompilerConfig.main method.
615     * 
616     * @param args The arguments passed on the command-line.
617     */
618    public static void main(String[] args)
619    {
620        CompilerConfig.main(args);
621    }
622
623    // -------------------------------------------------------------------------
624    // ------------- Listener stuff ------------------------- START ------------
625
626    /**
627     * This method parses install.xml for defined listeners and put them in the right position. If
628     * posible, the listeners will be validated. Listener declaration is a fragmention in
629     * install.xml like : &lt;listeners&gt; &lt;listener compiler="PermissionCompilerListener"
630     * installer="PermissionInstallerListener"/1gt; &lt;/listeners&gt;
631     * 
632     * @param type The listener type.
633     * @param className The class name.
634     * @param jarPath The jar path.
635     * @param constraints The list of constraints.
636     * @throws Exception Thrown in case an error occurs.
637     */
638    public void addCustomListener(int type, String className, String jarPath, List constraints) throws Exception
639    {
640        jarPath = replaceProperties(jarPath);
641        URL url = findIzPackResource(jarPath, "CustomAction jar file");
642        List filePaths = getContainedFilePaths(url);
643        String fullClassName = getFullClassName(url, className);
644        CustomData ca = new CustomData(fullClassName, filePaths, constraints, type);
645        packager.addCustomJar(ca, url);
646    }
647
648    /**
649     * Returns a list which contains the pathes of all files which are included in the given url.
650     * This method expects as the url param a jar.
651     * 
652     * @param url url of the jar file
653     * @return full qualified paths of the contained files
654     * @throws Exception
655     */
656    private List getContainedFilePaths(URL url) throws Exception
657    {
658        JarInputStream jis = new JarInputStream(url.openStream());
659        ZipEntry zentry = null;
660        ArrayList fullNames = new ArrayList();
661        while ((zentry = jis.getNextEntry()) != null)
662        {
663            String name = zentry.getName();
664            // Add only files, no directory entries.
665            if (!zentry.isDirectory()) fullNames.add(name);
666        }
667        jis.close();
668        return (fullNames);
669    }
670
671    /**
672     * Returns the qualified class name for the given class. This method expects as the url param a
673     * jar file which contains the given class. It scans the zip entries of the jar file.
674     * 
675     * @param url url of the jar file which contains the class
676     * @param className short name of the class for which the full name should be resolved
677     * @return full qualified class name
678     * @throws Exception
679     */
680    private String getFullClassName(URL url, String className) throws Exception
681    {
682        JarInputStream jis = new JarInputStream(url.openStream());
683        ZipEntry zentry = null;
684        while ((zentry = jis.getNextEntry()) != null)
685        {
686            String name = zentry.getName();
687            int lastPos = name.lastIndexOf(".class");
688            if (lastPos < 0)
689            {
690                continue; // No class file.
691            }
692            name = name.replace('/', '.');
693            int pos = -1;
694            if (className != null)
695            {
696                pos = name.indexOf(className);
697            }
698            if (name.length() == pos + className.length() + 6) // "Main" class
699            // found
700            {
701                jis.close();
702                return (name.substring(0, lastPos));
703            }
704        }
705        jis.close();
706        return (null);
707    }
708
709    // -------------------------------------------------------------------------
710    // ------------- Listener stuff ------------------------- END ------------
711
712    /**
713     * Used to handle the packager messages in the command-line mode.
714     * 
715     * @author julien created October 26, 2002
716     */
717    static class CmdlinePackagerListener implements PackagerListener
718    {
719
720        /**
721         * Print a message to the console at default priority (MSG_INFO).
722         * 
723         * @param info The information.
724         */
725        public void packagerMsg(String info)
726        {
727            packagerMsg(info, MSG_INFO);
728        }
729
730        /**
731         * Print a message to the console at the specified priority.
732         * 
733         * @param info The information.
734         */
735        public void packagerMsg(String info, int priority)
736        {
737            final String prefix;
738            switch (priority)
739            {
740            case MSG_DEBUG:
741                prefix = "[ DEBUG ] ";
742                break;
743            case MSG_ERR:
744                prefix = "[ ERROR ] ";
745                break;
746            case MSG_WARN:
747                prefix = "[ WARNING ] ";
748                break;
749            case MSG_INFO:
750            case MSG_VERBOSE:
751            default: // don't die, but don't prepend anything
752                prefix = "";
753            }
754
755            System.out.println(prefix + info);
756        }
757
758        /** Called when the packager starts. */
759        public void packagerStart()
760        {
761            System.out.println("[ Begin ]");
762            System.out.println();
763        }
764
765        /** Called when the packager stops. */
766        public void packagerStop()
767        {
768            System.out.println();
769            System.out.println("[ End ]");
770        }
771    }
772
773}