1 /*******************************************************************************
2 * Copyright (c) 2000, 2011 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
9 * IBM Corporation - initial API and implementation
10 * Ferenc Hechler, ferenc_hechler@users.sourceforge.net - 83258 [jar exporter] Deploy java application as executable jar
11 * Ferenc Hechler <ferenc_hechler@users.sourceforge.net> - [jar exporter] export directory entries in "Runnable JAR File" - https://bugs.eclipse.org/bugs/show_bug.cgi?id=243163
12 *******************************************************************************/
13 package org.eclipse.jdt.ui.jarpackager;
15 import java.io.BufferedInputStream;
16 import java.io.BufferedOutputStream;
18 import java.io.FileInputStream;
19 import java.io.FileNotFoundException;
20 import java.io.FileOutputStream;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.OutputStream;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.Comparator;
28 import java.util.HashSet;
29 import java.util.List;
31 import java.util.jar.JarEntry;
32 import java.util.jar.JarOutputStream;
33 import java.util.jar.Manifest;
34 import java.util.zip.ZipEntry;
36 import org.eclipse.swt.widgets.Shell;
38 import org.eclipse.core.filesystem.EFS;
39 import org.eclipse.core.filesystem.IFileInfo;
41 import org.eclipse.core.runtime.Assert;
42 import org.eclipse.core.runtime.CoreException;
43 import org.eclipse.core.runtime.IPath;
44 import org.eclipse.core.runtime.IProgressMonitor;
45 import org.eclipse.core.runtime.NullProgressMonitor;
46 import org.eclipse.core.runtime.OperationCanceledException;
47 import org.eclipse.core.runtime.Path;
49 import org.eclipse.core.resources.IContainer;
50 import org.eclipse.core.resources.IFile;
51 import org.eclipse.core.resources.IProject;
52 import org.eclipse.core.resources.IResource;
53 import org.eclipse.core.resources.ResourcesPlugin;
55 import org.eclipse.ltk.core.refactoring.RefactoringCore;
56 import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
57 import org.eclipse.ltk.core.refactoring.RefactoringDescriptorProxy;
59 import org.eclipse.jdt.internal.corext.util.Messages;
61 import org.eclipse.jdt.internal.ui.JavaPlugin;
62 import org.eclipse.jdt.internal.ui.jarpackager.JarPackagerMessages;
63 import org.eclipse.jdt.internal.ui.jarpackager.JarPackagerUtil;
64 import org.eclipse.jdt.internal.ui.viewsupport.BasicElementLabels;
68 * Creates a JAR file for the given JAR package data.
70 * Clients may subclass.
73 * @see org.eclipse.jdt.ui.jarpackager.JarPackageData
76 public class JarWriter3 {
78 private Set<String> fDirectories= new HashSet<String>();
80 private JarOutputStream fJarOutputStream;
82 private JarPackageData fJarPackage;
85 * Creates an instance which is used to create a JAR based
86 * on the given JarPackage.
88 * @param jarPackage the JAR specification
89 * @param parent the shell used to display question dialogs,
90 * or <code>null</code> if "false/no/cancel" is the answer
91 * and no dialog should be shown
92 * @throws CoreException to signal any other unusual termination.
93 * This can also be used to return information
94 * in the status object.
96 public JarWriter3(JarPackageData jarPackage, Shell parent) throws CoreException {
97 Assert.isNotNull(jarPackage, "The JAR specification is null"); //$NON-NLS-1$
98 fJarPackage= jarPackage;
99 Assert.isTrue(fJarPackage.isValid(), "The JAR package specification is invalid"); //$NON-NLS-1$
100 if (!canCreateJar(parent))
101 throw new OperationCanceledException();
104 if (fJarPackage.usesManifest() && fJarPackage.areGeneratedFilesExported()) {
105 Manifest manifest= fJarPackage.getManifestProvider().create(fJarPackage);
106 fJarOutputStream= new JarOutputStream(new BufferedOutputStream(new FileOutputStream(fJarPackage.getAbsoluteJarLocation().toFile())), manifest);
108 fJarOutputStream= new JarOutputStream(new BufferedOutputStream(new FileOutputStream(fJarPackage.getAbsoluteJarLocation().toFile())));
109 String comment= jarPackage.getComment();
111 fJarOutputStream.setComment(comment);
112 if (fJarPackage.isRefactoringAware()) {
113 Assert.isTrue(fJarPackage.areDirectoryEntriesIncluded());
114 final IPath metaPath= new Path(JarPackagerUtil.getMetaEntry());
115 addDirectories(metaPath);
116 addHistory(fJarPackage, new Path(JarPackagerUtil.getRefactoringsEntry()), new NullProgressMonitor());
118 } catch (IOException exception) {
119 throw JarPackagerUtil.createCoreException(exception.getLocalizedMessage(), exception);
124 * Creates the directory entries for the given path and writes it to the current archive.
126 * @param destinationPath the path to add
128 * @throws IOException if an I/O error has occurred
130 protected void addDirectories(IPath destinationPath) throws IOException {
131 addDirectories(destinationPath.toString());
135 * Creates the directory entries for the given path and writes it to the current archive.
137 * @param destPath the path to add
139 * @throws IOException if an I/O error has occurred
142 protected void addDirectories(String destPath) throws IOException {
143 String path= destPath.replace(File.separatorChar, '/');
144 int lastSlash= path.lastIndexOf('/');
145 List<JarEntry> directories= new ArrayList<JarEntry>(2);
146 while (lastSlash != -1) {
147 path= path.substring(0, lastSlash + 1);
148 if (!fDirectories.add(path))
151 JarEntry newEntry= new JarEntry(path);
152 newEntry.setMethod(ZipEntry.STORED);
155 newEntry.setTime(System.currentTimeMillis());
156 directories.add(newEntry);
158 lastSlash= path.lastIndexOf('/', lastSlash - 1);
161 for (int i= directories.size() - 1; i >= 0; --i) {
162 fJarOutputStream.putNextEntry(directories.get(i));
167 * Creates the directory entries for the given path and writes it to the
171 * the resource for which the parent directories are to be added
172 * @param destinationPath
175 * @throws IOException
176 * if an I/O error has occurred
177 * @throws CoreException
178 * if accessing the resource failes
180 protected void addDirectories(IResource resource, IPath destinationPath) throws IOException, CoreException {
181 IContainer parent= null;
182 String path= destinationPath.toString().replace(File.separatorChar, '/');
183 int lastSlash= path.lastIndexOf('/');
184 List<JarEntry> directories= new ArrayList<JarEntry>(2);
185 while (lastSlash != -1) {
186 path= path.substring(0, lastSlash + 1);
187 if (!fDirectories.add(path))
190 parent= resource.getParent();
191 long timeStamp= System.currentTimeMillis();
192 URI location= parent.getLocationURI();
193 if (location != null) {
194 IFileInfo info= EFS.getStore(location).fetchInfo();
196 timeStamp= info.getLastModified();
199 JarEntry newEntry= new JarEntry(path);
200 newEntry.setMethod(ZipEntry.STORED);
203 newEntry.setTime(timeStamp);
204 directories.add(newEntry);
206 lastSlash= path.lastIndexOf('/', lastSlash - 1);
209 for (int i= directories.size() - 1; i >= 0; --i) {
210 fJarOutputStream.putNextEntry(directories.get(i));
215 * Creates a new JarEntry with the passed path and contents, and writes it
216 * to the current archive.
218 * @param resource the file to write
219 * @param path the path inside the archive
221 * @throws IOException if an I/O error has occurred
222 * @throws CoreException if the resource can-t be accessed
224 protected void addFile(IFile resource, IPath path) throws IOException, CoreException {
225 JarEntry newEntry= new JarEntry(path.toString().replace(File.separatorChar, '/'));
226 byte[] readBuffer= new byte[4096];
228 if (fJarPackage.isCompressed())
229 newEntry.setMethod(ZipEntry.DEFLATED);
230 // Entry is filled automatically.
232 newEntry.setMethod(ZipEntry.STORED);
233 JarPackagerUtil.calculateCrcAndSize(newEntry, resource.getContents(false), readBuffer);
236 long lastModified= System.currentTimeMillis();
237 URI locationURI= resource.getLocationURI();
238 if (locationURI != null) {
239 IFileInfo info= EFS.getStore(locationURI).fetchInfo();
241 lastModified= info.getLastModified();
244 // Set modification time
245 newEntry.setTime(lastModified);
247 InputStream contentStream = resource.getContents(false);
249 addEntry(newEntry, contentStream);
253 * Write the given entry describing the given content to the
256 * @param entry the entry to write
257 * @param content the content to write
259 * @throws IOException If an I/O error occurred
263 protected void addEntry(JarEntry entry, InputStream content) throws IOException {
264 byte[] readBuffer= new byte[4096];
266 fJarOutputStream.putNextEntry(entry);
268 while ((count= content.read(readBuffer, 0, readBuffer.length)) != -1)
269 fJarOutputStream.write(readBuffer, 0, count);
275 * Commented out because some JREs throw an NPE if a stream
276 * is closed twice. This works because
277 * a) putNextEntry closes the previous entry
278 * b) closing the stream closes the last entry
280 // fJarOutputStream.closeEntry();
285 * Creates a new JAR file entry containing the refactoring history.
288 * the jar package data
290 * the path of the refactoring history file within the archive
292 * the progress monitor to use
293 * @throws IOException
294 * if no temp file could be written
295 * @throws CoreException
296 * if an error occurs while transforming the refactorings
298 private void addHistory(final JarPackageData data, final IPath path, final IProgressMonitor monitor) throws IOException, CoreException {
299 Assert.isNotNull(data);
300 Assert.isNotNull(path);
301 Assert.isNotNull(monitor);
302 final RefactoringDescriptorProxy[] proxies= data.getRefactoringDescriptors();
303 Arrays.sort(proxies, new Comparator<RefactoringDescriptorProxy>() {
305 public final int compare(final RefactoringDescriptorProxy first, final RefactoringDescriptorProxy second) {
306 final long delta= first.getTimeStamp() - second.getTimeStamp();
315 OutputStream output= null;
317 file= File.createTempFile("history", null); //$NON-NLS-1$
318 output= new BufferedOutputStream(new FileOutputStream(file));
320 RefactoringCore.getHistoryService().writeRefactoringDescriptors(proxies, output, RefactoringDescriptor.NONE, false, monitor);
324 } catch (IOException exception) {
327 writeMetaData(data, file, path);
329 if (output != null) {
332 } catch (IOException exception) {
344 * Checks if the JAR file can be overwritten.
345 * If the JAR package setting does not allow to overwrite the JAR
346 * then a dialog will ask the user again.
348 * @param parent the parent for the dialog,
349 * or <code>null</code> if no dialog should be presented
350 * @return <code>true</code> if it is OK to create the JAR
352 protected boolean canCreateJar(Shell parent) {
353 File file= fJarPackage.getAbsoluteJarLocation().toFile();
355 if (!file.canWrite())
357 if (fJarPackage.allowOverwrite())
359 return parent != null && JarPackagerUtil.askForOverwritePermission(parent, fJarPackage.getAbsoluteJarLocation(), true);
362 // Test if directory exists
363 String path= file.getAbsolutePath();
364 int separatorIndex = path.lastIndexOf(File.separator);
365 if (separatorIndex == -1) // i.e.- default directory, which is fine
367 File directory= new File(path.substring(0, separatorIndex));
368 if (!directory.exists()) {
369 if (JarPackagerUtil.askToCreateDirectory(parent, directory))
370 return directory.mkdirs();
378 * Closes the archive and does all required cleanup.
380 * @throws CoreException
381 * to signal any other unusual termination. This can also be
382 * used to return information in the status object.
384 public void close() throws CoreException {
385 if (fJarOutputStream != null)
387 fJarOutputStream.close();
388 registerInWorkspaceIfNeeded();
389 } catch (IOException ex) {
390 throw JarPackagerUtil.createCoreException(ex.getLocalizedMessage(), ex);
394 private void registerInWorkspaceIfNeeded() {
395 IPath jarPath= fJarPackage.getAbsoluteJarLocation();
396 IProject[] projects= ResourcesPlugin.getWorkspace().getRoot().getProjects();
397 for (int i= 0; i < projects.length; i++) {
398 IProject project= projects[i];
399 // The Jar is always put into the local file system. So it can only be
400 // part of a project if the project is local as well. So using getLocation
401 // is currently save here.
402 IPath projectLocation= project.getLocation();
403 if (projectLocation != null && projectLocation.isPrefixOf(jarPath)) {
405 jarPath= jarPath.removeFirstSegments(projectLocation.segmentCount());
406 jarPath= jarPath.removeLastSegments(1);
407 IResource containingFolder= project.findMember(jarPath);
408 if (containingFolder != null && containingFolder.isAccessible())
409 containingFolder.refreshLocal(IResource.DEPTH_ONE, null);
410 } catch (CoreException ex) {
411 // don't refresh the folder but log the problem
419 * Writes the passed resource to the current archive.
422 * the file to be written
423 * @param destinationPath
424 * the path for the file inside the archive
425 * @throws CoreException
426 * to signal any other unusual termination. This can also be
427 * used to return information in the status object.
429 public void write(IFile resource, IPath destinationPath) throws CoreException {
431 if (fJarPackage.areDirectoryEntriesIncluded())
432 addDirectories(resource, destinationPath);
433 addFile(resource, destinationPath);
434 } catch (IOException ex) {
435 // Ensure full path is visible
436 String message= null;
437 if (ex.getLocalizedMessage() != null)
438 message= Messages.format(JarPackagerMessages.JarWriter_writeProblemWithMessage, new Object[] {BasicElementLabels.getPathLabel(resource.getFullPath(), false), ex.getLocalizedMessage()});
440 message= Messages.format(JarPackagerMessages.JarWriter_writeProblem, BasicElementLabels.getPathLabel(resource.getFullPath(), false));
441 throw JarPackagerUtil.createCoreException(message, ex);
446 * Writes the meta file to the JAR file.
449 * the jar package data
451 * the file containing the meta data
453 * the path of the meta file within the archive
454 * @throws FileNotFoundException
455 * if the meta file could not be found
456 * @throws IOException
457 * if an input/output error occurs
459 private void writeMetaData(final JarPackageData data, final File file, final IPath path) throws FileNotFoundException, IOException {
460 Assert.isNotNull(data);
461 Assert.isNotNull(file);
462 Assert.isNotNull(path);
463 final JarEntry entry= new JarEntry(path.toString().replace(File.separatorChar, '/'));
464 byte[] buffer= new byte[4096];
465 if (data.isCompressed())
466 entry.setMethod(ZipEntry.DEFLATED);
468 entry.setMethod(ZipEntry.STORED);
469 JarPackagerUtil.calculateCrcAndSize(entry, new BufferedInputStream(new FileInputStream(file)), buffer);
471 entry.setTime(System.currentTimeMillis());
472 final InputStream stream= new BufferedInputStream(new FileInputStream(file));
474 fJarOutputStream.putNextEntry(entry);
476 while ((count= stream.read(buffer, 0, buffer.length)) != -1)
477 fJarOutputStream.write(buffer, 0, count);
481 } catch (IOException exception) {