001/*
002 * $Id: Packager.java,v 1.40 2005/08/26 06:48:50 bartzkau 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 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 * 
012 *     http://www.apache.org/licenses/LICENSE-2.0
013 *     
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020
021package com.izforge.izpack.compiler;
022
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.ObjectOutputStream;
028import java.io.OutputStream;
029import java.net.URL;
030import java.util.ArrayList;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Map;
036import java.util.Properties;
037import java.util.Set;
038import java.util.zip.Deflater;
039import java.util.zip.ZipException;
040import java.util.zip.ZipInputStream;
041
042// The declarations for ZipOutputStreams will be done
043// as full qualified to clear at the use point that
044// we do not use the standard class else the extended
045// from apache.
046//import org.apache.tools.zip.ZipOutputStream; 
047//import org.apache.tools.zip.ZipEntry;
048
049import com.izforge.izpack.CustomData;
050import com.izforge.izpack.GUIPrefs;
051import com.izforge.izpack.Info;
052import com.izforge.izpack.Pack;
053import com.izforge.izpack.PackFile;
054import com.izforge.izpack.Panel;
055import com.izforge.izpack.compressor.PackCompressor;
056import com.izforge.izpack.compressor.PackCompressorFactory;
057//import com.izforge.izpack.util.JarOutputStream;
058
059/**
060 * The packager class. The packager is used by the compiler to put files into an installer, and
061 * create the actual installer files.
062 * 
063 * @author Julien Ponge
064 * @author Chadwick McHenry
065 */
066public class Packager
067{
068
069    /** Path to the skeleton installer. */
070    public static final String SKELETON_SUBPATH = "lib/installer.jar";
071
072    /** Base file name of all jar files. This has no ".jar" suffix. */
073    private File baseFile = null;
074
075    /** Executable zipped output stream. First to open, last to close. 
076     *  Attention! This is our own JarOutputStream, not the java standard! */
077    private com.izforge.izpack.util.JarOutputStream primaryJarStream;
078
079    /** Basic installer info. */
080    private Info info = null;
081
082    /** Gui preferences of instatller. */
083    private GUIPrefs guiPrefs = null;
084
085    /** The variables used in the project */
086    private Properties variables = new Properties();
087
088    /** The ordered panels informations. */
089    private List panelList = new ArrayList();
090
091    /** The ordered packs informations (as PackInfo objects). */
092    private List packsList = new ArrayList();
093
094    /** The ordered langpack ISO3 names. */
095    private List langpackNameList = new ArrayList();
096
097    /** The ordered custom actions informations. */
098    private List customDataList = new ArrayList();
099
100    /** The langpack URLs keyed by ISO3 name. */
101    private Map installerResourceURLMap = new HashMap();
102
103    /** Jar file URLs who's contents will be copied into the installer. */
104    private Set includedJarURLs = new HashSet();
105
106    /** Each pack is created in a separte jar if webDirURL is non-null. */
107    private boolean packJarsSeparate = false;
108
109    /** The listeners. */
110    private PackagerListener listener;
111
112    /** The compression format to be used for pack compression */
113    private PackCompressor compressor;
114    
115    /** Files which are always written into the container file */
116    private HashMap alreadyWrittenFiles = new HashMap();
117    /** The constructor. 
118     * @throws CompilerException*/
119    public Packager() throws CompilerException
120    {
121        this("default");
122    }
123
124    /**
125     * Extended constructor.
126     * @param compr_format Compression format to be used for packs
127     * compression format (if supported)
128     * @throws CompilerException
129     */
130    public Packager(String compr_format) throws CompilerException
131    {
132        this( compr_format, -1);
133    }
134
135    /**
136     * Extended constructor.
137     * @param compr_format Compression format to be used for packs
138     * @param compr_level Compression level to be used with the chosen
139     * compression format (if supported)
140     * @throws CompilerException
141     */
142    public Packager(String compr_format, int compr_level) throws CompilerException
143    {
144        compressor = PackCompressorFactory.get( compr_format);
145        compressor.setCompressionLevel(compr_level);
146    }
147
148    /**
149     * Create the installer, beginning with the specified jar. If the name specified does not end in
150     * ".jar", it is appended. If secondary jars are created for packs (if the Info object added has
151     * a webDirURL set), they are created in the same directory, named sequentially by inserting
152     * ".pack#" (where '#' is the pack number) ".jar" suffix: e.g. "foo.pack1.jar". If any file
153     * exists, it is overwritten.
154     */
155    public void createInstaller(File primaryFile) throws Exception
156    {
157        // preliminary work
158        String baseName = primaryFile.getName();
159        if (baseName.endsWith(".jar"))
160        {
161            baseName = baseName.substring(0, baseName.length() - 4);
162            baseFile = new File(primaryFile.getParentFile(), baseName);
163        }
164        else
165            baseFile = primaryFile;
166
167        info.setInstallerBase(baseFile.getName());
168        packJarsSeparate = (info.getWebDirURL() != null);
169
170        // primary (possibly only) jar. -1 indicates primary
171        primaryJarStream = getJarOutputStream(baseFile.getName() + ".jar");
172
173        sendStart();
174        
175        // write the primary jar. MUST be first so manifest is not overwritten
176        // by
177        // an included jar
178        writeSkeletonInstaller();
179
180        writeInstallerObject("info", info);
181        writeInstallerObject("vars", variables);
182        writeInstallerObject("GUIPrefs", guiPrefs);
183        writeInstallerObject("panelsOrder", panelList);
184        writeInstallerObject("customData", customDataList);
185        writeInstallerObject("langpacks.info", langpackNameList);
186        writeInstallerResources();
187        writeIncludedJars();
188
189        // Pack File Data may be written to separate jars
190        writePacks();
191
192        // Finish up. closeAlways is a hack for pack compressions other than
193        // default. Some of it (e.g. BZip2) closes the slave of it also.
194        // But this should not be because the jar stream should be open 
195        // for the next pack. Therefore an own JarOutputStream will be used
196        // which close method will be blocked.
197        primaryJarStream.closeAlways();
198
199        sendStop();
200    }
201
202    /***********************************************************************************************
203     * Listener assistance
204     **********************************************************************************************/
205
206    /**
207     * Get the PackagerListener.
208     * @return the current PackagerListener
209     */
210    public PackagerListener getPackagerListener()
211    {
212        return listener;
213    }
214    /**
215     * Adds a listener.
216     * 
217     * @param listener The listener.
218     */
219    public void setPackagerListener(PackagerListener listener)
220    {
221        this.listener = listener;
222    }
223
224    /**
225     * Dispatches a message to the listeners.
226     * 
227     * @param job The job description.
228     */
229    private void sendMsg(String job)
230    {
231        sendMsg(job, PackagerListener.MSG_INFO);
232    }
233
234    /**
235     * Dispatches a message to the listeners at specified priority.
236     * 
237     * @param job The job description.
238     * @param priority The message priority.
239     */
240    private void sendMsg(String job, int priority)
241    {
242        if (listener != null) listener.packagerMsg(job, priority);
243    }
244
245    /** Dispatches a start event to the listeners. */
246    private void sendStart()
247    {
248        if (listener != null) listener.packagerStart();
249    }
250
251    /** Dispatches a stop event to the listeners. */
252    private void sendStop()
253    {
254        if (listener != null) listener.packagerStop();
255    }
256
257    /***********************************************************************************************
258     * Public methods to add data to the Installer being packed
259     **********************************************************************************************/
260
261    /**
262     * Sets the informations related to this installation.
263     * 
264     * @param info The info section.
265     * @exception Exception Description of the Exception
266     */
267    public void setInfo(Info info) throws Exception
268    {
269        sendMsg("Setting the installer information", PackagerListener.MSG_VERBOSE);
270        this.info = info;
271        if( ! getCompressor().useStandardCompression() && 
272                getCompressor().getDecoderMapperName() != null  )
273        {
274            this.info.setPackDecoderClassName(getCompressor().getDecoderMapperName());
275        }
276    }
277
278    /**
279     * Sets the GUI preferences.
280     * 
281     * @param prefs The new gUIPrefs value
282     * @exception Exception Description of the Exception
283     */
284    public void setGUIPrefs(GUIPrefs prefs)
285    {
286        sendMsg("Setting the GUI preferences", PackagerListener.MSG_VERBOSE);
287        guiPrefs = prefs;
288    }
289
290    /**
291     * Allows access to add, remove and update the variables for the project, which are maintained
292     * in the packager.
293     * 
294     * @return map of variable names to values
295     */
296    public Properties getVariables()
297    {
298        return variables;
299    }
300
301    /**
302     * Add a panel, where order is important. Only one copy of the class files neeed are inserted in
303     * the installer.
304     */
305    public void addPanelJar(Panel panel, URL jarURL)
306    {
307        panelList.add(panel); // serialized to keep order/variables correct
308        addJarContent(jarURL); // each included once, no matter how many times
309        // added
310    }
311
312    /**
313     * Add a custom data like custom actions, where order is important. Only one copy of the class
314     * files neeed are inserted in the installer.
315     * 
316     * @param ca custom action object
317     * @param url the URL to include once
318     */
319    public void addCustomJar(CustomData ca, URL url)
320    {
321        customDataList.add(ca); // serialized to keep order/variables correct
322        addJarContent(url); // each included once, no matter how many times
323        // added
324    }
325
326    /**
327     * Adds a pack, order is mostly irrelevant.
328     * 
329     * @param pack contains all the files and items that go with a pack
330     */
331    public void addPack(PackInfo pack)
332    {
333        packsList.add(pack);
334    }
335
336    /**
337     * Gets the packages list
338     */
339    public List getPacksList()
340    {
341        return packsList;
342    }
343
344    /**
345     * Adds a language pack.
346     * 
347     * @param iso3 The ISO3 code.
348     * @param xmlURL The location of the xml local info
349     * @param flagURL The location of the flag image resource
350     * @exception Exception Description of the Exception
351     */
352    public void addLangPack(String iso3, URL xmlURL, URL flagURL)
353    {
354        sendMsg("Adding langpack: " + iso3, PackagerListener.MSG_VERBOSE);
355        // put data & flag as entries in installer, and keep array of iso3's
356        // names
357        langpackNameList.add(iso3);
358        addResource("flag." + iso3, flagURL);
359        installerResourceURLMap.put("langpacks/" + iso3 + ".xml", xmlURL);
360    }
361
362    /**
363     * Adds a resource.
364     * 
365     * @param resId The resource Id.
366     * @param url The location of the data
367     * @exception Exception Description of the Exception
368     */
369    public void addResource(String resId, URL url)
370    {
371        sendMsg("Adding resource: " + resId, PackagerListener.MSG_VERBOSE);
372        installerResourceURLMap.put("res/" + resId, url);
373    }
374
375    /**
376     * Adds a native library.
377     * 
378     * @param name The native library name.
379     * @param url The url to get the data from.
380     * @exception Exception Description of the Exception
381     */
382    public void addNativeLibrary(String name, URL url) throws Exception
383    {
384        sendMsg("Adding native library: " + name, PackagerListener.MSG_VERBOSE);
385        installerResourceURLMap.put("native/" + name, url);
386    }
387
388
389    /**
390     * Adds a jar file content to the installer. Package structure is maintained. Need mechanism to
391     * copy over signed entry information.
392     * 
393     * @param jarURL The url of the jar to add to the installer. We use a URL so the jar may be
394     * nested within another.
395     */
396    public void addJarContent(URL jarURL)
397    {
398        addJarContent(jarURL, null);
399    }
400   /**
401     * Adds a jar file content to the installer. Package structure is maintained. Need mechanism to
402     * copy over signed entry information.
403     * 
404     * @param jarURL The url of the jar to add to the installer. We use a URL so the jar may be
405     * nested within another.
406     */
407    public void addJarContent(URL jarURL, List files)
408    {
409        Object [] cont = { jarURL, files };
410        sendMsg("Adding content of jar: " + jarURL.getFile(), PackagerListener.MSG_VERBOSE);
411        includedJarURLs.add(cont);
412    }
413
414    /**
415     * Marks a native library to be added to the uninstaller.
416     * 
417     * @param data the describing custom action data object
418     */
419    public void addNativeUninstallerLibrary(CustomData data)
420    {
421        customDataList.add(data); // serialized to keep order/variables
422        // correct
423
424    }
425
426    /***********************************************************************************************
427     * Private methods used when writing out the installer to jar files.
428     **********************************************************************************************/
429
430    /**
431     * Write skeleton installer to primary jar. It is just an included jar, except that we copy the
432     * META-INF as well.
433     */
434    private void writeSkeletonInstaller() throws IOException
435    {
436        sendMsg("Copying the skeleton installer", PackagerListener.MSG_VERBOSE);
437
438        InputStream is = Packager.class.getResourceAsStream("/" + SKELETON_SUBPATH);
439        if (is == null)
440        {
441            File skeleton = new File(Compiler.IZPACK_HOME, SKELETON_SUBPATH);
442            is = new FileInputStream(skeleton);
443        }
444        ZipInputStream inJarStream = new ZipInputStream(is);
445        copyZip(inJarStream, primaryJarStream);
446    }
447
448    /**
449     * Write an arbitrary object to primary jar.
450     */
451    private void writeInstallerObject(String entryName, Object object) throws IOException
452    {
453        primaryJarStream.putNextEntry(new org.apache.tools.zip.ZipEntry(entryName));
454        ObjectOutputStream out = new ObjectOutputStream(primaryJarStream);
455        out.writeObject(object);
456        out.flush();
457        primaryJarStream.closeEntry();
458    }
459
460    /** Write the data referenced by URL to primary jar. */
461    private void writeInstallerResources() throws IOException
462    {
463        sendMsg("Copying " + installerResourceURLMap.size() + " files into installer");
464
465        Iterator i = installerResourceURLMap.keySet().iterator();
466        while (i.hasNext())
467        {
468            String name = (String) i.next();
469            InputStream in = ((URL) installerResourceURLMap.get(name)).openStream();
470            primaryJarStream.putNextEntry(new org.apache.tools.zip.ZipEntry(name));
471            copyStream(in, primaryJarStream);
472            primaryJarStream.closeEntry();
473            in.close();
474        }
475    }
476
477    /** Copy included jars to primary jar. */
478    private void writeIncludedJars() throws IOException
479    {
480        sendMsg("Merging " + includedJarURLs.size() + " jars into installer");
481
482        Iterator i = includedJarURLs.iterator();
483        while (i.hasNext())
484        {
485            Object [] current = (Object []) i.next();
486            InputStream is = ((URL) current[0]).openStream();
487            ZipInputStream inJarStream = new ZipInputStream(is);
488            copyZip(inJarStream, primaryJarStream, (List) current[1]);
489        }
490    }
491
492    /**
493     * Write Packs to primary jar or each to a separate jar.
494     */
495    private void writePacks() throws Exception
496    {
497        final int num = packsList.size();
498        sendMsg("Writing " + num + " Pack" + (num > 1 ? "s" : "") + " into installer");
499
500        // Map to remember pack number and bytes offsets of back references
501        Map storedFiles = new HashMap();
502
503        // First write the serialized files and file metadata data for each pack
504        // while counting bytes.
505        
506        int packNumber = 0;
507        Iterator packIter = packsList.iterator();
508        while (packIter.hasNext())
509        {
510            PackInfo packInfo = (PackInfo) packIter.next();
511            Pack pack = packInfo.getPack();
512            pack.nbytes = 0;
513
514            // create a pack specific jar if required
515            com.izforge.izpack.util.JarOutputStream packStream = primaryJarStream;
516            if (packJarsSeparate)
517            {
518                // See installer.Unpacker#getPackAsStream for the counterpart
519                String name = baseFile.getName() + ".pack" + packNumber + ".jar";
520                packStream = getJarOutputStream(name);
521            }
522            OutputStream comprStream = packStream;
523
524            sendMsg("Writing Pack " + packNumber + ": " + pack.name, PackagerListener.MSG_VERBOSE);
525
526            // Retrieve the correct output stream
527            org.apache.tools.zip.ZipEntry entry = 
528                new org.apache.tools.zip.ZipEntry("packs/pack" + packNumber);
529            if( ! compressor.useStandardCompression())
530            {
531                entry.setMethod(org.apache.tools.zip.ZipEntry.STORED);
532                entry.setComment(compressor.getCompressionFormatSymbols()[0]);
533                // We must set the entry before we get the compressed stream
534                // because some writes initialize data (e.g. bzip2).
535                packStream.putNextEntry(entry);
536                packStream.flush(); // flush before we start counting
537                comprStream = compressor.getOutputStream(packStream);
538            }
539            else
540            {
541                int level = compressor.getCompressionLevel();
542                if( level >= 0 && level < 10 )
543                    packStream.setLevel(level);
544                packStream.putNextEntry(entry);
545                packStream.flush(); // flush before we start counting
546            }
547
548            ByteCountingOutputStream dos = new ByteCountingOutputStream(comprStream);
549            ObjectOutputStream objOut = new ObjectOutputStream(dos);
550
551            // We write the actual pack files
552            objOut.writeInt(packInfo.getPackFiles().size());
553
554            Iterator iter = packInfo.getPackFiles().iterator();
555            while (iter.hasNext())
556            {
557                boolean addFile = !pack.loose;
558                PackFile pf = (PackFile) iter.next();
559                File file = packInfo.getFile(pf);
560
561                // use a back reference if file was in previous pack, and in
562                // same jar
563                long[] info = (long[]) storedFiles.get(file);
564                if (info != null && !packJarsSeparate)
565                {
566                    pf.setPreviousPackFileRef((int) info[0], info[1]);
567                    addFile = false;
568                }
569
570                objOut.writeObject(pf); // base info
571                objOut.flush(); // make sure it is written
572
573                if (addFile && !pf.isDirectory())
574                {
575                    long pos = dos.getByteCount(); // get the position
576
577                    FileInputStream inStream = new FileInputStream(file);
578                    long bytesWritten = copyStream(inStream, objOut);
579
580                    if (bytesWritten != pf.length())
581                        throw new IOException("File size mismatch when reading " + file);
582
583                    inStream.close();
584                    storedFiles.put(file, new long[] { packNumber, pos});
585                }
586
587                // even if not written, it counts towards pack size
588                pack.nbytes += pf.length();
589            }
590
591            // Write out information about parsable files
592            objOut.writeInt(packInfo.getParsables().size());
593            iter = packInfo.getParsables().iterator();
594            while (iter.hasNext())
595                objOut.writeObject(iter.next());
596
597            // Write out information about executable files
598            objOut.writeInt(packInfo.getExecutables().size());
599            iter = packInfo.getExecutables().iterator();
600            while (iter.hasNext())
601                objOut.writeObject(iter.next());
602
603            // Write out information about updatecheck files
604            objOut.writeInt(packInfo.getUpdateChecks().size());
605            iter = packInfo.getUpdateChecks().iterator();
606            while (iter.hasNext())
607                objOut.writeObject(iter.next());
608
609            // Cleanup
610            objOut.flush();
611            if( ! compressor.useStandardCompression())
612            {
613                comprStream.close();
614            }
615
616            packStream.closeEntry();
617
618            // close pack specific jar if required
619            if (packJarsSeparate) packStream.closeAlways();
620
621            packNumber++;
622        }
623
624        // Now that we know sizes, write pack metadata to primary jar.
625        primaryJarStream.putNextEntry(new org.apache.tools.zip.ZipEntry("packs.info"));
626        ObjectOutputStream out = new ObjectOutputStream(primaryJarStream);
627        out.writeInt(packsList.size());
628
629        Iterator i = packsList.iterator();
630        while (i.hasNext())
631        {
632            PackInfo pack = (PackInfo) i.next();
633            out.writeObject(pack.getPack());
634        }
635        out.flush();
636        primaryJarStream.closeEntry();
637    }
638
639    /***********************************************************************************************
640     * Stream utilites for creation of the installer.
641     **********************************************************************************************/
642
643    /** Return a stream for the next jar. */
644    private com.izforge.izpack.util.JarOutputStream getJarOutputStream(String name) throws IOException
645    {
646        File file = new File(baseFile.getParentFile(), name);
647        sendMsg("Building installer jar: " + file.getAbsolutePath());
648
649        com.izforge.izpack.util.JarOutputStream jar = 
650            new com.izforge.izpack.util.JarOutputStream(file);
651        jar.setLevel(Deflater.BEST_COMPRESSION);
652        jar.setPreventClose(true); // Needed at using FilterOutputStreams which calls close
653                                    // of the slave at finalizing.
654
655        return jar;
656    }
657
658    /**
659     * Copies contents of one jar to another.
660     * 
661     * <p>
662     * TODO: it would be useful to be able to keep signature information from signed jar files, can
663     * we combine manifests and still have their content signed?
664     * 
665     * @see #copyStream(InputStream, OutputStream)
666     */
667    private void copyZip(ZipInputStream zin, org.apache.tools.zip.ZipOutputStream out) throws IOException
668    {
669        copyZip( zin, out, null );
670    }
671
672    /**
673     * Copies specified contents of one jar to another.
674     * 
675     * <p>
676     * TODO: it would be useful to be able to keep signature information from signed jar files, can
677     * we combine manifests and still have their content signed?
678     * 
679     * @see #copyStream(InputStream, OutputStream)
680     */
681    private void copyZip(ZipInputStream zin, org.apache.tools.zip.ZipOutputStream out,
682            List files) 
683    throws IOException
684    {
685        java.util.zip.ZipEntry zentry;
686        if( ! alreadyWrittenFiles.containsKey( out ))
687            alreadyWrittenFiles.put(out, new HashSet());
688        HashSet currentSet = (HashSet) alreadyWrittenFiles.get(out);
689        while ((zentry = zin.getNextEntry()) != null)
690        {
691            String currentName = zentry.getName();
692            String testName = currentName.replace('/', '.');
693            testName = testName.replace('\\', '.');
694            if( files != null )
695            {
696                Iterator i = files.iterator();
697                boolean founded = false;
698                while( i.hasNext())
699                {   // Make "includes" self to support regex.
700                    String doInclude = (String) i.next();
701                    if( testName.matches( doInclude  ) )
702                    {
703                        founded = true;
704                        break;
705                    }
706                }
707                if( ! founded )
708                    continue;
709            }
710            if( currentSet.contains(currentName))
711                continue;
712            try
713            {
714                out.putNextEntry(new org.apache.tools.zip.ZipEntry(currentName));
715                copyStream(zin, out);
716                out.closeEntry();
717                zin.closeEntry();
718                currentSet.add(currentName);
719            }
720            catch (ZipException x)
721            {
722                // This avoids any problem that can occur with duplicate
723                // directories. for instance all META-INF data in jars
724                // unfortunately this do not work with the apache ZipOutputStream...
725            }
726        }
727    }
728
729    /**
730     * Copies all the data from the specified input stream to the specified output stream.
731     * 
732     * @param in the input stream to read
733     * @param out the output stream to write
734     * @return the total number of bytes copied
735     * @exception IOException if an I/O error occurs
736     */
737    private long copyStream(InputStream in, OutputStream out) throws IOException
738    {
739        byte[] buffer = new byte[5120];
740        long bytesCopied = 0;
741        int bytesInBuffer;
742        while ((bytesInBuffer = in.read(buffer)) != -1)
743        {
744            out.write(buffer, 0, bytesInBuffer);
745            bytesCopied += bytesInBuffer;
746        }
747        return bytesCopied;
748    }
749    /**
750     * Returns the current pack compressor
751     * @return Returns the current pack compressor.
752     */
753    public PackCompressor getCompressor()
754    {
755        return compressor;
756    }
757}