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
.StringJoiner
;
25 import java
.util
.concurrent
.CompletableFuture
;
27 import javax
.xml
.XMLConstants
;
28 import javax
.xml
.namespace
.QName
;
29 import javax
.xml
.transform
.Source
;
30 import javax
.xml
.transform
.TransformerException
;
31 import javax
.xml
.transform
.TransformerFactory
;
32 import javax
.xml
.transform
.stream
.StreamResult
;
34 import org
.argeo
.api
.acr
.Content
;
35 import org
.argeo
.api
.acr
.ContentName
;
36 import org
.argeo
.api
.acr
.ContentResourceException
;
37 import org
.argeo
.api
.acr
.CrAttributeType
;
38 import org
.argeo
.api
.acr
.CrName
;
39 import org
.argeo
.api
.acr
.DName
;
40 import org
.argeo
.api
.acr
.NamespaceUtils
;
41 import org
.argeo
.api
.acr
.spi
.ContentProvider
;
42 import org
.argeo
.api
.acr
.spi
.ProvidedContent
;
43 import org
.argeo
.api
.acr
.spi
.ProvidedSession
;
44 import org
.argeo
.cms
.acr
.AbstractContent
;
45 import org
.argeo
.cms
.acr
.ContentUtils
;
46 import org
.argeo
.cms
.util
.FsUtils
;
48 /** Content persisted as a filesystem {@link Path}. */
49 public class FsContent
extends AbstractContent
implements ProvidedContent
{
50 // private CmsLog log = CmsLog.getLog(FsContent.class);
52 final static String USER_
= "user:";
54 private static final Map
<QName
, String
> BASIC_KEYS
;
55 private static final Map
<QName
, String
> POSIX_KEYS
;
57 BASIC_KEYS
= new HashMap
<>();
58 BASIC_KEYS
.put(DName
.creationdate
.qName(), "basic:creationTime");
59 BASIC_KEYS
.put(DName
.getlastmodified
.qName(), "basic:lastModifiedTime");
60 BASIC_KEYS
.put(DName
.getcontentlength
.qName(), "basic:size");
62 BASIC_KEYS
.put(CrName
.fileKey
.qName(), "basic:fileKey");
64 POSIX_KEYS
= new HashMap
<>(BASIC_KEYS
);
65 POSIX_KEYS
.put(DName
.owner
.qName(), "owner:owner");
66 POSIX_KEYS
.put(DName
.group
.qName(), "posix:group");
67 POSIX_KEYS
.put(CrName
.permissions
.qName(), "posix:permissions");
70 private final FsContentProvider provider
;
71 private final Path path
;
72 private final boolean isMountBase
;
73 private final QName name
;
75 protected FsContent(ProvidedSession session
, FsContentProvider contentProvider
, Path path
) {
77 this.provider
= contentProvider
;
79 this.isMountBase
= contentProvider
.isMountBase(path
);
80 // TODO check file names with ':' ?
82 String mountPath
= provider
.getMountPath();
83 if (mountPath
!= null && !mountPath
.equals(Content
.ROOT_PATH
)) {
84 Content mountPoint
= session
.getMountPoint(mountPath
);
85 this.name
= mountPoint
.getName();
87 this.name
= CrName
.root
.qName();
91 // TODO should we support prefixed name for known types?
92 QName providerName
= provider
.fromFsPrefixedName(path
.getFileName().toString());
93 // QName providerName = new QName(path.getFileName().toString());
94 // TODO remove extension if mounted?
95 this.name
= new ContentName(providerName
, session
);
99 protected FsContent(FsContent context
, Path path
) {
100 this(context
.getSession(), context
.getProvider(), path
);
103 private boolean isPosix() {
104 return path
.getFileSystem().supportedFileAttributeViews().contains("posix");
108 public QName
getName() {
116 @SuppressWarnings("unchecked")
118 public <A
> Optional
<A
> get(QName key
, Class
<A
> clss
) {
121 // We need to add user: when accessing via Files#getAttribute
123 if (POSIX_KEYS
.containsKey(key
)) {
124 value
= Files
.getAttribute(path
, toFsAttributeKey(key
));
126 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
,
127 UserDefinedFileAttributeView
.class);
128 String prefixedName
= provider
.toFsPrefixedName(key
);
129 if (!udfav
.list().contains(prefixedName
))
130 return Optional
.empty();
131 ByteBuffer buf
= ByteBuffer
.allocate(udfav
.size(prefixedName
));
132 udfav
.read(prefixedName
, buf
);
137 byte[] arr
= new byte[buf
.remaining()];
142 } catch (IOException e
) {
143 throw new ContentResourceException("Cannot retrieve attribute " + key
+ " for " + path
, e
);
146 if (value
instanceof FileTime
) {
147 if (clss
.isAssignableFrom(FileTime
.class))
149 Instant instant
= ((FileTime
) value
).toInstant();
150 if (Object
.class.isAssignableFrom(clss
)) {// plain object requested
153 // TODO perform trivial file conversion to other formats
156 // TODO better deal with multiple types
157 if (value
instanceof byte[]) {
158 String str
= new String((byte[]) value
, StandardCharsets
.UTF_8
);
159 String
[] arr
= str
.split("\n");
161 if (arr
.length
== 1) {
162 // if (clss.isAssignableFrom(String.class)) {
165 // res = (A) CrAttributeType.parse(arr[0]);
167 // if (isDefaultAttrTypeRequested(clss))
168 // return Optional.of((A) CrAttributeType.parse(str));
169 return CrAttributeType
.cast(clss
, str
);
172 List
<Object
> lst
= new ArrayList
<>();
173 for (String s
: arr
) {
174 lst
.add(CrAttributeType
.parse(s
));
180 // if (isDefaultAttrTypeRequested(clss))
181 // return Optional.of((A) CrAttributeType.parse(value.toString()));
182 return CrAttributeType
.cast(clss
, value
);
183 // if (clss.isAssignableFrom(value.getClass()))
184 // return Optional.of((A) value);
185 // if (clss.isAssignableFrom(String.class))
186 // return Optional.of((A) value.toString());
187 // log.warn("Cannot interpret " + key + " in " + this);
188 // return Optional.empty();
191 // } catch (ClassCastException e) {
192 // return Optional.empty();
195 return Optional
.of(res
);
199 protected Iterable
<QName
> keys() {
200 Set
<QName
> result
= new HashSet
<>(isPosix() ? POSIX_KEYS
.keySet() : BASIC_KEYS
.keySet());
201 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
, UserDefinedFileAttributeView
.class);
204 for (String name
: udfav
.list()) {
205 QName providerName
= provider
.fromFsPrefixedName(name
);
206 if (providerName
.getNamespaceURI().equals(XMLConstants
.XMLNS_ATTRIBUTE_NS_URI
))
207 continue; // skip prefix mapping
208 QName sessionName
= new ContentName(providerName
, getSession());
209 result
.add(sessionName
);
211 } catch (IOException e
) {
212 throw new ContentResourceException("Cannot list attributes for " + path
, e
);
219 protected void removeAttr(QName key
) {
220 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
, UserDefinedFileAttributeView
.class);
222 udfav
.delete(provider
.toFsPrefixedName(key
));
223 } catch (IOException e
) {
224 throw new ContentResourceException("Cannot delete attribute " + key
+ " for " + path
, e
);
229 public Object
put(QName key
, Object value
) {
230 Object previous
= get(key
);
233 if (value
instanceof List
) {
234 StringJoiner sj
= new StringJoiner("\n");
235 for (Object obj
: (List
<?
>) value
) {
236 sj
.add(obj
.toString());
238 toWrite
= sj
.toString();
240 toWrite
= value
.toString();
243 UserDefinedFileAttributeView udfav
= Files
.getFileAttributeView(path
, UserDefinedFileAttributeView
.class);
244 ByteBuffer bb
= ByteBuffer
.wrap(toWrite
.getBytes(StandardCharsets
.UTF_8
));
246 udfav
.write(provider
.toFsPrefixedName(key
), bb
);
247 } catch (IOException e
) {
248 throw new ContentResourceException("Cannot delete attribute " + key
+ " for " + path
, e
);
253 protected String
toFsAttributeKey(QName key
) {
254 if (POSIX_KEYS
.containsKey(key
))
255 return POSIX_KEYS
.get(key
);
257 return USER_
+ provider
.toFsPrefixedName(key
);
264 public Iterator
<Content
> iterator() {
265 if (Files
.isDirectory(path
)) {
267 return Files
.list(path
).map((p
) -> {
268 FsContent fsContent
= new FsContent(this, p
);
269 Optional
<String
> isMount
= fsContent
.get(CrName
.mount
.qName(), String
.class);
270 if (isMount
.orElse("false").equals("true")) {
271 QName
[] classes
= null;
272 ContentProvider contentProvider
= getSession().getRepository()
273 .getMountContentProvider(fsContent
, false, classes
);
274 Content mountedContent
= contentProvider
.get(getSession(), "");
275 return mountedContent
;
277 return (Content
) fsContent
;
280 } catch (IOException e
) {
281 throw new ContentResourceException("Cannot list " + path
, e
);
284 return Collections
.emptyIterator();
289 public Content
add(QName name
, QName
... classes
) {
292 Path newPath
= path
.resolve(provider
.toFsPrefixedName(name
));
293 if (ContentName
.contains(classes
, DName
.collection
.qName()))
294 Files
.createDirectory(newPath
);
296 Files
.createFile(newPath
);
298 // for(ContentClass clss:classes) {
299 // Files.setAttribute(newPath, name, newPath, null)
301 fsContent
= new FsContent(this, newPath
);
302 } catch (IOException e
) {
303 throw new ContentResourceException("Cannot create new content", e
);
306 if (classes
.length
> 0)
307 fsContent
.addContentClasses(classes
);
308 if (getSession().getRepository().shouldMount(classes
)) {
309 ContentProvider contentProvider
= getSession().getRepository().getMountContentProvider(fsContent
, true,
311 Content mountedContent
= contentProvider
.get(getSession(), "");
312 fsContent
.put(CrName
.mount
.qName(), "true");
313 return mountedContent
;
321 public void remove() {
323 FsUtils
.delete(path
);
324 } catch (IOException e
) {
325 throw new RuntimeException("Cannot delete " + path
, e
);
330 public Content
getParent() {
332 String mountPath
= provider
.getMountPath();
333 if (mountPath
== null || mountPath
.equals("/"))
335 String
[] parent
= ContentUtils
.getParentPath(mountPath
);
336 return getSession().get(parent
[0]);
338 return new FsContent(this, path
.getParent());
341 @SuppressWarnings("unchecked")
343 public <C
extends Closeable
> C
open(Class
<C
> clss
) throws IOException
, IllegalArgumentException
{
344 if (InputStream
.class.isAssignableFrom(clss
)) {
345 if (Files
.isDirectory(path
))
346 throw new UnsupportedOperationException("Cannot open " + path
+ " as stream, since it is a directory");
347 return (C
) Files
.newInputStream(path
);
348 } else if (OutputStream
.class.isAssignableFrom(clss
)) {
349 if (Files
.isDirectory(path
))
350 throw new UnsupportedOperationException("Cannot open " + path
+ " as stream, since it is a directory");
351 return (C
) Files
.newOutputStream(path
);
353 return super.open(clss
);
360 public ProvidedContent
getMountPoint(String relativePath
) {
361 Path childPath
= path
.resolve(relativePath
);
362 // TODO check that it is a mount
363 return new FsContent(this, childPath
);
371 public List
<QName
> getContentClasses() {
372 // List<QName> res = new ArrayList<>();
373 // List<String> value = getMultiple(DName.resourcetype.qName(), String.class);
374 // for (String s : value) {
375 // QName name = NamespaceUtils.parsePrefixedName(provider, s);
378 List
<QName
> res
= getMultiple(DName
.resourcetype
.qName(), QName
.class);
379 if (Files
.isDirectory(path
))
380 res
.add(DName
.collection
.qName());
385 public void addContentClasses(QName
... contentClass
) {
386 List
<String
> toWrite
= new ArrayList
<>();
387 for (QName cc
: getContentClasses()) {
388 if (cc
.equals(DName
.collection
.qName()))
390 toWrite
.add(NamespaceUtils
.toPrefixedName(provider
, cc
));
392 for (QName cc
: contentClass
) {
393 toWrite
.add(NamespaceUtils
.toPrefixedName(provider
, cc
));
395 put(DName
.resourcetype
.qName(), toWrite
);
403 public FsContentProvider
getProvider() {
410 @SuppressWarnings("unchecked")
411 public <A
> CompletableFuture
<A
> write(Class
<A
> clss
) {
412 if (isContentClass(DName
.collection
.qName())) {
413 throw new IllegalStateException("Cannot directly write to a collection");
415 if (InputStream
.class.isAssignableFrom(clss
)) {
416 CompletableFuture
<InputStream
> res
= new CompletableFuture
<>();
417 res
.thenAccept((in
) -> {
419 Files
.copy(in
, path
, StandardCopyOption
.REPLACE_EXISTING
);
420 } catch (IOException e
) {
421 throw new RuntimeException("Cannot write to " + path
, e
);
424 return (CompletableFuture
<A
>) res
;
425 } else if (Source
.class.isAssignableFrom(clss
)) {
426 CompletableFuture
<Source
> res
= new CompletableFuture
<Source
>();
427 res
.thenAccept((source
) -> {
428 // Path targetPath = path.getParent().resolve(path.getFileName()+".xml");
429 Path targetPath
= path
;
430 try (OutputStream out
= Files
.newOutputStream(targetPath
)) {
431 StreamResult result
= new StreamResult(out
);
432 TransformerFactory
.newDefaultInstance().newTransformer().transform(source
, result
);
433 } catch (IOException
| TransformerException e
) {
434 throw new RuntimeException("Cannot write to " + path
, e
);
437 return (CompletableFuture
<A
>) res
;
439 return super.write(clss
);