/*
* 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 com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.compiler.*;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osmorc.BundleManager;
import org.osmorc.facet.OsmorcFacet;
import org.osmorc.facet.OsmorcFacetConfiguration;
import org.osmorc.frameworkintegration.CachingBundleInfoProvider;
import org.osmorc.frameworkintegration.LibraryHandler;
import org.osmorc.i18n.OsmorcBundle;
import org.osmorc.manifest.BundleManifest;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This is a compiler step that builds up a bundle. Depending on user settings the compiler either uses a user-edited
* manifest or builds up a manifest using bnd.
*
* @author <a href="mailto:janthomae@janthomae.de">Jan Thomä</a>
* @version $Id$
*/
public class BundleCompiler implements PackagingCompiler {
private final Logger logger = Logger.getInstance("#org.osmorc.make.BundleCompiler");
public static final Condition<OrderEntry> NOT_FRAMEWORK_LIBRARY_CONDITION = new Condition<OrderEntry>() {
@Override
public boolean value(OrderEntry entry) {
return !(entry instanceof LibraryOrderEntry) || !ServiceManager.getService(LibraryHandler.class).isFrameworkInstanceLibrary((LibraryOrderEntry) entry);
}
};
@Nullable
private static String getOutputPath(final Module m, CompileContext context) {
final CompilerModuleExtension extension = CompilerModuleExtension.getInstance(m);
VirtualFile moduleCompilerOutputPath = extension.getCompilerOutputPath();
String path;
if (moduleCompilerOutputPath == null) {
// get the url
String outputPathUrl = extension.getCompilerOutputUrl();
// create the paths
// FIX IDEADEV-40112
File f = new File(VfsUtil.urlToPath(outputPathUrl));
if (!f.exists() && !f.mkdirs()) {
context.addMessage(CompilerMessageCategory.ERROR, OsmorcBundle.getTranslation("faceteditor.cannot.create.outputpath"), null, 0,0);
return null;
}
path = f.getParentFile().getPath() + File.separator + "bundles";
}
else {
path = moduleCompilerOutputPath.getParent().getPath() + File.separator + "bundles";
}
File f = new File(path);
if (!f.exists()) {
if (!f.mkdirs()) {
context.addMessage(CompilerMessageCategory.ERROR, "Could not create output path: " + path + " Please check file permissions.", null, 0,0 );
return null;
}
}
return path;
}
/**
* Deletes the jar file of a bundle when it is outdated.
*
* @param compileContext the compile context
* @param s ??
* @param validityState the validity state of the item that is outdated
*/
public void processOutdatedItem(CompileContext compileContext, String s,
@Nullable final ValidityState validityState) {
// delete the jar file of the module in case the stuff is outdated
// TODO: find a way to update jar files so we can speed this up
if (validityState != null) {
ApplicationManager.getApplication().runReadAction(new Runnable() {
public void run() {
BundleValidityState myvalstate = (BundleValidityState) validityState;
//noinspection ConstantConditions
String jarUrl = myvalstate.getOutputJarUrl();
if (jarUrl != null) {
FileUtil.delete(new File(jarUrl));
}
}
}
);
}
}
/**
* Returns all processingitems (== Bundles to be created) for the givne compile context
*
* @param compileContext the compile context
* @return a list of bundles that need to be compiled
*/
@NotNull
public ProcessingItem[] getProcessingItems(final CompileContext compileContext) {
return ApplicationManager.getApplication().runReadAction(new Computable<ProcessingItem[]>() {
public ProcessingItem[] compute() {
// find and add all dependent modules to the list of stuff to be compiled
CompileScope compilescope = compileContext.getCompileScope();
Module[] affectedModules = compilescope.getAffectedModules();
if (affectedModules.length == 0) {
return ProcessingItem.EMPTY_ARRAY;
}
List<ProcessingItem> result = new ArrayList<ProcessingItem>();
for (Module affectedModule : affectedModules) {
if ( OsmorcFacet.hasOsmorcFacet(affectedModule)) {
result.add(new BundleProcessingItem(affectedModule));
}
}
return result.toArray(new ProcessingItem[result.size()]);
}
});
}
/**
* Processes a processing item (=module)
*
* @param compileContext the compile context
* @param processingItems the list of processing items
* @return the list of processing items that remain for further processing (if any)
*/
public ProcessingItem[] process(CompileContext compileContext,
ProcessingItem[] processingItems) {
try {
for (ProcessingItem processingItem : processingItems) {
Module module = ((BundleProcessingItem) processingItem).getModule();
buildBundle(module, compileContext.getProgressIndicator(), compileContext);
}
}
catch (IOException ioexception) {
logger.error(ioexception);
}
return processingItems;
}
/**
* Builds the bundle for a given module.
*
* @param module the module
* @param progressIndicator the progress indicator
* @param compileContext
* @throws IOException in case something goes wrong.
*/
private static void buildBundle(final Module module, final ProgressIndicator progressIndicator,
final CompileContext compileContext)
throws IOException {
String messagePrefix = "["+module.getName()+"] ";
progressIndicator.setText("Building bundle for module " + module.getName());
// create the jar file
final File jarFile = new File(VfsUtil.urlToPath(getJarFileName(module)));
if (jarFile.exists()) { //noinspection ResultOfMethodCallIgnored
jarFile.delete();
}
if (!FileUtil.createParentDirs(jarFile)) {
compileContext.addMessage(CompilerMessageCategory.ERROR, messagePrefix + "Cannot create path to " + jarFile.getPath(),null, 0,0);
return;
}
final VirtualFile moduleOutputDir = new ReadAction<VirtualFile>() {
protected void run(Result<VirtualFile> result) {
result.setResult(getModuleOutputUrl(module));
}
}.execute().getResultObject();
final BndWrapper wrapper = new BndWrapper();
final OsmorcFacetConfiguration configuration = OsmorcFacet.getInstance(module).getConfiguration();
final List<String> classPaths = new ArrayList<String>();
if (moduleOutputDir != null) {
classPaths.add(moduleOutputDir.getUrl());
}
// build a bnd file or use a provided one.
String bndFileUrl = "";
Map<String, String> additionalProperties = new HashMap<String, String>();
if (configuration.isOsmorcControlsManifest() || configuration.isUseBndFile() || configuration.isUseBundlorFile() ) {
if (configuration.isUseBndFile()) {
File bndFile = findFileInModuleContentRoots(configuration.getBndFileLocation(), module);
if (bndFile == null || !bndFile.exists()) {
compileContext.addMessage(CompilerMessageCategory.ERROR,
String.format(messagePrefix + "The bnd file \"%s\" for module \"%s\" does not exist.",
configuration.getBndFileLocation(), module.getName()),
configuration.getBndFileLocation(), 0, 0);
return;
} else {
bndFileUrl = VfsUtil.pathToUrl(bndFile.getPath());
}
} else if ( configuration.isUseBundlorFile() ) {
// bundlor, in this case we use bnd for creating the jar only, and later run bundlor to
// do the manifest magic.
bndFileUrl = makeBndFile(module, "", compileContext);
if ( bndFileUrl == null ) {
// couldnt create bnd file.
return;
}
} else {
// fully osmorc controlled, no bnd file.
bndFileUrl = makeBndFile(module, configuration.asBndFile(), compileContext);
if ( bndFileUrl == null ) {
// couldnt create bnd file.
return;
}
}
} else {
boolean manifestExists = false;
BundleManager bundleManager = ServiceManager.getService(module.getProject(), BundleManager.class);
BundleManifest bundleManifest = bundleManager.getBundleManifest(module);
if (bundleManifest != null) {
PsiFile manifestFile = bundleManifest.getManifestFile();
if (manifestFile != null) {
String manifestFilePath = manifestFile.getVirtualFile().getPath();
if (manifestFilePath != null) {
bndFileUrl = makeBndFile(module, "-manifest " + manifestFilePath + "\n", compileContext);
manifestExists = true;
}
}
}
if (!manifestExists) {
compileContext.addMessage(CompilerMessageCategory.ERROR,
messagePrefix + "Manifest file for module " + module.getName() + ": '" + OsmorcFacet.getInstance(module).getManifestLocation() +
"' does not exist or cannot be found. Check that file exists and is not excluded from the module.", null, 0, 0);
return;
}
}
if (!configuration.isOsmorcControlsManifest() ||
(configuration.isOsmorcControlsManifest() && !configuration.isUseBndFile() && !configuration.isUseBundlorFile())) {
// in this case we manually add all the classpaths as resources
StringBuilder pathBuilder = new StringBuilder();
// add all the classpaths to include resources, so stuff from the project gets copied over.
// XXX: one could argue if this should be done for a non-osmorc build
for (int i = 0; i < classPaths.size(); i++) {
String classPath = classPaths.get(i);
String relPath = FileUtil.getRelativePath(new File(VfsUtil.urlToPath(bndFileUrl)),
new File(VfsUtil.urlToPath(classPath)));
if (i != 0) {
pathBuilder.append(",");
}
pathBuilder.append(relPath);
}
// now include the paths from the configuration
List<Pair<String, String>> list = configuration.getAdditionalJARContents();
for (Pair<String, String> stringStringPair : list) {
pathBuilder.append(",").append(stringStringPair.second).append(" = ").append(stringStringPair.first);
}
// and tell bnd what resources to include
additionalProperties.put("Include-Resource", pathBuilder.toString());
if (!configuration.isIgnorePatternValid()) {
compileContext.addMessage(CompilerMessageCategory.ERROR,
messagePrefix + "The file ignore pattern in the facet configuration is invalid.", null, 0, 0);
return;
}
// add the ignore pattern for the resources
if (!"".equals(configuration.getIgnoreFilePattern())) {
additionalProperties.put("-donotcopy", configuration.getIgnoreFilePattern());
}
}
String outputPath = jarFile.getPath();
if ( configuration.isUseBundlorFile() ) {
// we create a temp jar file in this case.
outputPath += ".tmp.jar";
}
wrapper.build(module, compileContext, bndFileUrl, ArrayUtil.toStringArray(classPaths), outputPath, additionalProperties);
// if we use bundlor, let bundlor work on the generated file.
if ( configuration.isUseBundlorFile() ) {
File bundlorFile = findFileInModuleContentRoots(configuration.getBundlorFileLocation(), module);
if (bundlorFile == null || !bundlorFile.exists()) {
compileContext.addMessage(CompilerMessageCategory.ERROR,
String.format(messagePrefix + "The Bundlor file \"%s\" for module \"%s\" does not exist.",
configuration.getBundlorFileLocation(), module.getName()),
configuration.getBundlorFileLocation(), 0, 0);
return;
}
BundlorWrapper bw = new BundlorWrapper();
try {
if (!bw.wrapModule(compileContext, outputPath, jarFile.getPath(), bundlorFile.getPath())) {
compileContext.addMessage(CompilerMessageCategory.ERROR, messagePrefix + "Bundlifying the file " + jarFile.getPath() + " with Bundlor failed.", null, 0,0);
return;
}
} finally {
// delete the tmp jar
File tempJar = new File(outputPath);
if ( tempJar.exists() ) {
if (!tempJar.delete()) {
compileContext.addMessage(CompilerMessageCategory.WARNING, messagePrefix + "Could not delete temporary file: " + tempJar.getPath(), null, 0,0);
}
}
}
}
if (!configuration.isUseBndFile() && !configuration.isUseBundlorFile()) {
// finally bundlify all the libs for this one
bundlifyLibraries(module, progressIndicator, compileContext);
}
}
@Nullable
private static String makeBndFile(Module module, String contents, CompileContext compileContext) throws IOException {
final String outputPath = getOutputPath(module, compileContext);
if ( outputPath == null ) {
return null;
}
File tmpFile = FileUtil.createTempFile(new File(outputPath), "osmorc", ".bnd", true);
// create one
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tmpFile));
bos.write(contents.getBytes());
bos.close();
tmpFile.deleteOnExit();
return VfsUtil.pathToUrl(tmpFile.getPath());
}
@Nullable
protected static File findFileInModuleContentRoots(String file, Module module) {
ModuleRootManager manager = ModuleRootManager.getInstance(module);
for (VirtualFile root : manager.getContentRoots()) {
VirtualFile result = VfsUtil.findRelativeFile(file, root);
if (result != null) {
return new File(result.getPath());
}
}
return null;
}
/**
* Returns the manifest file for the given module if it exists
*
* @param module the module
* @return the manifest file or null if it doesnt exist
*/
@Nullable
public static VirtualFile getManifestFile(@NotNull Module module) {
OsmorcFacet facet = OsmorcFacet.getInstance(module);
// FIXES Exception (http://ea.jetbrains.com/browser/ea_problems/17161)
if ( facet == null ) {
return null;
}
ModuleRootManager manager = ModuleRootManager.getInstance(module);
for (VirtualFile root : manager.getContentRoots()) {
VirtualFile result = VfsUtil.findRelativeFile(facet.getManifestLocation(), root);
// IDEADEV-40357
// if (result != null) {
// result = result.findChild("MANIFEST.MF");
// }
if (result != null) {
return result;
}
}
return null;
}
/**
* @return a description (prolly not used)
*/
@NotNull
public String getDescription() {
return "bundle compile";
}
/**
* Checks the configuration.
*
* @param compileScope the compilescope
* @return true if the configuration is valid, false otherwise
*/
public boolean validateConfiguration(CompileScope compileScope) {
return true;
}
/**
* Recreates a validity state from a data input stream
*
* @param in stream containing the data
* @return the validity state
* @throws IOException in case something goes wrong
*/
public ValidityState createValidityState(DataInput in) throws IOException {
return new BundleValidityState(in);
}
@Nullable
static VirtualFile getModuleOutputUrl(Module module) {
final CompilerModuleExtension extension = CompilerModuleExtension.getInstance(module);
if (extension != null) {
return extension.getCompilerOutputPath();
}
return null;
}
/**
* Bundlifies all libraries that belong to the given module and that are not bundles. The bundles are cached, so if
* the source library does not change, it will not be bundlified again.
*
* @param module the module whose libraries are to be bundled.
* @param indicator a progress indicator.
* @param compileContext
* @return a string array containing the urls of the bundlified libraries.
*/
public static String[] bundlifyLibraries(Module module, ProgressIndicator indicator,
@NotNull CompileContext compileContext) {
ArrayList<String> result = new ArrayList<String>();
final String[] urls = OrderEnumerator.orderEntries(module).withoutSdk().withoutModuleSourceEntries()
.satisfying(NOT_FRAMEWORK_LIBRARY_CONDITION).recursively().exportedOnly().classes().getUrls();
BndWrapper wrapper = new BndWrapper();
for (String url : urls) {
url = convertJarUrlToFileUrl(url);
if (CachingBundleInfoProvider.canBeBundlified(url)) { // Fixes IDEA-56666
indicator.setText("Bundling non-OSGi libraries for module: " + module.getName());
indicator.setText2(url);
// ok it is not a bundle, so we need to bundlify
final String outputPath = getOutputPath(module, compileContext);
if ( outputPath == null ) {
// couldnt create output path, abort here..
break;
}
String bundledLocation = wrapper.wrapLibrary(module, compileContext, url, outputPath);
// if no bundle could (or should) be created, we exempt this library
if (bundledLocation != null) {
result.add(fixFileURL(bundledLocation));
}
} else {
if ( CachingBundleInfoProvider.isBundle(url) ) { // Exclude non-bundles (IDEA-56666)
result.add(fixFileURL(url));
}
}
}
return ArrayUtil.toStringArray(result);
}
/**
* Converts a jar url gained from OrderEntry.getUrls or Library.getUrls into a file url that can be processed.
*
* @param url the url to be converted
* @return the converted url
*/
public static String convertJarUrlToFileUrl(String url) {
// urls end with !/ we cut that
// XXX: not sure if this is a hack
url = url.replaceAll("!.*", "");
url = url.replace("jar://", "file://");
return url;
}
/**
* On Windows a file url must have at least 3 slashes at the beginning. 2 for the protocoll separation and one for
* the empty host (e.g.: file:///c:/bla instead of file://c:/bla). If there are only two the drive letter is
* interpreted as the host of the url which naturally doesn't exist. On Unix systems it's the same case, but since
* all paths start with a slash, a misinterpretation of part of the path as a host cannot occur.
*
* @param url The URL to fix
* @return The fixed URL
*/
public static String fixFileURL(String url) {
return url.startsWith("file:///") ? url : url.replace("file://", "file:///");
}
/**
* Builds the name of the jar file for a given module.
*
* @param module the module
* @return the name of the jar file that will be produced for this module by this compiler, or
* null if the module does not have an Osmorc facet attached.
*/
@Nullable
public static String getJarFileName(final Module module) {
final OsmorcFacet facet = OsmorcFacet.getInstance(module);
if (facet != null) {
return facet.getConfiguration().getJarFileLocation();
}
return null;
}
}