/* * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jetbrains.osgi.bnd.imp; import aQute.bnd.build.Container; import aQute.bnd.build.Project; import aQute.bnd.build.Workspace; import aQute.bnd.header.Attrs; import aQute.bnd.service.Refreshable; import aQute.bnd.service.RepositoryPlugin; import com.intellij.compiler.CompilerConfiguration; import com.intellij.compiler.impl.javaCompiler.javac.JavacConfiguration; import com.intellij.facet.impl.FacetUtil; import com.intellij.ide.highlighter.ModuleFileType; import com.intellij.notification.NotificationDisplayType; import com.intellij.notification.NotificationGroup; import com.intellij.notification.NotificationType; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.ModifiableModuleModel; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.StdModuleTypes; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.Task; import com.intellij.openapi.projectRoots.JavaSdk; import com.intellij.openapi.projectRoots.ProjectJdkTable; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.projectRoots.SdkModificator; import com.intellij.openapi.roots.*; import com.intellij.openapi.roots.impl.ModifiableModelCommitter; import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.roots.libraries.LibraryTable; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.pom.java.LanguageLevel; import com.intellij.util.ObjectUtils; import com.intellij.util.PathUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerOptions; import org.jetbrains.osgi.jps.model.ManifestGenerationMode; import org.jetbrains.osgi.jps.model.OutputPathType; import org.osmorc.facet.OsmorcFacet; import org.osmorc.facet.OsmorcFacetConfiguration; import org.osmorc.facet.OsmorcFacetType; import java.io.File; import java.io.IOException; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import static org.osmorc.i18n.OsmorcBundle.message; public class BndProjectImporter { public static final String CNF_DIR = Workspace.CNFDIR; public static final String BUILD_FILE = Workspace.BUILDFILE; public static final String BND_FILE = Project.BNDFILE; public static final String BND_LIB_PREFIX = "bnd:"; public static final NotificationGroup NOTIFICATIONS = new NotificationGroup("OSGi Bnd Notifications", NotificationDisplayType.STICKY_BALLOON, true); private static final Logger LOG = Logger.getInstance(BndProjectImporter.class); private static final Key<Workspace> BND_WORKSPACE_KEY = Key.create("bnd.workspace.key"); private static final String JAVAC_SOURCE = "javac.source"; private static final String JAVAC_TARGET = "javac.target"; private static final String SRC_ROOT = "OSGI-OPT/src"; private static final String JDK_DEPENDENCY = "ee.j2se"; private static final Comparator<OrderEntry> ORDER_ENTRY_COMPARATOR = new Comparator<OrderEntry>() { @Override public int compare(OrderEntry o1, OrderEntry o2) { return weight(o1) - weight(o2); } private int weight(OrderEntry e) { return e instanceof JdkOrderEntry ? 2 : e instanceof ModuleSourceOrderEntry ? 0 : 1; } }; private static boolean isUnitTestMode() { return ApplicationManager.getApplication().isUnitTestMode(); } private final com.intellij.openapi.project.Project myProject; private final Workspace myWorkspace; private final Collection<Project> myProjects; private final Map<String, String> mySourcesMap = ContainerUtil.newTroveMap(FileUtil.PATH_HASHING_STRATEGY); public BndProjectImporter(@NotNull com.intellij.openapi.project.Project project, @NotNull Workspace workspace, @NotNull Collection<Project> toImport) { myProject = project; myWorkspace = workspace; myProjects = toImport; } @NotNull public Module createRootModule(@NotNull ModifiableModuleModel model) { String rootDir = myProject.getBasePath(); assert rootDir != null : myProject; String imlPath = rootDir + File.separator + myProject.getName() + ModuleFileType.DOT_DEFAULT_EXTENSION; Module module = model.newModule(imlPath, StdModuleTypes.JAVA.getId()); ModuleRootModificationUtil.addContentRoot(module, rootDir); ModuleRootModificationUtil.setSdkInherited(module); return module; } public void setupProject() { LanguageLevel sourceLevel = LanguageLevel.parse(myWorkspace.getProperty(JAVAC_SOURCE)); if (sourceLevel != null) { LanguageLevelProjectExtension.getInstance(myProject).setLanguageLevel(sourceLevel); } String targetLevel = myWorkspace.getProperty(JAVAC_TARGET); CompilerConfiguration.getInstance(myProject).setProjectBytecodeTarget(targetLevel); // compilation options (see Project#getCommonJavac()) JpsJavaCompilerOptions javacOptions = JavacConfiguration.getOptions(myProject, JavacConfiguration.class); javacOptions.DEBUGGING_INFO = booleanProperty(myWorkspace.getProperty("javac.debug", "true")); javacOptions.DEPRECATION = booleanProperty(myWorkspace.getProperty("java.deprecation")); javacOptions.ADDITIONAL_OPTIONS_STRING = myWorkspace.getProperty("java.options", ""); } public void resolve(boolean refresh) { if (!isUnitTestMode()) { new Task.Backgroundable(myProject, message("bnd.import.resolve.task"), true) { @Override public void run(@NotNull ProgressIndicator indicator) { if (resolve(indicator)) { ApplicationManager.getApplication().invokeLater(() -> { createProjectStructure(); if (refresh) { VirtualFileManager.getInstance().asyncRefresh(null); } }, BndProjectImporter.this.myProject.getDisposed()); } } }.queue(); } else { resolve(null); createProjectStructure(); } } private boolean resolve(@Nullable ProgressIndicator indicator) { int progress = 0; for (Project project : myProjects) { LOG.info("resolving: " + project.getBase()); if (indicator != null) { indicator.checkCanceled(); indicator.setText(project.getName()); } try { project.prepare(); } catch (Exception e) { checkErrors(project, e); return false; } checkWarnings(project, project.getErrors(), true); checkWarnings(project, project.getWarnings(), false); findSources(project); if (indicator != null) { indicator.setFraction((double)(++progress) / myProjects.size()); } } return true; } private void findSources(Project project) { try { findSources(project.getBootclasspath()); findSources(project.getBuildpath()); findSources(project.getTestpath()); } catch (Exception ignored) { } } private void findSources(Collection<Container> classpath) { for (Container dependency : classpath) { Container.TYPE type = dependency.getType(); if (type == Container.TYPE.REPO || type == Container.TYPE.EXTERNAL) { File file = dependency.getFile(); if (file.isFile() && FileUtilRt.extensionEquals(file.getName(), "jar")) { String path = file.getPath(); if (!mySourcesMap.containsKey(path)) { try { ZipFile zipFile = new ZipFile(file); try { ZipEntry srcRoot = zipFile.getEntry(SRC_ROOT); if (srcRoot != null) { mySourcesMap.put(path, SRC_ROOT); } } finally { zipFile.close(); } } catch (IOException e) { mySourcesMap.put(path, null); } } } } } } private void createProjectStructure() { if (myProject.isDisposed()) { return; } ApplicationManager.getApplication().runWriteAction(() -> { LanguageLevel projectLevel = LanguageLevelProjectExtension.getInstance(myProject).getLanguageLevel(); Map<Project, ModifiableRootModel> rootModels = ContainerUtil.newHashMap(); ModifiableModuleModel moduleModel = ModuleManager.getInstance(myProject).getModifiableModel(); LibraryTable.ModifiableModel libraryModel = ProjectLibraryTable.getInstance(myProject).getModifiableModel(); try { for (Project project : myProjects) { try { rootModels.put(project, createModule(moduleModel, project, projectLevel)); } catch (Exception e) { LOG.error(e); // should not happen, since project.prepare() is already called } } for (Project project : myProjects) { try { setDependencies(moduleModel, libraryModel, rootModels.get(project), project); } catch (Exception e) { LOG.error(e); // should not happen, since project.prepare() is already called } } } finally { libraryModel.commit(); ModifiableModelCommitter.multiCommit(rootModels.values(), moduleModel); } }); } private ModifiableRootModel createModule(ModifiableModuleModel moduleModel, Project project, LanguageLevel projectLevel) throws Exception { String name = project.getName(); Module module = moduleModel.findModuleByName(name); if (module == null) { String path = project.getBase().getPath() + File.separator + name + ModuleFileType.DOT_DEFAULT_EXTENSION; module = moduleModel.newModule(path, StdModuleTypes.JAVA.getId()); } ModifiableRootModel rootModel = ModuleRootManager.getInstance(module).getModifiableModel(); for (ContentEntry entry : rootModel.getContentEntries()) { rootModel.removeContentEntry(entry); } for (OrderEntry entry : rootModel.getOrderEntries()) { if (!(entry instanceof ModuleJdkOrderEntry || entry instanceof ModuleSourceOrderEntry)) { rootModel.removeOrderEntry(entry); } } rootModel.inheritSdk(); ContentEntry contentEntry = rootModel.addContentEntry(url(project.getBase())); for (File src : project.getSourcePath()) { contentEntry.addSourceFolder(url(src), false); } File testSrc = project.getTestSrc(); if (testSrc != null) { contentEntry.addSourceFolder(url(testSrc), true); } contentEntry.addExcludeFolder(url(project.getTarget())); LanguageLevel sourceLevel = LanguageLevel.parse(project.getProperty(JAVAC_SOURCE)); if (sourceLevel == projectLevel) sourceLevel = null; rootModel.getModuleExtension(LanguageLevelModuleExtension.class).setLanguageLevel(sourceLevel); CompilerModuleExtension compilerExt = rootModel.getModuleExtension(CompilerModuleExtension.class); compilerExt.inheritCompilerOutputPath(false); compilerExt.setExcludeOutput(true); compilerExt.setCompilerOutputPath(url(project.getSrcOutput())); compilerExt.setCompilerOutputPathForTests(url(project.getTestOutput())); String targetLevel = project.getProperty(JAVAC_TARGET); CompilerConfiguration.getInstance(myProject).setBytecodeTargetLevel(module, targetLevel); OsmorcFacet facet = OsmorcFacet.getInstance(module); if (project.isNoBundles() && facet != null) { FacetUtil.deleteFacet(facet); facet = null; } else if (!project.isNoBundles() && facet == null) { facet = FacetUtil.addFacet(module, OsmorcFacetType.getInstance()); } if (facet != null) { OsmorcFacetConfiguration facetConfig = facet.getConfiguration(); facetConfig.setManifestGenerationMode(ManifestGenerationMode.Bnd); facetConfig.setBndFileLocation(FileUtil.getRelativePath(path(project.getBase()), path(project.getPropertiesFile()), '/')); Map.Entry<String, Attrs> bsn = project.getBundleSymbolicName(); File bundle = project.getOutputFile(bsn != null ? bsn.getKey() : name, project.getBundleVersion()); facetConfig.setJarFileLocation(path(bundle), OutputPathType.SpecificOutputPath); facetConfig.setDoNotSynchronizeWithMaven(true); } return rootModel; } private void setDependencies(ModifiableModuleModel moduleModel, LibraryTable.ModifiableModel libraryModel, ModifiableRootModel rootModel, Project project) throws Exception { List<String> warnings = ContainerUtil.newArrayList(); Collection<Container> boot = project.getBootclasspath(); Set<Container> bootSet = Collections.emptySet(); if (!boot.isEmpty()) { setDependencies(moduleModel, libraryModel, rootModel, project, boot, false, bootSet, warnings); bootSet = ContainerUtil.newHashSet(boot); OrderEntry[] entries = rootModel.getOrderEntries(); if (entries.length > 2) { Arrays.sort(entries, ORDER_ENTRY_COMPARATOR); rootModel.rearrangeOrderEntries(entries); } } setDependencies(moduleModel, libraryModel, rootModel, project, project.getBuildpath(), false, bootSet, warnings); setDependencies(moduleModel, libraryModel, rootModel, project, project.getTestpath(), true, bootSet, warnings); checkWarnings(project, warnings, false); } private void setDependencies(ModifiableModuleModel moduleModel, LibraryTable.ModifiableModel libraryModel, ModifiableRootModel rootModel, Project project, Collection<Container> classpath, boolean tests, Set<Container> excluded, List<String> warnings) throws Exception { DependencyScope scope = tests ? DependencyScope.TEST : DependencyScope.COMPILE; for (Container dependency : classpath) { if (excluded.contains(dependency)) { continue; // skip boot path dependency } if (dependency.getType() == Container.TYPE.PROJECT && project == dependency.getProject()) { continue; // skip self-reference } try { addEntry(moduleModel, libraryModel, rootModel, dependency, scope); } catch (IllegalArgumentException e) { warnings.add(e.getMessage()); } } } private void addEntry(ModifiableModuleModel moduleModel, LibraryTable.ModifiableModel libraryModel, ModifiableRootModel rootModel, Container dependency, DependencyScope scope) throws IllegalArgumentException { File file = dependency.getFile(); String bsn = dependency.getBundleSymbolicName(); String version = dependency.getVersion(); String path = file.getPath(); if (path.contains(": ")) { throw new IllegalArgumentException("Cannot resolve " + bsn + ":" + version + ": " + path); } if (JDK_DEPENDENCY.equals(bsn)) { String name = BND_LIB_PREFIX + bsn + ":" + version; if (FileUtil.isAncestor(myWorkspace.getBase(), file, true)) { name += "-" + myProject.getName(); } ProjectJdkTable jdkTable = ProjectJdkTable.getInstance(); Sdk jdk = jdkTable.findJdk(name); if (jdk == null) { jdk = jdkTable.createSdk(name, JavaSdk.getInstance()); SdkModificator jdkModel = jdk.getSdkModificator(); jdkModel.setHomePath(file.getParent()); jdkModel.setVersionString(version); VirtualFile root = VirtualFileManager.getInstance().findFileByUrl(url(file)); assert root != null : file + " " + file.exists(); jdkModel.addRoot(root, OrderRootType.CLASSES); VirtualFile srcRoot = VirtualFileManager.getInstance().findFileByUrl(url(file) + SRC_ROOT); if (srcRoot != null) jdkModel.addRoot(srcRoot, OrderRootType.SOURCES); jdkModel.commitChanges(); jdkTable.addJdk(jdk); } rootModel.setSdk(jdk); return; } ExportableOrderEntry entry; switch (dependency.getType()) { case PROJECT: { String name = dependency.getProject().getName(); Module module = moduleModel.findModuleByName(name); if (module == null) { throw new IllegalArgumentException("Unknown module '" + name + "'"); } entry = (ModuleOrderEntry)ContainerUtil.find( rootModel.getOrderEntries(), e -> e instanceof ModuleOrderEntry && ((ModuleOrderEntry)e).getModule() == module); if (entry == null) { entry = rootModel.addModuleOrderEntry(module); } break; } case REPO: { String name = BND_LIB_PREFIX + bsn + ":" + version; Library library = libraryModel.getLibraryByName(name); if (library == null) { library = libraryModel.createLibrary(name); } Library.ModifiableModel model = library.getModifiableModel(); for (String url : model.getUrls(OrderRootType.CLASSES)) model.removeRoot(url, OrderRootType.CLASSES); for (String url : model.getUrls(OrderRootType.SOURCES)) model.removeRoot(url, OrderRootType.SOURCES); model.addRoot(url(file), OrderRootType.CLASSES); String srcRoot = mySourcesMap.get(path); if (srcRoot != null) { model.addRoot(url(file) + srcRoot, OrderRootType.SOURCES); } model.commit(); entry = rootModel.addLibraryEntry(library); break; } case EXTERNAL: { Library library = rootModel.getModuleLibraryTable().createLibrary(file.getName()); Library.ModifiableModel model = library.getModifiableModel(); model.addRoot(url(file), OrderRootType.CLASSES); String srcRoot = mySourcesMap.get(path); if (srcRoot != null) { model.addRoot(url(file) + srcRoot, OrderRootType.SOURCES); } model.commit(); entry = rootModel.findLibraryOrderEntry(library); assert entry != null : library; break; } default: throw new IllegalArgumentException("Unknown dependency '" + dependency + "' of type " + dependency.getType()); } entry.setScope(scope); } private void checkErrors(Project project, Exception e) { if (!isUnitTestMode()) { String text; LOG.warn(e); text = message("bnd.import.resolve.error", project.getName(), e.getMessage()); NOTIFICATIONS.createNotification(message("bnd.import.error.title"), text, NotificationType.ERROR, null).notify(myProject); } else { throw new AssertionError(e); } } private void checkWarnings(Project project, List<String> warnings, boolean error) { if (warnings != null && !warnings.isEmpty()) { if (!isUnitTestMode()) { LOG.warn(warnings.toString()); String text = message("bnd.import.warn.text", project.getName(), "<br>" + StringUtil.join(warnings, "<br>")); NotificationType type = error ? NotificationType.ERROR : NotificationType.WARNING; NOTIFICATIONS.createNotification(message("bnd.import.warn.title"), text, type, null).notify(myProject); } else { throw new AssertionError(warnings.toString()); } } } private static boolean booleanProperty(String value) { return "on".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value); } private static String path(File file) { return FileUtil.toSystemIndependentName(file.getPath()); } private static String url(File file) { return VfsUtil.getUrlForLibraryRoot(file); } @NotNull public static Collection<Project> getWorkspaceProjects(@NotNull Workspace workspace) throws Exception { return ContainerUtil.filter(workspace.getAllProjects(), Condition.NOT_NULL); } /** * Caches a workspace for methods below. */ @Nullable public static Workspace findWorkspace(@NotNull com.intellij.openapi.project.Project project) { String basePath = project.getBasePath(); if (basePath != null && new File(basePath, CNF_DIR).exists()) { try { Workspace ws = Workspace.getWorkspace(new File(basePath), CNF_DIR); BND_WORKSPACE_KEY.set(project, ws); return ws; } catch (Exception e) { LOG.error(e); } } return null; } @Nullable public static Workspace getWorkspace(@Nullable com.intellij.openapi.project.Project project) { return project == null || project.isDefault() ? null : BND_WORKSPACE_KEY.get(project); } public static void reimportWorkspace(@NotNull com.intellij.openapi.project.Project project) { if (!isUnitTestMode()) { new Task.Backgroundable(project, message("bnd.reimport.task"), true) { @Override public void run(@NotNull ProgressIndicator indicator) { doReimportWorkspace(project, indicator); } }.queue(); } else { doReimportWorkspace(project, null); } } private static void doReimportWorkspace(com.intellij.openapi.project.Project project, ProgressIndicator indicator) { Workspace workspace = getWorkspace(project); assert workspace != null : project; Collection<Project> projects; try { workspace.clear(); workspace.forceRefresh(); refreshRepositories(workspace, indicator); projects = getWorkspaceProjects(workspace); for (Project p : projects) { if (indicator != null) indicator.checkCanceled(); p.clear(); p.forceRefresh(); } } catch (Exception e) { LOG.error("ws=" + workspace.getBase(), e); return; } Runnable task = () -> { BndProjectImporter importer = new BndProjectImporter(project, workspace, projects); importer.setupProject(); importer.resolve(true); }; if (!isUnitTestMode()) { ApplicationManager.getApplication().invokeLater(task, project.getDisposed()); } else { task.run(); } } public static void reimportProjects(@NotNull com.intellij.openapi.project.Project project, @NotNull Collection<String> projectDirs) { if (!isUnitTestMode()) { new Task.Backgroundable(project, message("bnd.reimport.task"), true) { @Override public void run(@NotNull ProgressIndicator indicator) { doReimportProjects(project, projectDirs, indicator); } }.queue(); } else { doReimportProjects(project, projectDirs, null); } } private static void doReimportProjects(com.intellij.openapi.project.Project project, Collection<String> projectDirs, ProgressIndicator indicator) { Workspace workspace = getWorkspace(project); assert workspace != null : project; Collection<Project> projects; try { refreshRepositories(workspace, indicator); projects = ContainerUtil.newArrayListWithCapacity(projectDirs.size()); for (String dir : projectDirs) { if (indicator != null) indicator.checkCanceled(); Project p = workspace.getProject(PathUtil.getFileName(dir)); if (p != null) { p.clear(); p.forceRefresh(); projects.add(p); } } } catch (Exception e) { LOG.error("ws=" + workspace.getBase() + " pr=" + projectDirs, e); return; } Runnable task = () -> new BndProjectImporter(project, workspace, projects).resolve(true); if (!isUnitTestMode()) { ApplicationManager.getApplication().invokeLater(task, project.getDisposed()); } else { task.run(); } } private static void refreshRepositories(Workspace workspace, ProgressIndicator indicator) { List<RepositoryPlugin> plugins = workspace.getPlugins(RepositoryPlugin.class); for (RepositoryPlugin plugin : plugins) { if (indicator != null) indicator.checkCanceled(); if (plugin instanceof Refreshable) { try { ((Refreshable)plugin).refresh(); } catch (Exception e) { LOG.warn(ObjectUtils.notNull(e.getMessage(), "NPE") + ", plugin=" + plugin); LOG.debug(e); } } } } }