/* * 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.rubyproject; 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.net.URI; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.HashMap; import java.text.MessageFormat; 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.w3c.dom.Element; import org.w3c.dom.NodeList; import org.w3c.dom.Document; import org.netbeans.modules.ruby.spi.project.support.rake.RakeProjectHelper; 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.openide.util.EditableProperties; /** * 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, "SourceRoots.source.files"); public static final String DEFAULT_TEST_LABEL = NbBundle.getMessage(SourceRoots.class, "SourceRoots.test.files"); public static final String DEFAULT_SPEC_LABEL = NbBundle.getMessage(SourceRoots.class, "SourceRoots.spec.files"); 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<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)); } /** * 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) { readProjectMetadata(); } } 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) { readProjectMetadata(); } 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 (this) { //Local caching if (sourceRoots == null) { String[] srcProps = getRootProperties(); List<FileObject> result = new ArrayList<FileObject>(); for (String p : srcProps) { String prop = evaluator.getProperty(p); if (prop != null) { FileObject f = helper.getRakeProjectHelper().resolveFileObject(prop); if (f == null) { continue; } if (FileUtil.isArchiveFile(f)) { f = FileUtil.getArchiveRoot(f); } result.add(f); } } sourceRoots = Collections.unmodifiableList(result); } } return sourceRoots.toArray(new FileObject[sourceRoots.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 = evaluator.getProperty(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); } /** * Replaces the current roots by the new ones * @param roots the URLs of new roots * @param labels the names of roots */ public void putRoots (final URL[] roots, final String[] labels) { ProjectManager.mutex().writeAccess( new Mutex.Action<Void>() { public Void run() { String[] originalProps = getRootProperties(); URL[] originalRoots = getRootURLs(); Map<URL,String> oldRoots2props = new HashMap<URL,String>(); for (int i=0; i<originalProps.length;i++) { oldRoots2props.put (originalRoots[i],originalProps[i]); } Map<URL,String> newRoots2lab = new HashMap<URL,String>(); for (int i=0; i<roots.length;i++) { newRoots2lab.put (roots[i],labels[i]); } Element cfgEl = helper.getPrimaryConfigurationData(true); NodeList nl = cfgEl.getElementsByTagNameNS(RubyProjectType.PROJECT_CONFIGURATION_NAMESPACE, elementName); assert nl.getLength() == 1 : "Illegal project.xml. Expected exactly one <" + elementName + '>'; //NOI18N Element ownerElement = (Element) nl.item(0); //Remove all old roots NodeList rootsNodes = ownerElement.getElementsByTagNameNS(RubyProjectType.PROJECT_CONFIGURATION_NAMESPACE, "root"); //NOI18N while (rootsNodes.getLength()>0) { Element root = (Element) rootsNodes.item(0); ownerElement.removeChild(root); } //Remove all unused root properties List<URL> newRoots = Arrays.asList(roots); Map<URL,String> propsToRemove = new HashMap<URL,String>(oldRoots2props); propsToRemove.keySet().removeAll(newRoots); EditableProperties props = helper.getProperties(RakeProjectHelper.PROJECT_PROPERTIES_PATH); props.keySet().removeAll(propsToRemove.values()); helper.putProperties(RakeProjectHelper.PROJECT_PROPERTIES_PATH,props); //Add the new roots Document doc = ownerElement.getOwnerDocument(); oldRoots2props.keySet().retainAll(newRoots); for (URL newRoot : newRoots) { String rootName = oldRoots2props.get(newRoot); if (rootName == null) { //Root is new generate property for it props = helper.getProperties(RakeProjectHelper.PROJECT_PROPERTIES_PATH); String[] names = newRoot.getPath().split("/"); //NOI18N rootName = MessageFormat.format(newRootNameTemplate, new Object[] {names[names.length - 1], ""}); // NOI18N int rootIndex = 1; while (props.containsKey(rootName)) { rootIndex++; rootName = MessageFormat.format(newRootNameTemplate, new Object[] {names[names.length - 1], rootIndex}); } File f = FileUtil.normalizeFile(new File(URI.create(newRoot.toExternalForm()))); File projDir = FileUtil.toFile(helper.getRakeProjectHelper().getProjectDirectory()); String path = f.getAbsolutePath(); String prjPath = projDir.getAbsolutePath()+File.separatorChar; if (path.startsWith(prjPath)) { path = path.substring(prjPath.length()); } else { path = refHelper.createForeignFileReference(f, RubyProject.SOURCES_TYPE_RUBY); props = helper.getProperties(RakeProjectHelper.PROJECT_PROPERTIES_PATH); } props.put(rootName,path); helper.putProperties(RakeProjectHelper.PROJECT_PROPERTIES_PATH,props); } Element newRootNode = doc.createElementNS(RubyProjectType.PROJECT_CONFIGURATION_NAMESPACE, "root"); //NOI18N newRootNode.setAttribute("id",rootName); //NOI18N String label = newRoots2lab.get(newRoot); if (label != null && label.length()>0 && !label.equals (getRootDisplayName(null,rootName))) { //NOI18N newRootNode.setAttribute("name",label); //NOI18N } ownerElement.appendChild (newRootNode); } helper.putPrimaryConfigurationData(cfgEl,true); return null; } } ); } /** * 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 && RubyProjectGenerator.DEFAULT_TEST_SRC_NAME.equals(propName)) { //NOI18N rootName = DEFAULT_TEST_LABEL; } else if (isTest && RubyProjectGenerator.DEFAULT_SPEC_SRC_NAME.equals(propName)) { //NOI18N rootName = DEFAULT_SPEC_LABEL; } else if (!isTest && RubyProjectGenerator.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 root (lib/test/spec). * @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 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(RubyProjectType.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 RubyProjectGenerator and not yet customized if (nl.getLength()==1) { NodeList roots = ((Element)nl.item(0)).getElementsByTagNameNS(RubyProjectType.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 } } }