/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.ruby.railsprojects; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.HashSet; import java.util.Set; import org.netbeans.modules.ruby.railsprojects.ui.FoldersListSettings; import org.openide.ErrorManager; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.util.WeakListeners; import org.openide.util.Mutex; import org.openide.util.NbBundle; import org.netbeans.modules.ruby.spi.project.support.rake.RakeProjectEvent; import org.netbeans.modules.ruby.spi.project.support.rake.RakeProjectListener; import org.netbeans.modules.ruby.spi.project.support.rake.PropertyEvaluator; import org.netbeans.modules.ruby.spi.project.support.rake.ReferenceHelper; import org.netbeans.api.project.ProjectManager; import org.netbeans.api.queries.VisibilityQuery; import org.netbeans.modules.ruby.rubyproject.UpdateHelper; /** * This class represents a project source roots. It is used to obtain roots as Ant properties, FileObject's * or URLs. * @author Tomas Zezula */ public final class SourceRoots { public static final String PROP_ROOT_PROPERTIES = "rootProperties"; //NOI18N public static final String PROP_ROOTS = "roots"; //NOI18N public static final String DEFAULT_SOURCE_LABEL = NbBundle.getMessage(SourceRoots.class, "NAME_src.dir"); public static final String DEFAULT_TEST_LABEL = NbBundle.getMessage(SourceRoots.class, "NAME_test.src.dir"); private boolean showRSpec; private final UpdateHelper helper; private final PropertyEvaluator evaluator; private final ReferenceHelper refHelper; private final String elementName; private final String newRootNameTemplate; private List<String> sourceRootProperties; private List<String> sourceRootNames; private List<FileObject> sourceRoots; private List<FileObject> plainFiles; private List<URL> sourceRootURLs; private final PropertyChangeSupport support; private final ProjectMetadataListener listener; private final boolean isTest; private final File projectDir; /** * Creates new SourceRoots * @param helper * @param evaluator * @param elementName the name of XML element under which are declared the roots * @param newRootNameTemplate template for new property name of source root */ SourceRoots (UpdateHelper helper, PropertyEvaluator evaluator, ReferenceHelper refHelper, String elementName, boolean isTest, String newRootNameTemplate) { assert helper != null && evaluator != null && refHelper != null && elementName != null && newRootNameTemplate != null; this.helper = helper; this.evaluator = evaluator; this.refHelper = refHelper; this.elementName = elementName; this.isTest = isTest; this.newRootNameTemplate = newRootNameTemplate; this.projectDir = FileUtil.toFile(this.helper.getRakeProjectHelper().getProjectDirectory()); this.support = new PropertyChangeSupport(this); this.listener = new ProjectMetadataListener(); this.evaluator.addPropertyChangeListener (WeakListeners.propertyChange(this.listener,this.evaluator)); this.helper.getRakeProjectHelper().addRakeProjectListener (WeakListeners.create(RakeProjectListener.class, this.listener,this.helper)); //if (helper != null && helper.getRakeProjectHelper() != null) { // showRSpec = new RSpecSupport(/*helper.getRakeProjectHelper().getProjectDirectory(),*/ null).isRSpecInstalled(); //} showRSpec = true; } private String getNodeDescription(String key) { return NbBundle.getMessage(SourceRoots.class, key); // NOI18N } private void initializeRoots() { synchronized (this) { if (sourceRoots == null) { if (isTest) { initializeTestRoots(); } else if (FoldersListSettings.getDefault().getLogicalView()) { initializeRootsLogical(); } else { initializeRootsFiles(); } } } } private void initializeTestRoots() { sourceRootNames = new ArrayList<String>(); sourceRootProperties = new ArrayList<String>(); if (showRSpec) { sourceRootNames.add(getNodeDescription("rspec")); // NOI18N sourceRootProperties.add("spec"); // NOI18N } sourceRootNames.add(getNodeDescription("test")); // NOI18N sourceRootProperties.add("test"); // NOI18N List<FileObject> result = new ArrayList<FileObject>(); for (String p : sourceRootProperties) { FileObject f = helper.getRakeProjectHelper().resolveFileObject(p); if (f == null) { continue; } if (FileUtil.isArchiveFile(f)) { f = FileUtil.getArchiveRoot(f); } result.add(f); } sourceRoots = Collections.unmodifiableList(result); } private void addPlainFiles(FileObject dir, String... fileNames) { plainFiles = new ArrayList<FileObject>(20); for (String fileName : fileNames) { FileObject toAdd = dir.getFileObject(fileName); if (toAdd != null) { plainFiles.add(toAdd); } } } /** Create a logical view of the project: flatten app/ and test/ * and substitute logical names instead of the directory names */ private void initializeRootsLogical() { FileObject fo = helper.getRakeProjectHelper().getProjectDirectory(); addPlainFiles(fo, "Capfile", "Gemfile", "Rakefile", "README"); // show app/metal for Rack applications, but only if the folder already exists boolean metal = fo.getFileObject("app/metal") != null;//NOI18N boolean mails = fo.getFileObject("app/mails") != null;//NOI18N boolean middleware = fo.getFileObject("app/middleware") != null;//NOI18N boolean reports = fo.getFileObject("app/reports") != null;//NOI18N sourceRootNames = new ArrayList<String>(20); sourceRootProperties = new ArrayList<String>(20); // Note Keep list in sync with root properties list below sourceRootNames.add(getNodeDescription("app_controllers")); // NOI18N sourceRootNames.add(getNodeDescription("app_helpers")); // NOI18N if (metal) { sourceRootNames.add(getNodeDescription("app_metal")); // NOI18N } if (mails) { sourceRootNames.add(getNodeDescription("app_mails")); // NOI18N } if (middleware) { sourceRootNames.add(getNodeDescription("app_middleware")); // NOI18N } if (reports) { sourceRootNames.add(getNodeDescription("app_reports")); // NOI18N } sourceRootNames.add(getNodeDescription("app_models")); // NOI18N sourceRootNames.add(getNodeDescription("app_views")); // NOI18N sourceRootProperties.add("app/controllers"); // NOI18N sourceRootProperties.add("app/helpers"); // NOI18N if (metal) { sourceRootProperties.add("app/metal"); // NOI18N } if (mails) { sourceRootProperties.add("app/mails"); // NOI18N } if (middleware) { sourceRootProperties.add("app/middleware"); // NOI18N } if (reports) { sourceRootProperties.add("app/reports"); // NOI18N } sourceRootProperties.add("app/models"); // NOI18N sourceRootProperties.add("app/views"); // NOI18N // Add in other dirs we don't know about FileObject app = fo.getFileObject("app"); // NOI18N if (app != null) { Set<String> knownAppDirs = new HashSet<String>(); knownAppDirs.add("controllers"); // NOI18N knownAppDirs.add("helpers"); // NOI18N knownAppDirs.add("models"); // NOI18N knownAppDirs.add("views"); // NOI18N knownAppDirs.add("reports"); // NOI18N knownAppDirs.add("metal"); // NOI18N knownAppDirs.add("mails"); // NOI18N List<String> missing = findUnknownFolders(app, knownAppDirs); if (missing != null) { for (String name : missing) { String combinedName = "app/" + name; // NOI18N sourceRootNames.add(combinedName); sourceRootProperties.add(combinedName); } } } sourceRootNames.add(getNodeDescription("components")); // NOI18N sourceRootProperties.add("components"); // NOI18N sourceRootNames.add(getNodeDescription("config")); // NOI18N sourceRootProperties.add("config"); // NOI18N sourceRootNames.add(getNodeDescription("db")); // NOI18N sourceRootProperties.add("db"); // NOI18N sourceRootNames.add(getNodeDescription("lib")); // NOI18N sourceRootProperties.add("lib"); // NOI18N sourceRootNames.add(getNodeDescription("log")); // NOI18N sourceRootProperties.add("log"); // NOI18N sourceRootNames.add(getNodeDescription("public")); // NOI18N sourceRootProperties.add("public"); // NOI18N if (showRSpec) { sourceRootNames.add(getNodeDescription("rspec")); // NOI18N sourceRootProperties.add("spec"); // NOI18N } sourceRootNames.add(getNodeDescription("test")); // NOI18N sourceRootProperties.add("test"); // NOI18N sourceRootNames.add(getNodeDescription("script")); // NOI18N sourceRootProperties.add("script"); // NOI18N sourceRootNames.add(getNodeDescription("doc")); // NOI18N sourceRootProperties.add("doc"); // NOI18N // Vendor is treated specially. // It should be split up into multiple roots that are indexed // as platform (not as sources, thus not rescanned on subsequent startups, // and possibly pulling in preindexed libraries). sourceRootNames.add(getNodeDescription("vendor")); // NOI18N sourceRootProperties.add("vendor"); // NOI18N // Add in other top-level dirs we don't know about if (fo != null) { Set<String> knownTopDirs = new HashSet<String>(); // Deliberately hidden knownTopDirs.add("nbproject"); // NOI18N knownTopDirs.add("tmp"); // NOI18N knownTopDirs.add("app"); // NOI18N knownTopDirs.add("components"); // NOI18N knownTopDirs.add("config"); // NOI18N knownTopDirs.add("db"); // NOI18N knownTopDirs.add("lib"); // NOI18N knownTopDirs.add("log"); // NOI18N knownTopDirs.add("public"); // NOI18N knownTopDirs.add("spec"); // NOI18N knownTopDirs.add("lib"); // NOI18N knownTopDirs.add("test"); // NOI18N knownTopDirs.add("doc"); // NOI18N knownTopDirs.add("script"); // NOI18N knownTopDirs.add("vendor"); // NOI18N List<String> missing = findUnknownFolders(fo, knownTopDirs); if (missing != null) { for (String name : missing) { sourceRootNames.add(name); sourceRootProperties.add(name); } } } //Local caching assert sourceRoots == null; List<FileObject> result = new ArrayList<FileObject>(); for (String p : sourceRootProperties) { FileObject f = helper.getRakeProjectHelper().resolveFileObject(p); if (f == null) { continue; } if (FileUtil.isArchiveFile(f)) { f = FileUtil.getArchiveRoot(f); } result.add(f); } sourceRoots = Collections.unmodifiableList(result); // assert sourceRootNames.size() == sourceRootProperties.size() && // sourceRootNames.size() == sourceRoots.size(); } /** Look in the given directory and identify any folders we don't "know" about yet */ private static List<String> findUnknownFolders(FileObject folder, Set<String> known) { List<String> result = null; for (FileObject child : folder.getChildren()) { if (child.isFolder()) { String name = child.getNameExt(); if (!known.contains(name) && isVisible(child)) { if (result == null) { result = new ArrayList<String>(); } result.add(name); } } } if (result != null) { Collections.sort(result); } return result; } /** * XXX - copy-pasted from o.n.core.ui.options.filetypes.IgnoredFilesPreferences. * Take a look into {@link #isVisible()}. * <p/> * Default ignored files pattern. Pattern \.(cvsignore|svn|DS_Store) is covered by ^\..*$ */ private static final String DEFAULT_IGNORED_FILES = "^(CVS|SCCS|vssver.?\\.scc|#.*#|%.*%|_svn)$|~$|^\\.(?!htaccess$).*$"; //NOI18N private static boolean isVisible(final FileObject child) { // XXX should use VisibilityQuery#isVisible, but can't in this way. // See http://www.netbeans.org/nonav/issues/show_bug.cgi?id=119244 return !child.getNameExt().matches(DEFAULT_IGNORED_FILES); } /** * Initialize source roots to just match the Rails view. * Note that my load path will be way wrong for unit test execution and such - and * possibly for require-indexing (for require completion) */ private void initializeRootsFiles() { FileObject fo = helper.getRakeProjectHelper().getProjectDirectory(); if (fo == null) { initializeRootsLogical(); return; } assert sourceRoots == null; List<FileObject> result = new ArrayList<FileObject>(20); sourceRootNames = new ArrayList<String>(20); sourceRootProperties = new ArrayList<String>(20); plainFiles = new ArrayList<FileObject>(20); FileObject[] children = fo.getChildren(); for (FileObject f : children) { if (!VisibilityQuery.getDefault().isVisible(f)) { continue; } if (FileUtil.isArchiveFile(f)) { f = FileUtil.getArchiveRoot(f); } if (f.isFolder()) { String name = f.getName(); // Deliberately skipped if (name.equals("nbproject") || name.equals("tmp")) { // NOI18N continue; } result.add(f); } else { plainFiles.add(f); } } // Sort files alphabetically Collections.sort(result, new Comparator<FileObject>() { public int compare(FileObject f1, FileObject f2) { return f1.getNameExt().compareTo(f2.getNameExt()); } }); for (FileObject f : result) { String name = f.getNameExt(); sourceRootNames.add(name); sourceRootProperties.add(name); } sourceRoots = Collections.unmodifiableList(result); } /** * Returns the display names of soruce roots * The returned array has the same length as an array returned by the getRootProperties. * It may contain empty strings but not null. * @return an array of String */ public String[] getRootNames () { return ProjectManager.mutex().readAccess(new Mutex.Action<String[]>() { public String[] run() { synchronized (SourceRoots.this) { if (sourceRootNames == null) { initializeRoots(); } assert sourceRootNames != null; return sourceRootNames.toArray(new String[sourceRootNames.size()]); } } }); } /** * Returns names of Ant properties in the project.properties file holding the source roots. * @return an array of String */ public String[] getRootProperties () { return ProjectManager.mutex().readAccess(new Mutex.Action<String[]>() { public String[] run() { synchronized (SourceRoots.this) { if (sourceRootProperties == null) { initializeRoots(); } assert sourceRootProperties != null; return sourceRootProperties.toArray(new String[sourceRootProperties.size()]); } } }); } /** * Returns the source roots * @return an array of FileObject */ public FileObject[] getRoots () { return ProjectManager.mutex().readAccess(new Mutex.Action<FileObject[]>() { public FileObject[] run () { synchronized (SourceRoots.this) { initializeRoots(); assert sourceRoots != null; return sourceRoots.toArray(new FileObject[sourceRoots.size()]); } } }); } /** * Returns the extra files in the root dir (not corresponding to source folders) * @return an array of FileObject */ public FileObject[] getExtraFiles () { return ProjectManager.mutex().readAccess(new Mutex.Action<FileObject[]>() { public FileObject[] run () { synchronized (SourceRoots.this) { initializeRoots(); assert plainFiles != null; return plainFiles.toArray(new FileObject[plainFiles.size()]); } } }); } /** * Returns the source roots as URLs. * @return an array of URL */ public URL[] getRootURLs() { return ProjectManager.mutex().readAccess(new Mutex.Action<URL[]>() { public URL[] run () { synchronized (this) { //Local caching if (sourceRootURLs == null) { String[] srcProps = getRootProperties(); List<URL> result = new ArrayList<URL>(); for (int i = 0; i<srcProps.length; i++) { String prop = srcProps[i]; if (prop != null) { File f = helper.getRakeProjectHelper().resolveFile(prop); try { URL url = f.toURI().toURL(); if (!f.exists()) { url = new URL(url.toExternalForm() + "/"); // NOI18N } result.add(url); } catch (MalformedURLException e) { ErrorManager.getDefault().notify(e); } } } sourceRootURLs = Collections.unmodifiableList(result); } } return sourceRootURLs.toArray(new URL[sourceRootURLs.size()]); } }); } /** * Adds PropertyChangeListener * @param listener */ public void addPropertyChangeListener (PropertyChangeListener listener) { this.support.addPropertyChangeListener (listener); } /** * Removes PropertyChangeListener * @param listener */ public void removePropertyChangeListener (PropertyChangeListener listener) { this.support.removePropertyChangeListener (listener); } /** * Translates root name into display name of source/test root * @param rootName the name of root got from {@link SourceRoots#getRootNames} * @param propName the name of property the root is stored in * @return the label to be displayed */ public String getRootDisplayName (String rootName, String propName) { if (rootName == null || rootName.length() ==0) { // //If the prop is src.dir use the default name // if (isTest && RailsProjectGenerator.DEFAULT_TEST_SRC_NAME.equals(propName)) { //NOI18N // rootName = DEFAULT_TEST_LABEL; // } // else if (!isTest && RailsProjectGenerator.DEFAULT_SRC_NAME.equals(propName)) { //NOI18N // rootName = DEFAULT_SOURCE_LABEL; // } // else { //If the name is not given, it should be either a relative path in the project dir //or absolute path when the root is not under the project dir String propValue = evaluator.getProperty(propName); File sourceRoot = propValue == null ? null : helper.getRakeProjectHelper().resolveFile(propValue); rootName = createInitialDisplayName(sourceRoot); } // } return rootName; } /** * Creates initial display name of source/test root * @param sourceRoot the source root * @return the label to be displayed */ public String createInitialDisplayName (File sourceRoot) { String rootName; if (sourceRoot != null) { String srPath = sourceRoot.getAbsolutePath(); String pdPath = projectDir.getAbsolutePath() + File.separatorChar; if (srPath.startsWith(pdPath)) { rootName = srPath.substring(pdPath.length()); } else { rootName = sourceRoot.getAbsolutePath(); } } else { rootName = isTest ? DEFAULT_TEST_LABEL : DEFAULT_SOURCE_LABEL; } return rootName; } /** * Returns true if this SourceRoots instance represents source roots belonging to * the tests compilation unit. * @return boolean */ public boolean isTest () { return this.isTest; } private void resetCache (boolean isXMLChange, String propName) { boolean fire = false; synchronized (this) { //In case of change reset local cache if (isXMLChange) { this.sourceRootProperties = null; this.sourceRootNames = null; this.sourceRoots = null; this.sourceRootURLs = null; fire = true; } else if (propName == null || (sourceRootProperties != null && sourceRootProperties.contains(propName))) { this.sourceRoots = null; this.sourceRootURLs = null; fire = true; } } if (fire) { if (isXMLChange) { this.support.firePropertyChange (PROP_ROOT_PROPERTIES,null,null); } this.support.firePropertyChange (PROP_ROOTS,null,null); } } // private void readProjectMetadata () { // Element cfgEl = helper.getPrimaryConfigurationData(true); // NodeList nl = cfgEl.getElementsByTagNameNS(RailsProjectType.PROJECT_CONFIGURATION_NAMESPACE, elementName); // assert nl.getLength() == 0 || nl.getLength() == 1 : "Illegal project.xml"; //NOI18N // List<String> rootProps = new ArrayList<String>(); // List<String> rootNames = new ArrayList<String>(); // // It can be 0 in the case when the project is created by RailsProjectGenerator and not yet customized // if (nl.getLength()==1) { // NodeList roots = ((Element)nl.item(0)).getElementsByTagNameNS(RailsProjectType.PROJECT_CONFIGURATION_NAMESPACE, "root"); //NOI18N // for (int i=0; i<roots.getLength(); i++) { // Element root = (Element) roots.item(i); // String value = root.getAttribute("id"); //NOI18N // assert value.length() > 0 : "Illegal project.xml"; // rootProps.add(value); // value = root.getAttribute("name"); //NOI18N // rootNames.add (value); // } // } // this.sourceRootProperties = Collections.unmodifiableList(rootProps); // this.sourceRootNames = Collections.unmodifiableList(rootNames); // } private class ProjectMetadataListener implements PropertyChangeListener,RakeProjectListener { public void propertyChange(PropertyChangeEvent evt) { resetCache (false,evt.getPropertyName()); } public void configurationXmlChanged(RakeProjectEvent ev) { resetCache (true,null); } public void propertiesChanged(RakeProjectEvent ev) { //Handled by propertyChange } } }