blob: d0af4f69c300f026b0f16497c5dcb7b0740141b4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.maven.classpath;
import java.awt.Image;
import org.netbeans.modules.maven.api.FileUtilities;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Icon;
import javax.swing.event.ChangeListener;
import org.apache.maven.model.Resource;
import org.apache.maven.project.MavenProject;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.annotations.common.StaticResource;
import org.netbeans.modules.maven.api.NbMavenProject;
import org.netbeans.api.java.project.JavaProjectConstants;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.api.queries.SharabilityQuery;
import org.netbeans.modules.maven.NbMavenProjectImpl;
import org.netbeans.modules.maven.spi.nodes.NodeUtils;
import org.netbeans.spi.project.SourceGroupModifierImplementation;
import org.netbeans.spi.project.support.GenericSources;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.ChangeSupport;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import static org.netbeans.modules.maven.classpath.Bundle.*;
import org.netbeans.modules.maven.spi.nodes.OtherSourcesExclude;
import org.netbeans.spi.project.ProjectServiceProvider;
import org.openide.util.NbBundle.Messages;
import org.openide.util.Utilities;
/**
* Implementation of Sources interface for maven projects.
* generic and java are necessary for proper workings of the project, the rest is custom thing..
* IMHO at least..
* @author Milos Kleint
*/
@ProjectServiceProvider(service={Sources.class, SourceGroupModifierImplementation.class, OtherSourcesExclude.class}, projectType="org-netbeans-modules-maven")
public class MavenSourcesImpl implements Sources, SourceGroupModifierImplementation, OtherSourcesExclude {
public static final String TYPE_OTHER = "Resources"; //NOI18N
public static final String TYPE_TEST_OTHER = "TestResources"; //NOI18N
public static final String TYPE_GEN_SOURCES = "GeneratedSources"; //NOI18N
public static final String NAME_PROJECTROOT = "ProjectRoot"; //NOI18N
public static final String NAME_XDOCS = "XDocs"; //NOI18N
public static final String NAME_SOURCE = "1SourceRoot"; //NOI18N
public static final String NAME_TESTSOURCE = "2TestSourceRoot"; //NOI18N
public static final String NAME_GENERATED_SOURCE = "6GeneratedSourceRoot"; //NOI18N
public static final String NAME_GENERATED_TEST_SOURCE = "7GeneratedSourceRoot"; //NOI18N
private static final @StaticResource String OTHERS_BADGE = "org/netbeans/modules/maven/others-badge.png";
private final Project proj;
private final ChangeSupport cs = new ChangeSupport(this);
private final PropertyChangeListener pcl = new PropertyChangeListener() {
public @Override void propertyChange(PropertyChangeEvent evt) {
if (proj.getLookup().lookup(NbMavenProject.class).isUnloadable()) {
return; //let's just continue with the old value, stripping classpath for broken project and re-creating it later serves no greater good.
}
//explicitly listing both RESOURCE and PROJECT properties, it's unclear if both are required but since some other places call addWatchedPath but don't listen it's likely required
if (NbMavenProject.PROP_RESOURCE.equals(evt.getPropertyName()) || NbMavenProject.PROP_PROJECT.equals(evt.getPropertyName())) {
checkChanges(true, true);
}
}
};
private final Map<String, SourceGroup> javaGroup;
private final Map<File, SourceGroup> genSrcGroup;
private final Map<File, OtherGroup> otherMainGroups;
private final Map<File, OtherGroup> otherTestGroups;
private final Object lock = new Object();
public MavenSourcesImpl(Project proj) {
this.proj = proj;
javaGroup = new TreeMap<String, SourceGroup>();
genSrcGroup = new TreeMap<File, SourceGroup>();
otherMainGroups = new TreeMap<File, OtherGroup>();
otherTestGroups = new TreeMap<File, OtherGroup>();
}
private NbMavenProjectImpl project() {
return proj.getLookup().lookup(NbMavenProjectImpl.class);
}
@Messages({
"SG_Sources=Source Packages",
"SG_Test_Sources=Test Packages"
})
private void checkChanges(boolean fireChanges, boolean checkAlsoNonJavaStuff) {
boolean changed = false;
synchronized (lock) {
NbMavenProjectImpl project = project();
MavenProject mp = project.getOriginalMavenProject();
NbMavenProject watcher = project.getProjectWatcher();
File folder = FileUtilities.convertStringToFile(mp.getBuild().getSourceDirectory());
changed = changed | checkSourceGroupCache(folder, NAME_SOURCE, SG_Sources(), javaGroup, watcher);
folder = FileUtilities.convertStringToFile(mp.getBuild().getTestSourceDirectory());
changed = changed | checkSourceGroupCache(folder, NAME_TESTSOURCE, SG_Test_Sources(), javaGroup, watcher);
changed = changed | checkGeneratedGroupsCache();
if (checkAlsoNonJavaStuff) {
changed = changed | checkOtherGroupsCache(project.getOtherRoots(false), false);
changed = changed | checkOtherGroupsCache(project.getOtherRoots(true), true);
}
}
if (changed) {
if (fireChanges) {
cs.fireChange();
}
}
}
public @Override void addChangeListener(ChangeListener changeListener) {
if (!cs.hasListeners()) {
NbMavenProject.addPropertyChangeListener(project(), pcl);
}
cs.addChangeListener(changeListener);
}
public @Override void removeChangeListener(ChangeListener changeListener) {
cs.removeChangeListener(changeListener);
if (!cs.hasListeners()) {
NbMavenProject.removePropertyChangeListener(project(), pcl);
}
}
public @Override SourceGroup[] getSourceGroups(String str) {
if (Sources.TYPE_GENERIC.equals(str)) {
return new SourceGroup[] { GenericSources.group(proj, proj.getProjectDirectory(), NAME_PROJECTROOT,
ProjectUtils.getInformation(proj).getDisplayName(), null, null) };
}
if (JavaProjectConstants.SOURCES_TYPE_JAVA.equals(str)) {
List<SourceGroup> toReturn = new ArrayList<SourceGroup>();
synchronized (lock) {
// don't fire event at all..
checkChanges(false, false);
toReturn.addAll(javaGroup.values());
}
SourceGroup[] grp = new SourceGroup[toReturn.size()];
grp = toReturn.toArray(grp);
return grp;
}
if (TYPE_GEN_SOURCES.equals(str)) {
List<SourceGroup> toReturn = new ArrayList<SourceGroup>();
synchronized (lock) {
checkGeneratedGroupsCache();
toReturn.addAll(genSrcGroup.values());
}
SourceGroup[] grp = new SourceGroup[toReturn.size()];
grp = toReturn.toArray(grp);
return grp;
}
if (TYPE_OTHER.equals(str) || TYPE_TEST_OTHER.equals(str)) {
// TODO not all these are probably resources.. maybe need to split in 2 groups..
boolean test = TYPE_TEST_OTHER.equals(str);
List<SourceGroup> toReturn = new ArrayList<SourceGroup>();
File[] roots = project().getOtherRoots(test);
synchronized (lock) {
// don't fire event synchronously..
checkOtherGroupsCache(roots, test);
if (test && !otherTestGroups.isEmpty()) {
toReturn.addAll(otherTestGroups.values());
} else if (!test && !otherMainGroups.isEmpty()) {
toReturn.addAll(otherMainGroups.values());
}
}
SourceGroup[] grp = new SourceGroup[toReturn.size()];
grp = toReturn.toArray(grp);
return grp;
}
if (JavaProjectConstants.SOURCES_TYPE_RESOURCES.equals(str)) {
return getOrCreateResourceSourceGroup(false, false);
}
// logger.warn("unknown source type=" + str);
return new SourceGroup[0];
}
@Messages("SG_Project_Resources=Project Resources")
private SourceGroup[] getOrCreateResourceSourceGroup(boolean test, boolean create) {
URI[] uris = project().getResources(test);
if (uris.length > 0) {
List<URI> virtuals = new ArrayList<URI>();
List<SourceGroup> existing = new ArrayList<SourceGroup>();
for (URI u : uris) {
FileObject fo = FileUtilities.convertURItoFileObject(u);
if (fo == null) {
virtuals.add(u);
} else if (fo.isFolder()) {
existing.add(GenericSources.group(proj, fo, "resources", //NOI18N
SG_Project_Resources(), null, null));
}
}
if (create && existing.isEmpty()) {
File root = Utilities.toFile(virtuals.get(0));
try {
FileObject fo = FileUtil.createFolder(root);
existing.add(GenericSources.group(proj, fo, "resources", //NOI18N
SG_Project_Resources(), null, null));
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
//TODO we should probably add includes/excludes to source groups.
return existing.toArray(new SourceGroup[0]);
} else {
//TODO add <Resources> element to pom??
}
return new SourceGroup[0];
}
/**
* consult the SourceGroup cache, return true if anything changed..
*/
private boolean checkSourceGroupCache(@NullAllowed File rootF, String name, String displayName, Map<String, SourceGroup> groups, NbMavenProject watcher) {
FileObject root;
if (rootF != null) {
watcher.addWatchedPath(Utilities.toURI(rootF));
root = FileUtil.toFileObject(rootF);
} else {
root = null;
}
SourceGroup group = groups.get(name);
if ((root == null || root.isData()) && group != null) {
groups.remove(name);
return true;
}
if (root == null || root.isData()) {
return false;
}
boolean changed = false;
if (group == null) {
group = GenericSources.group(proj, root, name, displayName, null, null);
groups.put(name, group);
changed = true;
} else {
if (!group.getRootFolder().equals(root)) {
group = GenericSources.group(proj, root, name, displayName, null, null);
groups.put(name, group);
changed = true;
}
}
return changed;
}
private boolean checkGeneratedGroupsCache() {
boolean changed = false;
List<File> checked = new ArrayList<File>();
for (boolean test : new boolean[] {false, true}) {
for (URI u : project().getGeneratedSourceRoots(test)) {
File file = FileUtil.normalizeFile(Utilities.toFile(u));
FileObject folder = FileUtil.toFileObject(file);
changed |= checkGeneratedGroupCache(folder, file, file.getName(), test);
checked.add(file);
}
}
Set<File> currs = new HashSet<File>();
currs.addAll(genSrcGroup.keySet());
for (File curr : currs) {
if (!checked.contains(curr)) {
genSrcGroup.remove(curr);
changed = true;
}
}
return changed;
}
/**
* consult the SourceGroup cache, return true if anything changed..
*/
@Messages({
"# {0} - name suffix", "SG_Generated_Sources=Generated Sources ({0})",
"# {0} - name suffix", "SG_Generated_Test_Sources=Generated Test Sources ({0})"
})
private boolean checkGeneratedGroupCache(FileObject root, File rootFile, String nameSuffix, boolean test) {
SourceGroup group = genSrcGroup.get(rootFile);
if ((root == null || root.isData()) && group != null) {
genSrcGroup.remove(rootFile);
return true;
}
if (root == null || root.isData()) {
return false;
}
boolean changed = false;
String name = (test ? NAME_GENERATED_TEST_SOURCE : NAME_GENERATED_SOURCE) + nameSuffix;
String displayName = test ? SG_Generated_Test_Sources(nameSuffix) : SG_Generated_Sources(nameSuffix);
if (group == null) {
group = new GeneratedGroup(project(), root, name, displayName);
genSrcGroup.put(rootFile, group);
changed = true;
} else {
if (!group.getRootFolder().isValid() || !group.getRootFolder().equals(root)) {
group = new GeneratedGroup(project(), root, name, displayName);
genSrcGroup.put(rootFile, group);
changed = true;
}
}
return changed;
}
private boolean checkOtherGroupsCache(File[] roots, boolean test) {
boolean ch = false;
Set<File> toRemove = new HashSet<File>(test ? otherTestGroups.keySet() : otherMainGroups.keySet());
toRemove.removeAll(Arrays.asList(roots));
URI[] res = project().getResources(test);
Set<File> resources = new HashSet<File>();
for (URI ur : res) {
resources.add(Utilities.toFile(ur));
}
for (File f : roots) {
ch = ch | checkOtherGroup(f, resources, test);
}
for (File f : toRemove) {
//now this shall remove the nonexisting ones and even mark the change..
ch = ch | checkOtherGroup(f, resources, test);
}
return ch;
}
private boolean checkOtherGroup(File rootFile, Set<File> resourceRoots, boolean test) {
FileObject root = FileUtil.toFileObject(rootFile);
if (root != null && !root.isFolder()) {
root = null;
}
Map<File, OtherGroup> map = test ? otherTestGroups : otherMainGroups;
OtherGroup grp = map.get(rootFile);
boolean isResourceNow = resourceRoots.contains(rootFile);
boolean wasResourceBefore = grp != null && grp.getResource() != null;
if ((root == null && grp != null) || (root != null && grp != null && wasResourceBefore && !isResourceNow)) {
map.remove(rootFile);
return true;
}
if (root == null) {
return false;
}
boolean changed = false;
if (grp == null || !grp.getRootFolder().isValid() || !grp.getRootFolder().equals(root) ||
isResourceNow != wasResourceBefore) {
grp = new OtherGroup(project(), root, "Resource" + (test ? "Test":"Main") + root.getNameExt(), root.getName(), test); //NOI18N
map.put(rootFile, grp);
changed = true;
}
return changed;
}
public @Override SourceGroup createSourceGroup(String type, String hint) {
assert type != null;
MavenProject mp = project().getOriginalMavenProject();
File folder = null;
if (JavaProjectConstants.SOURCES_TYPE_RESOURCES.equals(type)) {
boolean main = JavaProjectConstants.SOURCES_HINT_MAIN.equals(hint);
SourceGroup[] grps = getOrCreateResourceSourceGroup(!main, true);
if (grps.length > 0) {
return grps[0];
}
return null;
}
if (JavaProjectConstants.SOURCES_TYPE_JAVA.equals(type)) {
if (JavaProjectConstants.SOURCES_HINT_MAIN.equals(hint)) {
folder = FileUtilities.convertStringToFile(mp.getBuild().getSourceDirectory());
}
if (JavaProjectConstants.SOURCES_HINT_TEST.equals(hint)) {
folder = FileUtilities.convertStringToFile(mp.getBuild().getTestSourceDirectory());
}
}
if (folder != null) {
FileObject fo;
try {
fo = FileUtil.createFolder(folder);
} catch (IOException x) { // XXX not allowed to rethrow
Logger.getLogger(MavenSourcesImpl.class.getName()).log(Level.INFO, null, x);
return null;
}
SourceGroup[] grps = getSourceGroups(type);
for (SourceGroup sg : grps) {
if (fo.equals(sg.getRootFolder())) {
return sg;
}
}
//shall we somehow report it?
}
return null;
}
public @Override boolean canCreateSourceGroup(String type, String hint) {
return (JavaProjectConstants.SOURCES_TYPE_RESOURCES.equals(type) ||
JavaProjectConstants.SOURCES_TYPE_JAVA.equals(type))
&& (JavaProjectConstants.SOURCES_HINT_MAIN.equals(hint) ||
JavaProjectConstants.SOURCES_HINT_TEST.equals(hint));
}
@Override
public Set<Path> excludedFolders() {
Set<Path> result = new HashSet<>();
FileObject mainDir = project().getProjectDirectory().getFileObject("src/main/java");
FileObject testDir = project().getProjectDirectory().getFileObject("src/test/java");
if (mainDir != null) {
result.add(FileUtil.toFile(mainDir).toPath());
}
if (testDir != null) {
result.add(FileUtil.toFile(testDir).toPath());
}
return result;
}
public static final class OtherGroup implements SourceGroup {
private final FileObject rootFolder;
private final File rootFile;
private final String name;
private final String displayName;
private final Icon icon;
private final Icon openedIcon;
private final NbMavenProjectImpl project;
private final Resource resource;
private final PropertyChangeSupport support = new PropertyChangeSupport(this);
@Messages("SG_Root_not_defined=<Root not defined>")
OtherGroup(NbMavenProjectImpl p, FileObject rootFold, String nm, String displayNm, boolean test) {
project = p;
rootFolder = rootFold;
rootFile = FileUtil.toFile(rootFolder);
resource = checkResource(rootFold,
test ? project.getOriginalMavenProject().getTestResources() :
project.getOriginalMavenProject().getResources());
if (resource != null) {
Image badge = ImageUtilities.loadImage(OTHERS_BADGE, true); //NOI18N
// ImageUtilities.addToolTipToImage(badge, "Resource root as defined in POM.");
icon = ImageUtilities.image2Icon(ImageUtilities.mergeImages(NodeUtils.getTreeFolderIcon(false), badge, 8, 8));
openedIcon = ImageUtilities.image2Icon(ImageUtilities.mergeImages(NodeUtils.getTreeFolderIcon(true), badge, 8, 8));
name = FileUtilities.relativizeFile(FileUtil.toFile(project.getProjectDirectory()), FileUtilities.convertStringToFile(resource.getDirectory()));
displayName = name;
} else {
icon = ImageUtilities.image2Icon(NodeUtils.getTreeFolderIcon(false));
openedIcon = ImageUtilities.image2Icon(NodeUtils.getTreeFolderIcon(true));
name = nm;
displayName = displayNm != null ? displayNm : SG_Root_not_defined();
}
}
public @Override FileObject getRootFolder() {
return rootFolder;
}
public File getRootFolderFile() {
return rootFile;
}
public Resource getResource() {
return resource;
}
public @Override String getName() {
return name;
}
public @Override String getDisplayName() {
if (resource != null && resource.getTargetPath() != null) {
return displayName + " -> " + resource.getTargetPath();
}
return displayName;
}
public @Override Icon getIcon(boolean opened) {
return opened ? icon : openedIcon;
}
public @Override boolean contains(FileObject file) {
if (file != rootFolder && !FileUtil.isParentOf(rootFolder, file)) {
return false;
}
if (project != null) {
if (file.isFolder() && file != project.getProjectDirectory() && ProjectManager.getDefault().isProject(file)) {
// #67450: avoid actually loading the nested project.
return false;
}
if (FileOwnerQuery.getOwner(file) != project) {
return false;
}
}
File f = FileUtil.toFile(file);
if (f != null) {
// MIXED, UNKNOWN, and SHARABLE -> include it
return SharabilityQuery.getSharability(f) != SharabilityQuery.NOT_SHARABLE;
} else {
// Not on disk, include it.
return true;
}
}
public @Override void addPropertyChangeListener(PropertyChangeListener l) {
support.addPropertyChangeListener(l);
}
public @Override void removePropertyChangeListener(PropertyChangeListener l) {
support.removePropertyChangeListener(l);
}
private Resource checkResource(FileObject rootFold, List<Resource> list) {
for (Resource elem : list) {
String dir = elem.getDirectory();
if (dir == null) { // #203635
continue;
}
URI uri = FileUtilities.getDirURI(project.getProjectDirectory(), dir);
FileObject fo = FileUtilities.convertURItoFileObject(uri);
if (fo != null && fo.equals(rootFold)) {
return elem;
}
}
return null;
}
}
/**
* MEVENIDE-536 - cannot use default implementation of SourceGroup because it
* won't include non-shareable folders..
*/
public static final class GeneratedGroup implements SourceGroup {
private final FileObject rootFolder;
private final String name;
private final String displayName;
private final Icon icon = null;
private final Icon openedIcon = null;
private NbMavenProjectImpl project;
GeneratedGroup(NbMavenProjectImpl p, FileObject rootFold, String nm, String displayNm/*,
Icon icn, Icon opened*/) {
project = p;
rootFolder = rootFold;
name = nm;
displayName = displayNm != null ? displayNm : SG_Root_not_defined();
// icon = icn;
// openedIcon = opened;
}
public @Override FileObject getRootFolder() {
return rootFolder;
}
public @Override String getName() {
return name;
}
public @Override String getDisplayName() {
return displayName;
}
public @Override Icon getIcon(boolean opened) {
return opened ? icon : openedIcon;
}
public @Override boolean contains(FileObject file) {
if (file != rootFolder && !FileUtil.isParentOf(rootFolder, file)) {
return false;
}
if (project != null) {
if (file.isFolder() && file != project.getProjectDirectory() && ProjectManager.getDefault().isProject(file)) {
// #67450: avoid actually loading the nested project.
return false;
}
if (FileOwnerQuery.getOwner(file) != project) {
return false;
}
}
return true;
}
public @Override void addPropertyChangeListener(PropertyChangeListener l) {
// XXX should react to ProjectInformation changes
}
public @Override void removePropertyChangeListener(PropertyChangeListener l) {
// XXX
}
}
}