Adapt to MS Windows
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / acr / fs / FsContent.java
index bfcd0118d76e6348fff11911cf71821dec183425..5920c420370c78a4051f9000820af7d16c1c34da 100644 (file)
 package org.argeo.cms.acr.fs;
 
+import java.io.Closeable;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.FileTime;
 import java.nio.file.attribute.UserDefinedFileAttributeView;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.StringJoiner;
+import java.util.concurrent.CompletableFuture;
 
+import javax.xml.XMLConstants;
 import javax.xml.namespace.QName;
+import javax.xml.transform.Source;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamResult;
 
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.ContentName;
 import org.argeo.api.acr.ContentResourceException;
+import org.argeo.api.acr.CrAttributeType;
 import org.argeo.api.acr.CrName;
-import org.argeo.api.acr.spi.AbstractContent;
+import org.argeo.api.acr.DName;
+import org.argeo.api.acr.NamespaceUtils;
+import org.argeo.api.acr.spi.ContentProvider;
 import org.argeo.api.acr.spi.ProvidedContent;
 import org.argeo.api.acr.spi.ProvidedSession;
-import org.argeo.util.FsUtils;
+import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.acr.AbstractContent;
+import org.argeo.cms.acr.ContentUtils;
+import org.argeo.cms.util.FsUtils;
 
