/*
* Copyright Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the authors tag. All rights reserved.
*
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU General Public License version 2.
*
* This particular file is subject to the "Classpath" exception as provided in the
* LICENSE file that accompanied this code.
*
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License,
* along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package com.redhat.ceylon.compiler.java.tools;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import com.redhat.ceylon.cmr.api.ArtifactContext;
import com.redhat.ceylon.cmr.api.RepositoryManager;
import com.redhat.ceylon.cmr.api.ArtifactCreator;
import com.redhat.ceylon.cmr.ceylon.CeylonUtils;
import com.redhat.ceylon.cmr.util.JarUtils;
import com.redhat.ceylon.common.Constants;
import com.redhat.ceylon.common.FileUtil;
import com.redhat.ceylon.common.log.Logger;
import com.redhat.ceylon.compiler.java.tools.JarEntryManifestFileObject.OsgiManifest;
import com.redhat.ceylon.compiler.typechecker.model.Module;
import com.sun.source.util.TaskListener;
import com.sun.tools.javac.main.OptionName;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Options;
public class JarOutputRepositoryManager {
private Map<Module,ProgressiveJar> openJars = new HashMap<Module, ProgressiveJar>();
private Log log;
private Options options;
private CeyloncFileManager ceyloncFileManager;
private TaskListener taskListener;
JarOutputRepositoryManager(Log log, Options options, CeyloncFileManager ceyloncFileManager, TaskListener taskListener){
this.log = log;
this.options = options;
this.ceyloncFileManager = ceyloncFileManager;
this.taskListener = taskListener;
}
public JavaFileObject getFileObject(RepositoryManager repositoryManager, Module module, String fileName, File sourceFile) throws IOException{
ProgressiveJar progressiveJar = getProgressiveJar(repositoryManager, module);
return progressiveJar.getJavaFileObject(fileName, sourceFile);
}
private ProgressiveJar getProgressiveJar(RepositoryManager repositoryManager, Module module) throws IOException {
ProgressiveJar jarFile = openJars.get(module);
if(jarFile == null){
jarFile = new ProgressiveJar(repositoryManager, module, log, options, ceyloncFileManager, taskListener);
openJars.put(module, jarFile);
}
return jarFile;
}
public void flush() throws IOException {
Exception ex = null;
try{
for(ProgressiveJar jarFile : openJars.values()){
try {
jarFile.close();
} catch (Exception e) {
ex = e;
}
}
}finally{
// make sure we clear on return and throw, so we don't try to flush again on throw
openJars.clear();
}
if (ex instanceof IOException) {
throw (IOException)ex;
}
}
static class ProgressiveJar {
private static final String META_INF = "META-INF";
private static final String MAPPING_FILE = META_INF+"/mapping.txt";
private File originalJarFile;
private File outputJarFile;
private JarOutputStream jarOutputStream;
final private Set<String> modifiedSourceFiles = new HashSet<String>();
final private Set<String> modifiedResourceFilesRel = new HashSet<String>();
final private Set<String> modifiedResourceFilesFull = new HashSet<String>();
final private Properties writtenClassesMapping = new Properties();
private Logger cmrLog;
private Options options;
private RepositoryManager repoManager;
private ArtifactContext carContext;
private ArtifactCreator srcCreator;
private ArtifactCreator resourceCreator;
private Module module;
private Set<String> folders = new HashSet<String>();
private boolean manifestWritten = false;
private boolean writeOsgiManifest;
private final String resourceRootPath;
private boolean writeMavenManifest;
private TaskListener taskListener;
public ProgressiveJar(RepositoryManager repoManager, Module module, Log log, Options options, CeyloncFileManager ceyloncFileManager, TaskListener taskListener) throws IOException{
this.options = options;
this.repoManager = repoManager;
this.carContext = new ArtifactContext(module.getNameAsString(), module.getVersion(), ArtifactContext.CAR);
this.cmrLog = new JavacLogger(options, Log.instance(ceyloncFileManager.getContext()));
this.srcCreator = CeylonUtils.makeSourceArtifactCreator(
repoManager,
ceyloncFileManager.getLocation(StandardLocation.SOURCE_PATH),
module.getNameAsString(), module.getVersion(),
options.get(OptionName.VERBOSE) != null, cmrLog);
this.resourceCreator = CeylonUtils.makeResourceArtifactCreator(
repoManager,
ceyloncFileManager.getLocation(StandardLocation.SOURCE_PATH),
ceyloncFileManager.getLocation(CeylonLocation.RESOURCE_PATH),
options.get(OptionName.CEYLONRESOURCEROOT),
module.getNameAsString(), module.getVersion(),
options.get(OptionName.VERBOSE) != null, cmrLog);
this.module = module;
this.writeOsgiManifest = !options.isSet(OptionName.CEYLONNOOSGI);
this.writeMavenManifest = !options.isSet(OptionName.CEYLONNOPOM);
// Determine the special path that signals that the files it contains
// should be moved to the root of the output JAR/CAR
String rrp = module.getNameAsString().replace('.', '/');
if (!rrp.isEmpty() && !rrp.endsWith("/")) {
rrp = rrp + "/";
}
String rootName = options.get(OptionName.CEYLONRESOURCEROOT);
if (rootName == null) {
rootName = Constants.DEFAULT_RESOURCE_ROOT;
}
this.resourceRootPath = rrp + rootName + "/";
this.taskListener = taskListener;
this.originalJarFile = repoManager.getArtifact(carContext);
this.outputJarFile = File.createTempFile("ceylon-compiler-", ".car");
this.jarOutputStream = new JarOutputStream(new FileOutputStream(outputJarFile));
}
private Properties getPreviousMapping() throws IOException {
if (originalJarFile != null) {
JarFile jarFile = null;
jarFile = new JarFile(originalJarFile);
try {
JarEntry entry = jarFile.getJarEntry(MAPPING_FILE);
if (entry != null) {
InputStream inputStream = jarFile.getInputStream(entry);
try {
Properties previousMapping = new Properties();
previousMapping.load(inputStream);
return previousMapping;
} finally {
inputStream.close();
}
}
} finally {
jarFile.close();
}
}
return null;
}
private Manifest getPreviousManifest() throws IOException {
if (originalJarFile != null) {
JarFile jarFile = null;
jarFile = new JarFile(originalJarFile);
try {
return jarFile.getManifest();
} finally {
jarFile.close();
}
}
return null;
}
public void close() throws IOException {
try {
Set<String> copiedSourceFiles = srcCreator.copy(modifiedSourceFiles);
resourceCreator.copy(modifiedResourceFilesFull);
if (writeOsgiManifest && !manifestWritten) {
Manifest manifest = new OsgiManifest(module, getPreviousManifest()).build();
writeManifestJarEntry(manifest);
}
if (writeMavenManifest) {
writeMavenManifest(module);
}
Properties previousMapping = getPreviousMapping();
writeMappingJarEntry(previousMapping, getJarFilter(previousMapping, copiedSourceFiles));
JarUtils.finishUpdatingJar(
originalJarFile, outputJarFile, carContext, jarOutputStream,
getJarFilter(previousMapping, copiedSourceFiles),
repoManager, options.get(OptionName.VERBOSE) != null, cmrLog, folders, options.isSet(OptionName.CEYLONPACK200));
String info;
if(module.isDefault())
info = module.getNameAsString();
else
info = module.getNameAsString() + "/" + module.getVersion();
cmrLog.info("Created module " + info);
if(taskListener instanceof CeylonTaskListener){
((CeylonTaskListener) taskListener).moduleCompiled(module.getNameAsString(), module.getVersion());
}
} finally {
FileUtil.deleteQuietly(outputJarFile);
}
}
private JarUtils.JarEntryFilter getJarFilter(final Properties previousMapping, final Set<String> copiedSourceFiles) {
return new JarUtils.JarEntryFilter() {
@Override
public boolean avoid(String entryFullName) {
if (entryFullName.endsWith(".class")) {
boolean classWasUpdated = writtenClassesMapping.containsKey(entryFullName);
if (previousMapping != null) {
String sourceFileForClass = previousMapping.getProperty(entryFullName);
classWasUpdated = classWasUpdated || copiedSourceFiles.contains(sourceFileForClass);
}
return classWasUpdated;
} else {
return modifiedResourceFilesRel.contains(entryFullName)
|| entryFullName.equals(MAPPING_FILE)
|| (writeOsgiManifest && OsgiManifest.isManifestFileName(entryFullName))
|| (writeMavenManifest && MavenPomUtil.isMavenDescriptor(entryFullName, module));
}
}
};
}
private void writeManifestJarEntry(Manifest manifest) {
try {
folders.add(META_INF+"/");
jarOutputStream.putNextEntry(new ZipEntry(OsgiManifest.MANIFEST_FILE_NAME));
manifest.write(jarOutputStream);
}
catch (IOException e) {
// TODO : log to the right place
}
finally {
try {
jarOutputStream.closeEntry();
}
catch (IOException ignore) {
}
}
}
private void writeMavenManifest(Module module) {
MavenPomUtil.writeMavenManifest(jarOutputStream, module, folders);
}
private void writeMappingJarEntry(Properties previousMapping, JarUtils.JarEntryFilter filter) {
Properties newMapping = new Properties();
newMapping.putAll(writtenClassesMapping);
if (previousMapping != null) {
// Add the previous mapping entries that are not related to an updated source file
for (String classFullName : previousMapping.stringPropertyNames()) {
if (!filter.avoid(classFullName)) {
newMapping.setProperty(classFullName, previousMapping.getProperty(classFullName));
}
}
}
// Write the mapping file to the Jar
try {
folders.add(META_INF+"/");
jarOutputStream.putNextEntry(new ZipEntry(MAPPING_FILE));
newMapping.store(jarOutputStream, "");
}
catch(IOException e) {
// TODO : log to the right place
}
finally {
try {
jarOutputStream.closeEntry();
} catch (IOException e) {
}
}
}
public JavaFileObject getJavaFileObject(String fileName, File sourceFile) {
String entryName = fileName.replace(File.separatorChar, '/');
if (!resourceRootPath.isEmpty() && entryName.startsWith(resourceRootPath)) {
// Files in the special "resource root path" get moved
// to the root of the output JAR/CAR
entryName = entryName.substring(resourceRootPath.length());
}
String folder = JarUtils.getFolder(entryName);
if (folder != null) {
folders.add(folder);
}
if (sourceFile != null) {
modifiedSourceFiles.add(sourceFile.getPath());
// record the class file we produce so that we don't save it from the original jar
addMappingEntry(entryName, JarUtils.toPlatformIndependentPath(srcCreator.getPaths(), sourceFile.getPath()));
} else {
modifiedResourceFilesRel.add(entryName);
modifiedResourceFilesFull.add(FileUtil.applyPath(resourceCreator.getPaths(), fileName).getPath());
if (writeOsgiManifest && OsgiManifest.isManifestFileName(entryName)) {
this.manifestWritten = true;
return new JarEntryManifestFileObject(outputJarFile.getPath(), jarOutputStream, entryName, module);
}
}
return new JarEntryFileObject(outputJarFile.getPath(), jarOutputStream, entryName);
}
private void addMappingEntry(String className,
String sourcePath) {
writtenClassesMapping.put(className, sourcePath);
}
}
}