001/*
002 * IzPack - Copyright 2001-2005 Julien Ponge, All Rights Reserved.
003 * 
004 * http://www.izforge.com/izpack/
005 * http://developer.berlios.de/projects/izpack/
006 * 
007 * Copyright 2003 Tino Schwarze
008 * 
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *     http://www.apache.org/licenses/LICENSE-2.0
014 *     
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 */
021
022package com.izforge.izpack.installer;
023
024import java.io.File;
025import java.io.IOException;
026import java.io.InputStream;
027import java.util.ArrayList;
028import java.util.Enumeration;
029import java.util.Iterator;
030import java.util.LinkedList;
031import java.util.List;
032import java.util.StringTokenizer;
033import java.util.Vector;
034
035import net.n3.nanoxml.NonValidator;
036import net.n3.nanoxml.StdXMLBuilder;
037import net.n3.nanoxml.StdXMLParser;
038import net.n3.nanoxml.StdXMLReader;
039import net.n3.nanoxml.XMLElement;
040
041import com.izforge.izpack.LocaleDatabase;
042import com.izforge.izpack.util.Debug;
043import com.izforge.izpack.util.FileExecutor;
044import com.izforge.izpack.util.OsConstraint;
045import com.izforge.izpack.util.VariableSubstitutor;
046
047/**
048 * This class does alle the work for compiling sources.
049 * 
050 * It responsible for
051 * <ul>
052 * <li>parsing the compilation spec XML file
053 * <li>collecting and creating all jobs
054 * <li>doing the actual compilation
055 * </ul>
056 * 
057 * @author Tino Schwarze
058 */
059public class CompileWorker implements Runnable
060{
061
062    /** Compilation jobs */
063    private ArrayList jobs;
064
065    /** Name of resource for specifying compilation parameters. */
066    private static final String SPEC_RESOURCE_NAME = "CompilePanel.Spec.xml";
067
068    private VariableSubstitutor vs;
069
070    /** We spawn a thread to perform compilation. */
071    private Thread compilationThread;
072
073    private XMLElement spec;
074
075    private AutomatedInstallData idata;
076
077    private CompileHandler handler;
078
079    private XMLElement compilerSpec;
080
081    private ArrayList compilerList;
082
083    private String compilerToUse;
084
085    private XMLElement compilerArgumentsSpec;
086
087    private ArrayList compilerArgumentsList;
088
089    private String compilerArgumentsToUse;
090
091    private CompileResult result = null;
092
093    /**
094     * The constructor.
095     * 
096     * @param idata The installation data.
097     * @param handler The handler to notify of progress.
098     */
099    public CompileWorker(AutomatedInstallData idata, CompileHandler handler) throws IOException
100    {
101        this.idata = idata;
102        this.handler = handler;
103        this.vs = new VariableSubstitutor(idata.getVariables());
104
105        this.compilationThread = null;
106
107        if (!readSpec()) throw new IOException("Error reading compilation specification");
108    }
109
110    /**
111     * Return list of compilers to choose from.
112     * 
113     * @return ArrayList of String
114     */
115    public ArrayList getAvailableCompilers()
116    {
117        readChoices(this.compilerSpec, this.compilerList);
118        return this.compilerList;
119    }
120
121    /**
122     * Set the compiler to use.
123     * 
124     * The compiler is checked before compilation starts.
125     * 
126     * @param compiler compiler to use (not checked)
127     */
128    public void setCompiler(String compiler)
129    {
130        this.compilerToUse = compiler;
131    }
132
133    /** Get the compiler used. */
134    public String getCompiler()
135    {
136        return this.compilerToUse;
137    }
138
139    /**
140     * Return list of compiler arguments to choose from.
141     * 
142     * @return ArrayList of String
143     */
144    public ArrayList getAvailableArguments()
145    {
146        readChoices(this.compilerArgumentsSpec, this.compilerArgumentsList);
147        return this.compilerArgumentsList;
148    }
149
150    /** Set the compiler arguments to use. */
151    public void setCompilerArguments(String arguments)
152    {
153        this.compilerArgumentsToUse = arguments;
154    }
155
156    /** Get the compiler arguments used. */
157    public String getCompilerArguments()
158    {
159        return this.compilerArgumentsToUse;
160    }
161
162    /** Get the result of the compilation. */
163    public CompileResult getResult()
164    {
165        return this.result;
166    }
167
168    /** Start the compilation in a separate thread. */
169    public void startThread()
170    {
171        this.compilationThread = new Thread(this, "compilation thread");
172        // will call this.run()
173        this.compilationThread.start();
174    }
175
176    /**
177     * This is called when the compilation thread is activated.
178     * 
179     * Can also be called directly if asynchronous processing is not desired.
180     */
181    public void run()
182    {
183        try
184        {
185            if (!collectJobs())
186            {
187                String[] dummy_command = { "no command"};
188
189                this.result = new CompileResult(this.idata.langpack
190                        .getString("CompilePanel.worker.nofiles"), dummy_command, "", "");
191            }
192            else
193            {
194                this.result = compileJobs();
195            }
196        }
197        catch (Exception e)
198        {
199            this.result = new CompileResult();
200            this.result.setStatus(CompileResult.FAILED);
201            this.result.setAction(CompileResult.ACTION_ABORT);
202        }
203
204        this.handler.stopAction();
205    }
206
207    private boolean readSpec()
208    {
209        InputStream input;
210        try
211        {
212            input = ResourceManager.getInstance().getInputStream(SPEC_RESOURCE_NAME);
213        }
214        catch (Exception e)
215        {
216            e.printStackTrace();
217            return false;
218        }
219
220        StdXMLParser parser = new StdXMLParser();
221        parser.setBuilder(new StdXMLBuilder());
222        parser.setValidator(new NonValidator());
223
224        try
225        {
226            parser.setReader(new StdXMLReader(input));
227
228            this.spec = (XMLElement) parser.parse();
229        }
230        catch (Exception e)
231        {
232            System.out.println("Error parsing XML specification for compilation.");
233            e.printStackTrace();
234            return false;
235        }
236
237        if (!this.spec.hasChildren()) return false;
238
239        this.compilerArgumentsList = new ArrayList();
240        this.compilerList = new ArrayList();
241
242        // read <global> information
243        XMLElement global = this.spec.getFirstChildNamed("global");
244
245        // use some default values if no <global> section found
246        if (global != null)
247        {
248
249            // get list of compilers
250            this.compilerSpec = global.getFirstChildNamed("compiler");
251
252            if (this.compilerSpec != null)
253            {
254                readChoices(this.compilerSpec, this.compilerList);
255            }
256
257            this.compilerArgumentsSpec = global.getFirstChildNamed("arguments");
258
259            if (this.compilerArgumentsSpec != null)
260            {
261                // basicly perform sanity check
262                readChoices(this.compilerArgumentsSpec, this.compilerArgumentsList);
263            }
264
265        }
266
267        // supply default values if no useful ones where found
268        if (this.compilerList.size() == 0)
269        {
270            this.compilerList.add("javac");
271            this.compilerList.add("jikes");
272        }
273
274        if (this.compilerArgumentsList.size() == 0)
275        {
276            this.compilerArgumentsList.add("-O -g:none");
277            this.compilerArgumentsList.add("-O");
278            this.compilerArgumentsList.add("-g");
279            this.compilerArgumentsList.add("");
280        }
281
282        return true;
283    }
284
285    // helper function
286    private void readChoices(XMLElement element, ArrayList result)
287    {
288        Vector choices = element.getChildrenNamed("choice");
289
290        if (choices == null) return;
291
292        result.clear();
293
294        Iterator choice_it = choices.iterator();
295
296        while (choice_it.hasNext())
297        {
298            XMLElement choice = (XMLElement) choice_it.next();
299
300            String value = choice.getAttribute("value");
301
302            if (value != null)
303            {
304                List osconstraints = OsConstraint.getOsList(choice);
305
306                if (OsConstraint.oneMatchesCurrentSystem(osconstraints))
307                {
308                    result.add(this.vs.substitute(value, "plain"));
309                }
310            }
311
312        }
313
314    }
315
316    /**
317     * Parse the compilation specification file and create jobs.
318     */
319    private boolean collectJobs() throws Exception
320    {
321        XMLElement data = this.spec.getFirstChildNamed("jobs");
322
323        if (data == null) return false;
324
325        // list of classpath entries
326        ArrayList classpath = new ArrayList();
327
328        this.jobs = new ArrayList();
329
330        // we throw away the toplevel compilation job
331        // (all jobs are collected in this.jobs)
332        collectJobsRecursive(data, classpath);
333
334        return true;
335    }
336
337    /** perform the actual compilation */
338    private CompileResult compileJobs()
339    {
340        ArrayList args = new ArrayList();
341        StringTokenizer tokenizer = new StringTokenizer(this.compilerArgumentsToUse);
342
343        while (tokenizer.hasMoreTokens())
344        {
345            args.add(tokenizer.nextToken());
346        }
347
348        Iterator job_it = this.jobs.iterator();
349
350        this.handler.startAction("Compilation", this.jobs.size());
351
352        // check whether compiler is valid (but only if there are jobs)
353        if (job_it.hasNext())
354        {
355            CompilationJob first_job = (CompilationJob) this.jobs.get(0);
356
357            CompileResult check_result = first_job.checkCompiler(this.compilerToUse, args);
358            if (!check_result.isContinue()) { return check_result; }
359
360        }
361
362        int job_no = 0;
363
364        while (job_it.hasNext())
365        {
366            CompilationJob job = (CompilationJob) job_it.next();
367
368            this.handler.nextStep(job.getName(), job.getSize(), job_no++);
369
370            CompileResult result = job.perform(this.compilerToUse, args);
371
372            if (!result.isContinue()) return result;
373        }
374
375        Debug.trace("compilation finished.");
376        return new CompileResult();
377    }
378
379    private CompilationJob collectJobsRecursive(XMLElement node, ArrayList classpath)
380            throws Exception
381    {
382        Enumeration toplevel_tags = node.enumerateChildren();
383        ArrayList ourclasspath = (ArrayList) classpath.clone();
384        ArrayList files = new ArrayList();
385
386        while (toplevel_tags.hasMoreElements())
387        {
388            XMLElement child = (XMLElement) toplevel_tags.nextElement();
389
390            if (child.getName().equals("classpath"))
391            {
392                changeClassPath(ourclasspath, child);
393            }
394            else if (child.getName().equals("job"))
395            {
396                CompilationJob subjob = collectJobsRecursive(child, ourclasspath);
397                if (subjob != null) this.jobs.add(subjob);
398            }
399            else if (child.getName().equals("directory"))
400            {
401                String name = child.getAttribute("name");
402
403                if (name != null)
404                {
405                    // substitute variables
406                    String finalname = this.vs.substitute(name, "plain");
407
408                    files.addAll(scanDirectory(new File(finalname)));
409                }
410
411            }
412            else if (child.getName().equals("file"))
413            {
414                String name = child.getAttribute("name");
415
416                if (name != null)
417                {
418                    // substitute variables
419                    String finalname = this.vs.substitute(name, "plain");
420
421                    files.add(new File(finalname));
422                }
423
424            }
425            else if (child.getName().equals("packdepency"))
426            {
427                String name = child.getAttribute("name");
428
429                if (name == null)
430                {
431                    System.out
432                            .println("invalid compilation spec: <packdepency> without name attribute");
433                    return null;
434                }
435
436                // check whether the wanted pack was selected for installation
437                Iterator pack_it = this.idata.selectedPacks.iterator();
438                boolean found = false;
439
440                while (pack_it.hasNext())
441                {
442                    com.izforge.izpack.Pack pack = (com.izforge.izpack.Pack) pack_it.next();
443
444                    if (pack.name.equals(name))
445                    {
446                        found = true;
447                        break;
448                    }
449                }
450
451                if (!found)
452                {
453                    Debug.trace("skipping job because pack " + name + " was not selected.");
454                    return null;
455                }
456
457            }
458
459        }
460
461        if (files.size() > 0)
462            return new CompilationJob(this.handler, this.idata.langpack, (String) node
463                    .getAttribute("name"), files, ourclasspath);
464
465        return null;
466    }
467
468    /** helper: process a <code>&lt;classpath&gt;</code> tag. */
469    private void changeClassPath(ArrayList classpath, XMLElement child) throws Exception
470    {
471        String add = child.getAttribute("add");
472        if (add != null)
473        {
474            add = this.vs.substitute(add, "plain");
475            if (!new File(add).exists())
476            {
477                if (!this.handler.emitWarning("Invalid classpath", "The path " + add
478                        + " could not be found.\nCompilation may fail."))
479                    throw new Exception("Classpath " + add + " does not exist.");
480            }
481            else
482            {
483                classpath.add(this.vs.substitute(add, "plain"));
484            }
485
486        }
487
488        String sub = child.getAttribute("sub");
489        if (sub != null)
490        {
491            int cpidx = -1;
492            sub = this.vs.substitute(sub, "plain");
493
494            do
495            {
496                cpidx = classpath.indexOf(sub);
497                classpath.remove(cpidx);
498            }
499            while (cpidx >= 0);
500
501        }
502
503    }
504
505    /**
506     * helper: recursively scan given directory.
507     * 
508     * @return list of files found (might be empty)
509     */
510    private ArrayList scanDirectory(File path)
511    {
512        Debug.trace("scanning directory " + path.getAbsolutePath());
513
514        ArrayList result = new ArrayList();
515
516        if (!path.isDirectory()) return result;
517
518        File[] entries = path.listFiles();
519
520        for (int i = 0; i < entries.length; i++)
521        {
522            File f = entries[i];
523
524            if (f == null) continue;
525
526            if (f.isDirectory())
527            {
528                result.addAll(scanDirectory(f));
529            }
530            else if ((f.isFile()) && (f.getName().toLowerCase().endsWith(".java")))
531            {
532                result.add(f);
533            }
534
535        }
536
537        return result;
538    }
539
540    /** a compilation job */
541    private static class CompilationJob
542    {
543
544        private CompileHandler listener;
545
546        private String name;
547
548        private ArrayList files;
549
550        private ArrayList classpath;
551
552        private LocaleDatabase langpack;
553
554        // XXX: figure that out (on runtime?)
555        private static final int MAX_CMDLINE_SIZE = 4096;
556
557        public CompilationJob(CompileHandler listener, LocaleDatabase langpack, ArrayList files,
558                ArrayList classpath)
559        {
560            this.listener = listener;
561            this.langpack = langpack;
562            this.name = null;
563            this.files = files;
564            this.classpath = classpath;
565        }
566
567        public CompilationJob(CompileHandler listener, LocaleDatabase langpack, String name,
568                ArrayList files, ArrayList classpath)
569        {
570            this.listener = listener;
571            this.langpack = langpack;
572            this.name = name;
573            this.files = files;
574            this.classpath = classpath;
575        }
576
577        public String getName()
578        {
579            if (this.name != null) return this.name;
580
581            return "";
582        }
583
584        public int getSize()
585        {
586            return this.files.size();
587        }
588
589        public CompileResult perform(String compiler, ArrayList arguments)
590        {
591            Debug.trace("starting job " + this.name);
592            // we have some maximum command line length - need to count
593            int cmdline_len = 0;
594
595            // used to collect the arguments for executing the compiler
596            LinkedList args = new LinkedList(arguments);
597
598            {
599                Iterator arg_it = args.iterator();
600                while (arg_it.hasNext())
601                    cmdline_len += ((String) arg_it.next()).length() + 1;
602            }
603
604            // add compiler in front of arguments
605            args.add(0, compiler);
606            cmdline_len += compiler.length() + 1;
607
608            // construct classpath argument for compiler
609            // - collect all classpaths
610            StringBuffer classpath_sb = new StringBuffer();
611            Iterator cp_it = this.classpath.iterator();
612            while (cp_it.hasNext())
613            {
614                String cp = (String) cp_it.next();
615                if (classpath_sb.length() > 0) classpath_sb.append(File.pathSeparatorChar);
616                classpath_sb.append(new File(cp).getAbsolutePath());
617            }
618
619            String classpath_str = classpath_sb.toString();
620
621            // - add classpath argument to command line
622            if (classpath_str.length() > 0)
623            {
624                args.add("-classpath");
625                cmdline_len = cmdline_len + 11;
626                args.add(classpath_str);
627                cmdline_len += classpath_str.length() + 1;
628            }
629
630            // remember how many arguments we have which don't change for the
631            // job
632            int common_args_no = args.size();
633            // remember how long the common command line is
634            int common_args_len = cmdline_len;
635
636            // used for execution
637            FileExecutor executor = new FileExecutor();
638            String output[] = new String[2];
639
640            // used for displaying the progress bar
641            String jobfiles = "";
642            int fileno = 0;
643            int last_fileno = 0;
644
645            // now iterate over all files of this job
646            Iterator file_it = this.files.iterator();
647
648            while (file_it.hasNext())
649            {
650                File f = (File) file_it.next();
651
652                String fpath = f.getAbsolutePath();
653
654                Debug.trace("processing " + fpath);
655
656                // we add the file _first_ to the arguments to have a better
657                // chance to get something done if the command line is almost
658                // MAX_CMDLINE_SIZE or even above
659                fileno++;
660                jobfiles += f.getName() + " ";
661                args.add(fpath);
662                cmdline_len += fpath.length();
663
664                // start compilation if maximum command line length reached
665                if (cmdline_len >= MAX_CMDLINE_SIZE)
666                {
667                    Debug.trace("compiling " + jobfiles);
668
669                    // display useful progress bar (avoid showing 100% while
670                    // still
671                    // compiling a lot)
672                    this.listener.progress(last_fileno, jobfiles);
673                    last_fileno = fileno;
674
675                    String[] full_cmdline = (String[]) args.toArray(output);
676
677                    int retval = executor.executeCommand(full_cmdline, output);
678
679                    // update progress bar: compilation of fileno files done
680                    this.listener.progress(fileno, jobfiles);
681
682                    if (retval != 0)
683                    {
684                        CompileResult result = new CompileResult(this.langpack
685                                .getString("CompilePanel.error"), full_cmdline, output[0],
686                                output[1]);
687                        this.listener.handleCompileError(result);
688                        if (!result.isContinue()) return result;
689                    }
690                    else
691                    {
692                        // verify that all files have been compiled successfully
693                        // I found that sometimes, no error code is returned
694                        // although
695                        // compilation failed.
696                        Iterator arg_it = args.listIterator(common_args_no);
697                        while (arg_it.hasNext())
698                        {
699                            File java_file = new File((String) arg_it.next());
700
701                            String basename = java_file.getName();
702                            int dotpos = basename.lastIndexOf('.');
703                            basename = basename.substring(0, dotpos) + ".class";
704                            File class_file = new File(java_file.getParentFile(), basename);
705
706                            if (!class_file.exists())
707                            {
708                                CompileResult result = new CompileResult(this.langpack
709                                        .getString("CompilePanel.error.noclassfile")
710                                        + java_file.getAbsolutePath(), full_cmdline, output[0],
711                                        output[1]);
712                                this.listener.handleCompileError(result);
713                                if (!result.isContinue()) return result;
714                                // don't continue any further
715                                break;
716                            }
717
718                        }
719
720                    }
721
722                    // clean command line: remove files we just compiled
723                    for (int i = args.size() - 1; i >= common_args_no; i--)
724                    {
725                        args.removeLast();
726                    }
727
728                    cmdline_len = common_args_len;
729                    jobfiles = "";
730                }
731
732            }
733
734            if (cmdline_len > common_args_len)
735            {
736                this.listener.progress(last_fileno, jobfiles);
737
738                String[] full_cmdline = (String[]) args.toArray(output);
739
740                int retval = executor.executeCommand(full_cmdline, output);
741
742                this.listener.progress(fileno, jobfiles);
743
744                if (retval != 0)
745                {
746                    CompileResult result = new CompileResult(this.langpack
747                            .getString("CompilePanel.error"), full_cmdline, output[0], output[1]);
748                    this.listener.handleCompileError(result);
749                    if (!result.isContinue()) return result;
750                }
751
752            }
753
754            Debug.trace("job " + this.name + " done (" + fileno + " files compiled)");
755
756            return new CompileResult();
757        }
758
759        /**
760         * Check whether the given compiler works.
761         * 
762         * This performs two steps:
763         * <ol>
764         * <li>check whether we can successfully call "compiler -help"</li>
765         * <li>check whether we can successfully call "compiler -help arguments" (not all compilers
766         * return an error here)</li>
767         * </ol>
768         * 
769         * On failure, the method CompileHandler#errorCompile is called with a descriptive error
770         * message.
771         * 
772         * @param compiler the compiler to use
773         * @param arguments additional arguments to pass to the compiler
774         * @return false on error
775         */
776        public CompileResult checkCompiler(String compiler, ArrayList arguments)
777        {
778            int retval = 0;
779            FileExecutor executor = new FileExecutor();
780            String[] output = new String[2];
781
782            Debug.trace("checking whether \"" + compiler + " -help\" works");
783
784            {
785                String[] args = { compiler, "-help"};
786
787                retval = executor.executeCommand(args, output);
788
789                if (retval != 0)
790                {
791                    CompileResult result = new CompileResult(this.langpack
792                            .getString("CompilePanel.error.compilernotfound"), args, output[0],
793                            output[1]);
794                    this.listener.handleCompileError(result);
795                    if (!result.isContinue()) return result;
796                }
797            }
798
799            Debug.trace("checking whether \"" + compiler + " -help +arguments\" works");
800
801            // used to collect the arguments for executing the compiler
802            LinkedList args = new LinkedList(arguments);
803
804            // add -help argument to prevent the compiler from doing anything
805            args.add(0, "-help");
806
807            // add compiler in front of arguments
808            args.add(0, compiler);
809
810            // construct classpath argument for compiler
811            // - collect all classpaths
812            StringBuffer classpath_sb = new StringBuffer();
813            Iterator cp_it = this.classpath.iterator();
814            while (cp_it.hasNext())
815            {
816                String cp = (String) cp_it.next();
817                if (classpath_sb.length() > 0) classpath_sb.append(File.pathSeparatorChar);
818                classpath_sb.append(new File(cp).getAbsolutePath());
819            }
820
821            String classpath_str = classpath_sb.toString();
822
823            // - add classpath argument to command line
824            if (classpath_str.length() > 0)
825            {
826                args.add("-classpath");
827                args.add(classpath_str);
828            }
829
830            String[] args_arr = (String[]) args.toArray(output);
831
832            retval = executor.executeCommand(args_arr, output);
833
834            if (retval != 0)
835            {
836                CompileResult result = new CompileResult(this.langpack
837                        .getString("CompilePanel.error.invalidarguments"), args_arr, output[0],
838                        output[1]);
839                this.listener.handleCompileError(result);
840                if (!result.isContinue()) return result;
841            }
842
843            return new CompileResult();
844        }
845
846    }
847
848}