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