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
.attribute
.FileTime
;
12 import java
.nio
.file
.attribute
.UserDefinedFileAttributeView
;
13 import java
.time
.Instant
;
14 import java
.util
.Collections
;
15 import java
.util
.HashMap
;
16 import java
.util
.HashSet
;
17 import java
.util
.Iterator
;
19 import java
.util
.Optional
;
22 import javax
.xml
.namespace
.QName
;
24 import org
.argeo
.api
.acr
.Content
;
25 import org
.argeo
.api
.acr
.ContentName
;
26 import org
.argeo
.api
.acr
.ContentResourceException
;
27 import org
.argeo
.api
.acr
.CrName
;
28 import org
.argeo
.api
.acr
.NamespaceUtils
;
29 import org
.argeo
.api
.acr
.spi
.ContentProvider
;
30 import org
.argeo
.api
.acr
.spi
.ProvidedContent
;
31 import org
.argeo
.api
.acr
.spi
.ProvidedSession
;
32 import org
.argeo
.cms
.acr
.AbstractContent
;
33 import org
.argeo
.cms
.acr
.ContentUtils
;
34 import org
.argeo
.util
.FsUtils
;
36 /** Content persisted as a filesystem {@link Path}. */
37 public class FsContent
extends AbstractContent
implements ProvidedContent
{
38 final static String USER_
= "user:";
40 private static final Map
<QName
, String
> BASIC_KEYS
;
41 private static final Map
<QName
, String
> POSIX_KEYS
;
43 BASIC_KEYS
= new HashMap
<>();
44 BASIC_KEYS
.put(CrName
.CREATION_TIME
.get(), "basic:creationTime");
45 BASIC_KEYS
.put(CrName
.LAST_MODIFIED_TIME
.get(), "basic:lastModifiedTime");
46 BASIC_KEYS
.put(CrName
.SIZE
.get(), "basic:size");
47 BASIC_KEYS
.put(CrName
.FILE_KEY
.get(), "basic:fileKey");
49 POSIX_KEYS
= new HashMap
<>(BASIC_KEYS
);
50 POSIX_KEYS
.put(CrName
.OWNER
.get(), "owner:owner");
51 POSIX_KEYS
.put(CrName
.GROUP
.get(), "posix:group");
52 POSIX_KEYS
.put(CrName
.PERMISSIONS
.get(), "posix:permissions");
55 private final ProvidedSession session
;
56 private final FsContentProvider provider
;
57 private final Path path
;
58 private final boolean isRoot
;
59 private final QName name
;
61 protected FsContent(ProvidedSession session
, FsContentProvider contentProvider
, Path path
) {
62 this.session
= session
;
63 this.provider
= contentProvider
;
65 this.isRoot
= contentProvider
.isMountRoot(path
);
66 // TODO check file names with ':' ?
68 String mountPath
= provider
.getMountPath();
69 if (mountPath
!= null && !mountPath
.equals("/")) {
70 Content mountPoint
= session
.getMountPoint(mountPath
);
71 this.name
= mountPoint
.getName();
73 this.name
= CrName
.ROOT
.get();
77 // TODO should we support prefixed name for known types?
78 // QName providerName = NamespaceUtils.parsePrefixedName(provider,
79 // path.getFileName().toString());
80 QName providerName
= new QName(path
.getFileName().toString());
81 // TODO remove extension if mounted?
82 this.name
= new ContentName(providerName
, session
);
86 protected FsContent(FsContent context
, Path path
) {
87 this(context
.getSession(), context
.getProvider(), path
);
90 private boolean isPosix() {
91 return path
.getFileSystem().supportedFileAttributeViews().contains("posix");
95 public QName
getName() {
103 @SuppressWarnings("unchecked")
105 public <A
> Optional
<A
> get(QName key
, Class
<A
> clss
) {
108 // We need to add user: when accessing via Files#getAttribute
110 if (POSIX_KEYS
.containsKey(key
)) {
111 value
= Files
.getAttribute(path
, toFsAttributeKey(key
));
113 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
,
114 UserDefinedFileAttributeView
.class);
115 String prefixedName
= NamespaceUtils
.toPrefixedName(provider
, key
);
116 if (!udfav
.list().contains(prefixedName
))
117 return Optional
.empty();
118 ByteBuffer buf
= ByteBuffer
.allocate(udfav
.size(prefixedName
));
119 udfav
.read(prefixedName
, buf
);
124 byte[] arr
= new byte[buf
.remaining()];
129 } catch (IOException e
) {
130 throw new ContentResourceException("Cannot retrieve attribute " + key
+ " for " + path
, e
);
133 if (value
instanceof FileTime
) {
134 if (clss
.isAssignableFrom(FileTime
.class))
136 Instant instant
= ((FileTime
) value
).toInstant();
137 if (Object
.class.isAssignableFrom(clss
)) {// plain object requested
140 // TODO perform trivial file conversion to other formats
142 if (value
instanceof byte[]) {
143 res
= (A
) new String((byte[]) value
, StandardCharsets
.UTF_8
);
148 } catch (ClassCastException e
) {
149 return Optional
.empty();
151 return Optional
.of(res
);
155 protected Iterable
<QName
> keys() {
156 Set
<QName
> result
= new HashSet
<>(isPosix() ? POSIX_KEYS
.keySet() : BASIC_KEYS
.keySet());
157 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
, UserDefinedFileAttributeView
.class);
160 for (String name
: udfav
.list()) {
161 QName providerName
= NamespaceUtils
.parsePrefixedName(provider
, name
);
162 QName sessionName
= new ContentName(providerName
, session
);
163 result
.add(sessionName
);
165 } catch (IOException e
) {
166 throw new ContentResourceException("Cannot list attributes for " + path
, e
);
173 protected void removeAttr(QName key
) {
174 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
, UserDefinedFileAttributeView
.class);
176 udfav
.delete(NamespaceUtils
.toPrefixedName(provider
, key
));
177 } catch (IOException e
) {
178 throw new ContentResourceException("Cannot delete attribute " + key
+ " for " + path
, e
);
183 public Object
put(QName key
, Object value
) {
184 Object previous
= get(key
);
185 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
, UserDefinedFileAttributeView
.class);
186 ByteBuffer bb
= ByteBuffer
.wrap(value
.toString().getBytes(StandardCharsets
.UTF_8
));
188 udfav
.write(NamespaceUtils
.toPrefixedName(provider
, key
), bb
);
189 } catch (IOException e
) {
190 throw new ContentResourceException("Cannot delete attribute " + key
+ " for " + path
, e
);
195 protected String
toFsAttributeKey(QName key
) {
196 if (POSIX_KEYS
.containsKey(key
))
197 return POSIX_KEYS
.get(key
);
199 return USER_
+ NamespaceUtils
.toPrefixedName(provider
, key
);
206 public Iterator
<Content
> iterator() {
207 if (Files
.isDirectory(path
)) {
209 return Files
.list(path
).map((p
) -> {
210 FsContent fsContent
= new FsContent(this, p
);
211 Optional
<String
> isMount
= fsContent
.get(CrName
.MOUNT
.get(), String
.class);
212 if (isMount
.orElse("false").equals("true")) {
213 QName
[] classes
= null;
214 ContentProvider contentProvider
= session
.getRepository().getMountContentProvider(fsContent
,
216 Content mountedContent
= contentProvider
.get(session
, fsContent
.getPath(), "");
217 return mountedContent
;
219 return (Content
) fsContent
;
222 } catch (IOException e
) {
223 throw new ContentResourceException("Cannot list " + path
, e
);
226 return Collections
.emptyIterator();
231 public Content
add(QName name
, QName
... classes
) {
234 Path newPath
= path
.resolve(NamespaceUtils
.toPrefixedName(provider
, name
));
235 if (ContentName
.contains(classes
, CrName
.COLLECTION
.get()))
236 Files
.createDirectory(newPath
);
238 Files
.createFile(newPath
);
240 // for(ContentClass clss:classes) {
241 // Files.setAttribute(newPath, name, newPath, null)
243 fsContent
= new FsContent(this, newPath
);
244 } catch (IOException e
) {
245 throw new ContentResourceException("Cannot create new content", e
);
248 if (session
.getRepository().shouldMount(classes
)) {
249 ContentProvider contentProvider
= session
.getRepository().getMountContentProvider(fsContent
, true, classes
);
250 Content mountedContent
= contentProvider
.get(session
, fsContent
.getPath(), "");
251 fsContent
.put(CrName
.MOUNT
.get(), "true");
252 return mountedContent
;
260 public void remove() {
261 FsUtils
.delete(path
);
265 public Content
getParent() {
267 String mountPath
= provider
.getMountPath();
268 if (mountPath
== null || mountPath
.equals("/"))
270 String
[] parent
= ContentUtils
.getParentPath(mountPath
);
271 return session
.get(parent
[0]);
273 return new FsContent(this, path
.getParent());
276 @SuppressWarnings("unchecked")
278 public <C
extends Closeable
> C
open(Class
<C
> clss
) throws IOException
, IllegalArgumentException
{
279 if (InputStream
.class.isAssignableFrom(clss
)) {
280 if (Files
.isDirectory(path
))
281 throw new UnsupportedOperationException("Cannot open " + path
+ " as stream, since it is a directory");
282 return (C
) Files
.newInputStream(path
);
283 } else if (OutputStream
.class.isAssignableFrom(clss
)) {
284 if (Files
.isDirectory(path
))
285 throw new UnsupportedOperationException("Cannot open " + path
+ " as stream, since it is a directory");
286 return (C
) Files
.newOutputStream(path
);
288 return super.open(clss
);
295 public ProvidedContent
getMountPoint(String relativePath
) {
296 Path childPath
= path
.resolve(relativePath
);
297 // TODO check that it is a mount
298 return new FsContent(this, childPath
);
305 public ProvidedSession
getSession() {
310 public FsContentProvider
getProvider() {