001: package tide.export;
002:
003: import javax.swing.JOptionPane;
004: import snow.utils.SysUtils;
005: import tide.project.ProjectUtils;
006: import tide.editor.MainEditorFrame;
007: import tide.sources.*;
008: import java.io.*;
009: import java.util.*;
010: import snow.utils.storage.*;
011: import java.util.zip.*;
012: import java.util.jar.*;
013:
014: /** Creation of the project jar file, with a lot of associated features.
015: */
016: public final class JarCreation {
017: private JarCreation() {
018: }
019:
020: /** Updates the given jar file with all the given files from the source folder (images, .java, ...)
021: * @return null if no files are found.
022: */
023: @edu.umd.cs.findbugs.annotations.CheckReturnValue
024: public static Process addFilesFromSourceFolder(
025: final File jarExecutor, final File jarFile,
026: final File source_home, boolean includeSourceFiles,
027: boolean includeResourceFiles,
028: final List<String> ignoredEndings) throws Exception {
029: if (!jarExecutor.exists())
030: throw new Exception("Jar tool no found: " + jarExecutor);
031:
032: final List<String> execCommand = new ArrayList<String>();
033: execCommand.add(jarExecutor.getAbsolutePath());
034: execCommand.add("-J-Xmx512M"); // ?? TODO: offers as parameter OR grew if out of memory detected
035: execCommand.add("-J-Duser.language=en");
036: execCommand.add("uvf"); // update !
037: execCommand.add(jarFile.getAbsolutePath());
038:
039: final StringBuilder sourceFilesList = new StringBuilder();
040: int srcLen = FileUtils.getCanonicalName(source_home).length();
041: if (includeSourceFiles) {
042: final List<SourceFile> allSources = MainEditorFrame.instance.sourcesTreePanel
043: .getTreeModel().getAllSourceFiles(false); // without ignored
044: sl: for (int i = 0; i < allSources.size(); i++) {
045: String relName = allSources.get(i).javaFile
046: .getAbsolutePath().substring(srcLen);
047: sourceFilesList.append("" + relName); // sources names have no spaces (?)
048: if (i < allSources.size() - 1)
049: sourceFilesList.append("\r\n");
050: }
051: sourceFilesList.append("\n");
052: }
053:
054: if (includeResourceFiles) {
055: // all but *.java files and ignored... (resources in ignored source folders are also not in the list)
056: final List<File> allResources = MainEditorFrame.instance.sourcesTreePanel
057: .getTreeModel().getAllResourceFiles(ignoredEndings);
058: for (int i = 0; i < allResources.size(); i++) {
059: String relName = allResources.get(i).getAbsolutePath()
060: .substring(srcLen).trim();
061: if (relName.indexOf(' ') > 0) {
062: relName = "\"" + relName.replace("\\", "/") + "\""; // important !, quote + replace \\ with / !
063: System.out
064: .println("Jar resource name with spaces: "
065: + relName);
066: }
067: sourceFilesList.append("" + relName);
068:
069: if (i < allResources.size() - 1)
070: sourceFilesList.append("\r\n");
071: }
072: sourceFilesList.append("\n");
073: }
074:
075: if (sourceFilesList.length() < 2)
076: return null; // [Oct2007]
077:
078: final File sourcesListFile = new File(source_home
079: .getAbsolutePath(), "sources_to_jar");
080: sourcesListFile.deleteOnExit();
081: FileUtils.saveToFile(sourceFilesList.toString(),
082: sourcesListFile);
083:
084: execCommand.add("-C");
085: execCommand.add(source_home.getAbsolutePath());
086: // not working well... java.util.zip.ZipException: duplicate entry: aaa/
087: execCommand.add("@" + sourcesListFile.getAbsolutePath());
088:
089: MainEditorFrame
090: .debugOut("Create jar file (add sources and/or resources)\n"
091: + "Command = " + execCommand);
092:
093: ProcessBuilder pb = new ProcessBuilder(execCommand);
094: pb.directory(source_home); // must be started in the source dir !!
095: Process proc = pb.start();
096:
097: return proc;
098: }
099:
100: /** @param customManifestFile is null (normal case), a correct and complete manif will be created
101: * Don't include classes excluded from project !
102: * ( they are also not compiled => not in the classes folder )
103: * => project must be cleared and compiled first.
104: */
105: public static Process createClassesJarFile(File jarExecutor,
106: File jarFile, File classesDest, String mainClassName,
107: List<File> classPath, File customManifestFile)
108: throws Exception {
109: if (!jarExecutor.exists())
110: throw new Exception("Jar tool no found: " + jarExecutor);
111:
112: // Manifest
113: //
114: File manifestFile = null;
115: if (customManifestFile == null) {
116: StringBuilder manifest = new StringBuilder(
117: "Manifest-Version: 1.0\nCreated-By: "
118: + System.getProperty("java.version",
119: "1.6.0_05") + " ("
120: + MainEditorFrame._VERSION + ")");
121:
122: if (mainClassName != null && mainClassName.length() > 0) {
123: manifest.append("\nMain-Class: " + mainClassName);
124: }
125:
126: if (classPath.size() > 0) {
127: manifest.append("\nClass-Path: ");
128: for (int i = 0; i < classPath.size(); i++) {
129: manifest.append("" + classPath.get(i).getName()); // [April2006]: names instead of full path... (not valid to have absolute paths !!)
130: if (i < classPath.size() - 1)
131: manifest.append(" "); // Two spaces !! one is the continuation char !!
132: }
133: }
134: manifest.append("\n"); // IMPORTANT !!
135:
136: manifestFile = File.createTempFile("Manifest", null);
137: manifestFile.deleteOnExit();
138: FileUtils.saveToFile(manifest.toString(), manifestFile);
139: } else {
140: manifestFile = customManifestFile;
141:
142: if (!manifestFile.exists())
143: throw new RuntimeException(
144: "The given custom manifest file doesn't exist: "
145: + manifestFile);
146:
147: String cont = FileUtils.getFileStringContent(manifestFile);
148: if (!cont.endsWith("\n")) {
149: //Todo: ask to fix ?
150: throw new Exception(
151: "The manifest file MUST end with a blank line (return)");
152: }
153: manifestFile = customManifestFile;
154: }
155:
156: List<String> execCommand = new ArrayList<String>();
157: execCommand.add(jarExecutor.getAbsolutePath());
158: execCommand.add("-J-Xmx512M"); // ??
159: execCommand.add("-J-Duser.language=en"); // [March2007]: tide outputs are in english.
160: execCommand.add("cvmf");
161: execCommand.add(manifestFile.getAbsolutePath());
162: execCommand.add(jarFile.getAbsolutePath());
163:
164: // ToDO: also can be made with an argument file
165: execCommand.add("-C");
166: execCommand.add(classesDest.getAbsolutePath());
167: // Thanks Mike K. for the bugix.
168: execCommand.add("."); // linux accept only this, windows both "." and "*"
169:
170: MainEditorFrame
171: .debugOut("Create jar file (classes)\nCommand = "
172: + execCommand);
173:
174: ProcessBuilder pb = new ProcessBuilder(execCommand);
175: pb.directory(classesDest);
176: return pb.start();
177: }
178:
179: /** A small batch launcher to start the jar app with the JVM arguments
180: * ( A double click on a jar will ignore them... [Jan2006] ).
181: */
182: public static void createBatchLauncher(File javaExecutor,
183: File jarFile, String jvmOptions, String appOptions)
184: throws Exception {
185:
186: File dest = (SysUtils.is_Windows_OS() ? new File(jarFile
187: .getAbsolutePath()
188: + ".bat") : new File(jarFile.getAbsolutePath() + ".sh"));
189: StringBuilder bf = new StringBuilder();
190: if (!SysUtils.is_Windows_OS()) {
191: bf.append("#/bin/sh\n"); // NOT \r\n !!
192: }
193: String jp = javaExecutor.getAbsolutePath();
194: if (jp.indexOf(' ') > 0)
195: bf.append("\""); // also linux ?
196: bf.append(jp);
197: if (jp.indexOf(' ') > 0)
198: bf.append("\"");
199:
200: if (jvmOptions.trim().length() > 0) {
201: bf.append(" " + jvmOptions);
202: }
203: bf.append(" -jar \"" + jarFile.getAbsolutePath() + "\"");
204: if (appOptions.trim().length() > 0) {
205: bf.append(" " + appOptions);
206: }
207:
208: if (!SysUtils.is_Windows_OS()) {
209: bf.append(" $*"); // allow passing any arguments
210: }
211:
212: FileUtils.saveToFile(bf.toString(), dest);
213:
214: if (!SysUtils.is_Windows_OS()) {
215: // chmod +x dest
216: //SysUtils.wri
217: try {
218: dest.setExecutable(true, false); // for all users
219: } catch (Exception e) {
220: JOptionPane
221: .showMessageDialog(
222: null,
223: "Can't set execute flag for the launch script\n\n "
224: + dest
225: + "\n\nPlease set it in a shell with the command \"sudo chmod +x "
226: + dest.getName() + "\"",
227: "Warning", JOptionPane.WARNING_MESSAGE);
228: }
229: }
230: }
231:
232: /** A jnlp launcher to start the jar through the net.
233: * Overtakes most settings from the project, as classpath, main method, ...
234: * app arguments, jvm arguments (not all are supported, be careful !)
235: */
236: public static void createJNLPLauncher(
237: File jarFile,
238: String jvmOptions, // TODO... (problem: not all options are available...)
239: String appOptions, String mainClassJavaName,
240: List<File> classPath, String codeBase, String title,
241: String vendor, String homepage, String descr,
242: String j2seversion, String xmx, // deprecated : replaced ba jvmOptions !
243: boolean isSigned) throws Exception {
244:
245: if (mainClassJavaName == null
246: || mainClassJavaName.trim().length() == 0) {
247: throw new Exception(
248: "The project must contain a valid main class name, otherwise no JNLP can be created."
249: + "\n(Nothing to launch !).");
250: }
251:
252: String fname = jarFile.getAbsolutePath();
253: String fnameBase = fname.substring(0, fname.length() - 4);
254: fname = fnameBase + ".jnlp";
255: File dest = new File(fname);
256: File icon = new File(fnameBase + ".png");
257:
258: StringBuilder bf = new StringBuilder(
259: "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<!-- JNLP File Created with tIDE -->\r\n<jnlp spec=\"1.0+\"");
260: bf.append("\r\n codebase=\"" + codeBase + "\"");
261: bf.append("\r\n href=\"" + dest.getName() + "\">");
262: bf.append("\r\n <information>");
263: bf.append("\r\n <title>" + title + "</title>");
264: bf.append("\r\n <vendor>" + vendor + "</vendor>");
265: bf.append("\r\n <homepage href=\"" + homepage + "\"/>");
266: bf.append("\r\n <description>" + descr + "</description>");
267: bf.append("\r\n <description kind=\"short\">" + descr
268: + "</description>");
269: bf.append("\r\n <icon href=\"" + icon.getName() + "\"/>");
270: bf.append("\r\n <offline-allowed/>");
271: bf.append("\r\n </information>");
272: bf.append("\r\n <security>");
273: if (isSigned) {
274: bf.append("\r\n <all-permissions/>");
275: }
276: bf.append("\r\n </security>");
277: bf.append("\r\n <resources>");
278: bf.append("\r\n <j2se version=\"" + j2seversion + "\"");
279: if (xmx != null && xmx.length() > 0) {
280: bf.append(" max-heap-size=\"" + xmx + "\"");
281: }
282: if (jvmOptions != null && jvmOptions.length() > 0) {
283: bf.append(" java-vm-args=\"" + jvmOptions + "\"");
284: }
285: bf.append("/>");
286: bf.append("\r\n <jar href=\"" + jarFile.getName() + "\"/>");
287: // add project class path files !
288: // They must also be exported (manually) and signed with the same key as the main jar !
289: if (classPath != null) {
290: for (File cpf : classPath) {
291: bf.append("\r\n <jar href=\"" + cpf.getName()
292: + "\"/>");
293: }
294: }
295: bf.append("\r\n </resources>");
296: bf.append("\r\n <application-desc main-class=\""
297: + mainClassJavaName + "\">");
298: // arguments
299: if (appOptions != null && appOptions.trim().length() > 0) {
300: for (final String apo : ProjectUtils.splitArgs(appOptions,
301: false)) {
302: bf.append("\r\n <argument>\"" + apo
303: + "\"</argument>");
304: }
305: }
306:
307: bf.append("\r\n </application-desc>");
308: bf.append("\r\n</jnlp>");
309: FileUtils.saveToFile(bf.toString(), dest);
310: }
311:
312: /** Since 1.3, create an index (incomplete ! ?) of the jar and the jars in the class-path.
313: * Important: the files to index are prsent in the classpath manifest entry and MUST be present
314: * in the destination folder (where the jar is)
315: */
316: public static Process indexJarFile(File jarExecutor, File jarFile)
317: throws Exception {
318: if (!jarExecutor.exists())
319: throw new Exception("Jar tool no found: " + jarExecutor);
320:
321: List<String> execCommand = new ArrayList<String>();
322: execCommand.add(jarExecutor.getAbsolutePath());
323: execCommand.add("-J-Duser.language=en");
324: execCommand.add("i");
325: execCommand.add("\"" + jarFile.getAbsolutePath() + "\""); // MUST BE QUOTED ! because of "program files"
326:
327: MainEditorFrame.debugOut("indexing jar files: " + execCommand);
328:
329: ProcessBuilder pb = new ProcessBuilder(execCommand);
330: pb.directory(jarFile.getParentFile());
331: return pb.start();
332: }
333:
334: /** pack200 and gzipped, => very good compression success... 5 to 9 on pure class contents!
335: * Be careful, the signature is NOT conservated, because pack200
336: * let a great freedom for the reconstruction, some variable tables may be shuffled !
337: * and sha-1 and other bytecode verifications are no more of same signature !!!
338: * => normalize the jar before signing !
339: *
340: * PROBLEM: uses JVM memory and causes problems... => call in another JVM !
341: */
342: @SuppressWarnings("unchecked")
343: public static void compress_pack200(File src, File dest)
344: throws Exception {
345: Pack200.Packer packer = Pack200.newPacker();
346:
347: // Initialize the state by setting the desired properties
348: Map p = packer.properties();
349: // take more time choosing codings for better compression
350: p.put(Pack200.Packer.EFFORT, "7"); // default is "5"
351: // use largest-possible archive segments (>10% better compression).
352: p.put(Pack200.Packer.SEGMENT_LIMIT, "-1");
353: // reorder files for better compression.
354: p.put(Pack200.Packer.KEEP_FILE_ORDER, Pack200.Packer.FALSE);
355: // smear modification times to a single value.
356: p.put(Pack200.Packer.MODIFICATION_TIME, Pack200.Packer.LATEST);
357: // ignore all JAR deflation requests,
358: // transmitting a single request to use "store" mode.
359: p.put(Pack200.Packer.DEFLATE_HINT, Pack200.Packer.FALSE);
360: // discard debug attributes
361: p.put(Pack200.Packer.CODE_ATTRIBUTE_PFX + "LineNumberTable",
362: Pack200.Packer.STRIP);
363: // throw an error if an attribute is unrecognized
364: p.put(Pack200.Packer.UNKNOWN_ATTRIBUTE, Pack200.Packer.ERROR);
365:
366: FileOutputStream fos = null;
367: try {
368: JarFile jarFile = new JarFile(src);
369: fos = new FileOutputStream(dest);
370: GZIPOutputStream gos = new GZIPOutputStream(fos);
371: // Call the packer
372: packer.pack(jarFile, gos);
373: jarFile.close();
374: gos.flush();
375: gos.close();
376:
377: /*System.out.println("Pack200 results: ");
378: System.out.println(" Before: \tjar file length="+src.length()+" bytes");
379: System.out.println(" After: \tjar file length="+dest.length()+" bytes");*/
380: } catch (Exception e) {
381: e.printStackTrace();
382: throw e;
383: } finally {
384: FileUtils.closeIgnoringExceptions(fos);
385: }
386: }
387:
388: /**
389: * Pack200 rearranges the contents of the resultant JAR file. The jarsigner hashes the contents of the class file
390: * and stores the hash in an encrypted digest in the manifest. When the unpacker runs on a packed packed, the contents
391: * of the classes will be rearranged and thus invalidate the signature. Therefore, the JAR file must be normalized first
392: * using pack200 and unpack200, and thereafter signed.
393: * [Nov2007] Could be also made with "pack200 --repack myarchive.jar"
394: *
395: * (Here's why this works: Any reordering the packer does of any classfile structures is idempotent, so the second packing
396: * does not change the orderings produced by the first packing. Also, the unpacker is guaranteed by the JSR 200 specification
397: * to produce a specific bytewise image for any given transmission ordering of archive elements.)
398: */
399: @SuppressWarnings("unchecked")
400: public static void replaceJarWithPack200Decompressed(File jarFile,
401: File pack200GzFile) throws Exception {
402: File tmpDest = File.createTempFile("tide_" + jarFile.getName(),
403: null);
404: tmpDest.deleteOnExit();
405: FileOutputStream fostream = null;
406: JarOutputStream jostream = null;
407: FileInputStream fis = null;
408:
409: try {
410: fostream = new FileOutputStream(tmpDest);
411: jostream = new JarOutputStream(fostream);
412:
413: Pack200.Unpacker unpacker = Pack200.newUnpacker();
414: Map p = unpacker.properties();
415: p.put(Pack200.Unpacker.DEFLATE_HINT, Pack200.Unpacker.TRUE);
416:
417: fis = new FileInputStream(pack200GzFile);
418: GZIPInputStream gis = new GZIPInputStream(fis);
419:
420: unpacker.unpack(gis, jostream);
421:
422: // Must explicitly close the output.
423: jostream.close();
424: fis.close();
425:
426: // ok, it worked => replace the files
427: if (jarFile.exists()) {
428: if (!jarFile.delete()) {
429: throw new Exception("Cannot delete " + jarFile);
430: }
431: }
432: } catch (Exception e) {
433: throw e;
434: } finally {
435: FileUtils.closeIgnoringExceptions(fis);
436: FileUtils.closeIgnoringExceptions(fostream);
437: }
438:
439: long lm = tmpDest.lastModified();
440:
441: //tmpDest.renameTo(jarFile); //thanks Mike K. suggering that this is not good for example if on diff partitions
442: FileUtils.copy(tmpDest, jarFile);
443:
444: // overtake the date. this is necessary because this function is also used by the updater.
445: jarFile.setLastModified(lm);
446: }
447:
448: /** To retrieve all the sources, use<pre>
449: * List<SourceFile> allSources = MainEditorFrame.instance.sourcesTreePanel.getTreeModel().getAllSourceFiles();</pre>
450: */
451: public static void create_separate_SourcesZipFile(
452: File sources_home, List<SourceFile> allSources,
453: List<File> resources, File dest) throws Exception {
454: if (!dest.getParentFile().exists()) {
455: dest.getParentFile().mkdirs();
456: }
457:
458: FileOutputStream fos = null;
459: ZipOutputStream zos = null;
460: try {
461: fos = new FileOutputStream(dest);
462: zos = new ZipOutputStream(fos);
463:
464: int srcLen = FileUtils.getCanonicalName(sources_home)
465: .length();
466:
467: sl: for (int i = 0; i < allSources.size(); i++) {
468: if (allSources.get(i).isIgnored())
469: continue sl;
470:
471: String relName = allSources.get(i).javaFile
472: .getAbsolutePath().substring(srcLen);
473: relName = "src/" + relName.replace("\\", "/");
474: FileUtils.addToZip(zos, allSources.get(i).javaFile,
475: relName);
476: }
477:
478: if (resources != null) {
479: for (File rfi : resources) {
480: String relName = rfi.getAbsolutePath().substring(
481: srcLen);
482: relName = "src/" + relName.replace("\\", "/");
483: FileUtils.addToZip(zos, rfi, relName);
484: }
485: }
486: } catch (Exception e) {
487: throw e;
488: } finally {
489: FileUtils.closeIgnoringExceptions(zos);
490: FileUtils.closeIgnoringExceptions(fos);
491: }
492: }
493: }
|