1 package org
.argeo
.cms
.acr
.fs
;
3 import java
.io
.Closeable
;
4 import java
.io
.IOException
;
5 import java
.io
.InputStream
;
6 import java
.io
.OutputStream
;
7 import java
.nio
.ByteBuffer
;
8 import java
.nio
.charset
.StandardCharsets
;
9 import java
.nio
.file
.Files
;
10 import java
.nio
.file
.Path
;
11 import java
.nio
.file
.StandardCopyOption
;
12 import java
.nio
.file
.attribute
.FileTime
;
13 import java
.nio
.file
.attribute
.UserDefinedFileAttributeView
;
14 import java
.time
.Instant
;
15 import java
.util
.ArrayList
;
16 import java
.util
.Collections
;
17 import java
.util
.HashMap
;
18 import java
.util
.HashSet
;
19 import java
.util
.Iterator
;
20 import java
.util
.List
;
22 import java
.util
.Optional
;
24 import java
.util
.concurrent
.CompletableFuture
;
26 import javax
.xml
.namespace
.QName
;
27 import javax
.xml
.transform
.Source
;
28 import javax
.xml
.transform
.TransformerException
;
29 import javax
.xml
.transform
.TransformerFactory
;
30 import javax
.xml
.transform
.stream
.StreamResult
;
32 import org
.argeo
.api
.acr
.Content
;
33 import org
.argeo
.api
.acr
.ContentName
;
34 import org
.argeo
.api
.acr
.ContentResourceException
;
35 import org
.argeo
.api
.acr
.CrName
;
36 import org
.argeo
.api
.acr
.NamespaceUtils
;
37 import org
.argeo
.api
.acr
.spi
.ContentProvider
;
38 import org
.argeo
.api
.acr
.spi
.ProvidedContent
;
39 import org
.argeo
.api
.acr
.spi
.ProvidedSession
;
40 import org
.argeo
.cms
.acr
.AbstractContent
;
41 import org
.argeo
.cms
.acr
.ContentUtils
;
42 import org
.argeo
.util
.FsUtils
;
44 /** Content persisted as a filesystem {@link Path}. */
45 public class FsContent
extends AbstractContent
implements ProvidedContent
{
46 final static String USER_
= "user:";
48 private static final Map
<QName
, String
> BASIC_KEYS
;
49 private static final Map
<QName
, String
> POSIX_KEYS
;
51 BASIC_KEYS
= new HashMap
<>();
52 BASIC_KEYS
.put(CrName
.creationTime
.qName(), "basic:creationTime");
53 BASIC_KEYS
.put(CrName
.lastModifiedTime
.qName(), "basic:lastModifiedTime");
54 BASIC_KEYS
.put(CrName
.size
.qName(), "basic:size");
55 BASIC_KEYS
.put(CrName
.fileKey
.qName(), "basic:fileKey");
57 POSIX_KEYS
= new HashMap
<>(BASIC_KEYS
);
58 POSIX_KEYS
.put(CrName
.owner
.qName(), "owner:owner");
59 POSIX_KEYS
.put(CrName
.group
.qName(), "posix:group");
60 POSIX_KEYS
.put(CrName
.permissions
.qName(), "posix:permissions");
63 private final FsContentProvider provider
;
64 private final Path path
;
65 private final boolean isMountBase
;
66 private final QName name
;
68 protected FsContent(ProvidedSession session
, FsContentProvider contentProvider
, Path path
) {
70 this.provider
= contentProvider
;
72 this.isMountBase
= contentProvider
.isMountBase(path
);
73 // TODO check file names with ':' ?
75 String mountPath
= provider
.getMountPath();
76 if (mountPath
!= null && !mountPath
.equals("/")) {
77 Content mountPoint
= session
.getMountPoint(mountPath
);
78 this.name
= mountPoint
.getName();
80 this.name
= CrName
.root
.qName();
84 // TODO should we support prefixed name for known types?
85 // QName providerName = NamespaceUtils.parsePrefixedName(provider,
86 // path.getFileName().toString());
87 QName providerName
= new QName(path
.getFileName().toString());
88 // TODO remove extension if mounted?
89 this.name
= new ContentName(providerName
, session
);
93 protected FsContent(FsContent context
, Path path
) {
94 this(context
.getSession(), context
.getProvider(), path
);
97 private boolean isPosix() {
98 return path
.getFileSystem().supportedFileAttributeViews().contains("posix");
102 public QName
getName() {
110 @SuppressWarnings("unchecked")
112 public <A
> Optional
<A
> get(QName key
, Class
<A
> clss
) {
115 // We need to add user: when accessing via Files#getAttribute
117 if (POSIX_KEYS
.containsKey(key
)) {
118 value
= Files
.getAttribute(path
, toFsAttributeKey(key
));
120 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
,
121 UserDefinedFileAttributeView
.class);
122 String prefixedName
= NamespaceUtils
.toPrefixedName(provider
, key
);
123 if (!udfav
.list().contains(prefixedName
))
124 return Optional
.empty();
125 ByteBuffer buf
= ByteBuffer
.allocate(udfav
.size(prefixedName
));
126 udfav
.read(prefixedName
, buf
);
131 byte[] arr
= new byte[buf
.remaining()];
136 } catch (IOException e
) {
137 throw new ContentResourceException("Cannot retrieve attribute " + key
+ " for " + path
, e
);
140 if (value
instanceof FileTime
) {
141 if (clss
.isAssignableFrom(FileTime
.class))
143 Instant instant
= ((FileTime
) value
).toInstant();
144 if (Object
.class.isAssignableFrom(clss
)) {// plain object requested
147 // TODO perform trivial file conversion to other formats
149 if (value
instanceof byte[]) {
150 res
= (A
) new String((byte[]) value
, StandardCharsets
.UTF_8
);
155 } catch (ClassCastException e
) {
156 return Optional
.empty();
158 return Optional
.of(res
);
162 protected Iterable
<QName
> keys() {
163 Set
<QName
> result
= new HashSet
<>(isPosix() ? POSIX_KEYS
.keySet() : BASIC_KEYS
.keySet());
164 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
, UserDefinedFileAttributeView
.class);
167 for (String name
: udfav
.list()) {
168 QName providerName
= NamespaceUtils
.parsePrefixedName(provider
, name
);
169 QName sessionName
= new ContentName(providerName
, getSession());
170 result
.add(sessionName
);
172 } catch (IOException e
) {
173 throw new ContentResourceException("Cannot list attributes for " + path
, e
);
180 protected void removeAttr(QName key
) {
181 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
, UserDefinedFileAttributeView
.class);
183 udfav
.delete(NamespaceUtils
.toPrefixedName(provider
, key
));
184 } catch (IOException e
) {
185 throw new ContentResourceException("Cannot delete attribute " + key
+ " for " + path
, e
);
190 public Object
put(QName key
, Object value
) {
191 Object previous
= get(key
);
192 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
, UserDefinedFileAttributeView
.class);
193 ByteBuffer bb
= ByteBuffer
.wrap(value
.toString().getBytes(StandardCharsets
.UTF_8
));
195 udfav
.write(NamespaceUtils
.toPrefixedName(provider
, key
), bb
);
196 } catch (IOException e
) {
197 throw new ContentResourceException("Cannot delete attribute " + key
+ " for " + path
, e
);
202 protected String
toFsAttributeKey(QName key
) {
203 if (POSIX_KEYS
.containsKey(key
))
204 return POSIX_KEYS
.get(key
);
206 return USER_
+ NamespaceUtils
.toPrefixedName(provider
, key
);
213 public Iterator
<Content
> iterator() {
214 if (Files
.isDirectory(path
)) {
216 return Files
.list(path
).map((p
) -> {
217 FsContent fsContent
= new FsContent(this, p
);
218 Optional
<String
> isMount
= fsContent
.get(CrName
.mount
.qName(), String
.class);
219 if (isMount
.orElse("false").equals("true")) {
220 QName
[] classes
= null;
221 ContentProvider contentProvider
= getSession().getRepository()
222 .getMountContentProvider(fsContent
, false, classes
);
223 Content mountedContent
= contentProvider
.get(getSession(), "");
224 return mountedContent
;
226 return (Content
) fsContent
;
229 } catch (IOException e
) {
230 throw new ContentResourceException("Cannot list " + path
, e
);
233 return Collections
.emptyIterator();
238 public Content
add(QName name
, QName
... classes
) {
241 Path newPath
= path
.resolve(NamespaceUtils
.toPrefixedName(provider
, name
));
242 if (ContentName
.contains(classes
, CrName
.collection
.qName()))
243 Files
.createDirectory(newPath
);
245 Files
.createFile(newPath
);
247 // for(ContentClass clss:classes) {
248 // Files.setAttribute(newPath, name, newPath, null)
250 fsContent
= new FsContent(this, newPath
);
251 } catch (IOException e
) {
252 throw new ContentResourceException("Cannot create new content", e
);
255 if (getSession().getRepository().shouldMount(classes
)) {
256 ContentProvider contentProvider
= getSession().getRepository().getMountContentProvider(fsContent
, true,
258 Content mountedContent
= contentProvider
.get(getSession(), "");
259 fsContent
.put(CrName
.mount
.qName(), "true");
260 return mountedContent
;
268 public void remove() {
269 FsUtils
.delete(path
);
273 public Content
getParent() {
275 String mountPath
= provider
.getMountPath();
276 if (mountPath
== null || mountPath
.equals("/"))
278 String
[] parent
= ContentUtils
.getParentPath(mountPath
);
279 return getSession().get(parent
[0]);
281 return new FsContent(this, path
.getParent());
284 @SuppressWarnings("unchecked")
286 public <C
extends Closeable
> C
open(Class
<C
> clss
) throws IOException
, IllegalArgumentException
{
287 if (InputStream
.class.isAssignableFrom(clss
)) {
288 if (Files
.isDirectory(path
))
289 throw new UnsupportedOperationException("Cannot open " + path
+ " as stream, since it is a directory");
290 return (C
) Files
.newInputStream(path
);
291 } else if (OutputStream
.class.isAssignableFrom(clss
)) {
292 if (Files
.isDirectory(path
))
293 throw new UnsupportedOperationException("Cannot open " + path
+ " as stream, since it is a directory");
294 return (C
) Files
.newOutputStream(path
);
296 return super.open(clss
);
303 public ProvidedContent
getMountPoint(String relativePath
) {
304 Path childPath
= path
.resolve(relativePath
);
305 // TODO check that it is a mount
306 return new FsContent(this, childPath
);
314 public List
<QName
> getContentClasses() {
315 List
<QName
> res
= new ArrayList
<>();
316 if (Files
.isDirectory(path
))
317 res
.add(CrName
.collection
.qName());
318 // TODO add other types
327 public FsContentProvider
getProvider() {
334 @SuppressWarnings("unchecked")
335 public <A
> CompletableFuture
<A
> write(Class
<A
> clss
) {
336 if (isContentClass(CrName
.collection
.qName())) {
337 throw new IllegalStateException("Cannot directly write to a collection");
339 if (InputStream
.class.isAssignableFrom(clss
)) {
340 CompletableFuture
<InputStream
> res
= new CompletableFuture
<>();
341 res
.thenAccept((in
) -> {
343 Files
.copy(in
, path
, StandardCopyOption
.REPLACE_EXISTING
);
344 } catch (IOException e
) {
345 throw new RuntimeException("Cannot write to " + path
, e
);
348 return (CompletableFuture
<A
>) res
;
349 } else if (Source
.class.isAssignableFrom(clss
)) {
350 CompletableFuture
<Source
> res
= new CompletableFuture
<Source
>();
351 res
.thenAccept((source
) -> {
352 // Path targetPath = path.getParent().resolve(path.getFileName()+".xml");
353 Path targetPath
= path
;
354 try (OutputStream out
= Files
.newOutputStream(targetPath
)) {
355 StreamResult result
= new StreamResult(out
);
356 TransformerFactory
.newDefaultInstance().newTransformer().transform(source
, result
);
357 } catch (IOException
| TransformerException e
) {
358 throw new RuntimeException("Cannot write to " + path
, e
);
361 return (CompletableFuture
<A
>) res
;
363 return super.write(clss
);