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