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);