blob: 65b56602fc37d5b76210955f40401766b2ee0c7b [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.modelcache;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.WeakHashMap;
import java.util.function.BiConsumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
import org.apache.maven.AbstractMavenLifecycleParticipant;
import org.apache.maven.execution.DefaultMavenExecutionResult;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenExecutionResult;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.project.ProjectActionContext;
import org.netbeans.modules.maven.M2AuxilaryConfigImpl;
import org.netbeans.modules.maven.api.NbMavenProject;
import org.netbeans.modules.maven.api.execute.RunConfig;
import org.netbeans.modules.maven.configurations.M2Configuration;
import org.netbeans.modules.maven.embedder.EmbedderFactory;
import org.netbeans.modules.maven.embedder.MavenEmbedder;
import org.netbeans.spi.project.AuxiliaryConfiguration;
import org.netbeans.spi.project.ProjectConfiguration;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Mutex;
import org.openide.util.Mutex.Action;
import org.openide.util.NbBundle;
/**
* externalize the creation of MavenProject instances outside of NbMavenProjectImpl
* and be able to access it without a project at hand
* @author mkleint
*/
public final class MavenProjectCache {
private static final Logger LOG = Logger.getLogger(MavenProjectCache.class.getName());
private static final String CONTEXT_EXECUTION_RESULT = "NB_Execution_Result";
private static final String CONTEXT_PARTICIPANTS = "NB_AbstractParticipant_Present";
/**
* Folder with module-configurable whitelist of lifecycle participants. Currently only 'ignore' can be specified.
*/
private static final String LIFECYCLE_PARTICIPANT_PREFIX = "Projects/" + NbMavenProject.TYPE + "/LifecycleParticipants/"; // NOI18N
/**
* Attribute that specifies the lifecycle participant should be silently ignored on model load.
*/
private static final String ATTR_IGNORE_ON_LOAD = "ignoreOnModelLoad"; // NOI18N
//File is referenced during lifetime of the Project. FileObject cannot be used as with rename it changes value@!!!
private static final Map<File, WeakReference<MavenProject>> file2Project = new WeakHashMap<File, WeakReference<MavenProject>>();
private static final Map<File, Mutex> file2Mutex = new WeakHashMap<File, Mutex>();
public static void clearMavenProject(final File pomFile) {
Mutex mutex = getMutex(pomFile);
mutex.writeAccess(new Action<MavenProject>() {
@Override
public MavenProject run() {
file2Project.remove(pomFile);
return null;
}
});
}
/**
* returns a MavenProject instance for given folder, if folder contains a pom.xml always returns an instance, if not returns null
* @param pomFile
* @param reload consult the cache and return the cached value if true, otherwise force a reload.
* @return
*/
public static MavenProject getMavenProject(final File pomFile, final boolean reload) {
Mutex mutex = getMutex(pomFile);
MavenProject mp = mutex.writeAccess(new Action<MavenProject>() {
@Override
public MavenProject run() {
if (!reload) {
WeakReference<MavenProject> ref = file2Project.get(pomFile);
if (ref != null) {
MavenProject mp = ref.get();
if (mp != null) {
return mp;
}
}
}
MavenProject mp = loadOriginalMavenProject(pomFile);
file2Project.put(pomFile, new WeakReference<MavenProject>(mp));
return mp;
}
});
return mp;
}
public static MavenProject loadMavenProject(final File pomFile, ProjectActionContext context, RunConfig runConf) {
if (context == null) {
return getMavenProject(pomFile, true);
} else {
return loadOriginalMavenProject(pomFile, context, runConf);
}
}
public static MavenExecutionResult getExecutionResult(MavenProject project) {
return (MavenExecutionResult) project.getContextValue(CONTEXT_EXECUTION_RESULT);
}
public static boolean unknownBuildParticipantObserved(MavenProject project) {
return project.getContextValue(CONTEXT_PARTICIPANTS) != null;
}
/**
* list of class names of build participants in the project, null when none are present.
* @param project
* @return
*/
public static Collection<String> getUnknownBuildParticipantsClassNames(MavenProject project) {
return (Collection<String>) project.getContextValue(CONTEXT_PARTICIPANTS);
}
@NbBundle.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 static @NonNull MavenProject loadOriginalMavenProject(final File pomFile) {
return loadOriginalMavenProject(pomFile, null, null);
}
private static boolean isLifecycleParticipatnIgnored(AbstractMavenLifecycleParticipant instance) {
String n = instance.getClass().getName();
FileObject check = FileUtil.getConfigFile(LIFECYCLE_PARTICIPANT_PREFIX + n);
return check != null && check.getAttribute(ATTR_IGNORE_ON_LOAD) == Boolean.TRUE;
}
private static @NonNull MavenProject loadOriginalMavenProject(final File pomFile, ProjectActionContext ctx, RunConfig runConf) {
long startLoading = System.currentTimeMillis();
MavenEmbedder projectEmbedder = EmbedderFactory.getProjectEmbedder();
MavenProject newproject = null;
//TODO have independent from M2AuxiliaryConfigImpl
FileObject projectDir = FileUtil.toFileObject(pomFile.getParentFile());
if (projectDir == null || !projectDir.isValid()) {
return getFallbackProject(pomFile);
}
AuxiliaryConfiguration aux = new M2AuxilaryConfigImpl(projectDir, false);
ActiveConfigurationProvider config = new ActiveConfigurationProvider(projectDir, aux);
M2Configuration active;
active = config.getActiveConfiguration();
if (ctx != null && ctx.getConfiguration() != null) {
ProjectConfiguration cfg = ctx.getConfiguration();
if (cfg instanceof M2Configuration) {
active = (M2Configuration)cfg;
}
}
MavenExecutionResult res = null;
try {
List<String> mavenConfigOpts = Collections.emptyList();
for (FileObject root = projectDir; root != null; root = root.getParent()) {
FileObject mavenConfig = root.getFileObject(".mvn/maven.config");
if (mavenConfig != null && mavenConfig.isData()) {
mavenConfigOpts = Arrays.asList(mavenConfig.asText().split("\\s+"));
break;
}
}
final MavenExecutionRequest req = projectEmbedder.createMavenExecutionRequest();
req.addActiveProfiles(active.getActivatedProfiles());
BiConsumer<String, String> addActiveProfiles = (opt, prefix) -> req.addActiveProfiles(Arrays.asList(opt.substring(prefix.length()).split(",")));
Iterator<String> optIt = mavenConfigOpts.iterator();
while (optIt.hasNext()) {
String opt = optIt.next();
// Could try to write/integrate a more general option parser here,
// but some options like -amd anyway violate GNU style.
if (opt.equals("-P") || opt.equals("--activate-profiles")) {
addActiveProfiles.accept(optIt.next(), "");
} else if (opt.startsWith("-P")) {
addActiveProfiles.accept(opt, "-P");
} else if (opt.startsWith("--activate-profiles=")) {
addActiveProfiles.accept(opt, "--activate-profiles=");
}
}
if (runConf != null) {
req.addActiveProfiles(runConf.getActivatedProfiles());
}
if (ctx != null && ctx.getProfiles() != null) {
req.addActiveProfiles(new ArrayList<>(ctx.getProfiles()));
}
req.setPom(pomFile);
req.setNoSnapshotUpdates(true);
req.setUpdateSnapshots(false);
//MEVENIDE-634 i'm wondering if this fixes the issue
req.setInteractiveMode(false);
// recursive == false is important to avoid checking all submodules for extensions
// that will not be used in current pom anyway..
// #135070
req.setRecursive(false);
req.setOffline(true);
//#238800 important to merge, not replace
Properties uprops = req.getUserProperties();
BiConsumer<String, String> setProperty = (opt, prefix) -> {
String value = opt.substring(prefix.length());
int equals = value.indexOf('=');
if (equals != -1) {
uprops.setProperty(value.substring(0, equals), value.substring(equals + 1));
} else {
uprops.setProperty(value, "true");
}
};
optIt = mavenConfigOpts.iterator();
while (optIt.hasNext()) {
String opt = optIt.next();
if (opt.equals("-D") || opt.equals("--define")) {
setProperty.accept(optIt.next(), "");
} else if (opt.startsWith("-D")) {
setProperty.accept(opt, "-D");
} else if (opt.startsWith("--define=")) {
setProperty.accept(opt, "--define=");
}
}
uprops.putAll(createUserPropsForProjectLoading(active.getProperties()));
if (ctx != null && ctx.getProperties() != null) {
for (String k : ctx.getProperties().keySet()) {
uprops.setProperty(k, ctx.getProperties().get(k));
}
}
if (runConf != null && runConf.getProperties() != null) {
Map<? extends String, ? extends String> props = runConf.getProperties();
for (String k : props.keySet()) {
uprops.setProperty(k, props.get(k));
}
}
req.setUserProperties(uprops);
res = projectEmbedder.readProjectWithDependencies(req, true);
newproject = res.getProject();
//#204898
if (newproject != null) {
ClassLoader projectRealm = newproject.getClassRealm();
if (projectRealm != null) {
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(projectRealm);
try {
//boolean execute = EnableParticipantsBuildAction.isEnabled(aux);
List<AbstractMavenLifecycleParticipant> lookup = projectEmbedder.getPlexus().lookupList(AbstractMavenLifecycleParticipant.class);
if (lookup.size() > 0) { //just in case..
// if (execute) {
// LOG.info("Executing External Build Participants...");
// MavenSession session = new MavenSession( projectEmbedder.getPlexus(), newproject.getProjectBuildingRequest().getRepositorySession(), req, res );
// session.setCurrentProject(newproject);
// session.setProjects(Collections.singletonList(newproject));
// projectEmbedder.setUpLegacySupport();
// projectEmbedder.getPlexus().lookup(LegacySupport.class).setSession(session);
//
// for (AbstractMavenLifecycleParticipant part : lookup) {
// try {
// Thread.currentThread().setContextClassLoader( part.getClass().getClassLoader() );
// part.afterSessionStart(session);
// part.afterProjectsRead(session);
// } catch (MavenExecutionException ex) {
// Exceptions.printStackTrace(ex);
// }
// }
// } else {
List<String> parts = new ArrayList<String>();
for (AbstractMavenLifecycleParticipant part : lookup) {
if (isLifecycleParticipatnIgnored(part)) {
//#204898 create a whitelist of known not harmful participants that can be just ignored
continue;
}
String name = part.getClass().getName();
parts.add(name);
}
if (parts.size() > 0) {
newproject.setContextValue(CONTEXT_PARTICIPANTS, parts);
}
// }
}
} catch (ComponentLookupException e) {
// this is just silly, lookupList should return an empty list!
} finally {
Thread.currentThread().setContextClassLoader(originalClassLoader);
}
}
}
} catch (RuntimeException | IOException 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 " + pomFile, exc); //NOI18N
res = new DefaultMavenExecutionResult();
res.addException(exc);
} finally {
if (newproject == null) {
newproject = getFallbackProject(pomFile);
}
//#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..
newproject.setProjectBuildingRequest(null);
//TODO some exceptions in result contain various model caches as well..
newproject.setContextValue(CONTEXT_EXECUTION_RESULT, res);
long endLoading = System.currentTimeMillis();
LOG.log(Level.FINE, "Loaded project in {0} msec at {1}", new Object[] {endLoading - startLoading, pomFile.getPath()});
if (LOG.isLoggable(Level.FINE) && SwingUtilities.isEventDispatchThread()) {
LOG.log(Level.FINE, "Project " + pomFile.getPath() + " loaded in AWT event dispatching thread!", new RuntimeException());
}
}
return newproject;
}
@NbBundle.Messages({
"LBL_Incomplete_Project_Name=<partially loaded Maven project>",
"LBL_Incomplete_Project_Desc=Partially loaded Maven project; try building it."
})
public static MavenProject getFallbackProject(File projectFile) throws AssertionError {
MavenProject newproject = new MavenProject();
newproject.setGroupId("error");
newproject.setArtifactId("error");
newproject.setVersion("0");
newproject.setPackaging("pom");
newproject.setName(Bundle.LBL_Incomplete_Project_Name());
newproject.setDescription(Bundle.LBL_Incomplete_Project_Desc());
newproject.setFile(projectFile);
return newproject;
}
public static boolean isFallbackproject(MavenProject prj) {
return "error".equals(prj.getGroupId()) && "error".equals(prj.getArtifactId()) && Bundle.LBL_Incomplete_Project_Name().equals(prj.getName());
}
public static Properties createUserPropsForProjectLoading(Map<String, String> activeConfiguration) {
Properties props = new Properties();
if (activeConfiguration != null) {
props.putAll(activeConfiguration);
}
return props;
}
private static Mutex getMutex(File pomFile) {
synchronized (file2Mutex) {
Mutex mutex = file2Mutex.get(pomFile);
if (mutex != null) {
return mutex;
}
mutex = new Mutex();
file2Mutex.put(pomFile, mutex);
return mutex;
}
}
}