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