jps model: support excluding file from content roots by pattern (IDEA-127753)

Currently only file/directory names, not relative paths, are matched.
This commit is contained in:
nik
2017-05-25 15:02:30 +03:00
parent e01380eb34
commit 20782eb0e4
11 changed files with 229 additions and 5 deletions
@@ -15,13 +15,17 @@
*/
package org.jetbrains.jps.indices.impl;
import com.intellij.openapi.fileTypes.impl.FileTypeAssocTable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.MultiMap;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.jps.indices.ModuleExcludeIndex;
import org.jetbrains.jps.model.JpsExcludePattern;
import org.jetbrains.jps.model.JpsModel;
import org.jetbrains.jps.model.fileTypes.FileNameMatcherFactory;
import org.jetbrains.jps.model.java.JpsJavaExtensionService;
import org.jetbrains.jps.model.java.JpsJavaModuleExtension;
import org.jetbrains.jps.model.java.JpsJavaProjectExtension;
@@ -41,10 +45,12 @@ public class ModuleExcludeIndexImpl implements ModuleExcludeIndex {
private final Set<File> myTopLevelContentRoots = new THashSet<>(FileUtil.FILE_HASHING_STRATEGY);
private final Map<JpsModule, ArrayList<File>> myModuleToExcludesMap = new THashMap<>();
private final Map<JpsModule, List<File>> myModuleToContentMap = new THashMap<>();
private final Map<File, FileTypeAssocTable<Boolean>> myExcludeFromContentRootTables = new THashMap<>(FileUtil.FILE_HASHING_STRATEGY);
public ModuleExcludeIndexImpl(JpsModel model) {
final Collection<JpsModule> allModules = model.getProject().getModules();
Map<File, JpsModule> contentToModule = new THashMap<>(FileUtil.FILE_HASHING_STRATEGY);
MultiMap<String, String> excludePatterns = MultiMap.createLinked();
for (final JpsModule module : allModules) {
final ArrayList<File> moduleExcludes = new ArrayList<>();
for (String url : module.getExcludeRootsList().getUrls()) {
@@ -61,6 +67,9 @@ public class ModuleExcludeIndexImpl implements ModuleExcludeIndex {
moduleExcludes.add(JpsPathUtil.urlToFile(testOutputUrl));
}
}
for (JpsExcludePattern pattern : module.getExcludePatterns()) {
excludePatterns.putValue(pattern.getBaseDirUrl(), pattern.getPattern());
}
List<String> contentUrls = module.getContentRootsList().getUrls();
final List<File> moduleContent = new ArrayList<>(contentUrls.size());
for (String contentUrl : contentUrls) {
@@ -78,6 +87,15 @@ public class ModuleExcludeIndexImpl implements ModuleExcludeIndex {
myExcludedRoots.addAll(moduleExcludes);
}
FileNameMatcherFactory factory = FileNameMatcherFactory.getInstance();
for (Map.Entry<String, Collection<String>> entry : excludePatterns.entrySet()) {
FileTypeAssocTable<Boolean> table = new FileTypeAssocTable<>();
for (String pattern : entry.getValue()) {
table.addAssociation(factory.createMatcher(pattern), Boolean.TRUE);
}
myExcludeFromContentRootTables.put(JpsPathUtil.urlToFile(entry.getKey()), table);
}
JpsJavaProjectExtension projectExtension = JpsJavaExtensionService.getInstance().getProjectExtension(model.getProject());
if (projectExtension != null) {
String url = projectExtension.getOutputUrl();
@@ -168,7 +186,7 @@ public class ModuleExcludeIndexImpl implements ModuleExcludeIndex {
private enum FileLocation { IN_CONTENT, EXCLUDED, NOT_IN_PROJECT }
private static FileLocation determineFileLocation(File file, Collection<File> roots, Collection<File> excluded) {
private FileLocation determineFileLocation(File file, Collection<File> roots, Collection<File> excluded) {
if (roots.isEmpty() && excluded.isEmpty()) {
return FileLocation.NOT_IN_PROJECT; // optimization
}
@@ -177,6 +195,10 @@ public class ModuleExcludeIndexImpl implements ModuleExcludeIndex {
if (excluded.contains(current)) {
return FileLocation.EXCLUDED;
}
FileTypeAssocTable<Boolean> table = myExcludeFromContentRootTables.get(current);
if (table != null && isExcludedByPattern(file, current, table)) {
return FileLocation.EXCLUDED;
}
if (roots.contains(current)) {
return FileLocation.IN_CONTENT;
}
@@ -185,6 +207,18 @@ public class ModuleExcludeIndexImpl implements ModuleExcludeIndex {
return FileLocation.NOT_IN_PROJECT;
}
private static boolean isExcludedByPattern(File file, File root, FileTypeAssocTable<Boolean> table) {
File current = file;
//noinspection FileEqualsUsage it's ok to compare files by 'equals' here be because these files are produced by the same 'getParentFile' calls
while (current != null && !current.equals(root)) {
if (table.findAssociatedFileType(current.getName()) != null) {
return true;
}
current = FileUtilRt.getParentFile(current);
}
return false;
}
@Override
public Collection<File> getModuleExcludes(JpsModule module) {
return myModuleToExcludesMap.get(module);
@@ -153,6 +153,23 @@ public class ModuleExcludeIndexTest extends JpsJavaModelTestCase {
assertExcluded(src);
}
public void testExcludeByPattern() {
File root1 = new File(myRoot, "root1");
File root2 = new File(myRoot, "root2");
JpsModule module = addModule();
addContentRoot(module, root1);
addContentRoot(module, root2);
module.addExcludePattern(JpsPathUtil.pathToUrl(root1.getAbsolutePath()), "*.txt");
module.addExcludePattern(JpsPathUtil.pathToUrl(root2.getAbsolutePath()), "out");
assertExcluded(new File(root1, "a.txt"));
assertExcluded(new File(root1, "dir/a.txt"));
assertNotExcluded(new File(root1, "A.java"));
assertNotExcluded(new File(root2, "a.txt"));
assertExcluded(new File(root2, "out"));
assertExcluded(new File(root2, "out/A.java"));
assertExcluded(new File(root2, "dir/out/A.java"));
}
private static void addSourceRoot(JpsModule module, File src) {
module.addSourceRoot(JpsPathUtil.pathToUrl(src.getAbsolutePath()), JavaSourceRootType.SOURCE);
}
@@ -0,0 +1,33 @@
/*
* 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.jps.model;
import org.jetbrains.annotations.NotNull;
/**
* Specifies a pattern for names of files which should be excluded from a module. If name of a file under {@link #getBaseDirUrl() the base directory}
* matches {@link #getPattern() the pattern} it'll be excluded from the containing module, if name of a directory matches the pattern the directory
* and all of its contents will be excluded. '?' and '*' wildcards are supported.
*
* @author nik
*/
public interface JpsExcludePattern extends JpsElement {
@NotNull
String getBaseDirUrl();
@NotNull
String getPattern();
}
@@ -56,6 +56,14 @@ public interface JpsModule extends JpsNamedElement, JpsReferenceableElement<JpsM
void removeSourceRoot(@NotNull String url, @NotNull JpsModuleSourceRootType rootType);
/**
* Add a {@link JpsExcludePattern pattern} for names of files which should be excluded from this module.
*/
void addExcludePattern(@NotNull String baseDirUrl, @NotNull String pattern);
void removeExcludePattern(@NotNull String baseDirUrl, @NotNull String pattern);
List<JpsExcludePattern> getExcludePatterns();
JpsDependenciesList getDependenciesList();
@NotNull
@@ -0,0 +1,55 @@
/*
* 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.jps.model.impl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.model.JpsExcludePattern;
import org.jetbrains.jps.model.ex.JpsElementBase;
/**
* @author nik
*/
public class JpsExcludePatternImpl extends JpsElementBase<JpsExcludePatternImpl> implements JpsExcludePattern {
private final String myBaseDirUrl;
private final String myPattern;
public JpsExcludePatternImpl(@NotNull String baseDirUrl, @NotNull String pattern) {
myBaseDirUrl = baseDirUrl;
myPattern = pattern;
}
@NotNull
@Override
public String getBaseDirUrl() {
return myBaseDirUrl;
}
@NotNull
@Override
public String getPattern() {
return myPattern;
}
@NotNull
@Override
public JpsExcludePatternImpl createCopy() {
return new JpsExcludePatternImpl(myBaseDirUrl, myPattern);
}
@Override
public void applyChanges(@NotNull JpsExcludePatternImpl modified) {
}
}
@@ -19,7 +19,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.model.*;
import org.jetbrains.jps.model.ex.JpsElementChildRoleBase;
import org.jetbrains.jps.model.ex.JpsElementCollectionRole;
import org.jetbrains.jps.model.ex.JpsNamedCompositeElementBase;
import org.jetbrains.jps.model.impl.JpsExcludePatternImpl;
import org.jetbrains.jps.model.impl.JpsUrlListRole;
import org.jetbrains.jps.model.library.JpsLibrary;
import org.jetbrains.jps.model.library.JpsLibraryCollection;
@@ -32,6 +34,7 @@ import org.jetbrains.jps.model.library.sdk.JpsSdkType;
import org.jetbrains.jps.model.library.sdk.JpsSdkReference;
import org.jetbrains.jps.model.module.*;
import java.util.Collections;
import java.util.List;
/**
@@ -41,6 +44,7 @@ public class JpsModuleImpl<P extends JpsElement> extends JpsNamedCompositeElemen
private static final JpsUrlListRole CONTENT_ROOTS_ROLE = new JpsUrlListRole("content roots");
private static final JpsUrlListRole EXCLUDED_ROOTS_ROLE = new JpsUrlListRole("excluded roots");
private static final JpsElementChildRole<JpsDependenciesListImpl> DEPENDENCIES_LIST_CHILD_ROLE = JpsElementChildRoleBase.create("dependencies");
private static final JpsElementCollectionRole<JpsExcludePattern> EXCLUDE_PATTERNS_ROLE = JpsElementCollectionRole.create(JpsElementChildRoleBase.create("exclude patterns"));
private final JpsModuleType<P> myModuleType;
private final JpsLibraryCollection myLibraryCollection;
@@ -140,6 +144,29 @@ public class JpsModuleImpl<P extends JpsElement> extends JpsNamedCompositeElemen
}
}
@Override
public void addExcludePattern(@NotNull String baseDirUrl, @NotNull String pattern) {
myContainer.getOrSetChild(EXCLUDE_PATTERNS_ROLE).addChild(new JpsExcludePatternImpl(baseDirUrl, pattern));
}
@Override
public void removeExcludePattern(@NotNull String baseDirUrl, @NotNull String pattern) {
JpsElementCollection<JpsExcludePattern> child = myContainer.getChild(EXCLUDE_PATTERNS_ROLE);
if (child != null) {
for (JpsExcludePattern excludePattern : child.getElements()) {
if (excludePattern.getBaseDirUrl().equals(baseDirUrl) && excludePattern.getPattern().equals(pattern)) {
child.removeChild(excludePattern);
}
}
}
}
@Override
public List<JpsExcludePattern> getExcludePatterns() {
JpsElementCollection<JpsExcludePattern> child = myContainer.getChild(EXCLUDE_PATTERNS_ROLE);
return child != null ? child.getElements() : Collections.emptyList();
}
@NotNull
@Override
public JpsDependenciesList getDependenciesList() {
@@ -52,6 +52,14 @@ public class JpsModuleTest extends JpsModelTestCase {
assertSameElements(ContainerUtil.newArrayList(modules), module);
}
public void testExcludedPatterns() {
JpsModule module = myProject.addModule("m", JpsJavaModuleType.INSTANCE);
module.addExcludePattern("file://url", "*.class");
JpsExcludePattern pattern = assertOneElement(module.getExcludePatterns());
assertEquals("file://url", pattern.getBaseDirUrl());
assertEquals("*.class", pattern.getPattern());
}
public void testModifiableModel() {
final JpsModule module = myProject.addModule("m", JpsJavaModuleType.INSTANCE);
final JpsModuleSourceRoot root0 = module.addSourceRoot("url1", JavaSourceRootType.SOURCE);
@@ -21,10 +21,7 @@ import com.intellij.util.text.UniqueNameGenerator;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.model.JpsCompositeElement;
import org.jetbrains.jps.model.JpsElement;
import org.jetbrains.jps.model.JpsElementFactory;
import org.jetbrains.jps.model.JpsElementReference;
import org.jetbrains.jps.model.*;
import org.jetbrains.jps.model.java.JpsJavaSdkType;
import org.jetbrains.jps.model.java.JpsJavaSdkTypeWrapper;
import org.jetbrains.jps.model.library.JpsLibrary;
@@ -54,6 +51,8 @@ public class JpsModuleRootModelSerializer {
public static final String PACKAGE_PREFIX_ATTRIBUTE = "packagePrefix";
public static final String IS_TEST_SOURCE_ATTRIBUTE = "isTestSource";
public static final String EXCLUDE_FOLDER_TAG = "excludeFolder";
public static final String EXCLUDE_PATTERN_TAG = "excludePattern";
public static final String EXCLUDE_PATTERN_ATTRIBUTE = "pattern";
public static final String ORDER_ENTRY_TAG = "orderEntry";
public static final String TYPE_ATTRIBUTE = "type";
public static final String SOURCE_FOLDER_TYPE = "sourceFolder";
@@ -85,6 +84,9 @@ public class JpsModuleRootModelSerializer {
for (Element excludeElement : getChildren(contentElement, EXCLUDE_FOLDER_TAG)) {
module.getExcludeRootsList().addUrl(excludeElement.getAttributeValue(URL_ATTRIBUTE));
}
for (Element excludePatternElement : getChildren(contentElement, EXCLUDE_PATTERN_TAG)) {
module.addExcludePattern(url, excludePatternElement.getAttributeValue(EXCLUDE_PATTERN_ATTRIBUTE));
}
}
final JpsDependenciesList dependenciesList = module.getDependenciesList();
@@ -202,6 +204,11 @@ public class JpsModuleRootModelSerializer {
contentElement.addContent(element);
}
}
for (JpsExcludePattern pattern : module.getExcludePatterns()) {
if (pattern.getBaseDirUrl().equals(url)) {
contentElement.addContent(new Element(EXCLUDE_PATTERN_TAG).setAttribute(EXCLUDE_PATTERN_ATTRIBUTE, pattern.getPattern()));
}
}
}
for (JpsDependencyElement dependency : module.getDependenciesList().getDependencies()) {
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludePattern pattern="*.class" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/excludePatterns.iml" filepath="$PROJECT_DIR$/excludePatterns.iml" />
</modules>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" assert-keyword="true" jdk-15="true" project-jdk-name="1.4" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
@@ -21,6 +21,7 @@ import org.jdom.Element;
import org.jetbrains.jps.model.JpsDummyElement;
import org.jetbrains.jps.model.JpsEncodingConfigurationService;
import org.jetbrains.jps.model.JpsEncodingProjectConfiguration;
import org.jetbrains.jps.model.JpsExcludePattern;
import org.jetbrains.jps.model.artifact.JpsArtifactService;
import org.jetbrains.jps.model.java.*;
import org.jetbrains.jps.model.library.JpsLibrary;
@@ -117,6 +118,16 @@ public class JpsProjectSerializationTest extends JpsSerializationTestCase {
assertSame(productionModule, testModuleProperties.getProductionModule());
}
public void testExcludePatterns() {
String projectPath = "/jps/model-serialization/testData/excludePatterns";
loadProject(projectPath + "/excludePatterns.ipr");
JpsModule module = assertOneElement(myProject.getModules());
JpsExcludePattern pattern = assertOneElement(module.getExcludePatterns());
assertEquals("*.class", pattern.getPattern());
assertEquals(assertOneElement(module.getContentRootsList().getUrls()), pattern.getBaseDirUrl());
doTestSaveModule(module, projectPath + "/excludePatterns.iml");
}
public void testProjectSdkWithoutType() {
loadProject("/jps/model-serialization/testData/projectSdkWithoutType/projectSdkWithoutType.ipr");
JpsSdkReference<JpsDummyElement> reference = myProject.getSdkReferencesTable().getSdkReference(JpsJavaSdkType.INSTANCE);