(ch.length);
+ }
+
+ result.add(new RevealedFileObject(mask, hiddenFo));
+ break;
+ }
+ }
+ }
+ if (result == null) {
+ return Collections.emptyList();
+ } else {
+ return result;
+ }
+ }
}
diff --git a/openide.filesystems/src/org/openide/filesystems/MultiFileSystem.java b/openide.filesystems/src/org/openide/filesystems/MultiFileSystem.java
--- a/openide.filesystems/src/org/openide/filesystems/MultiFileSystem.java
+++ b/openide.filesystems/src/org/openide/filesystems/MultiFileSystem.java
@@ -122,6 +122,17 @@
* be able to mask files not provided by their immediate siblings, but by cousins. For this reason, nested
* subclasses may call {@link #setPropagateMasks} to make the mask files propagate up one or more nesting levels
* and thus remain potent against cousin delegates.
+ *
+ * To support rollback, two pseudo-attribute is defined since 8.5: {@code revealEntries} typed
+ * as Map<String, FileObject & Callable>
. The attribute is available on a folder, and contains information
+ * on child FileObjects, which have been overriden or masked by the writable layer of the MFS. Map is keyed by
+ * child name, the value is a FileObject that allows access to the masked child attributes and/or content.
+ *
+ * The returned FileObjects do not leak its neighbours from the lower layers. The parent, children or siblings are
+ * returned from the MFS, if they exist. The FileObjects can be also casted to Callable<FileObject>
. When
+ * called, the original version of the file is restored on the MultiFileSystem, and the restored instance is returned
+ * as result.
+ *
*/
public class MultiFileSystem extends FileSystem {
static final long serialVersionUID = -767493828111559560L;
diff --git a/openide.filesystems/test/unit/src/org/openide/filesystems/MultiFileSystemRevealTest.java b/openide.filesystems/test/unit/src/org/openide/filesystems/MultiFileSystemRevealTest.java
new file mode 100644
--- /dev/null
+++ b/openide.filesystems/test/unit/src/org/openide/filesystems/MultiFileSystemRevealTest.java
@@ -0,0 +1,254 @@
+/*
+ * 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.OutputStreamWriter;
+import java.util.Collection;
+import java.util.concurrent.Callable;
+import org.netbeans.junit.NbTestCase;
+
+/**
+ * Assures behaviour the 'revealEntries' pseudo-attribute
+ * @author sdedic
+ */
+public class MultiFileSystemRevealTest extends NbTestCase {
+
+ public MultiFileSystemRevealTest(String name) {
+ super(name);
+ }
+
+ /**
+ * In the case of no user modifications, empty Collection is provided
+ * @throws Exception
+ */
+ public void testNoUserModifications() throws Exception {
+ MultiFileSystem fs = new MultiFileSystem(
+ TestUtilHid.createLocalFileSystem(getName() + "1", new String[0]),
+ TestUtilHid.createXMLFileSystem(getName() + "2", new String[] {
+ "folder/file1",
+ "folder/file2",
+ }),
+ TestUtilHid.createXMLFileSystem(getName() + "3", new String[] {
+ "folder/file1",
+ "folder/file3",
+ })
+ );
+ Collection fos = (Collection)fs.findResource("folder").getAttribute("revealEntries");
+ assertTrue(fos.isEmpty());
+ }
+
+ /**
+ * Checks that for a deleted file, a FileObject is produced.
+ * file1 and file3 are deleted from different layers, then undeleted.
+ *
+ * @throws Exception
+ */
+ public void testFileDeleted() throws Exception {
+ TestUtilHid.Resource root2 = TestUtilHid.createRoot();
+ root2.add("folder/file2");
+ root2.add("folder/file1").addAttribute("name", "stringvalue", "jouda");
+
+ TestUtilHid.Resource root3 = TestUtilHid.createRoot();
+ root3.add("folder/file1");
+ root3.add("folder/file3").addAttribute("name", "stringvalue", "bubak");
+
+ MultiFileSystem fs = new MultiFileSystem(
+ TestUtilHid.createLocalFileSystem(getName() + "1", new String[] {
+ "folder/file1_hidden",
+ "folder/file3_hidden",
+ }),
+ TestUtilHid.createXMLFileSystem(getName() + "2", root2),
+ TestUtilHid.createXMLFileSystem(getName() + "3", root3)
+ );
+ Collection fos = (Collection)fs.findResource("folder").getAttribute("revealEntries");
+ FileObject f = findFile(fos, "file1");
+ assertNotNull("Delete file1 should be revealed", f);
+ assertTrue(f instanceof Callable);
+ assertEquals("folder/file1", f.getPath());
+ assertSame(fs, f.getFileSystem());
+ assertNotNull(f.getParent().getFileObject("file2"));
+ assertNull(f.getParent().getFileObject("file3"));
+
+ assertNotNull(f.getAttribute("name"));
+ assertEquals("jouda", f.getAttribute("name"));
+
+ FileObject f2 = findFile(fos, "file3");
+ assertNotNull("Delete file1 should be revealed", f);
+ assertTrue(f2 instanceof Callable);
+ assertEquals("folder/file3", f2.getPath());
+ assertSame(fs, f2.getFileSystem());
+ assertNotNull(f2.getParent().getFileObject("file2"));
+ assertNull(f2.getParent().getFileObject("file1"));
+
+ assertNotNull(f2.getAttribute("name"));
+ assertEquals("bubak", f2.getAttribute("name"));
+
+ // try to un-delete the files:
+ ((Callable)f2).call();
+ ((Callable)f).call();
+
+ assertEquals(3, fs.findResource("folder").getChildren().length);
+ }
+
+ private FileObject findFile(Collection files, String name) {
+ for (FileObject f : files) {
+ if (f.getNameExt().equals(name)) {
+ return f;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * For a changed file, FileObject with the original content should
+ * be produced.
+ * @throws Exception
+ */
+ public void testFileChangedAttributesAndContent() throws Exception {
+ TestUtilHid.Resource root2 = TestUtilHid.createRoot();
+ root2.add("folder/file2");
+ root2.add("folder/file1").withContent("Povidam povidam pohadku");
+
+ TestUtilHid.Resource root3 = TestUtilHid.createRoot();
+ root3.add("folder/file1");
+ root3.add("folder/file3").addAttribute("name", "stringvalue", "jouda");
+
+ FileSystem lfs = TestUtilHid.createLocalFileSystem(getName() + "1", new String[] {
+ "folder/file1",
+ "folder/file3",
+ });
+ FileObject local1 = lfs.findResource("folder/file1");
+ OutputStreamWriter wr = new OutputStreamWriter(local1.getOutputStream());
+ wr.append("o cervenem kotatku");
+ wr.close();
+
+ FileObject local3 = lfs.findResource("folder/file3");
+ local3.setAttribute("test", "failed");
+ local3.setAttribute("name", "truhlik");
+
+ MultiFileSystem fs = new MultiFileSystem(
+ lfs,
+ TestUtilHid.createXMLFileSystem(getName() + "2", root2),
+ TestUtilHid.createXMLFileSystem(getName() + "3", root3)
+ );
+
+ Collection fos = (Collection)fs.findResource("folder").getAttribute("revealEntries");
+ FileObject f = findFile(fos, "file1");
+ assertNotNull("Delete file1 should be revealed", f);
+ assertTrue(f instanceof Callable);
+ assertEquals("Povidam povidam pohadku", f.asText());
+ assertEquals("folder/file1", f.getPath());
+ assertSame(fs, f.getFileSystem());
+ assertNotNull(f.getParent().getFileObject("file2"));
+
+ FileObject f2 = findFile(fos, "file3");
+ assertNotNull("Delete file1 should be revealed", f);
+ assertTrue(f2 instanceof Callable);
+ assertEquals("folder/file3", f2.getPath());
+ assertSame(fs, f2.getFileSystem());
+ assertNotNull(f2.getParent().getFileObject("file2"));
+
+ assertNull(f2.getAttribute("test"));
+ assertEquals("jouda", f2.getAttribute("name"));
+
+ FileObject multiFile1 = fs.findResource("folder/file1");
+ FileObject multiFile3 = fs.findResource("folder/file3");
+
+ assertEquals("o cervenem kotatku", multiFile1.asText());
+ assertEquals("truhlik", multiFile3.getAttribute("name"));
+
+ // try to un-delete the files:
+ assertSame(multiFile3, ((Callable)f2).call());
+ assertSame(multiFile1, ((Callable)f).call());
+
+ assertEquals("Povidam povidam pohadku", multiFile1.asText());
+ assertEquals("jouda", multiFile3.getAttribute("name"));
+
+ assertEquals(3, fs.findResource("folder").getChildren().length);
+ }
+
+ /**
+ * No reveal entry should be present for user-only modification
+ * @throws Exception
+ */
+ public void testUserOnlyContent() throws Exception {
+ MultiFileSystem fs = new MultiFileSystem(
+ TestUtilHid.createLocalFileSystem(getName() + "1", new String[] {
+ "folder/file4",
+ "folder/file5",
+ }),
+ TestUtilHid.createXMLFileSystem(getName() + "2", new String[] {
+ "folder/file1",
+ "folder/file2",
+ }),
+ TestUtilHid.createXMLFileSystem(getName() + "3", new String[] {
+ "folder/file1",
+ "folder/file3",
+ })
+ );
+ Collection fos = (Collection)fs.findResource("folder").getAttribute("revealEntries");
+ assertTrue(fos.isEmpty());
+ }
+
+ /**
+ * No reveal entry should be present for user-only modification
+ * @throws Exception
+ */
+ public void testNotWritableLayer() throws Exception {
+ MultiFileSystem fs = new MultiFileSystem(
+ TestUtilHid.createXMLFileSystem(getName() + "1", new String[] {
+ "folder/file1",
+ "folder/file3",
+ }),
+ TestUtilHid.createXMLFileSystem(getName() + "2", new String[] {
+ "folder/file1",
+ "folder/file2",
+ }),
+ TestUtilHid.createXMLFileSystem(getName() + "3", new String[] {
+ "folder/file1",
+ "folder/file3",
+ })
+ );
+ Collection fos = (Collection)fs.findResource("folder").getAttribute("revealEntries");
+ assertNull("Readonly MFS does not support revealEntries", fos);
+ }
+}
diff --git a/openide.filesystems/test/unit/src/org/openide/filesystems/TestUtilHid.java b/openide.filesystems/test/unit/src/org/openide/filesystems/TestUtilHid.java
--- a/openide.filesystems/test/unit/src/org/openide/filesystems/TestUtilHid.java
+++ b/openide.filesystems/test/unit/src/org/openide/filesystems/TestUtilHid.java
@@ -46,6 +46,7 @@
import java.io.File;
import java.io.FileOutputStream;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URL;
@@ -182,8 +183,19 @@
return xfs;
}
-
- public static File createXMLLayer(String testName, String[] resources) throws IOException {
+
+ public final static FileSystem createXMLFileSystem(String testName, Resource rootResource) throws IOException {
+ File xmlFile = createXMLLayer(testName, rootResource);
+
+ XMLFileSystem xfs = new XMLFileSystem ();
+ try {
+ xfs.setXmlUrl(Utilities.toURI(xmlFile).toURL());
+ } catch (Exception ex) {}
+
+ return xfs;
+ }
+
+ public static File createXMLLayer(String testName, Resource rootResource) throws IOException {
File tempFile = TestUtilHid.locationOfTempFolder("xfstest");
tempFile.mkdir();
@@ -193,17 +205,25 @@
xmlFile.createNewFile();
}
FileOutputStream xos = new FileOutputStream (xmlFile);
- ResourceElement root = new ResourceElement ("");
+ PrintWriter pw = new PrintWriter (xos);
+ pw.println("");
+ rootResource.serialize("", " ", pw, xmlFile.getParentFile());
+ pw.println("");
+ pw.close();
+ return xmlFile;
+ }
+
+ public static File createXMLLayer(String testName, String[] resources) throws IOException {
+ Resource root = createRoot();
for (int i = 0; i < resources.length; i++)
root.add (resources[i]);
-
- PrintWriter pw = new PrintWriter (xos);
- pw.println("");
- testStructure (pw,root.getChildren () ," ");
- pw.println("");
- pw.close();
- return xmlFile;
+
+ return createXMLLayer(testName, root);
+ }
+
+ public static Resource createRoot() {
+ return new Resource("");
}
private static void testStructure (PrintWriter pw,ResourceElement[] childern,String tab) {
@@ -223,25 +243,28 @@
}
}
- private static class ResourceElement {
+ static class ResourceElement {
String element;
ResourceElement (String element) {
//System.out.println(element);
this.element = element;
}
Map children = new HashMap ();
- void add (String resource) {
- add (new StringTokenizer (resource,"/"));
+ Resource add (String resource) {
+ return add (new StringTokenizer (resource,"/"));
}
- private void add (Enumeration en) {
+
+ Resource add (Enumeration en) {
//StringTokenizer tokens = StringTokenizer (resource);
if (en.hasMoreElements()) {
String chldElem = (String)en.nextElement();
- ResourceElement child = (ResourceElement)children.get(chldElem);
+ Resource child = (Resource)children.get(chldElem);
if (child == null)
- child = new ResourceElement(chldElem);
+ child = new Resource(chldElem);
children.put (chldElem,child);
- child.add (en);
+ return child.add (en);
+ } else {
+ return (Resource)this;
}
}
ResourceElement[] getChildren () {
@@ -258,6 +281,14 @@
String getName () {
return element;
}
+
+ boolean isLeaf() {
+ return children.isEmpty();
+ }
+
+ public void serialize(String path, String tab, PrintWriter pw, File baseFolder) throws IOException {
+
+ }
}
static class StatusFileSystem extends LocalFileSystem {
@@ -276,4 +307,89 @@
}
}
+
+ public static class Resource extends ResourceElement {
+ private Map attributeTypes = new HashMap();
+ private Map attributeContents = new HashMap();
+ private CharSequence fileContents = null;
+ private URL contentURL = null;
+ private boolean forceFolder;
+
+ public Resource(String element) {
+ super(element);
+ }
+
+ public Resource addAttribute(String name, String type, String valueContent) {
+ attributeTypes.put(name, type);
+ attributeContents.put(name, valueContent);
+
+ return this;
+ }
+
+ public Resource withContent(CharSequence seq) {
+ this.fileContents = seq;
+
+ return this;
+ }
+
+ public Resource withContentAt(URL contentURL) {
+ this.contentURL = contentURL;
+ return this;
+ }
+
+ public Resource forceFolder() {
+ this.forceFolder = true;
+ return this;
+ }
+
+ public boolean isLeaf() {
+ return !forceFolder && super.isLeaf();
+ }
+
+ public void serialize(String path, String tab, PrintWriter pw, File baseFolder) throws IOException {
+ if (!getName().isEmpty()) {
+ String n = getName();
+ if (isLeaf()) {
+ pw.print(tab+"");
+
+ for (String s : attributeContents.keySet()) {
+ pw.print("\n" + tab + " ");
+ }
+ pw.println();
+ }
+ String newPath = path + getName();
+ if (!newPath.isEmpty()) {
+ newPath = newPath + "/";
+ }
+ for (ResourceElement res : children.values()) {
+ res.serialize(newPath, tab + " ", pw, baseFolder);
+ }
+ if (!getName().isEmpty()) {
+ if (isLeaf()) {
+ pw.println(tab+"" );
+ } else {
+ pw.println(tab+"" );
+ }
+ }
+ }
+ }
}