| /* |
| * 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.queries; |
| |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.io.File; |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.prefs.BackingStoreException; |
| import java.util.prefs.Preferences; |
| import javax.swing.event.ChangeEvent; |
| import javax.swing.event.ChangeListener; |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.model.Model; |
| import org.apache.maven.model.Parent; |
| import org.apache.maven.model.io.ModelReader; |
| import org.apache.maven.project.MavenProject; |
| import org.netbeans.api.annotations.common.CheckForNull; |
| import org.netbeans.api.annotations.common.NonNull; |
| import org.netbeans.api.project.Project; |
| import org.netbeans.api.project.ProjectManager; |
| import org.netbeans.api.project.ui.OpenProjects; |
| import org.netbeans.api.project.ui.ProjectGroup; |
| import org.netbeans.api.project.ui.ProjectGroupChangeEvent; |
| import org.netbeans.api.project.ui.ProjectGroupChangeListener; |
| import org.netbeans.modules.maven.NbMavenProjectFactory; |
| import org.netbeans.modules.maven.NbMavenProjectImpl; |
| import org.netbeans.modules.maven.api.NbMavenProject; |
| import org.netbeans.modules.maven.embedder.EmbedderFactory; |
| import org.netbeans.modules.maven.modelcache.MavenProjectCache; |
| import org.netbeans.spi.project.FileOwnerQueryImplementation; |
| import org.openide.filesystems.FileObject; |
| import org.openide.filesystems.FileUtil; |
| import org.openide.filesystems.URLMapper; |
| import org.openide.util.Exceptions; |
| import org.openide.util.Lookup; |
| import org.openide.util.NbPreferences; |
| import org.openide.util.Utilities; |
| import org.openide.util.lookup.ServiceProvider; |
| import org.openide.util.lookup.ServiceProviders; |
| |
| /** |
| * Links the Maven project with its artifact in the local repository. |
| */ |
| @ServiceProviders({@ServiceProvider(service=FileOwnerQueryImplementation.class, position=97), @ServiceProvider(service=MavenFileOwnerQueryImpl.class)}) |
| public class MavenFileOwnerQueryImpl implements FileOwnerQueryImplementation { |
| private static final String EXTERNAL_OWNERS = "externalOwners"; |
| |
| private final PropertyChangeListener projectListener; |
| private final ProjectGroupChangeListener groupListener; |
| private final List<ChangeListener> listeners = new CopyOnWriteArrayList<ChangeListener>(); |
| |
| private static final AtomicReference<Preferences> prefs = new AtomicReference<Preferences>(NbPreferences.forModule(MavenFileOwnerQueryImpl.class).node(EXTERNAL_OWNERS)); |
| |
| private static final Logger LOG = Logger.getLogger(MavenFileOwnerQueryImpl.class.getName()); |
| |
| public MavenFileOwnerQueryImpl() { |
| projectListener = new PropertyChangeListener() { |
| public @Override void propertyChange(PropertyChangeEvent evt) { |
| if (NbMavenProject.PROP_PROJECT.equals(evt.getPropertyName())) { |
| if (!registerProject((NbMavenProjectImpl) evt.getSource(), true)) { |
| fireChange(new ChangeEvent(this)); |
| } |
| } |
| } |
| }; |
| groupListener = new ProjectGroupChangeListener() { |
| |
| @Override |
| public void projectGroupChanging(ProjectGroupChangeEvent event) { |
| Preferences old = prefs(); |
| Preferences n = event.getNewGroup() != null |
| ? event.getNewGroup().preferencesForPackage(MavenFileOwnerQueryImpl.class).node(EXTERNAL_OWNERS) |
| : NbPreferences.forModule(MavenFileOwnerQueryImpl.class).node(EXTERNAL_OWNERS); |
| prefs.compareAndSet(old, n); |
| } |
| |
| @Override |
| public void projectGroupChanged(ProjectGroupChangeEvent event) { |
| //TODO should we check what projects were kept open and register them with current group? |
| //some were already registered when projectOpenHook was triggered, |
| //but some might have been kept opened from previous group |
| for (Project prj : OpenProjects.getDefault().getOpenProjects()) { |
| NbMavenProjectImpl mp = prj.getLookup().lookup(NbMavenProjectImpl.class); |
| if (mp != null) { |
| registerProject(mp, false); |
| } |
| } |
| fireChange(new ChangeEvent(this)); //optimization, just one change gets fired. |
| } |
| }; |
| ProjectGroup pg = OpenProjects.getDefault().getActiveProjectGroup(); |
| //initial value is non-group setting but we need to check if a group is active |
| if (pg != null) { |
| Preferences old = prefs(); |
| prefs.compareAndSet(old, pg.preferencesForPackage(MavenFileOwnerQueryImpl.class).node(EXTERNAL_OWNERS)); |
| } |
| //not worth making weak, both are singletons kept forever |
| OpenProjects.getDefault().addProjectGroupChangeListener(groupListener); |
| } |
| |
| public static MavenFileOwnerQueryImpl getInstance() { |
| return Lookup.getDefault().lookup(MavenFileOwnerQueryImpl.class); |
| } |
| |
| public void attachProjectListener(NbMavenProjectImpl project) { |
| project.getProjectWatcher().removePropertyChangeListener(projectListener); |
| project.getProjectWatcher().addPropertyChangeListener(projectListener); |
| } |
| |
| public static String cacheKey(String groupId, String artifactId, String version) { |
| return groupId + ':' + artifactId + ":" + version; |
| } |
| |
| public void registerCoordinates(String groupId, String artifactId, String version, URL owner, boolean fire) { |
| String oldkey = groupId + ':' + artifactId; |
| //remove old key if pointing to the same project |
| if (owner.toString().equals(prefs().get(oldkey, null))) { |
| prefs().remove(oldkey); |
| } |
| |
| String key = cacheKey(groupId, artifactId, version); |
| String ownerString = owner.toString(); |
| try { |
| for (String k : prefs().keys()) { |
| if (ownerString.equals(prefs().get(k, null))) { |
| prefs().remove(k); |
| break; |
| } |
| } |
| } catch (BackingStoreException ex) { |
| LOG.log(Level.FINE, "Error iterating preference to find old mapping", ex); |
| } |
| |
| prefs().put(key, ownerString); |
| LOG.log(Level.FINE, "Registering {0} under {1}", new Object[] {owner, key}); |
| if (fire) { |
| fireChange(new GAVCHangeEvent(this, groupId, artifactId, version)); |
| } |
| |
| } |
| |
| /** |
| * |
| * @param project |
| * @return true if project was registered, false otherwise |
| */ |
| public boolean registerProject(NbMavenProjectImpl project, boolean fire) { |
| MavenProject model = project.getOriginalMavenProject(); |
| attachProjectListener(project); |
| if (NbMavenProject.isErrorPlaceholder(model)) { |
| LOG.log(Level.FINE, "will not register unloadable {0}", project.getPOMFile()); |
| //TODO we should remove the project's mapping in this case and wait for it to reappear loadable again |
| return false; |
| } |
| try { |
| registerCoordinates(model.getGroupId(), model.getArtifactId(), model.getVersion(), Utilities.toURI(project.getPOMFile().getParentFile()).toURL(), fire); |
| } catch (MalformedURLException ex) { |
| } |
| return true; |
| } |
| |
| public void addChangeListener(ChangeListener list) { |
| listeners.add(list); |
| } |
| |
| public void removeChangeListener(ChangeListener list) { |
| listeners.remove(list); |
| } |
| |
| private void fireChange(ChangeEvent event) { |
| for (ChangeListener l : listeners) { |
| l.stateChanged(event); |
| } |
| } |
| |
| public @Override Project getOwner(URI uri) { |
| LOG.log(Level.FINEST, "getOwner of uri={0}", uri); |
| if ("file".equals(uri.getScheme())) { //NOI18N |
| File file = Utilities.toFile(uri); |
| return getOwner(file); |
| } |
| return null; |
| } |
| |
| public @Override Project getOwner(FileObject fileObject) { |
| LOG.log(Level.FINEST, "getOwner of fileobject={0}", fileObject); |
| File file = FileUtil.toFile(fileObject); |
| if (file != null) { |
| return getOwner(file); |
| } |
| return null; |
| } |
| |
| /** |
| * Utility method to identify a file which might be an artifact in the local repository. |
| * @param file a putative artifact |
| * @return its coordinates (groupId/artifactId/version), or null if it cannot be identified |
| */ |
| static @CheckForNull String[] findCoordinates(File file) { |
| String nm = file.getName(); // commons-math-2.1.jar |
| File parentVer = file.getParentFile(); // ~/.m2/repository/org/apache/commons/commons-math/2.1 |
| if (parentVer != null) { |
| File parentArt = parentVer.getParentFile(); // ~/.m2/repository/org/apache/commons/commons-math |
| if (parentArt != null) { |
| String artifactID = parentArt.getName(); // commons-math |
| String version = parentVer.getName(); // 2.1 |
| if (nm.startsWith(artifactID + '-' + version)) { |
| File parentGroup = parentArt.getParentFile(); // ~/.m2/repository/org/apache/commons |
| if (parentGroup != null) { |
| // Split rest into separate method, to avoid linking EmbedderFactory unless and until needed. |
| return findCoordinates(parentGroup, artifactID, version); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| private static @CheckForNull String[] findCoordinates(File parentGroup, String artifactID, String version) { |
| File repo = EmbedderFactory.getProjectEmbedder().getLocalRepositoryFile(); // ~/.m2/repository |
| String repoS = repo.getAbsolutePath(); |
| if (!repoS.endsWith(File.separator)) { |
| repoS += File.separatorChar; // ~/.m2/repository/ |
| } |
| String parentGroupS = parentGroup.getAbsolutePath(); |
| if (parentGroupS.endsWith(File.separator)) { |
| parentGroupS = parentGroupS.substring(0, parentGroupS.length() - 1); |
| } |
| if (parentGroupS.startsWith(repoS)) { |
| String groupID = parentGroupS.substring(repoS.length()).replace(File.separatorChar, '.'); // org.apache.commons |
| return new String[] {groupID, artifactID, version}; |
| } else { |
| return null; |
| } |
| } |
| |
| private Project getOwner(File file) { |
| //#223841 at least one project opened is a stronger condition, embedder gets sometimes reset. |
| //once we have the project loaded, not loaded embedder doesn't matter anymore, we have to process. |
| // sometimes the embedder is loaded even though a maven project is not yet loaded, it doesn't hurt to proceed then. |
| if (!NbMavenProjectFactory.isAtLeastOneMavenProjectAround() && !EmbedderFactory.isProjectEmbedderLoaded()) { |
| return null; |
| } |
| |
| LOG.log(Level.FINER, "Looking for owner of {0}", file); |
| String[] coordinates = findCoordinates(file); |
| if (coordinates == null) { |
| LOG.log(Level.FINER, "{0} not an artifact in local repo", file); |
| return null; |
| } |
| return getOwner(coordinates[0], coordinates[1], coordinates[2]); |
| } |
| |
| public Project getOwner(String groupId, String artifactId, String version) { |
| LOG.log(Level.FINER, "Checking {0} / {1} / {2}", new Object[] {groupId, artifactId, version}); |
| String oldKey = groupId + ":" + artifactId; |
| String key = cacheKey(groupId, artifactId, version); |
| String ownerURI = prefs().get(key, null); |
| boolean usingOldKey = false; |
| if (ownerURI == null) { |
| ownerURI = prefs().get(oldKey, null); |
| usingOldKey = true; |
| } |
| if (ownerURI != null) { |
| boolean stale = true; |
| try { |
| FileObject projectDir = URLMapper.findFileObject(new URI(ownerURI).toURL()); |
| if (projectDir != null && projectDir.isFolder()) { |
| Project p = ProjectManager.getDefault().findProject(projectDir); |
| if (p != null) { |
| NbMavenProjectImpl mp = p.getLookup().lookup(NbMavenProjectImpl.class); |
| if (mp != null) { |
| MavenProject model = mp.getOriginalMavenProject(); |
| if (model.getGroupId().equals(groupId) && model.getArtifactId().equals(artifactId)) { |
| if (model.getVersion().equals(version)) { |
| LOG.log(Level.FINE, "Found match {0}", p); |
| //some projects get registered only via coordinates and we never listen on their changes, |
| //do it now, when we know the project is loaded |
| //since we match on GAV not GA now, it's more important to have changes in projects reflected in FOQ |
| attachProjectListener(mp); |
| return p; |
| } else { |
| LOG.log(Level.FINE, "Mismatch on version {0} in {1}", new Object[] {model.getVersion(), ownerURI}); |
| stale = false; // we merely remembered another version |
| registerProject(mp, true); |
| } |
| } else { |
| LOG.log(Level.FINE, "Mismatch on group and/or artifact ID in {0}", ownerURI); |
| registerProject(mp, true); |
| } |
| } else { |
| LOG.log(Level.FINE, "Not a Maven project {0} in {1}", new Object[] {p, ownerURI}); |
| } |
| } else { |
| LOG.log(Level.FINE, "No such project in {0}", ownerURI); |
| } |
| } else { |
| LOG.log(Level.FINE, "No such folder {0}", ownerURI); |
| } |
| } catch (IOException x) { |
| LOG.log(Level.FINE, "Could not load project in " + ownerURI, x); |
| } catch (URISyntaxException x) { |
| LOG.log(Level.INFO, null, x); |
| } |
| if (stale) { |
| if (usingOldKey) { |
| prefs().remove(oldKey); |
| } else { |
| prefs().remove(key); // stale |
| } |
| } |
| } else { |
| LOG.log(Level.FINE, "No known owner for {0}", key); |
| } |
| return null; |
| } |
| |
| |
| //NOTE: called from NBArtifactFixer, cannot contain references to ProjectManager |
| public File getOwnerPOM(String groupId, String artifactId, String version) { |
| LOG.log(Level.FINER, "Checking {0} / {1} / {2} (POM only)", new Object[] {groupId, artifactId, version}); |
| String oldKey = groupId + ":" + artifactId; |
| String key = cacheKey(groupId, artifactId, version); |
| String ownerURI = prefs().get(key, null); |
| boolean usingOldKey = false; |
| if (ownerURI == null) { |
| ownerURI = prefs().get(oldKey, null); |
| usingOldKey = true; |
| } |
| if (ownerURI != null) { |
| try { |
| URI uri = new URI(ownerURI); |
| if ("file".equals(uri.getScheme())) { |
| File pom = Utilities.toFile(uri.resolve("pom.xml")); |
| if (pom.isFile()) { |
| ModelReader reader = EmbedderFactory.getProjectEmbedder().lookupComponent(ModelReader.class); |
| Model model = reader.read(pom, Collections.singletonMap(ModelReader.IS_STRICT, false)); |
| Parent parent = model.getParent(); |
| if (groupId.equals(model.getGroupId()) || (parent != null && groupId.equals(parent.getGroupId()))) { |
| if (artifactId.equals(model.getArtifactId()) || (parent != null && artifactId.equals(parent.getArtifactId()))) { |
| if (version.equals(model.getVersion()) || (parent != null && version.equals(parent.getVersion()))) { |
| LOG.log(Level.FINE, "found match {0}", pom); |
| return pom; |
| } else { |
| LOG.log(Level.FINE, "mismatch on version in {0}", pom); |
| } |
| } else { |
| LOG.log(Level.FINE, "mismatch on artifactId in {0}", pom); |
| } |
| } else { |
| LOG.log(Level.FINE, "mismatch on groupId in {0}", pom); |
| } |
| // Might actually be a match due to use of e.g. string interpolation, so double-check with live project. |
| FileObject projectDir = URLMapper.findFileObject(new URI(ownerURI).toURL()); |
| if (projectDir != null && projectDir.isFolder()) { |
| File pomFile = new File(FileUtil.toFile(projectDir), "pom.xml"); |
| //TODO the file instance will not be the same instance passed by project. how does weakhashmap behave in such a case? |
| MavenProject prj = MavenProjectCache.getMavenProject(pomFile, false); |
| if (prj != null && prj.getGroupId().equals(groupId) && prj.getArtifactId().equals(artifactId) && prj.getVersion().equals(version)) { |
| return pom; |
| } |
| } else { |
| LOG.log(Level.FINE, "no live project match for {0}", pom); |
| } |
| } else { |
| LOG.log(Level.FINE, "no such file {0}", pom); |
| } |
| } else { |
| LOG.log(Level.FINE, "not a file URI {0}", uri); |
| } |
| } catch (IOException x) { |
| LOG.log(Level.FINE, "Could not load project in " + ownerURI, x); |
| } catch (URISyntaxException x) { |
| LOG.log(Level.INFO, null, x); |
| } |
| //not found match results in prefs cleanup. |
| if (usingOldKey) { |
| prefs().remove(oldKey); |
| } else { |
| prefs().remove(key); // stale |
| } |
| } else { |
| LOG.log(Level.FINE, "No known owner for {0}", key); |
| } |
| return null; |
| } |
| |
| static Preferences prefs() { |
| return prefs.get(); |
| } |
| |
| /** |
| * in some situations this ChangeEvent subclass can be fired, allowing listeners |
| * to optimize response based on GAV affected. |
| */ |
| public static class GAVCHangeEvent extends ChangeEvent { |
| private final String groupId; |
| private final String version; |
| private final String artifactId; |
| |
| public GAVCHangeEvent(@NonNull Object source, @NonNull String groupId, @NonNull String artifactId, @NonNull String version) { |
| super(source); |
| this.groupId = groupId; |
| this.version = version; |
| this.artifactId = artifactId; |
| } |
| |
| public String getGroupId() { |
| return groupId; |
| } |
| |
| public String getVersion() { |
| return version; |
| } |
| |
| public String getArtifactId() { |
| return artifactId; |
| } |
| |
| public boolean matches(Artifact art) { |
| return groupId.equals(art.getGroupId()) && artifactId.equals(art.getArtifactId()) && version.equals(art.getVersion()); |
| } |
| |
| } |
| } |