From: Mathieu Baudier Date: Fri, 3 Jun 2022 08:39:45 +0000 (+0200) Subject: Clean up and refactor ACR and component register. X-Git-Tag: v2.3.10~206 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=cc1dd97ebcc32e1bd754073ad23def182f460452;p=lgpl%2Fargeo-commons.git Clean up and refactor ACR and component register. --- diff --git a/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java b/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java index dab419794..5dd37f15b 100644 --- a/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java +++ b/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java @@ -27,11 +27,12 @@ import javax.xml.transform.Source; import javax.xml.transform.stream.StreamSource; import org.argeo.api.acr.Content; -import org.argeo.api.acr.ContentUtils; import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.acr.spi.AbstractContent; +import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.acr.ContentUtils; import org.argeo.jcr.Jcr; import org.argeo.jcr.JcrException; import org.argeo.jcr.JcrUtils; @@ -260,31 +261,16 @@ public class JcrContent extends AbstractContent { return super.open(clss); } -// class JcrKeyIterator implements Iterator { -// private final PropertyIterator propertyIterator; -// -// protected JcrKeyIterator(PropertyIterator propertyIterator) { -// this.propertyIterator = propertyIterator; -// } -// -// @Override -// public boolean hasNext() { -// return propertyIterator.hasNext(); -// } -// -// @Override -// public QName next() { -// Property property = null; -// try { -// property = propertyIterator.nextProperty(); -// // TODO map standard property names -// return NamespaceUtils.parsePrefixedName(provider, property.getName()); -// } catch (RepositoryException e) { -// throw new JcrException("Cannot retrieve property " + property, null); -// } -// } -// -// } + @Override + public ProvidedSession getSession() { + return session; + } + + @Override + public ContentProvider getProvider() { + return provider; + } + /* * STATIC UTLITIES */ diff --git a/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java b/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java index 9e0a0089a..198a2a3ed 100644 --- a/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java +++ b/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java @@ -12,11 +12,11 @@ import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.xml.namespace.NamespaceContext; -import org.argeo.api.acr.Content; -import org.argeo.api.acr.ContentUtils; import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.acr.CmsContentRepository; +import org.argeo.cms.acr.ContentUtils; import org.argeo.cms.jcr.CmsJcrUtils; import org.argeo.jcr.JcrException; import org.argeo.jcr.JcrUtils; @@ -32,6 +32,8 @@ public class JcrContentProvider implements ContentProvider, NamespaceContext { public void start(Map properties) { mountPath = properties.get(CmsContentRepository.ACR_MOUNT_PATH_PROPERTY); + if ("/".equals(mountPath)) + throw new IllegalArgumentException("JCR content provider cannot be root /"); Objects.requireNonNull(mountPath); adminSession = CmsJcrUtils.openDataAdminSession(jcrRepository, null); } @@ -45,7 +47,7 @@ public class JcrContentProvider implements ContentProvider, NamespaceContext { } @Override - public Content get(ProvidedSession contentSession, String mountPath, String relativePath) { + public ProvidedContent get(ProvidedSession contentSession, String mountPath, String relativePath) { String jcrWorkspace = ContentUtils.getParentPath(mountPath)[1]; String jcrPath = "/" + relativePath; return new JcrContent(contentSession, this, jcrWorkspace, jcrPath); diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ContentUtils.java b/org.argeo.api.acr/src/org/argeo/api/acr/ContentUtils.java deleted file mode 100644 index 5d1f59920..000000000 --- a/org.argeo.api.acr/src/org/argeo/api/acr/ContentUtils.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.argeo.api.acr; - -import java.io.PrintStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.Base64; -import java.util.List; -import java.util.function.BiConsumer; - -import javax.xml.namespace.QName; - -public class ContentUtils { - public static void traverse(Content content, BiConsumer doIt) { - traverse(content, doIt, 0); - } - - public static void traverse(Content content, BiConsumer doIt, int currentDepth) { - doIt.accept(content, currentDepth); - int nextDepth = currentDepth + 1; - for (Content child : content) { - traverse(child, doIt, nextDepth); - } - } - - public static void print(Content content, PrintStream out, int depth, boolean printText) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < depth; i++) { - sb.append(" "); - } - String prefix = sb.toString(); - out.println(prefix + content.getName()); - for (QName key : content.keySet()) { - out.println(prefix + " " + key + "=" + content.get(key)); - } - if (printText) { - if (content.hasText()) { - out.println(""); - } - } - } - - public static URI bytesToDataURI(byte[] arr) { - String base64Str = Base64.getEncoder().encodeToString(arr); - try { - final String PREFIX = "data:application/octet-stream;base64,"; - return new URI(PREFIX + base64Str); - } catch (URISyntaxException e) { - throw new IllegalStateException("Cannot serialize bytes a Base64 data URI", e); - } - - } - - public static byte[] bytesFromDataURI(URI uri) { - if (!"data".equals(uri.getScheme())) - throw new IllegalArgumentException("URI must have 'data' as a scheme"); - String schemeSpecificPart = uri.getSchemeSpecificPart(); - int commaIndex = schemeSpecificPart.indexOf(','); - String prefix = schemeSpecificPart.substring(0, commaIndex); - List info = Arrays.asList(prefix.split(";")); - if (!info.contains("base64")) - throw new IllegalArgumentException("URI must specify base64"); - - String base64Str = schemeSpecificPart.substring(commaIndex + 1); - return Base64.getDecoder().decode(base64Str); - - } - -// public static boolean isString(T t) { -// return t instanceof String; -// } - - /** - * Split a path (with '/' separator) in an array of length 2, the first part - * being the parent path (which could be either absolute or relative), the - * second one being the last segment, (guaranteed to be with '/'). - */ - public static String[] getParentPath(String path) { - int parentIndex = path.lastIndexOf('/'); - // TODO make it more robust - return new String[] { parentIndex != 0 ? path.substring(0, parentIndex) : "/", - path.substring(parentIndex + 1) }; - } - - /** Singleton. */ - private ContentUtils() { - - } - -} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java b/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java index ffa28af0a..446449ec0 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java @@ -4,6 +4,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.time.Instant; import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; import java.util.UUID; import javax.xml.XMLConstants; @@ -58,6 +61,7 @@ public enum CrAttributeType implements ContentNameSupplier { return XMLConstants.W3C_XML_SCHEMA_NS_URI; } + /** Default parsing procedure from a String to an object. */ public static Object parse(String str) { if (str == null) throw new IllegalArgumentException("String cannot be null"); @@ -113,6 +117,34 @@ public enum CrAttributeType implements ContentNameSupplier { return STRING.getFormatter().parse(str); } + /** Utility to convert a data: URI to bytes. */ + public static byte[] bytesFromDataURI(URI uri) { + if (!"data".equals(uri.getScheme())) + throw new IllegalArgumentException("URI must have 'data' as a scheme"); + String schemeSpecificPart = uri.getSchemeSpecificPart(); + int commaIndex = schemeSpecificPart.indexOf(','); + String prefix = schemeSpecificPart.substring(0, commaIndex); + List info = Arrays.asList(prefix.split(";")); + if (!info.contains("base64")) + throw new IllegalArgumentException("URI must specify base64"); + + String base64Str = schemeSpecificPart.substring(commaIndex + 1); + return Base64.getDecoder().decode(base64Str); + + } + + /** Utility to convert bytes to a data: URI. */ + public static URI bytesToDataURI(byte[] arr) { + String base64Str = Base64.getEncoder().encodeToString(arr); + try { + final String PREFIX = "data:application/octet-stream;base64,"; + return new URI(PREFIX + base64Str); + } catch (URISyntaxException e) { + throw new IllegalStateException("Cannot serialize bytes a Base64 data URI", e); + } + + } + static class BooleanFormatter implements AttributeFormatter { /** diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java b/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java index 7e089e8b2..1b29b3661 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java @@ -34,6 +34,10 @@ public enum CrName implements ContentNameSupplier { ; public final static String CR_NAMESPACE_URI = "http://argeo.org/ns/cr"; +// public final static String CR_BASIC_NAMESPACE_URI = CR_NAMESPACE_URI + "/basic"; +// public final static String CR_OWNER_NAMESPACE_URI = CR_NAMESPACE_URI + "/owner"; +// public final static String CR_POSIX_NAMESPACE_URI = CR_NAMESPACE_URI + "/posix"; + public final static String CR_DEFAULT_PREFIX = "cr"; private final ContentName value; diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/spi/AbstractContent.java b/org.argeo.api.acr/src/org/argeo/api/acr/spi/AbstractContent.java index 6bfc7cd5f..a1a37abb3 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/spi/AbstractContent.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/spi/AbstractContent.java @@ -15,7 +15,8 @@ import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.CrName; -public abstract class AbstractContent extends AbstractMap implements Content { +/** Partial reference implementation of a {@link ProvidedContent}. */ +public abstract class AbstractContent extends AbstractMap implements ProvidedContent { /* * ATTRIBUTES OPERATIONS @@ -40,6 +41,7 @@ public abstract class AbstractContent extends AbstractMap impleme return false; } + @SuppressWarnings("unchecked") @Override public Optional> getMultiple(QName key, Class clss) { Object value = get(key); @@ -153,8 +155,9 @@ public abstract class AbstractContent extends AbstractMap impleme @Override public int size() { + int count = 0; - for (QName key : keys()) { + for (Iterator it = keys().iterator(); it.hasNext();) { count++; } return count; diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java index c07a43b6e..850760134 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ContentProvider.java @@ -4,11 +4,9 @@ import java.util.Iterator; import javax.xml.namespace.NamespaceContext; -import org.argeo.api.acr.Content; - public interface ContentProvider extends NamespaceContext { - Content get(ProvidedSession session, String mountPath, String relativePath); + ProvidedContent get(ProvidedSession session, String mountPath, String relativePath); String getMountPath(); diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedContent.java b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedContent.java index d483e7fcc..d2509a49d 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedContent.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedContent.java @@ -2,6 +2,7 @@ package org.argeo.api.acr.spi; import org.argeo.api.acr.Content; +/** A {@link Content} implementation. */ public interface ProvidedContent extends Content { ProvidedSession getSession(); diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedRepository.java b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedRepository.java index dcf49d0d7..58c068996 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedRepository.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedRepository.java @@ -5,11 +5,11 @@ import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentRepository; +/** A {@link ContentRepository} implementation. */ public interface ProvidedRepository extends ContentRepository { void registerTypes(String prefix, String namespaceURI, String schemaSystemId); ContentProvider getMountContentProvider(Content mountPoint, boolean initialize, QName... types); - boolean shouldMount(QName... types); } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedSession.java b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedSession.java index 7c6912c4e..075f7bd1b 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedSession.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/spi/ProvidedSession.java @@ -6,6 +6,7 @@ import java.util.concurrent.CompletionStage; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentSession; +/** A {@link ContentSession} implementation. */ public interface ProvidedSession extends ContentSession { ProvidedRepository getRepository(); @@ -14,6 +15,9 @@ public interface ProvidedSession extends ContentSession { Content getMountPoint(String path); boolean isEditing(); + + void notifyModification(ProvidedContent content); + /* * NAMESPACE CONTEXT */ diff --git a/org.argeo.api.uuid/src/org/argeo/api/uuid/MacAddressUuidFactory.java b/org.argeo.api.uuid/src/org/argeo/api/uuid/MacAddressUuidFactory.java index fa68df1db..ab6d55d20 100644 --- a/org.argeo.api.uuid/src/org/argeo/api/uuid/MacAddressUuidFactory.java +++ b/org.argeo.api.uuid/src/org/argeo/api/uuid/MacAddressUuidFactory.java @@ -4,6 +4,7 @@ import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; +import java.util.Enumeration; import java.util.UUID; /** @@ -16,33 +17,55 @@ public class MacAddressUuidFactory extends ConcurrentUuidFactory { public final static UuidFactory DEFAULT = new MacAddressUuidFactory(); public MacAddressUuidFactory() { - this(0); + this(0, localHardwareAddressAsNodeId()); } public MacAddressUuidFactory(long initialClockRange) { - super(initialClockRange, localHardwareAddressAsNodeId()); + this(initialClockRange, localHardwareAddressAsNodeId()); } - public static byte[] localHardwareAddressAsNodeId() { + public MacAddressUuidFactory(byte[] hardwareAddress) { + this(0, hardwareAddress); + } + + public MacAddressUuidFactory(long initialClockRange, byte[] hardwareAddress) { + super(initialClockRange, hardwareAddress); + } + + private static byte[] localHardwareAddressAsNodeId() { InetAddress localHost; try { localHost = InetAddress.getLocalHost(); NetworkInterface nic = NetworkInterface.getByInetAddress(localHost); - return hardwareAddressToNodeId(nic); + if (nic != null) + return hardwareAddressToNodeId(nic); + Enumeration netInterfaces = null; + try { + netInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + throw new IllegalStateException(e); + } + if (netInterfaces == null || !netInterfaces.hasMoreElements()) + throw new IllegalStateException("No interfaces"); + return hardwareAddressToNodeId(netInterfaces.nextElement()); } catch (UnknownHostException | SocketException e) { throw new IllegalStateException(e); } } - public static byte[] hardwareAddressToNodeId(NetworkInterface nic) throws SocketException { - byte[] hardwareAddress = nic.getHardwareAddress(); - final int length = 6; - byte[] arr = new byte[length]; - for (int i = 0; i < length; i++) { - arr[i] = hardwareAddress[length - 1 - i]; + public static byte[] hardwareAddressToNodeId(NetworkInterface nic) { + try { + byte[] hardwareAddress = nic.getHardwareAddress(); + final int length = 6; + byte[] arr = new byte[length]; + for (int i = 0; i < length; i++) { + arr[i] = hardwareAddress[length - 1 - i]; + } + return arr; + } catch (SocketException e) { + throw new IllegalStateException("Cannot retrieve hardware address from NIC", e); } - return arr; } } diff --git a/org.argeo.cms/node/dc=example,dc=com.ldif b/org.argeo.cms/node/dc=example,dc=com.ldif new file mode 100644 index 000000000..43e7ade33 --- /dev/null +++ b/org.argeo.cms/node/dc=example,dc=com.ldif @@ -0,0 +1,41 @@ +dn: dc=example,dc=com +objectClass: domain +objectClass: extensibleObject +objectClass: top +dc: example + +dn: ou=Groups,dc=example,dc=com +objectClass: organizationalUnit +objectClass: top +ou: Groups + +dn: ou=People,dc=example,dc=com +objectClass: organizationalUnit +objectClass: top +ou: People + +dn: uid=demo,ou=People,dc=example,dc=com +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Demo User +description: Demo user +givenName: Demo +mail: demo@localhost +sn: User +uid: demo +userPassword:: e1NIQX1pZVNWNTVRYytlUU9hWURSU2hhL0Fqek5USkU9 + +dn: uid=root,ou=People,dc=example,dc=com +objectClass: inetOrgPerson +objectClass: person +objectClass: organizationalPerson +objectClass: top +cn: Super User +description: Superuser +givenName: Super +mail: root@localhost +sn: User +uid: root +userPassword:: e1NIQX1pZVNWNTVRYytlUU9hWURSU2hhL0Fqek5USkU9 diff --git a/org.argeo.cms/node/ou=deploy,ou=node.ldif b/org.argeo.cms/node/ou=deploy,ou=node.ldif new file mode 100644 index 000000000..52b3526c1 --- /dev/null +++ b/org.argeo.cms/node/ou=deploy,ou=node.ldif @@ -0,0 +1,15 @@ +dn: ou=org.argeo.api.userAdmin,ou=deploy,ou=node +ou: org.argeo.api.userAdmin + +dn: cn=60a26adcb1499e2b182329e94b7754275caea0b9,ou=org.argeo.api.userAdmin,ou=deploy,ou=node +baseDn: dc=example,dc=com +cn: 60a26adcb1499e2b182329e94b7754275caea0b9 + +dn: cn=f0f04f5ae5246a5678ac2be6245da45b04e623a8,ou=org.argeo.api.userAdmin,ou=deploy,ou=node +baseDn: ou=tokens,ou=node +cn: f0f04f5ae5246a5678ac2be6245da45b04e623a8 + +dn: cn=roles,ou=org.argeo.api.userAdmin,ou=deploy,ou=node +baseDn: ou=roles,ou=node +cn: roles + diff --git a/org.argeo.cms/node/ou=roles,ou=node.ldif b/org.argeo.cms/node/ou=roles,ou=node.ldif new file mode 100644 index 000000000..85247edce --- /dev/null +++ b/org.argeo.cms/node/ou=roles,ou=node.ldif @@ -0,0 +1,9 @@ +dn: ou=node +objectClass: organizationalUnit +objectClass: top +ou: node + +dn: ou=roles,ou=node +objectClass: organizationalUnit +objectClass: top +ou: roles diff --git a/org.argeo.cms/node/ou=tokens,ou=node.ldif b/org.argeo.cms/node/ou=tokens,ou=node.ldif new file mode 100644 index 000000000..4ae9b88dd --- /dev/null +++ b/org.argeo.cms/node/ou=tokens,ou=node.ldif @@ -0,0 +1,4 @@ +dn: ou=tokens,ou=node +objectClass: organizationalUnit +objectClass: top +ou: tokens 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 d91fb16fe..092876d39 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java @@ -7,18 +7,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.Locale; import java.util.Map; -import java.util.NavigableMap; import java.util.Set; -import java.util.TreeMap; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.xml.namespace.QName; @@ -29,17 +21,13 @@ import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; -import javax.xml.validation.Validator; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentSession; -import org.argeo.api.acr.ContentUtils; import org.argeo.api.acr.CrName; -import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedRepository; -import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.api.cms.CmsAuth; import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.CmsSession; @@ -58,13 +46,8 @@ import org.xml.sax.SAXException; public class CmsContentRepository implements ProvidedRepository { private final static CmsLog log = CmsLog.getLog(CmsContentRepository.class); - private NavigableMap partitions = new TreeMap<>(); - - // TODO synchronize ? -// private NavigableMap prefixes = new TreeMap<>(); - -// private Schema schema; - private ContentTypesManager contentTypesManager; + private final MountManager mountManager; + private final TypesManager typesManager; private CmsContentSession systemSession; @@ -76,14 +59,19 @@ public class CmsContentRepository implements ProvidedRepository { public final static String ACR_MOUNT_PATH_PROPERTY = "acr.mount.path"; public CmsContentRepository() { - contentTypesManager = new ContentTypesManager(); - contentTypesManager.init(); - Set types = contentTypesManager.listTypes(); + // types + typesManager = new TypesManager(); + typesManager.init(); + Set types = typesManager.listTypes(); for (QName type : types) { log.debug(type); } systemSession = newSystemSession(); + + // mounts + mountManager = new MountManager(systemSession); + } protected CmsContentSession newSystemSession() { @@ -95,7 +83,7 @@ public class CmsContentRepository implements ProvidedRepository { throw new RuntimeException("Could not login as data admin", e1); } finally { } - return new CmsContentSession(loginContext.getSubject(), Locale.getDefault()); + return new CmsContentSession(this, loginContext.getSubject(), Locale.getDefault()); } public void start() { @@ -120,7 +108,7 @@ public class CmsContentRepository implements ProvidedRepository { CmsSession cmsSession = CurrentUser.getCmsSession(); CmsContentSession contentSession = userSessions.get(cmsSession); if (contentSession == null) { - final CmsContentSession newContentSession = new CmsContentSession(cmsSession.getSubject(), locale); + final CmsContentSession newContentSession = new CmsContentSession(this, cmsSession.getSubject(), locale); cmsSession.addOnCloseCallback((c) -> { newContentSession.close(); userSessions.remove(cmsSession); @@ -130,29 +118,12 @@ public class CmsContentRepository implements ProvidedRepository { return contentSession; } - public void addProvider(String base, ContentProvider provider) { - partitions.put(base, provider); - if ("/".equals(base))// root - return; - String[] parentPath = ContentUtils.getParentPath(base); - Content parent = systemSession.get(parentPath[0]); - Content mount = parent.add(parentPath[1]); - // TODO use a boolean - // ContentName name = new ContentName(CrName.MOUNT.getNamespaceURI(), - // CrName.MOUNT.name(), systemSession); - mount.put(CrName.MOUNT.get(), "true"); + public void addProvider(ContentProvider provider) { + mountManager.addStructuralContentProvider(provider); } public void registerTypes(String prefix, String namespaceURI, String schemaSystemId) { - contentTypesManager.registerTypes(prefix, namespaceURI, schemaSystemId); -// String registeredUri = prefixes.get(prefix); -// if (registeredUri == null) { -// prefixes.put(prefix, namespaceURI); -// return; -// } -// if (!registeredUri.equals(namespaceURI)) -// throw new IllegalStateException("Prefix " + prefix + " is already registred for " + registeredUri); -// // do nothing if same namespace is already registered + typesManager.registerTypes(prefix, namespaceURI, schemaSystemId); } /* @@ -165,7 +136,7 @@ public class CmsContentRepository implements ProvidedRepository { // factory.setXIncludeAware(true); // factory.setSchema(contentTypesManager.getSchema()); // - DocumentBuilder dBuilder = contentTypesManager.newDocumentBuilder(); + DocumentBuilder dBuilder = typesManager.newDocumentBuilder(); Document document; // if (path != null && Files.exists(path)) { @@ -177,10 +148,10 @@ public class CmsContentRepository implements ProvidedRepository { document = dBuilder.newDocument(); Element root = document.createElementNS(CrName.CR_NAMESPACE_URI, CrName.ROOT.get().toPrefixedString()); - for (String prefix : contentTypesManager.getPrefixes().keySet()) { + for (String prefix : typesManager.getPrefixes().keySet()) { // root.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLConstants.XMLNS_ATTRIBUTE + ":" + prefix, // contentTypesManager.getPrefixes().get(prefix)); - DomUtils.addNamespace(root, prefix, contentTypesManager.getPrefixes().get(prefix)); + DomUtils.addNamespace(root, prefix, typesManager.getPrefixes().get(prefix)); } document.appendChild(root); @@ -193,8 +164,9 @@ public class CmsContentRepository implements ProvidedRepository { } // } - DomContentProvider contentProvider = new DomContentProvider(null, document); - addProvider("/", contentProvider); + String mountPath = "/"; + DomContentProvider contentProvider = new DomContentProvider(mountPath, document); + addProvider(contentProvider); } catch (DOMException | IOException e) { throw new IllegalStateException("Cannot init ACR root " + path, e); } @@ -208,7 +180,7 @@ public class CmsContentRepository implements ProvidedRepository { transformer.setOutputProperty(OutputKeys.INDENT, "yes"); DOMSource source = new DOMSource(document); - contentTypesManager.validate(source); + typesManager.validate(source); StreamResult result = new StreamResult(out); transformer.transform(source, result); } catch (TransformerException e) { @@ -223,176 +195,49 @@ public class CmsContentRepository implements ProvidedRepository { @Override public ContentProvider getMountContentProvider(Content mountPoint, boolean initialize, QName... types) { String mountPath = mountPoint.getPath(); - if (partitions.containsKey(mountPath)) - // TODO check consistency with types - return partitions.get(mountPath); - DocumentBuilder dBuilder = contentTypesManager.newDocumentBuilder(); - Document document; - if (initialize) { - QName firstType = types[0]; - document = dBuilder.newDocument(); - String prefix = ((ProvidedContent) mountPoint).getSession().getPrefix(firstType.getNamespaceURI()); - Element root = document.createElementNS(firstType.getNamespaceURI(), - prefix + ":" + firstType.getLocalPart()); - DomUtils.addNamespace(root, prefix, firstType.getNamespaceURI()); - document.appendChild(root); -// try (OutputStream out = mountPoint.open(OutputStream.class)) { -// writeDom(document, out); -// } catch (IOException e) { -// throw new IllegalStateException("Cannot write mount from " + mountPoint, e); -// } - } else { - try (InputStream in = mountPoint.open(InputStream.class)) { - document = dBuilder.parse(in); - // TODO check consistency with types - } catch (IOException | SAXException e) { - throw new IllegalStateException("Cannot load mount from " + mountPoint, e); + // TODO check consistency with types + + return mountManager.getOrAddMountedProvider(mountPath, (path) -> { + DocumentBuilder dBuilder = typesManager.newDocumentBuilder(); + Document document; + if (initialize) { + QName firstType = types[0]; + document = dBuilder.newDocument(); + String prefix = ((ProvidedContent) mountPoint).getSession().getPrefix(firstType.getNamespaceURI()); + Element root = document.createElementNS(firstType.getNamespaceURI(), + prefix + ":" + firstType.getLocalPart()); + DomUtils.addNamespace(root, prefix, firstType.getNamespaceURI()); + document.appendChild(root); + } else { + try (InputStream in = mountPoint.open(InputStream.class)) { + document = dBuilder.parse(in); + // TODO check consistency with types + } catch (IOException | SAXException e) { + throw new IllegalStateException("Cannot load mount from " + mountPoint, e); + } } - } - DomContentProvider contentProvider = new DomContentProvider(mountPath, document); - partitions.put(mountPath, contentProvider); - return contentProvider; + DomContentProvider contentProvider = new DomContentProvider(path, document); + return contentProvider; + }); } @Override public boolean shouldMount(QName... types) { if (types.length == 0) - throw new IllegalArgumentException("Types must be provided"); + return false; QName firstType = types[0]; - Set registeredTypes = contentTypesManager.listTypes(); + Set registeredTypes = typesManager.listTypes(); if (registeredTypes.contains(firstType)) return true; return false; } - /* - * NAMESPACE CONTEXT - */ - - /* - * SESSION - */ - - class CmsContentSession implements ProvidedSession { - private Subject subject; - private Locale locale; - - private CompletableFuture closed = new CompletableFuture<>(); - - private CompletableFuture edition; - - public CmsContentSession(Subject subject, Locale locale) { - this.subject = subject; - this.locale = locale; - } - - public void close() { - closed.complete(this); - } - - @Override - public CompletionStage onClose() { - return closed.minimalCompletionStage(); - } - - @Override - public Content get(String path) { - Map.Entry entry = partitions.floorEntry(path); - if (entry == null) - throw new IllegalArgumentException("No entry provider found for " + path); - String mountPath = entry.getKey(); - ContentProvider provider = entry.getValue(); - String relativePath = path.substring(mountPath.length()); - if (relativePath.length() > 0 && relativePath.charAt(0) == '/') - relativePath = relativePath.substring(1); - return provider.get(CmsContentSession.this, mountPath, relativePath); - } - - @Override - public Subject getSubject() { - return subject; - } - - @Override - public Locale getLocale() { - return locale; - } - - @Override - public ProvidedRepository getRepository() { - return CmsContentRepository.this; - } - - /* - * MOUNT MANAGEMENT - */ - @Override - public Content getMountPoint(String path) { - String[] parent = ContentUtils.getParentPath(path); - ProvidedContent mountParent = (ProvidedContent) get(parent[0]); -// Content mountPoint = mountParent.getProvider().get(CmsContentSession.this, null, path); - return mountParent.getMountPoint(parent[1]); - } - - /* - * NAMESPACE CONTEXT - */ - - @Override - public String getNamespaceURI(String prefix) { - return NamespaceUtils.getNamespaceURI((p) -> contentTypesManager.getPrefixes().get(p), prefix); - } - - @Override - public Iterator getPrefixes(String namespaceURI) { - return NamespaceUtils.getPrefixes( - (ns) -> contentTypesManager.getPrefixes().entrySet().stream().filter(e -> e.getValue().equals(ns)) - .map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()), - namespaceURI); - } - - @Override - public CompletionStage edit(Consumer work) { - edition = CompletableFuture.supplyAsync(() -> { - work.accept(this); - return this; - }).thenApply((s) -> { - // TODO optimise - for (ContentProvider provider : partitions.values()) { - if (provider instanceof DomContentProvider) { - ((DomContentProvider) provider).persist(s); - } - } - return s; - }); - return edition.minimalCompletionStage(); - } - - @Override - public boolean isEditing() { - return edition != null && !edition.isDone(); - } - -// @Override -// public String findNamespace(String prefix) { -// return prefixes.get(prefix); -// } -// -// @Override -// public Set findPrefixes(String namespaceURI) { -// Set res = prefixes.entrySet().stream().filter(e -> e.getValue().equals(namespaceURI)) -// .map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()); -// -// return res; -// } -// -// @Override -// public String findPrefix(String namespaceURI) { -// if (CrName.CR_NAMESPACE_URI.equals(namespaceURI) && prefixes.containsKey(CrName.CR_DEFAULT_PREFIX)) -// return CrName.CR_DEFAULT_PREFIX; -// return ProvidedSession.super.findPrefix(namespaceURI); -// } + MountManager getMountManager() { + return mountManager; + } + TypesManager getTypesManager() { + return typesManager; } } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java new file mode 100644 index 000000000..318080509 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentSession.java @@ -0,0 +1,153 @@ +package org.argeo.cms.acr; + +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.security.auth.Subject; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.ContentSession; +import org.argeo.api.acr.NamespaceUtils; +import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.api.acr.spi.ProvidedContent; +import org.argeo.api.acr.spi.ProvidedRepository; +import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.cms.acr.xml.DomContentProvider; + +/** Implements {@link ProvidedSession}. */ +class CmsContentSession implements ProvidedSession { + final private CmsContentRepository contentRepository; + + private Subject subject; + private Locale locale; + + private CompletableFuture closed = new CompletableFuture<>(); + + private CompletableFuture edition; + + private Set modifiedProviders = new TreeSet<>(); + + public CmsContentSession(CmsContentRepository contentRepository, Subject subject, Locale locale) { + this.contentRepository = contentRepository; + this.subject = subject; + this.locale = locale; + } + + public void close() { + closed.complete(this); + } + + @Override + public CompletionStage onClose() { + return closed.minimalCompletionStage(); + } + + @Override + public Content get(String path) { + ContentProvider contentProvider = contentRepository.getMountManager().findContentProvider(path); + String mountPath = contentProvider.getMountPath(); + String relativePath = path.substring(mountPath.length()); + if (relativePath.length() > 0 && relativePath.charAt(0) == '/') + relativePath = relativePath.substring(1); + ProvidedContent content = contentProvider.get(CmsContentSession.this, mountPath, relativePath); + return content; + } + + @Override + public Subject getSubject() { + return subject; + } + + @Override + public Locale getLocale() { + return locale; + } + + @Override + public ProvidedRepository getRepository() { + return contentRepository; + } + + /* + * MOUNT MANAGEMENT + */ + @Override + public Content getMountPoint(String path) { + String[] parent = ContentUtils.getParentPath(path); + ProvidedContent mountParent = (ProvidedContent) get(parent[0]); +// Content mountPoint = mountParent.getProvider().get(CmsContentSession.this, null, path); + return mountParent.getMountPoint(parent[1]); + } + + /* + * NAMESPACE CONTEXT + */ + + @Override + public String getNamespaceURI(String prefix) { + return NamespaceUtils.getNamespaceURI((p) -> contentRepository.getTypesManager().getPrefixes().get(p), prefix); + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + return NamespaceUtils.getPrefixes((ns) -> contentRepository.getTypesManager().getPrefixes().entrySet().stream() + .filter(e -> e.getValue().equals(ns)).map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()), + namespaceURI); + } + + @Override + public CompletionStage edit(Consumer work) { + edition = CompletableFuture.supplyAsync(() -> { + work.accept(this); + return this; + }).thenApply((s) -> { + // TODO optimise + for (ContentProvider provider : modifiedProviders) { + if (provider instanceof DomContentProvider) { + ((DomContentProvider) provider).persist(s); + } + } + return s; + }); + return edition.minimalCompletionStage(); + } + + @Override + public boolean isEditing() { + return edition != null && !edition.isDone(); + } + + @Override + public void notifyModification(ProvidedContent content) { + ContentProvider contentProvider = content.getProvider(); + modifiedProviders.add(contentProvider); + } + +// @Override +// public String findNamespace(String prefix) { +// return prefixes.get(prefix); +// } +// +// @Override +// public Set findPrefixes(String namespaceURI) { +// Set res = prefixes.entrySet().stream().filter(e -> e.getValue().equals(namespaceURI)) +// .map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()); +// +// return res; +// } +// +// @Override +// public String findPrefix(String namespaceURI) { +// if (CrName.CR_NAMESPACE_URI.equals(namespaceURI) && prefixes.containsKey(CrName.CR_DEFAULT_PREFIX)) +// return CrName.CR_DEFAULT_PREFIX; +// return ProvidedSession.super.findPrefix(namespaceURI); +// } + +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/acr/ContentTypesManager.java b/org.argeo.cms/src/org/argeo/cms/acr/ContentTypesManager.java deleted file mode 100644 index 48093bee0..000000000 --- a/org.argeo.cms/src/org/argeo/cms/acr/ContentTypesManager.java +++ /dev/null @@ -1,227 +0,0 @@ -package org.argeo.cms.acr; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.NavigableSet; -import java.util.Objects; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; - -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.stream.StreamSource; -import javax.xml.validation.Schema; -import javax.xml.validation.SchemaFactory; -import javax.xml.validation.Validator; - -import org.apache.xerces.impl.xs.XSImplementationImpl; -import org.apache.xerces.impl.xs.util.StringListImpl; -import org.apache.xerces.xs.StringList; -import org.apache.xerces.xs.XSAttributeDeclaration; -import org.apache.xerces.xs.XSConstants; -import org.apache.xerces.xs.XSElementDeclaration; -import org.apache.xerces.xs.XSException; -import org.apache.xerces.xs.XSImplementation; -import org.apache.xerces.xs.XSLoader; -import org.apache.xerces.xs.XSModel; -import org.apache.xerces.xs.XSNamedMap; -import org.apache.xerces.xs.XSTypeDefinition; -import org.argeo.api.acr.CrName; -import org.argeo.api.cms.CmsLog; -import org.xml.sax.ErrorHandler; -import org.xml.sax.SAXException; -import org.xml.sax.SAXParseException; - -public class ContentTypesManager { - private final static CmsLog log = CmsLog.getLog(ContentTypesManager.class); - private Map prefixes = new TreeMap<>(); - - // immutable factories - private SchemaFactory schemaFactory; - - /** Schema sources. */ - private List sources = new ArrayList<>(); - - // cached - private Schema schema; - DocumentBuilderFactory documentBuilderFactory; - private XSModel xsModel; - private NavigableSet types; - - private boolean validating = true; - - public ContentTypesManager() { - schemaFactory = SchemaFactory.newDefaultInstance(); - - // types - types = new TreeSet<>((qn1, qn2) -> { - if (Objects.equals(qn1.getNamespaceURI(), qn2.getNamespaceURI())) {// same namespace - return qn1.getLocalPart().compareTo(qn2.getLocalPart()); - } else { - return qn1.getNamespaceURI().compareTo(qn2.getNamespaceURI()); - } - }); - - } - - public synchronized void init() { -// prefixes.put(CrName.CR_DEFAULT_PREFIX, CrName.CR_NAMESPACE_URI); - prefixes.put("basic", CrName.CR_NAMESPACE_URI); - prefixes.put("owner", CrName.CR_NAMESPACE_URI); - prefixes.put("posix", CrName.CR_NAMESPACE_URI); - - for (CmsContentTypes cs : CmsContentTypes.values()) { - StreamSource source = new StreamSource(cs.getResource().toExternalForm()); - sources.add(source); - if (prefixes.containsKey(cs.getDefaultPrefix())) - throw new IllegalStateException("Prefix " + cs.getDefaultPrefix() + " is already mapped with " - + prefixes.get(cs.getDefaultPrefix())); - prefixes.put(cs.getDefaultPrefix(), cs.getNamespace()); - } - - reload(); - } - - public synchronized void registerTypes(String defaultPrefix, String namespace, String xsdSystemId) { - if (prefixes.containsKey(defaultPrefix)) - throw new IllegalStateException( - "Prefix " + defaultPrefix + " is already mapped with " + prefixes.get(defaultPrefix)); - prefixes.put(defaultPrefix, namespace); - - sources.add(new StreamSource(xsdSystemId)); - reload(); - } - - public Set listTypes() { -// TODO cache it? - return types; - } - - private synchronized void reload() { - try { - // schema - schema = schemaFactory.newSchema(sources.toArray(new Source[sources.size()])); - - // document builder factory - documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setNamespaceAware(true); - documentBuilderFactory.setXIncludeAware(true); - documentBuilderFactory.setSchema(getSchema()); - documentBuilderFactory.setValidating(validating); - - // XS model - // TODO use JVM implementation? -// DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); -// XSImplementation implementation = (XSImplementation) registry.getDOMImplementation("XS-Loader"); - XSImplementation xsImplementation = new XSImplementationImpl(); - XSLoader xsLoader = xsImplementation.createXSLoader(null); - List systemIds = new ArrayList<>(); - for (Source source : sources) { - systemIds.add(source.getSystemId()); - } - StringList sl = new StringListImpl(systemIds.toArray(new String[systemIds.size()]), systemIds.size()); - xsModel = xsLoader.loadURIList(sl); - - // types - XSNamedMap map = xsModel.getComponents(XSConstants.ELEMENT_DECLARATION); - for (int i = 0; i < map.getLength(); i++) { - XSElementDeclaration eDec = (XSElementDeclaration) map.item(i); - QName type = new QName(eDec.getNamespace(), eDec.getName()); - types.add(type); - } - } catch (XSException | SAXException e) { - throw new IllegalStateException("Cannot relaod types"); - } - } - - public DocumentBuilder newDocumentBuilder() { - try { - DocumentBuilder dBuilder = documentBuilderFactory.newDocumentBuilder(); - dBuilder.setErrorHandler(new ErrorHandler() { - - @Override - public void warning(SAXParseException exception) throws SAXException { - log.warn(exception); - } - - @Override - public void fatalError(SAXParseException exception) throws SAXException { - log.error(exception); - } - - @Override - public void error(SAXParseException exception) throws SAXException { - log.error(exception); - } - }); - return dBuilder; - } catch (ParserConfigurationException e) { - throw new IllegalStateException("Cannot create document builder", e); - } - } - - public void printTypes() { - try { - - // Convert top level complex type definitions to node types - log.debug("\n## TYPES"); - XSNamedMap map = xsModel.getComponents(XSConstants.TYPE_DEFINITION); - for (int i = 0; i < map.getLength(); i++) { - XSTypeDefinition tDef = (XSTypeDefinition) map.item(i); - log.debug(tDef); - } - // Convert local (anonymous) complex type defs found in top level - // element declarations - log.debug("\n## ELEMENTS"); - map = xsModel.getComponents(XSConstants.ELEMENT_DECLARATION); - for (int i = 0; i < map.getLength(); i++) { - XSElementDeclaration eDec = (XSElementDeclaration) map.item(i); - XSTypeDefinition tDef = eDec.getTypeDefinition(); - log.debug(eDec + ", " + tDef); - } - log.debug("\n## ATTRIBUTES"); - map = xsModel.getComponents(XSConstants.ATTRIBUTE_DECLARATION); - for (int i = 0; i < map.getLength(); i++) { - XSAttributeDeclaration eDec = (XSAttributeDeclaration) map.item(i); - XSTypeDefinition tDef = eDec.getTypeDefinition(); - log.debug(eDec.getNamespace() + ":" + eDec.getName() + ", " + tDef); - } - } catch (ClassCastException | XSException e) { - throw new RuntimeException(e); - } - - } - - public void validate(Source source) throws IOException { - if (!validating) - return; - Validator validator; - synchronized (this) { - validator = schema.newValidator(); - } - try { - validator.validate(source); - } catch (SAXException e) { - throw new IllegalArgumentException("Provided source is not valid", e); - } - } - - public Map getPrefixes() { - return prefixes; - } - - public List getSources() { - return sources; - } - - public Schema getSchema() { - return schema; - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java new file mode 100644 index 000000000..d0ce5d153 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java @@ -0,0 +1,63 @@ +package org.argeo.cms.acr; + +import java.io.PrintStream; +import java.util.function.BiConsumer; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; + +public class ContentUtils { + public static void traverse(Content content, BiConsumer doIt) { + traverse(content, doIt, 0); + } + + public static void traverse(Content content, BiConsumer doIt, int currentDepth) { + doIt.accept(content, currentDepth); + int nextDepth = currentDepth + 1; + for (Content child : content) { + traverse(child, doIt, nextDepth); + } + } + + public static void print(Content content, PrintStream out, int depth, boolean printText) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < depth; i++) { + sb.append(" "); + } + String prefix = sb.toString(); + out.println(prefix + content.getName()); + for (QName key : content.keySet()) { + out.println(prefix + " " + key + "=" + content.get(key)); + } + if (printText) { + if (content.hasText()) { + out.println(""); + } + } + } + + + +// public static boolean isString(T t) { +// return t instanceof String; +// } + + /** + * Split a path (with '/' separator) in an array of length 2, the first part + * being the parent path (which could be either absolute or relative), the + * second one being the last segment, (guaranteed to be with '/'). + */ + public static String[] getParentPath(String path) { + int parentIndex = path.lastIndexOf('/'); + // TODO make it more robust + return new String[] { parentIndex != 0 ? path.substring(0, parentIndex) : "/", + path.substring(parentIndex + 1) }; + } + + /** 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 new file mode 100644 index 000000000..3995dc30f --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/MountManager.java @@ -0,0 +1,59 @@ +package org.argeo.cms.acr; + +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.TreeMap; +import java.util.function.Function; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.CrName; +import org.argeo.api.acr.spi.ContentProvider; + +/** Manages the structural and dynamic mounts within the content repository. */ +class MountManager { + private final NavigableMap partitions = new TreeMap<>(); + + private final CmsContentSession systemSession; + + public MountManager(CmsContentSession systemSession) { + this.systemSession = systemSession; + } + + synchronized void addStructuralContentProvider(ContentProvider contentProvider) { + String mountPath = contentProvider.getMountPath(); + Objects.requireNonNull(mountPath); + if (partitions.containsKey(mountPath)) + throw new IllegalStateException("A provider is already registered for " + mountPath); + partitions.put(mountPath, contentProvider); + if ("/".equals(mountPath))// root + return; + String[] parentPath = ContentUtils.getParentPath(mountPath); + Content parent = systemSession.get(parentPath[0]); + Content mount = parent.add(parentPath[1]); + mount.put(CrName.MOUNT.get(), "true"); + + } + + synchronized ContentProvider getOrAddMountedProvider(String mountPath, Function factory) { + Objects.requireNonNull(factory); + if (!partitions.containsKey(mountPath)) { + ContentProvider contentProvider = factory.apply(mountPath); + if (!mountPath.equals(contentProvider.getMountPath())) + throw new IllegalArgumentException("Mount path " + mountPath + " is inconsistent with content provider " + + contentProvider.getMountPath()); + partitions.put(mountPath, contentProvider); + } + return partitions.get(mountPath); + } + + synchronized ContentProvider findContentProvider(String path) { + Map.Entry entry = partitions.floorEntry(path); + if (entry == null) + throw new IllegalArgumentException("No entry provider found for " + path); + String mountPath = entry.getKey(); + 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 new file mode 100644 index 000000000..dd2646fcd --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/TypesManager.java @@ -0,0 +1,227 @@ +package org.argeo.cms.acr; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +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.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import org.apache.xerces.impl.xs.XSImplementationImpl; +import org.apache.xerces.impl.xs.util.StringListImpl; +import org.apache.xerces.xs.StringList; +import org.apache.xerces.xs.XSAttributeDeclaration; +import org.apache.xerces.xs.XSConstants; +import org.apache.xerces.xs.XSElementDeclaration; +import org.apache.xerces.xs.XSException; +import org.apache.xerces.xs.XSImplementation; +import org.apache.xerces.xs.XSLoader; +import org.apache.xerces.xs.XSModel; +import org.apache.xerces.xs.XSNamedMap; +import org.apache.xerces.xs.XSTypeDefinition; +import org.argeo.api.cms.CmsLog; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** Register content types. */ +class TypesManager { + private final static CmsLog log = CmsLog.getLog(TypesManager.class); + private Map prefixes = new TreeMap<>(); + + // immutable factories + private SchemaFactory schemaFactory; + + /** Schema sources. */ + private List sources = new ArrayList<>(); + + // cached + private Schema schema; + DocumentBuilderFactory documentBuilderFactory; + private XSModel xsModel; + private NavigableSet types; + + private boolean validating = true; + + public TypesManager() { + schemaFactory = SchemaFactory.newDefaultInstance(); + + // types + types = new TreeSet<>((qn1, qn2) -> { + if (Objects.equals(qn1.getNamespaceURI(), qn2.getNamespaceURI())) {// same namespace + return qn1.getLocalPart().compareTo(qn2.getLocalPart()); + } else { + return qn1.getNamespaceURI().compareTo(qn2.getNamespaceURI()); + } + }); + + } + + public synchronized void init() { +// prefixes.put(CrName.CR_DEFAULT_PREFIX, CrName.CR_NAMESPACE_URI); +// prefixes.put("basic", CrName.CR_NAMESPACE_URI); +// prefixes.put("owner", CrName.CR_NAMESPACE_URI); +// prefixes.put("posix", CrName.CR_NAMESPACE_URI); + + for (CmsContentTypes cs : CmsContentTypes.values()) { + StreamSource source = new StreamSource(cs.getResource().toExternalForm()); + sources.add(source); + if (prefixes.containsKey(cs.getDefaultPrefix())) + throw new IllegalStateException("Prefix " + cs.getDefaultPrefix() + " is already mapped with " + + prefixes.get(cs.getDefaultPrefix())); + prefixes.put(cs.getDefaultPrefix(), cs.getNamespace()); + } + + reload(); + } + + public synchronized void registerTypes(String defaultPrefix, String namespace, String xsdSystemId) { + if (prefixes.containsKey(defaultPrefix)) + throw new IllegalStateException( + "Prefix " + defaultPrefix + " is already mapped with " + prefixes.get(defaultPrefix)); + prefixes.put(defaultPrefix, namespace); + + sources.add(new StreamSource(xsdSystemId)); + reload(); + } + + public Set listTypes() { +// TODO cache it? + return types; + } + + private synchronized void reload() { + try { + // schema + schema = schemaFactory.newSchema(sources.toArray(new Source[sources.size()])); + + // document builder factory + documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + documentBuilderFactory.setXIncludeAware(true); + documentBuilderFactory.setSchema(getSchema()); + documentBuilderFactory.setValidating(validating); + + // XS model + // TODO use JVM implementation? +// DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); +// XSImplementation implementation = (XSImplementation) registry.getDOMImplementation("XS-Loader"); + XSImplementation xsImplementation = new XSImplementationImpl(); + XSLoader xsLoader = xsImplementation.createXSLoader(null); + List systemIds = new ArrayList<>(); + for (Source source : sources) { + systemIds.add(source.getSystemId()); + } + StringList sl = new StringListImpl(systemIds.toArray(new String[systemIds.size()]), systemIds.size()); + xsModel = xsLoader.loadURIList(sl); + + // types + XSNamedMap map = xsModel.getComponents(XSConstants.ELEMENT_DECLARATION); + for (int i = 0; i < map.getLength(); i++) { + XSElementDeclaration eDec = (XSElementDeclaration) map.item(i); + QName type = new QName(eDec.getNamespace(), eDec.getName()); + types.add(type); + } + } catch (XSException | SAXException e) { + throw new IllegalStateException("Cannot relaod types"); + } + } + + public DocumentBuilder newDocumentBuilder() { + try { + DocumentBuilder dBuilder = documentBuilderFactory.newDocumentBuilder(); + dBuilder.setErrorHandler(new ErrorHandler() { + + @Override + public void warning(SAXParseException exception) throws SAXException { + log.warn(exception); + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + log.error(exception); + } + + @Override + public void error(SAXParseException exception) throws SAXException { + log.error(exception); + } + }); + return dBuilder; + } catch (ParserConfigurationException e) { + throw new IllegalStateException("Cannot create document builder", e); + } + } + + public void printTypes() { + try { + + // Convert top level complex type definitions to node types + log.debug("\n## TYPES"); + XSNamedMap map = xsModel.getComponents(XSConstants.TYPE_DEFINITION); + for (int i = 0; i < map.getLength(); i++) { + XSTypeDefinition tDef = (XSTypeDefinition) map.item(i); + log.debug(tDef); + } + // Convert local (anonymous) complex type defs found in top level + // element declarations + log.debug("\n## ELEMENTS"); + map = xsModel.getComponents(XSConstants.ELEMENT_DECLARATION); + for (int i = 0; i < map.getLength(); i++) { + XSElementDeclaration eDec = (XSElementDeclaration) map.item(i); + XSTypeDefinition tDef = eDec.getTypeDefinition(); + log.debug(eDec + ", " + tDef); + } + log.debug("\n## ATTRIBUTES"); + map = xsModel.getComponents(XSConstants.ATTRIBUTE_DECLARATION); + for (int i = 0; i < map.getLength(); i++) { + XSAttributeDeclaration eDec = (XSAttributeDeclaration) map.item(i); + XSTypeDefinition tDef = eDec.getTypeDefinition(); + log.debug(eDec.getNamespace() + ":" + eDec.getName() + ", " + tDef); + } + } catch (ClassCastException | XSException e) { + throw new RuntimeException(e); + } + + } + + public void validate(Source source) throws IOException { + if (!validating) + return; + Validator validator; + synchronized (this) { + validator = schema.newValidator(); + } + try { + validator.validate(source); + } catch (SAXException e) { + throw new IllegalArgumentException("Provided source is not valid", e); + } + } + + public Map getPrefixes() { + return prefixes; + } + + public List getSources() { + return sources; + } + + public Schema getSchema() { + return schema; + } + +} 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 a2cc52c42..c6266ee4f 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 @@ -24,13 +24,13 @@ import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentName; import org.argeo.api.acr.ContentResourceException; -import org.argeo.api.acr.ContentUtils; import org.argeo.api.acr.CrName; import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.acr.spi.AbstractContent; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.cms.acr.ContentUtils; import org.argeo.util.FsUtils; public class FsContent extends AbstractContent implements ProvidedContent { @@ -65,7 +65,7 @@ public class FsContent extends AbstractContent implements ProvidedContent { // TODO check file names with ':' ? if (isRoot) { String mountPath = provider.getMountPath(); - if (mountPath != null) { + if (mountPath != null && !mountPath.equals("/")) { Content mountPoint = session.getMountPoint(mountPath); this.name = mountPoint.getName(); } else { @@ -99,7 +99,26 @@ public class FsContent extends AbstractContent implements ProvidedContent { Object value; try { // We need to add user: when accessing via Files#getAttribute - value = Files.getAttribute(path, toFsAttributeKey(key)); + + if (POSIX_KEYS.containsKey(key)) { + value = Files.getAttribute(path, toFsAttributeKey(key)); + } else { + UserDefinedFileAttributeView udfav = Files.getFileAttributeView(path, + UserDefinedFileAttributeView.class); + String prefixedName = NamespaceUtils.toPrefixedName(provider, key); + if (!udfav.list().contains(prefixedName)) + return Optional.empty(); + ByteBuffer buf = ByteBuffer.allocate(udfav.size(prefixedName)); + udfav.read(prefixedName, buf); + buf.flip(); + if (buf.hasArray()) + value = buf.array(); + else { + byte[] arr = new byte[buf.remaining()]; + buf.get(arr); + value = arr; + } + } } catch (IOException e) { throw new ContentResourceException("Cannot retrieve attribute " + key + " for " + path, e); } @@ -239,7 +258,7 @@ public class FsContent extends AbstractContent implements ProvidedContent { public Content getParent() { if (isRoot) { String mountPath = provider.getMountPath(); - if (mountPath == null) + if (mountPath == null || mountPath.equals("/")) return null; String[] parent = ContentUtils.getParentPath(mountPath); return session.get(parent[0]); 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 3445733e1..e17384b94 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 @@ -17,6 +17,7 @@ import org.argeo.api.acr.ContentResourceException; import org.argeo.api.acr.CrName; import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; /** Access a file system as a {@link ContentProvider}. */ @@ -99,7 +100,7 @@ public class FsContentProvider implements ContentProvider { } @Override - public Content get(ProvidedSession session, String mountPath, String relativePath) { + public ProvidedContent get(ProvidedSession session, String mountPath, String relativePath) { return new FsContent(session, this, rootPath.resolve(relativePath)); } 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 9a1a58c55..bfea129b1 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.Path; import java.util.HashSet; import java.util.Iterator; import java.util.Optional; @@ -14,12 +13,10 @@ import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentName; -import org.argeo.api.acr.ContentUtils; -import org.argeo.api.acr.CrName; import org.argeo.api.acr.spi.AbstractContent; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; -import org.argeo.cms.acr.fs.FsContent; +import org.argeo.cms.acr.ContentUtils; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -242,7 +239,10 @@ public class DomContent extends AbstractContent implements ProvidedContent { public CompletableFuture write(Class clss) { if (String.class.isAssignableFrom(clss)) { CompletableFuture res = new CompletableFuture<>(); - res.thenAccept((s) -> element.setTextContent(s));// .thenRun(() -> provider.persist(session)); + res.thenAccept((s) -> { + session.notifyModification(this); + element.setTextContent(s); + }); 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 8caac1ad2..80523cb6c 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 @@ -17,6 +17,7 @@ import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentNotFoundException; import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.acr.CmsContentRepository; import org.w3c.dom.Document; @@ -60,7 +61,7 @@ public class DomContentProvider implements ContentProvider, NamespaceContext { // } @Override - public Content get(ProvidedSession session, String mountPath, String relativePath) { + public ProvidedContent get(ProvidedSession session, String mountPath, String relativePath) { if ("".equals(relativePath)) return new DomContent(session, this, document.getDocumentElement()); if (relativePath.startsWith("/")) diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java index aaf02e06a..4fee40688 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java @@ -8,7 +8,6 @@ import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsState; import org.argeo.cms.acr.CmsContentRepository; import org.argeo.cms.acr.fs.FsContentProvider; -import org.argeo.util.LangUtils; public class DeployedContentRepository extends CmsContentRepository { private final static String ROOT_XML = "cr:root.xml"; @@ -21,8 +20,8 @@ public class DeployedContentRepository extends CmsContentRepository { initRootContentProvider(rootXml); Path srvPath = KernelUtils.getOsgiInstancePath(CmsConstants.SRV_WORKSPACE); - FsContentProvider srvContentProvider = new FsContentProvider(CmsConstants.SRV_WORKSPACE, srvPath, false); - addProvider("/" + CmsConstants.SRV_WORKSPACE, srvContentProvider); + FsContentProvider srvContentProvider = new FsContentProvider("/" + CmsConstants.SRV_WORKSPACE, srvPath, false); + addProvider(srvContentProvider); } @Override @@ -31,8 +30,8 @@ public class DeployedContentRepository extends CmsContentRepository { } public void addContentProvider(ContentProvider provider, Map properties) { - String base = LangUtils.get(properties, CmsContentRepository.ACR_MOUNT_PATH_PROPERTY); - addProvider(base, provider); +// String base = LangUtils.get(properties, CmsContentRepository.ACR_MOUNT_PATH_PROPERTY); + addProvider(provider); } public void removeContentProvider(ContentProvider provider, Map properties) { diff --git a/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java b/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java index 5ffff1d0e..540da48c2 100644 --- a/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java +++ b/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java @@ -1,6 +1,7 @@ package org.argeo.cms.runtime; import java.util.Dictionary; +import java.util.concurrent.CompletableFuture; import org.argeo.api.cms.CmsContext; import org.argeo.api.cms.CmsDeployment; @@ -14,8 +15,7 @@ import org.argeo.osgi.transaction.SimpleTransactionManager; import org.argeo.osgi.transaction.WorkControl; import org.argeo.osgi.transaction.WorkTransaction; import org.argeo.util.register.Component; -import org.argeo.util.register.ComponentRegister; -import org.argeo.util.register.StaticRegister; +import org.argeo.util.register.SimpleRegister; import org.osgi.service.useradmin.UserAdmin; /** @@ -23,10 +23,11 @@ import org.osgi.service.useradmin.UserAdmin; * deployment. Useful for testing or AOT compilation. */ public class StaticCms { + private static SimpleRegister register = new SimpleRegister(); - public void start() { - ComponentRegister register = StaticRegister.getInstance(); + private CompletableFuture stopped = new CompletableFuture(); + public void start() { // CMS State CmsStateImpl cmsState = new CmsStateImpl(); Component cmsStateC = new Component.Builder<>(cmsState) // @@ -83,21 +84,28 @@ public class StaticCms { .addDependency(cmsDeploymentC.getType(CmsDeployment.class), cmsContext::setCmsDeployment, null) // .addDependency(userAdminC.getType(UserAdmin.class), cmsContext::setUserAdmin, null) // .build(register); - assert cmsContextC.getInstance() == cmsContext; + assert cmsContextC.get() == cmsContext; register.activate(); } public void stop() { - if (StaticRegister.getInstance().isActive()) - StaticRegister.getInstance().deactivate(); + if (register.isActive()) { + register.deactivate(); + } + register.clear(); + stopped.complete(null); + } + + public void waitForStop() { + stopped.join(); } public static void main(String[] args) { StaticCms staticCms = new StaticCms(); Runtime.getRuntime().addShutdownHook(new Thread(() -> staticCms.stop(), "Static CMS Shutdown")); staticCms.start(); - + staticCms.waitForStop(); } } diff --git a/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java b/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java index 7132b7c3f..5728b90db 100644 --- a/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java +++ b/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java @@ -5,16 +5,13 @@ import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ForkJoinPool; -import org.argeo.util.register.Register; -import org.argeo.util.register.Singleton; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; -public class OsgiRegister implements Register { +public class OsgiRegister { private final BundleContext bundleContext; private Executor executor; @@ -26,8 +23,7 @@ public class OsgiRegister implements Register { this.executor = ForkJoinPool.commonPool(); } - @Override - public Singleton set(T obj, Class clss, Map attributes, Class... classes) { + public void set(T obj, Class clss, Map attributes, Class... classes) { CompletableFuture> srf = new CompletableFuture>(); CompletableFuture postRegistration = CompletableFuture.supplyAsync(() -> { List lst = new ArrayList<>(); @@ -40,18 +36,18 @@ public class OsgiRegister implements Register { srf.complete(sr); return obj; }, executor); - Singleton singleton = new Singleton(clss, postRegistration); - - shutdownStarting. // - thenCompose(singleton::prepareUnregistration). // - thenRunAsync(() -> { - try { - srf.get().unregister(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - }, executor); - return singleton; +// Singleton singleton = new Singleton(clss, postRegistration); + +// shutdownStarting. // +// thenCompose(singleton::prepareUnregistration). // +// thenRunAsync(() -> { +// try { +// srf.get().unregister(); +// } catch (InterruptedException | ExecutionException e) { +// e.printStackTrace(); +// } +// }, executor); +// return singleton; } public void shutdown() { diff --git a/org.argeo.util/src/org/argeo/util/register/Component.java b/org.argeo.util/src/org/argeo/util/register/Component.java index 66ac2ada9..cb69b133d 100644 --- a/org.argeo.util/src/org/argeo/util/register/Component.java +++ b/org.argeo.util/src/org/argeo/util/register/Component.java @@ -6,12 +6,14 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.function.Consumer; +import java.util.function.Supplier; /** * A wrapper for an object, whose dependencies and life cycle can be managed. */ -public class Component { +public class Component implements Supplier, Comparable> { private final I instance; @@ -20,6 +22,7 @@ public class Component { private final Map, PublishedType> types; private final Set> dependencies; + private final Map properties; private CompletableFuture activationStarted = null; private CompletableFuture activated = null; @@ -27,10 +30,13 @@ public class Component { private CompletableFuture deactivationStarted = null; private CompletableFuture deactivated = null; + // internal private Set> dependants = new HashSet<>(); - Component(Consumer> register, I instance, Runnable init, Runnable close, - Set> dependencies, Set> classes) { + private RankingKey rankingKey; + + Component(ComponentRegister register, I instance, Runnable init, Runnable close, Set> dependencies, + Set> classes, Map properties) { assert instance != null; assert init != null; assert close != null; @@ -64,7 +70,11 @@ public class Component { // TODO check whether context is active, so that we start right away prepareNextActivation(); - register.accept(this); + long serviceId = register.register(this); + Map props = new HashMap<>(properties); + props.put(RankingKey.SERVICE_ID, serviceId); + this.properties = Collections.unmodifiableMap(props); + rankingKey = new RankingKey(properties); } private void prepareNextActivation() { @@ -130,7 +140,8 @@ public class Component { dependants.add(dependant); } - public I getInstance() { + @Override + public I get() { return instance; } @@ -145,6 +156,15 @@ public class Component { return types.containsKey(clss); } + public Map getProperties() { + return properties; + } + + @Override + public int compareTo(Component o) { + return rankingKey.compareTo(rankingKey); + } + /** A type which has been explicitly exposed by a component. */ public static class PublishedType { private Component component; @@ -165,10 +185,14 @@ public class Component { public Class getType() { return clss; } + + public CompletionStage getValue() { + return value.minimalCompletionStage(); + } } /** Builds a {@link Component}. */ - public static class Builder { + public static class Builder implements Supplier { private final I instance; private Runnable init; @@ -176,12 +200,13 @@ public class Component { private Set> dependencies = new HashSet<>(); private Set> types = new HashSet<>(); + private final Map properties = new HashMap<>(); public Builder(I instance) { this.instance = instance; } - public Component build(Consumer> register) { + public Component build(ComponentRegister register) { // default values if (types.isEmpty()) { types.add(getInstanceClass()); @@ -195,7 +220,7 @@ public class Component { }; // instantiation - Component component = new Component(register, instance, init, close, dependencies, types); + Component component = new Component(register, instance, init, close, dependencies, types, properties); for (Dependency dependency : dependencies) { dependency.type.getPublisher().addDependant(dependency); } @@ -226,6 +251,13 @@ public class Component { return this; } + public void addProperty(String key, Object value) { + if (properties.containsKey(key)) + throw new IllegalStateException("Key " + key + " is already set."); + properties.put(key, value); + } + + @Override public I get() { return instance; } diff --git a/org.argeo.util/src/org/argeo/util/register/ComponentRegister.java b/org.argeo.util/src/org/argeo/util/register/ComponentRegister.java index eb8acba02..d5f9ea421 100644 --- a/org.argeo.util/src/org/argeo/util/register/ComponentRegister.java +++ b/org.argeo.util/src/org/argeo/util/register/ComponentRegister.java @@ -1,14 +1,24 @@ package org.argeo.util.register; import java.util.Map; -import java.util.function.Consumer; +import java.util.SortedSet; import java.util.function.Predicate; -public interface ComponentRegister extends Consumer> { - Component find(Class clss, Predicate> filter); +/** A register of components which can coordinate their activation. */ +public interface ComponentRegister { + long register(Component component); + + SortedSet> find(Class clss, Predicate> filter); Component get(Object instance); +// default PublishedType getType(Class clss) { +// SortedSet> components = find(clss, null); +// if (components.size() == 0) +// return null; +// return components.first().getType(clss); +// } + void activate(); void deactivate(); diff --git a/org.argeo.util/src/org/argeo/util/register/RankingKey.java b/org.argeo.util/src/org/argeo/util/register/RankingKey.java new file mode 100644 index 000000000..7a43e359e --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/register/RankingKey.java @@ -0,0 +1,105 @@ +package org.argeo.util.register; + +import java.util.Map; +import java.util.Objects; + +/** + * Key used to classify and filter available components. + */ +public class RankingKey implements Comparable { + public final static String SERVICE_PID = "service.pid"; + public final static String SERVICE_ID = "service.id"; + public final static String SERVICE_RANKING = "service.ranking"; + + private String pid; + private Integer ranking = 0; + private Long id = 0l; + + public RankingKey(String pid, Integer ranking, Long id) { + super(); + this.pid = pid; + this.ranking = ranking; + this.id = id; + } + + public RankingKey(Map properties) { + this.pid = properties.containsKey(SERVICE_PID) ? properties.get(SERVICE_PID).toString() : null; + this.ranking = properties.containsKey(SERVICE_RANKING) + ? Integer.parseInt(properties.get(SERVICE_RANKING).toString()) + : 0; + this.id = properties.containsKey(SERVICE_ID) ? (Long) properties.get(SERVICE_ID) : null; + } + + @Override + public int hashCode() { + Integer result = 0; + if (pid != null) + result = +pid.hashCode(); + if (ranking != null) + result = +ranking; + return result; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return new RankingKey(pid, ranking, id); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(""); + if (pid != null) + sb.append(pid); + if (ranking != null && ranking != 0) + sb.append(' ').append(ranking); + return sb.toString(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RankingKey)) + return false; + RankingKey other = (RankingKey) obj; + return Objects.equals(pid, other.pid) && Objects.equals(ranking, other.ranking) && Objects.equals(id, other.id); + } + + @Override + public int compareTo(RankingKey o) { + if (pid != null && o.pid != null) { + if (pid.equals(o.pid)) { + if (ranking.equals(o.ranking)) + if (id != null && o.id != null) + return id.compareTo(o.id); + else + return 0; + else + return ranking.compareTo(o.ranking); + } else { + return pid.compareTo(o.pid); + } + + } else { + } + return -1; + } + + public String getPid() { + return pid; + } + + public Integer getRanking() { + return ranking; + } + + public Long getId() { + return id; + } + + public static RankingKey minPid(String pid) { + return new RankingKey(pid, Integer.MIN_VALUE, null); + } + + public static RankingKey maxPid(String pid) { + return new RankingKey(pid, Integer.MAX_VALUE, null); + } +} diff --git a/org.argeo.util/src/org/argeo/util/register/Register.java b/org.argeo.util/src/org/argeo/util/register/Register.java deleted file mode 100644 index 17062809e..000000000 --- a/org.argeo.util/src/org/argeo/util/register/Register.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.argeo.util.register; - -import java.util.Map; - -/** A dynamic register of objects. */ -public interface Register { - Singleton set(T obj, Class clss, Map attributes, Class... classes); - - void shutdown(); -} diff --git a/org.argeo.util/src/org/argeo/util/register/SimpleRegister.java b/org.argeo.util/src/org/argeo/util/register/SimpleRegister.java new file mode 100644 index 000000000..7aa9ebef9 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/register/SimpleRegister.java @@ -0,0 +1,124 @@ +package org.argeo.util.register; + +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Predicate; + +/** A minimal component register. */ +public class SimpleRegister implements ComponentRegister { + private final AtomicBoolean started = new AtomicBoolean(false); + private final IdentityHashMap> components = new IdentityHashMap<>(); + private final AtomicLong nextServiceId = new AtomicLong(0l); + + @Override + public long register(Component component) { + return registerComponent(component); + } + + @SuppressWarnings({ "unchecked" }) + @Override + public synchronized SortedSet> find(Class clss, + Predicate> filter) { + SortedSet> result = new TreeSet<>(); + instances: for (Object instance : components.keySet()) { + if (!clss.isAssignableFrom(instance.getClass())) + continue instances; + Component component = (Component) components.get(instance); + + if (component.isPublishedType(clss)) { + if (filter != null) { + filter.test(component.getProperties()); + } + result.add(component); + } + } + if (result.isEmpty()) + return null; + return result; + + } + + synchronized long registerComponent(Component component) { + if (started.get()) // TODO make it really dynamic + throw new IllegalStateException("Already activated"); + if (components.containsKey(component.get())) + throw new IllegalArgumentException("Already registered as component"); + components.put(component.get(), component); + return nextServiceId.incrementAndGet(); + } + + @Override + public synchronized Component get(Object instance) { + if (!components.containsKey(instance)) + throw new IllegalArgumentException("Not registered as component"); + return components.get(instance); + } + + @Override + public synchronized void activate() { + if (started.get()) + throw new IllegalStateException("Already activated"); + Set> constraints = new HashSet<>(); + for (Component component : components.values()) { + component.startActivating(); + constraints.add(component.getActivated()); + } + + // wait + try { + CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(true)) + .get(); + } catch (InterruptedException e) { + throw new RuntimeException("Register activation has been interrupted", e); + } catch (ExecutionException e) { + if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) { + throw (RuntimeException) e.getCause(); + } else { + throw new IllegalStateException("Cannot activate register", e.getCause()); + } + } + } + + @Override + public synchronized void deactivate() { + if (!started.get()) + throw new IllegalStateException("Not activated"); + Set> constraints = new HashSet<>(); + for (Component component : components.values()) { + component.startDeactivating(); + constraints.add(component.getDeactivated()); + } + + // wait + try { + CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(false)) + .get(); + } catch (InterruptedException e) { + throw new RuntimeException("Register deactivation has been interrupted", e); + } catch (ExecutionException e) { + if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) { + throw (RuntimeException) e.getCause(); + } else { + throw new IllegalStateException("Cannot deactivate register", e.getCause()); + } + } + } + + @Override + public synchronized boolean isActive() { + return started.get(); + } + + @Override + public synchronized void clear() { + components.clear(); + } +} diff --git a/org.argeo.util/src/org/argeo/util/register/Singleton.java b/org.argeo.util/src/org/argeo/util/register/Singleton.java deleted file mode 100644 index 5d70e9aeb..000000000 --- a/org.argeo.util/src/org/argeo/util/register/Singleton.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.argeo.util.register; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Future; -import java.util.function.Consumer; - -public class Singleton { - private final Class clss; - private final CompletableFuture registrationStage; - private final List> unregistrationHooks = new ArrayList<>(); - - public Singleton(Class clss, CompletableFuture registrationStage) { - this.clss = clss; - this.registrationStage = registrationStage; - } - - CompletionStage getRegistrationStage() { - return registrationStage.minimalCompletionStage(); - } - - public void addUnregistrationHook(Consumer todo) { - unregistrationHooks.add(todo); - } - - public Future getValue() { - return registrationStage.copy(); - } - - public CompletableFuture prepareUnregistration(Void v) { - List> lst = new ArrayList<>(); - for (Consumer hook : unregistrationHooks) { - lst.add(registrationStage.thenAcceptAsync(hook)); - } - CompletableFuture prepareUnregistrationStage = CompletableFuture - .allOf(lst.toArray(new CompletableFuture[lst.size()])); - return prepareUnregistrationStage; - } -} diff --git a/org.argeo.util/src/org/argeo/util/register/StaticRegister.java b/org.argeo.util/src/org/argeo/util/register/StaticRegister.java deleted file mode 100644 index 0dcba13f8..000000000 --- a/org.argeo.util/src/org/argeo/util/register/StaticRegister.java +++ /dev/null @@ -1,122 +0,0 @@ -package org.argeo.util.register; - -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Predicate; - -/** A minimal component register. */ -public class StaticRegister implements ComponentRegister { - private final static StaticRegister instance = new StaticRegister(); - - public static ComponentRegister getInstance() { - return instance; - } - - private final AtomicBoolean started = new AtomicBoolean(false); - private final IdentityHashMap> components = new IdentityHashMap<>(); - - @Override - public void accept(Component component) { - registerComponent(component); - } - - @SuppressWarnings({ "unchecked" }) - @Override - public synchronized Component find(Class clss, Predicate> filter) { - Set> result = new HashSet<>(); - instances: for (Object instance : components.keySet()) { - if (!clss.isAssignableFrom(instance.getClass())) - continue instances; - Component component = (Component) components.get(instance); - - // TODO filter - if (component.isPublishedType(clss)) - result.add(component); - } - if (result.isEmpty()) - return null; - return result.iterator().next(); - - } - - synchronized void registerComponent(Component component) { - if (started.get()) // TODO make it really dynamic - throw new IllegalStateException("Already activated"); - if (components.containsKey(component.getInstance())) - throw new IllegalArgumentException("Already registered as component"); - components.put(component.getInstance(), component); - } - - @Override - public synchronized Component get(Object instance) { - if (!components.containsKey(instance)) - throw new IllegalArgumentException("Not registered as component"); - return components.get(instance); - } - - @Override - public synchronized void activate() { - if (started.get()) - throw new IllegalStateException("Already activated"); - Set> constraints = new HashSet<>(); - for (Component component : components.values()) { - component.startActivating(); - constraints.add(component.getActivated()); - } - - // wait - try { - CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(true)) - .get(); - } catch (InterruptedException e) { - throw new RuntimeException("Register activation has been interrupted", e); - } catch (ExecutionException e) { - if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) { - throw (RuntimeException) e.getCause(); - } else { - throw new IllegalStateException("Cannot activate register", e.getCause()); - } - } - } - - @Override - public synchronized void deactivate() { - if (!started.get()) - throw new IllegalStateException("Not activated"); - Set> constraints = new HashSet<>(); - for (Component component : components.values()) { - component.startDeactivating(); - constraints.add(component.getDeactivated()); - } - - // wait - try { - CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(false)) - .get(); - } catch (InterruptedException e) { - throw new RuntimeException("Register deactivation has been interrupted", e); - } catch (ExecutionException e) { - if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) { - throw (RuntimeException) e.getCause(); - } else { - throw new IllegalStateException("Cannot deactivate register", e.getCause()); - } - } - } - - @Override - public synchronized boolean isActive() { - return started.get(); - } - - @Override - public synchronized void clear() { - components.clear(); - } - -}