]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java
Additional HTTP headers
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / acr / fs / FsContent.java
1 package org.argeo.cms.acr.fs;
2
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;
21 import java.util.Map;
22 import java.util.Optional;
23 import java.util.Set;
24 import java.util.StringJoiner;
25 import java.util.concurrent.CompletableFuture;
26
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;
33
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;
47
48 /** Content persisted as a filesystem {@link Path}. */
49 public class FsContent extends AbstractContent implements ProvidedContent {
50 // private CmsLog log = CmsLog.getLog(FsContent.class);
51
52 final static String USER_ = "user:";
53
54 private static final Map<QName, String> BASIC_KEYS;
55 private static final Map<QName, String> POSIX_KEYS;
56 static {
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");
61
62 BASIC_KEYS.put(CrName.fileKey.qName(), "basic:fileKey");
63
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");
68 }
69
70 private final FsContentProvider provider;
71 private final Path path;
72 private final boolean isMountBase;
73 private final QName name;
74
75 protected FsContent(ProvidedSession session, FsContentProvider contentProvider, Path path) {
76 super(session);
77 this.provider = contentProvider;
78 this.path = path;
79 this.isMountBase = contentProvider.isMountBase(path);
80 // TODO check file names with ':' ?
81 if (isMountBase) {
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();
86 } else {
87 this.name = CrName.root.qName();
88 }
89 } else {
90
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);
96 }
97 }
98
99 protected FsContent(FsContent context, Path path) {
100 this(context.getSession(), context.getProvider(), path);
101 }
102
103 private boolean isPosix() {
104 return path.getFileSystem().supportedFileAttributeViews().contains("posix");
105 }
106
107 @Override
108 public QName getName() {
109 return name;
110 }
111
112 /*
113 * ATTRIBUTES
114 */
115
116 @SuppressWarnings("unchecked")
117 @Override
118 public <A> Optional<A> get(QName key, Class<A> clss) {
119 Object value;
120 try {
121 // We need to add user: when accessing via Files#getAttribute
122
123 if (POSIX_KEYS.containsKey(key)) {
124 value = Files.getAttribute(path, toFsAttributeKey(key));
125 } else {
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);
133 buf.flip();
134 if (buf.hasArray())
135 value = buf.array();
136 else {
137 byte[] arr = new byte[buf.remaining()];
138 buf.get(arr);
139 value = arr;
140 }
141 }
142 } catch (IOException e) {
143 throw new ContentResourceException("Cannot retrieve attribute " + key + " for " + path, e);
144 }
145 A res = null;
146 if (value instanceof FileTime) {
147 if (clss.isAssignableFrom(FileTime.class))
148 res = (A) value;
149 Instant instant = ((FileTime) value).toInstant();
150 if (Object.class.isAssignableFrom(clss)) {// plain object requested
151 res = (A) instant;
152 }
153 // TODO perform trivial file conversion to other formats
154 }
155
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");
160
161 if (arr.length == 1) {
162 // if (clss.isAssignableFrom(String.class)) {
163 // res = (A) arr[0];
164 // } else {
165 // res = (A) CrAttributeType.parse(arr[0]);
166 // }
167 // if (isDefaultAttrTypeRequested(clss))
168 // return Optional.of((A) CrAttributeType.parse(str));
169 return CrAttributeType.cast(clss, str);
170
171 } else {
172 List<Object> lst = new ArrayList<>();
173 for (String s : arr) {
174 lst.add(CrAttributeType.parse(s));
175 }
176 res = (A) lst;
177 }
178 }
179 if (res == null) {
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();
189 // try {
190 // res = (A) value;
191 // } catch (ClassCastException e) {
192 // return Optional.empty();
193 // }
194 }
195 return Optional.of(res);
196 }
197
198 @Override
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);
202 if (udfav != null) {
203 try {
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);
210 }
211 } catch (IOException e) {
212 throw new ContentResourceException("Cannot list attributes for " + path, e);
213 }
214 }
215 return result;
216 }
217
218 @Override
219 protected void removeAttr(QName key) {
220 UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class);
221 try {
222 udfav.delete(provider.toFsPrefixedName(key));
223 } catch (IOException e) {
224 throw new ContentResourceException("Cannot delete attribute " + key + " for " + path, e);
225 }
226 }
227
228 @Override
229 public Object put(QName key, Object value) {
230 Object previous = get(key);
231
232 String toWrite;
233 if (value instanceof List) {
234 StringJoiner sj = new StringJoiner("\n");
235 for (Object obj : (List<?>) value) {
236 sj.add(obj.toString());
237 }
238 toWrite = sj.toString();
239 } else {
240 toWrite = value.toString();
241 }
242
243 UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class);
244 ByteBuffer bb = ByteBuffer.wrap(toWrite.getBytes(StandardCharsets.UTF_8));
245 try {
246 udfav.write(provider.toFsPrefixedName(key), bb);
247 } catch (IOException e) {
248 throw new ContentResourceException("Cannot delete attribute " + key + " for " + path, e);
249 }
250 return previous;
251 }
252
253 protected String toFsAttributeKey(QName key) {
254 if (POSIX_KEYS.containsKey(key))
255 return POSIX_KEYS.get(key);
256 else
257 return USER_ + provider.toFsPrefixedName(key);
258 }
259
260 /*
261 * CONTENT OPERATIONS
262 */
263 @Override
264 public Iterator<Content> iterator() {
265 if (Files.isDirectory(path)) {
266 try {
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;
276 } else {
277 return (Content) fsContent;
278 }
279 }).iterator();
280 } catch (IOException e) {
281 throw new ContentResourceException("Cannot list " + path, e);
282 }
283 } else {
284 return Collections.emptyIterator();
285 }
286 }
287
288 @Override
289 public Content add(QName name, QName... classes) {
290 FsContent fsContent;
291 try {
292 Path newPath = path.resolve(provider.toFsPrefixedName(name));
293 if (ContentName.contains(classes, DName.collection.qName()))
294 Files.createDirectory(newPath);
295 else
296 Files.createFile(newPath);
297
298 // for(ContentClass clss:classes) {
299 // Files.setAttribute(newPath, name, newPath, null)
300 // }
301 fsContent = new FsContent(this, newPath);
302 } catch (IOException e) {
303 throw new ContentResourceException("Cannot create new content", e);
304 }
305
306 if (classes.length > 0)
307 fsContent.addContentClasses(classes);
308 if (getSession().getRepository().shouldMount(classes)) {
309 ContentProvider contentProvider = getSession().getRepository().getMountContentProvider(fsContent, true,
310 classes);
311 Content mountedContent = contentProvider.get(getSession(), "");
312 fsContent.put(CrName.mount.qName(), "true");
313 return mountedContent;
314
315 } else {
316 return fsContent;
317 }
318 }
319
320 @Override
321 public void remove() {
322 try {
323 FsUtils.delete(path);
324 } catch (IOException e) {
325 throw new RuntimeException("Cannot delete " + path, e);
326 }
327 }
328
329 @Override
330 public Content getParent() {
331 if (isMountBase) {
332 String mountPath = provider.getMountPath();
333 if (mountPath == null || mountPath.equals("/"))
334 return null;
335 String[] parent = ContentUtils.getParentPath(mountPath);
336 return getSession().get(parent[0]);
337 }
338 return new FsContent(this, path.getParent());
339 }
340
341 @SuppressWarnings("unchecked")
342 @Override
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);
352 }
353 return super.open(clss);
354 }
355
356 /*
357 * MOUNT MANAGEMENT
358 */
359 @Override
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);
364 }
365
366 /*
367 * TYPING
368 */
369
370 @Override
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);
376 // res.add(name);
377 // }
378 List<QName> res = getMultiple(DName.resourcetype.qName(), QName.class);
379 if (Files.isDirectory(path))
380 res.add(DName.collection.qName());
381 return res;
382 }
383
384 @Override
385 public void addContentClasses(QName... contentClass) {
386 List<String> toWrite = new ArrayList<>();
387 for (QName cc : getContentClasses()) {
388 if (cc.equals(DName.collection.qName()))
389 continue; // skip
390 toWrite.add(NamespaceUtils.toPrefixedName(provider, cc));
391 }
392 for (QName cc : contentClass) {
393 toWrite.add(NamespaceUtils.toPrefixedName(provider, cc));
394 }
395 put(DName.resourcetype.qName(), toWrite);
396 }
397
398 /*
399 * ACCESSORS
400 */
401
402 @Override
403 public FsContentProvider getProvider() {
404 return provider;
405 }
406
407 /*
408 * READ / WRITE
409 */
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");
414 }
415 if (InputStream.class.isAssignableFrom(clss)) {
416 CompletableFuture<InputStream> res = new CompletableFuture<>();
417 res.thenAccept((in) -> {
418 try {
419 Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
420 } catch (IOException e) {
421 throw new RuntimeException("Cannot write to " + path, e);
422 }
423 });
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);
435 }
436 });
437 return (CompletableFuture<A>) res;
438 } else {
439 return super.write(clss);
440 }
441 }
442 }