+/** Content persisted as a filesystem {@link Path}. */
 public class FsContent extends AbstractContent implements ProvidedContent {
-       private final static String USER_ = "user:";
+       private CmsLog log = CmsLog.getLog(FsContent.class);
+
+       final static String USER_ = "user:";
 
        private static final Map<QName, String> BASIC_KEYS;
        private static final Map<QName, String> POSIX_KEYS;
        static {
                BASIC_KEYS = new HashMap<>();
-               BASIC_KEYS.put(CrName.CREATION_TIME.get(), "basic:creationTime");
-               BASIC_KEYS.put(CrName.LAST_MODIFIED_TIME.get(), "basic:lastModifiedTime");
-               BASIC_KEYS.put(CrName.SIZE.get(), "basic:size");
-               BASIC_KEYS.put(CrName.FILE_KEY.get(), "basic:fileKey");
+               BASIC_KEYS.put(DName.creationdate.qName(), "basic:creationTime");
+               BASIC_KEYS.put(DName.getlastmodified.qName(), "basic:lastModifiedTime");
+               BASIC_KEYS.put(DName.getcontentlength.qName(), "basic:size");
+
+               BASIC_KEYS.put(CrName.fileKey.qName(), "basic:fileKey");
 
                POSIX_KEYS = new HashMap<>(BASIC_KEYS);
-               POSIX_KEYS.put(CrName.OWNER.get(), "owner:owner");
-               POSIX_KEYS.put(CrName.GROUP.get(), "posix:group");
-               POSIX_KEYS.put(CrName.PERMISSIONS.get(), "posix:permissions");
+               POSIX_KEYS.put(DName.owner.qName(), "owner:owner");
+               POSIX_KEYS.put(DName.group.qName(), "posix:group");
+               POSIX_KEYS.put(CrName.permissions.qName(), "posix:permissions");
        }
 
-       private final ProvidedSession session;
        private final FsContentProvider provider;
        private final Path path;
-       private final boolean isRoot;
+       private final boolean isMountBase;
        private final QName name;
 
        protected FsContent(ProvidedSession session, FsContentProvider contentProvider, Path path) {
-               this.session = session;
+               super(session);
                this.provider = contentProvider;
                this.path = path;
-               this.isRoot = contentProvider.isRoot(path);
+               this.isMountBase = contentProvider.isMountBase(path);
                // TODO check file names with ':' ?
-               if (isRoot)
-                       this.name = CrName.ROOT.get();
-               else
-                       this.name = session.parsePrefixedName(path.getFileName().toString());
+               if (isMountBase) {
+                       String mountPath = provider.getMountPath();
+                       if (mountPath != null && !mountPath.equals(ContentUtils.ROOT_SLASH)) {
+                               Content mountPoint = session.getMountPoint(mountPath);
+                               this.name = mountPoint.getName();
+                       } else {
+                               this.name = CrName.root.qName();
+                       }
+               } else {
+
+                       // TODO should we support prefixed name for known types?
+                       QName providerName = provider.fromFsPrefixedName(path.getFileName().toString());
+//                     QName providerName = new QName(path.getFileName().toString());
+                       // TODO remove extension if mounted?
+                       this.name = new ContentName(providerName, session);
+               }
        }
 
        protected FsContent(FsContent context, Path path) {
@@ -80,12 +114,32 @@ public class FsContent extends AbstractContent implements ProvidedContent {
         * ATTRIBUTES
         */
 
+       @SuppressWarnings("unchecked")
        @Override
        public <A> Optional<A> get(QName key, Class<A> clss) {
                Object value;
                try {
                        // We need to add user: when accessing via Files#getAttribute
-                       value = Files.getAttribute(path, toFsAttributeKey(key));
+
+                       if (POSIX_KEYS.containsKey(key)) {
+                               value = Files.getAttribute(path, toFsAttributeKey(key));
+                       } else {
+                               UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path,
+                                               UserDefinedFileAttributeView.class);
+                               String prefixedName = provider.toFsPrefixedName(key);
+                               if (!udfav.list().contains(prefixedName))
+                                       return Optional.empty();
+                               ByteBuffer buf = ByteBuffer.allocate(udfav.size(prefixedName));
+                               udfav.read(prefixedName, buf);
+                               buf.flip();
+                               if (buf.hasArray())
+                                       value = buf.array();
+                               else {
+                                       byte[] arr = new byte[buf.remaining()];
+                                       buf.get(arr);
+                                       value = arr;
+                               }
+                       }
                } catch (IOException e) {
                        throw new ContentResourceException("Cannot retrieve attribute " + key + " for " + path, e);
                }
@@ -99,15 +153,41 @@ public class FsContent extends AbstractContent implements ProvidedContent {
                        }
                        // TODO perform trivial file conversion to other formats
                }
+
+               // TODO better deal with multiple types
                if (value instanceof byte[]) {
-                       res = (A) new String((byte[]) value, StandardCharsets.UTF_8);
-               }
-               if (res == null)
-                       try {
-                               res = (A) value;
-                       } catch (ClassCastException e) {
-                               return Optional.empty();
+                       String str = new String((byte[]) value, StandardCharsets.UTF_8);
+                       String[] arr = str.split("\n");
+
+                       if (arr.length == 1) {
+                               if (clss.isAssignableFrom(String.class)) {
+                                       res = (A) arr[0];
+                               } else {
+                                       res = (A) CrAttributeType.parse(arr[0]);
+                               }
+                       } else {
+                               List<Object> lst = new ArrayList<>();
+                               for (String s : arr) {
+                                       lst.add(CrAttributeType.parse(s));
+                               }
+                               res = (A) lst;
                        }
+               }
+               if (res == null) {
+                       if (isDefaultAttrTypeRequested(clss))
+                               return Optional.of((A) CrAttributeType.parse(value.toString()));
+                       if (clss.isAssignableFrom(value.getClass()))
+                               return Optional.of((A) value);
+                       if (clss.isAssignableFrom(String.class))
+                               return Optional.of((A) value.toString());
+                       log.warn("Cannot interpret " + key + " in " + this);
+                       return Optional.empty();
+//                     try {
+//                             res = (A) value;
+//                     } catch (ClassCastException e) {
+//                             return Optional.empty();
+//                     }
+               }
                return Optional.of(res);
        }
 
@@ -118,7 +198,11 @@ public class FsContent extends AbstractContent implements ProvidedContent {
                if (udfav != null) {
                        try {
                                for (String name : udfav.list()) {
-                                       result.add(session.parsePrefixedName(name));
+                                       QName providerName = provider.fromFsPrefixedName(name);
+                                       if (providerName.getNamespaceURI().equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI))
+                                               continue; // skip prefix mapping
+                                       QName sessionName = new ContentName(providerName, getSession());
+                                       result.add(sessionName);
                                }
                        } catch (IOException e) {
                                throw new ContentResourceException("Cannot list attributes for " + path, e);
@@ -131,7 +215,7 @@ public class FsContent extends AbstractContent implements ProvidedContent {
        protected void removeAttr(QName key) {
                UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class);
                try {
-                       udfav.delete(session.toPrefixedName(key));
+                       udfav.delete(provider.toFsPrefixedName(key));
                } catch (IOException e) {
                        throw new ContentResourceException("Cannot delete attribute " + key + " for " + path, e);
                }
@@ -140,10 +224,22 @@ public class FsContent extends AbstractContent implements ProvidedContent {
        @Override
        public Object put(QName key, Object value) {
                Object previous = get(key);
+
+               String toWrite;
+               if (value instanceof List) {
+                       StringJoiner sj = new StringJoiner("\n");
+                       for (Object obj : (List<?>) value) {
+                               sj.add(obj.toString());
+                       }
+                       toWrite = sj.toString();
+               } else {
+                       toWrite = value.toString();
+               }
+
                UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class);
-               ByteBuffer bb = ByteBuffer.wrap(value.toString().getBytes(StandardCharsets.UTF_8));
+               ByteBuffer bb = ByteBuffer.wrap(toWrite.getBytes(StandardCharsets.UTF_8));
                try {
-                       int size = udfav.write(session.toPrefixedName(key), bb);
+                       udfav.write(provider.toFsPrefixedName(key), bb);
                } catch (IOException e) {
                        throw new ContentResourceException("Cannot delete attribute " + key + " for " + path, e);
                }
@@ -154,7 +250,7 @@ public class FsContent extends AbstractContent implements ProvidedContent {
                if (POSIX_KEYS.containsKey(key))
                        return POSIX_KEYS.get(key);
                else
-                       return USER_ + session.toPrefixedName(key);
+                       return USER_ + provider.toFsPrefixedName(key);
        }
 
        /*
@@ -164,7 +260,19 @@ public class FsContent extends AbstractContent implements ProvidedContent {
        public Iterator<Content> iterator() {
                if (Files.isDirectory(path)) {
                        try {
-                               return Files.list(path).map((p) -> (Content) new FsContent(this, p)).iterator();
+                               return Files.list(path).map((p) -> {
+                                       FsContent fsContent = new FsContent(this, p);
+                                       Optional<String> isMount = fsContent.get(CrName.mount.qName(), String.class);
+                                       if (isMount.orElse("false").equals("true")) {
+                                               QName[] classes = null;
+                                               ContentProvider contentProvider = getSession().getRepository()
+                                                               .getMountContentProvider(fsContent, false, classes);
+                                               Content mountedContent = contentProvider.get(getSession(), "");
+                                               return mountedContent;
+                                       } else {
+                                               return (Content) fsContent;
+                                       }
+                               }).iterator();
                        } catch (IOException e) {
                                throw new ContentResourceException("Cannot list " + path, e);
                        }
@@ -175,9 +283,10 @@ public class FsContent extends AbstractContent implements ProvidedContent {
 
        @Override
        public Content add(QName name, QName... classes) {
+               FsContent fsContent;
                try {
-                       Path newPath = path.resolve(session.toPrefixedName(name));
-                       if (ContentName.contains(classes, CrName.COLLECTION.get()))
+                       Path newPath = path.resolve(provider.toFsPrefixedName(name));
+                       if (ContentName.contains(classes, DName.collection.qName()))
                                Files.createDirectory(newPath);
                        else
                                Files.createFile(newPath);
@@ -185,10 +294,23 @@ public class FsContent extends AbstractContent implements ProvidedContent {
 //             for(ContentClass clss:classes) {
 //                     Files.setAttribute(newPath, name, newPath, null)
 //             }
-                       return new FsContent(this, newPath);
+                       fsContent = new FsContent(this, newPath);
                } catch (IOException e) {
                        throw new ContentResourceException("Cannot create new content", e);
                }
+
+               if (classes.length > 0)
+                       fsContent.addContentClasses(classes);
+               if (getSession().getRepository().shouldMount(classes)) {
+                       ContentProvider contentProvider = getSession().getRepository().getMountContentProvider(fsContent, true,
+                                       classes);
+                       Content mountedContent = contentProvider.get(getSession(), "");
+                       fsContent.put(CrName.mount.qName(), "true");
+                       return mountedContent;
+
+               } else {
+                       return fsContent;
+               }
        }
 
        @Override
@@ -198,22 +320,114 @@ public class FsContent extends AbstractContent implements ProvidedContent {
 
        @Override
        public Content getParent() {
-               if (isRoot)
-                       return null;// TODO deal with mounts
+               if (isMountBase) {
+                       String mountPath = provider.getMountPath();
+                       if (mountPath == null || mountPath.equals("/"))
+                               return null;
+                       String[] parent = ContentUtils.getParentPath(mountPath);
+                       return getSession().get(parent[0]);
+               }
                return new FsContent(this, path.getParent());
        }
 
+       @SuppressWarnings("unchecked")
+       @Override
+       public <C extends Closeable> C open(Class<C> clss) throws IOException, IllegalArgumentException {
+               if (InputStream.class.isAssignableFrom(clss)) {
+                       if (Files.isDirectory(path))
+                               throw new UnsupportedOperationException("Cannot open " + path + " as stream, since it is a directory");
+                       return (C) Files.newInputStream(path);
+               } else if (OutputStream.class.isAssignableFrom(clss)) {
+                       if (Files.isDirectory(path))
+                               throw new UnsupportedOperationException("Cannot open " + path + " as stream, since it is a directory");
+                       return (C) Files.newOutputStream(path);
+               }
+               return super.open(clss);
+       }
+
        /*
-        * ACCESSORS
+        * MOUNT MANAGEMENT
         */
        @Override
-       public ProvidedSession getSession() {
-               return session;
+       public ProvidedContent getMountPoint(String relativePath) {
+               Path childPath = path.resolve(relativePath);
+               // TODO check that it is a mount
+               return new FsContent(this, childPath);
        }
 
+       /*
+        * TYPING
+        */
+
+       @Override
+       public List<QName> getContentClasses() {
+               List<QName> res = new ArrayList<>();
+               List<String> value = getMultiple(DName.resourcetype.qName(), String.class);
+               for (String s : value) {
+                       QName name = NamespaceUtils.parsePrefixedName(provider, s);
+                       res.add(name);
+               }
+               if (Files.isDirectory(path))
+                       res.add(DName.collection.qName());
+               return res;
+       }
+
+       @Override
+       public void addContentClasses(QName... contentClass) {
+               List<String> toWrite = new ArrayList<>();
+               for (QName cc : getContentClasses()) {
+                       if (cc.equals(DName.collection.qName()))
+                               continue; // skip
+                       toWrite.add(NamespaceUtils.toPrefixedName(provider, cc));
+               }
+               for (QName cc : contentClass) {
+                       toWrite.add(NamespaceUtils.toPrefixedName(provider, cc));
+               }
+               put(DName.resourcetype.qName(), toWrite);
+       }
+
+       /*
+        * ACCESSORS
+        */
+
        @Override
        public FsContentProvider getProvider() {
                return provider;
        }
 
+       /*
+        * READ / WRITE
+        */
+       @SuppressWarnings("unchecked")
+       public <A> CompletableFuture<A> write(Class<A> clss) {
+               if (isContentClass(DName.collection.qName())) {
+                       throw new IllegalStateException("Cannot directly write to a collection");
+               }
+               if (InputStream.class.isAssignableFrom(clss)) {
+                       CompletableFuture<InputStream> res = new CompletableFuture<>();
+                       res.thenAccept((in) -> {
+                               try {
+                                       Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
+                               } catch (IOException e) {
+                                       throw new RuntimeException("Cannot write to " + path, e);
+                               }
+                       });
+                       return (CompletableFuture<A>) res;
+               } else if (Source.class.isAssignableFrom(clss)) {
+                       CompletableFuture<Source> res = new CompletableFuture<Source>();
+                       res.thenAccept((source) -> {
+//                             Path targetPath = path.getParent().resolve(path.getFileName()+".xml");
+                               Path targetPath = path;
+                               try (OutputStream out = Files.newOutputStream(targetPath)) {
+                                       StreamResult result = new StreamResult(out);
+                                       TransformerFactory.newDefaultInstance().newTransformer().transform(source, result);
+                               } catch (IOException | TransformerException e) {
+                                       throw new RuntimeException("Cannot write to " + path, e);
+                               }
+                       });
+                       return (CompletableFuture<A>) res;
+               } else {
+                       return super.write(clss);
+               }
+       }
 }