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