blob: a727e334d5c0b737da1cca2b7b4190cdaa97f0bb [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;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.swing.SwingUtilities;
import org.apache.maven.DefaultMaven;
import org.apache.maven.Maven;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
import org.apache.maven.cli.MavenCli;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenExecutionResult;
import org.apache.maven.model.Model;
import org.apache.maven.model.Resource;
import org.apache.maven.model.building.ModelBuildingException;
import org.apache.maven.model.building.ModelBuildingResult;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.java.project.classpath.ProjectClassPathModifier;
import org.netbeans.api.project.Project;
import org.netbeans.api.queries.VisibilityQuery;
import org.netbeans.modules.maven.api.Constants;
import org.netbeans.modules.maven.api.FileUtilities;
import org.netbeans.modules.maven.api.NbMavenProject;
import org.netbeans.modules.maven.api.PluginPropertyUtils;
import org.netbeans.modules.maven.api.execute.ActiveJ2SEPlatformProvider;
import org.netbeans.modules.maven.configurations.M2ConfigProvider;
import org.netbeans.modules.maven.configurations.M2Configuration;
import org.netbeans.modules.maven.configurations.ProjectProfileHandlerImpl;
import org.netbeans.modules.maven.cos.CopyResourcesOnSave;
import org.netbeans.modules.maven.debug.MavenJPDAStart;
import org.netbeans.modules.maven.embedder.EmbedderFactory;
import org.netbeans.modules.maven.embedder.MavenEmbedder;
import org.netbeans.modules.maven.modelcache.MavenProjectCache;
import org.netbeans.modules.maven.options.MavenSettings;
import org.netbeans.modules.maven.problems.ProblemReporterImpl;
import org.netbeans.modules.maven.queries.PomCompilerOptionsQueryImpl;
import org.netbeans.modules.maven.queries.UnitTestsCompilerOptionsQueryImpl;
import org.netbeans.modules.maven.spi.nodes.OtherSourcesExclude;
import org.netbeans.modules.maven.spi.queries.JavaLikeRootProvider;
import org.netbeans.spi.java.project.support.LookupMergerSupport;
import org.netbeans.spi.project.ProjectState;
import org.netbeans.spi.project.support.LookupProviderSupport;
import org.netbeans.spi.project.ui.support.UILookupMergerSupport;
import org.openide.filesystems.FileAttributeEvent;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileRenameEvent;
import org.openide.filesystems.FileUtil;
import org.openide.util.Lookup;
import org.openide.util.NbBundle.Messages;
import org.openide.util.NbCollections;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
import org.openide.util.lookup.Lookups;
import org.openide.util.lookup.ProxyLookup;
/**
* A Maven-based project.
*/
public final class NbMavenProjectImpl implements Project {
private static final Logger LOG = Logger.getLogger(NbMavenProjectImpl.class.getName());
//sequential execution might be necesary for #166919
public static final RequestProcessor RELOAD_RP = new RequestProcessor("Maven project reloading", 1); //NOI18
//minor optimization. In case the queue already holds the task and is not run, delay, if running reschedule.
private final RequestProcessor.Task reloadTask = RELOAD_RP.create(new Runnable() {
@Override
public void run() {
problemReporter.clearReports(); //#167741 -this will trigger node refresh?
MavenProject prj = loadOriginalMavenProject(true);
synchronized (NbMavenProjectImpl.this) {
MavenProject old = project == null ? null : project.get();
if (old != null && MavenProjectCache.isFallbackproject(prj)) {
prj.setPackaging(old.getPackaging()); //#229366 preserve packaging for broken projects to avoid changing lookup.
}
project = new SoftReference<MavenProject>(prj);
if (hardReferencingMavenProject) {
hardRefProject = prj;
}
}
ACCESSOR.doFireReload(watcher);
}
});
private final FileObject fileObject;
private final FileObject folderFileObject;
private final File projectFile;
private final Lookup basicLookup;
private final Lookup completeLookup;
private final Lookup lookup;
private final Updater openedProjectUpdater;
private Reference<MavenProject> project;
private boolean hardReferencingMavenProject = false; //only should be true when project is open.
private MavenProject hardRefProject;
private ProblemReporterImpl problemReporter;
private final @NonNull NbMavenProject watcher;
private final M2ConfigProvider configProvider;
private final @NonNull MavenProjectPropsImpl auxprops;
private ProjectProfileHandlerImpl profileHandler;
private CopyResourcesOnSave copyResourcesOnSave;
private final Object COPYRESOURCES_LOCK = new Object();
@org.netbeans.api.annotations.common.SuppressWarnings("MS_SHOULD_BE_FINAL")
public static WatcherAccessor ACCESSOR = null;
static {
// invokes static initializer of ModelHandle.class
// that will assign value to the ACCESSOR field above
Class<?> c = NbMavenProject.class;
try {
Class.forName(c.getName(), true, c.getClassLoader());
} catch (Exception ex) {
LOG.log(Level.SEVERE, "very wrong, very wrong, yes indeed", ex);
}
}
//#224012
private ProjectOpenedHookImpl hookImpl;
private Exception ex;
private final Object LOCK_224012 = new Object();
boolean setIssue224012(ProjectOpenedHookImpl hook, Exception exception) {
synchronized (LOCK_224012) {
if (hookImpl == null) {
hookImpl = hook;
ex = exception;
return true;
} else {
LOG.log(Level.INFO, " first creation stacktrace", ex);
LOG.log(Level.INFO, " second creation stacktrace", exception);
LOG.log(Level.WARNING, "Spotted issue 224012 (https://netbeans.org/bugzilla/show_bug.cgi?id=224012). Please report the incident wth IDE log attached.");
return false;
}
}
}
private final Object MODEL_LOCK = new Object();
private Model model;
public Model getRawModel() throws ModelBuildingException {
synchronized(MODEL_LOCK) {
if(model == null) {
MavenEmbedder projectEmbedder = EmbedderFactory.getProjectEmbedder();
ModelBuildingResult br = projectEmbedder.executeModelBuilder(getPOMFile());
model = br.getRawModel();
}
return model;
}
}
public static abstract class WatcherAccessor {
public abstract NbMavenProject createWatcher(NbMavenProjectImpl proj);
public abstract void doFireReload(NbMavenProject watcher);
}
/**
* Creates a new instance of MavenProject, should never be called by user code.
* but only by MavenProjectFactory!!!
*/
NbMavenProjectImpl(FileObject folder, FileObject projectFO, ProjectState projectState) {
this.projectFile = FileUtil.normalizeFile(FileUtil.toFile(projectFO));
fileObject = projectFO;
folderFileObject = folder;
lookup = Lookups.proxy(new Lookup.Provider() {
@Override
public Lookup getLookup() {
if (completeLookup == null) {
//not fully initialized constructor
LOG.log(Level.FINE, "accessing project's lookup before the instance is fully initialized at " + projectFile, new Exception());
assert basicLookup != null;
return basicLookup;
} else {
return completeLookup;
}
}
});
watcher = ACCESSOR.createWatcher(this);
openedProjectUpdater = new Updater(new FileProvider() {
@Override
public File[] getFiles() {
File homeFile = FileUtil.normalizeFile(MavenCli.USER_MAVEN_CONFIGURATION_HOME);
return new File[] {
new File(projectFile.getParentFile(), "nb-configuration.xml"), //NOI18N
projectFile,
new File(new File(projectFile.getParentFile(), ".mvn"), "maven.config"), //NOI18N
new File(homeFile, "settings.xml"), //NOI18N
};
}
});
problemReporter = new ProblemReporterImpl(this);
M2AuxilaryConfigImpl auxiliary = new M2AuxilaryConfigImpl(folder, true);
auxprops = new MavenProjectPropsImpl(auxiliary, this);
profileHandler = new ProjectProfileHandlerImpl(this, auxiliary);
configProvider = new M2ConfigProvider(this, auxiliary, profileHandler);
// @PSP's and the like, and PackagingProvider impls, may check project lookup for e.g. NbMavenProject, so init lookup in two stages:
basicLookup = createBasicLookup(projectState, auxiliary);
//here we always load the MavenProject instance because we need to touch the packaging from pom.
completeLookup = LookupProviderSupport.createCompositeLookup(basicLookup, new PackagingTypeDependentLookup(watcher));
}
public File getPOMFile() {
return projectFile;
}
public @NonNull NbMavenProject getProjectWatcher() {
return watcher;
}
public ProblemReporterImpl getProblemReporter() {
return problemReporter;
}
public String getHintJavaPlatform() {
String hint = getAuxProps().get(Constants.HINT_JDK_PLATFORM, true);
if (hint == null) {
hint = MavenSettings.getDefault().getDefaultJdk();
}
return hint == null || hint.isEmpty() ? null : hint;
}
/**
* load a project with properties and profiles other than the current ones.
* @param embedder embedder to use
* @param activeProfiles
* @param properties
* @return
*/
//TODO revisit usage, eventually should be only reuse MavenProjectCache
public @NonNull MavenProject loadMavenProject(MavenEmbedder embedder, List<String> activeProfiles, Properties properties) {
try {
MavenExecutionRequest req = embedder.createMavenExecutionRequest();
req.addActiveProfiles(activeProfiles);
req.setPom(projectFile);
req.setNoSnapshotUpdates(true);
req.setUpdateSnapshots(false);
//#238800 important to merge, not replace
if (properties != null) {
Properties uprops = req.getUserProperties();
uprops.putAll(properties);
req.setUserProperties(uprops);
}
//MEVENIDE-634 i'm wondering if this fixes the issue
req.setInteractiveMode(false);
req.setOffline(true);
// recursive == false is important to avoid checking all submodules for extensions
// that will not be used in current pom anyway..
// #135070
req.setRecursive(false);
MavenExecutionResult res = embedder.readProjectWithDependencies(req, true);
//#215159 clear the project building request, it references multiple Maven Models via the RepositorySession cache
//is not used in maven itself, most likely used by m2e only..
if (!res.hasExceptions()) {
res.getProject().setProjectBuildingRequest(null);
return res.getProject();
} else {
List<Throwable> exc = res.getExceptions();
for (Throwable ex : exc) {
LOG.log(Level.FINE, "Exception thrown while loading maven project at " + getProjectDirectory(), ex); //NOI18N
}
}
} catch (RuntimeException exc) {
//guard against exceptions that are not processed by the embedder
//#136184 NumberFormatException
LOG.log(Level.INFO, "Runtime exception thrown while loading maven project at " + getProjectDirectory(), exc); //NOI18N
}
return MavenProjectCache.getFallbackProject(this.getPOMFile());
}
/**
* replacement for MavenProject.getParent() which has bad long term memory behaviour. We offset it by recalculating/reparsing everything
* therefore should not be used lightly!
* pass a MavenProject instance and current configuration and other settings will be applied when loading the parent.
* @param project
* @return null or the parent mavenproject
*/
public MavenProject loadParentOf(MavenEmbedder embedder, MavenProject project) throws ProjectBuildingException {
MavenProject parent = null;
ProjectBuilder builder = embedder.lookupComponent(ProjectBuilder.class);
MavenExecutionRequest req = embedder.createMavenExecutionRequest();
M2Configuration active = configProvider.getActiveConfiguration();
req.addActiveProfiles(active.getActivatedProfiles());
req.setNoSnapshotUpdates(true);
req.setUpdateSnapshots(false);
req.setInteractiveMode(false);
req.setRecursive(false);
req.setOffline(true);
//#238800 important to merge, not replace
Properties uprops = req.getUserProperties();
uprops.putAll(MavenProjectCache.createUserPropsForProjectLoading(active.getProperties()));
req.setUserProperties(uprops);
ProjectBuildingRequest request = req.getProjectBuildingRequest();
request.setRemoteRepositories(project.getRemoteArtifactRepositories());
DefaultMaven maven = (DefaultMaven) embedder.lookupComponent(Maven.class);
request.setRepositorySession(maven.newRepositorySession(req));
if (project.getParentFile() != null) {
parent = builder.build(project.getParentFile(), request).getProject();
} else if (project.getModel().getParent() != null) {
parent = builder.build(project.getParentArtifact(), request).getProject();
}
//clear the project building request, it references multiple Maven Models via the RepositorySession cache
//is not used in maven itself, most likely used by m2e only..
if (parent != null) {
parent.setProjectBuildingRequest(null);
}
MavenEmbedder.normalizePaths(parent);
return parent;
}
public List<String> getCurrentActiveProfiles() {
List<String> toRet = new ArrayList<String>();
toRet.addAll(configProvider.getActiveConfiguration().getActivatedProfiles());
return toRet;
}
//#172952 for property expression resolution we need this to include
// the properties of the platform to properly resolve stuff like com.sun.boot.class.path
public Map<? extends String,? extends String> createSystemPropsForPropertyExpressions() {
Map<String,String> props = NbCollections.checkedMapByCopy(EmbedderFactory.getProjectEmbedder().getSystemProperties(), String.class, String.class, true);
ActiveJ2SEPlatformProvider platformProvider = getLookup().lookup(ActiveJ2SEPlatformProvider.class);
if (platformProvider != null) { // may be null inside PackagingProvider
props.putAll(platformProvider.getJavaPlatform().getSystemProperties());
}
return props;
}
public Map<? extends String,? extends String> createUserPropsForPropertyExpressions() {
return NbCollections.checkedMapByCopy(configProvider.getActiveConfiguration().getProperties(), String.class, String.class, true);
}
/**
* getter for the maven's own project representation.. this instance is cached but gets reloaded
* when one the pom files have changed.
*/
public @NonNull MavenProject getOriginalMavenProject() {
MavenProject mp;
synchronized (this) {
mp = project == null ? null : project.get();
if (mp != null) {
return mp;
}
if (mp == null) {
// PENDING: should be the whole project load synchronized ?
mp = loadOriginalMavenProject(false);
project = new SoftReference<MavenProject>(mp);
if (hardReferencingMavenProject) {
hardRefProject = mp;
}
}
}
// in case someone got already information from the NbMavenProject:
ACCESSOR.doFireReload(watcher);
return mp;
}
/**
* a marginally unreliable, non blocking method for figuring if the model is loaded or not.
* @return
*/
public boolean isMavenProjectLoaded() {
Reference<MavenProject> prj = project;
if (prj != null) {
return prj.get() != null;
}
return false;
}
/**
* open projects should always hard reference the Mavenproject instance to prevent it from
* being GCed, the instance will get reloaded almost instantly anyway
*/
void startHardReferencingMavenPoject() {
synchronized (this) {
hardReferencingMavenProject = true;
MavenProject mp = project == null ? null : project.get();
hardRefProject = mp;
}
}
/**
* open projects should always hard reference the Mavenproject instance to prevent it from
* being GCed, the instance will get reloaded almost instantly anyway
*/
void stopHardReferencingMavenPoject() {
synchronized (this) {
hardReferencingMavenProject = false;
hardRefProject = null;
}
}
@Messages({
"TXT_RuntimeException=RuntimeException occurred in Apache Maven embedder while loading",
"TXT_RuntimeExceptionLong=RuntimeException occurred in Apache Maven embedder while loading the project. \n"
+ "This is preventing the project model from loading properly. \n"
+ "Please file a bug report with details about your project and the IDE's log file.\n\n"
})
private @NonNull MavenProject loadOriginalMavenProject(boolean reload) {
MavenProject newproject;
try {
synchronized(MODEL_LOCK) {
model = null;
}
newproject = MavenProjectCache.getMavenProject(this.getPOMFile(), reload);
if (newproject == null) { //null when no pom.xml in project folder..
newproject = MavenProjectCache.getFallbackProject(projectFile);
}
final MavenExecutionResult res = MavenProjectCache.getExecutionResult(newproject);
final MavenProject np = newproject;
} finally {
if (LOG.isLoggable(Level.FINE) && SwingUtilities.isEventDispatchThread()) {
LOG.log(Level.FINE, "Project " + getProjectDirectory().getPath() + " loaded in AWT event dispatching thread!", new RuntimeException());
}
}
assert newproject != null;
return newproject;
}
public RequestProcessor.Task fireProjectReload() {
//#227101 not only AWT and project read/write mutex has to be checked, there are some additional more
//complex scenarios that can lead to deadlock. Just give up and always fire changes in separate RP.
if (Boolean.getBoolean("test.reload.sync")) {
reloadTask.run();
//for tests just do sync reload, even though silly, even sillier is to attempt to sync the threads..
} else {
reloadTask.schedule(0); //asuming here that schedule(0) will move the scheduled task in the queue if not yet executed
}
return reloadTask;
}
public static void refreshLocalRepository(NbMavenProjectImpl project) {
File file = project.getEmbedder().getLocalRepositoryFile();
FileUtil.refreshFor(file);
}
/** Begin listening to pom.xml changes. */
void attachUpdater() {
openedProjectUpdater.attachAll();
}
void detachUpdater() {
openedProjectUpdater.detachAll();
}
/**
* The root directory of the project where the POM resides.
*/
@Override
public FileObject getProjectDirectory() {
return folderFileObject;
}
public @CheckForNull String getArtifactRelativeRepositoryPath() {
Artifact artifact = getOriginalMavenProject().getArtifact();
if (artifact == null) {
return null;
}
return getArtifactRelativeRepositoryPath(artifact);
}
/**
* path of test artifact in local repository
* @return
*/
public @CheckForNull String getTestArtifactRelativeRepositoryPath() {
Artifact main = getOriginalMavenProject().getArtifact();
if (main == null) {
return null;
}
ArtifactHandlerManager artifactHandlerManager = getEmbedder().lookupComponent(ArtifactHandlerManager.class);
assert artifactHandlerManager != null : "ArtifactHandlerManager component not found in maven";
Artifact test = new DefaultArtifact(main.getGroupId(), main.getArtifactId(), main.getVersionRange(),
Artifact.SCOPE_TEST, "test-jar", "tests", artifactHandlerManager.getArtifactHandler("test-jar"));
return getArtifactRelativeRepositoryPath(test);
}
public String getArtifactRelativeRepositoryPath(@NonNull Artifact artifact) {
return getEmbedder().getLocalRepository().pathOf(artifact);
}
public MavenEmbedder getEmbedder() {
return EmbedderFactory.getProjectEmbedder();
}
public @NonNull MavenProjectPropsImpl getAuxProps() {
return auxprops;
}
public URI[] getSourceRoots(boolean test) {
List<URI> uris = new ArrayList<URI>();
for (String root : test ? getOriginalMavenProject().getTestCompileSourceRoots() : getOriginalMavenProject().getCompileSourceRoots()) {
uris.add(FileUtilities.convertStringToUri(root));
}
for (JavaLikeRootProvider rp : getLookup().lookupAll(JavaLikeRootProvider.class)) {
// XXX for a few purposes (listening) it is desirable to list these even before they exist, but usually it is just noise (cf. #196414 comment #2)
FileObject root = getProjectDirectory().getFileObject("src/" + (test ? "test" : "main") + "/" + rp.kind());
if (root != null && root.isFolder()) {
uris.add(root.toURI());
}
}
return uris.toArray(new URI[uris.size()]);
}
public URI[] getGeneratedSourceRoots(boolean test) {
//#241874 calculate the test source roots up front just in case they are in target/generated-sources. if so, remove the from non-test generated source roots
Set<URI> BHTestUris = new HashSet<URI>();
String[] buildHelpers = PluginPropertyUtils.getPluginPropertyList(this,
"org.codehaus.mojo", //NOI18N
"build-helper-maven-plugin", "sources", "source", "add-test-source"); //NOI18N
if (buildHelpers != null && buildHelpers.length > 0) {
File root = FileUtil.toFile(getProjectDirectory());
for (String helper : buildHelpers) {
BHTestUris.add(FileUtilities.getDirURI(root, helper));
}
}
URI uri = FileUtilities.getDirURI(getProjectDirectory(), test ? "target/generated-test-sources" : "target/generated-sources"); //NOI18N
Set<URI> uris = new HashSet<URI>();
File[] roots = Utilities.toFile(uri).listFiles();
if (roots != null) {
for (File root : roots) {
if (!VisibilityQuery.getDefault().isVisible(root)) { //#214002
continue;
}
if (!test && root.getName().startsWith("test-")) {
continue;
}
File[] kids = root.listFiles();
URI u = Utilities.toURI(root);
if (!test && BHTestUris.contains(u)) {
continue; //a test source root was put in target/generated-sources - #241874
}
if (kids != null && /* #190626 */kids.length > 0) {
uris.add(u);
} else {
watcher.addWatchedPath(u); //TODO who reacts to this?
}
}
}
if (test) { // MCOMPILER-167
roots = Utilities.toFile(FileUtilities.getDirURI(getProjectDirectory(), "target/generated-sources")).listFiles();
if (roots != null) {
for (File root : roots) {
if (!VisibilityQuery.getDefault().isVisible(root)) { //#214002
continue;
}
if (root.getName().startsWith("test-")) {
File[] kids = root.listFiles();
if (kids != null && kids.length > 0) {
uris.add(Utilities.toURI(root));
} else {
watcher.addWatchedPath(Utilities.toURI(root)); //TODO who reacts to this?
}
}
}
}
}
if (!test) {
buildHelpers = PluginPropertyUtils.getPluginPropertyList(this,
"org.codehaus.mojo", //NOI18N
"build-helper-maven-plugin", "sources", "source", "add-source"); //NOI18N
if (buildHelpers != null && buildHelpers.length > 0) {
File root = FileUtil.toFile(getProjectDirectory());
for (String helper : buildHelpers) {
uris.add(FileUtilities.getDirURI(root, helper));
}
}
} else {
uris.addAll(BHTestUris);
}
return uris.toArray(new URI[uris.size()]);
}
public URI getWebAppDirectory() {
//TODO hack, should be supported somehow to read this..
String prop = PluginPropertyUtils.getPluginProperty(this, Constants.GROUP_APACHE_PLUGINS,
Constants.PLUGIN_WAR, //NOI18N
"warSourceDirectory", //NOI18N
"war", null); //NOI18N
prop = prop == null ? "src/main/webapp" : prop; //NOI18N
return FileUtilities.getDirURI(getProjectDirectory(), prop);
}
public URI getSiteDirectory() {
//TODO hack, should be supported somehow to read this..
String prop = PluginPropertyUtils.getPluginProperty(this, Constants.GROUP_APACHE_PLUGINS,
Constants.PLUGIN_SITE, //NOI18N
"siteDirectory", //NOI18N
"site", null); //NOI18N
prop = prop == null ? "src/site" : prop; //NOI18N
return FileUtilities.getDirURI(getProjectDirectory(), prop);
}
public URI getEarAppDirectory() {
//TODO hack, should be supported somehow to read this..
String prop = PluginPropertyUtils.getPluginProperty(this, Constants.GROUP_APACHE_PLUGINS,
Constants.PLUGIN_EAR, //NOI18N
"earSourceDirectory", //NOI18N
"ear", null); //NOI18N
prop = prop == null ? "src/main/application" : prop; //NOI18N
return FileUtilities.getDirURI(getProjectDirectory(), prop);
}
public URI[] getResources(boolean test) {
List<URI> toRet = new ArrayList<URI>();
URI projectroot = getProjectDirectory().toURI();
Set<URI> sourceRoots = null;
List<Resource> res = test ? getOriginalMavenProject().getTestResources() : getOriginalMavenProject().getResources();
LBL : for (Resource elem : res) {
String dir = elem.getDirectory();
if (dir == null) {
continue; // #191742
}
URI uri = FileUtilities.getDirURI(getProjectDirectory(), dir);
if (elem.getTargetPath() != null || !elem.getExcludes().isEmpty() || !elem.getIncludes().isEmpty()) {
URI rel = projectroot.relativize(uri);
if (rel.isAbsolute()) { //outside of project directory
continue;// #195928, #231517
}
if (sourceRoots == null) {
sourceRoots = new HashSet<URI>();
sourceRoots.addAll(Arrays.asList(getSourceRoots(true)));
sourceRoots.addAll(Arrays.asList(getSourceRoots(false)));
//should we also consider generated sources? most like not necessary
}
for (URI sr : sourceRoots) {
if (!uri.relativize(sr).isAbsolute()) {
continue LBL;// #195928, #231517
}
}
//hope for the best now
}
// if (new File(uri).exists()) {
toRet.add(uri);
// }
}
return toRet.toArray(new URI[toRet.size()]);
}
public File[] getOtherRoots(boolean test) {
URI uri = FileUtilities.getDirURI(getProjectDirectory(), test ? "src/test" : "src/main"); //NOI18N
Set<File> toRet = new HashSet<File>();
File fil = Utilities.toFile(uri);
if (fil.exists()) {
try {
Path sourceRoot = fil.toPath();
OtherRootsVisitor visitor = new OtherRootsVisitor(getLookup(), sourceRoot);
Files.walkFileTree(sourceRoot, visitor);
toRet.addAll(visitor.getOtherRoots());
} catch (IOException ex) {
// log as info to keep trace about possible problems,
// but lets not be too agressive with level and notification
// see also issue #251071
LOG.log(Level.INFO, null, ex);
}
}
URI[] res = getResources(test);
for (URI rs : res) {
File fl = Utilities.toFile(rs);
//in node view we need only the existing ones, if anything else needs all,
// a new method is probably necessary..
if (fl.exists()) {
toRet.add(fl);
}
}
return toRet.toArray(new File[0]);
}
private static class OtherRootsVisitor extends SimpleFileVisitor<Path> {
private final Lookup lookup;
private final List<Path> otherRoots;
private final Path sourceRoot;
public OtherRootsVisitor(Lookup lookup, Path sourceRoot) {
this.lookup = lookup;
this.sourceRoot = sourceRoot;
this.otherRoots = new ArrayList<>();
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
// To avoid including src/main and src/test directories
if (sourceRoot.equals(dir)) {
return FileVisitResult.CONTINUE;
}
if(!VisibilityQuery.getDefault().isVisible(dir.toFile())) {
return FileVisitResult.SKIP_SUBTREE;
}
for (OtherSourcesExclude rp : lookup.lookupAll(OtherSourcesExclude.class)) {
for (Path folder : rp.excludedFolders()) {
// In case of excluded folders (e.g. src/main/java) we can simply skip whole subtree
if (folder.equals(dir)) {
return FileVisitResult.SKIP_SUBTREE;
}
// We are vising directory which is a parent of one of excluded directory.
// In such case we don't want to skip the subtree (b/c it might contain directories
// that falling under other roots), but we also don't want to add it to the results
// (to avoid adding everything)
if (folder.startsWith(dir)) {
return FileVisitResult.CONTINUE;
}
}
}
for (JavaLikeRootProvider rp : lookup.lookupAll(JavaLikeRootProvider.class)) {
if (rp.kind().equalsIgnoreCase(dir.getFileName().toString())) {
return FileVisitResult.SKIP_SUBTREE;
}
}
// If the directory wasn't excluded until now, it should be shown in Other Sources node
if (isOtherRoot(dir)) {
otherRoots.add(dir);
}
return FileVisitResult.CONTINUE;
}
private boolean isOtherRoot(Path dir) throws IOException {
if (!dir.toFile().isDirectory() || Files.isHidden(dir)) {
return false;
}
// Walk through the other roots and check if a parent of this dir is
// already available in other roots to avoid folder duplication
for (Path path : otherRoots) {
if (dir.startsWith(path)) {
return false;
}
}
return true;
}
public List<File> getOtherRoots() {
List<File> result = new ArrayList<>();
for (Path path : otherRoots) {
result.add(path.toFile());
}
return Collections.unmodifiableList(result);
}
}
@Override
public Lookup getLookup() {
return lookup;
}
CopyResourcesOnSave getCopyOnSaveResources() {
synchronized (COPYRESOURCES_LOCK) {
if (copyResourcesOnSave == null) {
copyResourcesOnSave = new CopyResourcesOnSave(watcher, this);
}
return copyResourcesOnSave;
}
}
private static class PackagingTypeDependentLookup extends ProxyLookup implements PropertyChangeListener {
//#243866 both NbMavenProject and PackagingTypeDependentLookup are hard referenced from NbMavenProjectImpl
//it should be safe to weak reference here, all should be GCed together.
private final WeakReference<NbMavenProject> watcherRef;
private String packaging;
private final Lookup general;
private volatile List<String> currentIds = new ArrayList<>();
@SuppressWarnings("LeakingThisInConstructor")
PackagingTypeDependentLookup(NbMavenProject watcher) {
this.watcherRef = new WeakReference<NbMavenProject>(watcher);
//needs to be kept around to prevent recreating instances
general = Lookups.forPath("Projects/org-netbeans-modules-maven/Lookup"); //NOI18N
check();
watcher.addPropertyChangeListener(WeakListeners.propertyChange(this, watcher));
}
private String pluginDirectory(Artifact pluginArtifact) {
String groupId = pluginArtifact.getGroupId();
String artId = pluginArtifact.getArtifactId();
return groupId + ":" + artId;
}
/**
* Defines at least some order: let the layer positions to
* @param componentSet
* @return
*/
private List<String> partialComponentsOrder(Collection<String> componentSet) {
List<FileObject> fos = new ArrayList<>();
FileObject root = FileUtil.getConfigFile("Projects/org-netbeans-modules-maven");
for (String s : componentSet) {
FileObject f = root.getFileObject(s);
if (f != null) {
fos.add(f);
}
}
List<String> orderedNames = FileUtil.getOrder(fos, false).stream().map(FileObject::getNameExt).collect(Collectors.toList());
List<String> origList = new ArrayList<>(componentSet);
origList.removeAll(orderedNames);
orderedNames.addAll(origList);
return orderedNames;
}
private void check() {
//this call effectively calls project.getLookup(), when called in constructor will get back to the project's baselookup only.
// but when called from propertyChange() then will call on entire composite lookup, is it a problem? #230469
List<String> newComponents = new ArrayList<>();
NbMavenProject watcher = watcherRef.get();
String newPackaging = packaging != null ? packaging : NbMavenProject.TYPE_JAR;
List<Lookup> lookups = new ArrayList<>();
List<String> old = currentIds;
if (watcher != null) {
newPackaging = watcher.getPackagingType();
if (newPackaging == null) {
newPackaging = NbMavenProject.TYPE_JAR;
}
Set<Artifact> arts = watcher.getMavenProject().getPluginArtifacts();
List<String> compNames = new ArrayList<>();
if (arts != null) {
for (Artifact a : arts) {
compNames.add(pluginDirectory(a));
}
}
compNames.add(newPackaging);
newComponents = partialComponentsOrder(compNames);
} else {
newComponents.add(newPackaging);
}
if (!newComponents.equals(old)) {
for (String s : newComponents) {
lookups.add(Lookups.forPath("Projects/org-netbeans-modules-maven/" + s + "/Lookup")); // NOI18N
}
// put the general lookup last, so plugin - specific ones can override it
lookups.add(general);
lookups.add(Lookups.forPath("Projects/org-netbeans-modules-maven/_any/Lookup")); // NOI18N
synchronized (this) {
if (currentIds != old) {
// the next computation started after us, do not interfere.
return;
}
currentIds = newComponents;
}
setLookups(lookups.toArray(new Lookup[lookups.size()]));
}
}
public @Override void propertyChange(PropertyChangeEvent evt) {
if (NbMavenProject.PROP_PROJECT.equals(evt.getPropertyName())) {
check();
}
}
}
private Lookup createBasicLookup(ProjectState state, M2AuxilaryConfigImpl auxiliary) {
return Lookups.fixed(
this,
fileObject,
auxiliary,
auxiliary.getProblemProvider(),
auxprops,
new MavenProjectPropsImpl.Merger(auxprops),
profileHandler,
configProvider,
problemReporter,
watcher,
state,
UILookupMergerSupport.createPrivilegedTemplatesMerger(),
UILookupMergerSupport.createRecommendedTemplatesMerger(),
UILookupMergerSupport.createProjectProblemsProviderMerger(),
LookupProviderSupport.createSourcesMerger(),
LookupProviderSupport.createSharabilityQueryMerger(),
ProjectClassPathModifier.extenderForModifier(this),
LookupMergerSupport.createClassPathModifierMerger(),
new UnitTestsCompilerOptionsQueryImpl(this),
new PomCompilerOptionsQueryImpl(this),
LookupMergerSupport.createCompilerOptionsQueryMerger(),
MavenJPDAStart.create(this)
);
}
//MEVENIDE-448 seems to help against creation of duplicate project instances
// no idea why, it's supposed to be ProjectManager job.. maybe related to
// maven impl of SubProjectProvider or FileOwnerQueryImplementation
//TODO need to investigate why it's like that..
//a renamed FileObject for project folder stays the same, changing the identity of the project, we have to use File.
@Override
public int hashCode() {
return getPOMFile().hashCode() * 13;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Project) {
NbMavenProjectImpl impl;
if (obj instanceof NbMavenProjectImpl) {
impl = ((NbMavenProjectImpl) obj);
} else {
impl = ((Project) obj).getLookup().lookup(NbMavenProjectImpl.class);
}
if (impl != null) {
return getPOMFile().equals(impl.getPOMFile());
}
}
return false;
}
@Override
public String toString() {
return "Maven[" + getPOMFile().getAbsolutePath() + "]"; //NOI18N
}
interface FileProvider {
File[] getFiles();
}
private class Updater implements FileChangeListener {
private final FileProvider fileProvider;
private List<File> filesToWatch;
private long lastTime = 0;
private Map<File, Long> lastMods;
/** Relative file paths to watch. */
Updater(FileProvider toWatch) {
fileProvider = toWatch;
}
@Override
public void fileAttributeChanged(FileAttributeEvent fileAttributeEvent) {
}
@Override
public void fileChanged(FileEvent fileEvent) {
if (lastTime < fileEvent.getTime()) {
lastTime = System.currentTimeMillis();
// System.out.println("fired based on " + fileEvent.getFile() + fileEvent.getTime());
NbMavenProject.fireMavenProjectReload(NbMavenProjectImpl.this);
}
}
@Override
public void fileDataCreated(FileEvent fileEvent) {
if (lastTime < fileEvent.getTime()) {
lastTime = System.currentTimeMillis();
// System.out.println("fired based on " + fileEvent.getFile() + fileEvent.getTime());
NbMavenProject.fireMavenProjectReload(NbMavenProjectImpl.this);
}
}
@Override
public void fileDeleted(FileEvent fileEvent) {
lastTime = System.currentTimeMillis();
NbMavenProject.fireMavenProjectReload(NbMavenProjectImpl.this);
}
@Override
public void fileFolderCreated(FileEvent fileEvent) {
//TODO possibly remove this fire.. watch for actual path..
// NbMavenProject.fireMavenProjectReload(NbMavenProjectImpl.this);
}
@Override
public void fileRenamed(FileRenameEvent fileRenameEvent) {
}
synchronized void attachAll() {
this.filesToWatch = new ArrayList<>(Arrays.asList(fileProvider.getFiles()));
filesToWatch.addAll(getParents());
Collections.sort(filesToWatch);
for (File file : filesToWatch) {
try {
FileUtil.addFileChangeListener(this, file);
} catch (IllegalArgumentException ex) {
//giving up on ever figuring why OPH is sometimes calls opened() twice in a row on single project.
//There's way too many moving parts.
// * project lookup could be creating multiple instances of OPH
// * a close or open method for a random project/OPH could throw exception skipping our close?
// while OPL catches RuntimeExceptions, OPH merger bypasses that behaviour and handles all OPH as unit.
// * something in OPL or Group is wrong in terms of threading, timing or open/close projects calculation (could be equals/hascode on project related)
LOG.log(Level.INFO, "project opened twice in a row, issue #236211 for " + projectFile.getAbsolutePath(), ex);
Thread.dumpStack();
assert false : "project opened twice in a row, issue #236211 for " + projectFile.getAbsolutePath();
}
}
if(lastMods == null) {
// attached for the first time,
// preserve lastModified of interestig files
lastMods = new HashMap<>(filesToWatch.size());
for (File file : filesToWatch) {
lastMods.put(file, file.lastModified());
}
} else {
for (Map.Entry<File, Long> e : lastMods.entrySet()) {
File file = e.getKey();
long ts = file.lastModified();
if( e.getValue() < ts ) {
// attached after being previously dettached and
// lastModified of an interesting file changed in the meantime
// -> force pom refresh
lastMods.put(file, ts);
NbMavenProject.fireMavenProjectReload(NbMavenProjectImpl.this);
}
}
}
}
protected List<File> getParents() {
LinkedList<File> ret = new LinkedList<>();
MavenProject project = getOriginalMavenProject();
while(true) {
try {
MavenProject parent = loadParentOf(getEmbedder(), project);
File parentFile = parent != null ? parent.getFile() : null;
if(parentFile != null) {
ret.add(parentFile);
project = parent;
} else {
break;
}
} catch (ProjectBuildingException ex) {
break;
}
}
return ret;
}
synchronized void detachAll() {
if (filesToWatch != null) {
List<File> toWatch = filesToWatch;
filesToWatch = null;
for (File file : toWatch) {
try {
FileUtil.removeFileChangeListener(this, file);
} catch (IllegalArgumentException ex) {
LOG.log(Level.INFO, "project closed twice in a row, issue #236211 for " + projectFile.getAbsolutePath(), ex);
Thread.dumpStack();
assert false : "project closed twice in a row, issue #236211 for " + projectFile.getAbsolutePath();
}
}
}
}
}
}