| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * 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.google.appindexing.api; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.VisibleForTesting; |
| import com.android.tools.idea.gradle.dsl.model.GradleBuildModel; |
| import com.android.tools.idea.gradle.dsl.model.dependencies.ArtifactDependencyModel; |
| import com.android.tools.idea.gradle.dsl.model.dependencies.ArtifactDependencySpec; |
| import com.android.tools.idea.gradle.dsl.model.dependencies.CommonConfigurationNames; |
| import com.android.tools.idea.gradle.dsl.model.dependencies.DependenciesModel; |
| import com.google.appindexing.util.DeepLinkUtils; |
| import com.google.appindexing.util.ManifestUtils; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.intellij.ide.highlighter.JavaFileType; |
| import com.intellij.lang.java.JavaLanguage; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.module.ModuleUtilCore; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.psi.*; |
| import com.intellij.psi.codeStyle.CodeStyleManager; |
| import com.intellij.psi.codeStyle.JavaCodeStyleManager; |
| import com.intellij.psi.util.InheritanceUtil; |
| import com.intellij.psi.xml.XmlFile; |
| import com.intellij.psi.xml.XmlTag; |
| import com.intellij.util.text.VersionComparatorUtil; |
| import com.siyeh.ig.psiutils.ImportUtils; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import static com.android.tools.idea.gradle.dsl.model.values.GradleValue.getValues; |
| |
| /** |
| * A class for detecting the situation where developers need App Indexing API code and inserting |
| * the API code in their projects. |
| */ |
| public final class ApiCreator { |
| |
| |
| static private final String METHOD_GET_ACTION = "getIndexApiAction"; |
| |
| static private final String CLASS_ACTION = "Action"; |
| static private final String CLASS_FIREBASE_USER_ACTION = "FirebaseUserActions"; |
| static private final String CLASS_BUILDERS_ACTIONS = "Actions"; |
| |
| |
| static private final String CLASS_ACTION_FULL = "com.google.firebase.appindexing.Action"; |
| static private final String CLASS_FIREBASE_USER_ACTION_FULL = "com.google.firebase.appindexing.FirebaseUserActions"; |
| static private final String CLASS_BUILDERS_ACTIONS_FULL = "com.google.firebase.appindexing.builders.Actions"; |
| |
| |
| |
| static private final List<String> SIGNATURE_ON_START = Lists.newArrayList(); |
| static private final List<String> SIGNATURE_ON_STOP = Lists.newArrayList(); |
| static private final String ON_START_FORMAT = "@Override\n" + |
| "public void onStart(){\n" + |
| " super.onStart();\n" + |
| " \n" + |
| " // ATTENTION: This was auto-generated to implement the App Indexing API.\n" + |
| " // See https://g.co/AppIndexing/AndroidStudio for more information.\n" + |
| " %1$s.getInstance().start(%2$s());\n" + |
| "}"; |
| static private final String ON_STOP_FORMAT = "@Override\n" + |
| "public void onStop() {\n" + |
| " \n" + |
| " // ATTENTION: This was auto-generated to implement the App Indexing API.\n" + |
| " // See https://g.co/AppIndexing/AndroidStudio for more information.\n" + |
| " %1$s.getInstance().end(%2$s());\n" + |
| " super.onStop();\n" + |
| "}"; |
| |
| static private final String GET_ACTION_FORMAT = "/**\n" + |
| " * ATTENTION: This was auto-generated to implement the App Indexing API.\n" + |
| " * See https://g.co/AppIndexing/AndroidStudio for more information.\n" + |
| " */\n" + |
| "public %4$s %1$s() {\n" + |
| " return %5$s.newView(\"%2$s\", \"%3$s\");\n" + |
| "}"; |
| |
| static private final String FIREBASE_USER_ACTION_START_TEMPLATE = "%1$s.getInstance().start(%2$s());"; |
| static private final String FIREBASE_USER_ACTION_END_TEMPLATE = "%1$s.getInstance().end(%2$s());"; |
| // Constant to look up existing usage. |
| static private final String FIREBASE_USER_ACTION_START = "FirebaseUserActions.getInstance().start"; |
| // Constant to look up existing usage. |
| static private final String FIREBASE_USER_ACTION_END = "FirebaseUserActions.getInstance().end"; |
| |
| static private final List<String> COMMENT_IN_JAVA = Lists |
| .newArrayList("// ATTENTION: This was auto-generated to implement the App Indexing API.", |
| "// See https://g.co/AppIndexing/AndroidStudio for more information."); |
| /* To prompt developers to update deep link url, sets default url to a noticeable wrong url */ |
| static private final String DEFAULT_DEEP_LINK_URL = "http://[ENTER-YOUR-URL-HERE]"; |
| |
| // Dependencies list of App Indexing API. |
| // It should be compatible with https://firebase.google.com/docs/app-indexing/android/app |
| // We'll check if all dependencies are added to project and module build.gradle files before generating code. |
| private static final String APP_INDEXING_LIB_DEPENDENCY = "com.google.firebase:firebase-appindexing:10.0.1+"; |
| private static final String APP_INDEXING_CLASSPATH_DEPENDENCY = "com.google.gms:google-services:3.0.0+"; |
| private static final String APP_INDEXING_PLUGIN_DEPENDENCY = "com.google.gms.google-services"; |
| |
| private Project myProject = null; |
| private Module myModule = null; |
| private PsiFile myFile = null; |
| |
| private PsiClass myActivity = null; |
| |
| private PsiElementFactory myFactory = null; |
| private CodeStyleManager myCodeStyleManager = null; |
| |
| private PsiCodeBlock myOnStart = null; |
| private PsiCodeBlock myOnStop = null; |
| |
| /* The App Indexing API statements in onStart / onStop method */ |
| private List<PsiStatement> myStartStatements = Lists.newArrayList(); |
| private List<PsiStatement> myEndStatements = Lists.newArrayList(); |
| |
| private Map<String, String> myImportClasses = Maps.newHashMap(); |
| |
| public ApiCreator(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) { |
| myProject = project; |
| myFile = file; |
| myModule = ModuleUtilCore.findModuleForFile(myFile.getVirtualFile(), myProject); |
| |
| if (myFile instanceof PsiJavaFile) { |
| PsiElement element = myFile.findElementAt(editor.getCaretModel().getOffset()); |
| if (element != null) { |
| myActivity = getSurroundingInheritingClass(element, SdkConstants.CLASS_ACTIVITY); |
| if (myActivity != null) { |
| myOnStart = getMethodBodyByName("onStart", SIGNATURE_ON_START, myActivity); |
| myOnStop = getMethodBodyByName("onStop", SIGNATURE_ON_STOP, myActivity); |
| if (myOnStart != null) { |
| myStartStatements = StatementFilter.filterCodeBlock(FIREBASE_USER_ACTION_START, myOnStart); |
| } |
| if (myOnStop != null) { |
| myEndStatements = StatementFilter.filterCodeBlock(FIREBASE_USER_ACTION_END, myOnStop); |
| } |
| } |
| } |
| } |
| myFactory = (PsiElementFactory)JVMElementFactories.getFactory(JavaLanguage.INSTANCE, myProject); |
| myCodeStyleManager = CodeStyleManager.getInstance(myProject); |
| myImportClasses.put(CLASS_ACTION, CLASS_ACTION_FULL); |
| myImportClasses.put(CLASS_BUILDERS_ACTIONS, CLASS_BUILDERS_ACTIONS_FULL); |
| myImportClasses.put(CLASS_FIREBASE_USER_ACTION, CLASS_FIREBASE_USER_ACTION_FULL); |
| } |
| |
| /** |
| * If the current caret's position is eligible for creating App Indexing API code, |
| * i.e. the caret is inside an 'Activity' class |
| * and the class does not call app indexing api. |
| * |
| * @return true if the current caret's position needs app indexing generating intention. |
| */ |
| public static boolean eligibleForInsertingAppIndexingApiCode(@NotNull Editor editor, @NotNull PsiFile file) { |
| if (!(file instanceof PsiJavaFile)) return false; |
| PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); |
| if (element == null) return false; |
| PsiClass activity = getSurroundingInheritingClass(element, SdkConstants.CLASS_ACTIVITY); |
| if (activity == null) return false; |
| if (!hasEnoughDependencies(ModuleUtilCore.findModuleForPsiElement(element))) return false; |
| PsiCodeBlock onStart = getMethodBodyByName("onStart", SIGNATURE_ON_START, activity); |
| if (onStart == null) return true; |
| PsiCodeBlock onStop = getMethodBodyByName("onStop", SIGNATURE_ON_STOP, activity); |
| if (onStop == null) return true; |
| List<PsiStatement> startStatements = StatementFilter.filterCodeBlock(FIREBASE_USER_ACTION_START, onStart); |
| if (startStatements.isEmpty()) return true; |
| List<PsiStatement> endStatements = StatementFilter.filterCodeBlock(FIREBASE_USER_ACTION_END, onStop); |
| if (endStatements.isEmpty()) return true; |
| return false; |
| } |
| |
| |
| /** |
| * Inserts the AppIndexing API code for the activity where the caret is in. |
| */ |
| |
| public void insertAppIndexingApiCodeForActivity() { |
| if (myModule == null || myActivity == null || myFactory == null || myCodeStyleManager == null) { |
| Logger.getInstance(ApiCreator.class).info("Unable to generate App Indexing API code."); |
| return; |
| } |
| insertAppIndexingApiCodeInJavaFile(getDeepLinkOfActivity()); |
| } |
| |
| |
| /** |
| * Generates App Indexing API code in Java source file. |
| * Steps: |
| * 1. Generates import statements in activity class. |
| * 2. Generates App Indexing API call in activity. |
| * |
| * @param deepLink The deep link for the activity. It may be nullable, because |
| * we should deal with situation where no deep link is specified |
| * in AndroidManifest.xml. |
| */ |
| @VisibleForTesting |
| void insertAppIndexingApiCodeInJavaFile(@Nullable String deepLink) { |
| insertImportStatements(); |
| |
| String getActionMethodCallText = insertGetActionMethod(deepLink); |
| addOrMergeOnStart(getActionMethodCallText); |
| addOrMergeOnStop(getActionMethodCallText); |
| |
| JavaCodeStyleManager.getInstance(myProject).shortenClassReferences(myActivity); |
| |
| unlockFromPsiOperation(myFile); |
| } |
| |
| /** |
| * Insert a method which construct Action instance and return it. Return the name of the method inserted. |
| * |
| * @param deepLink The deeplink used to construct the Action instance. |
| * @return The name of the method inserted. |
| */ |
| @NotNull |
| private String insertGetActionMethod(@Nullable String deepLink) { |
| String methodName = getUnusedMethodName(METHOD_GET_ACTION); |
| String pageName = myActivity.getName(); |
| pageName = pageName == null ? "" : StringUtil.trimEnd(pageName, "Activity"); |
| String url = deepLink == null ? DEFAULT_DEEP_LINK_URL : deepLink; |
| String getActionMethod = String.format( |
| GET_ACTION_FORMAT, methodName, pageName, url, myImportClasses.get(CLASS_ACTION), myImportClasses.get(CLASS_BUILDERS_ACTIONS)); |
| PsiMethod newGetAction = myFactory.createMethodFromText(getActionMethod, null); |
| myActivity.add(newGetAction); |
| return methodName; |
| } |
| |
| @NotNull |
| private String getUnusedMethodName(@NotNull String methodName) { |
| PsiMethod[] psiMethods = myActivity.getMethods(); |
| Set<String> usedMethodNames = Sets.newHashSet(); |
| for (PsiMethod p : psiMethods) { |
| usedMethodNames.add(p.getName()); |
| } |
| String unusedMethodName = methodName; |
| for (int suffix = 0; usedMethodNames.contains(unusedMethodName); suffix++) { |
| unusedMethodName = methodName + suffix; |
| } |
| return unusedMethodName; |
| } |
| |
| /** |
| * Generates import statements of app indexing Java Code. |
| */ |
| private void insertImportStatements() { |
| // Without App Indexing dependency, App-Indexing-related classes cannot be found, |
| // so "shortenClassReferences" cannot be used. |
| for (Map.Entry<String, String> className : myImportClasses.entrySet()) { |
| if (!ImportUtils.hasOnDemandImportConflict(className.getValue(), myActivity) && |
| !hasExactImportConflict(className.getValue(), (PsiJavaFile)myFile)) { |
| insertSingleImportIfNeeded(className.getValue()); |
| className.setValue(className.getKey()); |
| } |
| } |
| } |
| |
| // Copied from ImportUtils - where it was made private in 1.5 so we can't call it directly. |
| private static boolean hasExactImportConflict(String fqName, PsiJavaFile file) { |
| final PsiImportList imports = file.getImportList(); |
| if (imports == null) { |
| return false; |
| } |
| final PsiImportStatement[] importStatements = imports.getImportStatements(); |
| final int lastDotIndex = fqName.lastIndexOf((int)'.'); |
| final String shortName = fqName.substring(lastDotIndex + 1); |
| final String dottedShortName = '.' + shortName; |
| for (final PsiImportStatement importStatement : importStatements) { |
| if (importStatement.isOnDemand()) { |
| continue; |
| } |
| final String importName = importStatement.getQualifiedName(); |
| if (importName == null) { |
| return false; |
| } |
| if (!importName.equals(fqName) && importName.endsWith(dottedShortName)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Generates app indexing api calls in onStart method. |
| * |
| * @param getActionMethodCallText The name of the method getAction. |
| */ |
| private void addOrMergeOnStart(@NotNull String getActionMethodCallText) { |
| if (myOnStart == null) { |
| String onStartMethod = |
| String.format(ON_START_FORMAT, myImportClasses.get(CLASS_FIREBASE_USER_ACTION), getActionMethodCallText); |
| myActivity.add(myFactory.createMethodFromText(onStartMethod, null)); |
| return; |
| } |
| // AppIndex.AppIndexApi.start() at the bottom. |
| if (myStartStatements.isEmpty()) { |
| String startText = |
| String.format(FIREBASE_USER_ACTION_START_TEMPLATE, myImportClasses.get(CLASS_FIREBASE_USER_ACTION), getActionMethodCallText); |
| PsiElement startStatement = myFactory.createStatementFromText(startText, null); |
| startStatement = myOnStart.add(startStatement); |
| addCommentsBefore(COMMENT_IN_JAVA, myOnStart, startStatement); |
| } |
| } |
| |
| /** |
| * Generates app indexing api calls in onStop method. |
| * |
| * @param getActionMethodCallText The name of the method getAction. |
| */ |
| private void addOrMergeOnStop(@NotNull String getActionMethodCallText) { |
| if (myOnStop == null) { |
| String onStopMethod = String.format(ON_STOP_FORMAT, myImportClasses.get(CLASS_FIREBASE_USER_ACTION), getActionMethodCallText); |
| myActivity.add(myFactory.createMethodFromText(onStopMethod, null)); |
| return; |
| } |
| |
| if (!myEndStatements.isEmpty()) { |
| return; |
| } |
| // Creates Action variable and AppIndex.AppIndexApi.end() on the top. |
| PsiStatement endStatement = myFactory.createStatementFromText( |
| String.format(FIREBASE_USER_ACTION_END_TEMPLATE, myImportClasses.get(CLASS_FIREBASE_USER_ACTION), getActionMethodCallText), null); |
| List<PsiStatement> superOnStopStatements = StatementFilter.filterCodeBlock("super.onStop();", myOnStop); |
| if (!superOnStopStatements.isEmpty()) { |
| // after "super.onStop();" |
| endStatement = (PsiStatement)myOnStop.addAfter(endStatement, superOnStopStatements.get(0)); |
| addCommentsBefore(COMMENT_IN_JAVA, myOnStop, endStatement); |
| } |
| else { |
| // after "{" in the code block. |
| endStatement = (PsiStatement)myOnStop.addAfter(endStatement, myOnStop.getFirstBodyElement()); |
| addCommentsBefore(COMMENT_IN_JAVA, myOnStop, endStatement); |
| } |
| } |
| |
| /** |
| * Returns deep links of current activity in AndroidManifest.xml. |
| */ |
| @Nullable |
| @VisibleForTesting |
| String getDeepLinkOfActivity() { |
| XmlFile manifest = ManifestUtils.getAndroidManifestPsi(myModule); |
| if (manifest != null && manifest.getRootTag() != null) { |
| List<XmlTag> activityTags = ManifestUtils.searchXmlTagsByName(manifest.getRootTag(), SdkConstants.TAG_ACTIVITY); |
| for (XmlTag activityTag : activityTags) { |
| String activityName = activityTag.getAttributeValue(SdkConstants.ATTR_NAME, SdkConstants.ANDROID_URI); |
| if (activityName != null && activityName.equals("." + myActivity.getName())) { |
| List<String> deepLinks = DeepLinkUtils.getAllDeepLinks(activityTag); |
| if (!deepLinks.isEmpty()) { |
| return deepLinks.get(0); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Adds comments before a specific psi element. |
| * |
| * @param texts The text of the comments. |
| * "// Comment1. |
| * // Comment2." are 2 comments. |
| * Factory cannot create 2 comments together, so we split them into list of comment texts. |
| * @param element The psi element for adding comment in. |
| * @param anchor The psi element the added comment is anchored before. |
| */ |
| private void addCommentsBefore(@NotNull List<String> texts, @NotNull PsiElement element, @NotNull PsiElement anchor) { |
| myCodeStyleManager.reformat(element); |
| for (String text : texts) { |
| element.addBefore(myFactory.createCommentFromText(text, null), anchor); |
| myCodeStyleManager.reformat(element); |
| } |
| } |
| |
| /** |
| * Unlocks the file locked by PSI operation, so that the project can sync. |
| * |
| * @param file The file locked by PSI operation. |
| */ |
| private void unlockFromPsiOperation(@NotNull PsiFile file) { |
| Document doc = PsiDocumentManager.getInstance(myProject).getDocument(file); |
| if (doc != null) { |
| PsiDocumentManager.getInstance(myProject).doPostponedOperationsAndUnblockDocument(doc); |
| } |
| } |
| |
| /** |
| * Gets the surrounding class, if it inherits from a particular super class. |
| * |
| * @param element The PsiElement for query. |
| * @param inheritedClass the name of the class to inherit from |
| * @return The surrounding class that meet the inheriting requirement. |
| */ |
| @Nullable |
| private static PsiClass getSurroundingInheritingClass(@NotNull PsiElement element, @NotNull String inheritedClass) { |
| while (element != null) { |
| if (element instanceof PsiClass) { |
| PsiClass psiClass = (PsiClass)element; |
| if (InheritanceUtil.isInheritor(psiClass, inheritedClass)) { |
| return psiClass; |
| } |
| } |
| if (element instanceof PsiFile) { |
| return null; |
| } |
| element = element.getParent(); |
| } |
| return null; |
| } |
| |
| /** |
| * Gets the body of the method in a specific class, given the method name. |
| * |
| * @param name The method name. |
| * @param parametersTypeName The type name of the parameters in the specific method. |
| * @param psiClass The class to find method in. |
| * @return The code block body of the method, which will be null if no method is found with designated name or parameters. |
| */ |
| @Nullable |
| public static PsiCodeBlock getMethodBodyByName(@NotNull String name, |
| @NotNull List<String> parametersTypeName, |
| @NotNull PsiClass psiClass) { |
| PsiMethod[] psiMethods = psiClass.findMethodsByName(name, false); |
| for (PsiMethod psiMethod : psiMethods) { |
| PsiType[] types = psiMethod.getSignature(PsiSubstitutor.EMPTY).getParameterTypes(); |
| if (types.length != parametersTypeName.size()) { |
| continue; |
| } |
| boolean correctSignature = true; |
| for (int i = 0; i < types.length; i++) { |
| if (!types[i].getCanonicalText().equals(parametersTypeName.get(i))) { |
| correctSignature = false; |
| break; |
| } |
| } |
| if (correctSignature) { |
| return psiMethod.getBody(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Inserts a single import statement. |
| * |
| * @param className The name of the import class. |
| */ |
| private void insertSingleImportIfNeeded(@NotNull String className) { |
| PsiImportList importList = ((PsiJavaFile)myFile).getImportList(); |
| if (importList != null && !hasImportStatement(importList, className)) { |
| String dummyFileName = "_Dummy_" + className + "_." + JavaFileType.INSTANCE.getDefaultExtension(); |
| PsiJavaFile aFile = (PsiJavaFile)PsiFileFactory.getInstance(myProject) |
| .createFileFromText(dummyFileName, JavaFileType.INSTANCE, "import " + className + ";"); |
| PsiImportList dummyImportList = aFile.getImportList(); |
| if (dummyImportList != null) { |
| PsiImportStatement[] statements = dummyImportList.getImportStatements(); |
| PsiImportStatement statement = (PsiImportStatement)myCodeStyleManager.reformat(statements[0]); |
| importList.add(statement); |
| } |
| } |
| } |
| |
| /** |
| * If an class has been imported in the importList. |
| * |
| * @param importList The import list. |
| * @param className The name of the class to be imported. |
| * @return True if an class has been imported. |
| */ |
| public static boolean hasImportStatement(@NotNull PsiImportList importList, @NotNull String className) { |
| String packageName = className.substring(0, className.lastIndexOf('.')); |
| PsiImportStatement singleImport = importList.findSingleClassImportStatement(className); |
| PsiImportStatement onDemandImport = importList.findOnDemandImportStatement(packageName); |
| return singleImport != null || onDemandImport != null; |
| } |
| |
| |
| /** |
| * Check if the existing dependencies are enough to insert App Indexing code. |
| * |
| * @return true if there are enough dependencies. |
| */ |
| // TODO: Consider to replace this method with DependencyStateManager.getPendingDependencies method, once it's refactored out from firebase module. |
| static boolean hasEnoughDependencies(@NotNull Module module) { |
| final GradleBuildModel gradleBuildModel = GradleBuildModel.get(module); |
| // Possibly null in scenario such as gradle sync in progress. |
| if (gradleBuildModel == null) { |
| return false; |
| } |
| DependenciesModel dependenciesModel = gradleBuildModel.dependencies(); |
| if (dependenciesModel == null) { |
| return false; |
| } |
| if (!hasDependency(APP_INDEXING_LIB_DEPENDENCY, dependenciesModel.artifacts())) { |
| return false; |
| } |
| |
| // Check the plugin dependency. |
| final List<String> plugins = getValues(gradleBuildModel.appliedPlugins()); |
| if (!plugins.contains(APP_INDEXING_PLUGIN_DEPENDENCY)) { |
| return false; |
| } |
| |
| // Check classpath dependency at the project level. |
| final GradleBuildModel projectGradleBuildModel = GradleBuildModel.get(module.getProject()); |
| if (projectGradleBuildModel == null) { |
| return false; |
| } |
| final DependenciesModel dependencies = projectGradleBuildModel.buildscript().dependencies(); |
| return hasDependency(APP_INDEXING_CLASSPATH_DEPENDENCY, dependencies.artifacts(CommonConfigurationNames.CLASSPATH)); |
| } |
| |
| /** |
| * Check if the designated dependency already exists |
| * |
| * @param dependencyValue module or classpath dependency value with version. |
| * @param existingDeps current existing dependencies. |
| * @return true if the designated dependency already exists. |
| */ |
| @NotNull |
| private static boolean hasDependency(@NotNull String dependencyValue, |
| @NotNull List<ArtifactDependencyModel> existingDeps) { |
| |
| ArtifactDependencySpec requiredDepSpec = ArtifactDependencySpec.create(dependencyValue); |
| if (requiredDepSpec == null) { |
| return true; |
| } |
| for (ArtifactDependencyModel dependency : existingDeps) { |
| ArtifactDependencySpec foundDepSpec = ArtifactDependencySpec.create(dependency); |
| if (foundDepSpec.equalsIgnoreVersion(requiredDepSpec) && |
| VersionComparatorUtil.compare(foundDepSpec.version, requiredDepSpec.version) >= 0) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |