diff --git a/config/alfresco/bootstrap-context.xml b/config/alfresco/bootstrap-context.xml
index 1832a58de1..1fe3ac8dfc 100644
--- a/config/alfresco/bootstrap-context.xml
+++ b/config/alfresco/bootstrap-context.xml
@@ -119,6 +119,10 @@
/
alfresco/bootstrap/categories.xml
+
+ /
+ alfresco/bootstrap/multilingualRoot.xml
+
/${spaces.company_home.childname}/${spaces.guest_home.childname}
alfresco/bootstrap/tutorial.xml
diff --git a/config/alfresco/bootstrap/multilingualRoot.xml b/config/alfresco/bootstrap/multilingualRoot.xml
new file mode 100644
index 0000000000..8269b67343
--- /dev/null
+++ b/config/alfresco/bootstrap/multilingualRoot.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ GROUP_EVERYONE
+ Consumer
+
+
+
+
+
\ No newline at end of file
diff --git a/config/alfresco/messages/patch-service.properties b/config/alfresco/messages/patch-service.properties
index c2f04aca09..b37e7addfd 100644
--- a/config/alfresco/messages/patch-service.properties
+++ b/config/alfresco/messages/patch-service.properties
@@ -108,4 +108,6 @@ patch.invalidNameEnding.rewritten=Name ''{0}'' rewritten to ''{1}''
patch.systemDescriptorContent.description=Adds the version properties content to the system descriptor.
patch.systemDescriptorContent.result=Added the version properties content to the system descriptor.
patch.systemDescriptorContent.err.no_version_properties=The version.properties resource could not be found.
-patch.systemDescriptorContent.err.no_descriptor=The system descriptor could not be found.
\ No newline at end of file
+patch.systemDescriptorContent.err.no_descriptor=The system descriptor could not be found.
+
+patch.multilingualBootstrap.description=Bootstraps the node that will hold the multilingual containers.
\ No newline at end of file
diff --git a/config/alfresco/model-specific-services-context.xml b/config/alfresco/model-specific-services-context.xml
index d516a66285..310bdeac98 100644
--- a/config/alfresco/model-specific-services-context.xml
+++ b/config/alfresco/model-specific-services-context.xml
@@ -2,7 +2,9 @@
-
+
+
+
@@ -37,5 +39,11 @@
+
+
+
+
+
+
diff --git a/config/alfresco/model/contentModel.xml b/config/alfresco/model/contentModel.xml
index 804b6645cd..21a70e7091 100644
--- a/config/alfresco/model/contentModel.xml
+++ b/config/alfresco/model/contentModel.xml
@@ -218,15 +218,27 @@
+
+ Multilingual Root
+ sys:container
+
+
+
+ false
+ false
+
+
+ cm:mlContainer
+ false
+ true
+
+
+
+
+
Multilingual Container
- sys:base
-
-
- Edition Label
- d:text
-
-
+ sys:container
@@ -234,12 +246,15 @@
false
- sys:localized
+ cm:mlDocument
true
true
+
+ cm:versionable
+
@@ -694,6 +709,14 @@
+
+ Multilingual Document
+
+ sys:localized
+ cm:versionable
+
+
+
diff --git a/config/alfresco/patch/patch-services-context.xml b/config/alfresco/patch/patch-services-context.xml
index 75c33cf672..cfaabf94c9 100644
--- a/config/alfresco/patch/patch-services-context.xml
+++ b/config/alfresco/patch/patch-services-context.xml
@@ -517,5 +517,25 @@
+
+ patch.multilingualBootstrap
+ patch.multilingualBootstrap.description
+ 0
+ 29
+ 30
+
+
+
+
+
+ /cm:multilingualRoot
+
+
+
+ /
+ alfresco/bootstrap/multilingualRoot.xml
+
+
+
diff --git a/config/alfresco/public-services-context.xml b/config/alfresco/public-services-context.xml
index 240279078f..aeb7711ec1 100644
--- a/config/alfresco/public-services-context.xml
+++ b/config/alfresco/public-services-context.xml
@@ -1222,4 +1222,43 @@
+
+
+
+
+
+ org.alfresco.service.cmr.ml.MultilingualContentService
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${server.transaction.mode.default}
+
+
+
+
+
+
+ org.alfresco.service.cmr.ml.MultilingualContentService
+
+
+ Multilingual Content Service
+
+
+
diff --git a/config/alfresco/version.properties b/config/alfresco/version.properties
index 9995e3cfce..7caed947b5 100644
--- a/config/alfresco/version.properties
+++ b/config/alfresco/version.properties
@@ -19,4 +19,4 @@ version.build=@build-number@
# Schema number
-version.schema=23
+version.schema=30
diff --git a/source/java/org/alfresco/model/ContentModel.java b/source/java/org/alfresco/model/ContentModel.java
index 237de9ad6f..4e39502db9 100644
--- a/source/java/org/alfresco/model/ContentModel.java
+++ b/source/java/org/alfresco/model/ContentModel.java
@@ -43,7 +43,7 @@ public interface ContentModel
// tag for temporary nodes
static final QName ASPECT_TEMPORARY = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "temporary");
- // tag for temporary nodes
+ // tag for localized nodes
static final QName ASPECT_LOCALIZED = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "localized");
static final QName PROP_LOCALE = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "locale");
@@ -200,6 +200,10 @@ public interface ContentModel
static final QName ASPECT_REFERENCES_NODE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "referencesnode");
static final QName PROP_NODE_REF = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "noderef");
+ // Multilingual Type
+ static final QName TYPE_MULTILINGUAL_CONTAINER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "mlContainer");
+ static final QName ASSOC_MULTILINGUAL_CHILD = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "mlChild");
+ static final QName ASPECT_MULTILINGUAL_DOCUMENT = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "mlDocument");
//
// User Model Definitions
diff --git a/source/java/org/alfresco/repo/model/ml/MultilingualContentServiceImpl.java b/source/java/org/alfresco/repo/model/ml/MultilingualContentServiceImpl.java
new file mode 100644
index 0000000000..fa5342d7da
--- /dev/null
+++ b/source/java/org/alfresco/repo/model/ml/MultilingualContentServiceImpl.java
@@ -0,0 +1,226 @@
+/*
+ * 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.model.ml;
+
+import java.util.List;
+import java.util.Locale;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.model.ContentModel;
+import org.alfresco.service.cmr.ml.MultilingualContentService;
+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.ResultSet;
+import org.alfresco.service.cmr.search.SearchParameters;
+import org.alfresco.service.cmr.search.SearchService;
+import org.alfresco.service.cmr.version.VersionService;
+import org.alfresco.service.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.service.namespace.RegexQNamePattern;
+import org.alfresco.util.PropertyMap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Multilingual support implementation
+ *
+ * @author Derek Hulley
+ */
+public class MultilingualContentServiceImpl implements MultilingualContentService
+{
+ private static Log logger = LogFactory.getLog(MultilingualContentServiceImpl.class);
+
+ private NodeService nodeService;
+ private SearchService searchService;
+ private VersionService versionService;
+ private SearchParameters searchParametersMLRoot;
+
+ public MultilingualContentServiceImpl()
+ {
+ searchParametersMLRoot = new SearchParameters();
+ searchParametersMLRoot.setLanguage(SearchService.LANGUAGE_XPATH);
+ searchParametersMLRoot.setLimit(1);
+ searchParametersMLRoot.addStore(new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"));
+ searchParametersMLRoot.setQuery("/cm:multilingualRoot");
+ }
+
+ public void setNodeService(NodeService nodeService)
+ {
+ this.nodeService = nodeService;
+ }
+
+ public void setSearchService(SearchService searchService)
+ {
+ this.searchService = searchService;
+ }
+
+ public void setVersionService(VersionService versionService)
+ {
+ this.versionService = versionService;
+ }
+
+ public void renameWithMLExtension(NodeRef translationNodeRef)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * @return Returns a reference to the node that will hold all the cm:mlContainer nodes.
+ */
+ private NodeRef getMLContainerRoot()
+ {
+ ResultSet rs = searchService.query(searchParametersMLRoot);
+ try
+ {
+ if (rs.length() > 0)
+ {
+ NodeRef mlRootNodeRef = rs.getNodeRef(0);
+ // done
+ return mlRootNodeRef;
+ }
+ else
+ {
+ throw new AlfrescoRuntimeException(
+ "Unable to find bootstrap location for ML Root using query: " + searchParametersMLRoot.getQuery());
+ }
+ }
+ finally
+ {
+ rs.close();
+ }
+ }
+
+ private static final QName QNAME_ML_CONTAINER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "mlContainer");
+ private static final QName QNAME_ML_TRANSLATION = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "mlTranslation");
+ /**
+ * @return Returns a new cm:mlContainer
+ */
+ private NodeRef makeMLContainer()
+ {
+ NodeRef mlContainerRootNodeRef = getMLContainerRoot();
+ // Create the container
+ ChildAssociationRef assocRef = nodeService.createNode(
+ mlContainerRootNodeRef,
+ ContentModel.ASSOC_CHILDREN,
+ QNAME_ML_CONTAINER,
+ ContentModel.TYPE_MULTILINGUAL_CONTAINER);
+ // done
+ return assocRef.getChildRef();
+ }
+
+ /**
+ * Retrieve or create a cm:mlDocument container for the given node, which must have the
+ * cm:mlDocument already applied.
+ *
+ * @param mlDocumentNodeRef an existing cm:mlDocument
+ * @return Returns the cm:mlContainer parent
+ */
+ private NodeRef getOrCreateMLContainer(NodeRef mlDocumentNodeRef)
+ {
+ if (!nodeService.hasAspect(mlDocumentNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT))
+ {
+ throw new IllegalArgumentException(
+ "Node must have aspect " + ContentModel.ASPECT_MULTILINGUAL_DOCUMENT + " applied");
+ }
+ // Now check if a parent mlContainer exists
+ NodeRef mlContainerNodeRef = null;
+ boolean createAssociation = false;
+ List parentAssocRefs = nodeService.getParentAssocs(
+ mlDocumentNodeRef,
+ ContentModel.ASSOC_MULTILINGUAL_CHILD,
+ RegexQNamePattern.MATCH_ALL);
+ if (parentAssocRefs.size() == 0)
+ {
+ // Create a ML container
+ mlContainerNodeRef = makeMLContainer();
+ createAssociation = true;
+ }
+ else if (parentAssocRefs.size() == 1)
+ {
+ // Just get it
+ ChildAssociationRef toKeepAssocRef = parentAssocRefs.get(0);
+ mlContainerNodeRef = toKeepAssocRef.getParentRef();
+ createAssociation = true;
+ }
+ else if (parentAssocRefs.size() > 1)
+ {
+ // This is a problem - destroy all but the first
+ logger.warn("Cleaning up multiple multilingual containers on node: " + mlDocumentNodeRef);
+ ChildAssociationRef toKeepAssocRef = parentAssocRefs.get(0);
+ mlContainerNodeRef = toKeepAssocRef.getParentRef();
+ // Remove all the associations to the container
+ boolean first = true;
+ for (ChildAssociationRef assocRef : parentAssocRefs)
+ {
+ if (first)
+ {
+ first = false;
+ continue;
+ }
+ nodeService.removeChildAssociation(assocRef);
+ }
+ }
+ // Associate the translation with the container
+ if (createAssociation)
+ {
+ nodeService.addChild(
+ mlContainerNodeRef,
+ mlDocumentNodeRef,
+ ContentModel.ASSOC_MULTILINGUAL_CHILD,
+ QNAME_ML_TRANSLATION);
+ }
+ // done
+ return mlContainerNodeRef;
+ }
+
+ public NodeRef makeTranslation(NodeRef contentNodeRef, Locale locale)
+ {
+ // Add the aspect using the given locale, of necessary
+ if (!nodeService.hasAspect(contentNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT))
+ {
+ PropertyMap properties = new PropertyMap();
+ properties.put(ContentModel.PROP_LOCALE, locale);
+ nodeService.addAspect(contentNodeRef, ContentModel.ASPECT_MULTILINGUAL_DOCUMENT, properties);
+ }
+ else
+ {
+ // The aspect is present, so just ensure that the locale is correct
+ nodeService.setProperty(contentNodeRef, ContentModel.PROP_LOCALE, locale);
+ }
+ // Get or create the container
+ NodeRef mlContainerNodeRef = getOrCreateMLContainer(contentNodeRef);
+ // done
+ return mlContainerNodeRef;
+ }
+
+ public NodeRef addTranslation(NodeRef newTranslationNodeRef, NodeRef translationOfNodeRef, Locale locale)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public NodeRef getTranslationContainer(NodeRef translationNodeRef)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public NodeRef createEdition(NodeRef mlContainerNodeRef, NodeRef translationNodeRef)
+ {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/source/java/org/alfresco/repo/model/ml/MultilingualContentServiceImplTest.java b/source/java/org/alfresco/repo/model/ml/MultilingualContentServiceImplTest.java
new file mode 100644
index 0000000000..028b906c5f
--- /dev/null
+++ b/source/java/org/alfresco/repo/model/ml/MultilingualContentServiceImplTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.model.ml;
+
+import java.util.Locale;
+
+import junit.framework.TestCase;
+
+import org.alfresco.model.ContentModel;
+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.ml.MultilingualContentService;
+import org.alfresco.service.cmr.model.FileFolderService;
+import org.alfresco.service.cmr.repository.ContentWriter;
+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.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.service.transaction.TransactionService;
+import org.alfresco.util.ApplicationContextHelper;
+import org.springframework.context.ApplicationContext;
+
+/**
+ * @see org.alfresco.repo.ml.MultilingualContentServiceImpl
+ *
+ * @author Derek Hulley
+ */
+public class MultilingualContentServiceImplTest extends TestCase
+{
+ private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
+
+ private ServiceRegistry serviceRegistry;
+ private AuthenticationComponent authenticationComponent;
+ private TransactionService transactionService;
+ private NodeService nodeService;
+ private FileFolderService fileFolderService;
+ private MultilingualContentService multilingualContentService;
+ private NodeRef folderNodeRef;
+
+ @Override
+ protected void setUp() throws Exception
+ {
+ serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
+ authenticationComponent = (AuthenticationComponent) ctx.getBean("AuthenticationComponent");
+ transactionService = serviceRegistry.getTransactionService();
+ nodeService = serviceRegistry.getNodeService();
+ fileFolderService = serviceRegistry.getFileFolderService();
+ multilingualContentService = (MultilingualContentService) ctx.getBean("MultilingualContentService");
+
+ // Run as admin
+ authenticationComponent.setCurrentUser("admin");
+
+ // Create a folder to work in
+ TransactionWork createFolderWork = new TransactionWork()
+ {
+ public NodeRef doWork() throws Exception
+ {
+ StoreRef storeRef = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore");
+ NodeRef rootNodeRef = nodeService.getRootNode(storeRef);
+ // Create the folder
+ NodeRef folderNodeRef = nodeService.createNode(
+ rootNodeRef,
+ ContentModel.ASSOC_CHILDREN,
+ QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "testFolder"),
+ ContentModel.TYPE_FOLDER).getChildRef();
+ // done
+ return folderNodeRef;
+ }
+ };
+ folderNodeRef = TransactionUtil.executeInUserTransaction(transactionService, createFolderWork);
+ }
+
+ @Override
+ protected void tearDown() throws Exception
+ {
+ // Clear authentication
+ try
+ {
+ authenticationComponent.clearCurrentSecurityContext();
+ }
+ catch (Throwable e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ private NodeRef createContent()
+ {
+ NodeRef contentNodeRef = fileFolderService.create(
+ folderNodeRef,
+ "" + System.currentTimeMillis(),
+ ContentModel.TYPE_CONTENT).getNodeRef();
+ // add some content
+ ContentWriter contentWriter = fileFolderService.getWriter(contentNodeRef);
+ contentWriter.putContent("ABC");
+ // done
+ return contentNodeRef;
+ }
+
+ public void testSetup() throws Exception
+ {
+ // Ensure that content can be created
+ createContent();
+ }
+
+ public void testMakeTranslation() throws Exception
+ {
+ NodeRef contentNodeRef = createContent();
+ // Turn the content into a translation with the appropriate structures
+ NodeRef mlContainerNodeRef = multilingualContentService.makeTranslation(contentNodeRef, Locale.CHINESE);
+ // Check it
+ assertNotNull("Container not created", mlContainerNodeRef);
+ }
+}
diff --git a/source/java/org/alfresco/service/cmr/ml/MultilingualContentService.java b/source/java/org/alfresco/service/cmr/ml/MultilingualContentService.java
index fcab74ddd2..7d4a3d06e0 100644
--- a/source/java/org/alfresco/service/cmr/ml/MultilingualContentService.java
+++ b/source/java/org/alfresco/service/cmr/ml/MultilingualContentService.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2006 Alfresco, Inc.
+ * Copyright (C) 2007 Alfresco, Inc.
*
* Licensed under the Mozilla Public License version 1.1
* with a permitted attribution clause. You may obtain a
@@ -31,38 +31,41 @@ import org.alfresco.service.cmr.repository.NodeRef;
public interface MultilingualContentService
{
/**
- * Rename an existing cm:translation by adding locale suffixes to the base name.
+ * Rename an existing sys:localized by adding locale suffixes to the base name.
* Where there are name clashes with existing documents, a numerical naming scheme will be
* adopted.
*
- * @param translationNodeRef An existing cm:translation
+ * @param localizedNodeRef An existing sys:localized
*/
- @Auditable(key = Auditable.Key.ARG_0, parameters = {"translationNodeRef"})
- void renameWithMLExtension(NodeRef translationNodeRef);
+ @Auditable(key = Auditable.Key.ARG_0, parameters = {"localizedNodeRef"})
+ void renameWithMLExtension(NodeRef localizedNodeRef);
/**
- * Make an existing document translatable. If it is already translatable, then nothing is done.
+ * Make an existing document into a translation by adding the cm:mlDocument aspect and
+ * creating a cm:mlContainer parent. If it is already a translation, then nothing is done.
*
- * @param contentNodeRef An existing cm:content
- * @return Returns the cm:mlContainer translation parent
+ * @param contentNodeRef An existing cm:content
+ * @return Returns the cm:mlContainer translation parent
+ *
+ * @see org.alfresco.model.ContentModel#ASPECT_MULTILINGUAL_DOCUMENT
*/
@Auditable(key = Auditable.Key.ARG_0, parameters = {"contentNodeRef", "locale"})
- NodeRef makeTranslatable(NodeRef contentNodeRef, Locale locale);
+ NodeRef makeTranslation(NodeRef contentNodeRef, Locale locale);
/**
* Make a translation out of an existing document. The necessary translation structures will be created
* as necessary.
*
- * @param newTranslationNodeRef An existing cm:content
- * @param translationOfNodeRef An existing cm:translation or cm:mlContainer
- * @return Returns the cm:mlContainer translation parent
+ * @param newTranslationNodeRef An existing cm:content
+ * @param translationOfNodeRef An existing cm:mlDocument or cm:mlContainer
+ * @return Returns the cm:mlContainer translation parent
*/
@Auditable(key = Auditable.Key.ARG_0, parameters = {"newTranslationNodeRef", "translationOfNodeRef", "locale"})
NodeRef addTranslation(NodeRef newTranslationNodeRef, NodeRef translationOfNodeRef, Locale locale);
/**
*
- * @return Returns the cm:mlContainer translation parent
+ * @return Returns the cm:mlContainer translation parent
*/
@Auditable(key = Auditable.Key.ARG_0, parameters = {"translationNodeRef"})
NodeRef getTranslationContainer(NodeRef translationNodeRef);
@@ -70,10 +73,10 @@ public interface MultilingualContentService
/**
* Create a new edition of an existing cm:mlContainer.
*
- * @param mlContainerNodeRef An existing cm:mlContainer
- * @param translationNodeRef The specific cm:translation to use as the starting point
- * of the new edition.
- * @return Returns the cm:mlContainer
+ * @param mlContainerNodeRef An existing cm:mlContainer
+ * @param translationNodeRef The specific cm:mlDocument to use as the starting point
+ * of the new edition.
+ * @return Returns the cm:mlContainer
*/
@Auditable(key = Auditable.Key.ARG_0, parameters = {"mlContainerNodeRef", "translationNodeRef"})
NodeRef createEdition(NodeRef mlContainerNodeRef, NodeRef translationNodeRef);