/* * Copyright 2000-2013 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 com.intellij.openapi.externalSystem.util; import com.intellij.execution.rmi.RemoteUtil; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.externalSystem.ExternalSystemAutoImportAware; import com.intellij.openapi.externalSystem.ExternalSystemManager; import com.intellij.openapi.externalSystem.model.DataNode; import com.intellij.openapi.externalSystem.model.ExternalSystemException; import com.intellij.openapi.externalSystem.model.Key; import com.intellij.openapi.externalSystem.model.ProjectSystemId; import com.intellij.openapi.externalSystem.model.project.LibraryData; import com.intellij.openapi.externalSystem.model.settings.ExternalSystemExecutionSettings; import com.intellij.openapi.externalSystem.service.ParametersEnhancer; import com.intellij.openapi.externalSystem.settings.AbstractExternalSystemLocalSettings; import com.intellij.openapi.externalSystem.settings.AbstractExternalSystemSettings; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import consulo.vfs.util.ArchiveVfsUtil; import com.intellij.util.*; import com.intellij.util.containers.ContainerUtilRt; import com.intellij.util.containers.TransferToEDTQueue; import com.intellij.util.lang.UrlClassLoader; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author Denis Zhdanov * @since 4/1/13 1:31 PM */ public class ExternalSystemApiUtil { private static final Logger LOG = Logger.getInstance("#" + ExternalSystemApiUtil.class.getName()); private static final String LAST_USED_PROJECT_PATH_PREFIX = "LAST_EXTERNAL_PROJECT_PATH_"; @NotNull public static final String PATH_SEPARATOR = "/"; @NotNull private static final Pattern ARTIFACT_PATTERN = Pattern.compile("(?:.*/)?(.+?)(?:-([\\d+](?:\\.[\\d]+)*))?(?:\\.[^\\.]+?)?"); @NotNull public static final Comparator<Object> ORDER_AWARE_COMPARATOR = new Comparator<Object>() { @Override public int compare(@NotNull Object o1, @NotNull Object o2) { int order1 = getOrder(o1); int order2 = getOrder(o2); return (order1 < order2) ? -1 : ((order1 == order2) ? 0 : 1); } private int getOrder(@NotNull Object o) { Queue<Class<?>> toCheck = new ArrayDeque<Class<?>>(); toCheck.add(o.getClass()); while (!toCheck.isEmpty()) { Class<?> clazz = toCheck.poll(); Order annotation = clazz.getAnnotation(Order.class); if (annotation != null) { return annotation.value(); } Class<?> c = clazz.getSuperclass(); if (c != null) { toCheck.add(c); } Class<?>[] interfaces = clazz.getInterfaces(); Collections.addAll(toCheck, interfaces); } return ExternalSystemConstants.UNORDERED; } }; @NotNull private static final NullableFunction<DataNode<?>, Key<?>> GROUPER = new NullableFunction<DataNode<?>, Key<?>>() { @Override public Key<?> fun(DataNode<?> node) { return node.getKey(); } }; @NotNull private static final Comparator<Object> COMPARABLE_GLUE = new Comparator<Object>() { @SuppressWarnings("unchecked") @Override public int compare(Object o1, Object o2) { return ((Comparable)o1).compareTo(o2); } }; @NotNull private static final TransferToEDTQueue<Runnable> TRANSFER_TO_EDT_QUEUE = new TransferToEDTQueue<Runnable>("External System queue", new Processor<Runnable>() { @Override public boolean process(Runnable runnable) { runnable.run(); return true; } }, Condition.FALSE, 300); private ExternalSystemApiUtil() { } @NotNull public static String extractNameFromPath(@NotNull String path) { String strippedPath = stripPath(path); final int i = strippedPath.lastIndexOf(PATH_SEPARATOR); final String result; if (i < 0 || i >= strippedPath.length() - 1) { result = strippedPath; } else { result = strippedPath.substring(i + 1); } return result; } @NotNull private static String stripPath(@NotNull String path) { String[] endingsToStrip = {"/", "!", ".jar"}; StringBuilder buffer = new StringBuilder(path); for (String ending : endingsToStrip) { if (buffer.lastIndexOf(ending) == buffer.length() - ending.length()) { buffer.setLength(buffer.length() - ending.length()); } } return buffer.toString(); } @NotNull public static String getLibraryName(@NotNull Library library) { final String result = library.getName(); if (result != null) { return result; } for (OrderRootType type : OrderRootType.getAllTypes()) { for (String url : library.getUrls(type)) { String candidate = extractNameFromPath(url); if (!StringUtil.isEmpty(candidate)) { return candidate; } } } assert false; return "unknown-lib"; } public static boolean isRelated(@NotNull Library library, @NotNull LibraryData libraryData) { return getLibraryName(library).equals(libraryData.getInternalName()); } public static boolean isExternalSystemLibrary(@NotNull Library library, @NotNull ProjectSystemId externalSystemId) { return library.getName() != null && StringUtil.startsWith(library.getName(), externalSystemId.getReadableName() + ": "); } @Nullable public static ArtifactInfo parseArtifactInfo(@NotNull String fileName) { Matcher matcher = ARTIFACT_PATTERN.matcher(fileName); if (!matcher.matches()) { return null; } return new ArtifactInfo(matcher.group(1), null, matcher.group(2)); } public static void orderAwareSort(@NotNull List<?> data) { Collections.sort(data, ORDER_AWARE_COMPARATOR); } /** * @param path target path * @return absolute path that points to the same location as the given one and that uses only slashes */ @NotNull public static String toCanonicalPath(@NotNull String path) { String p = normalizePath(new File(path).getAbsolutePath()); assert p != null; return PathUtil.getCanonicalPath(p); } @NotNull public static String getLocalFileSystemPath(@NotNull VirtualFile file) { final VirtualFile archiveRoot = ArchiveVfsUtil.getVirtualFileForArchive(file); if (archiveRoot != null) { return archiveRoot.getPath(); } return toCanonicalPath(file.getPath()); } @Nullable public static ExternalSystemManager<?, ?, ?, ?, ?> getManager(@NotNull ProjectSystemId externalSystemId) { for (ExternalSystemManager manager : ExternalSystemManager.EP_NAME.getExtensions()) { if (externalSystemId.equals(manager.getSystemId())) { return manager; } } return null; } @SuppressWarnings("ManualArrayToCollectionCopy") @NotNull public static Collection<ExternalSystemManager<?, ?, ?, ?, ?>> getAllManagers() { List<ExternalSystemManager<?, ?, ?, ?, ?>> result = ContainerUtilRt.newArrayList(); for (ExternalSystemManager manager : ExternalSystemManager.EP_NAME.getExtensions()) { result.add(manager); } return result; } @NotNull public static Map<Key<?>, List<DataNode<?>>> group(@NotNull Collection<DataNode<?>> nodes) { return groupBy(nodes, GROUPER); } @NotNull public static <K, V> Map<DataNode<K>, List<DataNode<V>>> groupBy(@NotNull Collection<DataNode<V>> nodes, @NotNull final Key<K> key) { return groupBy(nodes, new NullableFunction<DataNode<V>, DataNode<K>>() { @Nullable @Override public DataNode<K> fun(DataNode<V> node) { return node.getDataNode(key); } }); } @NotNull public static <K, V> Map<K, List<V>> groupBy(@NotNull Collection<V> nodes, @NotNull NullableFunction<V, K> grouper) { Map<K, List<V>> result = ContainerUtilRt.newHashMap(); for (V data : nodes) { K key = grouper.fun(data); if (key == null) { LOG.warn(String.format( "Skipping entry '%s' during grouping. Reason: it's not possible to build a grouping key with grouping strategy '%s'. " + "Given entries: %s", data, grouper.getClass(), nodes)); continue; } List<V> grouped = result.get(key); if (grouped == null) { result.put(key, grouped = ContainerUtilRt.newArrayList()); } grouped.add(data); } if (!result.isEmpty() && result.keySet().iterator().next() instanceof Comparable) { List<K> ordered = ContainerUtilRt.newArrayList(result.keySet()); Collections.sort(ordered, COMPARABLE_GLUE); Map<K, List<V>> orderedResult = ContainerUtilRt.newLinkedHashMap(); for (K k : ordered) { orderedResult.put(k, result.get(k)); } return orderedResult; } return result; } @SuppressWarnings("unchecked") @NotNull public static <T> Collection<DataNode<T>> getChildren(@NotNull DataNode<?> node, @NotNull Key<T> key) { Collection<DataNode<T>> result = null; for (DataNode<?> child : node.getChildren()) { if (!key.equals(child.getKey())) { continue; } if (result == null) { result = ContainerUtilRt.newArrayList(); } result.add((DataNode<T>)child); } return result == null ? Collections.<DataNode<T>>emptyList() : result; } @SuppressWarnings("unchecked") @Nullable public static <T> DataNode<T> find(@NotNull DataNode<?> node, @NotNull Key<T> key) { for (DataNode<?> child : node.getChildren()) { if (key.equals(child.getKey())) { return (DataNode<T>)child; } } return null; } @SuppressWarnings("unchecked") @Nullable public static <T> DataNode<T> find(@NotNull DataNode<?> node, @NotNull Key<T> key, BooleanFunction<DataNode<T>> predicate) { for (DataNode<?> child : node.getChildren()) { if (key.equals(child.getKey()) && predicate.fun((DataNode<T>)child)) { return (DataNode<T>)child; } } return null; } @SuppressWarnings("unchecked") @Nullable public static <T> DataNode<T> findParent(@NotNull DataNode<?> node, @NotNull Key<T> key) { return findParent(node, key, null); } @SuppressWarnings("unchecked") @Nullable public static <T> DataNode<T> findParent(@NotNull DataNode<?> node, @NotNull Key<T> key, @Nullable BooleanFunction<DataNode<T>> predicate) { DataNode<?> parent = node.getParent(); if (parent == null) return null; return key.equals(parent.getKey()) && (predicate == null || predicate.fun((DataNode<T>)parent)) ? (DataNode<T>)parent : findParent(parent, key, predicate); } @SuppressWarnings("unchecked") @NotNull public static <T> Collection<DataNode<T>> findAll(@NotNull DataNode<?> parent, @NotNull Key<T> key) { Collection<DataNode<T>> result = null; for (DataNode<?> child : parent.getChildren()) { if (!key.equals(child.getKey())) { continue; } if (result == null) { result = ContainerUtilRt.newArrayList(); } result.add((DataNode<T>)child); } return result == null ? Collections.<DataNode<T>>emptyList() : result; } public static void executeProjectChangeAction(@NotNull final DisposeAwareProjectChange task) { executeProjectChangeAction(false, task); } public static void executeProjectChangeAction(boolean synchronous, @NotNull final DisposeAwareProjectChange task) { executeOnEdt(synchronous, new Runnable() { public void run() { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { task.run(); } }); } }); } public static void executeOnEdt(boolean synchronous, @NotNull Runnable task) { if (synchronous) { if (ApplicationManager.getApplication().isDispatchThread()) { task.run(); } else { UIUtil.invokeAndWaitIfNeeded(task); } } else { UIUtil.invokeLaterIfNeeded(task); } } /** * Adds runnable to Event Dispatch Queue * if we aren't in UnitTest of Headless environment mode * * @param runnable Runnable */ public static void addToInvokeLater(final Runnable runnable) { final Application application = ApplicationManager.getApplication(); final boolean unitTestMode = application.isUnitTestMode(); if (unitTestMode) { UIUtil.invokeLaterIfNeeded(runnable); } else if (application.isHeadlessEnvironment() || application.isDispatchThread()) { runnable.run(); } else { TRANSFER_TO_EDT_QUEUE.offer(runnable); } } /** * Configures given classpath to reference target i18n bundle file(s). * * @param classPath process classpath * @param bundlePath path to the target bundle file * @param contextClass class from the same content root as the target bundle file */ public static void addBundle(@NotNull PathsList classPath, @NotNull String bundlePath, @NotNull Class<?> contextClass) { String pathToUse = bundlePath.replace('.', '/'); if (!pathToUse.endsWith(".properties")) { pathToUse += ".properties"; } if (!pathToUse.startsWith("/")) { pathToUse = '/' + pathToUse; } String root = PathManager.getResourceRoot(contextClass, pathToUse); if (root != null) { classPath.add(root); } } @SuppressWarnings("ConstantConditions") @Nullable public static String normalizePath(@Nullable String s) { return StringUtil.isEmpty(s) ? null : s.replace('\\', ExternalSystemConstants.PATH_SEPARATOR); } /** * We can divide all 'import from external system' use-cases into at least as below: * <pre> * <ul> * <li>this is a new project being created (import project from external model);</li> * <li>a new module is being imported from an external project into an existing ide project;</li> * </ul> * </pre> * This method allows to differentiate between them (e.g. we don't want to change language level when new module is imported to * an existing project). * * @return <code>true</code> if new project is being imported; <code>false</code> if new module is being imported */ public static boolean isNewProjectConstruction() { return ProjectManager.getInstance().getOpenProjects().length == 0; } // @NotNull // public static String getLastUsedExternalProjectPath(@NotNull ProjectSystemId externalSystemId) { // return PropertiesComponent.getInstance().getValue(LAST_USED_PROJECT_PATH_PREFIX + externalSystemId.getReadableName(), ""); // } public static void storeLastUsedExternalProjectPath(@Nullable String path, @NotNull ProjectSystemId externalSystemId) { if (path != null) { PropertiesComponent.getInstance().setValue(LAST_USED_PROJECT_PATH_PREFIX + externalSystemId.getReadableName(), path); } } @NotNull public static String getProjectRepresentationName(@NotNull String targetProjectPath, @Nullable String rootProjectPath) { if (rootProjectPath == null) { File rootProjectDir = new File(targetProjectPath); if (rootProjectDir.isFile()) { rootProjectDir = rootProjectDir.getParentFile(); } return rootProjectDir.getName(); } File rootProjectDir = new File(rootProjectPath); if (rootProjectDir.isFile()) { rootProjectDir = rootProjectDir.getParentFile(); } File targetProjectDir = new File(targetProjectPath); if (targetProjectDir.isFile()) { targetProjectDir = targetProjectDir.getParentFile(); } StringBuilder buffer = new StringBuilder(); for (File f = targetProjectDir; f != null && !FileUtil.filesEqual(f, rootProjectDir); f = f.getParentFile()) { buffer.insert(0, f.getName()).insert(0, ":"); } buffer.insert(0, rootProjectDir.getName()); return buffer.toString(); } /** * There is a possible case that external project linked to an ide project is a multi-project, i.e. contains more than one * module. * <p/> * This method tries to find root project's config path assuming that given path points to a sub-project's config path. * * @param externalProjectPath external sub-project's config path * @param externalSystemId target external system * @param project target ide project * @return root external project's path if given path is considered to point to a known sub-project's config; * <code>null</code> if it's not possible to find a root project's config path on the basis of the * given path */ @Nullable public static String getRootProjectPath(@NotNull String externalProjectPath, @NotNull ProjectSystemId externalSystemId, @NotNull Project project) { ExternalSystemManager<?, ?, ?, ?, ?> manager = getManager(externalSystemId); if (manager == null) { return null; } if (manager instanceof ExternalSystemAutoImportAware) { return ((ExternalSystemAutoImportAware)manager).getAffectedExternalProjectPath(externalProjectPath, project); } return null; } /** * {@link RemoteUtil#unwrap(Throwable) unwraps} given exception if possible and builds error message for it. * * @param e exception to process * @return error message for the given exception */ @SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "IOResourceOpenedButNotSafelyClosed"}) @NotNull public static String buildErrorMessage(@NotNull Throwable e) { Throwable unwrapped = RemoteUtil.unwrap(e); String reason = unwrapped.getLocalizedMessage(); if (!StringUtil.isEmpty(reason)) { return reason; } else if (unwrapped.getClass() == ExternalSystemException.class) { return String.format("exception during working with external system: %s", ((ExternalSystemException)unwrapped).getOriginalReason()); } else { StringWriter writer = new StringWriter(); unwrapped.printStackTrace(new PrintWriter(writer)); return writer.toString(); } } @SuppressWarnings("unchecked") @NotNull public static AbstractExternalSystemSettings getSettings(@NotNull Project project, @NotNull ProjectSystemId externalSystemId) throws IllegalArgumentException { ExternalSystemManager<?, ?, ?, ?, ?> manager = getManager(externalSystemId); if (manager == null) { throw new IllegalArgumentException(String.format( "Can't retrieve external system settings for id '%s'. Reason: no such external system is registered", externalSystemId.getReadableName() )); } return manager.getSettingsProvider().fun(project); } @SuppressWarnings("unchecked") public static <S extends AbstractExternalSystemLocalSettings> S getLocalSettings(@NotNull Project project, @NotNull ProjectSystemId externalSystemId) throws IllegalArgumentException { ExternalSystemManager<?, ?, ?, ?, ?> manager = getManager(externalSystemId); if (manager == null) { throw new IllegalArgumentException(String.format( "Can't retrieve local external system settings for id '%s'. Reason: no such external system is registered", externalSystemId.getReadableName() )); } return (S)manager.getLocalSettingsProvider().fun(project); } @SuppressWarnings("unchecked") public static <S extends ExternalSystemExecutionSettings> S getExecutionSettings(@NotNull Project project, @NotNull String linkedProjectPath, @NotNull ProjectSystemId externalSystemId) throws IllegalArgumentException { ExternalSystemManager<?, ?, ?, ?, ?> manager = getManager(externalSystemId); if (manager == null) { throw new IllegalArgumentException(String.format( "Can't retrieve external system execution settings for id '%s'. Reason: no such external system is registered", externalSystemId.getReadableName() )); } return (S)manager.getExecutionSettingsProvider().fun(Pair.create(project, linkedProjectPath)); } /** * Historically we prefer to work with third-party api not from ide process but from dedicated slave process (there is a risk * that third-party api has bugs which might make the whole ide process corrupted, e.g. a memory leak at the api might crash * the whole ide process). * <p/> * However, we do allow to explicitly configure the ide to work with third-party external system api from the ide process. * <p/> * This method allows to check whether the ide is configured to use 'out of process' or 'in process' mode for the system. * * @param externalSystemId target external system * * @return <code>true</code> if the ide is configured to work with external system api from the ide process; * <code>false</code> otherwise */ public static boolean isInProcessMode(ProjectSystemId externalSystemId) { return Registry.is(externalSystemId.getId() + ExternalSystemConstants.USE_IN_PROCESS_COMMUNICATION_REGISTRY_KEY_SUFFIX, false); } /** * There is a possible case that methods of particular object should be executed with classpath different from the one implied * by the current class' class loader. External system offers {@link ParametersEnhancer#enhanceLocalProcessing(List)} method * for defining that custom classpath. * <p/> * It's also possible that particular implementation of {@link ParametersEnhancer} is compiled using dependency to classes * which are provided by the {@link ParametersEnhancer#enhanceLocalProcessing(List) expanded classpath}. E.g. a class * <code>'A'</code> might use method of class <code>'B'</code> and 'A' is located at the current (system/plugin) classpath but * <code>'B'</code> is not. We need to reload <code>'A'</code> using its expanded classpath then, i.e. create new class loaded * with that expanded classpath and load <code>'A'</code> by it. * <p/> * This method allows to do that. * * @param clazz custom classpath-aware class which instance should be created (is assumed to have a no-args constructor) * @param <T> target type * @return newly created instance of the given class loaded by custom classpath-aware loader * @throws IllegalAccessException as defined by reflection processing * @throws InstantiationException as defined by reflection processing * @throws NoSuchMethodException as defined by reflection processing * @throws InvocationTargetException as defined by reflection processing * @throws ClassNotFoundException as defined by reflection processing */ @NotNull public static <T extends ParametersEnhancer> T reloadIfNecessary(@NotNull final Class<T> clazz) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException { T instance = clazz.newInstance(); List<URL> urls = ContainerUtilRt.newArrayList(); instance.enhanceLocalProcessing(urls); if (urls.isEmpty()) { return instance; } final ClassLoader baseLoader = clazz.getClassLoader(); Method method = baseLoader.getClass().getMethod("getUrls"); if (method != null) { //noinspection unchecked urls.addAll((Collection<? extends URL>)method.invoke(baseLoader)); } UrlClassLoader loader = new UrlClassLoader(UrlClassLoader.build().urls(urls).parent(baseLoader.getParent())) { @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.equals(clazz.getName())) { return super.loadClass(name, resolve); } else { try { return baseLoader.loadClass(name); } catch (ClassNotFoundException e) { return super.loadClass(name, resolve); } } } }; //noinspection unchecked return (T)loader.loadClass(clazz.getName()).newInstance(); } @Contract("_, null -> false") public static boolean isExternalSystemAwareModule(@NotNull ProjectSystemId systemId, @Nullable Module module) { return module != null && systemId.getId().equals(module.getOptionValue(ExternalSystemConstants.EXTERNAL_SYSTEM_ID_KEY)); } @Contract("_, null -> false") public static boolean isExternalSystemAwareModule(@NotNull String systemId, @Nullable Module module) { return module != null && systemId.equals(module.getOptionValue(ExternalSystemConstants.EXTERNAL_SYSTEM_ID_KEY)); } @Nullable public static String getExternalProjectPath(@Nullable Module module) { return module != null ? module.getOptionValue(ExternalSystemConstants.LINKED_PROJECT_PATH_KEY) : null; } @Nullable public static String getExternalProjectId(@Nullable Module module) { return module != null ? module.getOptionValue(ExternalSystemConstants.LINKED_PROJECT_ID_KEY) : null; } }