diff -r c3b7c6f096ef openide.filesystems/apichanges.xml --- a/openide.filesystems/apichanges.xml Tue Jan 10 12:13:19 2012 +0100 +++ b/openide.filesystems/apichanges.xml Thu Jan 12 18:28:03 2012 +0100 @@ -49,6 +49,25 @@ Filesystems API + + + Support for time stamps on subtrees of files + + + + + +

+ Added new class for communication between + Parsing API + and file systems that are capable to record revision of + a file tree (like version control systems and Mac OS X file + system). +

+
+ + +
Introduced FileObject.revert diff -r c3b7c6f096ef openide.filesystems/arch.xml --- a/openide.filesystems/arch.xml Tue Jan 10 12:13:19 2012 +0100 +++ b/openide.filesystems/arch.xml Thu Jan 12 18:28:03 2012 +0100 @@ -874,6 +874,14 @@ about dynamically changing the system filesystem.

+ +

Since version 7.56 it is possible to talk to various filesystems and + let them record a state of files under given root effectively. Later + one can query the changes that happened meanwhile. For more information + see the documentation to the TreeStamp + class. +

+
diff -r c3b7c6f096ef openide.filesystems/manifest.mf --- a/openide.filesystems/manifest.mf Tue Jan 10 12:13:19 2012 +0100 +++ b/openide.filesystems/manifest.mf Thu Jan 12 18:28:03 2012 +0100 @@ -2,5 +2,5 @@ OpenIDE-Module: org.openide.filesystems OpenIDE-Module-Localizing-Bundle: org/openide/filesystems/Bundle.properties OpenIDE-Module-Layer: org/openide/filesystems/resources/layer.xml -OpenIDE-Module-Specification-Version: 7.55 +OpenIDE-Module-Specification-Version: 7.56 diff -r c3b7c6f096ef openide.filesystems/src/org/openide/filesystems/TreeStamp.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/openide.filesystems/src/org/openide/filesystems/TreeStamp.java Thu Jan 12 18:28:03 2012 +0100 @@ -0,0 +1,266 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2012 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]" + * + * 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. + * + * Contributor(s): + * + * Portions Copyrighted 2012 Sun Microsystems, Inc. + */ +package org.openide.filesystems; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** Provides support for snapshot over tree of files on a file system. The + * Parsing API + * uses this information (if available) to keep track about the change files + * under given source root. Typical usage involves obtaining the stamp: + *
TreeStamp stamp = TreeStamp.{@link #findStamp(org.openide.filesystems.FileObject) findStamp(root)}
+ * persisting it (either via serialization or by storing {@link #getId() base id} and + * {@link #getModified() list of modified files} manually). Later, during next + * start one can ask for: + *
{@code Set} changedSince = TreeStamp.{@link #findModified(org.openide.filesystems.FileObject, java.lang.String) findModified(root, stamp.getId())}
+ * The set of changed files is then obtained by + * unifying {@link #getModified() stamp.getModified()} and changedSince sets. + *

+ * After creation, this class is unmodifiable and thread safe for access. + * + * @author Jaroslav Tulach + * @since 7.56 + */ +public final class TreeStamp implements java.io.Serializable { + static final long serialVersionId = 45903850438L; + private static final String[] EMPTY = new String[0]; + + private final String id; + private final String series; + private final String[] modified; + + private TreeStamp( + String id, String series, Set modified + ) { + this.id = id; + this.series = series; + this.modified = modified == null ? EMPTY : modified.toArray(EMPTY); + } + + /** The identification of the tree of files. In case of Mercurial that would be + * the current change set of the repository. In case of Subversion the + * revision of the repository. Some files may differ from this ideal + * id, those are then enumerated in {@link #getModified()} list. + * + * @return string identifying the tree + */ + public String getId() { + return id; + } + + /** Branch or other stable identification of the tree. Some + * systems (like Mercurial) often switch from one revision of sources + * to another while the files remain on the same place. In some situations + * the Parsing API + * should realize that in spite the files stay on the same place, + * they are in fact supposed to be completely different and that they + * deserve separate storage. + * + * @return null or identification of a sequence of {@link #getId() ids} + */ + public String getSeries() { + return series; + } + + /** Usually the sources slightly differ with respect to {@link #getId()}. + * In such situation the {@link TreeStamp stamp} should contain enumeration + * of the file names that are different. + * + * @return immutable collection with relative paths to files that differ + * from the {@link #getId() baseId} + */ + public Set getModified() { + return Collections.unmodifiableSet(new LinkedHashSet(Arrays.asList(modified))); + } + + /** Computes {@link TreeStamp time stamp} for the files under given + * root. Locates appropriate {@link Provider} and asks it to compute + * the stamp. If the {@link Provider} refuses to compute it, the + * return value may be null. + * + * @param rootOfTree the root to compute stamp for + * @return null or snapshot of the state under the root + */ + public static TreeStamp findStamp(FileObject rootOfTree) { + return findStamp(Collections.singleton(rootOfTree)).get(rootOfTree); + } + + /** A batch search for time stamps. Locates appropriate {@link #Provider} for + * each root. Groups them together and asks them to compute a stamp. + * + * + * @param roots the roots to compute stamp for + * @return map of roots and their appropriate stamps. It is possible + * that some of the roots do not have their {@link TreeStamp} in the + * map - a sign that no provider decided to compute one + */ + public static Map findStamp( + Collection roots + ) { + Map snapshots = new LinkedHashMap(); + for (Map.Entry> entry : groups(roots).entrySet()) { + Map map = entry.getKey().snapshot(entry.getValue()); + if (map != null) { + snapshots.putAll(map); + } + } + return Collections.unmodifiableMap(snapshots); + } + + /** Computes the {@link FileObject files} that + * differ from the recorded {@link #getId() baseId} + * recorded last time. + * + * @param root + * @param baseId + * @return immutable set of relative paths of files that differ from baseId + * @throws IOException if the operation fails on I/O or if the baseId cannot + * be understood + */ + public static Set findModified(FileObject root, String baseId) + throws IOException { + Set ret = findModified(Collections.singletonMap(root, baseId)).get(root); + if (ret == null) { + throw new IOException("Nothing computed for " + root); + } + return ret; + } + + /** Batch operation to find modified files under provided roots. The input + * is the map of roots and their {@link #getId() baseIds}. The system + * finds provider for each root, groups them for effectiveness and then + * ask each of the provider to find differences since the previous base. + * + * @param rootsAndIds map from a root to its associated {@link #getId() baseId} + * @return map from a subset of provided roots to set of relative paths that + * changed under their tree + */ + public static Map> findModified( + Map rootsAndIds + ) { + Map> modified = new LinkedHashMap>(); + for (Map.Entry> entry : groups(rootsAndIds.keySet()).entrySet()) { + Map copy = new LinkedHashMap(rootsAndIds); + copy.keySet().retainAll(entry.getValue()); + Map> ret = entry.getKey().differences(copy); + if (ret != null) { + modified.putAll(ret); + } + } + return modified; + } + + private static Map> groups(Collection roots) { + Map> groups = new IdentityHashMap>(); + for (FileObject fo : roots) { + Object p = fo.getAttribute(Provider.class.getName()); + if (p instanceof Provider) { + Provider provider = (Provider) p; + Set arr = groups.get(provider); + if (arr == null) { + arr = new LinkedHashSet(); + groups.put(provider, arr); + } + arr.add(fo); + } + } + return groups; + } + + /** Class that can be provided by a file system, if it has a capability + * to capture {@link TreeStamp} for some folders. Each folder that is + * capable to provide {@link TreeStamp} should respond to + *

fo.getAttribute(Provider.class.getName())
+ * by returning a shared instance of the {@link Provider}. In case + * of batch operations the providers are compared using == + * when doing {@link #findStamp(java.util.Collection) grouping}, that is + * why usually return the same instance for each folder of the same type. + */ + public static abstract class Provider { + /** Constructor for subclasses */ + protected Provider() { + } + + /** Factory method to create a {@link TreeStamp} based on its + * {@link TreeStamp#getId() baseId}, {@link TreeStamp#getSeries() series identification} + * and set of {@link TreeStamp#getModified() modified files}. + * + * @param id base ID. + * @param series series ID. May be null. + * @param modified set of relative names of modified files. May be null if + * no files are changed. + * @return immutable instance of tree stamp + */ + protected final TreeStamp create(String id, String series, Set modified) { + return new TreeStamp(id, series, modified); + } + + /** If the provider is capable to compute stamp for all files under provided root, + * it should {@link #create(java.lang.String, java.lang.String, java.util.Set) create a + * stamp} object and return a mapping between individual roots and such + * {@link TreeStamp} instances. + * + * @param roots the queried roots + * @return null or map from subset of roots to {@link TreeStamp} + */ + protected abstract Map snapshot(Set roots); + + /** If the provider recognized provide {@link TreeStamp#getId() baseId} for a root, + * it should try to check what changes happened compared to such baseId. + * If the baseId cannot be recognized or the differences cannot be + * obtained for a root, the root should not be present in the returned map. + * + * @param rootsAndIds the queried roots and their {@link TreeStamp#getId() baseIds} + * @return null or map from subset of roots to + * relative names of files that changed under the root + */ + protected abstract Map> differences(Map rootsAndIds); + } +} diff -r c3b7c6f096ef openide.filesystems/test/unit/src/org/openide/filesystems/TreeStampTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/openide.filesystems/test/unit/src/org/openide/filesystems/TreeStampTest.java Thu Jan 12 18:28:03 2012 +0100 @@ -0,0 +1,226 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2012 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]" + * + * 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. + * + * Contributor(s): + * + * Portions Copyrighted 2012 Sun Microsystems, Inc. + */ +package org.openide.filesystems; + +import java.beans.PropertyVetoException; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.netbeans.junit.NbTestCase; + +/** Tests finding the {@link TreeStamp.Provider} and the behavior of its + * grouping. + * + * @author Jaroslav Tulach + */ +public class TreeStampTest extends NbTestCase { + private LFSWithProvider fs1; + private LFSWithProvider fs2; + + public TreeStampTest(String n) { + super(n); + } + + + @Override + protected void setUp() throws IOException, PropertyVetoException { + clearWorkDir(); + File r1 = new File(getWorkDir(), "root1"); + File r2 = new File(getWorkDir(), "root2"); + + r1.mkdirs(); + r2.mkdirs(); + + fs1 = new LFSWithProvider(r1); + fs2 = new LFSWithProvider(r2); + } + + public void testFindStampIndividualCalls() throws Exception { + fs1.provider = new LFSProvider(); + fs2.provider = new LFSProvider(); + fs1.provider.myId = "alone"; + fs2.provider.myId = "lonely"; + + final List roots = Arrays.asList(fs1.getRoot(), fs2.getRoot()); + Map res = TreeStamp.findStamp(roots); + assertNotNull("Map returned", res); + assertEquals("Two elements there", 2, res.size()); + assertEquals("grouping is the id1", "alone", res.get(fs1.getRoot()).getId()); + assertEquals("grouping is the id2", "lonely", res.get(fs2.getRoot()).getId()); + + assertEquals("Individual queries1", 1, fs1.provider.roots.size()); + assertEquals("Individual queries2", 1, fs2.provider.roots.size()); + assertEquals("Root fs queried1", fs1.getRoot(), fs1.provider.roots.iterator().next()); + assertEquals("Root fs queried2", fs2.getRoot(), fs2.provider.roots.iterator().next()); + } + + public void testFindStampGrouping() throws Exception { + final LFSProvider p = new LFSProvider(); + fs1.provider = p; + fs2.provider = p; + p.myId = "grouping"; + + final List roots = Arrays.asList(fs1.getRoot(), fs2.getRoot()); + Map res = TreeStamp.findStamp(roots); + assertNotNull("Map returned", res); + assertEquals("Two elements there", 2, res.size()); + assertEquals("grouping is the id1", "grouping", res.get(fs1.getRoot()).getId()); + assertEquals("grouping is the id2", "grouping", res.get(fs2.getRoot()).getId()); + + assertEquals("Both roots provided at once", roots, new ArrayList(p.roots)); + } + + public void testFindModifiedIndividualCalls() throws Exception { + fs1.provider = new LFSProvider(); + fs2.provider = new LFSProvider(); + fs1.provider.baseId = "basic"; + fs2.provider.baseId = "extra"; + + final Map rootsAndIds = new HashMap(); + rootsAndIds.put(fs1.getRoot(), "basic"); + rootsAndIds.put(fs2.getRoot(), "extra"); + + Map> res = TreeStamp.findModified(rootsAndIds); + + assertNotNull("Map returned", res); + assertEquals("Two elements there", 2, res.size()); + assertEquals("root1 is modified", Collections.singleton(""), res.get(fs1.getRoot())); + assertEquals("root2 is modified", Collections.singleton(""), res.get(fs2.getRoot())); + + assertEquals("Each provide queried1", 1, fs1.provider.rootsAndIds.size()); + assertEquals("Each provide queried2", 1, fs2.provider.rootsAndIds.size()); + } + + public void testFindModifiedGrouping() throws Exception { + final LFSProvider p = new LFSProvider(); + fs1.provider = p; + fs2.provider = p; + p.baseId = "basic"; + + final Map rootsAndIds = new HashMap(); + rootsAndIds.put(fs1.getRoot(), "different"); + rootsAndIds.put(fs2.getRoot(), "basic"); + + Map> res = TreeStamp.findModified(rootsAndIds); + + assertNotNull("Map returned", res); + assertEquals("One elements there", 1, res.size()); + assertEquals("root is modified", Collections.singleton(""), res.get(fs2.getRoot())); + + assertEquals("Both roots provided at once", rootsAndIds, p.rootsAndIds); + } + + + private static final class LFSWithProvider extends LocalFileSystem + implements AbstractFileSystem.Attr { + LFSProvider provider; + + public LFSWithProvider(File root) throws IOException, PropertyVetoException { + this.attr = this; + setRootDirectory(root); + } + + @Override + public Object readAttribute(String name, String attrName) { + if (TreeStamp.Provider.class.getName().equals(attrName)) { + return provider; + } + return ((Attr)change).readAttribute(name, attrName); + } + + @Override + public void writeAttribute(String name, String attrName, Object value) throws IOException { + ((Attr)change).writeAttribute(name, attrName, value); + } + + @Override + public Enumeration attributes(String name) { + return ((Attr)change).attributes(name); + } + + @Override + public void renameAttributes(String oldName, String newName) { + ((Attr)change).renameAttributes(oldName, newName); + } + + @Override + public void deleteAttributes(String name) { + ((Attr)change).deleteAttributes(name); + } + } + + private static final class LFSProvider extends TreeStamp.Provider { + String myId; + Set roots; + String baseId; + Map rootsAndIds; + + @Override + protected Map snapshot(Set roots) { + assertNull("No call to snapshot yet", this.roots); + this.roots = roots; + + Map map = new HashMap(); + for (FileObject f : roots) { + map.put(f, create(myId, null, null)); + } + return map; + } + + @Override + protected Map> differences(Map rootsAndIds) { + assertNull("No call to differences yet", this.rootsAndIds); + this.rootsAndIds = rootsAndIds; + + Map> map = new HashMap>(); + for (FileObject f : rootsAndIds.keySet()) { + if (baseId.equals(rootsAndIds.get(f))) { + map.put(f, Collections.singleton("")); + } + } + return map; + } + } +}