From 2d6b7c0c3badea29451c4d8e41ebb5aca2258806 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Mon, 29 Aug 2016 10:34:32 +0000 Subject: [PATCH] Proper LDIF backend for deploy configs. git-svn-id: https://svn.argeo.org/commons/trunk@9087 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc --- .../src/org/argeo/node/NodeConstants.java | 10 +- .../src/org/argeo/node/NodeState.java | 2 + .../argeo/cms/internal/kernel/Activator.java | 58 ++-- .../cms/internal/kernel/CmsDeployment.java | 265 ++++++++++++++++-- .../argeo/cms/internal/kernel/CmsState.java | 180 +++--------- .../internal/kernel/FirstInitProperties.java | 59 ++++ .../cms/internal/kernel/KernelConstants.java | 8 +- .../internal/kernel/RepositoryBuilder.java | 35 ++- .../kernel/RepositoryServiceFactory.java | 18 +- .../cms/maintenance/DataDeploymentUi.java | 2 +- .../util/naming/AttributesDictionary.java | 7 +- .../src/org/argeo/util/naming/LdifWriter.java | 6 + .../servlet/OpenInViewSessionProvider.java | 1 + .../jackrabbit/servlet/RemotingServlet.java | 1 + .../jackrabbit/servlet/WebdavServlet.java | 1 + .../src/org/argeo/util/LangUtils.java | 36 ++- 16 files changed, 468 insertions(+), 221 deletions(-) create mode 100644 org.argeo.cms/src/org/argeo/cms/internal/kernel/FirstInitProperties.java diff --git a/org.argeo.cms.api/src/org/argeo/node/NodeConstants.java b/org.argeo.cms.api/src/org/argeo/node/NodeConstants.java index 138979205..039b2f9e9 100644 --- a/org.argeo.cms.api/src/org/argeo/node/NodeConstants.java +++ b/org.argeo.cms.api/src/org/argeo/node/NodeConstants.java @@ -14,7 +14,14 @@ public interface NodeConstants { /* * FACTORY PIDs */ - String JACKRABBIT_FACTORY_PID = "org.argeo.jackrabbit.config"; + String NODE_REPOS_FACTORY_PID = "org.argeo.node.repos"; + + /* + * DEPLOY + */ + String DEPLOY_BASEDN = "ou=deploy,ou=node"; +// String DEPLOY_SERVICES_BASEDN = "ou=services," + DEPLOY_BASEDN; +// String DEPLOY_SERVICE_FACTORIES_BASEDN = "ou=serviceFactories," + DEPLOY_BASEDN; /* * FRAMEWORK PROPERTIES @@ -34,5 +41,6 @@ public interface NodeConstants { * STANDARD ATTRIBUTES */ String CN = "cn"; + String OU = "ou"; String LABELED_URI = "labeledUri"; } diff --git a/org.argeo.cms.api/src/org/argeo/node/NodeState.java b/org.argeo.cms.api/src/org/argeo/node/NodeState.java index 572d7eea4..7568cd95b 100644 --- a/org.argeo.cms.api/src/org/argeo/node/NodeState.java +++ b/org.argeo.cms.api/src/org/argeo/node/NodeState.java @@ -9,4 +9,6 @@ public interface NodeState { List getLocales(); String getHostname(); + + boolean isClean(); } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java index 03cbbd90d..6aacfd493 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java @@ -12,8 +12,8 @@ import org.apache.commons.logging.LogFactory; import org.argeo.cms.CmsException; import org.argeo.node.ArgeoLogger; import org.argeo.node.NodeConstants; +import org.argeo.node.NodeDeployment; import org.argeo.node.NodeState; -import org.argeo.node.RepoConf; import org.argeo.util.LangUtils; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; @@ -49,6 +49,7 @@ public class Activator implements BundleActivator { private NodeLogger logger; private CmsState nodeState; + private CmsDeployment nodeDeployment; @Override public void start(BundleContext bundleContext) throws Exception { @@ -91,24 +92,27 @@ public class Activator implements BundleActivator { if (props == null) { if (log.isDebugEnabled()) log.debug("Clean node state"); - Dictionary envProps = getStatePropertiesFromEnvironment(); + Dictionary envProps = new Hashtable<>(); // Use the UUID of the first framework run as state UUID cn = bc.getProperty(Constants.FRAMEWORK_UUID); envProps.put(NodeConstants.CN, cn); nodeConf.update(envProps); } else { // Check if state is in line with environment - Dictionary envProps = getStatePropertiesFromEnvironment(); - for (String key : LangUtils.keys(envProps)) { - Object envValue = envProps.get(key); - Object storedValue = props.get(key); - if (storedValue == null) - throw new CmsException("No state value for env " + key + "=" + envValue - + ", please clean the OSGi configuration."); - if (!storedValue.equals(envValue)) - throw new CmsException("State value for " + key + "=" + storedValue - + " is different from env value =" + envValue + ", please clean the OSGi configuration."); - } + // Dictionary envProps = new Hashtable<>(); + // for (String key : LangUtils.keys(envProps)) { + // Object envValue = envProps.get(key); + // Object storedValue = props.get(key); + // if (storedValue == null) + // throw new CmsException("No state value for env " + key + "=" + + // envValue + // + ", please clean the OSGi configuration."); + // if (!storedValue.equals(envValue)) + // throw new CmsException("State value for " + key + "=" + + // storedValue + // + " is different from env value =" + envValue + ", please clean + // the OSGi configuration."); + // } cn = props.get(NodeConstants.CN); if (cn == null) throw new CmsException("No state UUID available"); @@ -118,6 +122,13 @@ public class Activator implements BundleActivator { regProps.put(NodeConstants.CN, cn); bc.registerService(LangUtils.names(NodeState.class, ManagedService.class), nodeState, regProps); + try { + nodeDeployment = new CmsDeployment(); + bc.registerService(LangUtils.names(NodeDeployment.class), nodeDeployment, null); + } catch (RuntimeException e) { + e.printStackTrace(); + throw e; + } } @Override @@ -144,27 +155,6 @@ public class Activator implements BundleActivator { return bc.getService(sr); } - protected Dictionary getStatePropertiesFromEnvironment() { - Hashtable props = new Hashtable<>(); - // i18n - copyFrameworkProp(NodeConstants.I18N_DEFAULT_LOCALE, props); - copyFrameworkProp(NodeConstants.I18N_LOCALES, props); - // user admin - copyFrameworkProp(NodeConstants.ROLES_URI, props); - copyFrameworkProp(NodeConstants.USERADMIN_URIS, props); - // data - for (RepoConf repoConf : RepoConf.values()) - copyFrameworkProp(NodeConstants.NODE_REPO_PROP_PREFIX + repoConf.name(), props); - // TODO add other environment sources - return props; - } - - private void copyFrameworkProp(String key, Dictionary props) { - String value = bc.getProperty(key); - if (value != null) - props.put(key, value); - } - public static NodeState getNodeState() { return instance.nodeState; } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsDeployment.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsDeployment.java index b36e0f4c5..2f1b16f3c 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsDeployment.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsDeployment.java @@ -2,69 +2,257 @@ package org.argeo.cms.internal.kernel; import static org.argeo.node.DataModelNamespace.CMS_DATA_MODEL_NAMESPACE; +import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.io.Writer; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Dictionary; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; import javax.jcr.Repository; import javax.jcr.Session; +import javax.naming.InvalidNameException; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.core.RepositoryContext; import org.argeo.cms.CmsException; import org.argeo.jcr.ArgeoJcrConstants; import org.argeo.jcr.JcrUtils; import org.argeo.node.DataModelNamespace; +import org.argeo.node.NodeConstants; import org.argeo.node.NodeDeployment; +import org.argeo.node.NodeState; +import org.argeo.util.naming.AttributesDictionary; +import org.argeo.util.naming.LdifParser; +import org.argeo.util.naming.LdifWriter; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; -import org.osgi.service.cm.ConfigurationException; -import org.osgi.service.cm.ManagedService; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationEvent; +import org.osgi.service.cm.SynchronousConfigurationListener; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; -public class CmsDeployment implements NodeDeployment, ManagedService { +public class CmsDeployment implements NodeDeployment, SynchronousConfigurationListener { private final Log log = LogFactory.getLog(getClass()); private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext(); - private Repository deployedNodeRepository; + private Path deployPath = KernelUtils.getOsgiInstancePath(KernelConstants.DEPLOY_PATH); + private SortedMap deployConfigs = new TreeMap<>(); + + // private Repository deployedNodeRepository; private HomeRepository homeRepository; private Long availableSince; + public CmsDeployment() { + ConfigurationAdmin configurationAdmin = bc.getService(bc.getServiceReference(ConfigurationAdmin.class)); + // FIXME no guarantee this is already available + NodeState nodeState = bc.getService(bc.getServiceReference(NodeState.class)); + try { + initDeployConfigs(configurationAdmin, nodeState); + } catch (IOException e) { + throw new CmsException("Could not init deploy configs", e); + } + bc.registerService(SynchronousConfigurationListener.class, this, null); + + new ServiceTracker<>(bc, RepositoryContext.class, new RepositoryContextStc()).open(); + } + + private void initDeployConfigs(ConfigurationAdmin configurationAdmin, NodeState nodeState) throws IOException { + if (!Files.exists(deployPath)) {// first init + Files.createDirectories(deployPath.getParent()); + Files.createFile(deployPath); + FirstInitProperties firstInitProperties = new FirstInitProperties(); + + Dictionary nodeConfig = firstInitProperties.getNodeRepositoryConfig(); + // node repository is mandatory + putFactoryDeployConfig(NodeConstants.NODE_REPOS_FACTORY_PID, nodeConfig); + + Dictionary webServerConfig = firstInitProperties.getHttpServerConfig(); + if (!webServerConfig.isEmpty()) + putFactoryDeployConfig(KernelConstants.JETTY_FACTORY_PID, webServerConfig); + + saveDeployedConfigs(); + } + + try (InputStream in = Files.newInputStream(deployPath)) { + deployConfigs = new LdifParser().read(in); + } + if (nodeState.isClean()) { + for (LdapName dn : deployConfigs.keySet()) { + Rdn lastRdn = dn.getRdn(dn.size() - 1); + LdapName prefix = (LdapName) dn.getPrefix(dn.size() - 1); + if (prefix.toString().equals(NodeConstants.DEPLOY_BASEDN)) { + if (lastRdn.getType().equals(NodeConstants.CN)) { + // service + String pid = lastRdn.getValue().toString(); + Configuration conf = configurationAdmin.getConfiguration(pid); + AttributesDictionary dico = new AttributesDictionary(deployConfigs.get(dn)); + conf.update(dico); + } else { + // service factory definition + } + } else { + // service factory service + Rdn beforeLastRdn = dn.getRdn(dn.size() - 2); + assert beforeLastRdn.getType().equals(NodeConstants.OU); + String factoryPid = beforeLastRdn.getValue().toString(); + Configuration conf = configurationAdmin.createFactoryConfiguration(factoryPid.toString(), null); + AttributesDictionary dico = new AttributesDictionary(deployConfigs.get(dn)); + conf.update(dico); + } + } + } + // TODO check consistency if not clean + } + @Override - public void updated(Dictionary properties) throws ConfigurationException { - if (properties == null) - return; + public void configurationEvent(ConfigurationEvent event) { + try { + if (ConfigurationEvent.CM_UPDATED == event.getType()) { + ConfigurationAdmin configurationAdmin = bc.getService(event.getReference()); + Configuration conf = configurationAdmin.getConfiguration(event.getPid(), null); + LdapName serviceDn = null; + String factoryPid = conf.getFactoryPid(); + if (factoryPid != null) { + LdapName serviceFactoryDn = serviceFactoryDn(factoryPid); + if (deployConfigs.containsKey(serviceFactoryDn)) { + for (LdapName dn : deployConfigs.keySet()) { + if (dn.startsWith(serviceFactoryDn)) { + Rdn lastRdn = dn.getRdn(dn.size() - 1); + assert lastRdn.getType().equals(NodeConstants.CN); + Object value = conf.getProperties().get(lastRdn.getType()); + assert value != null; + if (value.equals(lastRdn.getValue())) { + serviceDn = dn; + break; + } + } + } - if (deployedNodeRepository != null) { - if (availableSince != null) { - throw new CmsException("Deployment is already available"); + Object cn = conf.getProperties().get(NodeConstants.CN); + if (cn == null) + throw new IllegalArgumentException("Properties must contain cn"); + if (serviceDn == null) { + putFactoryDeployConfig(factoryPid, conf.getProperties()); + } else { + Attributes attrs = deployConfigs.get(serviceDn); + assert attrs != null; + AttributesDictionary.copy(conf.getProperties(), attrs); + } + saveDeployedConfigs(); + if (log.isDebugEnabled()) + log.debug("Updated deploy config " + serviceDn(factoryPid, cn.toString())); + } else { + // ignore non config-registered service factories + } + } else { + serviceDn = serviceDn(event.getPid()); + if (deployConfigs.containsKey(serviceDn)) { + Attributes attrs = deployConfigs.get(serviceDn); + assert attrs != null; + AttributesDictionary.copy(conf.getProperties(), attrs); + saveDeployedConfigs(); + if (log.isDebugEnabled()) + log.debug("Updated deploy config " + serviceDn); + } else { + // ignore non config-registered services + } + } } + } catch (Exception e) { + log.error("Could not handle configuration event", e); + } + } + + private void putFactoryDeployConfig(String factoryPid, Dictionary props) { + Object cn = props.get(NodeConstants.CN); + if (cn == null) + throw new IllegalArgumentException("cn must be set in properties"); + LdapName serviceFactorydn = serviceFactoryDn(factoryPid); + if (!deployConfigs.containsKey(serviceFactorydn)) + deployConfigs.put(serviceFactorydn, new BasicAttributes(NodeConstants.OU, factoryPid)); + LdapName serviceDn = serviceDn(factoryPid, cn.toString()); + Attributes attrs = new BasicAttributes(); + AttributesDictionary.copy(props, attrs); + deployConfigs.put(serviceDn, attrs); + } + + private void putDeployConfig(String servicePid, Dictionary props) { + LdapName serviceDn = serviceDn(servicePid); + Attributes attrs = new BasicAttributes(NodeConstants.CN, servicePid); + AttributesDictionary.copy(props, attrs); + deployConfigs.put(serviceDn, attrs); + } - availableSince = System.currentTimeMillis(); + void saveDeployedConfigs() throws IOException { + try (Writer writer = Files.newBufferedWriter(deployPath)) { + new LdifWriter(writer).write(deployConfigs); + } + } - prepareDataModel(KernelUtils.openAdminSession(deployedNodeRepository)); - Hashtable regProps = new Hashtable(); - regProps.put(ArgeoJcrConstants.JCR_REPOSITORY_ALIAS, ArgeoJcrConstants.ALIAS_HOME); - homeRepository = new HomeRepository(deployedNodeRepository); - // register - bc.registerService(Repository.class, homeRepository, regProps); + private LdapName serviceFactoryDn(String factoryPid) { + try { + return new LdapName(NodeConstants.OU + "=" + factoryPid + "," + NodeConstants.DEPLOY_BASEDN); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot generate DN from " + factoryPid, e); + } + } + + private LdapName serviceDn(String servicePid) { + try { + return new LdapName(NodeConstants.CN + "=" + servicePid + "," + NodeConstants.DEPLOY_BASEDN); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot generate DN from " + servicePid, e); + } + } - } else { - throw new CmsException("No node repository available"); + private LdapName serviceDn(String factoryPid, String cn) { + try { + return (LdapName) serviceFactoryDn(factoryPid).add(new Rdn(NodeConstants.CN, cn)); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot generate DN from " + factoryPid + " and " + cn, e); } } + private void prepareNodeRepository(Repository deployedNodeRepository) { + if (availableSince != null) { + throw new CmsException("Deployment is already available"); + } + + availableSince = System.currentTimeMillis(); + + prepareDataModel(KernelUtils.openAdminSession(deployedNodeRepository)); + Hashtable regProps = new Hashtable(); + regProps.put(ArgeoJcrConstants.JCR_REPOSITORY_ALIAS, ArgeoJcrConstants.ALIAS_HOME); + homeRepository = new HomeRepository(deployedNodeRepository); + // register + bc.registerService(Repository.class, homeRepository, regProps); + } + /** Session is logged out. */ private void prepareDataModel(Session adminSession) { try { @@ -122,13 +310,46 @@ public class CmsDeployment implements NodeDeployment, ManagedService { log.debug("Published data model " + name); } - public void setDeployedNodeRepository(Repository deployedNodeRepository) { - this.deployedNodeRepository = deployedNodeRepository; - } + // public void setDeployedNodeRepository(Repository deployedNodeRepository) + // { + // this.deployedNodeRepository = deployedNodeRepository; + // } @Override public long getAvailableSince() { return availableSince; } + private class RepositoryContextStc implements ServiceTrackerCustomizer { + + @Override + public RepositoryContext addingService(ServiceReference reference) { + RepositoryContext nodeRepo = bc.getService(reference); + Object cn = reference.getProperty(NodeConstants.CN); + if (cn != null && cn.equals(ArgeoJcrConstants.ALIAS_NODE)) { + prepareNodeRepository(nodeRepo.getRepository()); + // nodeDeployment.setDeployedNodeRepository(nodeRepo.getRepository()); + // Dictionary props = + // LangUtils.init(Constants.SERVICE_PID, + // NodeConstants.NODE_DEPLOYMENT_PID); + // props.put(NodeConstants.CN, + // nodeRepo.getRootNodeId().toString()); + // register + // bc.registerService(LangUtils.names(NodeDeployment.class, + // ManagedService.class), nodeDeployment, props); + } + + return nodeRepo; + } + + @Override + public void modifiedService(ServiceReference reference, RepositoryContext service) { + } + + @Override + public void removedService(ServiceReference reference, RepositoryContext service) { + } + + } + } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsState.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsState.java index eb57b1b19..f8073c46f 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsState.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsState.java @@ -8,10 +8,10 @@ import static org.argeo.cms.internal.kernel.KernelUtils.getFrameworkProp; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.InetAddress; import java.net.URI; import java.net.UnknownHostException; -import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -19,9 +19,14 @@ import java.util.Dictionary; import java.util.Hashtable; import java.util.List; import java.util.Locale; +import java.util.SortedMap; import java.util.UUID; import javax.jcr.RepositoryFactory; +import javax.naming.InvalidNameException; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; import javax.transaction.TransactionManager; import javax.transaction.TransactionSynchronizationRegistry; import javax.transaction.UserTransaction; @@ -37,6 +42,8 @@ import org.argeo.node.NodeDeployment; import org.argeo.node.NodeState; import org.argeo.node.RepoConf; import org.argeo.util.LangUtils; +import org.argeo.util.naming.AttributesDictionary; +import org.argeo.util.naming.LdifParser; import org.eclipse.equinox.http.jetty.JettyConfigurator; import org.eclipse.equinox.http.jetty.JettyConstants; import org.eclipse.rap.rwt.application.ApplicationConfiguration; @@ -83,7 +90,7 @@ public class CmsState implements NodeState, ManagedService { // private RepositoryService repositoryService; // Deployment - private final CmsDeployment nodeDeployment = new CmsDeployment(); + // private final CmsDeployment nodeDeployment = new CmsDeployment(); private boolean cleanState = false; private URI nodeRepoUri = null; @@ -105,10 +112,10 @@ public class CmsState implements NodeState, ManagedService { @Override public void updated(Dictionary properties) throws ConfigurationException { if (properties == null) { - // TODO this should not happen anymore - this.cleanState = true; - if (log.isTraceEnabled()) - log.trace("Clean state"); + // // TODO this should not happen anymore + // this.cleanState = true; + // if (log.isTraceEnabled()) + // log.trace("Clean state"); return; } String stateUuid = properties.get(NodeConstants.CN).toString(); @@ -123,11 +130,11 @@ public class CmsState implements NodeState, ManagedService { nodeRepoUri = KernelUtils.getOsgiInstanceUri("repos/node"); - initI18n(properties); + initI18n(); initServices(); - initDeployConfigs(properties); - initWebServer(); - initNodeDeployment(); +// initDeployConfigs(); + // initWebServer(); + // initNodeDeployment(); // kernel thread kernelThread = new KernelThread(threadGroup, "Kernel Thread"); @@ -138,17 +145,16 @@ public class CmsState implements NodeState, ManagedService { } } - private void initI18n(Dictionary stateProps) { - Object defaultLocaleValue = stateProps.get(NodeConstants.I18N_DEFAULT_LOCALE); + private void initI18n() { + Object defaultLocaleValue = KernelUtils.getFrameworkProp(NodeConstants.I18N_DEFAULT_LOCALE); defaultLocale = defaultLocaleValue != null ? new Locale(defaultLocaleValue.toString()) : new Locale(ENGLISH.getLanguage()); - locales = asLocaleList(stateProps.get(NodeConstants.I18N_LOCALES)); + locales = asLocaleList(KernelUtils.getFrameworkProp(NodeConstants.I18N_LOCALES)); } private void initServices() { // trackers new ServiceTracker(bc, HttpService.class, new PrepareHttpStc()).open(); - new ServiceTracker<>(bc, RepositoryContext.class, new RepositoryContextStc()).open(); initTransactionManager(); @@ -156,15 +162,16 @@ public class CmsState implements NodeState, ManagedService { RepositoryServiceFactory repositoryServiceFactory = new RepositoryServiceFactory(); shutdownHooks.add(() -> repositoryServiceFactory.shutdown()); bc.registerService(ManagedServiceFactory.class, repositoryServiceFactory, - LangUtils.init(Constants.SERVICE_PID, NodeConstants.JACKRABBIT_FACTORY_PID)); + LangUtils.init(Constants.SERVICE_PID, NodeConstants.NODE_REPOS_FACTORY_PID)); NodeRepositoryFactory repositoryFactory = new NodeRepositoryFactory(); bc.registerService(RepositoryFactory.class, repositoryFactory, null); - RepositoryService repositoryService = new RepositoryService(); - shutdownHooks.add(() -> repositoryService.shutdown()); - bc.registerService(LangUtils.names(ManagedService.class, MetaTypeProvider.class), repositoryService, - LangUtils.init(Constants.SERVICE_PID, NodeConstants.NODE_REPO_PID)); + // RepositoryService repositoryService = new RepositoryService(); + // shutdownHooks.add(() -> repositoryService.shutdown()); + // bc.registerService(LangUtils.names(ManagedService.class, + // MetaTypeProvider.class), repositoryService, + // LangUtils.init(Constants.SERVICE_PID, NodeConstants.NODE_REPO_PID)); // Security NodeUserAdmin userAdmin = new NodeUserAdmin(); @@ -231,42 +238,6 @@ public class CmsState implements NodeState, ManagedService { // LangUtils.init(PROPERTY_CONTEXT_NAME, "user")); // } - private void initDeployConfigs(Dictionary stateProps) throws IOException { - Path deployPath = KernelUtils.getOsgiInstancePath(KernelConstants.DIR_NODE + '/' + KernelConstants.DIR_DEPLOY); - Files.createDirectories(deployPath); - - Path nodeConfigPath = deployPath.resolve(NodeConstants.NODE_REPO_PID + ".properties"); - if (!Files.exists(nodeConfigPath)) { - Dictionary nodeConfig = getNodeConfig(stateProps); - nodeConfig.put(ArgeoJcrConstants.JCR_REPOSITORY_ALIAS, ArgeoJcrConstants.ALIAS_NODE); - nodeConfig.put(RepoConf.labeledUri.name(), nodeRepoUri.toString()); - LangUtils.storeAsProperties(nodeConfig, nodeConfigPath); - } - - if (cleanState) { - try (DirectoryStream ds = Files.newDirectoryStream(deployPath)) { - for (Path path : ds) { - if (Files.isDirectory(path)) {// managed factories - try (DirectoryStream factoryDs = Files.newDirectoryStream(path)) { - for (Path confPath : factoryDs) { - Configuration conf = configurationAdmin - .createFactoryConfiguration(path.getFileName().toString()); - Dictionary props = LangUtils.loadFromProperties(confPath); - conf.update(props); - } - } - } else {// managed services - String pid = path.getFileName().toString(); - pid = pid.substring(0, pid.length() - ".properties".length()); - Configuration conf = configurationAdmin.getConfiguration(pid); - Dictionary props = LangUtils.loadFromProperties(path); - conf.update(props); - } - } - } - } - } - // private void initRepositories(Dictionary stateProps) throws // IOException { // // register @@ -282,52 +253,11 @@ public class CmsState implements NodeState, ManagedService { // MetaTypeProvider.class), repositoryService, regProps); // } - private void initWebServer() { - String httpPort = getFrameworkProp("org.osgi.service.http.port"); - String httpsPort = getFrameworkProp("org.osgi.service.http.port.secure"); - /// TODO make it more generic - String httpHost = getFrameworkProp("org.eclipse.equinox.http.jetty.http.host"); - try { - if (httpPort != null || httpsPort != null) { - final Hashtable jettyProps = new Hashtable(); - if (httpPort != null) { - jettyProps.put(JettyConstants.HTTP_PORT, httpPort); - jettyProps.put(JettyConstants.HTTP_ENABLED, true); - } - if (httpsPort != null) { - jettyProps.put(JettyConstants.HTTPS_PORT, httpsPort); - jettyProps.put(JettyConstants.HTTPS_ENABLED, true); - jettyProps.put(JettyConstants.SSL_KEYSTORETYPE, "PKCS12"); - // jettyProps.put(JettyConstants.SSL_KEYSTORE, - // nodeSecurity.getHttpServerKeyStore().getCanonicalPath()); - jettyProps.put(JettyConstants.SSL_PASSWORD, "changeit"); - jettyProps.put(JettyConstants.SSL_WANTCLIENTAUTH, true); - } - if(httpHost!=null){ - jettyProps.put(JettyConstants.HTTP_HOST, httpHost); - } - if (configurationAdmin != null) { - // TODO make filter more generic - String filter = "(" + JettyConstants.HTTP_PORT + "=" + httpPort + ")"; - if (configurationAdmin.listConfigurations(filter) != null) - return; - Configuration jettyConf = configurationAdmin - .createFactoryConfiguration(KernelConstants.JETTY_FACTORY_PID, null); - jettyConf.update(jettyProps); - - } else { - JettyConfigurator.startServer("default", jettyProps); - } - } - } catch (Exception e) { - throw new CmsException("Cannot initialize web server on " + httpPortsMsg(httpPort, httpsPort), e); - } - } - - private void initNodeDeployment() throws IOException { - Configuration nodeDeploymentConf = configurationAdmin.getConfiguration(NodeConstants.NODE_DEPLOYMENT_PID); - nodeDeploymentConf.update(new Hashtable<>()); - } + // private void initNodeDeployment() throws IOException { + // Configuration nodeDeploymentConf = + // configurationAdmin.getConfiguration(NodeConstants.NODE_DEPLOYMENT_PID); + // nodeDeploymentConf.update(new Hashtable<>()); + // } void shutdown() { // if (transactionManager != null) @@ -361,49 +291,6 @@ public class CmsState implements NodeState, ManagedService { new GogoShellKiller().start(); } - private Dictionary getNodeConfig(Dictionary properties) { - // Object repoType = properties.get(NodeConstants.NODE_REPO_PROP_PREFIX - // + RepoConf.type.name()); - // if (repoType == null) - // return null; - - Hashtable props = new Hashtable(); - for (RepoConf repoConf : RepoConf.values()) { - Object value = properties.get(NodeConstants.NODE_REPO_PROP_PREFIX + repoConf.name()); - if (value != null) - props.put(repoConf.name(), value); - } - return props; - } - - private class RepositoryContextStc implements ServiceTrackerCustomizer { - - @Override - public RepositoryContext addingService(ServiceReference reference) { - RepositoryContext nodeRepo = bc.getService(reference); - Object repoUri = reference.getProperty(ArgeoJcrConstants.JCR_REPOSITORY_URI); - if (repoUri != null && repoUri.equals(nodeRepoUri.toString())) { - nodeDeployment.setDeployedNodeRepository(nodeRepo.getRepository()); - Dictionary props = LangUtils.init(Constants.SERVICE_PID, - NodeConstants.NODE_DEPLOYMENT_PID); - props.put(NodeConstants.CN, nodeRepo.getRootNodeId().toString()); - // register - bc.registerService(LangUtils.names(NodeDeployment.class, ManagedService.class), nodeDeployment, props); - } - - return nodeRepo; - } - - @Override - public void modifiedService(ServiceReference reference, RepositoryContext service) { - } - - @Override - public void removedService(ServiceReference reference, RepositoryContext service) { - } - - } - private class PrepareHttpStc implements ServiceTrackerCustomizer { private DataHttp dataHttp; private NodeHttp nodeHttp; @@ -442,6 +329,11 @@ public class CmsState implements NodeState, ManagedService { } + @Override + public boolean isClean() { + return cleanState; + } + /* * ACCESSORS */ diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/FirstInitProperties.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/FirstInitProperties.java new file mode 100644 index 000000000..cc2c3efe4 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/FirstInitProperties.java @@ -0,0 +1,59 @@ +package org.argeo.cms.internal.kernel; + +import static org.argeo.cms.internal.kernel.KernelUtils.getFrameworkProp; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.argeo.jcr.ArgeoJcrConstants; +import org.argeo.node.NodeConstants; +import org.argeo.node.RepoConf; +import org.eclipse.equinox.http.jetty.JettyConstants; + +/** + * Interprets framework properties in order to generate the initial deploy + * configuration. + */ +class FirstInitProperties { + Dictionary getNodeRepositoryConfig() { + Hashtable props = new Hashtable(); + for (RepoConf repoConf : RepoConf.values()) { + Object value = getFrameworkProp(NodeConstants.NODE_REPO_PROP_PREFIX + repoConf.name()); + if (value != null) + props.put(repoConf.name(), value); + } + props.put(NodeConstants.CN, ArgeoJcrConstants.ALIAS_NODE); + props.put(ArgeoJcrConstants.JCR_REPOSITORY_ALIAS, ArgeoJcrConstants.ALIAS_NODE); + return props; + } + + Dictionary getHttpServerConfig() { + String httpPort = getFrameworkProp("org.osgi.service.http.port"); + String httpsPort = getFrameworkProp("org.osgi.service.http.port.secure"); + /// TODO make it more generic + String httpHost = getFrameworkProp("org.eclipse.equinox.http.jetty.http.host"); + + final Hashtable props = new Hashtable(); + // try { + if (httpPort != null || httpsPort != null) { + if (httpPort != null) { + props.put(JettyConstants.HTTP_PORT, httpPort); + props.put(JettyConstants.HTTP_ENABLED, true); + } + if (httpsPort != null) { + props.put(JettyConstants.HTTPS_PORT, httpsPort); + props.put(JettyConstants.HTTPS_ENABLED, true); + props.put(JettyConstants.SSL_KEYSTORETYPE, "PKCS12"); + // jettyProps.put(JettyConstants.SSL_KEYSTORE, + // nodeSecurity.getHttpServerKeyStore().getCanonicalPath()); + props.put(JettyConstants.SSL_PASSWORD, "changeit"); + props.put(JettyConstants.SSL_WANTCLIENTAUTH, true); + } + if (httpHost != null) { + props.put(JettyConstants.HTTP_HOST, httpHost); + } + props.put(NodeConstants.CN, "default"); + } + return props; + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java index 344a154c5..4ce7f0fc5 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java @@ -1,5 +1,7 @@ package org.argeo.cms.internal.kernel; +import org.argeo.node.NodeConstants; + public interface KernelConstants { @@ -9,11 +11,15 @@ public interface KernelConstants { // Directories final static String DIR_NODE = "node"; - final static String DIR_DEPLOY = "deploy"; + final static String DIR_REPOS = "repos"; +// final static String DIR_DEPLOY = "deploy"; final static String DIR_TRANSACTIONS = "transactions"; final static String DIR_PKI = "pki"; final static String DIR_PKI_PRIVATE = DIR_PKI + "/private"; + // Files + String DEPLOY_PATH = KernelConstants.DIR_NODE + '/' + NodeConstants.DEPLOY_BASEDN + ".ldif"; + // Security final static String DEFAULT_SECURITY_KEY = "argeo"; final static String JAAS_CONFIG = "/org/argeo/cms/internal/kernel/jaas.cfg"; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryBuilder.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryBuilder.java index 9cd761b2a..abfb44642 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryBuilder.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryBuilder.java @@ -26,6 +26,7 @@ import org.apache.jackrabbit.core.config.RepositoryConfigurationParser; import org.argeo.cms.CmsException; import org.argeo.jcr.ArgeoJcrConstants; import org.argeo.jcr.ArgeoJcrException; +import org.argeo.node.NodeConstants; import org.argeo.node.RepoConf; import org.osgi.framework.Constants; import org.osgi.service.cm.ConfigurationAdmin; @@ -91,23 +92,35 @@ class RepositoryBuilder { private Properties getConfigurationProperties(JackrabbitType type, Dictionary properties) { Properties props = new Properties(); - keys: for (Enumeration keys = properties.keys(); keys.hasMoreElements();) { + for (Enumeration keys = properties.keys(); keys.hasMoreElements();) { String key = keys.nextElement(); - if (key.equals(ConfigurationAdmin.SERVICE_FACTORYPID) || key.equals(Constants.SERVICE_PID) - || key.equals(ArgeoJcrConstants.JCR_REPOSITORY_ALIAS)) - continue keys; - String value = prop(properties, RepoConf.valueOf(key)); - if (value != null) - props.put(key, value); + // if (key.equals(ConfigurationAdmin.SERVICE_FACTORYPID) || + // key.equals(Constants.SERVICE_PID) + // || key.equals(ArgeoJcrConstants.JCR_REPOSITORY_ALIAS)) + // continue keys; + // try { + // String value = prop(properties, RepoConf.valueOf(key)); + // if (value != null) + props.put(key, properties.get(key)); + // } catch (IllegalArgumentException e) { + // // ignore non RepoConf + // // FIXME make it more flexible/extensible + // } } // home String homeUri = props.getProperty(RepoConf.labeledUri.name()); Path homePath; - try { - homePath = Paths.get(new URI(homeUri)).toAbsolutePath(); - } catch (URISyntaxException e) { - throw new CmsException("Invalid repository home URI", e); + if (homeUri == null) { + String cn = props.getProperty(NodeConstants.CN); + assert cn != null; + homePath = KernelUtils.getOsgiInstancePath(KernelConstants.DIR_REPOS + '/' + cn); + } else { + try { + homePath = Paths.get(new URI(homeUri)).toAbsolutePath(); + } catch (URISyntaxException e) { + throw new CmsException("Invalid repository home URI", e); + } } Path rootUuidPath = homePath.resolve("repository/meta/rootUUID"); if (!Files.exists(rootUuidPath)) { diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryServiceFactory.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryServiceFactory.java index a4e3a9bb8..7e8b1c07e 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryServiceFactory.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryServiceFactory.java @@ -6,11 +6,10 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.jackrabbit.api.JackrabbitRepository; import org.apache.jackrabbit.core.RepositoryContext; import org.argeo.ArgeoException; import org.argeo.jcr.ArgeoJcrConstants; -import org.argeo.node.RepoConf; +import org.argeo.node.NodeConstants; import org.argeo.util.LangUtils; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; @@ -23,6 +22,7 @@ class RepositoryServiceFactory implements ManagedServiceFactory { private final BundleContext bc = FrameworkUtil.getBundle(RepositoryServiceFactory.class).getBundleContext(); private Map repositories = new HashMap(); + private Map pidToCn = new HashMap(); @Override public String getName() { @@ -47,8 +47,15 @@ class RepositoryServiceFactory implements ManagedServiceFactory { RepositoryContext repositoryContext = repositoryBuilder.createRepositoryContext(properties); repositories.put(pid, repositoryContext); Dictionary props = LangUtils.init(Constants.SERVICE_PID, pid); - props.put(ArgeoJcrConstants.JCR_REPOSITORY_URI, properties.get(RepoConf.labeledUri.name())); - bc.registerService(JackrabbitRepository.class, repositoryContext.getRepository(), props); + // props.put(ArgeoJcrConstants.JCR_REPOSITORY_URI, + // properties.get(RepoConf.labeledUri.name())); + Object cn = properties.get(NodeConstants.CN); + if (cn != null) { + props.put(NodeConstants.CN, cn); + props.put(ArgeoJcrConstants.JCR_REPOSITORY_ALIAS, cn); + pidToCn.put(pid, cn); + } + bc.registerService(RepositoryContext.class, repositoryContext, props); } catch (Exception e) { throw new ArgeoException("Cannot create Jackrabbit repository " + pid, e); } @@ -67,6 +74,9 @@ class RepositoryServiceFactory implements ManagedServiceFactory { for (String pid : repositories.keySet()) { try { repositories.get(pid).getRepository().shutdown(); + if (log.isDebugEnabled()) + log.debug("Shut down repository " + pid + + (pidToCn.containsKey(pid) ? " (" + pidToCn.get(pid) + ")" : "")); } catch (Exception e) { log.error("Error when shutting down Jackrabbit repository " + pid, e); } diff --git a/org.argeo.cms/src/org/argeo/cms/maintenance/DataDeploymentUi.java b/org.argeo.cms/src/org/argeo/cms/maintenance/DataDeploymentUi.java index 8e0c56431..49f3fc351 100644 --- a/org.argeo.cms/src/org/argeo/cms/maintenance/DataDeploymentUi.java +++ b/org.argeo.cms/src/org/argeo/cms/maintenance/DataDeploymentUi.java @@ -48,7 +48,7 @@ class DataDeploymentUi extends AbstractOsgiComposite { try { ConfigurationAdmin confAdmin = bc.getService(bc.getServiceReference(ConfigurationAdmin.class)); Configuration[] confs = confAdmin.listConfigurations( - "(" + ConfigurationAdmin.SERVICE_FACTORYPID + "=" + NodeConstants.JACKRABBIT_FACTORY_PID + ")"); + "(" + ConfigurationAdmin.SERVICE_FACTORYPID + "=" + NodeConstants.NODE_REPOS_FACTORY_PID + ")"); if (confs == null || confs.length == 0) { Group buttonGroup = new Group(parent, SWT.NONE); buttonGroup.setText("Repository Type"); diff --git a/org.argeo.security.core/src/org/argeo/util/naming/AttributesDictionary.java b/org.argeo.security.core/src/org/argeo/util/naming/AttributesDictionary.java index a6fddb440..73099ef4e 100644 --- a/org.argeo.security.core/src/org/argeo/util/naming/AttributesDictionary.java +++ b/org.argeo.security.core/src/org/argeo/util/naming/AttributesDictionary.java @@ -118,9 +118,12 @@ public class AttributesDictionary extends Dictionary { attr.set(i, values[i]); } } else { - if (attr.size() != 1) + if (attr.size() > 1) throw new IllegalArgumentException("Attribute " + key + " is multi-valued"); - attr.set(0, value.toString()); + if (attr.size() == 1) + attr.set(0, value.toString()); + else + attr.add(value.toString()); } return oldValue; } diff --git a/org.argeo.security.core/src/org/argeo/util/naming/LdifWriter.java b/org.argeo.security.core/src/org/argeo/util/naming/LdifWriter.java index 76586c3b3..56f322476 100644 --- a/org.argeo.security.core/src/org/argeo/util/naming/LdifWriter.java +++ b/org.argeo.security.core/src/org/argeo/util/naming/LdifWriter.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; +import java.util.Map; import javax.naming.NamingEnumeration; import javax.naming.NamingException; @@ -57,6 +58,11 @@ public class LdifWriter { } } + public void write(Map entries) throws IOException { + for (LdapName dn : entries.keySet()) + writeEntry(dn, entries.get(dn)); + } + protected void writeAttribute(Attribute attribute) throws NamingException, IOException { for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { Object value = attrValues.next(); diff --git a/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/OpenInViewSessionProvider.java b/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/OpenInViewSessionProvider.java index 519a21840..52a9883ee 100644 --- a/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/OpenInViewSessionProvider.java +++ b/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/OpenInViewSessionProvider.java @@ -33,6 +33,7 @@ import org.argeo.jcr.JcrUtils; * Implements an open session in view patter: a new JCR session is created for * each request */ +@Deprecated public class OpenInViewSessionProvider implements SessionProvider, Serializable { private static final long serialVersionUID = 2270957712453841368L; diff --git a/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/RemotingServlet.java b/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/RemotingServlet.java index 7a949260a..3fdb5d2e1 100644 --- a/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/RemotingServlet.java +++ b/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/RemotingServlet.java @@ -21,6 +21,7 @@ import org.apache.jackrabbit.server.SessionProvider; import org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet; /** Provides remote access to a JCR repository */ +@Deprecated public class RemotingServlet extends JcrRemotingServlet { public final static String INIT_PARAM_RESOURCE_PATH_PREFIX = JcrRemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX; public final static String INIT_PARAM_HOME = JcrRemotingServlet.INIT_PARAM_HOME; diff --git a/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/WebdavServlet.java b/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/WebdavServlet.java index c2346f00e..e3176b742 100644 --- a/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/WebdavServlet.java +++ b/org.argeo.server.jcr/src/org/argeo/jackrabbit/servlet/WebdavServlet.java @@ -30,6 +30,7 @@ import org.apache.jackrabbit.webdav.WebdavResponse; import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet; /** WebDav servlet whose repository is injected */ +@Deprecated public class WebdavServlet extends SimpleWebdavServlet { public final static String INIT_PARAM_RESOURCE_CONFIG = SimpleWebdavServlet.INIT_PARAM_RESOURCE_CONFIG; public final static String INIT_PARAM_RESOURCE_PATH_PREFIX = SimpleWebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX; diff --git a/org.argeo.util/src/org/argeo/util/LangUtils.java b/org.argeo.util/src/org/argeo/util/LangUtils.java index 3b29e868c..3c0baed1c 100644 --- a/org.argeo.util/src/org/argeo/util/LangUtils.java +++ b/org.argeo.util/src/org/argeo/util/LangUtils.java @@ -3,14 +3,18 @@ package org.argeo.util; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.Dictionary; import java.util.Enumeration; import java.util.Hashtable; -import java.util.Map; import java.util.Properties; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + public class LangUtils { /* * NON-API OSGi @@ -92,6 +96,36 @@ public class LangUtils { } } + public static void appendAsLdif(String dnBase, String dnKey, Dictionary props, Path path) + throws IOException { + if (props == null) + throw new IllegalArgumentException("Props cannot be null"); + Object dnValue = props.get(dnKey); + String dnStr = dnKey + '=' + dnValue + ',' + dnBase; + LdapName dn; + try { + dn = new LdapName(dnStr); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot interpret DN " + dnStr, e); + } + if (dnValue == null) + throw new IllegalArgumentException("DN key " + dnKey + " must have a value"); + try (Writer writer = Files.newBufferedWriter(path, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) { + writer.append("\ndn: "); + writer.append(dn.toString()); + writer.append('\n'); + for (Enumeration keys = props.keys(); keys.hasMoreElements();) { + String key = keys.nextElement(); + Object value = props.get(key); + writer.append(key); + writer.append(": "); + // FIXME deal with binary and multiple values + writer.append(value.toString()); + writer.append('\n'); + } + } + } + public static Dictionary loadFromProperties(Path path) throws IOException { Properties toLoad = new Properties(); try (InputStream in = Files.newInputStream(path)) { -- 2.30.2