/*
* Copyright (c) 2007-2009, Osmorc Development Team
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright notice, this list
* of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this
* list of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
* * Neither the name of 'Osmorc Development Team' nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.osmorc.make;
import aQute.lib.osgi.Analyzer;
import aQute.lib.osgi.Builder;
import aQute.lib.osgi.Jar;
import aQute.lib.osgi.Verifier;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.compiler.CompileContext;
import com.intellij.openapi.compiler.CompilerMessageCategory;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.framework.Constants;
import org.osmorc.StacktraceUtil;
import org.osmorc.frameworkintegration.LibraryBundlificationRule;
import org.osmorc.settings.ApplicationSettings;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Class which wraps bnd and integrates it into IntellIJ.
* <p/>
* TODO: pedantic setting
*
* @author <a href="mailto:janthomae@janthomae.de">Jan Thomä</a>
* @version $Id:$
*/
public class BndWrapper {
/**
* Wraps an existing jar file using bnd's analyzer. This class will check and use any applying bundlification rules
* for this library that have been set up in Osmorcs library bundlification dialog.
* <p/>
*
*
* @param module
* @param compileContext a compile context
* @param sourceJarUrl the URL to the source jar file
* @param outputPath the path where to place the bundled library.
* @return the URL to the bundled library.
*/
@Nullable
public String wrapLibrary(Module module, @NotNull CompileContext compileContext, final String sourceJarUrl, String outputPath) {
String messagePrefix = "[" + module.getName() + "] ";
try {
File targetDir = new File(outputPath);
File sourceFile = new File(VfsUtil.urlToPath(sourceJarUrl));
if (!sourceFile.exists() ) {
compileContext.addMessage(CompilerMessageCategory.WARNING, messagePrefix + "The library " + sourceFile.getPath() + " does not exist. Please check your module settings. Ignoring missing library.", null, 0,0);
return null;
}
if ( sourceFile.isDirectory() ) {
// ok it's an exploded directory, we cannot bundle it.
return null;
}
File targetFile = new File(targetDir.getPath() + File.separator + sourceFile.getName());
Map<String, String> additionalProperties = new HashMap<String, String>();
// okay try to find a rule for this nice package:
long lastModified = Long.MIN_VALUE;
ApplicationSettings settings = ServiceManager.getService(ApplicationSettings.class);
for (LibraryBundlificationRule bundlificationRule : settings.getLibraryBundlificationRules()) {
if (bundlificationRule.appliesTo(sourceFile.getName())) {
if (bundlificationRule.isDoNotBundle()) {
return null; // make it quick in this case
}
additionalProperties.putAll(bundlificationRule.getAdditionalPropertiesMap());
// if a rule applies which has been changed recently we need to re-bundle the file
lastModified = Math.max(lastModified, bundlificationRule.getLastModified());
// if stop after this rule is true, we will no longer try to find any more matching rules
if (bundlificationRule.isStopAfterThisRule()) {
break;
}
}
}
if (!targetFile.exists() || targetFile.lastModified() < sourceFile.lastModified() ||
targetFile.lastModified() < lastModified) {
if (doWrap(module, compileContext, sourceFile, targetFile, additionalProperties)) {
return VfsUtil.pathToUrl(targetFile.getCanonicalPath());
}
} else {
// Fixes IDEADEV-39099. When the wrapper does not return anything the library is not regarded
// as a bundle.
return VfsUtil.pathToUrl(targetFile.getCanonicalPath());
}
}
catch (final Exception e) {
// There is some reported issue where a lot of exceptions have been thrown which caused a ton of popup
// boxes, so we better put this into the compile context as normal error message. Can't reproduce the issue
// but i think it's stil the better way.
// IDEA-27101
// IDEA-69149 - Changed this form ERROR to WARNING, as a non-bundlified library might not be fatal (especially when importing a ton of libs from maven)
compileContext.addMessage(CompilerMessageCategory.WARNING,
MessageFormat.format(messagePrefix + "There was an unexpected problem when trying to bundlify {0}: {1}", sourceJarUrl, StacktraceUtil.stackTraceToString(e)), null, 0, 0);
}
return null;
}
/**
* Internal function which does the actual wrapping. This is 90% borrowed from bnd's source code.
*
*
* @param module
* @param compileContext the compile context
* @param inputJar the input file
* @param outputJar the output file
* @param properties properties for the manifest. these may contain bnd instructions
* @return true if the bundling was successful, false otherwise.
* @throws Exception in case something goes wrong.
*/
private boolean doWrap(@NotNull Module module,
@NotNull final CompileContext compileContext,
@NotNull File inputJar,
@NotNull final File outputJar,
@NotNull Map<String, String> properties) throws Exception {
final String messagePrefix = "[" + module.getName() + "][Library " + inputJar.getName() + "] ";
String sourceFileUrl = VfsUtil.pathToUrl(inputJar.getPath());
Analyzer analyzer = new ReportingAnalyzer(compileContext, sourceFileUrl);
analyzer.setPedantic(false);
analyzer.setJar(inputJar);
Jar dot = analyzer.getJar();
analyzer.putAll(properties, false);
if (analyzer.getProperty(Constants.IMPORT_PACKAGE) == null) {
analyzer.setProperty(Constants.IMPORT_PACKAGE, "*;resolution:=optional");
}
if (analyzer.getProperty(Constants.BUNDLE_SYMBOLICNAME) == null) {
Pattern p = Pattern.compile("(" + Verifier.SYMBOLICNAME.pattern() + ")(-[0-9])?.*\\.jar");
String base = inputJar.getName();
Matcher m = p.matcher(base);
if (m.matches()) {
base = m.group(1);
} else {
compileContext.addMessage(CompilerMessageCategory.ERROR,
messagePrefix + "Can not calculate name of output bundle, rename jar or use -properties", sourceFileUrl, 0, 0);
return false;
}
analyzer.setProperty(Constants.BUNDLE_SYMBOLICNAME, base);
}
if (analyzer.getProperty(Constants.EXPORT_PACKAGE) == null) {
// avoid spurious error messages about string starting with ","
// String export = analyzer.calculateExportsFromContents(dot).replaceFirst("^\\s*,", "");
analyzer.setProperty(Constants.EXPORT_PACKAGE, "*");
// analyzer.setProperty(Constants.EXPORT_PACKAGE, export);
}
analyzer.mergeManifest(dot.getManifest());
String version = analyzer.getProperty(Constants.BUNDLE_VERSION);
if (version != null) {
version = Builder.cleanupVersion(version);
analyzer.setProperty(Constants.BUNDLE_VERSION, version);
}
Manifest mf = analyzer.calcManifest();
Jar jar = analyzer.getJar();
final File f = FileUtil.createTempFile("tmpbnd", ".jar");
jar.write(f);
jar.close();
analyzer.close();
// IDEA-26817 delete the old bundle, so the renameTo later works...
if ( outputJar.exists() ) {
if ( !outputJar.delete() ) {
compileContext.addMessage(CompilerMessageCategory.ERROR,
messagePrefix + "Could not delete outdated generated bundle. Is " + outputJar.getPath() + " writable?", null, 0, 0);
return false;
}
}
final Ref<Boolean> result = new Ref<Boolean>(false);
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
public void run() {
result.set(ApplicationManager.getApplication().runWriteAction(new Computable<Boolean>() {
public Boolean compute() {
// this should work in 99% of the cases
if (!f.renameTo(outputJar)) {
// and this is for the remaining 1%.
VirtualFile src = LocalFileSystem.getInstance().findFileByIoFile(f);
if (src == null) {
compileContext.addMessage(CompilerMessageCategory.ERROR,
messagePrefix + "No jar file was created. This should not happen. Is " + f.getPath() + " writable?", null, 0,
0);
return false;
}
// make sure the parent folder exists:
File parentFolder = outputJar.getParentFile();
if (!parentFolder.exists()) {
if (!parentFolder.mkdirs()) {
compileContext
.addMessage(CompilerMessageCategory.ERROR, messagePrefix + "Cannot create output folder. Is " + parentFolder.getPath() + " writable?",
null, 0, 0);
return false;
}
}
// now get the target folder
VirtualFile target = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(parentFolder);
if (target == null) {
// this actually should not happen but since we are bound by murphy's law, we check this as well
// and believe it or not it DID happen.
compileContext.addMessage(CompilerMessageCategory.ERROR, messagePrefix + "Output path " +
parentFolder.getPath() +
" was created but cannot be found anymore. This should not happen.",
null, 0, 0);
return false;
}
// IDEA-26817: target must be the dir, not the the file, so:
// and then put this in. This should produce the correct result.
try {
VfsUtilCore.copyFile(this, src, target, outputJar.getName());
}
catch (IOException e) {
compileContext.addMessage(CompilerMessageCategory.ERROR, messagePrefix + "Could not copy " + src + " to " + target, null, 0, 0);
return false;
}
}
return true;
}
}));
}
}, ModalityState.defaultModalityState());
return result.get();
}
public boolean build(@NotNull Module module, @NotNull CompileContext compileContext, @NotNull String bndFileUrl,
@NotNull String[] classPathUrls, @NotNull String outputPath,
@NotNull Map<String, String> additionalProperties) {
String messagePrefix = "["+module.getName()+"] ";
File[] classPathEntries = new File[classPathUrls.length];
for (int i = 0; i < classPathUrls.length; i++) {
String classPathUrl = classPathUrls[i];
classPathEntries[i] = new File(VfsUtil.urlToPath(classPathUrl));
}
File bndFile = new File(VfsUtil.urlToPath(bndFileUrl));
File outFile = new File(outputPath);
Properties props = new Properties();
for (Map.Entry<String, String> stringStringEntry : additionalProperties.entrySet()) {
props.setProperty(stringStringEntry.getKey(), stringStringEntry.getValue());
}
try {
return doBuild(module, compileContext, bndFile, classPathEntries, outFile, props);
}
catch (Exception e) {
compileContext.addMessage(CompilerMessageCategory.ERROR, messagePrefix + "Unexpected error: " + e.getMessage(), null, 0, 0);
return false;
}
}
private boolean doBuild(@NotNull Module module, @NotNull CompileContext compileContext, @NotNull File bndFile, @NotNull File[] classpath,
@NotNull File output, @NotNull Properties additionalProperties)
throws Exception {
String messagePrefix = "["+module.getName()+"] ";
ReportingBuilder builder = new ReportingBuilder(compileContext, VfsUtil.pathToUrl(bndFile.getPath()), module);
builder.setPedantic(false);
builder.setProperties(bndFile);
builder.mergeProperties(additionalProperties, true);
// FIX for IDEADEV-39089
// am not really sure if this is a good idea all the time but then again what use is building a bundle without exports in 90% of the cases?
//if (builder.getProperty(Constants.EXPORT_PACKAGE) == null) {
// builder.setProperty(Constants.EXPORT_PACKAGE, "*");
//}
builder.setClasspath(classpath);
// XXX: seems to be a new bug in bnd, when calling build(), begin is not called, therefore the ignores dont work..
// so i have overridden it and calling it manually here..
builder.begin();
// Check if the manifest version is missing (IDEADEV-41174)
String manifest = builder.getProperty(ReportingBuilder.MANIFEST);
if (manifest != null) {
File manifestFile = builder.getFile(manifest);
if (manifestFile != null && manifestFile.exists() && manifestFile.canRead()) {
Properties props = new Properties();
FileInputStream fileInputStream = new FileInputStream(manifestFile);
try {
props.load(fileInputStream);
final String value = props.getProperty(Attributes.Name.MANIFEST_VERSION.toString());
if (value == null || value.length() == 0 || value.trim().length() == 0) {
compileContext.addMessage(CompilerMessageCategory.WARNING,
messagePrefix + "Your manifest does not contain a Manifest-Version entry. This will produce an empty manifest in the resulting bundle.",
VfsUtil.pathToUrl(manifestFile.getAbsolutePath()), 0, 0);
}
}
catch (Exception ex) {
compileContext.addMessage(CompilerMessageCategory.INFORMATION, messagePrefix + "There was a problem reading your manifest.", VfsUtil.pathToUrl(manifestFile.getAbsolutePath()) , 0, 0);
}
finally {
fileInputStream.close();
}
}
}
Jar jar = builder.build();
jar.setName(output.getName());
jar.write(output);
builder.close();
return true;
}
}