/******************************************************************************* * Copyright (c) 2015 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.dash.cloudfoundry.packaging; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.springframework.boot.loader.tools.JarWriter; import org.springframework.boot.loader.tools.Libraries; import org.springframework.boot.loader.tools.Library; import org.springframework.boot.loader.tools.LibraryCallback; import org.springframework.boot.loader.tools.LibraryScope; import org.springframework.boot.loader.tools.Repackager; import org.springframework.ide.eclipse.boot.core.ISpringBootProject; import org.springframework.ide.eclipse.boot.core.SpringBootCore; import org.springframework.ide.eclipse.boot.dash.model.UserInteractions; import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate; import org.springframework.ide.eclipse.boot.util.FileUtil; import org.springframework.ide.eclipse.boot.util.JavaProjectUtil; import org.springframework.ide.eclipse.boot.util.Log; import org.springsource.ide.eclipse.commons.frameworks.core.maintype.MainTypeFinder; public class CloudApplicationArchiverStrategyAsJar implements CloudApplicationArchiverStrategy { private static final String TEMP_FOLDER_NAME = "springidetempFolderForJavaAppJar"; private static final boolean DEBUG = false; private static void debug(String string) { if (DEBUG) { System.out.println(string); } } /** * Classpath entries spilt into two lists, one that correspond to the current project's output folders * and all the others (which correspond to the project's dependencies). The dependencies could be * jars or output folders for other projects in the workspace. */ private static class SplitClasspath { private List<File> projectContents = new ArrayList<>(2); //one or two is typical private List<File> dependencies = new ArrayList<>(); public SplitClasspath(IJavaProject jp, File[] entries) { Set<File> outputFolders = toFileSet(JavaProjectUtil.getOutputFolders(jp)); for (File file : entries) { if (contains(outputFolders, file)) { projectContents.add(file); } else { dependencies.add(file); } } } private boolean contains(Set<File> outputFolders, File file) { return outputFolders.contains(canonical(file)); } private File canonical(File file) { try { return file.getCanonicalFile(); } catch (IOException e) { //Next best thing: return file.getAbsoluteFile(); } } /** * Convert a collection of Eclipse IContainer to List of java.io.File. Containers that * don't correspond to stuff on disk are silently ignored. */ private Set<File> toFileSet(Set<IContainer> containers) { Set<File> files = new HashSet<>(); for (IContainer folder : containers) { IPath loc = folder.getLocation(); if (loc!=null) { File file = loc.toFile(); files.add(canonical(file)); //use canonical file to make equals / Set work as expected. } } return files; } @Override public String toString() { StringBuilder builder = new StringBuilder("SplitClasspath(\n"); for (File file : projectContents) { builder.append(" "+file+"\n"); } builder.append(" ------------\n"); for (File file : dependencies) { builder.append(" "+file+"\n"); } builder.append(")"); return builder.toString(); } } private static final File[] NO_FILES = new File[]{}; private static class Archiver implements ICloudApplicationArchiver { private IJavaProject jp; private IType mainType; private ILaunchConfiguration conf; private BootLaunchConfigurationDelegate delegate; private JarNameGenerator jarNames; private File _tempFolder; Archiver(IJavaProject jp, IType mainType) throws CoreException { this.jp = jp; this.mainType = mainType; this.conf = BootLaunchConfigurationDelegate.createWorkingCopy(mainType); this.delegate = new BootLaunchConfigurationDelegate(); this.jarNames = new JarNameGenerator(); } private SplitClasspath getRuntimeClasspath() throws CoreException { return new SplitClasspath(jp, toFiles(delegate.getClasspath(conf))); } private File[] toFiles(String[] classpath) { if (classpath!=null) { File[] files = new File[classpath.length]; for (int i = 0; i < files.length; i++) { files[i] = new File(classpath[i]); } return files; } return NO_FILES; } @Override public File getApplicationArchive(IProgressMonitor mon) throws Exception { SplitClasspath classpath = getRuntimeClasspath(); File tempFolder = getTempFolder(); File baseJar = new File(tempFolder, jp.getElementName()+".original.jar"); File repackagedJar = new File(tempFolder, jp.getElementName()+".repackaged.jar"); createBaseJar(classpath.projectContents, baseJar); repackage(baseJar, classpath.dependencies, repackagedJar); return repackagedJar; } private File getTempFolder() throws IOException { if (_tempFolder==null) { _tempFolder = FileUtil.getTempFolder(TEMP_FOLDER_NAME); } // TODO Auto-generated method stub return _tempFolder; } private void createBaseJar(List<File> projectContents, File baseJar) throws FileNotFoundException, IOException { JarWriter jarWriter = new JarWriter(baseJar); try { for (File outputFolder : projectContents) { writeFolder(jarWriter, outputFolder); } } finally { jarWriter.close(); } } private void writeFolder(JarWriter jarWriter, File baseFolder) throws FileNotFoundException, IOException { for (String name : baseFolder.list()) { write(jarWriter, baseFolder, name); } } private void write(JarWriter jarWriter, File baseFolder, String relativePath) throws FileNotFoundException, IOException { debug("Writing: "+relativePath + " from "+baseFolder); File file = new File(baseFolder, relativePath); if (file.isDirectory()) { debug("Folder"); for (String name : file.list()) { write(jarWriter, baseFolder, pathJoin(relativePath, name)); } } else if (file.isFile()) { debug("File"); jarWriter.writeEntry(relativePath, new FileInputStream(file)); } else { debug("Huh?"); } } private String pathJoin(String relativePath, String name) { return relativePath + "/" +name; } private void repackage(File baseJar, List<File> dependencies, File repackagedJar) throws IOException { Repackager repackager = new Repackager(baseJar); repackager.setMainClass(mainType.getFullyQualifiedName()); repackager.repackage(repackagedJar, asLibraries(dependencies)); } private Libraries asLibraries(final List<File> dependencies) { return new Libraries() { public void doWithLibraries(LibraryCallback callback) throws IOException { for (File dep : dependencies) { if (dep.isFile()) { callback.library(new Library(jarNames.createName(dep), dep, LibraryScope.COMPILE, false)); } else if (dep.isDirectory()) { String jarName = jarNames.createName(dep); File jarFile = new File(getTempFolder(), jarName); JarWriter jarWriter = new JarWriter(jarFile); try { writeFolder(jarWriter, dep); } finally { jarWriter.close(); } callback.library(new Library(jarName, jarFile, LibraryScope.COMPILE, false)); } } } }; } } private SpringBootCore springBootCore = SpringBootCore.getDefault(); private IProject project; private UserInteractions ui; public CloudApplicationArchiverStrategyAsJar(IProject project, UserInteractions ui) { this.project = project; this.ui = ui; } @Override public ICloudApplicationArchiver getArchiver(IProgressMonitor mon) { try { final IJavaProject jp = getJavaProject(); if (jp!=null && checkPackagingType(jp)) { final IType type = getMainType(jp, mon); if (type!=null) { return new Archiver(jp, type); } } } catch (Exception e) { Log.log(e); } return null; } private boolean checkPackagingType(IJavaProject jp) throws CoreException { ISpringBootProject bootProject = springBootCore.project(jp); if (bootProject==null) { //Gradle is poorly supported. We don't know how to determin packaging type. So just // give such projects the benefit of the doubdt. They *might have the correct // packaging type. return true; } return ISpringBootProject.PACKAGING_JAR.equals(bootProject.getPackaging()); } private IJavaProject getJavaProject() { try { if (project.isAccessible() && project.hasNature(JavaCore.NATURE_ID)) { return JavaCore.create(project); } } catch (Exception e) { Log.log(e); } return null; } private IType getMainType(IJavaProject jp, IProgressMonitor mon) { try { IType[] candidates = MainTypeFinder.guessMainTypes(jp, mon); if (candidates!=null && candidates.length>0) { if (candidates.length==1) { return candidates[0]; } else { //TODO: should persist main type so we don't ask again next time. // however we persist this, user must be able to change it. // Prolly we should create a launchconf to store info like this and // create UI for user to modify it. return ui.chooseMainType(candidates, "Choose a main type", "Deploying a standalone boot-app requires " + "that the main type is identified. We found several candidates, please choose one." ); } } } catch (Exception e) { Log.log(e); } return null; } }