001/*
002 * $Id: CompilerConfig.java,v 1.8 2005/09/09 03:28:22 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.BufferedInputStream;
029import java.io.BufferedOutputStream;
030import java.io.File;
031import java.io.FileNotFoundException;
032import java.io.FileInputStream;
033import java.io.FileOutputStream;
034import java.io.IOException;
035import java.io.InputStream;
036import java.net.MalformedURLException;
037import java.net.URL;
038import java.net.URLClassLoader;
039import java.util.ArrayList;
040import java.util.Date;
041import java.util.Enumeration;
042import java.util.HashMap;
043import java.util.Iterator;
044import java.util.List;
045import java.util.Map;
046import java.util.Properties;
047import java.util.Set;
048import java.util.StringTokenizer;
049import java.util.TreeMap;
050import java.util.Vector;
051import java.util.jar.JarInputStream;
052import java.util.zip.ZipEntry;
053
054import org.apache.tools.ant.DirectoryScanner;
055
056import com.izforge.izpack.CustomData;
057import com.izforge.izpack.ExecutableFile;
058import com.izforge.izpack.GUIPrefs;
059import com.izforge.izpack.Info;
060import com.izforge.izpack.PackFile;
061import com.izforge.izpack.Panel;
062import com.izforge.izpack.ParsableFile;
063import com.izforge.izpack.UpdateCheck;
064import com.izforge.izpack.compiler.Compiler.CmdlinePackagerListener;
065import com.izforge.izpack.event.CompilerListener;
066import com.izforge.izpack.util.Debug;
067import com.izforge.izpack.util.OsConstraint;
068import com.izforge.izpack.util.VariableSubstitutor;
069
070import net.n3.nanoxml.IXMLReader;
071import net.n3.nanoxml.NonValidator;
072import net.n3.nanoxml.StdXMLBuilder;
073import net.n3.nanoxml.StdXMLParser;
074import net.n3.nanoxml.StdXMLReader;
075import net.n3.nanoxml.XMLElement;
076
077/**
078 * A parser for the installer xml configuration. This parses a document
079 * conforming to the installation.dtd and populates a Compiler instance to
080 * perform the install compilation. 
081 * 
082 * @author Scott Stark
083 * @version $Revision: 1.8 $
084 */
085public class CompilerConfig extends Thread
086{
087    /** The compiler version. */
088    public final static String VERSION = "1.0";
089
090    /** Standard installer. */
091    public final static String STANDARD = "standard";
092
093    /** Web installer. */
094    public final static String WEB = "web";
095
096    /** Constant for checking attributes. */
097    private static boolean YES = true;
098
099    /** Constant for checking attributes. */
100    private static boolean NO = false;
101
102    /** The xml install file */
103    private String filename;
104    /** The xml install configuration text */
105    private String installText;
106    /** The base directory. */
107    protected String basedir;
108
109    /** The installer packager compiler */
110    private Compiler compiler;
111
112    /**
113     * List of CompilerListeners which should be called at packaging
114     */
115    protected List compilerListeners = new ArrayList();
116
117    /**
118     * Set the IzPack home directory
119     * @param izHome - the izpack home directory
120     */
121    public static void setIzpackHome(String izHome)
122    {
123        Compiler.setIzpackHome(izHome);
124    }
125
126    /**
127     * The constructor.
128     * 
129     * @param filename The XML filename.
130     * @param basedir The base directory.
131     * @param kind The installer kind.
132     * @param output The installer filename.
133     * @throws CompilerException
134     */
135    public CompilerConfig(String filename, String basedir, String kind, String output) throws CompilerException
136    {
137        this(filename, basedir, kind, output, (PackagerListener) null);
138    }
139    /**
140     * The constructor.
141     * 
142     * @param filename The XML filename.
143     * @param basedir The base directory.
144     * @param kind The installer kind.
145     * @param output The installer filename.
146     * @param listener The PackagerListener.
147     * @throws CompilerException
148     */
149    public CompilerConfig(String filename, String basedir, String kind, String output, PackagerListener listener) 
150    throws CompilerException
151    {
152        this(filename,basedir,kind,output, "default", listener);
153    }
154
155    /**
156     * @param filename The XML filename.
157     * @param kind The installer kind.
158     * @param output The installer filename.
159     * @param compr_format The compression format to be used for packs.
160     * @param listener The PackagerListener.
161     * @throws CompilerException
162     */
163    public CompilerConfig(String filename, String base, String kind, String output, String compr_format, 
164            PackagerListener listener) throws CompilerException
165    {
166        this(filename,base,kind,output, compr_format,listener, (String) null);
167    }
168
169    /**
170     * 
171     * @param basedir The base directory.
172     * @param kind The installer kind.
173     * @param output The installer filename.
174     * @param listener The PackagerListener.
175     * @param installText The install xml configuration text
176     * @throws CompilerException
177     */
178    public CompilerConfig(String basedir, String kind, String output, PackagerListener listener,
179            String installText) throws CompilerException
180    {
181        this((String) null, basedir, kind, output, "default", listener, installText);
182    }
183    /**
184     * 
185     * @param filename The XML filename.
186     * @param basedir The base directory.
187     * @param kind The installer kind.
188     * @param output The installer filename.
189     * @param compr_format The compression format to be used for packs.
190     * @param listener The PackagerListener.
191     * @param installText The install xml configuration text
192     * @throws CompilerException
193     */
194    public CompilerConfig(String filename, String basedir, String kind, String output, String compr_format, 
195            PackagerListener listener, String installText) throws CompilerException
196    {
197        this( filename, basedir, kind, output, compr_format, -1, listener, installText);
198    }
199
200    /**
201     * @param filename The XML filename.
202     * @param basedir The base directory.
203     * @param kind The installer kind.
204     * @param output The installer filename.
205     * @param compr_format The compression format to be used for packs.
206     * @param compr_level Compression level to be used if supported.
207     * @param listener The PackagerListener.
208     * @param installText The install xml configuration text
209    * @throws CompilerException
210     */
211    public CompilerConfig(String filename, String basedir, String kind, String output, String compr_format, 
212            int compr_level, PackagerListener listener, String installText) throws CompilerException
213    {
214        this.filename = filename;
215        this.installText = installText;
216        this.basedir = basedir;
217        this.compiler = new Compiler(basedir, kind, output, compr_format, compr_level);
218        compiler.setPackagerListener(listener);
219    }
220
221
222
223    /**
224     * Add a name value pair to the project property set. It is <i>not</i>
225     * replaced it is already in the set of properties.
226     * 
227     * @param name the name of the property
228     * @param value the value to set
229     * @return true if the property was not already set
230     */
231    public boolean addProperty(String name, String value)
232    {
233        return compiler.addProperty(name, value);
234    }
235
236    /**
237     * Access the install compiler
238     * @return the install compiler
239     */
240    public Compiler getCompiler()
241    {
242        return compiler;
243    }
244
245    /**
246     * Retrieves the packager listener
247     */
248    public PackagerListener getPackagerListener()
249    {
250        return compiler.getPackagerListener();
251    }
252
253    /** Compile the installation */
254    public void compile()
255    {
256        start();
257    }
258
259    /** The run() method. */
260    public void run()
261    {
262        try
263        {
264            executeCompiler();
265        }
266        catch (CompilerException ce)
267        {
268            System.out.println(ce.getMessage() + "\n");
269        }
270        catch (Exception e)
271        {
272            if (Debug.stackTracing())
273            {
274                e.printStackTrace();
275            }
276            else
277            {
278                System.out.println("ERROR: " + e.getMessage());
279            }
280        }
281    }
282
283    /**
284     * Compiles the installation.
285     * 
286     * @exception Exception Description of the Exception
287     */
288    public void executeCompiler() throws Exception
289    {
290        // normalize and test: TODO: may allow failure if we require write
291        // access
292        File base = new File(basedir).getAbsoluteFile();
293        if (!base.canRead() || !base.isDirectory())
294            throw new CompilerException("Invalid base directory: " + base);
295
296        // add izpack built in property
297        compiler.setProperty("basedir", base.toString());
298
299        // We get the XML data tree
300        XMLElement data = getXMLTree();
301
302        // Listeners to various events
303        addCustomListeners(data);
304
305        // Read the properties and perform replacement on the rest of the tree
306        substituteProperties(data);
307
308        // We add all the information
309        addVariables(data);
310        addInfo(data);
311        addGUIPrefs(data);
312        addLangpacks(data);
313        addResources(data);
314        addNativeLibraries(data);
315        addJars(data);
316        addPanels(data);
317        addPacks(data);
318
319        // We ask the packager to create the installer
320        compiler.createInstaller();
321    }
322
323    public boolean wasSuccessful()
324    {
325        return compiler.wasSuccessful();
326    }
327
328    /**
329     * Returns the GUIPrefs.
330     * 
331     * @param data The XML data.
332     * @exception CompilerException Description of the Exception
333     */
334    protected void addGUIPrefs(XMLElement data) throws CompilerException
335    {
336        notifyCompilerListener("addGUIPrefs", CompilerListener.BEGIN, data);
337        // We get the XMLElement & the attributes
338        XMLElement gp = data.getFirstChildNamed("guiprefs");
339        GUIPrefs prefs = new GUIPrefs();
340        if (gp != null)
341        {
342            prefs.resizable = requireYesNoAttribute(gp, "resizable");
343            prefs.width = requireIntAttribute(gp, "width");
344            prefs.height = requireIntAttribute(gp, "height");
345
346            // Look and feel mappings
347            Iterator it = gp.getChildrenNamed("laf").iterator();
348            while (it.hasNext())
349            {
350                XMLElement laf = (XMLElement) it.next();
351                String lafName = requireAttribute(laf, "name");
352                requireChildNamed(laf, "os");
353
354                Iterator oit = laf.getChildrenNamed("os").iterator();
355                while (oit.hasNext())
356                {
357                    XMLElement os = (XMLElement) oit.next();
358                    String osName = requireAttribute(os, "family");
359                    prefs.lookAndFeelMapping.put(osName, lafName);
360                }
361
362                Iterator pit = laf.getChildrenNamed("param").iterator();
363                Map params = new TreeMap();
364                while (pit.hasNext())
365                {
366                    XMLElement param = (XMLElement) pit.next();
367                    String name = requireAttribute(param, "name");
368                    String value = requireAttribute(param, "value");
369                    params.put(name, value);
370                }
371                prefs.lookAndFeelParams.put(lafName, params);
372            }
373            // Load modifier
374            it = gp.getChildrenNamed("modifier").iterator();
375            while (it.hasNext())
376            {
377                XMLElement curentModifier = (XMLElement) it.next();
378                String key = requireAttribute(curentModifier, "key");
379                String value = requireAttribute(curentModifier, "value");
380                prefs.modifier.put(key, value);
381
382            }
383            // make sure jar contents of each are available in installer
384            // map is easier to read/modify than if tree
385            HashMap lafMap = new HashMap();
386            lafMap.put("liquid", "liquidlnf.jar");
387            lafMap.put("kunststoff", "kunststoff.jar");
388            lafMap.put("metouia", "metouia.jar");
389            lafMap.put("looks", "looks.jar");
390
391            // is this really what we want? a double loop? needed, since above,
392            // it's
393            // the /last/ lnf for an os which is used, so can't add during
394            // initial
395            // loop
396            Iterator kit = prefs.lookAndFeelMapping.keySet().iterator();
397            while (kit.hasNext())
398            {
399                String lafName = (String) prefs.lookAndFeelMapping.get(kit.next());
400                String lafJarName = (String) lafMap.get(lafName);
401                if (lafJarName == null) parseError(gp, "Unrecognized Look and Feel: " + lafName);
402
403                URL lafJarURL = findIzPackResource("lib/" + lafJarName, "Look and Feel Jar file",
404                        gp);
405                compiler.addJarContent(lafJarURL);
406            }
407        }
408        compiler.setGUIPrefs(prefs);
409        notifyCompilerListener("addGUIPrefs", CompilerListener.END, data);
410    }
411
412    /**
413     * Add project specific external jar files to the installer.
414     * 
415     * @param data The XML data.
416     */
417    protected void addJars(XMLElement data) throws Exception
418    {
419        notifyCompilerListener("addJars", CompilerListener.BEGIN, data);
420        Iterator iter = data.getChildrenNamed("jar").iterator();
421        while (iter.hasNext())
422        {
423            XMLElement el = (XMLElement) iter.next();
424            String src = requireAttribute(el, "src");
425            URL url = findProjectResource(src, "Jar file", el);
426            compiler.addJarContent(url);
427            // Additionals for mark a jar file also used in the uninstaller.
428            // The contained files will be copied from the installer into the
429            // uninstaller if needed.
430            // Therefore the contained files of the jar should be in the
431            // installer also
432            // they are used only from the uninstaller. This is the reason why
433            // the stage
434            // wiil be only observed for the uninstaller.
435            String stage = el.getAttribute("stage");
436            if (stage != null
437                    && (stage.equalsIgnoreCase("both") || stage.equalsIgnoreCase("uninstall")))
438            {
439                CustomData ca = new CustomData(null, getContainedFilePaths(url), null,
440                        CustomData.UNINSTALLER_JAR);
441                compiler.addCustomJar(ca, url);
442            }
443        }
444        notifyCompilerListener("addJars", CompilerListener.END, data);
445    }
446
447    /**
448     * Add native libraries to the installer.
449     * 
450     * @param data The XML data.
451     */
452    protected void addNativeLibraries(XMLElement data) throws Exception
453    {
454        boolean needAddOns = false;
455        notifyCompilerListener("addNativeLibraries", CompilerListener.BEGIN, data);
456        Iterator iter = data.getChildrenNamed("native").iterator();
457        while (iter.hasNext())
458        {
459            XMLElement el = (XMLElement) iter.next();
460            String type = requireAttribute(el, "type");
461            String name = requireAttribute(el, "name");
462            String path = "bin/native/" + type + "/" + name;
463            URL url = findIzPackResource(path, "Native Library", el);
464            compiler.addNativeLibrary(name, url);
465            // Additionals for mark a native lib also used in the uninstaller
466            // The lib will be copied from the installer into the uninstaller if
467            // needed.
468            // Therefore the lib should be in the installer also it is used only
469            // from
470            // the uninstaller. This is the reason why the stage wiil be only
471            // observed
472            // for the uninstaller.
473            String stage = el.getAttribute("stage");
474            List constraints = OsConstraint.getOsList(el);
475            if (stage != null
476                    && (stage.equalsIgnoreCase("both") || stage.equalsIgnoreCase("uninstall")))
477            {
478                ArrayList al = new ArrayList();
479                al.add(name);
480                CustomData cad = new CustomData(null, al, constraints, CustomData.UNINSTALLER_LIB);
481                compiler.addNativeUninstallerLibrary(cad);
482                needAddOns = true;
483            }
484
485        }
486        if (needAddOns)
487        {
488            // Add the uninstaller extensions as a resource if specified
489            XMLElement root = requireChildNamed(data, "info");
490            XMLElement uninstallInfo = root.getFirstChildNamed("uninstaller");
491            if (validateYesNoAttribute(uninstallInfo, "write", YES))
492            {
493                URL url = findIzPackResource("lib/uninstaller-ext.jar", "Uninstaller extensions",
494                        root);
495                compiler.addResource("IzPack.uninstaller-ext", url);
496            }
497
498        }
499        notifyCompilerListener("addNativeLibraries", CompilerListener.END, data);
500    }
501
502    /**
503     * Add packs and their contents to the installer.
504     * 
505     * @param data The XML data.
506     */
507    protected void addPacks(XMLElement data) throws CompilerException
508    {
509        notifyCompilerListener("addPacks", CompilerListener.BEGIN, data);
510        // Initialisation
511        XMLElement root = requireChildNamed(data, "packs");
512
513        // at least one pack is required
514        Vector packElements = root.getChildrenNamed("pack");
515        if (packElements.isEmpty()) parseError(root, "<packs> requires a <pack>");
516
517        Iterator packIter = packElements.iterator();
518        while (packIter.hasNext())
519        {
520            XMLElement el = (XMLElement) packIter.next();
521
522            // Trivial initialisations
523            String name = requireAttribute(el, "name");
524            String id = el.getAttribute("id");
525            boolean loose = "true".equalsIgnoreCase(el.getAttribute("loose", "false"));
526            String description = requireChildNamed(el, "description").getContent();
527            boolean required = requireYesNoAttribute(el, "required");
528            String group = el.getAttribute("group");
529            String installGroups = el.getAttribute("installGroups");
530
531            PackInfo pack = new PackInfo(name, id, description, required, loose);
532            pack.setOsConstraints(OsConstraint.getOsList(el)); // TODO:
533            // unverified
534            pack.setPreselected(validateYesNoAttribute(el, "preselected", YES));
535            // Set the pack group if specified
536            if (group != null)
537                pack.setGroup(group);
538            // Set the pack install groups if specified
539            if (installGroups != null)
540            {
541                StringTokenizer st = new StringTokenizer(installGroups, ",");
542                while (st.hasMoreTokens())
543                {
544                    String igroup = st.nextToken();
545                    pack.addInstallGroup(igroup);
546                }
547            }
548
549            // We get the parsables list
550            Iterator iter = el.getChildrenNamed("parsable").iterator();
551            while (iter.hasNext())
552            {
553                XMLElement p = (XMLElement) iter.next();
554                String target = requireAttribute(p, "targetfile");
555                String type = p.getAttribute("type", "plain");
556                String encoding = p.getAttribute("encoding", null);
557                List osList = OsConstraint.getOsList(p); // TODO: unverified
558
559                pack.addParsable(new ParsableFile(target, type, encoding, osList));
560            }
561
562            // We get the executables list
563            iter = el.getChildrenNamed("executable").iterator();
564            while (iter.hasNext())
565            {
566                XMLElement e = (XMLElement) iter.next();
567                ExecutableFile executable = new ExecutableFile();
568                String val; // temp value
569
570                executable.path = requireAttribute(e, "targetfile");
571
572                // when to execute this executable
573                val = e.getAttribute("stage", "never");
574                if ("postinstall".equalsIgnoreCase(val))
575                    executable.executionStage = ExecutableFile.POSTINSTALL;
576                else if ("uninstall".equalsIgnoreCase(val))
577                    executable.executionStage = ExecutableFile.UNINSTALL;
578
579                // type of this executable
580                val = e.getAttribute("type", "bin");
581                if ("jar".equalsIgnoreCase(val))
582                {
583                    executable.type = ExecutableFile.JAR;
584                    executable.mainClass = e.getAttribute("class"); // executable
585                    // class
586                }
587
588                // what to do if execution fails
589                val = e.getAttribute("failure", "ask");
590                if ("abort".equalsIgnoreCase(val))
591                    executable.onFailure = ExecutableFile.ABORT;
592                else if ("warn".equalsIgnoreCase(val)) executable.onFailure = ExecutableFile.WARN;
593
594                // whether to keep the executable after executing it
595                val = e.getAttribute("keep");
596                executable.keepFile = "true".equalsIgnoreCase(val);
597
598                // get arguments for this executable
599                XMLElement args = e.getFirstChildNamed("args");
600                if (null != args)
601                {
602                    Iterator argIterator = args.getChildrenNamed("arg").iterator();
603                    while (argIterator.hasNext())
604                    {
605                        XMLElement arg = (XMLElement) argIterator.next();
606                        executable.argList.add(requireAttribute(arg, "value"));
607                    }
608                }
609
610                executable.osList = OsConstraint.getOsList(e); // TODO:
611                // unverified
612
613                pack.addExecutable(executable);
614            }
615
616            // We get the files list
617            iter = el.getChildrenNamed("file").iterator();
618            while (iter.hasNext())
619            {
620                XMLElement f = (XMLElement) iter.next();
621                String src = requireAttribute(f, "src");
622                String targetdir = requireAttribute(f, "targetdir");
623                List osList = OsConstraint.getOsList(f); // TODO: unverified
624                int override = getOverrideValue(f);
625                Map additionals = getAdditionals(f);
626
627                File file = new File(src);
628                if (!file.isAbsolute()) file = new File(basedir, src);
629
630                try
631                {
632                    addRecursively(file, targetdir, osList, override, pack, additionals);
633                }
634                catch (Exception x)
635                {
636                    parseError(f, x.getMessage(), x);
637                }
638            }
639
640            // We get the singlefiles list
641            iter = el.getChildrenNamed("singlefile").iterator();
642            while (iter.hasNext())
643            {
644                XMLElement f = (XMLElement) iter.next();
645                String src = requireAttribute(f, "src");
646                String target = requireAttribute(f, "target");
647                List osList = OsConstraint.getOsList(f); // TODO: unverified
648                int override = getOverrideValue(f);
649                Map additionals = getAdditionals(f);
650
651                File file = new File(src);
652                if (!file.isAbsolute()) file = new File(basedir, src);
653
654                try
655                {
656                    pack.addFile(file, target, osList, override, additionals);
657                }
658                catch (FileNotFoundException x)
659                {
660                    parseError(f, x.getMessage(), x);
661                }
662            }
663
664            // We get the fileset list
665            iter = el.getChildrenNamed("fileset").iterator();
666            while (iter.hasNext())
667            {
668                XMLElement f = (XMLElement) iter.next();
669                String dir_attr = requireAttribute(f, "dir");
670
671                File dir = new File(dir_attr);
672                if (!dir.isAbsolute()) dir = new File(basedir, dir_attr);
673                if (!dir.isDirectory()) // also tests '.exists()'
674                    parseError(f, "Invalid directory 'dir': " + dir_attr);
675
676                boolean casesensitive = validateYesNoAttribute(f, "casesensitive", YES);
677                boolean defexcludes = validateYesNoAttribute(f, "defaultexcludes", YES);
678                String targetdir = requireAttribute(f, "targetdir");
679                List osList = OsConstraint.getOsList(f); // TODO: unverified
680                int override = getOverrideValue(f);
681                Map additionals = getAdditionals(f);
682
683                // get includes and excludes
684                Vector xcludesList = null;
685                String[] includes = null;
686                xcludesList = f.getChildrenNamed("include");
687                if (!xcludesList.isEmpty())
688                {
689                    includes = new String[xcludesList.size()];
690                    for (int j = 0; j < xcludesList.size(); j++)
691                    {
692                        XMLElement xclude = (XMLElement) xcludesList.get(j);
693                        includes[j] = requireAttribute(xclude, "name");
694                    }
695                }
696                String[] excludes = null;
697                xcludesList = f.getChildrenNamed("exclude");
698                if (!xcludesList.isEmpty())
699                {
700                    excludes = new String[xcludesList.size()];
701                    for (int j = 0; j < xcludesList.size(); j++)
702                    {
703                        XMLElement xclude = (XMLElement) xcludesList.get(j);
704                        excludes[j] = requireAttribute(xclude, "name");
705                    }
706                }
707
708                // parse additional fileset attributes "includes" and "excludes"
709                String[] toDo = new String[] { "includes", "excludes"};
710                // use the existing containers filled from include and exclude
711                // and add the includes and excludes to it
712                String[][] containers = new String[][] { includes, excludes};
713                for (int j = 0; j < toDo.length; ++j)
714                {
715                    String inex = f.getAttribute(toDo[j]);
716                    if (inex != null && inex.length() > 0)
717                    { // This is the same "splitting" as ant PatternSet do ...
718                        StringTokenizer tok = new StringTokenizer(inex, ", ", false);
719                        int newSize = tok.countTokens();
720                        int k = 0;
721                        String[] nCont = null;
722                        if (containers[j] != null && containers[j].length > 0)
723                        { // old container exist; create a new which can hold
724                            // all values
725                            // and copy the old stuff to the front
726                            newSize += containers[j].length;
727                            nCont = new String[newSize];
728                            for (; k < containers[j].length; ++k)
729                                nCont[k] = containers[j][k];
730                        }
731                        if (nCont == null) // No container for old values
732                            // created,
733                            // create a new one.
734                            nCont = new String[newSize];
735                        for (; k < newSize; ++k)
736                            // Fill the new one or expand the existent container
737                            nCont[k] = tok.nextToken();
738                        containers[j] = nCont;
739                    }
740                }
741                includes = containers[0]; // push the new includes to the
742                // local var
743                excludes = containers[1]; // push the new excludes to the
744                // local var
745
746                // scan and add fileset
747                DirectoryScanner ds = new DirectoryScanner();
748                ds.setIncludes(includes);
749                ds.setExcludes(excludes);
750                if (defexcludes) ds.addDefaultExcludes();
751                ds.setBasedir(dir);
752                ds.setCaseSensitive(casesensitive);
753                ds.scan();
754
755                String[] files = ds.getIncludedFiles();
756                String[] dirs = ds.getIncludedDirectories();
757
758                // Directory scanner has done recursion, add files and
759                // directories
760                for (int i = 0; i < files.length; ++i)
761                {
762                    try
763                    {
764                        String target = new File(targetdir, files[i]).getPath();
765                        pack
766                                .addFile(new File(dir, files[i]), target, osList, override,
767                                        additionals);
768                    }
769                    catch (FileNotFoundException x)
770                    {
771                        parseError(f, x.getMessage(), x);
772                    }
773                }
774                for (int i = 0; i < dirs.length; ++i)
775                {
776                    try
777                    {
778                        String target = new File(targetdir, dirs[i]).getPath();
779                        pack.addFile(new File(dir, dirs[i]), target, osList, override, additionals);
780                    }
781                    catch (FileNotFoundException x)
782                    {
783                        parseError(f, x.getMessage(), x);
784                    }
785                }
786            }
787
788            // get the updatechecks list
789            iter = el.getChildrenNamed("updatecheck").iterator();
790            while (iter.hasNext())
791            {
792                XMLElement f = (XMLElement) iter.next();
793
794                String casesensitive = f.getAttribute("casesensitive");
795
796                // get includes and excludes
797                ArrayList includesList = new ArrayList();
798                ArrayList excludesList = new ArrayList();
799
800                // get includes and excludes
801                Iterator include_it = f.getChildrenNamed("include").iterator();
802                while (include_it.hasNext())
803                {
804                    XMLElement inc_el = (XMLElement) include_it.next();
805                    includesList.add(requireAttribute(inc_el, "name"));
806                }
807
808                Iterator exclude_it = f.getChildrenNamed("exclude").iterator();
809                while (exclude_it.hasNext())
810                {
811                    XMLElement excl_el = (XMLElement) exclude_it.next();
812                    excludesList.add(requireAttribute(excl_el, "name"));
813                }
814
815                pack.addUpdateCheck(new UpdateCheck(includesList, excludesList, casesensitive));
816            }
817            // We get the dependencies
818            iter = el.getChildrenNamed("depends").iterator();
819            while (iter.hasNext())
820            {
821                XMLElement dep = (XMLElement) iter.next();
822                String depName = requireAttribute(dep, "packname");
823                pack.addDependency(depName);
824
825            }
826
827            // We add the pack
828            compiler.addPack(pack);
829        }
830        compiler.checkDependencies();
831
832        notifyCompilerListener("addPacks", CompilerListener.END, data);
833    }
834
835    /**
836     * Checks whether the dependencies stated in the configuration file are correct. Specifically it
837     * checks that no pack point to a non existent pack and also that there are no circular
838     * dependencies in the packs.
839     */
840    public void checkDependencies(List packs) throws CompilerException
841    {
842        // Because we use package names in the configuration file we assosiate
843        // the names with the objects
844        Map names = new HashMap();
845        for (int i = 0; i < packs.size(); i++)
846        {
847            PackInfo pack = (PackInfo) packs.get(i);
848            names.put(pack.getPack().name, pack);
849        }
850        int result = dfs(packs, names);
851        // @todo More informative messages to include the source of the error
852        if (result == -2)
853            parseError("Circular dependency detected");
854        else if (result == -1) parseError("A dependency doesn't exist");
855    }
856
857    /**
858     * We use the dfs graph search algorithm to check whether the graph is acyclic as described in:
859     * Thomas H. Cormen, Charles Leiserson, Ronald Rivest and Clifford Stein. Introduction to
860     * algorithms 2nd Edition 540-549,MIT Press, 2001
861     * 
862     * @param packs The graph
863     * @param names The name map
864     */
865    private int dfs(List packs, Map names)
866    {
867        Map edges = new HashMap();
868        for (int i = 0; i < packs.size(); i++)
869        {
870            PackInfo pack = (PackInfo) packs.get(i);
871            if (pack.colour == PackInfo.WHITE)
872            {
873                if (dfsVisit(pack, names, edges) != 0) return -1;
874            }
875
876        }
877        return checkBackEdges(edges);
878    }
879
880    /**
881     * This function checks for the existence of back edges.
882     */
883    private int checkBackEdges(Map edges)
884    {
885        Set keys = edges.keySet();
886        for (Iterator iterator = keys.iterator(); iterator.hasNext();)
887        {
888            final Object key = iterator.next();
889            int color = ((Integer) edges.get(key)).intValue();
890            if (color == PackInfo.GREY) { return -2; }
891        }
892        return 0;
893
894    }
895
896    /**
897     * This class is used for the classification of the edges
898     */
899    private class Edge
900    {
901
902        PackInfo u;
903
904        PackInfo v;
905
906        Edge(PackInfo u, PackInfo v)
907        {
908            this.u = u;
909            this.v = v;
910        }
911    }
912
913    private int dfsVisit(PackInfo u, Map names, Map edges)
914    {
915        u.colour = PackInfo.GREY;
916        List deps = u.getDependencies();
917        if (deps != null)
918        {
919            for (int i = 0; i < deps.size(); i++)
920            {
921                String name = (String) deps.get(i);
922                PackInfo v = (PackInfo) names.get(name);
923                if (v == null)
924                {
925                    System.out.println("Failed to find dependency: "+name);
926                    return -1;
927                }
928                Edge edge = new Edge(u, v);
929                if (edges.get(edge) == null) edges.put(edge, new Integer(v.colour));
930
931                if (v.colour == PackInfo.WHITE)
932                {
933
934                    final int result = dfsVisit(v, names, edges);
935                    if (result != 0) return result;
936                }
937            }
938        }
939        u.colour = PackInfo.BLACK;
940        return 0;
941    }
942
943    /**
944     * Recursive method to add files in a pack.
945     * 
946     * @param file The file to add.
947     * @param targetdir The relative path to the parent.
948     * @param osList The target OS constraints.
949     * @param override Overriding behaviour.
950     * @param pack Pack to be packed into
951     * @param additionals Map which contains additional data
952     * @exception FileNotFoundException if the file does not exist
953     */
954    protected void addRecursively(File file, String targetdir, List osList, int override,
955            PackInfo pack, Map additionals) throws IOException
956    {
957        String targetfile = targetdir + "/" + file.getName();
958        if (!file.isDirectory())
959            pack.addFile(file, targetfile, osList, override, additionals);
960        else
961        {
962            File[] files = file.listFiles();
963            if (files.length == 0) // The directory is empty so must be added
964                pack.addFile(file, targetfile, osList, override, additionals);
965            else
966            {
967                // new targetdir = targetfile;
968                for (int i = 0; i < files.length; i++)
969                    addRecursively(files[i], targetfile, osList, override, pack, additionals);
970            }
971        }
972    }
973
974    /**
975     * Parse panels and their paramters, locate the panels resources and add to the Packager.
976     * 
977     * @param data The XML data.
978     * @exception CompilerException Description of the Exception
979     */
980    protected void addPanels(XMLElement data) throws CompilerException
981    {
982        notifyCompilerListener("addPanels", CompilerListener.BEGIN, data);
983        XMLElement root = requireChildNamed(data, "panels");
984
985        // at least one panel is required
986        Vector panels = root.getChildrenNamed("panel");
987        if (panels.isEmpty()) parseError(root, "<panels> requires a <panel>");
988
989        // We process each panel markup
990        Iterator iter = panels.iterator();
991        while (iter.hasNext())
992        {
993            XMLElement xmlPanel = (XMLElement) iter.next();
994
995            // create the serialized Panel data
996            Panel panel = new Panel();
997            panel.osConstraints = OsConstraint.getOsList(xmlPanel);
998            String className = xmlPanel.getAttribute("classname");
999
1000            // Panel files come in jars packaged w/ IzPack
1001            String jarPath = "bin/panels/" + className + ".jar";
1002            URL url = findIzPackResource(jarPath, "Panel jar file", xmlPanel);
1003            String fullClassName = null;
1004            try
1005            {
1006                fullClassName = getFullClassName(url, className);
1007            }
1008            catch (Exception e)
1009            {
1010                ;
1011            }
1012            if (fullClassName != null)
1013                panel.className = fullClassName;
1014            else
1015                panel.className = className;
1016            // insert into the packager
1017            compiler.addPanelJar(panel, url);
1018        }
1019        notifyCompilerListener("addPanels", CompilerListener.END, data);
1020    }
1021
1022    /**
1023     * Adds the resources.
1024     * 
1025     * @param data The XML data.
1026     * @exception CompilerException Description of the Exception
1027     */
1028    protected void addResources(XMLElement data) throws CompilerException
1029    {
1030        notifyCompilerListener("addResources", CompilerListener.BEGIN, data);
1031        XMLElement root = data.getFirstChildNamed("resources");
1032        if (root == null) return;
1033
1034        // We process each res markup
1035        Iterator iter = root.getChildrenNamed("res").iterator();
1036        while (iter.hasNext())
1037        {
1038            XMLElement res = (XMLElement) iter.next();
1039            String id = requireAttribute(res, "id");
1040            String src = requireAttribute(res, "src");
1041            boolean parse = validateYesNoAttribute(res, "parse", NO);
1042
1043            // basedir is not prepended if src is already an absolute path
1044            URL url = findProjectResource(src, "Resource", res);
1045
1046            // substitute variable values in the resource if parsed
1047            if (parse)
1048            {
1049                if (compiler.getVariables().isEmpty())
1050                {
1051                    parseWarn(res, "No variables defined. " + url.getPath() + " not parsed.");
1052                }
1053                else
1054                {
1055                    String type = res.getAttribute("type");
1056                    String encoding = res.getAttribute("encoding");
1057                    File parsedFile = null;
1058
1059                    try
1060                    {
1061                        // make the substitutions into a temp file
1062                        InputStream bin = new BufferedInputStream(url.openStream());
1063
1064                        parsedFile = File.createTempFile("izpp", null);
1065                        parsedFile.deleteOnExit();
1066                        FileOutputStream outFile = new FileOutputStream(parsedFile);
1067                        BufferedOutputStream bout = new BufferedOutputStream(outFile);
1068
1069                        VariableSubstitutor vs = new VariableSubstitutor(compiler.getVariables());
1070                        vs.substitute(bin, bout, type, encoding);
1071                        bin.close();
1072                        bout.close();
1073
1074                        // and specify the substituted file to be added to the
1075                        // packager
1076                        url = parsedFile.toURL();
1077                    }
1078                    catch (IOException x)
1079                    {
1080                        parseError(res, x.getMessage(), x);
1081                    }
1082                }
1083            }
1084
1085            compiler.addResource(id, url);
1086        }
1087        notifyCompilerListener("addResources", CompilerListener.END, data);
1088    }
1089
1090    /**
1091     * Adds the ISO3 codes of the langpacks and associated resources.
1092     * 
1093     * @param data The XML data.
1094     * @exception CompilerException Description of the Exception
1095     */
1096    protected void addLangpacks(XMLElement data) throws CompilerException
1097    {
1098        notifyCompilerListener("addLangpacks", CompilerListener.BEGIN, data);
1099        XMLElement root = requireChildNamed(data, "locale");
1100
1101        // at least one langpack is required
1102        Vector locals = root.getChildrenNamed("langpack");
1103        if (locals.isEmpty()) parseError(root, "<locale> requires a <langpack>");
1104
1105        // We process each langpack markup
1106        Iterator iter = locals.iterator();
1107        while (iter.hasNext())
1108        {
1109            XMLElement el = (XMLElement) iter.next();
1110            String iso3 = requireAttribute(el, "iso3");
1111            String path;
1112
1113            path = "bin/langpacks/installer/" + iso3 + ".xml";
1114            URL iso3xmlURL = findIzPackResource(path, "ISO3 file", el);
1115
1116            path = "bin/langpacks/flags/" + iso3 + ".gif";
1117            URL iso3FlagURL = findIzPackResource(path, "ISO3 flag image", el);
1118
1119            compiler.addLangPack(iso3, iso3xmlURL, iso3FlagURL);
1120        }
1121        notifyCompilerListener("addLangpacks", CompilerListener.END, data);
1122    }
1123
1124    /**
1125     * Builds the Info class from the XML tree.
1126     * 
1127     * @param data The XML data. return The Info.
1128     * @exception Exception Description of the Exception
1129     */
1130    protected void addInfo(XMLElement data) throws Exception
1131    {
1132        notifyCompilerListener("addInfo", CompilerListener.BEGIN, data);
1133        // Initialisation
1134        XMLElement root = requireChildNamed(data, "info");
1135
1136        Info info = new Info();
1137        info.setAppName(requireContent(requireChildNamed(root, "appname")));
1138        info.setAppVersion(requireContent(requireChildNamed(root, "appversion")));
1139        // We get the installation subpath
1140        XMLElement subpath = root.getFirstChildNamed("appsubpath");
1141        if (subpath != null)
1142        {
1143            info.setInstallationSubPath(requireContent(subpath));
1144        }
1145
1146        // validate and insert app URL
1147        final XMLElement URLElem = root.getFirstChildNamed("url");
1148        if (URLElem != null)
1149        {
1150            URL appURL = requireURLContent(URLElem);
1151            info.setAppURL(appURL.toString());
1152        }
1153
1154        // We get the authors list
1155        XMLElement authors = root.getFirstChildNamed("authors");
1156        if (authors != null)
1157        {
1158            Iterator iter = authors.getChildrenNamed("author").iterator();
1159            while (iter.hasNext())
1160            {
1161                XMLElement author = (XMLElement) iter.next();
1162                String name = requireAttribute(author, "name");
1163                String email = requireAttribute(author, "email");
1164                info.addAuthor(new Info.Author(name, email));
1165            }
1166        }
1167
1168        // We get the java version required
1169        XMLElement javaVersion = root.getFirstChildNamed("javaversion");
1170        if (javaVersion != null) info.setJavaVersion(requireContent(javaVersion));
1171
1172        // validate and insert (and require if -web kind) web dir
1173        XMLElement webDirURL = root.getFirstChildNamed("webdir");
1174        if (webDirURL != null) info.setWebDirURL(requireURLContent(webDirURL).toString());
1175        String kind = compiler.getKind();
1176        if (kind != null)
1177        {
1178            if (kind.equalsIgnoreCase(WEB) && webDirURL == null)
1179            {
1180                parseError(root, "<webdir> required when \"WEB\" installer requested");
1181            }
1182            else if (kind.equalsIgnoreCase(STANDARD) && webDirURL != null)
1183            {
1184                // Need a Warning? parseWarn(webDirURL, "Not creating web
1185                // installer.");
1186                info.setWebDirURL(null);
1187            }
1188        }
1189
1190        // Add the uninstaller as a resource if specified
1191        XMLElement uninstallInfo = root.getFirstChildNamed("uninstaller");
1192        if (validateYesNoAttribute(uninstallInfo, "write", YES))
1193        {
1194            URL url = findIzPackResource("lib/uninstaller.jar", "Uninstaller", root);
1195            compiler.addResource("IzPack.uninstaller", url);
1196
1197            if (uninstallInfo != null)
1198            {
1199                String uninstallerName = uninstallInfo.getAttribute("name");
1200                if (uninstallerName != null && uninstallerName.endsWith(".jar")
1201                        && uninstallerName.length() > ".jar".length())
1202                    info.setUninstallerName(uninstallerName);
1203            }
1204        }
1205        // Add the path for the summary log file if specified
1206        XMLElement slfPath = root.getFirstChildNamed("summarylogfilepath");
1207        if (slfPath != null) info.setSummaryLogFilePath(requireContent(slfPath));
1208
1209        compiler.setInfo(info);
1210        notifyCompilerListener("addInfo", CompilerListener.END, data);
1211    }
1212
1213    /**
1214     * Variable declaration is a fragment of the xml file. For example:
1215     * 
1216     * <pre>
1217     * 
1218     *  
1219     *   
1220     *    
1221     *        &lt;variables&gt;
1222     *          &lt;variable name=&quot;nom&quot; value=&quot;value&quot;/&gt;
1223     *          &lt;variable name=&quot;foo&quot; value=&quot;pippo&quot;/&gt;
1224     *        &lt;/variables&gt;
1225     *      
1226     *    
1227     *   
1228     *  
1229     * </pre>
1230     * 
1231     * variable declared in this can be referred to in parsable files.
1232     * 
1233     * @param data The XML data.
1234     * @exception CompilerException Description of the Exception
1235     */
1236    protected void addVariables(XMLElement data) throws CompilerException
1237    {
1238        notifyCompilerListener("addVariables", CompilerListener.BEGIN, data);
1239        // We get the varible list
1240        XMLElement root = data.getFirstChildNamed("variables");
1241        if (root == null) return;
1242
1243        Properties variables = compiler.getVariables();
1244
1245        Iterator iter = root.getChildrenNamed("variable").iterator();
1246        while (iter.hasNext())
1247        {
1248            XMLElement var = (XMLElement) iter.next();
1249            String name = requireAttribute(var, "name");
1250            String value = requireAttribute(var, "value");
1251            if (variables.contains(name))
1252                parseWarn(var, "Variable '" + name + "' being overwritten");
1253            variables.setProperty(name, value);
1254        }
1255        notifyCompilerListener("addVariables", CompilerListener.END, data);
1256    }
1257
1258    /**
1259     * Properties declaration is a fragment of the xml file. For example:
1260     * 
1261     * <pre>
1262     * 
1263     *  
1264     *   
1265     *    
1266     *        &lt;properties&gt;
1267     *          &lt;property name=&quot;app.name&quot; value=&quot;Property Laden Installer&quot;/&gt;
1268     *          &lt;!-- Ant styles 'location' and 'refid' are not yet supported --&gt;
1269     *          &lt;property file=&quot;filename-relative-to-install?&quot;/&gt;
1270     *          &lt;property file=&quot;filename-relative-to-install?&quot; prefix=&quot;prefix&quot;/&gt;
1271     *          &lt;!-- Ant style 'url' and 'resource' are not yet supported --&gt;
1272     *          &lt;property environment=&quot;prefix&quot;/&gt;
1273     *        &lt;/properties&gt;
1274     *      
1275     *    
1276     *   
1277     *  
1278     * </pre>
1279     * 
1280     * variable declared in this can be referred to in parsable files.
1281     * 
1282     * @param data The XML data.
1283     * @exception CompilerException Description of the Exception
1284     */
1285    protected void substituteProperties(XMLElement data) throws CompilerException
1286    {
1287        notifyCompilerListener("substituteProperties", CompilerListener.BEGIN, data);
1288
1289        XMLElement root = data.getFirstChildNamed("properties");
1290        if (root != null)
1291        {
1292            // add individual properties
1293            Iterator iter = root.getChildrenNamed("property").iterator();
1294            while (iter.hasNext())
1295            {
1296                XMLElement prop = (XMLElement) iter.next();
1297                Property property = new Property(prop, this);
1298                property.execute();
1299            }
1300        }
1301
1302        // temporarily remove the 'properties' branch, replace all properties in
1303        // the remaining DOM, and replace properties branch.
1304        // TODO: enhance XMLElement with an "indexOf(XMLElement)" method
1305        // and addChild(XMLElement, int) so returns to the same place.
1306        if (root != null) data.removeChild(root);
1307
1308        substituteAllProperties(data);
1309        if (root != null) data.addChild(root);
1310
1311        notifyCompilerListener("substituteProperties", CompilerListener.END, data);
1312    }
1313
1314    /**
1315     * Perform recursive substitution on all properties
1316     */
1317    protected void substituteAllProperties(XMLElement element) throws CompilerException
1318    {
1319        Enumeration attributes = element.enumerateAttributeNames();
1320        while (attributes.hasMoreElements())
1321        {
1322            String name = (String) attributes.nextElement();
1323            String value = compiler.replaceProperties(element.getAttribute(name));
1324            element.setAttribute(name, value);
1325        }
1326
1327        String content = element.getContent();
1328        if (content != null)
1329        {
1330            element.setContent(compiler.replaceProperties(content));
1331        }
1332
1333        Enumeration children = element.enumerateChildren();
1334        while (children.hasMoreElements())
1335        {
1336            XMLElement child = (XMLElement) children.nextElement();
1337            substituteAllProperties(child);
1338        }
1339    }
1340
1341    /**
1342     * Returns the XMLElement representing the installation XML file.
1343     * 
1344     * @return The XML tree.
1345     * @exception CompilerException For problems with the installation file
1346     * @exception IOException for errors reading the installation file
1347     */
1348    protected XMLElement getXMLTree() throws CompilerException, IOException
1349    {
1350        // Initialises the parser
1351        IXMLReader reader = null;
1352        if( filename != null )
1353        {
1354            File file = new File(filename).getAbsoluteFile();
1355            if (!file.canRead()) throw new CompilerException("Invalid file: " + file);
1356            reader = new StdXMLReader(new FileInputStream(filename));
1357            // add izpack built in property
1358            compiler.setProperty("izpack.file", file.toString());
1359        }
1360        else if( installText != null )
1361        {
1362            reader = StdXMLReader.stringReader(installText);
1363        }
1364        else
1365        {
1366            throw new CompilerException("Neither install file or text specified");
1367        }
1368
1369        StdXMLParser parser = new StdXMLParser();
1370        parser.setBuilder(new StdXMLBuilder());
1371        parser.setReader(reader);
1372        parser.setValidator(new NonValidator());
1373
1374        // We get it
1375        XMLElement data = null;
1376        try
1377        {
1378            data = (XMLElement) parser.parse();
1379        }
1380        catch (Exception x)
1381        {
1382            throw new CompilerException("Error parsing installation file", x);
1383        }
1384
1385        // We check it
1386        if (!"installation".equalsIgnoreCase(data.getName()))
1387            parseError(data, "this is not an IzPack XML installation file");
1388        if (!requireAttribute(data, "version").equalsIgnoreCase(VERSION))
1389            parseError(data, "the file version is different from the compiler version");
1390
1391        // We finally return the tree
1392        return data;
1393    }
1394
1395    protected int getOverrideValue(XMLElement f) throws CompilerException
1396    {
1397        int override = PackFile.OVERRIDE_UPDATE;
1398
1399        String override_val = f.getAttribute("override");
1400        if (override_val != null)
1401        {
1402            if (override_val.equalsIgnoreCase("true"))
1403            {
1404                override = PackFile.OVERRIDE_TRUE;
1405            }
1406            else if (override_val.equalsIgnoreCase("false"))
1407            {
1408                override = PackFile.OVERRIDE_FALSE;
1409            }
1410            else if (override_val.equalsIgnoreCase("asktrue"))
1411            {
1412                override = PackFile.OVERRIDE_ASK_TRUE;
1413            }
1414            else if (override_val.equalsIgnoreCase("askfalse"))
1415            {
1416                override = PackFile.OVERRIDE_ASK_FALSE;
1417            }
1418            else if (override_val.equalsIgnoreCase("update"))
1419            {
1420                override = PackFile.OVERRIDE_UPDATE;
1421            }
1422            else
1423                parseError(f, "invalid value for attribute \"override\"");
1424        }
1425
1426        return override;
1427    }
1428
1429    /**
1430     * Look for a project specified resources, which, if not absolute, are sought relative to the
1431     * projects basedir. The path should use '/' as the fileSeparator. If the resource is not found,
1432     * a CompilerException is thrown indicating fault in the parent element.
1433     * 
1434     * @param path the relative path (using '/' as separator) to the resource.
1435     * @param desc the description of the resource used to report errors
1436     * @param parent the XMLElement the resource is specified in, used to report errors
1437     * @return a URL to the resource.
1438     */
1439    private URL findProjectResource(String path, String desc, XMLElement parent)
1440            throws CompilerException
1441    {
1442        URL url = null;
1443        File resource = new File(path);
1444        if (!resource.isAbsolute()) resource = new File(basedir, path);
1445
1446        if (!resource.exists()) // fatal
1447            parseError(parent, desc + " not found: " + resource);
1448
1449        try
1450        {
1451            url = resource.toURL();
1452        }
1453        catch (MalformedURLException how)
1454        {
1455            parseError(parent, desc + "(" + resource + ")", how);
1456        }
1457
1458        return url;
1459    }
1460
1461    /**
1462     * Look for an IzPack resource either in the compiler jar, or within IZPACK_HOME. The path must
1463     * not be absolute. The path must use '/' as the fileSeparator (it's used to access the jar
1464     * file). If the resource is not found, a CompilerException is thrown indicating fault in the
1465     * parent element.
1466     * 
1467     * @param path the relative path (using '/' as separator) to the resource.
1468     * @param desc the description of the resource used to report errors
1469     * @param parent the XMLElement the resource is specified in, used to report errors
1470     * @return a URL to the resource.
1471     */
1472    private URL findIzPackResource(String path, String desc, XMLElement parent)
1473            throws CompilerException
1474    {
1475        URL url = getClass().getResource("/" + path);
1476        if (url == null)
1477        {
1478            File resource = new File(path);
1479
1480            if (!resource.isAbsolute()) resource = new File(Compiler.IZPACK_HOME, path);
1481
1482            if (!resource.exists()) // fatal
1483                parseError(parent, desc + " not found: " + resource);
1484
1485            try
1486            {
1487                url = resource.toURL();
1488            }
1489            catch (MalformedURLException how)
1490            {
1491                parseError(parent, desc + "(" + resource + ")", how);
1492            }
1493        }
1494
1495        return url;
1496    }
1497
1498    /**
1499     * Create parse error with consistent messages. Includes file name. For use When parent is
1500     * unknown.
1501     * 
1502     * @param message Brief message explaining error
1503     */
1504    protected void parseError(String message) throws CompilerException
1505    {
1506        throw new CompilerException(filename + ":" + message);
1507    }
1508
1509    /**
1510     * Create parse error with consistent messages. Includes file name and line # of parent. It is
1511     * an error for 'parent' to be null.
1512     * 
1513     * @param parent The element in which the error occured
1514     * @param message Brief message explaining error
1515     */
1516    protected void parseError(XMLElement parent, String message) throws CompilerException
1517    {
1518        throw new CompilerException(filename + ":" + parent.getLineNr() + ": " + message);
1519    }
1520
1521    /**
1522     * Create a chained parse error with consistent messages. Includes file name and line # of
1523     * parent. It is an error for 'parent' to be null.
1524     * 
1525     * @param parent The element in which the error occured
1526     * @param message Brief message explaining error
1527     */
1528    protected void parseError(XMLElement parent, String message, Throwable cause)
1529            throws CompilerException
1530    {
1531        throw new CompilerException(filename + ":" + parent.getLineNr() + ": " + message, cause);
1532    }
1533
1534    /**
1535     * Create a parse warning with consistent messages. Includes file name and line # of parent. It
1536     * is an error for 'parent' to be null.
1537     * 
1538     * @param parent The element in which the warning occured
1539     * @param message Warning message
1540     */
1541    protected void parseWarn(XMLElement parent, String message)
1542    {
1543        System.out.println(filename + ":" + parent.getLineNr() + ": " + message);
1544    }
1545
1546    /**
1547     * Call getFirstChildNamed on the parent, producing a meaningful error message on failure. It is
1548     * an error for 'parent' to be null.
1549     * 
1550     * @param parent The element to search for a child
1551     * @param name Name of the child element to get
1552     */
1553    protected XMLElement requireChildNamed(XMLElement parent, String name) throws CompilerException
1554    {
1555        XMLElement child = parent.getFirstChildNamed(name);
1556        if (child == null)
1557            parseError(parent, "<" + parent.getName() + "> requires child <" + name + ">");
1558        return child;
1559    }
1560
1561    /**
1562     * Call getContent on an element, producing a meaningful error message if not present, or empty,
1563     * or a valid URL. It is an error for 'element' to be null.
1564     * 
1565     * @param element The element to get content of
1566     */
1567    protected URL requireURLContent(XMLElement element) throws CompilerException
1568    {
1569        URL url = null;
1570        try
1571        {
1572            url = new URL(requireContent(element));
1573        }
1574        catch (MalformedURLException x)
1575        {
1576            parseError(element, "<" + element.getName() + "> requires valid URL", x);
1577        }
1578        return url;
1579    }
1580
1581    /**
1582     * Call getContent on an element, producing a meaningful error message if not present, or empty.
1583     * It is an error for 'element' to be null.
1584     * 
1585     * @param element The element to get content of
1586     */
1587    protected String requireContent(XMLElement element) throws CompilerException
1588    {
1589        String content = element.getContent();
1590        if (content == null || content.length() == 0)
1591            parseError(element, "<" + element.getName() + "> requires content");
1592        return content;
1593    }
1594
1595    /**
1596     * Call getAttribute on an element, producing a meaningful error message if not present, or
1597     * empty. It is an error for 'element' or 'attribute' to be null.
1598     * 
1599     * @param element The element to get the attribute value of
1600     * @param attribute The name of the attribute to get
1601     */
1602    protected String requireAttribute(XMLElement element, String attribute)
1603            throws CompilerException
1604    {
1605        String value = element.getAttribute(attribute);
1606        if (value == null)
1607            parseError(element, "<" + element.getName() + "> requires attribute '" + attribute
1608                    + "'");
1609        return value;
1610    }
1611
1612    /**
1613     * Get a required attribute of an element, ensuring it is an integer. A meaningful error message
1614     * is generated as a CompilerException if not present or parseable as an int. It is an error for
1615     * 'element' or 'attribute' to be null.
1616     * 
1617     * @param element The element to get the attribute value of
1618     * @param attribute The name of the attribute to get
1619     */
1620    protected int requireIntAttribute(XMLElement element, String attribute)
1621            throws CompilerException
1622    {
1623        String value = element.getAttribute(attribute);
1624        if (value == null || value.length() == 0)
1625            parseError(element, "<" + element.getName() + "> requires attribute '" + attribute
1626                    + "'");
1627        try
1628        {
1629            return Integer.parseInt(value);
1630        }
1631        catch (NumberFormatException x)
1632        {
1633            parseError(element, "'" + attribute + "' must be an integer");
1634        }
1635        return 0; // never happens
1636    }
1637
1638    /**
1639     * Call getAttribute on an element, producing a meaningful error message if not present, or one
1640     * of "yes" or "no". It is an error for 'element' or 'attribute' to be null.
1641     * 
1642     * @param element The element to get the attribute value of
1643     * @param attribute The name of the attribute to get
1644     */
1645    protected boolean requireYesNoAttribute(XMLElement element, String attribute)
1646            throws CompilerException
1647    {
1648        String value = requireAttribute(element, attribute);
1649        if (value.equalsIgnoreCase("yes")) return true;
1650        if (value.equalsIgnoreCase("no")) return false;
1651
1652        parseError(element, "<" + element.getName() + "> invalid attribute '" + attribute
1653                + "': Expected (yes|no)");
1654
1655        return false; // never happens
1656    }
1657
1658    /**
1659     * Call getAttribute on an element, producing a meaningful warning if not "yes" or "no". If the
1660     * 'element' or 'attribute' are null, the default value is returned.
1661     * 
1662     * @param element The element to get the attribute value of
1663     * @param attribute The name of the attribute to get
1664     * @param defaultValue Value returned if attribute not present or invalid
1665     */
1666    protected boolean validateYesNoAttribute(XMLElement element, String attribute,
1667            boolean defaultValue)
1668    {
1669        if (element == null) return defaultValue;
1670
1671        String value = element.getAttribute(attribute, (defaultValue ? "yes" : "no"));
1672        if (value.equalsIgnoreCase("yes")) return true;
1673        if (value.equalsIgnoreCase("no")) return false;
1674
1675        // TODO: should this be an error if it's present but "none of the
1676        // above"?
1677        parseWarn(element, "<" + element.getName() + "> invalid attribute '" + attribute
1678                + "': Expected (yes|no) if present");
1679
1680        return defaultValue;
1681    }
1682
1683    /**
1684     * The main method if the compiler is invoked by a command-line call.
1685     * 
1686     * @param args The arguments passed on the command-line.
1687     */
1688    public static void main(String[] args)
1689    {
1690        // Outputs some informations
1691        System.out.println("");
1692        System.out.println(".::  IzPack - Version " + Compiler.IZPACK_VERSION + " ::.");
1693        System.out.println("");
1694        System.out.println("< compiler specifications version : " + VERSION + " >");
1695        System.out.println("");
1696        System.out.println("- Copyright (C) 2001-2005 Julien Ponge");
1697        System.out.println("- Visit http://www.izforge.com/ for the latests releases");
1698        System.out.println("- Released under the terms of the Apache Software License version 2.0.");
1699        System.out.println("");
1700
1701        // exit code 1 means: error
1702        int exitCode = 1;
1703
1704        // We analyse the command line parameters
1705        try
1706        {
1707            // Our arguments
1708            String filename;
1709            String base = ".";
1710            String kind = "standard";
1711            String output;
1712            String compr_format = "default";
1713            int compr_level = -1;
1714
1715            // First check
1716            int nArgs = args.length;
1717            if (nArgs < 3) throw new Exception("no arguments given");
1718
1719            // We get the IzPack home directory
1720            int stdArgsIndex;
1721            String home = ".";
1722            if (args[0].equalsIgnoreCase("-HOME"))
1723            {
1724                stdArgsIndex = 2;
1725                home = args[1];
1726            }
1727            else
1728            {
1729                stdArgsIndex = 0;
1730                String izHome = System.getProperty("IZPACK_HOME");
1731                if (izHome != null) home = izHome;
1732            }
1733
1734            File homeFile = new File(home);
1735            if (!homeFile.exists() && homeFile.isDirectory())
1736            {
1737                System.err.println("IZPACK_HOME (" + home + ") doesn't exist");
1738                System.exit(-1);
1739            }
1740            Compiler.setIzpackHome(home);
1741
1742            // The users wants to know the command line parameters
1743            if (args[stdArgsIndex].equalsIgnoreCase("-?"))
1744            {
1745                System.out.println("-> Command line parameters are : (xml file) [args]");
1746                System.out.println("   (xml file): the xml file describing the installation");
1747                System.out
1748                        .println("   -b (base) : indicates the base path that the compiler will use for filenames");
1749                System.out.println("               default is the current path");
1750                System.out.println("   -k (kind) : indicates the kind of installer to generate");
1751                System.out.println("               default is standard");
1752                System.out.println("   -o (out)  : indicates the output file name");
1753                System.out.println("               default is the xml file name\n");
1754                System.out.println("   -c (compression)  : indicates the compression format to be used for packs");
1755                System.out.println("               default is the internal deflate compression\n");
1756                System.out.println("   -l (compression-level)  : indicates the level for the used compression format");
1757                System.out.println("                if supported. Only integer are valid\n");
1758
1759                System.out
1760                        .println("   When using vm option -DSTACKTRACE=true there is all kind of debug info ");
1761                System.out.println("");
1762            }
1763            else
1764            {
1765                // We can parse the other parameters & try to compile the
1766                // installation
1767
1768                // We get the input file name and we initialize the output file
1769                // name
1770                filename = args[stdArgsIndex];
1771                // default jar files names are based on input file name
1772                output = filename.substring(0, filename.length() - 3) + "jar";
1773
1774                // We parse the other ones
1775                int pos = stdArgsIndex + 1;
1776                while (pos < nArgs)
1777                    if ((args[pos].startsWith("-")) && (args[pos].length() == 2))
1778                    {
1779                        switch (args[pos].toLowerCase().charAt(1))
1780                        {
1781                        case 'b':
1782                            if ((pos + 1) < nArgs)
1783                            {
1784                                pos++;
1785                                base = args[pos];
1786                            }
1787                            else
1788                                throw new Exception("base argument missing");
1789                            break;
1790                        case 'k':
1791                            if ((pos + 1) < nArgs)
1792                            {
1793                                pos++;
1794                                kind = args[pos];
1795                            }
1796                            else
1797                                throw new Exception("kind argument missing");
1798                            break;
1799                        case 'o':
1800                            if ((pos + 1) < nArgs)
1801                            {
1802                                pos++;
1803                                output = args[pos];
1804                            }
1805                            else
1806                                throw new Exception("output argument missing");
1807                            break;
1808                        case 'c':
1809                            if ((pos + 1) < nArgs)
1810                            {
1811                                pos++;
1812                                compr_format = args[pos];
1813                            }
1814                            else
1815                                throw new Exception("compression format argument missing");
1816                            break;
1817                        case 'l':
1818                            if ((pos + 1) < nArgs)
1819                            {
1820                                pos++;
1821                                compr_level = Integer.parseInt(args[pos]);
1822                            }
1823                            else
1824                                throw new Exception("compression level argument missing");
1825                            break;
1826                        default:
1827                            throw new Exception("unknown argument");
1828                        }
1829                        pos++;
1830                    }
1831                    else
1832                        throw new Exception("bad argument");
1833
1834                // Outputs what we are going to do
1835                System.out.println("-> Processing  : " + filename);
1836                System.out.println("-> Output      : " + output);
1837                System.out.println("-> Base path   : " + base);
1838                System.out.println("-> Kind        : " + kind);
1839                System.out.println("-> Compression : " + compr_format);
1840                System.out.println("-> Compr. level: " + compr_level);
1841                System.out.println("");
1842
1843                // Calls the compiler
1844                CmdlinePackagerListener listener = new CmdlinePackagerListener();
1845                CompilerConfig compiler = new CompilerConfig(filename, base, kind, output, 
1846                        compr_format, compr_level, listener, (String) null);
1847                compiler.executeCompiler();
1848
1849                // Waits
1850                while (compiler.isAlive())
1851                    Thread.sleep(100);
1852
1853                if (compiler.wasSuccessful()) exitCode = 0;
1854
1855                System.out.println("Build time: " + new Date());
1856            }
1857        }
1858        catch (Exception err)
1859        {
1860            // Something bad has happened
1861            System.err.println("-> Fatal error :");
1862            System.err.println("   " + err.getMessage());
1863            err.printStackTrace();
1864            System.err.println("");
1865            System.err.println("(tip : use -? to get the commmand line parameters)");
1866        }
1867
1868        // Closes the JVM
1869        System.exit(exitCode);
1870    }
1871
1872    // -------------------------------------------------------------------------
1873    // ------------- Listener stuff ------------------------- START ------------
1874
1875    /**
1876     * This method parses install.xml for defined listeners and put them in the right position. If
1877     * posible, the listeners will be validated. Listener declaration is a fragmention in
1878     * install.xml like : <listeners> <listener compiler="PermissionCompilerListener"
1879     * installer="PermissionInstallerListener"/> </<listeners>
1880     * 
1881     * @param data the XML data
1882     * @exception Exception Description of the Exception
1883     */
1884    private void addCustomListeners(XMLElement data) throws Exception
1885    {
1886        // We get the listeners
1887        XMLElement root = data.getFirstChildNamed("listeners");
1888        if (root == null) return;
1889
1890        Iterator iter = root.getChildrenNamed("listener").iterator();
1891        while (iter.hasNext())
1892        {
1893            XMLElement xmlAction = (XMLElement) iter.next();
1894            Object[] listener = getCompilerListenerInstance(xmlAction);
1895            if (listener != null)
1896                addCompilerListener((CompilerListener) listener[0]);
1897            String[] typeNames = new String[] { "installer", "uninstaller"};
1898            int[] types = new int[] { CustomData.INSTALLER_LISTENER,
1899                    CustomData.UNINSTALLER_LISTENER};
1900            for (int i = 0; i < typeNames.length; ++i)
1901            {
1902                String className = xmlAction.getAttribute(typeNames[i]);
1903                if (className != null)
1904                {
1905                    // Check for a jar attribute on the listener
1906                    String jarPath = xmlAction.getAttribute("jar");
1907                    jarPath = compiler.replaceProperties(jarPath);
1908                    if( jarPath == null )
1909                        jarPath = "bin/customActions/" + className + ".jar";
1910                    List constraints = OsConstraint.getOsList(xmlAction);
1911                    compiler.addCustomListener(types[i], className, jarPath, constraints);
1912                }
1913            }
1914        }
1915
1916    }
1917
1918    /**
1919     * Returns a list which contains the pathes of all files which are included in the given url.
1920     * This method expects as the url param a jar.
1921     * 
1922     * @param url url of the jar file
1923     * @return full qualified paths of the contained files
1924     * @throws Exception
1925     */
1926    private List getContainedFilePaths(URL url) throws Exception
1927    {
1928        JarInputStream jis = new JarInputStream(url.openStream());
1929        ZipEntry zentry = null;
1930        ArrayList fullNames = new ArrayList();
1931        while ((zentry = jis.getNextEntry()) != null)
1932        {
1933            String name = zentry.getName();
1934            // Add only files, no directory entries.
1935            if (!zentry.isDirectory()) fullNames.add(name);
1936        }
1937        jis.close();
1938        return (fullNames);
1939    }
1940
1941    /**
1942     * Returns the qualified class name for the given class. This method expects as the url param a
1943     * jar file which contains the given class. It scans the zip entries of the jar file.
1944     * 
1945     * @param url url of the jar file which contains the class
1946     * @param className short name of the class for which the full name should be resolved
1947     * @return full qualified class name
1948     * @throws Exception
1949     */
1950    private String getFullClassName(URL url, String className) throws Exception
1951    {
1952        JarInputStream jis = new JarInputStream(url.openStream());
1953        ZipEntry zentry = null;
1954        while ((zentry = jis.getNextEntry()) != null)
1955        {
1956            String name = zentry.getName();
1957            int lastPos = name.lastIndexOf(".class");
1958            if (lastPos < 0)
1959            {
1960                continue; // No class file.
1961            }
1962            name = name.replace('/', '.');
1963            int pos = -1;
1964            if (className != null)
1965            {
1966                pos = name.indexOf(className);
1967            }
1968            if (name.length() == pos + className.length() + 6) // "Main" class
1969            // found
1970            {
1971                jis.close();
1972                return (name.substring(0, lastPos));
1973            }
1974        }
1975        jis.close();
1976        return (null);
1977    }
1978
1979    /**
1980     * Returns the compiler listener which is defined in the xml element. As
1981     * xml element a "listner" node will be expected. Additional it is expected,
1982     * that either "findIzPackResource" returns an url based on
1983     * "bin/customActions/[className].jar", or that the listener element has
1984     * a jar attribute specifying the listener jar path. The class will be
1985     * loaded via an URLClassLoader.
1986     * 
1987     * @param var the xml element of the "listener" node
1988     * @return instance of the defined compiler listener
1989     * @throws Exception
1990     */
1991    private Object[] getCompilerListenerInstance(XMLElement var) throws Exception
1992    {
1993        String className = var.getAttribute("compiler");
1994        Class listener = null;
1995        Object instance = null;
1996        if (className == null) return (null);
1997
1998        // CustomAction files come in jars packaged IzPack, or they can be
1999        // specified via a jar attribute on the listener
2000        String jarPath = var.getAttribute("jar");
2001        jarPath = compiler.replaceProperties(jarPath);
2002        if( jarPath == null )
2003            jarPath = "bin/customActions/" + className + ".jar";
2004        URL url = findIzPackResource(jarPath, "CustomAction jar file", var);
2005        String fullName = getFullClassName(url, className);
2006        if (url != null)
2007        {
2008            if (getClass().getResource("/" + jarPath) != null)
2009            { // Oops, standalone, URLClassLoader will not work ...
2010                // Write the jar to a temp file.
2011                InputStream in = null;
2012                FileOutputStream outFile = null;
2013                byte[] buffer = new byte[5120];
2014                File tf = null;
2015                try
2016                {
2017                    tf = File.createTempFile("izpj", ".jar");
2018                    tf.deleteOnExit();
2019                    outFile = new FileOutputStream(tf);
2020                    in = getClass().getResourceAsStream("/" + jarPath);
2021                    long bytesCopied = 0;
2022                    int bytesInBuffer;
2023                    while ((bytesInBuffer = in.read(buffer)) != -1)
2024                    {
2025                        outFile.write(buffer, 0, bytesInBuffer);
2026                        bytesCopied += bytesInBuffer;
2027                    }
2028                }
2029                finally
2030                {
2031                    if (in != null) in.close();
2032                    if (outFile != null) outFile.close();
2033                }
2034                url = tf.toURL();
2035
2036            }
2037            // Use the class loader of the interface as parent, else
2038            // compile will fail at using it via an Ant task.
2039            URLClassLoader ucl = new URLClassLoader(new URL[] { url}, CompilerListener.class
2040                    .getClassLoader());
2041            listener = ucl.loadClass(fullName);
2042        }
2043        if (listener != null)
2044            instance = listener.newInstance();
2045        else
2046            parseError(var, "Cannot find defined compiler listener " + className);
2047        if (!CompilerListener.class.isInstance(instance))
2048            parseError(var, "'" + className + "' must be implemented "
2049                    + CompilerListener.class.toString());
2050        List constraints = OsConstraint.getOsList(var);
2051        return (new Object[] { instance, className, constraints});
2052    }
2053
2054    /**
2055     * Add a CompilerListener. A registered CompilerListener will be called at every enhancmend
2056     * point of compiling.
2057     * 
2058     * @param pe CompilerListener which should be added
2059     */
2060    private void addCompilerListener(CompilerListener pe)
2061    {
2062        compilerListeners.add(pe);
2063    }
2064
2065    /**
2066     * Calls all defined compile listeners notify method with the given data
2067     * 
2068     * @param callerName name of the calling method as string
2069     * @param state CompileListener.BEGIN or END
2070     * @param data current install data
2071     * @throws CompilerException
2072     */
2073    private void notifyCompilerListener(String callerName, int state, XMLElement data)
2074            throws CompilerException
2075    {
2076        Iterator i = compilerListeners.iterator();
2077        Packager packager = compiler.getPackager();
2078        while (i != null && i.hasNext())
2079        {
2080            CompilerListener listener = (CompilerListener) i.next();
2081            listener.notify(callerName, state, data, packager);
2082        }
2083
2084    }
2085
2086    /**
2087     * Calls the reviseAdditionalDataMap method of all registered CompilerListener's.
2088     * 
2089     * @param f file releated XML node
2090     * @return a map with the additional attributes
2091     */
2092    private Map getAdditionals(XMLElement f) throws CompilerException
2093    {
2094        Iterator i = compilerListeners.iterator();
2095        Map retval = null;
2096        try
2097        {
2098            while (i != null && i.hasNext())
2099            {
2100                retval = ((CompilerListener) i.next()).reviseAdditionalDataMap(retval, f);
2101            }
2102        }
2103        catch (CompilerException ce)
2104        {
2105            parseError(f, ce.getMessage());
2106        }
2107        return (retval);
2108    }
2109}