diff --git a/config/alfresco/application-context.xml b/config/alfresco/application-context.xml
index 5ca124e1c2..0b7a896325 100644
--- a/config/alfresco/application-context.xml
+++ b/config/alfresco/application-context.xml
@@ -30,6 +30,13 @@
+
+
+
-
-
-
diff --git a/config/alfresco/bootstrap-context.xml b/config/alfresco/bootstrap-context.xml
index fd3189c903..1d9f58fa79 100644
--- a/config/alfresco/bootstrap-context.xml
+++ b/config/alfresco/bootstrap-context.xml
@@ -270,6 +270,14 @@
+
+
+
+
+
+
+
+
diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml
index 767a32a0c5..3fccc42714 100644
--- a/config/alfresco/core-services-context.xml
+++ b/config/alfresco/core-services-context.xml
@@ -213,6 +213,7 @@
alfresco.messages.system-messages
+ alfresco.messages.module-messagesalfresco.messages.dictionary-messagesalfresco.messages.version-servicealfresco.messages.permissions-service
diff --git a/config/alfresco/messages/module-messages.properties b/config/alfresco/messages/module-messages.properties
new file mode 100644
index 0000000000..d98dc8cab6
--- /dev/null
+++ b/config/alfresco/messages/module-messages.properties
@@ -0,0 +1,12 @@
+# Module messages
+
+module.msg.found_modules=Found {0} module(s).
+module.msg.starting= Starting module ''{0}'' version {1}.
+module.msg.installing= Installing module ''{0}'' version {1}.
+module.msg.upgrading= Upgrading module ''{0}'' version {1} (was {2}).
+
+module.err.downgrading_not_supported=\nDowngrading of modules is not supported.\nModule ''{0}'' version {1} is currently installed and must be uninstalled before version {2} can be installed.
+module.err.already_executed=The module component has already been executed: {0}.{1}
+module.err.execution_failed=A module component ''{0}'' failed to execute: {1}
+module.err.component_already_registered=A component named ''{0}'' has already been registered for module ''{1}''.
+module.err.unable_to_open_module_properties=The module properties file ''{0}'' could not be read.
\ No newline at end of file
diff --git a/config/alfresco/module-context.xml b/config/alfresco/module-context.xml
new file mode 100644
index 0000000000..db670a0ebb
--- /dev/null
+++ b/config/alfresco/module-context.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/alfresco/module/test/module-context.xml.sample b/config/alfresco/module/test/module-context.xml.sample
new file mode 100644
index 0000000000..586382e10e
--- /dev/null
+++ b/config/alfresco/module/test/module-context.xml.sample
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/alfresco/module/test/module.properties.sample b/config/alfresco/module/test/module.properties.sample
new file mode 100644
index 0000000000..6f07422d49
--- /dev/null
+++ b/config/alfresco/module/test/module.properties.sample
@@ -0,0 +1,5 @@
+# A test module
+module.id=test.xyz
+module.title=Test XYZ
+module.description=A module for unit testing
+module.version=3.0
\ No newline at end of file
diff --git a/config/alfresco/public-services-context.xml b/config/alfresco/public-services-context.xml
index 2b15e9e115..a830294e78 100644
--- a/config/alfresco/public-services-context.xml
+++ b/config/alfresco/public-services-context.xml
@@ -1320,4 +1320,32 @@
+
+
+
+
+ org.alfresco.service.cmr.module.ModuleService
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${server.transaction.mode.readOnly}
+ ${server.transaction.mode.default}
+
+
+
+
diff --git a/source/java/org/alfresco/repo/admin/patch/AbstractPatch.java b/source/java/org/alfresco/repo/admin/patch/AbstractPatch.java
index b921a3e0f7..4c74cdd1ff 100644
--- a/source/java/org/alfresco/repo/admin/patch/AbstractPatch.java
+++ b/source/java/org/alfresco/repo/admin/patch/AbstractPatch.java
@@ -43,7 +43,7 @@ import org.apache.commons.logging.LogFactory;
public abstract class AbstractPatch implements Patch
{
/**
- * I18N message when properties nto set.
+ * I18N message when properties not set.
*
*
{0} = property name
*
{1} = patch instance
diff --git a/source/java/org/alfresco/repo/admin/registry/RegistryKey.java b/source/java/org/alfresco/repo/admin/registry/RegistryKey.java
new file mode 100644
index 0000000000..81531c4a87
--- /dev/null
+++ b/source/java/org/alfresco/repo/admin/registry/RegistryKey.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2007 Alfresco, Inc.
+ *
+ * Licensed under the Mozilla Public License version 1.1
+ * with a permitted attribution clause. You may obtain a
+ * copy of the License at
+ *
+ * http://www.alfresco.org/legal/license.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.alfresco.repo.admin.registry;
+
+import java.io.Serializable;
+
+/**
+ * Key for looking up registry metadata.
+ *
+ * @author Derek Hulley
+ */
+public class RegistryKey implements Serializable
+{
+ private static final long serialVersionUID = 1137822242292626854L;
+
+ static final String REGISTRY_1_0_URI = "http://www.alfresco.org/system/registry/1.0";
+
+ private String namespaceUri;
+ private String[] path;
+ private String property;
+
+ /**
+ * Build a registry key from a given array of elements.
+ */
+ private static String buildPathString(String... elements)
+ {
+ if (elements.length == 0)
+ {
+ return "/";
+ }
+ StringBuilder sb = new StringBuilder();
+ for (String element : elements)
+ {
+ if (element == null || element.length() == 0)
+ {
+ throw new IllegalArgumentException("Key elements may not be empty or null");
+ }
+ sb.append("/").append(element);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * For path /a/b/c and property 'x', put in
"a", "b", "c", "x"
+ * The property can also be null as in
"a", "b", "c", null
+ *
+ * @param namespaceUri the key namespace to use. If left null then the
+ * {@link #REGISTRY_1_0_URI default} will be used.
+ * @param key the path elements followed by the property name.
+ */
+ public RegistryKey(String namespaceUri, String... key)
+ {
+ if (namespaceUri == null)
+ {
+ namespaceUri = REGISTRY_1_0_URI;
+ }
+ this.namespaceUri = namespaceUri;
+ // The last value is the property
+ int length = key.length;
+ if (length == 0)
+ {
+ throw new IllegalArgumentException("No value supplied for the RegistryKey property");
+ }
+ this.property = key[length - 1];
+ this.path = new String[length - 1];
+ System.arraycopy(key, 0, path, 0, length - 1);
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("RegistryKey")
+ .append("[ ").append(RegistryKey.buildPathString(path)).append("/").append(property)
+ .append(" ]");
+ return sb.toString();
+ }
+
+ public String getNamespaceUri()
+ {
+ return namespaceUri;
+ }
+
+ public String[] getPath()
+ {
+ return path;
+ }
+
+ public String getProperty()
+ {
+ return property;
+ }
+
+}
diff --git a/source/java/org/alfresco/repo/admin/registry/RegistryService.java b/source/java/org/alfresco/repo/admin/registry/RegistryService.java
index 338d0a02c3..9000a28b14 100644
--- a/source/java/org/alfresco/repo/admin/registry/RegistryService.java
+++ b/source/java/org/alfresco/repo/admin/registry/RegistryService.java
@@ -29,16 +29,16 @@ public interface RegistryService
/**
* Assign a value to the registry key, which must be of the form /a/b/c.
*
- * @param key the registry key path delimited with '/'.
+ * @param key the registry key.
* @param value any value that can be stored in the repository.
*/
- void addValue(String key, Serializable value);
+ void addValue(RegistryKey key, Serializable value);
/**
- * @param key the registry key path delimited with '/'.
+ * @param key the registry key.
* @return Returns the value stored in the key.
*
* @see #addValue(String, Serializable)
*/
- Serializable getValue(String key);
+ Serializable getValue(RegistryKey key);
}
diff --git a/source/java/org/alfresco/repo/admin/registry/RegistryServiceImpl.java b/source/java/org/alfresco/repo/admin/registry/RegistryServiceImpl.java
index a73cd97acc..1f77d22160 100644
--- a/source/java/org/alfresco/repo/admin/registry/RegistryServiceImpl.java
+++ b/source/java/org/alfresco/repo/admin/registry/RegistryServiceImpl.java
@@ -18,7 +18,6 @@ package org.alfresco.repo.admin.registry;
import java.io.Serializable;
import java.util.List;
-import java.util.StringTokenizer;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
@@ -33,6 +32,7 @@ import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
import org.alfresco.util.PropertyCheck;
+import org.alfresco.util.PropertyMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -154,65 +154,44 @@ public class RegistryServiceImpl implements RegistryService
return registryRootNodeRef;
}
- /**
- * @return Returns a pair representing the node path and the property name
- */
- private Pair splitKey(String key)
- {
- int index = key.lastIndexOf('/');
- Pair result = null;
- if (index < 0) // It is just a property
- {
- result = new Pair("/", key);
- }
- else
- {
- String propertyName = key.substring(index + 1, key.length());
- if (propertyName.length() == 0)
- {
- throw new IllegalArgumentException("The registry key is invalid: " + key);
- }
- result = new Pair(key.substring(0, index), propertyName);
- }
- // done
- return result;
- }
-
/**
* @return Returns the node and property name represented by the key or null
* if it doesn't exist and was not allowed to be created
*/
- private Pair getPath(String key, boolean create)
+ private Pair getPath(RegistryKey key, boolean create)
{
// Get the root
NodeRef currentNodeRef = getRegistryRootNodeRef();
- // Split the key
- Pair keyPair = splitKey(key);
- // Parse the key
- StringTokenizer tokenizer = new StringTokenizer(keyPair.getFirst(), "/");
+ // Get the key and property
+ String namespaceUri = key.getNamespaceUri();
+ String[] pathElements = key.getPath();
+ String property = key.getProperty();
// Find the node and property to put the value
- while (tokenizer.hasMoreTokens())
+ for (String pathElement : pathElements)
{
- String token = tokenizer.nextToken();
- String name = QName.createValidLocalName(token);
- QName qname = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, name);
+ QName assocQName = QName.createQName(
+ namespaceUri,
+ QName.createValidLocalName(pathElement));
// Find the node
List childAssocRefs = nodeService.getChildAssocs(
currentNodeRef,
ContentModel.ASSOC_CHILDREN,
- qname);
+ assocQName);
int size = childAssocRefs.size();
if (size == 0) // Found nothing with that path
{
if (create) // Must create the path
{
- // Create the node
+ // Create the node (with a name)
+ PropertyMap properties = new PropertyMap();
+ properties.put(ContentModel.PROP_NAME, pathElement);
currentNodeRef = nodeService.createNode(
currentNodeRef,
ContentModel.ASSOC_CHILDREN,
- qname,
- ContentModel.TYPE_CONTAINER).getChildRef();
+ assocQName,
+ ContentModel.TYPE_CONTAINER,
+ properties).getChildRef();
}
else
{
@@ -244,15 +223,15 @@ public class RegistryServiceImpl implements RegistryService
}
// Create the result
QName propertyQName = QName.createQName(
- NamespaceService.SYSTEM_MODEL_1_0_URI,
- QName.createValidLocalName(keyPair.getSecond()));
+ namespaceUri,
+ QName.createValidLocalName(property));
Pair resultPair = new Pair(currentNodeRef, propertyQName);
// done
if (logger.isDebugEnabled())
{
logger.debug("Converted registry key: \n" +
- " key pair: " + keyPair + "\n" +
- " result: " + resultPair);
+ " Key: " + key + "\n" +
+ " Result: " + resultPair);
}
if (resultPair.getFirst() == null)
{
@@ -267,7 +246,7 @@ public class RegistryServiceImpl implements RegistryService
/**
* @inheritDoc
*/
- public void addValue(String key, Serializable value)
+ public void addValue(RegistryKey key, Serializable value)
{
// Get the path, with creation support
Pair keyPair = getPath(key, true);
@@ -282,7 +261,7 @@ public class RegistryServiceImpl implements RegistryService
}
}
- public Serializable getValue(String key)
+ public Serializable getValue(RegistryKey key)
{
// Get the path, without creating
Pair keyPair = getPath(key, false);
diff --git a/source/java/org/alfresco/repo/admin/registry/RegistryServiceImplTest.java b/source/java/org/alfresco/repo/admin/registry/RegistryServiceImplTest.java
index 42a454f882..7d97d0f622 100644
--- a/source/java/org/alfresco/repo/admin/registry/RegistryServiceImplTest.java
+++ b/source/java/org/alfresco/repo/admin/registry/RegistryServiceImplTest.java
@@ -40,7 +40,7 @@ public class RegistryServiceImplTest extends TestCase
authenticationComponent = (AuthenticationComponent) ctx.getBean("AuthenticationComponent");
registryService = (RegistryService) ctx.getBean("RegistryService");
- // Run as admin
+ // Run as system user
authenticationComponent.setSystemUserAsCurrentUser();
}
@@ -65,13 +65,16 @@ public class RegistryServiceImplTest extends TestCase
private static final Long VALUE_ONE = 1L;
private static final Long VALUE_TWO = 2L;
private static final Long VALUE_THREE = 3L;
- private static final String KEY_A_B_C_1 = "/a/b/c/1";
- private static final String KEY_A_B_C_2 = "/a/b/c/2";
- private static final String KEY_A_B_C_3 = "/a/b/c/3";
- private static final String KEY_A_B_C_D_1 = "/a/b/c/d/1";
- private static final String KEY_A_B_C_D_2 = "/a/b/c/d/2";
- private static final String KEY_A_B_C_D_3 = "/a/b/c/d/3";
- private static final String KEY_SPECIAL = "/me & you/ whatever";
+ private static final RegistryKey KEY_A_B_C_0 = new RegistryKey(null, "a", "b", "c", "0");
+ private static final RegistryKey KEY_A_B_C_1 = new RegistryKey(null, "a", "b", "c", "1");
+ private static final RegistryKey KEY_A_B_C_2 = new RegistryKey(null, "a", "b", "c", "2");
+ private static final RegistryKey KEY_A_B_C_3 = new RegistryKey(null, "a", "b", "c", "3");
+ private static final RegistryKey KEY_A_B_C_D_0 = new RegistryKey(null, "a", "b", "c", "d", "0");
+ private static final RegistryKey KEY_A_B_C_D_1 = new RegistryKey(null, "a", "b", "c", "d", "1");
+ private static final RegistryKey KEY_A_B_C_D_2 = new RegistryKey(null, "a", "b", "c", "d", "2");
+ private static final RegistryKey KEY_A_B_C_D_3 = new RegistryKey(null, "a", "b", "c", "d", "3");
+ private static final RegistryKey KEY_X_Y_Z_0 = new RegistryKey(null, "x", "y", "z", "0");
+ private static final RegistryKey KEY_SPECIAL = new RegistryKey(null, "me & you", "whatever");
/**
* General writing and reading back.
*/
@@ -91,9 +94,9 @@ public class RegistryServiceImplTest extends TestCase
assertEquals("Incorrect value from service registry", VALUE_TWO, registryService.getValue(KEY_A_B_C_D_2));
assertEquals("Incorrect value from service registry", VALUE_THREE, registryService.getValue(KEY_A_B_C_D_3));
- assertNull("Missing key should return null value", registryService.getValue("/a/b/c/0"));
- assertNull("Missing key should return null value", registryService.getValue("/a/b/c/d/0"));
- assertNull("Missing key should return null value", registryService.getValue("/x/y/z/0"));
+ assertNull("Missing key should return null value", registryService.getValue(KEY_A_B_C_0));
+ assertNull("Missing key should return null value", registryService.getValue(KEY_A_B_C_D_0));
+ assertNull("Missing key should return null value", registryService.getValue(KEY_X_Y_Z_0));
}
public void testSpecialCharacters()
diff --git a/source/java/org/alfresco/repo/domain/PropertyValue.java b/source/java/org/alfresco/repo/domain/PropertyValue.java
index 341dcc7724..4e3f517eb8 100644
--- a/source/java/org/alfresco/repo/domain/PropertyValue.java
+++ b/source/java/org/alfresco/repo/domain/PropertyValue.java
@@ -35,6 +35,7 @@ import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.EqualsHelper;
+import org.alfresco.util.VersionNumber;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -256,6 +257,20 @@ public class PropertyValue implements Cloneable, Serializable
{
return DefaultTypeConverter.INSTANCE.convert(Locale.class, value);
}
+ },
+ VERSION_NUMBER
+ {
+ @Override
+ protected ValueType getPersistedType(Serializable value)
+ {
+ return ValueType.STRING;
+ }
+
+ @Override
+ Serializable convert(Serializable value)
+ {
+ return DefaultTypeConverter.INSTANCE.convert(VersionNumber.class, value);
+ }
};
/**
@@ -362,6 +377,10 @@ public class PropertyValue implements Cloneable, Serializable
{
return ValueType.LOCALE;
}
+ else if (value instanceof VersionNumber)
+ {
+ return ValueType.VERSION_NUMBER;
+ }
else
{
// type is not recognised as belonging to any particular slot
diff --git a/source/java/org/alfresco/repo/importer/ImporterBootstrap.java b/source/java/org/alfresco/repo/importer/ImporterBootstrap.java
index 9b4e1b6eb3..9313b97307 100644
--- a/source/java/org/alfresco/repo/importer/ImporterBootstrap.java
+++ b/source/java/org/alfresco/repo/importer/ImporterBootstrap.java
@@ -30,10 +30,11 @@ import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.ResourceBundle;
-import java.util.StringTokenizer;
import javax.transaction.UserTransaction;
+import net.sf.acegisecurity.Authentication;
+
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.i18n.I18NUtil;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
@@ -249,21 +250,8 @@ public class ImporterBootstrap extends AbstractLifecycleBean
public void setLocale(String locale)
{
// construct locale
- StringTokenizer t = new StringTokenizer(locale, "_");
- int tokens = t.countTokens();
- if (tokens == 1)
- {
- this.locale = new Locale(locale);
- }
- else if (tokens == 2)
- {
- this.locale = new Locale(t.nextToken(), t.nextToken());
- }
- else if (tokens == 3)
- {
- this.locale = new Locale(t.nextToken(), t.nextToken(), t.nextToken());
- }
-
+ this.locale = I18NUtil.parseLocale(locale);
+
// store original
strLocale = locale;
}
@@ -333,6 +321,7 @@ public class ImporterBootstrap extends AbstractLifecycleBean
}
UserTransaction userTransaction = transactionService.getUserTransaction();
+ Authentication authentication = authenticationComponent.getCurrentAuthentication();
authenticationComponent.setSystemUserAsCurrentUser();
try
@@ -448,7 +437,7 @@ public class ImporterBootstrap extends AbstractLifecycleBean
}
finally
{
- try {authenticationComponent.clearCurrentSecurityContext(); } catch (Throwable ex) {}
+ try {authenticationComponent.setCurrentAuthentication(authentication); } catch (Throwable ex) {}
}
}
diff --git a/source/java/org/alfresco/repo/module/AbstractModuleComponent.java b/source/java/org/alfresco/repo/module/AbstractModuleComponent.java
new file mode 100644
index 0000000000..d7f930e87d
--- /dev/null
+++ b/source/java/org/alfresco/repo/module/AbstractModuleComponent.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2007 Alfresco, Inc.
+ *
+ * Licensed under the Mozilla Public License version 1.1
+ * with a permitted attribution clause. You may obtain a
+ * copy of the License at
+ *
+ * http://www.alfresco.org/legal/license.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.alfresco.repo.module;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.i18n.I18NUtil;
+import org.alfresco.repo.security.authentication.AuthenticationComponent;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.cmr.module.ModuleService;
+import org.alfresco.util.EqualsHelper;
+import org.alfresco.util.PropertyCheck;
+import org.alfresco.util.VersionNumber;
+import org.springframework.beans.factory.BeanNameAware;
+
+/**
+ * Implementation of a {@link org.alfresco.repo.module.ModuleComponent} to provide
+ * the basic necessities.
+ *
+ * @see #executeInternal()
+ *
+ * @author Roy Wetherall
+ * @author Derek Hulley
+ * @since 2.0
+ */
+public abstract class AbstractModuleComponent implements ModuleComponent, BeanNameAware
+{
+ private static final String ERR_ALREADY_EXECUTED = "module.err.already_executed";
+ private static final String ERR_EXECUTION_FAILED = "module.err.execution_failed";
+
+ // Supporting components
+ protected ServiceRegistry serviceRegistry;
+ protected AuthenticationComponent authenticationComponent;
+ protected ModuleService moduleService;
+
+ private String moduleId;
+ private String name;
+ private String description;
+ private VersionNumber sinceVersion;
+ private VersionNumber appliesFromVersion;
+ private VersionNumber appliesToVersion;
+ private List dependsOn;
+ /** Defaults to true */
+ private boolean executeOnceOnly;
+ private boolean executed;
+
+ public AbstractModuleComponent()
+ {
+ sinceVersion = VersionNumber.VERSION_ZERO;
+ appliesFromVersion = VersionNumber.VERSION_ZERO;
+ appliesToVersion = VersionNumber.VERSION_BIG;
+ dependsOn = new ArrayList(0);
+ executeOnceOnly = true;
+ executed = false;
+ }
+
+ /**
+ * Checks for the presence of all generally-required properties.
+ */
+ protected void checkProperties()
+ {
+ PropertyCheck.mandatory(this, "serviceRegistry", serviceRegistry);
+ PropertyCheck.mandatory(this, "authenticationComponent", authenticationComponent);
+ PropertyCheck.mandatory(this, "moduleId", moduleId);
+ PropertyCheck.mandatory(this, "name", name);
+ PropertyCheck.mandatory(this, "sinceVersion", sinceVersion);
+ PropertyCheck.mandatory(this, "appliesFromVersion", appliesFromVersion);
+ PropertyCheck.mandatory(this, "appliesToVersion", appliesToVersion);
+ }
+
+ /**
+ * @see #getModuleId()
+ * @see #getName()
+ */
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("ModuleComponent")
+ .append("[ module=").append(moduleId)
+ .append(", name=").append(name)
+ .append(", since=").append(sinceVersion)
+ .append(", appliesFrom=").append(appliesFromVersion)
+ .append(", appliesTo=").append(appliesToVersion)
+ .append(", onceOnly=").append(executeOnceOnly)
+ .append("]");
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (false == obj instanceof ModuleComponent)
+ {
+ return false;
+ }
+ ModuleComponent that = (ModuleComponent) obj;
+ return (EqualsHelper.nullSafeEquals(this.moduleId, that.getModuleId())
+ && EqualsHelper.nullSafeEquals(this.name, that.getName()));
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return moduleId.hashCode() + 17 * name.hashCode();
+ }
+
+ public void setAuthenticationComponent(AuthenticationComponent authenticationComponent)
+ {
+ this.authenticationComponent = authenticationComponent;
+ }
+
+ /**
+ * Set the module service to register with. If not set, the component will not be
+ * automatically started.
+ *
+ * @param moduleService the service to register against. This is optional.
+ */
+ public void setModuleService(ModuleService moduleService)
+ {
+ this.moduleService = moduleService;
+ }
+
+ public void setServiceRegistry(ServiceRegistry serviceRegistry)
+ {
+ this.serviceRegistry = serviceRegistry;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public String getModuleId()
+ {
+ return moduleId;
+ }
+
+ /**
+ * @param moduleId the globally unique module name.
+ */
+ public void setModuleId(String moduleId)
+ {
+ this.moduleId = moduleId;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+ /**
+ * Set the component name, which must be unique within the context of the
+ * module. If the is not set, then the bean name will be used.
+ *
+ * @param name the name of the component within the module.
+ *
+ * @see #setBeanName(String)
+ */
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ /**
+ * Convenience method that will set the name of the component to
+ * match the bean name, unless the {@link #setName(String) name} has
+ * been explicitly set.
+ */
+ public void setBeanName(String name)
+ {
+ setName(name);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public String getDescription()
+ {
+ return description;
+ }
+
+ /**
+ * Set the component's description. This will automatically be I18N'ized, so it may just
+ * be a resource bundle key.
+ *
+ * @param description a description of the component.
+ */
+ public void setDescription(String description)
+ {
+ this.description = description;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public VersionNumber getSinceVersionNumber()
+ {
+ return sinceVersion;
+ }
+
+ /**
+ * Set the version number for which this component was added.
+ */
+ public void setSinceVersion(String version)
+ {
+ this.sinceVersion = new VersionNumber(version);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public VersionNumber getAppliesFromVersionNumber()
+ {
+ return appliesFromVersion;
+ }
+
+ /**
+ * Set the minimum module version number to which this component applies.
+ * Default 0.0.
+ */
+ public void setAppliesFromVersion(String version)
+ {
+ this.appliesFromVersion = new VersionNumber(version);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public VersionNumber getAppliesToVersionNumber()
+ {
+ return appliesToVersion;
+ }
+
+ /**
+ * Set the minimum module version number to which this component applies.
+ * Default 999.0.
+ */
+ public void setAppliesToVersion(String version)
+ {
+ this.appliesToVersion = new VersionNumber(version);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public List getDependsOn()
+ {
+ return dependsOn;
+ }
+
+ /**
+ * @param dependsOn a list of modules that must be executed before this one
+ */
+ public void setDependsOn(List dependsOn)
+ {
+ this.dependsOn = dependsOn;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return Returns true always. Override as required.
+ */
+ public boolean isExecuteOnceOnly()
+ {
+ return executeOnceOnly;
+ }
+
+ /**
+ * @param executeOnceOnly true to force execution of this component with
+ * each startup or false if it must only be executed once.
+ */
+ public void setExecuteOnceOnly(boolean executeOnceOnly)
+ {
+ this.executeOnceOnly = executeOnceOnly;
+ }
+
+ public void init()
+ {
+ // Ensure that the description gets I18N'ized
+ description = I18NUtil.getMessage(description);
+ // Register the component with the service
+ if (moduleService != null) // Allows optional registration of the component
+ {
+ moduleService.registerComponent(this);
+ }
+ }
+
+ /**
+ * The method that performs the actual work. For the most part, derived classes will
+ * only have to override this method to be fully functional.
+ *
+ * @throws Throwable any problems, just throw them
+ */
+ protected abstract void executeInternal() throws Throwable;
+
+ /**
+ * @inheritDoc
+ *
+ * @see #executeInternal() the abstract method to be implemented by subclasses
+ */
+ public final synchronized void execute()
+ {
+ // ensure that this has not been executed already
+ if (executed)
+ {
+ throw AlfrescoRuntimeException.create(ERR_ALREADY_EXECUTED, moduleId, name);
+ }
+ // Ensure properties have been set
+ checkProperties();
+ // Execute
+ try
+ {
+ executeInternal();
+ }
+ catch (Throwable e)
+ {
+ throw AlfrescoRuntimeException.create(e, ERR_EXECUTION_FAILED, name, e.getMessage());
+ }
+ finally
+ {
+ // There are no second chances
+ executed = true;
+ }
+ }
+}
diff --git a/source/java/org/alfresco/repo/module/ComponentsTest.java b/source/java/org/alfresco/repo/module/ComponentsTest.java
new file mode 100644
index 0000000000..98a2f382c2
--- /dev/null
+++ b/source/java/org/alfresco/repo/module/ComponentsTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2007 Alfresco, Inc.
+ *
+ * Licensed under the Mozilla Public License version 1.1
+ * with a permitted attribution clause. You may obtain a
+ * copy of the License at
+ *
+ * http://www.alfresco.org/legal/license.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.alfresco.repo.module;
+
+import java.util.Collection;
+
+import javax.transaction.UserTransaction;
+
+import junit.framework.TestCase;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.security.authentication.AuthenticationComponent;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.cmr.search.CategoryService;
+import org.alfresco.service.transaction.TransactionService;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+/**
+ * Tests various module components.
+ *
+ * @see org.alfresco.repo.module.ImporterModuleComponent
+ * @see org.alfresco.repo.module.ModuleComponent
+ *
+ * @author Derek Hulley
+ */
+public class ComponentsTest extends TestCase
+{
+ private static ApplicationContext ctx = new ClassPathXmlApplicationContext("module/module-component-test-beans.xml");
+
+ private ServiceRegistry serviceRegistry;
+ private AuthenticationComponent authenticationComponent;
+ private TransactionService transactionService;
+ private NodeService nodeService;
+ private UserTransaction txn;
+
+ @Override
+ protected void setUp() throws Exception
+ {
+ serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
+ authenticationComponent = (AuthenticationComponent) ctx.getBean("AuthenticationComponent");
+ transactionService = serviceRegistry.getTransactionService();
+ nodeService = serviceRegistry.getNodeService();
+
+ // Run as system user
+ authenticationComponent.setSystemUserAsCurrentUser();
+
+ // Start a transaction
+ txn = transactionService.getUserTransaction();
+ }
+
+ @Override
+ protected void tearDown() throws Exception
+ {
+ // Clear authentication
+ try
+ {
+ authenticationComponent.clearCurrentSecurityContext();
+ }
+ catch (Throwable e)
+ {
+ e.printStackTrace();
+ }
+ // Rollback the transaction
+ try
+ {
+// txn.rollback();
+ txn.commit();
+ }
+ catch (Throwable e)
+ {
+ // Ignore
+ }
+ }
+
+ /** Ensure that the test starts and stops properly */
+ public void testSetup() throws Exception
+ {
+ }
+
+ private NodeRef getLoadedCategoryRoot()
+ {
+ StoreRef storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore");
+
+ CategoryService categoryService = serviceRegistry.getCategoryService();
+ // Check if the categories exist
+ Collection assocRefs = categoryService.getRootCategories(
+ storeRef,
+ ContentModel.ASPECT_GEN_CLASSIFIABLE);
+ // Find it
+ for (ChildAssociationRef assocRef : assocRefs)
+ {
+ NodeRef nodeRef = assocRef.getChildRef();
+ if (nodeRef.getId().equals("test:xyz-root"))
+ {
+ // Found it
+ return nodeRef;
+ }
+ }
+ return null;
+ }
+
+ public void testImporterModuleComponent() throws Exception
+ {
+ // Delete any pre-existing data
+ NodeRef nodeRef = getLoadedCategoryRoot();
+ if (nodeRef != null)
+ {
+ CategoryService categoryService = serviceRegistry.getCategoryService();
+ categoryService.deleteCategory(nodeRef);
+ }
+ // Double check to make sure it is gone
+ nodeRef = getLoadedCategoryRoot();
+ assertNull("Category not deleted", nodeRef);
+
+ ImporterModuleComponent component = (ImporterModuleComponent) ctx.getBean("module.test.importerComponent");
+ // Execute it
+ component.execute();
+
+ // Now make sure the data exists
+ nodeRef = getLoadedCategoryRoot();
+ assertNotNull("Loaded category root not found", nodeRef);
+ }
+}
diff --git a/source/java/org/alfresco/repo/module/ImporterModuleComponent.java b/source/java/org/alfresco/repo/module/ImporterModuleComponent.java
new file mode 100644
index 0000000000..ed30973ba3
--- /dev/null
+++ b/source/java/org/alfresco/repo/module/ImporterModuleComponent.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2007 Alfresco, Inc.
+ *
+ * Licensed under the Mozilla Public License version 1.1
+ * with a permitted attribution clause. You may obtain a
+ * copy of the License at
+ *
+ * http://www.alfresco.org/legal/license.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.alfresco.repo.module;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import org.alfresco.repo.importer.ImporterBootstrap;
+import org.alfresco.util.PropertyCheck;
+
+
+/**
+ * Generic module component that can be wired up to import data into the system.
+ *
+ * @author Derek Hulley
+ * @since 2.0
+ */
+public class ImporterModuleComponent extends AbstractModuleComponent
+{
+ private ImporterBootstrap importer;
+ private Properties bootstrapView;
+ private List bootstrapViews;
+
+ /**
+ * Set the helper that has details of the store to load the data into.
+ * Alfresco has a set of predefined importers for all the common stores in use.
+ *
+ * @param importer the bootstrap bean that performs the store bootstrap.
+ */
+ public void setImporter(ImporterBootstrap importer)
+ {
+ this.importer = importer;
+ }
+
+ /**
+ * Set a list of bootstrap views to import.
+ * This is an alternative to {@link #setBootstrapViews(List)}.
+ *
+ * @param bootstrapView the bootstrap data location
+ *
+ * @see ImporterBootstrap#setBootstrapViews(List)
+ */
+ public void setBootstrapView(Properties bootstrapView)
+ {
+ this.bootstrapView = bootstrapView;
+ }
+
+ /**
+ * Set a list of bootstrap views to import.
+ * This is an alternative to {@link #setBootstrapView(Properties)}.
+ *
+ * @param bootstrapViews the bootstrap data locations
+ *
+ * @see ImporterBootstrap#setBootstrapViews(List)
+ */
+ public void setBootstrapViews(List bootstrapViews)
+ {
+ this.bootstrapViews = bootstrapViews;
+ }
+
+ @Override
+ protected void checkProperties()
+ {
+ PropertyCheck.mandatory(this, "importerBootstrap", importer);
+ if (bootstrapView == null && bootstrapViews == null)
+ {
+ PropertyCheck.mandatory(this, null, "bootstrapViews or bootstrapView");
+ }
+ // fulfil contract of override
+ super.checkProperties();
+ }
+
+ @Override
+ protected void executeInternal() throws Throwable
+ {
+ // Construct the bootstrap views
+ List views = new ArrayList(1);
+ if (bootstrapViews != null)
+ {
+ views.addAll(bootstrapViews);
+ }
+ if (bootstrapView != null)
+ {
+ views.add(bootstrapView);
+ }
+ // modify the bootstrapper
+ importer.setBootstrapViews(views);
+ importer.setUseExistingStore(true); // allow import into existing store
+
+ importer.bootstrap();
+ // Done
+ }
+}
diff --git a/source/java/org/alfresco/repo/module/ModuleComponent.java b/source/java/org/alfresco/repo/module/ModuleComponent.java
new file mode 100644
index 0000000000..a8b5a2b7fc
--- /dev/null
+++ b/source/java/org/alfresco/repo/module/ModuleComponent.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2007 Alfresco, Inc.
+ *
+ * Licensed under the Mozilla Public License version 1.1
+ * with a permitted attribution clause. You may obtain a
+ * copy of the License at
+ *
+ * http://www.alfresco.org/legal/license.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.alfresco.repo.module;
+
+import java.util.List;
+
+import org.alfresco.util.VersionNumber;
+
+/**
+ * Interface for classes that control the startup and shutdown behaviour of modules.
+ *
+ * Note that the execution order of these components is on the basis of dependencies
+ * only. The version numbering determines only whether a component will be executed
+ * and doesn't imply any ordering.
+ *
+ * Equals and Hashcode method must be implemented.
+ *
+ * @author Derek Hulley
+ * @since 2.0
+ */
+public interface ModuleComponent
+{
+ /**
+ * @return Returns the globally unique module ID.
+ */
+ String getModuleId();
+
+ /**
+ * @return Returns the name of the component in the context of the module ID. It does not
+ * have to be globally unique.
+ */
+ String getName();
+
+ /**
+ *
+ * @return Returns a description of the component.
+ */
+ String getDescription();
+
+ /**
+ * @return Returns the version number of the module for which this component was introduced.
+ */
+ VersionNumber getSinceVersionNumber();
+
+ /**
+ * @return Returns the smallest version number of the module to which this component applies.
+ */
+ VersionNumber getAppliesFromVersionNumber();
+
+ /**
+ * @return Returns the largest version number of the module to which this component applies.
+ */
+ VersionNumber getAppliesToVersionNumber();
+
+ /**
+ * A list of module components that must be executed prior to this instance.
+ * This is the only way to guarantee ordered execution. The dependencies may include
+ * components from other modules, guaranteeing an early failure if a module is missing.
+ *
+ * @return Returns a list of components that must be executed prior to this component.
+ */
+ List getDependsOn();
+
+ /**
+ * @return Returns true if the component is to be successfully executed exactly once,
+ * or false if the component must be executed with each startup.
+ */
+ boolean isExecuteOnceOnly();
+
+ /**
+ * Perform the actual component's work. Execution will be done within the context of a
+ * system account with an enclosing transaction. Long-running processes should be spawned
+ * from the calling thread, if required.
+ *
+ * All failures should just be thrown out as runtime exceptions and will be dealt with by
+ * the associated module infrastructure.
+ */
+ void execute();
+}
diff --git a/source/java/org/alfresco/repo/module/ModuleComponentHelper.java b/source/java/org/alfresco/repo/module/ModuleComponentHelper.java
new file mode 100644
index 0000000000..c26d9067c6
--- /dev/null
+++ b/source/java/org/alfresco/repo/module/ModuleComponentHelper.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2007 Alfresco, Inc.
+ *
+ * Licensed under the Mozilla Public License version 1.1
+ * with a permitted attribution clause. You may obtain a
+ * copy of the License at
+ *
+ * http://www.alfresco.org/legal/license.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.alfresco.repo.module;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import net.sf.acegisecurity.Authentication;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.i18n.I18NUtil;
+import org.alfresco.repo.admin.registry.RegistryKey;
+import org.alfresco.repo.admin.registry.RegistryService;
+import org.alfresco.repo.security.authentication.AuthenticationComponent;
+import org.alfresco.repo.transaction.TransactionUtil;
+import org.alfresco.repo.transaction.TransactionUtil.TransactionWork;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.cmr.module.ModuleDetails;
+import org.alfresco.service.cmr.module.ModuleService;
+import org.alfresco.service.transaction.TransactionService;
+import org.alfresco.util.PropertyCheck;
+import org.alfresco.util.VersionNumber;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Helper class to split up some of the code for managing module components. This class handles
+ * the execution of the module components.
+ *
+ * @author Derek Hulley
+ */
+public class ModuleComponentHelper
+{
+ public static final String URI_MODULES_1_0 = "http://www.alfresco.org/system/modules/1.0";
+ private static final String REGISTRY_PATH_MODULES = "modules";
+ private static final String REGISTRY_PROPERTY_INSTALLED_VERSION = "installedVersion";
+ private static final String REGISTRY_PROPERTY_CURRENT_VERSION = "currentVersion";
+ private static final String REGISTRY_PATH_COMPONENTS = "components";
+ private static final String REGISTRY_PROPERTY_EXECUTION_DATE = "executionDate";
+
+ private static final String MSG_FOUND_MODULES = "module.msg.found_modules";
+ private static final String MSG_STARTING = "module.msg.starting";
+ private static final String MSG_INSTALLING = "module.msg.installing";
+ private static final String MSG_UPGRADING = "module.msg.upgrading";
+ private static final String ERR_NO_DOWNGRADE = "module.err.downgrading_not_supported";
+ private static final String ERR_COMPONENT_ALREADY_REGISTERED = "module.err.component_already_registered";
+
+ private static Log logger = LogFactory.getLog(ModuleComponentHelper.class);
+ private static Log loggerService = LogFactory.getLog(ModuleServiceImpl.class);
+
+ private ServiceRegistry serviceRegistry;
+ private AuthenticationComponent authenticationComponent;
+ private RegistryService registryService;
+ private ModuleService moduleService;
+ private Map> componentsByNameByModule;
+
+ /** Default constructor */
+ public ModuleComponentHelper()
+ {
+ componentsByNameByModule = new HashMap>(7);
+ }
+
+ /**
+ * @param serviceRegistry provides access to the service APIs
+ */
+ public void setServiceRegistry(ServiceRegistry serviceRegistry)
+ {
+ this.serviceRegistry = serviceRegistry;
+ }
+
+ /**
+ * @param authenticationComponent allows execution as system user.
+ */
+ public void setAuthenticationComponent(AuthenticationComponent authenticationComponent)
+ {
+ this.authenticationComponent = authenticationComponent;
+ }
+
+ /**
+ * @param registryService the service used to persist component execution details.
+ */
+ public void setRegistryService(RegistryService registryService)
+ {
+ this.registryService = registryService;
+ }
+
+ /**
+ * @param moduleService the service from which to get the available modules.
+ */
+ public void setModuleService(ModuleService moduleService)
+ {
+ this.moduleService = moduleService;
+ }
+
+ /**
+ * Add a managed module component to the registry of components. These will be controlled
+ * by the {@link #startModules()} method.
+ *
+ * @param component a module component to be executed
+ */
+ public synchronized void registerComponent(ModuleComponent component)
+ {
+ String moduleId = component.getModuleId();
+ String name = component.getName();
+ // Get the map of components for the module
+ Map componentsByName = componentsByNameByModule.get(moduleId);
+ if (componentsByName == null)
+ {
+ componentsByName = new HashMap(11);
+ componentsByNameByModule.put(moduleId, componentsByName);
+ }
+ // Check if the component has already been registered
+ if (componentsByName.containsKey(name))
+ {
+ throw AlfrescoRuntimeException.create(ERR_COMPONENT_ALREADY_REGISTERED, name, moduleId);
+ }
+ // Add it
+ componentsByName.put(name, component);
+ // Done
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Registered component: " + component);
+ }
+ }
+
+ /**
+ * @return Returns the map of components keyed by name. The map could be empty but
+ * will never be null.
+ */
+ private synchronized Map getComponents(String moduleId)
+ {
+ Map componentsByName = componentsByNameByModule.get(moduleId);
+ if (componentsByName != null)
+ {
+ // Done
+ return componentsByName;
+ }
+ else
+ {
+ // Done
+ return Collections.emptyMap();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public synchronized void startModules()
+ {
+ // Check properties
+ PropertyCheck.mandatory(this, "serviceRegistry", serviceRegistry);
+ PropertyCheck.mandatory(this, "authenticationComponent", authenticationComponent);
+ PropertyCheck.mandatory(this, "registryService", registryService);
+ PropertyCheck.mandatory(this, "moduleService", moduleService);
+ /*
+ * Ensure transactionality and the correct authentication
+ */
+ // Get the current authentication
+ Authentication authentication = authenticationComponent.getCurrentAuthentication();
+ try
+ {
+ TransactionService transactionService = serviceRegistry.getTransactionService();
+ authenticationComponent.setSystemUserAsCurrentUser();
+ // Get all the modules
+ List modules = moduleService.getAllModules();
+ loggerService.info(I18NUtil.getMessage(MSG_FOUND_MODULES, modules.size()));
+ // Process each module in turn. Ordering is not important.
+ final Set executedComponents = new HashSet(10);
+ for (final ModuleDetails module : modules)
+ {
+ TransactionWork