]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java
b8f98d2c84996e7fa4c6b1ad39e465037298b076
[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.namespace.QName;
28 import javax.xml.transform.Source;
29 import javax.xml.transform.TransformerException;
30 import javax.xml.transform.TransformerFactory;
31 import javax.xml.transform.stream.StreamResult;
32
33 import org.argeo.api.acr.Content;
34 import org.argeo.api.acr.ContentName;
35 import org.argeo.api.acr.ContentResourceException;
36 import org.argeo.api.acr.CrAttributeType;
37 import org.argeo.api.acr.CrName;
38 import org.argeo.api.acr.NamespaceUtils;
39 import org.argeo.api.acr.spi.ContentProvider;
40 import org.argeo.api.acr.spi.ProvidedContent;
41 import org.argeo.api.acr.spi.ProvidedSession;
42 import org.argeo.api.cms.CmsLog;
43 import org.argeo.cms.acr.AbstractContent;
44 import org.argeo.cms.acr.ContentUtils;
45 import org.argeo.util.FsUtils;
46
47 /** Content persisted as a filesystem {@link Path}. */
48 public class FsContent extends AbstractContent implements ProvidedContent {
49 private CmsLog log = CmsLog.getLog(FsContent.class);
50
51 final static String USER_ = "user:";
52
53 private static final Map<QName, String> BASIC_KEYS;
54 private static final Map<QName, String> POSIX_KEYS;
55 static {
56 BASIC_KEYS = new HashMap<>();
57 BASIC_KEYS.put(CrName.creationTime.qName(), "basic:creationTime");
58 BASIC_KEYS.put(CrName.lastModifiedTime.qName(), "basic:lastModifiedTime");
59 BASIC_KEYS.put(CrName.size.qName(), "basic:size");
60 BASIC_KEYS.put(CrName.fileKey.qName(), "basic:fileKey");
61
62 POSIX_KEYS = new HashMap<>(BASIC_KEYS);
63 POSIX_KEYS.put(CrName.owner.qName(), "owner:owner");
64 POSIX_KEYS.put(CrName.group.qName(), "posix:group");
65 POSIX_KEYS.put(CrName.permissions.qName(), "posix:permissions");
66 }
67
68 private final FsContentProvider provider;
69 private final Path path;
70 private final boolean isMountBase;
71 private final QName name;
72
73 protected FsContent(ProvidedSession session, FsContentProvider contentProvider, Path path) {
74 super(session);
75 this.provider = contentProvider;
76 this.path = path;
77 this.isMountBase = contentProvider.isMountBase(path);
78 // TODO check file names with ':' ?
79 if (isMountBase) {
80 String mountPath = provider.getMountPath();
81 if (mountPath != null && !mountPath.equals(ContentUtils.ROOT_SLASH)) {
82 Content mountPoint = session.getMountPoint(mountPath);
83 this.name = mountPoint.getName();
84 } else {
85 this.name = CrName.root.qName();
86 }
87 } else {
88
89 // TODO should we support prefixed name for known types?
90 QName providerName = NamespaceUtils.parsePrefixedName(provider, path.getFileName().toString());
91 // QName providerName = new QName(path.getFileName().toString());
92 // TODO remove extension if mounted?
93 this.name = new ContentName(providerName, session);
94 }
95 }
96
97 protected FsContent(FsContent context, Path path) {
98 this(context.getSession(), context.getProvider(), path);
99 }
100
101 private boolean isPosix() {
102 return path.getFileSystem().supportedFileAttributeViews().contains("posix");
103 }
104
105 @Override
106 public QName getName() {
107 return name;
108 }
109
110 /*
111 * ATTRIBUTES
112 */
113
114 @SuppressWarnings("unchecked")
115 @Override
116 public <A> Optional<A> get(QName key, Class<A> clss) {
117 Object value;
118 try {
119 // We need to add user: when accessing via Files#getAttribute
120
121 if (POSIX_KEYS.containsKey(key)) {
122 value = Files.getAttribute(path, toFsAttributeKey(key));
123 } else {
124 UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path,
125 UserDefinedFileAttributeView.class);
126 String prefixedName = NamespaceUtils.toPrefixedName(provider, key);
127 if (!udfav.list().contains(prefixedName))
128 return Optional.empty();
129 ByteBuffer buf = ByteBuffer.allocate(udfav.size(prefixedName));
130 udfav.read(prefixedName, buf);
131 buf.flip();
132 if (buf.hasArray())
133 value = buf.array();
134 else {
135 byte[] arr = new byte[buf.remaining()];
136 buf.get(arr);
137 value = arr;
138 }
139 }
140 } catch (IOException e) {
141 throw new ContentResourceException("Cannot retrieve attribute " + key + " for " + path, e);
142 }
143 A res = null;
144 if (value instanceof FileTime) {
145 if (clss.isAssignableFrom(FileTime.class))
146 res = (A) value;
147 Instant instant = ((FileTime) value).toInstant();
148 if (Object.class.isAssignableFrom(clss)) {// plain object requested
149 res = (A) instant;
150 }
151 // TODO perform trivial file conversion to other formats
152 }
153
154 // TODO better deal with multiple types
155 if (value instanceof byte[]) {
156 String str = new String((byte[]) value, StandardCharsets.UTF_8);
157 String[] arr = str.split("\n");
158
159 if (arr.length == 1) {
160 if (clss.isAssignableFrom(String.class)) {
161 res = (A) arr[0];
162 } else {
163 res = (A) CrAttributeType.parse(arr[0]);
164 }
165 } else {
166 List<Object> lst = new ArrayList<>();
167 for (String s : arr) {
168 lst.add(CrAttributeType.parse(s));
169 }
170 res = (A) lst;
171 }
172 }
173 if (res == null) {
174 if (isDefaultAttrTypeRequested(clss))
175 return Optional.of((A) CrAttributeType.parse(value.toString()));
176 if (clss.isAssignableFrom(value.getClass()))
177 return Optional.of((A) value);
178 if (clss.isAssignableFrom(String.class))
179 return Optional.of((A) value.toString());
180 log.warn("Cannot interpret " + key + " in " + this);
181 return Optional.empty();
182 // try {
183 // res = (A) value;
184 // } catch (ClassCastException e) {
185 // return Optional.empty();
186 // }
187 }
188 return Optional.of(res);
189 }
190
191 @Override
192 protected Iterable<QName> keys() {
193 Set<QName> result = new HashSet<>(isPosix() ? POSIX_KEYS.keySet() : BASIC_KEYS.keySet());
194 UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class);
195 if (udfav != null) {
196 try {
197 for (String name : udfav.list()) {
198 QName providerName = NamespaceUtils.parsePrefixedName(provider, name);
199 QName sessionName = new ContentName(providerName, getSession());
200 result.add(sessionName);
201 }
202 } catch (IOException e) {
203 throw new ContentResourceException("Cannot list attributes for " + path, e);
204 }
205 }
206 return result;
207 }
208
209 @Override
210 protected void removeAttr(QName key) {
211 UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class);
212 try {
213 udfav.delete(NamespaceUtils.toPrefixedName(provider, key));
214 } catch (IOException e) {
215 throw new ContentResourceException("Cannot delete attribute " + key + " for " + path, e);
216 }
217 }
218
219 @Override
220 public Object put(QName key, Object value) {
221 Object previous = get(key);
222
223 String toWrite;
224 if (value instanceof List) {
225 StringJoiner sj = new StringJoiner("\n");
226 for (Object obj : (List<?>) value) {
227 sj.add(obj.toString());
228 }
229 toWrite = sj.toString();
230 } else {
231 toWrite = value.toString();
232 }
233
234 UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class);
235 ByteBuffer bb = ByteBuffer.wrap(toWrite.getBytes(StandardCharsets.UTF_8));
236 try {
237 udfav.write(NamespaceUtils.toPrefixedName(provider, key), bb);
238 } catch (IOException e) {
239 throw new ContentResourceException("Cannot delete attribute " + key + " for " + path, e);
240 }
241 return previous;
242 }
243
244 protected String toFsAttributeKey(QName key) {
245 if (POSIX_KEYS.containsKey(key))
246 return POSIX_KEYS.get(key);
247 else
248 return USER_ + NamespaceUtils.toPrefixedName(provider, key);
249 }
250
251 /*
252 * CONTENT OPERATIONS
253 */
254 @Override
255 public Iterator<Content> iterator() {
256 if (Files.isDirectory(path)) {
257 try {
258 return Files.list(path).map((p) -> {
259 FsContent fsContent = new FsContent(this, p);
260 Optional<String> isMount = fsContent.get(CrName.mount.qName(), String.class);
261 if (isMount.orElse("false").equals("true")) {
262 QName[] classes = null;
263 ContentProvider contentProvider = getSession().getRepository()
264 .getMountContentProvider(fsContent, false, classes);
265 Content mountedContent = contentProvider.get(getSession(), "");
266 return mountedContent;
267 } else {
268 return (Content) fsContent;
269 }
270 }).iterator();
271 } catch (IOException e) {
272 throw new ContentResourceException("Cannot list " + path, e);
273 }
274 } else {
275 return Collections.emptyIterator();
276 }
277 }
278
279 @Override
280 public Content add(QName name, QName... classes) {
281 FsContent fsContent;
282 try {
283 Path newPath = path.resolve(NamespaceUtils.toPrefixedName(provider, name));
284 if (ContentName.contains(classes, CrName.collection.qName()))
285 Files.createDirectory(newPath);
286 else
287 Files.createFile(newPath);
288
289 // for(ContentClass clss:classes) {
290 // Files.setAttribute(newPath, name, newPath, null)
291 // }
292 fsContent = new FsContent(this, newPath);
293 } catch (IOException e) {
294 throw new ContentResourceException("Cannot create new content", e);
295 }
296
297 if (classes.length > 0)
298 fsContent.addContentClasses(classes);
299 if (getSession().getRepository().shouldMount(classes)) {
300 ContentProvider contentProvider = getSession().getRepository().getMountContentProvider(fsContent, true,
301 classes);
302 Content mountedContent = contentProvider.get(getSession(), "");
303 fsContent.put(CrName.mount.qName(), "true");
304 return mountedContent;
305
306 } else {
307 return fsContent;
308 }
309 }
310
311 @Override
312 public void remove() {
313 FsUtils.delete(path);
314 }
315
316 @Override
317 public Content getParent() {
318 if (isMountBase) {
319 String mountPath = provider.getMountPath();
320 if (mountPath == null || mountPath.equals("/"))
321 return null;
322 String[] parent = ContentUtils.getParentPath(mountPath);
323 return getSession().get(parent[0]);
324 }
325 return new FsContent(this, path.getParent());
326 }
327
328 @SuppressWarnings("unchecked")
329 @Override
330 public <C extends Closeable> C open(Class<C> clss) throws IOException, IllegalArgumentException {
331 if (InputStream.class.isAssignableFrom(clss)) {
332 if (Files.isDirectory(path))
333 throw new UnsupportedOperationException("Cannot open " + path + " as stream, since it is a directory");
334 return (C) Files.newInputStream(path);
335 } else if (OutputStream.class.isAssignableFrom(clss)) {
336 if (Files.isDirectory(path))
337 throw new UnsupportedOperationException("Cannot open " + path + " as stream, since it is a directory");
338 return (C) Files.newOutputStream(path);
339 }
340 return super.open(clss);
341 }
342
343 /*
344 * MOUNT MANAGEMENT
345 */
346 @Override
347 public ProvidedContent getMountPoint(String relativePath) {
348 Path childPath = path.resolve(relativePath);
349 // TODO check that it is a mount
350 return new FsContent(this, childPath);
351 }
352
353 /*
354 * TYPING
355 */
356
357 @Override
358 public List<QName> getContentClasses() {
359 List<QName> res = new ArrayList<>();
360 List<String> value = getMultiple(CrName.cc.qName(), String.class);
361 for (String s : value) {
362 QName name = NamespaceUtils.parsePrefixedName(provider, s);
363 res.add(name);
364 }
365 if (Files.isDirectory(path))
366 res.add(CrName.collection.qName());
367 return res;
368 }
369
370 @Override
371 public void addContentClasses(QName... contentClass) {
372 List<String> toWrite = new ArrayList<>();
373 for (QName cc : getContentClasses()) {
374 if (cc.equals(CrName.collection.qName()))
375 continue; // skip
376 toWrite.add(NamespaceUtils.toPrefixedName(provider, cc));
377 }
378 for (QName cc : contentClass) {
379 toWrite.add(NamespaceUtils.toPrefixedName(provider, cc));
380 }
381 put(CrName.cc.qName(), toWrite);
382 }
383
384 /*
385 * ACCESSORS
386 */
387
388 @Override
389 public FsContentProvider getProvider() {
390 return provider;
391 }
392
393 /*
394 * READ / WRITE
395 */
396 @SuppressWarnings("unchecked")
397 public <A> CompletableFuture<A> write(Class<A> clss) {
398 if (isContentClass(CrName.collection.qName())) {
399 throw new IllegalStateException("Cannot directly write to a collection");
400 }
401 if (InputStream.class.isAssignableFrom(clss)) {
402 CompletableFuture<InputStream> res = new CompletableFuture<>();
403 res.thenAccept((in) -> {
404 try {
405 Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
406 } catch (IOException e) {
407 throw new RuntimeException("Cannot write to " + path, e);
408 }
409 });
410 return (CompletableFuture<A>) res;
411 } else if (Source.class.isAssignableFrom(clss)) {
412 CompletableFuture<Source> res = new CompletableFuture<Source>();
413 res.thenAccept((source) -> {
414 // Path targetPath = path.getParent().resolve(path.getFileName()+".xml");
415 Path targetPath = path;
416 try (OutputStream out = Files.newOutputStream(targetPath)) {
417 StreamResult result = new StreamResult(out);
418 TransformerFactory.newDefaultInstance().newTransformer().transform(source, result);
419 } catch (IOException | TransformerException e) {
420 throw new RuntimeException("Cannot write to " + path, e);
421 }
422 });
423 return (CompletableFuture<A>) res;
424 } else {
425 return super.write(clss);
426 }
427 }
428 }