From c0342975a37c70895c2e8f6b341d790700168d7f Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Thu, 25 Aug 2022 12:07:43 +0200 Subject: [PATCH] Improve ACR, introduce migration from JCR. --- .../argeo/cms/jcr/acr/JcrContentUtils.java | 172 ++++++++++++++++++ .../AbstractMaintenanceService.java | 12 ++ .../src/org/argeo/api/acr/Content.java | 29 +++ .../src/org/argeo/api/acr/NamespaceUtils.java | 8 +- .../src/org/argeo/api/cms/CmsSession.java | 4 + .../org/argeo/api/cms/DataAdminPrincipal.java | 5 + .../cms/acr/AbstractContentRepository.java | 6 + .../argeo/cms/acr/CmsContentRepository.java | 11 +- .../org/argeo/cms/acr/CmsContentSession.java | 4 +- .../src/org/argeo/cms/acr/ContentUtils.java | 55 +++++- .../src/org/argeo/cms/acr/MountManager.java | 9 +- .../src/org/argeo/cms/acr/TypesManager.java | 6 +- .../src/org/argeo/cms/acr/fs/FsContent.java | 22 +++ .../argeo/cms/acr/fs/FsContentProvider.java | 24 ++- .../src/org/argeo/cms/acr/xml/DomContent.java | 48 ++++- .../argeo/cms/acr/xml/DomContentProvider.java | 10 + .../argeo/cms/auth/DataAdminLoginModule.java | 1 + 17 files changed, 404 insertions(+), 22 deletions(-) create mode 100644 jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentUtils.java diff --git a/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentUtils.java b/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentUtils.java new file mode 100644 index 000000000..702f4d6ba --- /dev/null +++ b/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentUtils.java @@ -0,0 +1,172 @@ +package org.argeo.cms.jcr.acr; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.ContentName; +import org.argeo.api.acr.CrName; +import org.argeo.api.acr.NamespaceUtils; +import org.argeo.api.acr.spi.ProvidedContent; +import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.api.cms.CmsLog; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** Utilities around integration between JCR and ACR. */ +public class JcrContentUtils { + private final static CmsLog log = CmsLog.getLog(JcrContentUtils.class); + + public static void copyFiles(Node folder, Content collection) { + try { + nodes: for (NodeIterator it = folder.getNodes(); it.hasNext();) { + Node node = it.nextNode(); + String name = node.getName(); + if (node.isNodeType(NodeType.NT_FILE)) { + Content file = collection.anyOrAddChild(new ContentName(name)); + try (InputStream in = JcrUtils.getFileAsStream(node)) { + file.write(InputStream.class).complete(in); + } + } else if (node.isNodeType(NodeType.NT_FOLDER)) { + Content subCol = collection.add(name, CrName.collection.qName()); + copyFiles(node, subCol); + } else { + QName qName = NamespaceUtils.parsePrefixedName(name); + if (NamespaceUtils.hasNamespace(qName)) { + if (node.getIndex() > 1) { + log.warn("Same name siblings not supported, skipping " + node); + continue nodes; + } + Content content = collection.add(qName, qName); + Source source = toSource(node); + ((ProvidedContent) content).getSession().edit((s) -> { + ((ProvidedSession) s).notifyModification((ProvidedContent) content); + content.write(Source.class).complete(source); +// ContentUtils.traverse(content, +// (c, depth) -> ContentUtils.print(c, System.out, depth, false)); + }).toCompletableFuture().join(); + + } else { + // ignore + log.debug(() -> "Ignored " + node); + continue nodes; + } + } + } + } catch (RepositoryException e) { + throw new JcrException("Cannot copy files from " + folder + " to " + collection, e); + } catch (IOException e) { + throw new RuntimeException("Cannot copy files from " + folder + " to " + collection, e); + } + } + + private static Source toSource(Node node) { + try (PipedInputStream in = new PipedInputStream();) { + + CompletableFuture toDo = CompletableFuture.supplyAsync(() -> { + try { + DocumentBuilder documentBuilder = DocumentBuilderFactory.newNSInstance().newDocumentBuilder(); + return documentBuilder.parse(in); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new RuntimeException("Cannot parse", e); + } + }); + + // TODO optimise + try (PipedOutputStream out = new PipedOutputStream(in)) { + node.getSession().exportDocumentView(node.getPath(), out, true, false); + } catch (IOException | RepositoryException e) { + throw new RuntimeException("Cannot export " + node + " in workspace " + Jcr.getWorkspaceName(node), e); + } + Document document = toDo.get(); + cleanJcrDom(document); + return new DOMSource(document); + } catch (IOException | InterruptedException | ExecutionException e1) { + throw new RuntimeException("Cannot parse", e1); + } + + } + + static final String JCR_NAMESPACE_URI = "http://www.jcp.org/jcr/1.0"; + + public static void cleanJcrDom(Document document) { + Element documentElement = document.getDocumentElement(); + Set namespaceUris = new HashSet<>(); + cleanJcrDom(documentElement, namespaceUris); + + // remove unused namespaces + NamedNodeMap attrs = documentElement.getAttributes(); + Set toRemove = new HashSet<>(); + for (int i = 0; i < attrs.getLength(); i++) { + Attr attr = (Attr) attrs.item(i); +// log.debug("Check "+i+" " + attr); + String prefix = attr.getPrefix(); + if (prefix != null && prefix.equals(XMLConstants.XMLNS_ATTRIBUTE)) { + String namespaceUri = attr.getValue(); + if (!namespaceUris.contains(namespaceUri)) { + toRemove.add(attr); + //log.debug("Removing "+i+" " + namespaceUri); + } + } + } + for(Attr attr:toRemove) + documentElement.removeAttributeNode(attr); + + } + + private static void cleanJcrDom(Element element, Set namespaceUris) { + NodeList children = element.getElementsByTagName("*"); + for (int i = 0; i < children.getLength(); i++) { + Element child = (Element) children.item(i); + if (!namespaceUris.contains(child.getNamespaceURI())) + namespaceUris.add(child.getNamespaceURI()); + cleanJcrDom(child, namespaceUris); + } + + NamedNodeMap attrs = element.getAttributes(); + attributes: for (int i = 0; i < attrs.getLength(); i++) { + Attr attr = (Attr) attrs.item(i); + String namespaceUri = attr.getNamespaceURI(); + if (namespaceUri == null) + continue attributes; + if (JCR_NAMESPACE_URI.equals(namespaceUri)) { + element.removeAttributeNode(attr); + continue attributes; + } + if (!namespaceUris.contains(namespaceUri)) + namespaceUris.add(attr.getNamespaceURI()); + + } + + } + + /** singleton */ + private JcrContentUtils() { + } + +} diff --git a/jcr/org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java b/jcr/org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java index 8e2ac8960..977adac58 100644 --- a/jcr/org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java +++ b/jcr/org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java @@ -10,6 +10,7 @@ import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.Session; +import org.argeo.api.acr.spi.ProvidedRepository; import org.argeo.api.cms.CmsLog; import org.argeo.cms.jcr.CmsJcrUtils; import org.argeo.jcr.Jcr; @@ -29,6 +30,8 @@ public abstract class AbstractMaintenanceService { private UserAdmin userAdmin; private WorkTransaction userTransaction; + private ProvidedRepository contentRepository; + public void init() { makeSureRolesExists(getRequiredRoles()); configureStandardRoles(); @@ -217,4 +220,13 @@ public abstract class AbstractMaintenanceService { this.userTransaction = userTransaction; } + public void setContentRepository(ProvidedRepository contentRepository) { + this.contentRepository = contentRepository; + } + + protected ProvidedRepository getContentRepository() { + return contentRepository; + } + + } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/Content.java b/org.argeo.api.acr/src/org/argeo/api/acr/Content.java index 0fdc14fe3..4aac92de7 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/Content.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/Content.java @@ -2,6 +2,7 @@ package org.argeo.api.acr; import java.io.Closeable; import java.io.IOException; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; @@ -129,6 +130,34 @@ public interface Content extends Iterable, Map { throw new UnsupportedOperationException("Cannot write content " + this + " as " + clss.getName()); } + /* + * CHILDREN + */ + + default boolean hasChild(QName name) { + for (Content child : this) { + if (child.getName().equals(name)) + return true; + } + return false; + } + + default Content anyOrAddChild(QName name, QName... classes) { + Content child = anyChild(name); + if (child != null) + return child; + return this.add(name, classes); + } + + /** Any child with this name, or null if there is none */ + default Content anyChild(QName name) { + for (Content child : this) { + if (child.getName().equals(name)) + return child; + } + return null; + } + /* * CONVENIENCE METHODS */ diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java b/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java index 904d50ed5..64566ea0c 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java @@ -61,8 +61,8 @@ public class NamespaceUtils { }; - /** singleton */ - private NamespaceUtils() { + public static boolean hasNamespace(QName qName) { + return !qName.getNamespaceURI().equals(XMLConstants.NULL_NS_URI); } /* @@ -127,4 +127,8 @@ public class NamespaceUtils { return prefixes.iterator(); } + /** singleton */ + private NamespaceUtils() { + } + } diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/CmsSession.java b/org.argeo.api.cms/src/org/argeo/api/cms/CmsSession.java index 1d61c6c67..dda1dac1f 100644 --- a/org.argeo.api.cms/src/org/argeo/api/cms/CmsSession.java +++ b/org.argeo.api.cms/src/org/argeo/api/cms/CmsSession.java @@ -39,4 +39,8 @@ public interface CmsSession { void registerView(String uid, Object view); void addOnCloseCallback(Consumer onClose); + + public static boolean hasCmsSession(Subject subject) { + return !subject.getPrivateCredentials(CmsSessionId.class).isEmpty(); + } } diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/DataAdminPrincipal.java b/org.argeo.api.cms/src/org/argeo/api/cms/DataAdminPrincipal.java index bc12bcbe2..70c50ee9b 100644 --- a/org.argeo.api.cms/src/org/argeo/api/cms/DataAdminPrincipal.java +++ b/org.argeo.api.cms/src/org/argeo/api/cms/DataAdminPrincipal.java @@ -2,6 +2,8 @@ package org.argeo.api.cms; import java.security.Principal; +import javax.security.auth.Subject; + /** Allows to modify any data. */ public final class DataAdminPrincipal implements Principal { private final String name = CmsConstants.ROLE_DATA_ADMIN; @@ -26,4 +28,7 @@ public final class DataAdminPrincipal implements Principal { return name.toString(); } + public static boolean isDataAdmin(Subject subject) { + return !subject.getPrincipals(DataAdminPrincipal.class).isEmpty(); + } } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java index 26ba6be2a..a2d8069dd 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java @@ -216,4 +216,10 @@ public abstract class AbstractContentRepository implements ProvidedRepository { TypesManager getTypesManager() { return typesManager; } + + CmsContentSession getSystemSession() { + return systemSession; + } + + } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java index 6285710e8..474e07232 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java @@ -13,9 +13,11 @@ import org.argeo.api.acr.spi.ProvidedRepository; import org.argeo.api.cms.CmsAuth; import org.argeo.api.cms.CmsSession; import org.argeo.api.cms.CmsState; +import org.argeo.api.cms.DataAdminPrincipal; import org.argeo.api.uuid.UuidFactory; import org.argeo.cms.auth.CurrentUser; import org.argeo.cms.internal.runtime.CmsContextImpl; +import org.argeo.util.CurrentSubject; /** * Multi-session {@link ProvidedRepository}, integrated with a CMS. @@ -40,7 +42,14 @@ public class CmsContentRepository extends AbstractContentRepository { @Override public ContentSession get(Locale locale) { - // Subject subject = Subject.getSubject(AccessController.getContext()); + if (!CmsSession.hasCmsSession(CurrentSubject.current())) { + if (DataAdminPrincipal.isDataAdmin(CurrentSubject.current())) { + // TODO open multiple data admin sessions? + return getSystemSession(); + } + throw new IllegalStateException("Caller must be authenticated"); + } + CmsSession cmsSession = CurrentUser.getCmsSession(); CmsContentSession contentSession = userSessions.get(cmsSession); if (contentSession == null) { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java index f811642c7..13494dd0a 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java @@ -1,8 +1,8 @@ package org.argeo.cms.acr; +import java.util.HashSet; import java.util.Locale; import java.util.Set; -import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -34,7 +34,7 @@ class CmsContentSession implements ProvidedSession { private CompletableFuture edition; - private Set modifiedProviders = new TreeSet<>(); + private Set modifiedProviders = new HashSet<>(); private Content sessionRunDir; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java index 5ea79662a..17144cfdb 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java @@ -6,12 +6,18 @@ import java.util.List; import java.util.StringJoiner; import java.util.function.BiConsumer; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; import javax.xml.namespace.QName; import org.argeo.api.acr.Content; +import org.argeo.api.acr.ContentRepository; import org.argeo.api.acr.ContentSession; +import org.argeo.api.acr.CrName; +import org.argeo.api.cms.CmsAuth; import org.argeo.cms.CmsUserManager; import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.util.CurrentSubject; import org.osgi.service.useradmin.Role; /** Utilities and routines around {@link Content}. */ @@ -113,11 +119,6 @@ public class ContentUtils { throw new IllegalArgumentException("Path " + path + " contains //"); } - /** Singleton. */ - private ContentUtils() { - - } - public static Content roleToContent(CmsUserManager userManager, ContentSession contentSession, Role role) { UserDirectory userDirectory = userManager.getDirectory(role); String path = CmsContentRepository.DIRECTORY_BASE + SLASH + userDirectory.getName() + SLASH @@ -126,4 +127,48 @@ public class ContentUtils { return content; } + /* + * CONSUMER UTILS + */ + + public static Content createCollections(ContentSession session, String path) { + if (session.exists(path)) { + Content content = session.get(path); + if (!content.isContentClass(CrName.collection.qName())) { + throw new IllegalStateException("Content " + path + " already exists, but is not a collection"); + } else { + return content; + } + } else { + String[] parentPath = getParentPath(path); + Content parent = createCollections(session, parentPath[0]); + Content content = parent.add(parentPath[1], CrName.collection.qName()); + return content; + } + } + + public static ContentSession openDataAdminSession(ContentRepository repository) { + LoginContext loginContext; + try { + loginContext = CmsAuth.DATA_ADMIN.newLoginContext(); + loginContext.login(); + } catch (LoginException e1) { + throw new RuntimeException("Could not login as data admin", e1); + } finally { + } + + ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(ContentUtils.class.getClassLoader()); + return CurrentSubject.callAs(loginContext.getSubject(), () -> repository.get()); + } finally { + Thread.currentThread().setContextClassLoader(currentCl); + } + } + + /** Singleton. */ + private ContentUtils() { + + } + } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java b/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java index 69b76ddc6..8cb908936 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java @@ -52,8 +52,13 @@ class MountManager { if (entry == null) throw new IllegalArgumentException("No entry provider found for " + path); String mountPath = entry.getKey(); - if (!path.startsWith(mountPath)) - throw new IllegalArgumentException("Path " + path + " doesn't have a content provider"); + if (!path.startsWith(mountPath)) { + // FIXME make it more robust and find when there is no content provider + String[] parent = ContentUtils.getParentPath(path); + return findContentProvider(parent[0]); + // throw new IllegalArgumentException("Path " + path + " doesn't have a content + // provider"); + } ContentProvider contentProvider = entry.getValue(); assert mountPath.equals(contentProvider.getMountPath()); return contentProvider; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/TypesManager.java b/org.argeo.cms/src/org/argeo/cms/acr/TypesManager.java index 6cfb2e5e9..23d6042d7 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/TypesManager.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/TypesManager.java @@ -65,7 +65,7 @@ class TypesManager { private XSModel xsModel; private SortedMap> types; - private boolean validating = true; + private boolean validating = false; private final static boolean limited = false; @@ -99,6 +99,7 @@ class TypesManager { if (xsdSystemId != null) { sources.add(new StreamSource(xsdSystemId)); reload(); + log.debug(() -> "Registered types " + namespace + " from " + xsdSystemId); } } @@ -452,7 +453,8 @@ class TypesManager { try { validator.validate(source); } catch (SAXException e) { - throw new IllegalArgumentException("Provided source is not valid", e); + log.error(source + " is not valid ", e); + // throw new IllegalArgumentException("Provided source is not valid", e); } } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java index 76dc9da71..77c2f7a75 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java @@ -8,6 +8,7 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.UserDefinedFileAttributeView; import java.time.Instant; @@ -20,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import javax.xml.namespace.QName; @@ -322,4 +324,24 @@ public class FsContent extends AbstractContent implements ProvidedContent { return provider; } + /* + * READ / WRITE + */ + public CompletableFuture write(Class clss) { + if (isContentClass(CrName.collection.qName())) { + throw new IllegalStateException("Cannot directly write to a collection"); + } + if (InputStream.class.isAssignableFrom(clss)) { + CompletableFuture res = new CompletableFuture<>(); + res.thenAccept((in) -> { + try { + Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException("Cannot write to " + path, e); + } + }); + return (CompletableFuture) res; + } + return super.write(clss); + } } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContentProvider.java index 1b5741431..59a4d8deb 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContentProvider.java @@ -16,6 +16,7 @@ import java.util.stream.Collectors; import org.argeo.api.acr.ContentResourceException; import org.argeo.api.acr.CrName; import org.argeo.api.acr.NamespaceUtils; +import org.argeo.api.acr.RuntimeNamespaceContext; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; @@ -111,15 +112,15 @@ public class FsContentProvider implements ContentProvider { return new FsContent(session, this, rootPath.resolve(relativePath)); } - /* - * NAMESPACE CONTEXT - */ - @Override public boolean exists(ProvidedSession session, String relativePath) { return Files.exists(rootPath.resolve(relativePath)); } + /* + * NAMESPACE CONTEXT + */ + @Override public String getNamespaceURI(String prefix) { return NamespaceUtils.getNamespaceURI((p) -> prefixes.get(p), prefix); @@ -127,8 +128,19 @@ public class FsContentProvider implements ContentProvider { @Override public Iterator getPrefixes(String namespaceURI) { - return NamespaceUtils.getPrefixes((ns) -> prefixes.entrySet().stream().filter(e -> e.getValue().equals(ns)) - .map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()), namespaceURI); + Iterator res = NamespaceUtils.getPrefixes((ns) -> prefixes.entrySet().stream() + .filter(e -> e.getValue().equals(ns)).map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()), + namespaceURI); + if (!res.hasNext()) { + String prefix = RuntimeNamespaceContext.getNamespaceContext().getPrefix(namespaceURI); + if (prefix != null) { + registerPrefix(prefix, namespaceURI); + return getPrefixes(namespaceURI); + } else { + throw new IllegalArgumentException("Unknown namespace " + namespaceURI); + } + } + return res; } } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java index d2b5188f5..30ecd8e82 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java @@ -1,7 +1,6 @@ package org.argeo.cms.acr.xml; import java.nio.CharBuffer; -import java.nio.file.Files; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; @@ -13,16 +12,22 @@ import java.util.concurrent.CompletableFuture; import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; import javax.xml.namespace.QName; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.dom.DOMSource; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentName; -import org.argeo.api.acr.CrName; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.acr.AbstractContent; import org.argeo.cms.acr.ContentUtils; import org.w3c.dom.Attr; +import org.w3c.dom.DOMException; import org.w3c.dom.Document; +import org.w3c.dom.DocumentFragment; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; @@ -257,6 +262,9 @@ public class DomContent extends AbstractContent implements ProvidedContent { String textContent = element.getTextContent(); CharBuffer buf = CharBuffer.wrap(textContent); return (A) buf; + } else if (Source.class.isAssignableFrom(clss)) { + DOMSource source = new DOMSource(element); + return (A) source; } return super.adapt(clss); } @@ -270,6 +278,42 @@ public class DomContent extends AbstractContent implements ProvidedContent { element.setTextContent(s); }); return (CompletableFuture) res; + } else if (Source.class.isAssignableFrom(clss)) { + CompletableFuture res = new CompletableFuture<>(); + res.thenAccept((source) -> { + try { + Transformer transformer = provider.getTransformerFactory().newTransformer(); + DocumentFragment documentFragment = element.getOwnerDocument().createDocumentFragment(); + DOMResult result = new DOMResult(documentFragment); + transformer.transform(source, result); + // Node parentNode = element.getParentNode(); + Element resultElement = (Element) documentFragment.getFirstChild(); + QName resultName = toQName(resultElement); + if (!resultName.equals(getName())) + throw new IllegalArgumentException(resultName + "+ is not compatible with " + getName()); + + // attributes + NamedNodeMap attrs = resultElement.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Attr attr2 = (Attr) element.getOwnerDocument().importNode(attrs.item(i), true); + element.getAttributes().setNamedItem(attr2); + } + + // Move all the children + while (element.hasChildNodes()) { + element.removeChild(element.getFirstChild()); + } + while (resultElement.hasChildNodes()) { + element.appendChild(resultElement.getFirstChild()); + } +// parentNode.replaceChild(resultNode, element); +// element = (Element)resultNode; + + } catch (DOMException | TransformerException e) { + throw new RuntimeException("Cannot write to element", e); + } + }); + return (CompletableFuture) res; } return super.write(clss); } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java index d6e246df5..f76d38ce0 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContentProvider.java @@ -8,6 +8,7 @@ import java.util.Iterator; import java.util.List; import javax.xml.namespace.NamespaceContext; +import javax.xml.transform.TransformerFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; @@ -32,12 +33,17 @@ public class DomContentProvider implements ContentProvider, NamespaceContext { // TODO centralise in some executor? private final ThreadLocal xPath; + private TransformerFactory transformerFactory; + private String mountPath; public DomContentProvider(String mountPath, Document document) { this.mountPath = mountPath; this.document = document; this.document.normalizeDocument(); + + transformerFactory = TransformerFactory.newInstance(); + XPathFactory xPathFactory = XPathFactory.newInstance(); xPath = new ThreadLocal<>() { @@ -145,4 +151,8 @@ public class DomContentProvider implements ContentProvider, NamespaceContext { return Collections.unmodifiableList(res).iterator(); } + TransformerFactory getTransformerFactory() { + return transformerFactory; + } + } diff --git a/org.argeo.cms/src/org/argeo/cms/auth/DataAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/DataAdminLoginModule.java index ea1046be9..d4f402853 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/DataAdminLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/DataAdminLoginModule.java @@ -45,4 +45,5 @@ public class DataAdminLoginModule implements LoginModule { subject.getPrincipals().removeAll(subject.getPrincipals(DataAdminPrincipal.class)); return true; } + } -- 2.30.2