b72334373f037c54ef9aa2800891795d51c9d960
[idea/community.git] / java / idea-ui / src / com / intellij / ide / util / importProject / JavaModuleInsight.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.ide.util.importProject;
3
4 import com.intellij.ide.highlighter.JavaFileType;
5 import com.intellij.ide.util.projectWizard.importSources.DetectedProjectRoot;
6 import com.intellij.ide.util.projectWizard.importSources.DetectedSourceRoot;
7 import com.intellij.ide.util.projectWizard.importSources.JavaModuleSourceRoot;
8 import com.intellij.ide.util.projectWizard.importSources.JavaSourceRootDetectionUtil;
9 import com.intellij.lang.java.JavaParserDefinition;
10 import com.intellij.lexer.Lexer;
11 import com.intellij.openapi.application.ReadAction;
12 import com.intellij.openapi.diagnostic.Logger;
13 import com.intellij.openapi.module.StdModuleTypes;
14 import com.intellij.openapi.progress.ProcessCanceledException;
15 import com.intellij.openapi.progress.ProgressIndicator;
16 import com.intellij.openapi.project.ProjectManager;
17 import com.intellij.openapi.util.io.FileUtil;
18 import com.intellij.openapi.util.text.StringUtil;
19 import com.intellij.pom.java.LanguageLevel;
20 import com.intellij.psi.*;
21 import com.intellij.util.Consumer;
22 import com.intellij.util.IncorrectOperationException;
23 import com.intellij.util.containers.ContainerUtil;
24 import one.util.streamex.StreamEx;
25 import org.jetbrains.annotations.NotNull;
26 import org.jetbrains.annotations.Nullable;
27
28 import java.io.File;
29 import java.io.IOException;
30 import java.util.*;
31 import java.util.zip.ZipEntry;
32 import java.util.zip.ZipFile;
33
34 public final class JavaModuleInsight extends ModuleInsight {
35   private static final Logger LOG = Logger.getInstance(JavaModuleInsight.class);
36   private final Lexer myLexer;
37
38   public JavaModuleInsight(@Nullable final ProgressIndicator progress,
39                            Set<String> existingModuleNames,
40                            Set<String> existingProjectLibraryNames) {
41     super(progress, existingModuleNames, existingProjectLibraryNames);
42     myLexer = JavaParserDefinition.createLexer(LanguageLevel.JDK_1_5);
43   }
44
45   @Override
46   public void scanModules() {
47     scanModuleInfoFiles();
48
49     super.scanModules();
50   }
51
52   private void scanModuleInfoFiles() {
53     final List<DetectedSourceRoot> allRoots = super.getSourceRootsToScan();
54     final List<JavaModuleSourceRoot> moduleInfoRoots = StreamEx
55       .of(allRoots)
56       .select(JavaModuleSourceRoot.class)
57       .filter(JavaModuleSourceRoot::isWithModuleInfoFile)
58       .filter(root -> !isIgnoredName(root.getDirectory()))
59       .toList();
60     if (moduleInfoRoots.isEmpty()) {
61       return;
62     }
63     myProgress.setIndeterminate(true);
64     myProgress.pushState();
65     try {
66       Map<String, ModuleInfo> moduleInfos = new HashMap<>();
67       for (JavaModuleSourceRoot moduleInfoRoot : moduleInfoRoots) {
68         final File sourceRoot = moduleInfoRoot.getDirectory();
69         myProgress.setText("Scanning " + sourceRoot.getPath());
70         final ModuleInfo moduleInfo = scanModuleInfoFile(sourceRoot);
71         if (moduleInfo != null) {
72           moduleInfo.descriptor = createModuleDescriptor(moduleInfo.directory, Collections.singletonList(moduleInfoRoot));
73           moduleInfos.put(moduleInfo.name, moduleInfo);
74           addExportedPackages(sourceRoot, moduleInfo.exportsPackages);
75         }
76       }
77       myProgress.setText("Building modules layout...");
78       for (ModuleInfo moduleInfo : moduleInfos.values()) {
79         for (String requiresModule : moduleInfo.requiresModules) {
80           ModuleInfo requiredModuleInfo = moduleInfos.get(requiresModule);
81           if (requiredModuleInfo != null) {
82             moduleInfo.descriptor.addDependencyOn(requiredModuleInfo.descriptor);
83           }
84         }
85       }
86
87       addModules(StreamEx.of(moduleInfos.values()).map(info -> info.descriptor).toList());
88     }
89     catch (ProcessCanceledException ignored) { }
90     finally {
91       myProgress.popState();
92     }
93   }
94
95   @NotNull
96   @Override
97   protected List<DetectedSourceRoot> getSourceRootsToScan() {
98     final List<DetectedSourceRoot> allRoots = super.getSourceRootsToScan();
99     return ContainerUtil.filter(allRoots, r -> !(r instanceof JavaModuleSourceRoot) || !((JavaModuleSourceRoot)r).isWithModuleInfoFile());
100   }
101
102   private ModuleInfo scanModuleInfoFile(@NotNull File directory) {
103     File file = new File(directory, PsiJavaModule.MODULE_INFO_FILE);
104     myProgress.setText2(file.getName());
105     try {
106       String text = FileUtil.loadFile(file);
107
108       PsiFileFactory factory = PsiFileFactory.getInstance(ProjectManager.getInstance().getDefaultProject());
109       ModuleInfo moduleInfo = ReadAction.compute(() -> {
110         PsiFile psiFile = factory.createFileFromText(PsiJavaModule.MODULE_INFO_FILE, JavaFileType.INSTANCE, text);
111         PsiJavaModule javaModule = psiFile instanceof PsiJavaFile ? ((PsiJavaFile)psiFile).getModuleDeclaration() : null;
112         if (javaModule == null) {
113           throw new IncorrectOperationException("Incorrect module declaration '" + file.getPath() + "'");
114         }
115         ModuleInfo info = new ModuleInfo(javaModule.getName());
116         javaModule.accept(new ModuleInfoVisitor(info));
117         return info;
118       });
119
120       File moduleDirectory = directory;
121       while (!isEntryPointRoot(moduleDirectory) && !moduleInfo.name.equals(moduleDirectory.getName())) {
122         File parent = moduleDirectory.getParentFile();
123         if (parent == null) break;
124         moduleDirectory = parent;
125       }
126       moduleInfo.directory = moduleDirectory;
127
128       return moduleInfo;
129     }
130     catch (IOException | IncorrectOperationException e) {
131       LOG.info(e);
132       return null;
133     }
134   }
135
136   @Override
137   protected boolean isSourceFile(final File file) {
138     return StringUtil.endsWithIgnoreCase(file.getName(), ".java");
139   }
140
141   @Override
142   protected boolean isLibraryFile(final String fileName) {
143     return StringUtil.endsWithIgnoreCase(fileName, ".jar") || StringUtil.endsWithIgnoreCase(fileName, ".zip");
144   }
145
146   @Override
147   protected void scanSourceFileForImportedPackages(final CharSequence chars, final Consumer<String> result) {
148     myLexer.start(chars);
149
150     JavaSourceRootDetectionUtil.skipWhiteSpaceAndComments(myLexer);
151     if (myLexer.getTokenType() == JavaTokenType.PACKAGE_KEYWORD) {
152       advanceLexer(myLexer);
153       if (readPackageName(chars, myLexer) == null) {
154         return;
155       }
156     }
157
158     while (true) {
159       if (myLexer.getTokenType() == JavaTokenType.SEMICOLON) {
160         advanceLexer(myLexer);
161       }
162       if (myLexer.getTokenType() != JavaTokenType.IMPORT_KEYWORD) {
163         return;
164       }
165       advanceLexer(myLexer);
166
167       boolean isStaticImport = false;
168       if (myLexer.getTokenType() == JavaTokenType.STATIC_KEYWORD) {
169         isStaticImport = true;
170         advanceLexer(myLexer);
171       }
172
173       final String packageName = readPackageName(chars, myLexer);
174       if (packageName == null) {
175         return;
176       }
177
178       if (packageName.endsWith(".*")) {
179         result.consume(packageName.substring(0, packageName.length() - ".*".length()));
180       }
181       else {
182         int lastDot = packageName.lastIndexOf('.');
183         if (lastDot > 0) {
184           String _packageName = packageName.substring(0, lastDot);
185           if (isStaticImport) {
186             lastDot = _packageName.lastIndexOf('.');
187             if (lastDot > 0) {
188               result.consume(_packageName.substring(0, lastDot));
189             }
190           }
191           else {
192             result.consume(_packageName);
193           }
194         }
195       }
196     }
197   }
198
199   @Nullable
200   private static String readPackageName(final CharSequence text, final Lexer lexer) {
201     final StringBuilder buffer = new StringBuilder();
202     while (true) {
203       if (lexer.getTokenType() != JavaTokenType.IDENTIFIER && lexer.getTokenType() != JavaTokenType.ASTERISK) {
204         break;
205       }
206       buffer.append(text, lexer.getTokenStart(), lexer.getTokenEnd());
207
208       advanceLexer(lexer);
209       if (lexer.getTokenType() != JavaTokenType.DOT) {
210         break;
211       }
212       buffer.append('.');
213
214       advanceLexer(lexer);
215     }
216
217     String packageName = buffer.toString();
218     if (packageName.length() == 0 || StringUtil.endsWithChar(packageName, '.') || StringUtil.startsWithChar(packageName, '*')) {
219       return null;
220     }
221     return packageName;
222   }
223
224   private static void advanceLexer(final Lexer lexer) {
225     lexer.advance();
226     JavaSourceRootDetectionUtil.skipWhiteSpaceAndComments(lexer);
227   }
228
229   @Override
230   protected void scanLibraryForDeclaredPackages(File file, Consumer<String> result) throws IOException {
231     try (ZipFile zip = new ZipFile(file)) {
232       final Enumeration<? extends ZipEntry> entries = zip.entries();
233       while (entries.hasMoreElements()) {
234         final String entryName = entries.nextElement().getName();
235         if (StringUtil.endsWithIgnoreCase(entryName, ".class")) {
236           final int index = entryName.lastIndexOf('/');
237           if (index > 0) {
238             final String packageName = entryName.substring(0, index).replace('/', '.');
239             result.consume(packageName);
240           }
241         }
242       }
243     }
244   }
245
246   @Override
247   protected ModuleDescriptor createModuleDescriptor(final File moduleContentRoot, final Collection<DetectedSourceRoot> sourceRoots) {
248     return new ModuleDescriptor(moduleContentRoot, StdModuleTypes.JAVA, sourceRoots);
249   }
250
251   @Override
252   public boolean isApplicableRoot(final DetectedProjectRoot root) {
253     return root instanceof JavaModuleSourceRoot;
254   }
255
256   private static final class ModuleInfo {
257     final String name;
258     final Set<String> requiresModules = new HashSet<>();
259     final Set<String> exportsPackages = new HashSet<>();
260
261     File directory;
262     ModuleDescriptor descriptor;
263
264     private ModuleInfo(@NotNull String name) {
265       this.name = name;
266     }
267   }
268
269   private static class ModuleInfoVisitor extends JavaRecursiveElementVisitor {
270     private final ModuleInfo myInfo;
271
272     ModuleInfoVisitor(ModuleInfo info) {
273       myInfo = info;
274     }
275
276     @Override
277     public void visitRequiresStatement(PsiRequiresStatement statement) {
278       super.visitRequiresStatement(statement);
279       String referenceText = statement.getModuleName();
280       if (referenceText != null) {
281         myInfo.requiresModules.add(referenceText);
282       }
283     }
284
285     @Override
286     public void visitPackageAccessibilityStatement(PsiPackageAccessibilityStatement statement) {
287       super.visitPackageAccessibilityStatement(statement);
288       if (statement.getRole() == PsiPackageAccessibilityStatement.Role.EXPORTS) {
289         PsiJavaCodeReferenceElement reference = statement.getPackageReference();
290         if (reference != null) {
291           String qualifiedName = reference.getQualifiedName();
292           if (qualifiedName != null) {
293             myInfo.exportsPackages.add(qualifiedName);
294           }
295         }
296       }
297     }
298   }
299 }