All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.maven.classpath.MavenSourcesImpl Maven / Gradle / Ivy

There is a newer version: RELEASE240
Show newest version
/*
 * 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 javaGroup;
    private final Map genSrcGroup;
    private final Map otherMainGroups;
    private final Map otherTestGroups;

    
    private final Object lock = new Object();
    
    public MavenSourcesImpl(Project proj) {
        this.proj = proj;
        javaGroup = new TreeMap();
        genSrcGroup = new TreeMap();
        otherMainGroups = new TreeMap();
        otherTestGroups = new TreeMap();
    }
    
    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 toReturn = new ArrayList();
            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 toReturn = new ArrayList();
            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 toReturn = new ArrayList();
            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 virtuals = new ArrayList();
            List existing = new ArrayList();
            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  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 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 checked = new ArrayList();
        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 currs = new HashSet();
        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 toRemove = new HashSet(test ? otherTestGroups.keySet() : otherMainGroups.keySet());
        toRemove.removeAll(Arrays.asList(roots));

        URI[] res = project().getResources(test);
        Set resources = new HashSet();
        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 resourceRoots, boolean test) {
        FileObject root = FileUtil.toFileObject(rootFile);
        if (root != null && !root.isFolder()) {
            root = null;
        }
        Map 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 excludedFolders() {
        Set 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=")
        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 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
        }
        
    }
    
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy