Index: main/plugin-cms/nodetypes/content_nodetypes.xml
===================================================================
--- main/plugin-cms/nodetypes/content_nodetypes.xml	(revision 25585)
+++ main/plugin-cms/nodetypes/content_nodetypes.xml	(working copy)
@@ -33,7 +33,8 @@
         </supertypes>
         <propertyDefinition name="ametys-internal:workflowId" requiredType="Long" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="false"/>
         <propertyDefinition name="ametys-internal:workflowRef" requiredType="Reference" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="false"/>
-        <propertyDefinition name="ametys-internal:contentType" requiredType="String" autoCreated="false" mandatory="true" onParentVersion="COPY" protected="false" multiple="false"/>
+        <propertyDefinition name="ametys-internal:contentType" requiredType="String" autoCreated="false" mandatory="true" onParentVersion="COPY" protected="false" multiple="true"/>
+        <propertyDefinition name="ametys-internal:mixins" requiredType="String" autoCreated="false" mandatory="false" onParentVersion="COPY" protected="false" multiple="true"/>
         <childNodeDefinition name="ametys-internal:consistency" autoCreated="false" defaultPrimaryType="nt:unstructured" mandatory="false" onParentVersion="IGNORE" protected="false" sameNameSiblings="false">
 			<requiredPrimaryTypes>
 				<requiredPrimaryType>nt:unstructured</requiredPrimaryType>
Index: main/plugin-cms/sitemap.xmap
===================================================================
--- main/plugin-cms/sitemap.xmap	(revision 25585)
+++ main/plugin-cms/sitemap.xmap	(working copy)
@@ -22,7 +22,7 @@
             <map:generator name="xhtml" src="org.ametys.cms.transformation.XhtmlGenerator"/>
             <map:generator name="input-stream" src="org.ametys.cms.transformation.InputStreamGenerator"/>
             <map:generator name="metadataset" src="org.ametys.cms.content.MetadataSetDefGenerator"/>
-			<map:generator name="additional-content-info" src="org.ametys.cms.content.AdditionContentPropertiesGenerator" logger="org.ametys.cms.content.ReferencingContentsGenerator"/>
+			<map:generator name="additional-content-info" src="org.ametys.cms.content.AdditionalContentPropertiesGenerator" logger="org.ametys.cms.content.ReferencingContentsGenerator"/>
             
             <map:generator name="search" src="org.ametys.cms.search.generators.SearchGenerator" logger="org.ametys.cms.search"/>
             <map:generator name="select-content-search" src="org.ametys.cms.search.generators.SelectContentSearchGenerator" logger="org.ametys.cms.search"/>
@@ -89,6 +89,8 @@
              
             <map:action name="select-workspace" src="org.ametys.cms.repository.SelectWorkspaceAction" logger="org.ametys.cms.repository.SelectWorkspaceAction"/>
             
+            <map:action name="set-content-type" src="org.ametys.cms.content.AddOrRemoveContentTypeAction"/>
+            <map:action name="set-mixin" src="org.ametys.cms.content.AddOrRemoveMixinAction"/>
             <map:action name="import-simple-contents" src="org.ametys.cms.repository.ImportSimpleContentsAction"/>
             <map:action name="create-content-by-copy" src="org.ametys.cms.workflow.copy.CreateContentByCopyAction"/>
             <map:action name="edit-content-by-copy" src="org.ametys.cms.workflow.copy.EditContentByCopyAction"/>
@@ -273,6 +275,10 @@
                 <map:read src="context://WEB-INF/param/content-types/{1}/{2}"/>
             </map:match>
             
+            <map:match pattern="dynamic-content-types_resources/*/**">
+                <map:read src="context://WEB-INF/param/content-types/_dynamic/{1}/{2}"/>
+            </map:match>
+            
             <map:match pattern="wrapped-content_resources/**">
                 <map:read src="context://WEB-INF/param/wrapped-content_resources/{1}"/>
             </map:match>
@@ -779,6 +785,32 @@
                 </map:act>
             </map:match>
             
+            <map:match pattern="content/add-content-type">
+            	<map:act type="set-content-type">
+            		<map:read type="json"/>
+            	</map:act>
+            </map:match>
+            
+            <map:match pattern="content/remove-content-type">
+            	<map:act type="set-content-type">
+            		<map:parameter name="remove" value="true"/>
+            		<map:read type="json"/>
+            	</map:act>
+            </map:match>
+            
+            <map:match pattern="content/add-mixin">
+            	<map:act type="set-mixin">
+            		<map:read type="json"/>
+            	</map:act>
+            </map:match>
+            
+            <map:match pattern="content/remove-mixin">
+            	<map:act type="set-mixin">
+            		<map:parameter name="remove" value="true"/>
+            		<map:read type="json"/>
+            	</map:act>
+            </map:match>
+            
             <!-- +
                  | CONTENT DUPLICATION
                  + -->
Index: main/plugin-cms/src/org/ametys/cms/source/DefaultContentView.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/source/DefaultContentView.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/source/DefaultContentView.java	(working copy)
@@ -26,6 +26,8 @@
 import org.apache.excalibur.source.Source;
 import org.apache.excalibur.source.SourceResolver;
 
+import org.ametys.cms.contenttype.ContentTypesHelper;
+
 /**
  * Default implementation of a {@link ContentView}
  * Will first look in directory context://WEB-INF/stylesheets/content/article/article-[view].xsl
@@ -35,11 +37,14 @@
 {
     /** The source resolver */
     protected SourceResolver _resolver;
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
     
     @Override
     public void service(ServiceManager manager) throws ServiceException
     {
         _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
     }
     
     @Override
@@ -78,7 +83,7 @@
     /**
      * Returns the ordered list of URI to be tested to find a stylesheet to render the Content.
      * @param location the requested location
-     * @param contentType the type of the current Content
+     * @param contentType the content type for rendering
      * @param view the content view
      * @param format the format of the output (html, pdf, ...)
      * @param pluginName the plugin name
@@ -91,6 +96,9 @@
         ArrayList<String> result = new ArrayList<String>();
         
         // first look in the filesystem (case of automatic content types)
+        result.add("context://WEB-INF/param/content-types/_dynamic" + pluginName + "/stylesheets/" + contentType + "/" + contentType + f + "-" + view + ".xsl");
+        result.add("context://WEB-INF/param/content-types/_dynamic/" + pluginName + "/stylesheets/" + contentType + "/" + contentType + f + ".xsl");
+        
         result.add("context://WEB-INF/param/content-types/" + pluginName + "/stylesheets/" + contentType + "/" + contentType + f + "-" + view + ".xsl");
         result.add("context://WEB-INF/param/content-types/" + pluginName + "/stylesheets/" + contentType + "/" + contentType + f + ".xsl");
         
Index: main/plugin-cms/src/org/ametys/cms/lucene/MetadataIndexer.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/lucene/MetadataIndexer.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/lucene/MetadataIndexer.java	(working copy)
@@ -36,10 +36,10 @@
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
 
-import org.ametys.cms.contenttype.ContentType;
+import org.ametys.cms.contenttype.AbstractMetadataSetElement;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.contenttype.MetadataDefinition;
 import org.ametys.cms.contenttype.MetadataDefinitionReference;
-import org.ametys.cms.contenttype.AbstractMetadataSetElement;
 import org.ametys.cms.contenttype.RepeaterDefinition;
 import org.ametys.plugins.explorer.resources.metadata.TikaProvider;
 import org.ametys.plugins.repository.metadata.CompositeMetadata;
@@ -59,6 +59,8 @@
     
     /** The users manager. */
     protected UsersManager _usersManager;
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
     
     /** The Tika instance. */
     protected Tika _tika;
@@ -74,6 +76,7 @@
     {
         _parser = (SAXParser) manager.lookup(SAXParser.ROLE);
         _usersManager = (UsersManager) manager.lookup(UsersManager.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
         TikaProvider tikaProvider = (TikaProvider) manager.lookup(TikaProvider.ROLE);
         _tika = tikaProvider.getTika();
     }
@@ -94,11 +97,12 @@
      * @param metadataSet the metadata set to index.
      * @param metadata the root composite metadata.
      * @param parentDefinition the parent metadata definition.
-     * @param cType the content type.
+     * @param cTypes the content types.
+     * @param mixins the mixins
      * @param document the document to index into.
      * @throws Exception if an error occurs.
      */
-    public void indexMetadataSet(AbstractMetadataSetElement metadataSet, CompositeMetadata metadata, MetadataDefinition parentDefinition, ContentType cType, Document document) throws Exception
+    public void indexMetadataSet(AbstractMetadataSetElement metadataSet, CompositeMetadata metadata, MetadataDefinition parentDefinition, String[] cTypes, String[] mixins, Document document) throws Exception
     {
         for (AbstractMetadataSetElement element : metadataSet.getElements())
         {
@@ -112,20 +116,20 @@
                     MetadataDefinition definition;
                     if (parentDefinition == null)
                     {
-                        definition = cType.getMetadataDefinition(name);
+                        definition = _contentTypesHelper.getMetadataDefinition(name, cTypes, mixins);
                     }
                     else
                     {
                         definition = parentDefinition.getMetadataDefinition(name);
                     }
                     
-                    indexMetadata(metadataSet, metadata, cType, document, element, name, definition);
+                    indexMetadata(metadataSet, metadata, cTypes, mixins, document, element, name, definition);
                 }
             }
             else
             {
                 // case of fieldSets, even if it would be really strange to have a fieldSet in the index metadataSet !
-                indexMetadataSet(element, metadata, parentDefinition, cType, document);
+                indexMetadataSet(element, metadata, parentDefinition, cTypes, mixins, document);
             }
         }
     }
@@ -134,7 +138,8 @@
      * Index a content metadata.
      * @param metadataSet 
      * @param metadata the metadata to index.
-     * @param cType the content type.
+     * @param cTypes the content types.
+     * @param mixins the mixin types.
      * @param document the document to index into.
      * @param element the metadata set element.
      * @param name the metadata name.
@@ -143,16 +148,17 @@
      * @throws IOException 
      * @throws Exception if an error occurs.
      */
-    public void indexMetadata(AbstractMetadataSetElement metadataSet, CompositeMetadata metadata, ContentType cType, Document document, AbstractMetadataSetElement element, String name, MetadataDefinition definition) throws SAXException, IOException, Exception
+    public void indexMetadata(AbstractMetadataSetElement metadataSet, CompositeMetadata metadata, String[] cTypes, String[] mixins, Document document, AbstractMetadataSetElement element, String name, MetadataDefinition definition) throws SAXException, IOException, Exception
     {
-        indexMetadata(metadataSet, metadata, cType, document, element, name, name, definition);
+        indexMetadata(metadataSet, metadata, cTypes, mixins, document, element, name, name, definition);
     }
     
     /**
      * Index a content metadata.
      * @param metadataSet 
      * @param metadata the metadata to index.
-     * @param cType the content type.
+     * @param cTypes the content types.
+     * @param mixins the mixins.
      * @param document the document to index into.
      * @param element the metadata set element.
      * @param name the metadata name.
@@ -162,7 +168,7 @@
      * @throws IOException 
      * @throws Exception if an error occurs.
      */
-    public void indexMetadata(AbstractMetadataSetElement metadataSet, CompositeMetadata metadata, ContentType cType, Document document, AbstractMetadataSetElement element, String name, String fieldName, MetadataDefinition definition) throws SAXException, IOException, Exception
+    public void indexMetadata(AbstractMetadataSetElement metadataSet, CompositeMetadata metadata, String[] cTypes, String[] mixins, Document document, AbstractMetadataSetElement element, String name, String fieldName, MetadataDefinition definition) throws SAXException, IOException, Exception
     {
         switch (definition.getType())
         {
@@ -184,12 +190,12 @@
                     for (String entry : entries)
                     {
                         CompositeMetadata subMetadata = composite.getCompositeMetadata(entry);
-                        indexMetadataSet(element, subMetadata, definition, cType, document);
+                        indexMetadataSet(element, subMetadata, definition, cTypes, mixins, document);
                     }
                 }
                 else
                 {
-                    indexMetadataSet(element, composite, definition, cType, document);
+                    indexMetadataSet(element, composite, definition, cTypes, mixins, document);
                 }
                 
                 break;
Index: main/plugin-cms/src/org/ametys/cms/clientsideelement/content/SmartContentClientSideElementHelper.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/clientsideelement/content/SmartContentClientSideElementHelper.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/clientsideelement/content/SmartContentClientSideElementHelper.java	(revision 0)
@@ -0,0 +1,298 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.clientsideelement.content;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.avalon.framework.component.Component;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.avalon.framework.service.Serviceable;
+import org.apache.commons.lang.StringUtils;
+
+import org.ametys.cms.clientsideelement.SmartContentClientSideElement;
+import org.ametys.cms.repository.Content;
+import org.ametys.cms.repository.ModifiableContent;
+import org.ametys.cms.repository.WorkflowAwareContent;
+import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
+import org.ametys.plugins.repository.lock.LockHelper;
+import org.ametys.plugins.repository.lock.LockableAmetysObject;
+import org.ametys.plugins.workflow.Workflow;
+import org.ametys.runtime.right.RightsManager;
+import org.ametys.runtime.right.RightsManager.RightResult;
+import org.ametys.runtime.user.CurrentUserProvider;
+import org.ametys.runtime.user.User;
+import org.ametys.runtime.user.UsersManager;
+import org.ametys.runtime.util.I18nizableText;
+
+import com.opensymphony.workflow.spi.Step;
+
+/**
+ * Component helper for {@link SmartContentClientSideElement}
+ *
+ */
+public class SmartContentClientSideElementHelper implements Serviceable, Component
+{
+    /** The Avalon Role */
+    public static final String ROLE = SmartContentClientSideElementHelper.class.getName();
+    
+    private UsersManager _usersManager;
+    private Workflow _workflow;
+    private CurrentUserProvider _userProvider;
+    private RightsManager _rightsManager;
+    
+    @Override
+    public void service(ServiceManager manager) throws ServiceException
+    {
+        _usersManager = (UsersManager) manager.lookup(UsersManager.ROLE);
+        _workflow = (Workflow) manager.lookup(Workflow.ROLE);
+        _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
+        _rightsManager = (RightsManager) manager.lookup(RightsManager.ROLE);
+    }
+    
+    /**
+     * Determines if the workflow step is correct
+     * @param parameters The client side element 's parameters
+     * @param content the content
+     * @return true if the workflow step is incorrect
+     */
+    public boolean isWorkflowStepCorrect(Map<String, Object> parameters, Content content)
+    {
+        WorkflowAwareContent waContent = (WorkflowAwareContent) content;
+        long wId = waContent.getWorkflowId();
+        List<Step> steps = _workflow.getCurrentSteps(wId);
+        
+        List<Integer> stepIds = new ArrayList<Integer>();
+        for (Step step : steps)
+        {
+            stepIds.add(step.getStepId());
+        }
+
+        String correctStepsAsString = parameters.get("enabled-on-workflow-step-only").toString();
+        String[] correctStepsAsStringArray = correctStepsAsString.split(", ?");
+        
+        for (String correctStepAsString : correctStepsAsStringArray)
+        {
+            if (!StringUtils.isEmpty(correctStepAsString) && stepIds.contains(Integer.parseInt(correctStepAsString)))
+            {
+                return true;
+            }
+        }
+        
+        return false;
+    }
+    
+    /**
+     * Get i18n description when user can not do action because he has no right on content
+     * @param parameters The client side element 's parameters
+     * @param content The content
+     * @return The {@link I18nizableText} description
+     */
+    public I18nizableText getNoRightDescription (Map<String, Object> parameters, Content content)
+    {
+        List<String> rightI18nParameters = new ArrayList<String>();
+        rightI18nParameters.add(content.getTitle());
+        
+        I18nizableText ed = (I18nizableText) parameters.get("noright-content-description");
+        return  new I18nizableText(ed.getCatalogue(), ed.getKey(), rightI18nParameters);
+    }
+    
+    /**
+     * Get i18n description when user can not do action because the content is locked
+     * @param parameters The client side element 's parameters
+     * @param content The content
+     * @return The {@link I18nizableText} description
+     */
+    public I18nizableText getLockedDescription (Map<String, Object> parameters, Content content)
+    {
+        String currentLockerLogin = ((LockableAmetysObject) content).getLockOwner();
+        User currentLocker = currentLockerLogin != null ? _usersManager.getUser(currentLockerLogin) : null;
+        
+        List<String> lockI18nParameters = new ArrayList<String>();
+        lockI18nParameters.add(content.getTitle());
+        lockI18nParameters.add(currentLocker != null ? currentLocker.getFullName() : "");
+        lockI18nParameters.add(currentLockerLogin != null ? currentLockerLogin : "Anonymous");
+        
+        I18nizableText ed = (I18nizableText) parameters.get("locked-content-description");
+        return new I18nizableText(ed.getCatalogue(), ed.getKey(), lockI18nParameters);
+    }
+    
+    /**
+     * Get i18n description when user can not do action because there is no workflow action unvailable
+     * @param parameters The client side element 's parameters
+     * @param content The content
+     * @return The {@link I18nizableText} description
+     */
+    public I18nizableText getWorkflowActionUnvailableDescription (Map<String, Object> parameters, Content content)
+    {
+        List<String> workflowI18nParameters = new ArrayList<String>();
+        workflowI18nParameters.add(content.getTitle());
+        
+        I18nizableText ed = (I18nizableText) parameters.get("workflowaction-content-description");
+        return new I18nizableText(ed.getCatalogue(), ed.getKey(), workflowI18nParameters);
+    }
+    
+    /**
+     * Get i18n description when user can not do action because the workflow step is incorrect
+     * @param parameters The client side element 's parameters
+     * @param content The content
+     * @return The {@link I18nizableText} description
+     */
+    public I18nizableText getIncorrectWorkflowStepDescription (Map<String, Object> parameters, Content content)
+    {
+        List<String> workflowI18nParameters = new ArrayList<String>();
+        workflowI18nParameters.add(content.getTitle());
+        
+        I18nizableText ed = (I18nizableText) parameters.get("workflowstep-content-description");
+        return new I18nizableText(ed.getCatalogue(), ed.getKey(), workflowI18nParameters);
+    }
+    
+    /**
+     * Get i18n description when user can not do action because the content is not modifiable
+     * @param parameters The client side element 's parameters
+     * @param content The content
+     * @return The {@link I18nizableText} description
+     */
+    public I18nizableText getNoModifiableDescription (Map<String, Object> parameters, Content content)
+    {
+        List<String> modifiableI18nParameters = new ArrayList<String>();
+        modifiableI18nParameters.add(content.getTitle());
+        
+        I18nizableText ed = (I18nizableText) parameters.get("nomodifiable-content-description");
+        return new I18nizableText(ed.getCatalogue(), ed.getKey(), modifiableI18nParameters);
+    }
+    
+    /**
+     * Get i18n description when user can do action
+     * @param parameters The client side element 's parameters
+     * @param content The content
+     * @return The {@link I18nizableText} description
+     */
+    public I18nizableText getAllRightDescription (Map<String, Object> parameters, Content content)
+    {
+        List<String> allrightI18nParameters = new ArrayList<String>();
+        allrightI18nParameters.add(content.getTitle());
+        
+        I18nizableText ed = (I18nizableText) parameters.get("allright-content-description");
+        return new I18nizableText(ed.getCatalogue(), ed.getKey(), allrightI18nParameters);
+    }
+    
+    /**
+     * Determines if the content is locked
+     * @param content the content
+     * @return true if the content is locked
+     */
+    public boolean isLocked(Content content)
+    {
+        if (content instanceof LockableAmetysObject)
+        {
+            LockableAmetysObject lockableContent = (LockableAmetysObject) content;
+            if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, _userProvider.getUser()))
+            {
+                return true;
+            }
+        }
+        
+        return false;
+    }
+    
+    /**
+     * Determines if the content is modifiable
+     * @param content the content
+     * @return true if the content is modifiable
+     */
+    public boolean isModifiable(Content content)
+    {
+        return content instanceof ModifiableContent;
+    }
+    
+    /**
+     * Determines if the user has sufficient right for the given content
+     * @param rights The client side element 's rights
+     * @param content the content
+     * @return true if user has sufficient right
+     */
+    public boolean hasRight (Map<String, String> rights, Content content)
+    {
+        if (rights.isEmpty())
+        {
+            return true;
+        }
+        
+        Set<String> rightsToCheck = rights.keySet();
+        for (String rightToCheck : rightsToCheck)
+        {
+            if (StringUtils.isNotEmpty(rightToCheck))
+            {
+                if (_rightsManager.hasRight(_userProvider.getUser(), rightToCheck, getContentRightContext (content)) == RightResult.RIGHT_OK)
+                {
+                    return true;
+                }
+            }
+        }
+        
+        return false;
+    }
+    
+    /**
+     * Get the right context to check right on content
+     * @param content the content
+     * @return the right context
+     */
+    public String getContentRightContext (Content content)
+    {
+        return "/contents/" + content.getName();
+    }
+    
+    /**
+     * Determines if the workflow action is correct
+     * @param parameters The client side element 's parameters
+     * @param content the content
+     * @return true if the workflow action is incorrect
+     */
+    public int workflowAction(Map<String, Object> parameters, Content content)
+    {
+        WorkflowAwareContent waContent = (WorkflowAwareContent) content;
+        long wId = waContent.getWorkflowId();
+        
+        Map<String, Object> vars = new HashMap<String, Object>();
+        vars.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
+        int[] actionIds = _workflow.getAvailableActions(wId, vars);
+        Arrays.sort(actionIds);
+        
+        String correctActionsAsString = parameters.get("enabled-on-workflow-action-only").toString();
+        String[] correctActionsAsStringArray = correctActionsAsString.split(", ?");
+        
+        int correctActionId = -1;
+        for (String correctActionAsString : correctActionsAsStringArray)
+        {
+            int actionId = Integer.parseInt(correctActionAsString);
+            if (!StringUtils.isEmpty(correctActionAsString) && Arrays.binarySearch(actionIds, actionId) >= 0)
+            {
+                correctActionId = actionId;
+                break;
+            }
+        }
+        
+        return correctActionId;
+    }
+    
+}
Index: main/plugin-cms/src/org/ametys/cms/clientsideelement/relations/SetContentMetadataClientSideElement.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/clientsideelement/relations/SetContentMetadataClientSideElement.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/clientsideelement/relations/SetContentMetadataClientSideElement.java	(working copy)
@@ -16,6 +16,7 @@
 package org.ametys.cms.clientsideelement.relations;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -25,6 +26,8 @@
 import org.apache.avalon.framework.component.Component;
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
 
 import org.ametys.cms.contenttype.ContentType;
@@ -112,50 +115,78 @@
     private List<MetadataDefinition> _findCompatibleMetadata(List<Content> contentToReference, List<Content> contentToEdit)
     {
         // First we need to find the type of the target metadata we are looking for
-        Set<String> contentTypesToReference = new HashSet<String>();
+        Collection<String> contentTypesToReference = new HashSet<String>();
         for (Content content: contentToReference)
         {
-            contentTypesToReference.add(content.getType());
+            Set<String> ancestorsAndMySelf = new HashSet<String>();
+            
+            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
+            for (String id : allContentTypes)
+            {
+                ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id));
+                ancestorsAndMySelf.add(id);
+            }
+            
+            if (contentTypesToReference == null)
+            {
+                contentTypesToReference = ancestorsAndMySelf;
+            }
+            else
+            {
+                contentTypesToReference = CollectionUtils.intersection(contentTypesToReference, ancestorsAndMySelf);
+            }
         }
-        String metadataContentType = _ctypesHelper.getCommonAncestor(contentTypesToReference);
 
         // Second we need to know if this metadata will be mulitple or not
         boolean requiresMultiple = contentToReference.size() > 1;
         
         // Third we need to know the target content type
-        Set<String> contentTypesToEdit = new HashSet<String>();
+        Collection<String> contentTypesToEdit = new ArrayList<String>();
         for (Content content: contentToEdit)
         {
-            contentTypesToEdit.add(content.getType());
+            Set<String> ancestorsAndMySelf = new HashSet<String>();
+            
+            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
+            for (String id : allContentTypes)
+            {
+                ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id));
+                ancestorsAndMySelf.add(id);
+            }
+            
+            if (contentTypesToEdit.size() == 0)
+            {
+                contentTypesToEdit = ancestorsAndMySelf;
+            }
+            else
+            {
+                contentTypesToEdit = CollectionUtils.intersection(contentTypesToEdit, ancestorsAndMySelf);
+            }
         }
-        String targetContentTypeName = _ctypesHelper.getCommonAncestor(contentTypesToEdit);
 
-        // Fourth lets list the whole content types tree of the targetContentType
-        Set<String> compatibleContentTypes = _ctypesHelper.getAncestors(metadataContentType);
-        compatibleContentTypes.add(metadataContentType);
-        
         // Now lets navigate in the target content type to find a content metadata limited to the metadata content type (or its parent types), that is multiple if necessary
-        
         List<MetadataDefinition> metadata = new ArrayList<MetadataDefinition>();
         
-        ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName);
-        for (String metadataName : targetContentType.getMetadataNames())
+        for (String targetContentTypeName : contentTypesToEdit)
         {
-            MetadataDefinition metaDef = targetContentType.getMetadataDefinition(metadataName);
-            metadata.addAll(_findCompatibleMetadata(metaDef, false, compatibleContentTypes, requiresMultiple));
+            ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName);
+            for (String metadataName : targetContentType.getMetadataNames())
+            {
+                MetadataDefinition metaDef = targetContentType.getMetadataDefinition(metadataName);
+                metadata.addAll(_findCompatibleMetadata(targetContentTypeName, metaDef, false, contentTypesToReference, requiresMultiple));
+            }
         }
         
-        
         return metadata;
     }
     
-    private List<MetadataDefinition> _findCompatibleMetadata(MetadataDefinition metaDef, boolean anyParentIsMultiple, Set<String> compatibleContentTypes, boolean requiresMultiple)
+    private List<MetadataDefinition> _findCompatibleMetadata(String targetContentTypeName, MetadataDefinition metaDef, boolean anyParentIsMultiple, Collection<String> compatibleContentTypes, boolean requiresMultiple)
     {
         List<MetadataDefinition> metadata = new ArrayList<MetadataDefinition>();
         
         if (MetadataType.CONTENT.equals(metaDef.getType()) 
                 && (metaDef.getContentType() == null || compatibleContentTypes.contains(metaDef.getContentType()))
-                && (!requiresMultiple || metaDef.isMultiple() || anyParentIsMultiple))
+                && (!requiresMultiple || metaDef.isMultiple() || anyParentIsMultiple)
+                && metaDef.getReferenceContentType().equals(targetContentTypeName))
         {
             metadata.add(metaDef);
         }
@@ -164,7 +195,7 @@
             for (String subMetadataName : metaDef.getMetadataComponentsName())
             {
                 MetadataDefinition subMetaDef = metaDef.getMetadataDefinition(subMetadataName);
-                metadata.addAll(_findCompatibleMetadata(subMetaDef, anyParentIsMultiple || metaDef instanceof RepeaterDefinition, compatibleContentTypes, requiresMultiple));
+                metadata.addAll(_findCompatibleMetadata(targetContentTypeName, subMetaDef, anyParentIsMultiple || metaDef instanceof RepeaterDefinition, compatibleContentTypes, requiresMultiple));
             }
         }
         
@@ -184,18 +215,40 @@
     {
         List<Content> contentToReference = _resolve(contentIdsToReference);
         List<Content> contentToEdit = _resolve(contentIdsToEdit);
-
         
-        Set<String> contentTypesToEdit = new HashSet<String>();
+        Collection<String> contentTypesToEdit = new ArrayList<String>();
         for (Content content: contentToEdit)
         {
-            contentTypesToEdit.add(content.getType());
+            Set<String> ancestorsAndMySelf = new HashSet<String>();
+            
+            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
+            for (String id : allContentTypes)
+            {
+                ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id));
+                ancestorsAndMySelf.add(id);
+            }
+            
+            if (contentTypesToEdit.size() == 0)
+            {
+                contentTypesToEdit = ancestorsAndMySelf;
+            }
+            else
+            {
+                contentTypesToEdit = CollectionUtils.intersection(contentTypesToEdit, ancestorsAndMySelf);
+            }
+        }
+        
+        for (String targetContentTypeName : contentTypesToEdit)
+        {
+            ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName);
+            MetadataDefinition metadataDef = targetContentType.getMetadataDefinitionByPath(metadatapath);
+            if (metadataDef != null)
+            {
+                return _setContentMetatada(contentToReference, contentToEdit, metadataDef, workflowActionIds);
+            }
         }
-        String targetContentTypeName = _ctypesHelper.getCommonAncestor(contentTypesToEdit);
-        ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName);
-        MetadataDefinition metadataDef = targetContentType.getMetadataDefinitionByPath(metadatapath);
 
-        return _setContentMetatada(contentToReference, contentToEdit, metadataDef, workflowActionIds);
+        throw new IllegalStateException("Unable to find medatata definition to path '" + metadatapath + "'.");
     }
     
     /**
Index: main/plugin-cms/src/org/ametys/cms/clientsideelement/FormEditionModeMenu.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/clientsideelement/FormEditionModeMenu.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/clientsideelement/FormEditionModeMenu.java	(working copy)
@@ -21,12 +21,13 @@
 
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.commons.lang.StringUtils;
 
-import org.ametys.cms.contenttype.ContentType;
+import org.ametys.cms.contenttype.AbstractMetadataSetElement;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.contenttype.Fieldset;
 import org.ametys.cms.contenttype.MetadataSet;
-import org.ametys.cms.contenttype.AbstractMetadataSetElement;
 import org.ametys.cms.repository.Content;
 import org.ametys.cms.ui.SimpleMenu;
 import org.ametys.plugins.repository.AmetysObjectResolver;
@@ -43,6 +44,8 @@
     
     /** Content type extension point. */
     protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
     
     @Override
     public void service(ServiceManager smanager) throws ServiceException
@@ -50,6 +53,7 @@
         super.service(smanager);
         _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
         _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
     }
     
     /**
@@ -64,23 +68,17 @@
     {
         Map<String, Object> results = new HashMap<String, Object>();
         
-        // CMS-5261
-        // TODO remplace by :
-        // MetadataSet metadataSetForEdition = _contentTypesHelper.getMetadataSetForEdition(metadataSetName, content.getTypes());
-        // boolean found = _searchForTabFieldSet(metadataSetForEdition.getElements());
-        
         Content content = _resolver.resolveById(contentId);
-        ContentType cType = _contentTypeExtensionPoint.getExtension(content.getType());
-        MetadataSet metadataSet = cType.getMetadataSetForEdition(metadataSetName);
+        MetadataSet metadataSetForEdition = _contentTypesHelper.getMetadataSetForEdition(metadataSetName, content.getTypes(), content.getMixinTypes());
         
-        if (metadataSet == null)
+        if (metadataSetForEdition == null)
         {
-            String errorMsg = String.format("Unknown metadata set '%s' of type 'edition' for content type '%s'", metadataSetName, cType.getId());
+            String errorMsg = String.format("Unknown metadata set '%s' of type 'edition' for content type(s) '%s'", metadataSetName, StringUtils.join(content.getTypes(), ','));
             getLogger().error(errorMsg);
             throw new IllegalArgumentException(errorMsg);
         }
         
-        boolean found = _searchForTabFieldSet(metadataSet.getElements());
+        boolean found = _searchForTabFieldSet(metadataSetForEdition.getElements());
         results.put("has-tab", found);
         
         return results;
Index: main/plugin-cms/src/org/ametys/cms/clientsideelement/SmartContentTypesGallery.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/clientsideelement/SmartContentTypesGallery.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/clientsideelement/SmartContentTypesGallery.java	(revision 0)
@@ -0,0 +1,192 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.clientsideelement;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.avalon.framework.context.Context;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+
+import org.ametys.cms.clientsideelement.content.SmartContentClientSideElementHelper;
+import org.ametys.cms.repository.Content;
+import org.ametys.cms.repository.WorkflowAwareContent;
+import org.ametys.plugins.repository.AmetysObjectResolver;
+import org.ametys.runtime.ui.Callable;
+
+/**
+ * This element creates a menu with one gallery item per content type classified by category. 
+ * The user rights are checked. 
+ */
+public class SmartContentTypesGallery extends ContentTypesGallery
+{
+    /** Ametys object resolver */
+    protected AmetysObjectResolver _resolver;
+    /** The context */
+    protected Context _context;
+    /** Helper for smart content client elements */
+    protected SmartContentClientSideElementHelper _smartHelper;
+    
+    @Override
+    public void service(ServiceManager manager) throws ServiceException
+    {
+        super.service(manager);
+        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
+        _smartHelper = (SmartContentClientSideElementHelper) manager.lookup(SmartContentClientSideElementHelper.ROLE);
+    }
+    
+    /**
+     * Get informations on contents' state
+     * @param contentsId the ids of contents
+     * @return informations on contents' state
+     */
+    @Callable
+    public Map<String, Object> getStatus(List<String> contentsId)
+    {
+        Map<String, Object> results = new HashMap<String, Object>();
+        
+        results.put("unmodifiable-contents", new ArrayList<Map<String, Object>>());
+        results.put("locked-contents", new ArrayList<Map<String, Object>>());
+        results.put("noright-contents", new ArrayList<Map<String, Object>>());
+        results.put("invalidworkflowaction-contents", new ArrayList<Map<String, Object>>());
+        results.put("invalidworkflowstep-contents", new ArrayList<Map<String, Object>>());
+        results.put("allright-contents", new ArrayList<Map<String, Object>>());
+        
+        for (String contentId : contentsId)
+        {
+            Content content = _resolver.resolveById(contentId);
+            
+            boolean error = false;
+            
+            if (content instanceof WorkflowAwareContent)
+            {
+                // Is modifiable
+                String enabledOnModifiableOnly = (String) this._initialParameters.get("enabled-on-modifiable-only"); 
+                if ("true".equals(enabledOnModifiableOnly) && !_smartHelper.isModifiable(content))
+                {
+                    Map<String, Object> contentParams = getContentDefaultParameters (content);
+                    contentParams.put("description", _smartHelper.getNoModifiableDescription(this._initialParameters, content));
+                    
+                    List<Map<String, Object>> unModifiableContents = (List<Map<String, Object>>) results.get("unmodifiable-contents");
+                    unModifiableContents.add(contentParams);
+
+                    error = true;
+                }
+                
+                // Is locked
+                String enabledOnUnlockOnly = (String) this._initialParameters.get("enabled-on-unlock-only"); 
+                if ("true".equals(enabledOnUnlockOnly) && _smartHelper.isLocked(content))
+                {
+                    Map<String, Object> contentParams = getContentDefaultParameters (content);
+                    contentParams.put("description", _smartHelper.getLockedDescription(this._initialParameters, content));
+                    
+                    List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) results.get("locked-contents");
+                    lockedContents.add(contentParams);
+                    
+                    error = true;
+                }
+                
+                // Has right correct
+                String enabledOnRightOnly = (String) this._initialParameters.get("enabled-on-right-only"); 
+                if ("true".equals(enabledOnRightOnly) && !_smartHelper.hasRight(_rights, content))
+                {
+                    Map<String, Object> contentParams = getContentDefaultParameters (content);
+                    contentParams.put("description", _smartHelper.getNoRightDescription (this._initialParameters, content));
+                    
+                    List<Map<String, Object>> norightContents = (List<Map<String, Object>>) results.get("noright-contents");
+                    norightContents.add(contentParams);
+                    
+                    error = true;
+                }
+                
+                // Is workflow action correct
+                String enabledOnWorkflowActionOnly = (String) this._initialParameters.get("enabled-on-workflow-action-only"); 
+                if (enabledOnWorkflowActionOnly != null)
+                {
+                    int actionId = _smartHelper.workflowAction(this._initialParameters, content);
+                    if (actionId == -1)
+                    {
+                        Map<String, Object> contentParams = getContentDefaultParameters (content);
+                        contentParams.put("description", _smartHelper.getWorkflowActionUnvailableDescription(this._initialParameters, content));
+                        
+                        List<Map<String, Object>> invalidActionContents = (List<Map<String, Object>>) results.get("invalidworkflowaction-contents");
+                        invalidActionContents.add(contentParams);
+                        
+                        error = true;
+                    }
+                    else
+                    {
+                        results.put("workflowaction-content-actionId", actionId);
+                    }
+                }
+                
+                // Is workflow step correct
+                String enabledOnWorkflowStepOnly = (String) this._initialParameters.get("enabled-on-workflow-step-only"); 
+                if (enabledOnWorkflowStepOnly != null && !_smartHelper.isWorkflowStepCorrect(this._initialParameters, content))
+                {
+                    Map<String, Object> contentParams = getContentDefaultParameters (content);
+                    contentParams.put("description", _smartHelper.getIncorrectWorkflowStepDescription(this._initialParameters, content));
+                    
+                    List<Map<String, Object>> invalidStepContents = (List<Map<String, Object>>) results.get("invalidworkflowstep-contents");
+                    invalidStepContents.add(contentParams);
+                    
+                    error = true;
+                }
+                
+                if (_isAllRight (content, error, results))
+                {
+                    Map<String, Object> contentParams = getContentDefaultParameters (content);
+                    contentParams.put("description", _smartHelper.getAllRightDescription(this._initialParameters, content));
+                    
+                    List<Map<String, Object>> allrightContents = (List<Map<String, Object>>) results.get("allright-contents");
+                    allrightContents.add(contentParams);
+                }
+            }
+        }
+        
+        return results;
+    }
+    
+    /**
+     * Determines if the user can finally do action on content
+     * @param content The content
+     * @param hasError true if a error has already occurs
+     * @param results The result parameters to be passed to client side
+     * @return true if the user can finally do action on content
+     */
+    protected boolean _isAllRight (Content content, boolean hasError, Map<String, Object> results)
+    {
+        return !hasError;
+    }
+    
+    
+    /**
+     * Get the default content's parameters
+     * @param content The content
+     * @return The default parameters
+     */
+    protected Map<String, Object> getContentDefaultParameters (Content content)
+    {
+        Map<String, Object> contentParams = new HashMap<String, Object>();
+        contentParams.put("id", content.getId());
+        contentParams.put("title", content.getTitle());
+        
+        return contentParams;
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/clientsideelement/ContentTypesGallery.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/clientsideelement/ContentTypesGallery.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/clientsideelement/ContentTypesGallery.java	(revision 0)
@@ -0,0 +1,367 @@
+/*
+ *  Copyright 2010 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.clientsideelement;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.apache.avalon.framework.component.ComponentException;
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.ConfigurationException;
+import org.apache.avalon.framework.configuration.DefaultConfiguration;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+
+import org.ametys.cms.contenttype.ContentType;
+import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.ui.SimpleMenu;
+import org.ametys.runtime.right.RightsManager.RightResult;
+import org.ametys.runtime.ui.ClientSideElement;
+import org.ametys.runtime.ui.StaticClientSideElement;
+import org.ametys.runtime.util.I18nUtils;
+import org.ametys.runtime.util.I18nizableText;
+
+/**
+ * This element creates a menu with one gallery item per content type classified by category. 
+ * The user rights are checked. 
+ */
+public class ContentTypesGallery extends SimpleMenu
+{
+    /** The list of content types */
+    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
+    /** The i18n utils */
+    protected I18nUtils _i18nUtils;
+    
+    private boolean _contentTypesInitialized;
+    
+    @Override
+    public void service(ServiceManager smanager) throws ServiceException
+    {
+        super.service(smanager);
+        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
+        _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE);
+    }
+    
+    @Override
+    protected void _getGalleryItems(Map<String, Object> parameters, Map<String, Object> contextualParameters)
+    {
+        try
+        {
+            _lazyInitializeContentTypeGallery();
+        }
+        catch (Exception e)
+        {
+            throw new IllegalStateException("Unable to lookup client side element local components", e);
+        }
+        
+        if (_galleryItems.size() > 0)
+        {
+            parameters.put("gallery-item", new LinkedHashMap<String, Object>());
+            
+            for (GalleryItem galleryItem : _galleryItems)
+            {
+                Map<String, Object> galleryItems = (Map<String, Object>) parameters.get("gallery-item");
+                galleryItems.put("gallery-groups", new ArrayList<Object>());
+                
+                for (GalleryGroup galleryGp : galleryItem.getGroups())
+                {
+                    List<Object> galleryGroups = (List<Object>) galleryItems.get("gallery-groups");
+                    
+                    Map<String, Object> groupParams = new LinkedHashMap<String, Object>();
+                    
+                    I18nizableText label = galleryGp.getLabel();
+                    groupParams.put("label", label);
+                    
+                    // Group items
+                    List<String> gpItems = new ArrayList<String>();
+                    for (ClientSideElement element : galleryGp.getItems())
+                    {
+                        String cTypeId = element.getId().substring(this.getId().length() + 1);
+                        ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
+                        
+                        if (hasRight(cType))
+                        {
+                            gpItems.add(element.getId());
+                        }
+                    }
+                    
+                    if (gpItems.size() > 0)
+                    {
+                        groupParams.put("items", gpItems);
+                        galleryGroups.add(groupParams);
+                    }
+                }
+            }
+        }
+    }
+    
+    private void _lazyInitializeContentTypeGallery() throws ConfigurationException
+    {
+        if (!_contentTypesInitialized)
+        {
+            Map<I18nizableText, Set<ContentType>> cTypesByGroup = _getContentTypesByGroup();
+            
+            if (cTypesByGroup.size() > 0)
+            {
+                GalleryItem galleryItem = new GalleryItem();
+                
+                for (I18nizableText groupLabel : cTypesByGroup.keySet())
+                {
+                    GalleryGroup galleryGroup = new GalleryGroup(groupLabel);
+                    galleryItem.addGroup(galleryGroup);
+                    
+                    Set<ContentType> cTypes = cTypesByGroup.get(groupLabel);
+                    for (ContentType cType : cTypes)
+                    {
+                        String id = this.getId() + "-" + cType.getId();
+                        
+                        try
+                        {
+                            Configuration conf = _getContentTypeConfiguration (id, cType);
+                            _galleryItemManager.addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
+                            galleryGroup.addItem(new UnresolvedItem(id, true));
+                        }
+                        catch (ComponentException e)
+                        {
+                            throw new ConfigurationException("Unable to configure local client side element of id " + id, e);
+                        }
+                    }
+                }
+                
+                _galleryItems.add(galleryItem);
+            }
+            
+            if (_galleryItems.size() > 0)
+            {
+                try
+                {
+                    _galleryItemManager.initialize();
+                }
+                catch (Exception e)
+                {
+                    throw new ConfigurationException("Unable to lookup parameter local components", e);
+                }
+            }
+        }
+        
+        _contentTypesInitialized = true;
+    }
+    
+    /**
+     * Get the configuration of the content type item
+     * @param id The id of item
+     * @param cType The content type
+     * @return The configuration
+     */
+    protected Configuration _getContentTypeConfiguration (String id, ContentType cType)
+    {
+        DefaultConfiguration conf = new DefaultConfiguration("extension");
+        conf.setAttribute("id", id);
+        
+        DefaultConfiguration classConf = new DefaultConfiguration("class");
+        classConf.setAttribute("name", "Ametys.ribbon.element.ui.ButtonController");
+        
+        // Parent controller id
+        DefaultConfiguration parentIdConf = new DefaultConfiguration("controllerParentId");
+        parentIdConf.setValue(this.getId());
+        classConf.addChild(parentIdConf);
+        
+        // Label
+        DefaultConfiguration labelConf = new DefaultConfiguration("label");
+        labelConf.setValue(_i18nUtils.translate(cType.getLabel()));
+        classConf.addChild(labelConf);
+        
+        // Description
+        DefaultConfiguration descConf = new DefaultConfiguration("description");
+        descConf.setValue(_i18nUtils.translate(cType.getDescription()));
+        classConf.addChild(descConf);
+        
+        // Content type
+        DefaultConfiguration typeConf = new DefaultConfiguration("contentTypes");
+        typeConf.setValue(cType.getId());
+        classConf.addChild(typeConf);
+        
+        // Icons
+        DefaultConfiguration iconSmallConf = new DefaultConfiguration("icon-small");
+        iconSmallConf.setValue(cType.getSmallIcon());
+        classConf.addChild(iconSmallConf);
+        DefaultConfiguration iconMediumConf = new DefaultConfiguration("icon-medium");
+        iconMediumConf.setValue(cType.getMediumIcon());
+        classConf.addChild(iconMediumConf);
+        DefaultConfiguration iconLargeConf = new DefaultConfiguration("icon-large");
+        iconLargeConf.setValue(cType.getLargeIcon());
+        classConf.addChild(iconLargeConf);
+        
+        // Common configuration
+        Map<String, Object> commonConfig = (Map<String, Object>) this._initialParameters.get("items-config");
+        for (String tagName : commonConfig.keySet())
+        {
+            DefaultConfiguration c = new DefaultConfiguration(tagName);
+            c.setValue(String.valueOf(commonConfig.get(tagName)));
+            classConf.addChild(c);
+        }
+        
+        conf.addChild(classConf);
+        return conf;
+    }
+    
+    /**
+     * Get the list of content types classified by groups
+     * @return The content types
+     */
+    protected Map<I18nizableText, Set<ContentType>> _getContentTypesByGroup ()
+    {
+        Map<I18nizableText, Set<ContentType>> groups = new TreeMap<I18nizableText, Set<ContentType>>(new I18nizableTextComparator());
+        
+        if (this._initialParameters.get("contentTypes") != null)
+        {
+            String[] contentTypesIds = ((String) this._initialParameters.get("contentTypes")).split(",");
+            
+            boolean allowInherit = "true".equals(this._initialParameters.get("allowInherit"));
+            
+            for (String contentTypeId : contentTypesIds)
+            {
+                ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
+                
+                if (isValidContentType(contentType))
+                {
+                    addContentType (contentType, groups);
+                }
+                
+                if (allowInherit)
+                {
+                    for (String subTypeId : _contentTypeExtensionPoint.getSubTypes(contentTypeId))
+                    {
+                        ContentType subContentType = _contentTypeExtensionPoint.getExtension(subTypeId);
+                        if (isValidContentType(subContentType))
+                        {
+                            addContentType (subContentType, groups);
+                        }
+                    }
+                }
+            }
+        }
+        else
+        {
+            Set<String> contentTypesIds = _contentTypeExtensionPoint.getExtensionsIds();
+            
+            for (String contentTypeId : contentTypesIds)
+            {
+                ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
+                
+                if (isValidContentType(contentType))
+                {
+                    addContentType (contentType, groups);
+                }
+            }
+        }
+        
+        return groups;
+    }
+    
+    /**
+     * Add content to groups
+     * @param contentType The content type
+     * @param groups The groups
+     */
+    protected void addContentType (ContentType contentType, Map<I18nizableText, Set<ContentType>> groups)
+    {
+        I18nizableText group = contentType.getCategory();
+        if ((group.isI18n() && group.getKey().isEmpty()) || (!group.isI18n() && group.getLabel().isEmpty()))
+        {
+            group = new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_CREATECONTENTMENU_GROUP_10_CONTENT");
+        }
+        
+        if (!groups.containsKey(group))
+        {
+            groups.put(group, new TreeSet<ContentType>(new ContentTypeComparator()));
+        }
+        Set<ContentType> cTypes = groups.get(group);
+        cTypes.add(contentType);
+    }
+    
+    
+    /**
+     * Determines if the content type is a valid content type for the gallery
+     * @param contentType The coentent
+     * @return true if it is a valid content type
+     */
+    protected boolean isValidContentType (ContentType contentType)
+    {
+        return !contentType.isAbstract() && !contentType.isPrivate() && !contentType.isMixin();
+    }
+    
+    /**
+     * Test if the current user has the right needed by the content type to create a content.
+     * @param cType the content type
+     * @return true if the user has the right needed, false otherwise.
+     */
+    protected boolean hasRight(ContentType cType)
+    {
+        String right = cType.getRight();
+        
+        if (right == null)
+        {
+            return true;
+        }
+        else
+        {
+            String login = _currentUserProvider.getUser();
+            return _rightsManager.hasRight(login, right, "/contributor") == RightResult.RIGHT_OK || _rightsManager.hasRight(login, right, "/contents") == RightResult.RIGHT_OK;
+        }
+    }
+    
+    class ContentTypeComparator implements Comparator<ContentType>
+    {
+        @Override
+        public int compare(ContentType c1, ContentType c2)
+        {
+            I18nizableText t1 = c1.getLabel();
+            I18nizableText t2 = c2.getLabel();
+            
+            String str1 = t1.isI18n() ? t1.getKey() : t1.getLabel();
+            String str2 = t2.isI18n() ? t2.getKey() : t2.getLabel();
+            
+            int compareTo = str1.toString().compareTo(str2.toString());
+            if (compareTo == 0)
+            {
+                // Content types have same keys but there are not equals, so do not return 0 to add it in TreeSet
+                // Indeed, in a TreeSet implementation two elements that are equal by the method compareTo are, from the standpoint of the set, equal 
+                return 1;
+            }
+            return compareTo;
+        }
+    }
+    
+    class I18nizableTextComparator implements Comparator<I18nizableText>
+    {
+        @Override
+        public int compare(I18nizableText t1, I18nizableText t2)
+        {
+            String str1 = t1.isI18n() ? t1.getKey() : t1.getLabel();
+            String str2 = t2.isI18n() ? t2.getKey() : t2.getLabel();
+            
+            return str1.toString().compareTo(str2.toString());
+        }
+    }
+    
+}
Index: main/plugin-cms/src/org/ametys/cms/clientsideelement/MixinContentTypesGallery.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/clientsideelement/MixinContentTypesGallery.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/clientsideelement/MixinContentTypesGallery.java	(revision 0)
@@ -0,0 +1,31 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.clientsideelement;
+
+import org.ametys.cms.contenttype.ContentType;
+
+/**
+ * This element creates a menu with one gallery item per mixin content type classified by category. 
+ * The user rights are checked. 
+ */
+public class MixinContentTypesGallery extends SmartContentTypesGallery
+{
+    @Override
+    protected boolean isValidContentType(ContentType contentType)
+    {
+        return contentType.isMixin() && !contentType.isPrivate() && !contentType.isAbstract();
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/transformation/xslt/AmetysXSLTHelper.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/transformation/xslt/AmetysXSLTHelper.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/transformation/xslt/AmetysXSLTHelper.java	(working copy)
@@ -35,6 +35,7 @@
 import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
 import org.ametys.cms.repository.Content;
+import org.ametys.cms.transformation.dom.StringElement;
 import org.ametys.cms.transformation.dom.TagElement;
 import org.ametys.plugins.explorer.resources.Resource;
 import org.ametys.plugins.explorer.resources.ResourceCollection;
@@ -82,33 +83,73 @@
     /* ------------------------ */
     
     /**
-     * Get the content type of a content
+     * Get the content types of a content
      * @param contentId The content id
      * @return The content type or empty if the content does not exist
      */
-    public static String contentType (String contentId)
+    public static NodeList contentTypes(String contentId)
     {
+        ArrayList<StringElement> contentTypes = new ArrayList<StringElement>();
+        
         try
         {
             Content content = _ametysObjectResolver.resolveById(contentId);
+            
             try
             {
-                return content.getType();
+                for (String id : content.getTypes())
+                {
+                    contentTypes.add(new StringElement("content-type", "id", id));
+                }
             }
             catch (AmetysRepositoryException e)
             {
                 _logger.error("Can not get type of content with id '" + contentId + "'", e);
-                return "";
             }
         }
         catch (UnknownAmetysObjectException e)
         {
             _logger.error("Can not get type of content with id '" + contentId + "'", e);
-            return "";
         }
+        
+        return new AmetysNodeList(contentTypes);
     }
     
     /**
+     * Get the mixins of a content
+     * @param contentId The content id
+     * @return The content type or empty if the content does not exist
+     */
+    public static NodeList contentMixinTypes(String contentId)
+    {
+        ArrayList<StringElement> contentTypes = new ArrayList<StringElement>();
+        
+        try
+        {
+            Content content = _ametysObjectResolver.resolveById(contentId);
+            
+            try
+            {
+                for (String id : content.getMixinTypes())
+                {
+                    contentTypes.add(new StringElement("mixin", "id", id));
+                }
+            }
+            catch (AmetysRepositoryException e)
+            {
+                _logger.error("Can not get type of content with id '" + contentId + "'", e);
+            }
+        }
+        catch (UnknownAmetysObjectException e)
+        {
+            _logger.error("Can not get type of content with id '" + contentId + "'", e);
+        }
+        
+        return new AmetysNodeList(contentTypes);
+    }
+    
+    
+    /**
      * Get the metadata of a content
      * @param contentId The content id
      * @param metadataName The metadata name (/ for composite)
Index: main/plugin-cms/src/org/ametys/cms/repository/ModifiableContent.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/ModifiableContent.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/repository/ModifiableContent.java	(working copy)
@@ -31,12 +31,18 @@
 public interface ModifiableContent extends Content, ModifiableMetadataAwareAmetysObject, ModifiableDublinCoreAwareAmetysObject, RemovableAmetysObject
 {
     /**
-     * Set the type of this Content.<br>
-     * This method may only be called on a new Content, ie. before its first save.
-     * @param type the type of this content.
+     * Set the types of this Content.<br>
+     * @param types the types of this content.
+     * @throws AmetysRepositoryException if an error occurs.
+     */
+    public void setTypes(String[] types) throws AmetysRepositoryException;
+    
+    /**
+     * Set the mixins of this Content.<br>
+     * @param mixins the mixins of this content.
      * @throws AmetysRepositoryException if an error occurs.
      */
-    public void setType(String type) throws AmetysRepositoryException;
+    public void setMixinTypes(String[] mixins) throws AmetysRepositoryException;
 
     /**
      * Set the type of this Content.<br>
Index: main/plugin-cms/src/org/ametys/cms/repository/ModifiableContentHelper.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/ModifiableContentHelper.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/repository/ModifiableContentHelper.java	(working copy)
@@ -58,9 +58,6 @@
     /** Constants for lastModified Metadata* */
     public static final String METADATA_MODIFIED = "lastModified";
     
-    /** Constants for contentType Metadata* */
-    public static final String METADATA_CONTENTTYPE = "contentType";
-    
     private ModifiableContentHelper()
     {
         // Hides default constructor.
@@ -74,20 +71,48 @@
      */
     public static void setType(DefaultContent content, String type) throws AmetysRepositoryException
     {
+        setTypes(content, new String[] {type});
+    }
+    
+    /**
+     * Set {@link DefaultContent} types.
+     * @param content the {@link DefaultContent} to set.
+     * @param types the types.
+     * @throws AmetysRepositoryException if an error occurs.
+     */
+    public static void setTypes(DefaultContent content, String[] types) throws AmetysRepositoryException
+    {
         Node node = content.getNode();
         
-        if (!node.isNew())
+        try
         {
-            throw new AmetysRepositoryException("Cannot call setType on an already persisted Content");
+            content._checkLock();
+            node.setProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_CONTENTTYPE, types);
         }
+        catch (RepositoryException e)
+        {
+            throw new AmetysRepositoryException("Unable to set contentType property", e);
+        }
+    }
+    
+    /**
+     * Set {@link DefaultContent} mixins.
+     * @param content the {@link DefaultContent} to set.
+     * @param mixins the mixins.
+     * @throws AmetysRepositoryException if an error occurs.
+     */
+    public static void setMixinTypes(DefaultContent content, String[] mixins) throws AmetysRepositoryException
+    {
+        Node node = content.getNode();
         
         try
         {
-            node.setProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_CONTENTTYPE, type);
+            content._checkLock();
+            node.setProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_MIXINCONTENTTYPES, mixins);
         }
         catch (RepositoryException e)
         {
-            throw new AmetysRepositoryException("Unable to set contentType property", e);
+            throw new AmetysRepositoryException("Unable to set mixins property", e);
         }
     }
     
Index: main/plugin-cms/src/org/ametys/cms/repository/MixinTypeExpression.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/MixinTypeExpression.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/repository/MixinTypeExpression.java	(revision 0)
@@ -0,0 +1,75 @@
+/*
+ *  Copyright 2010 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.repository;
+
+import org.ametys.plugins.repository.AmetysRepositoryException;
+import org.ametys.plugins.repository.RepositoryConstants;
+import org.ametys.plugins.repository.query.expression.Expression;
+
+/**
+ * Constructs an {@link Expression} testing the mixin type.
+ */
+public class MixinTypeExpression implements Expression
+{
+    private Operator _operator;
+    private String[] _values;
+
+    /**
+     * Creates the expression.
+     * @param operator the operator to make the comparison (only Operator.EQ and Operator.NE allowed)
+     * @param value the content type value
+     */
+    public MixinTypeExpression(Operator operator, String... value)
+    {
+        if (Operator.EQ != operator && Operator.NE != operator)
+        {
+            throw new AmetysRepositoryException("Test operator '" + "' is unknown for test's expression.");
+        }
+        
+        _operator = operator;
+        _values = value;
+    }
+    
+    public String build()
+    {
+        StringBuilder xpath = new StringBuilder();
+        
+        if (_values.length == 1)
+        {
+            xpath.append('@').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_MIXINCONTENTTYPES)
+                 .append(' ').append(_operator).append(" '").append(_values[0].replaceAll("'", "''")).append("'");
+            
+            return xpath.toString();
+        }
+        
+        xpath.append(Operator.EQ.equals(_operator) ? '(' : "not(");
+        
+        for (int i = 0; i < _values.length; i++)
+        {
+            if (i > 0)
+            {
+                xpath.append(" or ");
+            }
+            
+            xpath.append('@').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_MIXINCONTENTTYPES)
+                 .append(" = '").append(_values[i].replaceAll("'", "''")).append("'");
+        }
+        
+        xpath.append(')');
+        
+        return xpath.toString();
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/repository/SearchGenerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/SearchGenerator.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/repository/SearchGenerator.java	(working copy)
@@ -43,6 +43,7 @@
 import org.ametys.cms.contenttype.AbstractMetadataSetElement;
 import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.contenttype.MetadataDefinitionReference;
 import org.ametys.cms.contenttype.MetadataManager;
 import org.ametys.cms.contenttype.MetadataSet;
@@ -67,6 +68,7 @@
 import org.ametys.runtime.right.RightsManager;
 import org.ametys.runtime.user.User;
 import org.ametys.runtime.user.UsersManager;
+import org.ametys.runtime.util.I18nUtils;
 import org.ametys.runtime.util.I18nizableText;
 import org.ametys.runtime.util.cocoon.AbstractCurrentUserProviderServiceableGenerator;
 import org.ametys.runtime.util.parameter.ParameterHelper;
@@ -109,7 +111,13 @@
     
     /** The cms language manager */
     protected LanguagesManager _languagesManager;
+    
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
 
+    /** I18n utils */
+    protected I18nUtils _i18nUtils;
+    
     @Override
     public void service(ServiceManager serviceManager) throws ServiceException
     {
@@ -122,6 +130,8 @@
         _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
         _metadataManager = (MetadataManager) manager.lookup(MetadataManager.ROLE);
         _languagesManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
+        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
     }
 
     public void generate() throws IOException, SAXException, ProcessingException
@@ -338,10 +348,11 @@
     /**
      * Extracts the metadata in the request parameters and returns the {@link MetadataSet} to SAX
      * @param request The request
-     * @param contentType The content type
+     * @param cTypes The content types
+     * @param mixins The mixins
      * @return the metadata to SAX in a {@link MetadataSet}
      */
-    protected MetadataSet getMetadataToSAX (Request request, ContentType contentType)
+    protected MetadataSet getMetadataToSAX (Request request, String[] cTypes, String[] mixins)
     {
         MetadataSet metadataSet = new MetadataSet();
        
@@ -355,7 +366,7 @@
                 String propertyPath = name.substring("property-".length());
                 // Split on composite metadata
                 String[] parts = propertyPath.split("/");
-                if (_isMetadata(contentType, parts[0]))
+                if (_isMetadata(cTypes, mixins, parts[0]))
                 {
                     for (String propertyName : parts)
                     {
@@ -376,9 +387,9 @@
         return metadataSet;
     }
     
-    private boolean _isMetadata (ContentType contentType, String name)
+    private boolean _isMetadata (String[] cTypes, String[] mixins, String name)
     {
-        return contentType.getMetadataDefinition(name) != null;
+        return _contentTypesHelper.getMetadataDefinition(name, cTypes, mixins) != null;
     }
     
     /**
@@ -548,11 +559,9 @@
     {
         AttributesImpl attrs = new AttributesImpl();
         
-        ContentType contentType =  _contentTypeExtensionPoint.getExtension(content.getType());
-        
-        attrs.addCDATAAttribute("smallIcon", contentType != null ? contentType.getSmallIcon() : "/plugins/cms/resources/img/contenttype/unknown-small.png");
-        attrs.addCDATAAttribute("mediumIcon", contentType != null ? contentType.getMediumIcon() : "/plugins/cms/resources/img/contenttype/unknown-medium.png");
-        attrs.addCDATAAttribute("largeIcon", contentType != null ? contentType.getLargeIcon() : "/plugins/cms/resources/img/contenttype/unknown-large.png");
+        attrs.addCDATAAttribute("smallIcon", _contentTypesHelper.getSmallIcon(content));
+        attrs.addCDATAAttribute("mediumIcon", _contentTypesHelper.getMediumIcon(content));
+        attrs.addCDATAAttribute("largeIcon", _contentTypesHelper.getLargeIcon(content));
         attrs.addCDATAAttribute("id", content.getId());
         attrs.addCDATAAttribute("name", content.getName());
         attrs.addCDATAAttribute("title", content.getTitle());
@@ -566,12 +575,15 @@
             
         XMLUtils.startElement(contentHandler, "content", attrs);
 
-        // SAX the content type label
-        if (contentType != null)
+        List<String> cTypeLabels = new ArrayList<String>();
+        for (String cTypeId : content.getTypes())
         {
-            contentType.getLabel().toSAX(contentHandler, "content-type");
+            ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
+            cTypeLabels.add(_i18nUtils.translate(cType.getLabel(), content.getLanguage()));
         }
         
+        XMLUtils.createElement(contentHandler, "content-type", StringUtils.join(cTypeLabels, ", "));
+        
         if (saxWorkflow && content instanceof WorkflowAwareContent)
         {
             // SAX the current workflow step
@@ -583,7 +595,7 @@
         
         XMLUtils.startElement(contentHandler, "metadata");
         
-        MetadataSet metadataSet = getMetadataToSAX(request, contentType);
+        MetadataSet metadataSet = getMetadataToSAX(request, content.getTypes(), content.getMixinTypes());
         _metadataManager.saxReadableMetadata(contentHandler, content, metadataSet);
         
         // SAX commons properties
Index: main/plugin-cms/src/org/ametys/cms/repository/CopiableContentComponent.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/CopiableContentComponent.java	(revision 25586)
+++ main/plugin-cms/src/org/ametys/cms/repository/CopiableContentComponent.java	(working copy)
@@ -94,7 +94,7 @@
             
             ModifiableContent content = parent.createChild(contentName, originalContentType);
             content.setLanguage(originalContent.getLanguage());
-            content.setType(originalContent.getType());
+            content.setTypes(originalContent.getTypes());
             
             if (originalContent instanceof WorkflowAwareContent)
             {
Index: main/plugin-cms/src/org/ametys/cms/repository/ModifiableDefaultContent.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/ModifiableDefaultContent.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/repository/ModifiableDefaultContent.java	(working copy)
@@ -21,8 +21,6 @@
 
 import javax.jcr.Node;
 import javax.jcr.RepositoryException;
-import javax.jcr.lock.Lock;
-import javax.jcr.lock.LockManager;
 
 import org.apache.commons.lang.ArrayUtils;
 
@@ -44,8 +42,6 @@
  */
 public class ModifiableDefaultContent<F extends ModifiableContentFactory> extends DefaultContent<F> implements CommentableContent, LockableAmetysObject, ModifiableWorkflowAwareContent, AnnotableContent//, CopiableAmetysObject
 {
-    private boolean _lockAlreadyChecked;
-    
     /**
      * Creates a JCR-based Content.
      * @param node the JCR Node backing this Content.
@@ -57,9 +53,14 @@
         super(node, parentPath, factory);
     }
 
-    public void setType(String type) throws AmetysRepositoryException
+    public void setTypes(String[] types) throws AmetysRepositoryException
     {
-        ModifiableContentHelper.setType(this, type);
+        ModifiableContentHelper.setTypes(this, types);
+    }
+    
+    public void setMixinTypes(String[] mixins) throws AmetysRepositoryException
+    {
+        ModifiableContentHelper.setMixinTypes(this, mixins);
     }
     
     public void setLanguage(String language) throws AmetysRepositoryException
@@ -379,19 +380,4 @@
             annotations.removeMetadata(annotationName);
         }
     }
-    
-    private void _checkLock() throws RepositoryException
-    {
-        if (!_lockAlreadyChecked && getNode().isLocked())
-        {
-            LockManager lockManager = getNode().getSession().getWorkspace().getLockManager();
-            
-            Lock lock = lockManager.getLock(getNode().getPath());
-            Node lockHolder = lock.getNode();
-            
-            lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString());
-            _lockAlreadyChecked = true;
-        }
-    }
-    
 }
Index: main/plugin-cms/src/org/ametys/cms/repository/Content.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/Content.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/repository/Content.java	(working copy)
@@ -48,11 +48,11 @@
 public interface Content extends MetadataAwareAmetysObject, DublinCoreAwareAmetysObject
 {
     /**
-     * Retrieves the type of this content.
-     * @return the type of this content.
+     * Retrieves the types of this content.
+     * @return the types of this content.
      * @throws AmetysRepositoryException if an error occurs.
      */
-    public String getType() throws AmetysRepositoryException;
+    public String[] getTypes() throws AmetysRepositoryException;
     
     /**
      * Retrieves the mixin types of this content.
Index: main/plugin-cms/src/org/ametys/cms/repository/ContentTypeOrMixinTypeExpression.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/ContentTypeOrMixinTypeExpression.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/repository/ContentTypeOrMixinTypeExpression.java	(revision 0)
@@ -0,0 +1,84 @@
+/*
+ *  Copyright 2010 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.repository;
+
+import org.ametys.plugins.repository.AmetysRepositoryException;
+import org.ametys.plugins.repository.RepositoryConstants;
+import org.ametys.plugins.repository.query.expression.Expression;
+
+/**
+ * Constructs an {@link Expression} testing the content type.
+ */
+public class ContentTypeOrMixinTypeExpression implements Expression
+{
+    private Operator _operator;
+    private String[] _values;
+
+    /**
+     * Creates the expression.
+     * @param operator the operator to make the comparison (only Operator.EQ and Operator.NE allowed)
+     * @param value the content type value
+     */
+    public ContentTypeOrMixinTypeExpression(Operator operator, String... value)
+    {
+        if (Operator.EQ != operator && Operator.NE != operator)
+        {
+            throw new AmetysRepositoryException("Test operator '" + "' is unknown for test's expression.");
+        }
+        
+        _operator = operator;
+        _values = value;
+    }
+    
+    public String build()
+    {
+        StringBuilder xpath = new StringBuilder();
+        
+        if (_values.length == 1)
+        {
+            xpath.append("(");
+            xpath.append('@').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_CONTENTTYPE)
+                 .append(' ').append(_operator).append(" '").append(_values[0].replaceAll("'", "''")).append("'");
+            
+            xpath.append(" or ");
+            
+            xpath.append('@').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_MIXINCONTENTTYPES)
+            .append(' ').append(_operator).append(" '").append(_values[0].replaceAll("'", "''")).append("'");
+            xpath.append(")");
+            return xpath.toString();
+        }
+        
+        xpath.append(Operator.EQ.equals(_operator) ? '(' : "not(");
+        
+        for (int i = 0; i < _values.length; i++)
+        {
+            if (i > 0)
+            {
+                xpath.append(" or ");
+            }
+            
+            xpath.append('@').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_CONTENTTYPE)
+                 .append(" = '").append(_values[i].replaceAll("'", "''")).append("'");
+            xpath.append(" or ");
+            xpath.append('@').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_MIXINCONTENTTYPES)
+            .append(" = '").append(_values[i].replaceAll("'", "''")).append("'");
+        }
+        
+        xpath.append(')');
+        
+        return xpath.toString();
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/repository/ContentTypeOrMixinExpression.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/ContentTypeOrMixinExpression.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/repository/ContentTypeOrMixinExpression.java	(revision 0)
@@ -0,0 +1,84 @@
+/*
+ *  Copyright 2010 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.repository;
+
+import org.ametys.plugins.repository.AmetysRepositoryException;
+import org.ametys.plugins.repository.RepositoryConstants;
+import org.ametys.plugins.repository.query.expression.Expression;
+
+/**
+ * Constructs an {@link Expression} testing the content type.
+ */
+public class ContentTypeOrMixinExpression implements Expression
+{
+    private Operator _operator;
+    private String[] _values;
+
+    /**
+     * Creates the expression.
+     * @param operator the operator to make the comparison (only Operator.EQ and Operator.NE allowed)
+     * @param value the content type value
+     */
+    public ContentTypeOrMixinExpression(Operator operator, String... value)
+    {
+        if (Operator.EQ != operator && Operator.NE != operator)
+        {
+            throw new AmetysRepositoryException("Test operator '" + "' is unknown for test's expression.");
+        }
+        
+        _operator = operator;
+        _values = value;
+    }
+    
+    public String build()
+    {
+        StringBuilder xpath = new StringBuilder();
+        
+        if (_values.length == 1)
+        {
+            xpath.append("(");
+            xpath.append('@').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_CONTENTTYPE)
+                 .append(' ').append(_operator).append(" '").append(_values[0].replaceAll("'", "''")).append("'");
+            
+            xpath.append(" or ");
+            
+            xpath.append('@').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_MIXINCONTENTTYPES)
+            .append(' ').append(_operator).append(" '").append(_values[0].replaceAll("'", "''")).append("'");
+            xpath.append(")");
+            return xpath.toString();
+        }
+        
+        xpath.append(Operator.EQ.equals(_operator) ? '(' : "not(");
+        
+        for (int i = 0; i < _values.length; i++)
+        {
+            if (i > 0)
+            {
+                xpath.append(" or ");
+            }
+            
+            xpath.append('@').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_CONTENTTYPE)
+                 .append(" = '").append(_values[i].replaceAll("'", "''")).append("'");
+            xpath.append(" or ");
+            xpath.append('@').append(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL).append(':').append(DefaultContent.METADATA_MIXINCONTENTTYPES)
+            .append(" = '").append(_values[i].replaceAll("'", "''")).append("'");
+        }
+        
+        xpath.append(')');
+        
+        return xpath.toString();
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/repository/DefaultContent.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/repository/DefaultContent.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/repository/DefaultContent.java	(working copy)
@@ -28,6 +28,8 @@
 import javax.jcr.PropertyIterator;
 import javax.jcr.RepositoryException;
 import javax.jcr.Value;
+import javax.jcr.lock.Lock;
+import javax.jcr.lock.LockManager;
 
 import org.ametys.plugins.explorer.resources.ResourceCollection;
 import org.ametys.plugins.repository.AmetysObject;
@@ -88,6 +90,8 @@
     /** Constant for the attachment node name. */
     public static final String ATTACHMENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":attachments";
     
+    private boolean _lockAlreadyChecked;
+    
     /**
      * Creates a JCR-based Content.
      * @param node the JCR Node backing this Content.
@@ -100,11 +104,26 @@
     }
     
     @Override
-    public String getType() throws AmetysRepositoryException
+    public String[] getTypes() throws AmetysRepositoryException
     {
         try
         {
-            return getNode().getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_CONTENTTYPE).getString();
+            if (getNode().hasProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_CONTENTTYPE))
+            {
+                Value[] values = getNode().getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_CONTENTTYPE).getValues();
+                
+                String[] types = new String[values.length];
+                for (int i = 0; i < values.length; i++)
+                {
+                    Value value = values[i];
+                    types[i] = value.getString();
+                }
+                return types;
+            }
+            else
+            {
+                return new String[0];
+            }
         }
         catch (javax.jcr.RepositoryException ex)
         {
@@ -291,6 +310,21 @@
         return _getFactory()._getCopiableHelper().copy(this, parent, name);
     }
     
+    void _checkLock() throws RepositoryException
+    {
+        Node node = getNode();
+        if (!_lockAlreadyChecked && getNode().isLocked())
+        {
+            LockManager lockManager = node.getSession().getWorkspace().getLockManager();
+            
+            Lock lock = lockManager.getLock(node.getPath());
+            Node lockHolder = lock.getNode();
+            
+            lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString());
+            _lockAlreadyChecked = true;
+        }
+    }
+    
     // Dublin Core metadata. //
     
     @Override
Index: main/plugin-cms/src/org/ametys/cms/workflow/copy/CreateContentByCopyFunction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/workflow/copy/CreateContentByCopyFunction.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/workflow/copy/CreateContentByCopyFunction.java	(working copy)
@@ -73,7 +73,7 @@
         
         transientVars.put(CreateContentFunction.CONTENT_NAME_KEY, contentTitle);
         transientVars.put(CreateContentFunction.CONTENT_TITLE_KEY, contentTitle);
-        transientVars.put(CreateContentFunction.CONTENT_TYPE_KEY, baseContent.getType());
+        transientVars.put(CreateContentFunction.CONTENT_TYPES_KEY, baseContent.getTypes());
         transientVars.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, baseContent.getLanguage());
         
         // Super method call.
Index: main/plugin-cms/src/org/ametys/cms/workflow/CreateContentFunction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/workflow/CreateContentFunction.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/workflow/CreateContentFunction.java	(working copy)
@@ -59,6 +59,8 @@
     public static final String CONTENT_TITLE_KEY = CreateContentFunction.class.getName() + "$contentTitle";
     /** Constant for storing the content type to use into the transient variables map. */
     public static final String CONTENT_TYPE_KEY = CreateContentFunction.class.getName() + "$contentType";
+    /** Constant for storing the content type to use into the transient variables map. */
+    public static final String CONTENT_TYPES_KEY = CreateContentFunction.class.getName() + "$contentTypes";
     /** Constant for storing the content language to use into the transient variables map. */
     public static final String CONTENT_LANGUAGE_KEY = CreateContentFunction.class.getName() + "$contentLanguage";
     /** Constant for storing the content language to use into the transient variables map. */
@@ -88,7 +90,23 @@
         {
             String desiredContentName = _getNonNullVar(transientVars, CONTENT_NAME_KEY, "Missing content name");
             String contentTitle = _getNonNullVar(transientVars, CONTENT_TITLE_KEY, "Missing content title");
-            String contentType = _getNonNullVar(transientVars, CONTENT_TYPE_KEY, "Missing content type");
+            
+            String[] contentTypes = null;
+            String contentType = (String) transientVars.get(CONTENT_TYPE_KEY);
+            if (transientVars.get(CONTENT_TYPE_KEY) != null)
+            {
+                contentTypes = new String[] {contentType};
+            }
+            else if (transientVars.get(CONTENT_TYPES_KEY) != null)
+            {
+                contentTypes = (String[]) transientVars.get(CONTENT_TYPES_KEY);
+            }
+            
+            if (contentTypes == null)
+            {
+                throw new WorkflowException("Missing content type");
+            }
+            
             String contentLanguage = _getNonNullVar(transientVars, CONTENT_LANGUAGE_KEY, "Missing content language");
             
             ModifiableTraversableAmetysObject contents = _getContentRoot(transientVars);
@@ -96,7 +114,7 @@
             ModifiableWorkflowAwareContent content = _createContent(desiredContentName, contents);
 
             content.setTitle(contentTitle);
-            content.setType(contentType);
+            content.setTypes(contentTypes);
             content.setLanguage(contentLanguage);
             
             // Set the workflow id
Index: main/plugin-cms/src/org/ametys/cms/workflow/EditContentFunction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/workflow/EditContentFunction.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/workflow/EditContentFunction.java	(working copy)
@@ -20,6 +20,7 @@
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
@@ -36,6 +37,7 @@
 import javax.jcr.ValueFactory;
 
 import org.apache.avalon.framework.activity.Initializable;
+import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.math.NumberUtils;
 
@@ -44,6 +46,7 @@
 import org.ametys.cms.contenttype.ConsistencyExtractor;
 import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.contenttype.MetadataDefinition;
 import org.ametys.cms.contenttype.MetadataDefinitionReference;
 import org.ametys.cms.contenttype.MetadataSet;
@@ -119,6 +122,8 @@
     protected AmetysObjectResolver _resolver;
     /** Content type extension point. */
     protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
     /** Upload manager. */
     protected UploadManager _uploadManager;
     /** Observation manager available to subclasses. */
@@ -137,6 +142,7 @@
         _observationManager = (ObservationManager) _manager.lookup(ObservationManager.ROLE);
         _jsonUtils = (JSONUtils) _manager.lookup(JSONUtils.ROLE);
         _workflow = (Workflow) _manager.lookup(Workflow.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) _manager.lookup(ContentTypesHelper.ROLE);
     }
     
     @Override
@@ -175,20 +181,11 @@
                 throw new WorkflowException("Unable to get the JSON parameters");
             }
             
-            String contentTypeId = content.getType();
-            ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
-            
-            if (contentType == null)
-            {
-                throw new WorkflowException(String.format("Unknown content type '%s' for content %s",
-                                            contentTypeId, content));
-            }
-
             Map<String, Object> rawValues = (Map<String, Object>) jsonParameters.get("values");
-            MetadataSet metadataSet = getMetadataSet(jsonParameters, rawValues.keySet(), contentType, modifiableContent);
+            MetadataSet metadataSet = getMetadataSet(jsonParameters, rawValues.keySet(), modifiableContent);
             
             Map<String, Object> rawComments = (Map<String, Object>) jsonParameters.get("comments");
-            _bindAndValidateContent(contentType, modifiableContent, errors, metadataSet, rawValues, rawComments, login);
+            _bindAndValidateContent(modifiableContent, errors, metadataSet, rawValues, rawComments, login);
 
             if (errors.hasErrors())
             {
@@ -237,22 +234,21 @@
      * Get the metadata set for the content
      * @param jsParameters 
      * @param parameterNames The name of the form parameters extracted from the request.
-     * @param contentType 
-     * @param content 
+     * @param content The content
      * @return The metadata set asked in the request or a builtin metadataset
      * @throws WorkflowException If an error occured while getting the metadata set
      */
-    protected MetadataSet getMetadataSet(Map<String, Object> jsParameters, Set<String> parameterNames, ContentType contentType, Content content) throws WorkflowException
+    protected MetadataSet getMetadataSet(Map<String, Object> jsParameters, Set<String> parameterNames, Content content) throws WorkflowException
     {
         MetadataSet metadataSet;
         
         String metadataSetName = (String) jsParameters.get(METADATA_SET_PARAM);
         if (metadataSetName != null)
         {
-            metadataSet = contentType.getMetadataSetForEdition(metadataSetName);
+            metadataSet = _contentTypesHelper.getMetadataSetForEdition(metadataSetName, content.getTypes(), content.getMixinTypes());
             if (metadataSet == null)
             {
-                throw new WorkflowException(String.format("No metadata set for name '%s' and content type '%s'", metadataSetName, contentType.getId()));
+                throw new WorkflowException(String.format("No metadata set for name '%s' and content type(s) '%s'", metadataSetName, StringUtils.join(content.getTypes(), ',')));
             }
         }
         else
@@ -294,20 +290,23 @@
      */
     protected void _buildConsistencies(ModifiableContent content)
     {
-        String contentTypeId = content.getType();
-        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
-        ConsistencyExtractor consistencyExtractor = contentType.getConsistencyExtractor();
-        if (consistencyExtractor != null)
+        Map<String, List<String>> elements = new HashMap<String, List<String>>();
+        
+        for (String cTypeId : content.getTypes())
         {
-            Map<String, List<String>> elements = consistencyExtractor.getConsistencyElement(content);
-            
-            content.setConsistencyElements(elements);
+            ContentType contentType = _contentTypeExtensionPoint.getExtension(cTypeId);
+            ConsistencyExtractor consistencyExtractor = contentType.getConsistencyExtractor();
+            if (consistencyExtractor != null)
+            {
+                elements.putAll(consistencyExtractor.getConsistencyElement(content));
+            }
         }
+      
+        content.setConsistencyElements(elements);
     }
 
     /**
      * Bind and validate a form.
-     * @param contentType 
      * @param content the content.
      * @param allErrors the errors.
      * @param metadataSet 
@@ -316,27 +315,26 @@
      * @param login the user login.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateContent(ContentType contentType, ModifiableContent content, AllErrors allErrors, MetadataSet metadataSet, Map<String, Object> rawValues, Map<String, Object> rawComments, String login) throws WorkflowException
+    protected void _bindAndValidateContent(ModifiableContent content, AllErrors allErrors, MetadataSet metadataSet, Map<String, Object> rawValues, Map<String, Object> rawComments, String login) throws WorkflowException
     {
         Form form = new Form();
         
         // First bind and validate in the form object
-        _bindAndValidateMetadataSetElement(contentType, content, form, allErrors, metadataSet, rawValues, rawComments, null, "");
+        _bindAndValidateMetadataSetElement(content, form, allErrors, metadataSet, rawValues, rawComments, null, "");
         
         // Additional validation
-        _validateForm(contentType, content, form, allErrors);
+        _validateForm(content, form, allErrors);
         
         // Do not synchronize if there is at least one error
         if (!allErrors.hasErrors())
         {
             // Synchronize form fields and content metadata
-            _synchronizeMetadataSetElement(contentType, content, content.getMetadataHolder(), form, allErrors, login, metadataSet, null, "");
+            _synchronizeMetadataSetElement(content, content.getMetadataHolder(), form, allErrors, login, metadataSet, null, "");
         }
     }
 
     /**
      * Bind and validate a metadata set element.
-     * @param contentType the content type.
      * @param content the content.
      * @param form the form.
      * @param allErrors the errors.
@@ -347,14 +345,14 @@
      * @param metadataPath the metadata path.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateMetadataSetElement(ContentType contentType, Content content, Form form, AllErrors allErrors, AbstractMetadataSetElement metadataSetElement, Map<String, Object> rawValues, Map<String, Object> rawComments, MetadataDefinition parentMetadataDefinition, String metadataPath) throws WorkflowException
+    protected void _bindAndValidateMetadataSetElement(Content content, Form form, AllErrors allErrors, AbstractMetadataSetElement metadataSetElement, Map<String, Object> rawValues, Map<String, Object> rawComments, MetadataDefinition parentMetadataDefinition, String metadataPath) throws WorkflowException
     {
         for (AbstractMetadataSetElement subElement : metadataSetElement.getElements())
         {
             if (subElement instanceof MetadataDefinitionReference)
             {
                 MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) subElement;
-                MetadataDefinition metadataDefinition = _getMetadataDefinition(contentType, parentMetadataDefinition, metadataDefRef.getMetadataName());
+                MetadataDefinition metadataDefinition = _getMetadataDefinition(content, parentMetadataDefinition, metadataDefRef.getMetadataName());
                 
                 if (metadataDefinition == null)
                 {
@@ -362,47 +360,50 @@
                 }
                 
                 String subMetadataPath = metadataPath + metadataDefinition.getName();
-                if (contentType.canWrite(content, subMetadataPath))
+                if (_contentTypesHelper.canWrite(content, subMetadataPath))
                 {
-                    _bindAndValidateMetadata(contentType, content, form, allErrors, subElement, rawValues, rawComments, metadataDefinition, subMetadataPath);
+                    _bindAndValidateMetadata(content, form, allErrors, subElement, rawValues, rawComments, metadataDefinition, subMetadataPath);
                     _bindComments(rawComments, form, metadataDefinition, subMetadataPath); // Handle metadata comments
                 }
             }
             else
             {
-                _bindAndValidateMetadataSetElement(contentType, content, form, allErrors, subElement, rawValues, rawComments, parentMetadataDefinition, metadataPath);
+                _bindAndValidateMetadataSetElement(content, form, allErrors, subElement, rawValues, rawComments, parentMetadataDefinition, metadataPath);
             }
         }
     }
 
     /**
      * Validates the form.
-     * @param contentType the content type.
      * @param content the content.
      * @param form the form.
      * @param allErrors the errors.
      */
-    protected void _validateForm(ContentType contentType, Content content, Form form, AllErrors allErrors)
+    protected void _validateForm(Content content, Form form, AllErrors allErrors)
     {
-        Validator validator = contentType.getGlobalValidator();
-
-        if (validator != null)
+        Errors errors = new Errors();
+        
+        for (String cTypeId : content.getTypes())
         {
-            Errors errors = new Errors();
+            ContentType contentType = _contentTypeExtensionPoint.getExtension(cTypeId);
             
-            validator.validate(form, errors);
-            
-            if (errors.hasErrors())
+            Validator validator = contentType.getGlobalValidator();
+            if (validator != null)
             {
-                // Global error
-                allErrors.addError("_global", errors);
+                validator.validate(form, errors);
             }
         }
+        
+        if (errors.hasErrors())
+        {
+            // Global error
+            allErrors.addError("_global", errors);
+        }
+       
     }
 
     /**
      * Synchronize a metadata set element with a composite metadata.
-     * @param contentType the content type.
      * @param content the content.
      * @param metadata the composite metadata to synchronize.
      * @param form the form.
@@ -413,25 +414,25 @@
      * @param metadataPath the metadata path.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _synchronizeMetadataSetElement(ContentType contentType, Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, String login, AbstractMetadataSetElement metadataSetElement, MetadataDefinition parentMetadataDefinition, String metadataPath) throws WorkflowException
+    protected void _synchronizeMetadataSetElement(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, String login, AbstractMetadataSetElement metadataSetElement, MetadataDefinition parentMetadataDefinition, String metadataPath) throws WorkflowException
     {
         for (AbstractMetadataSetElement subElement : metadataSetElement.getElements())
         {
             if (subElement instanceof MetadataDefinitionReference)
             {
                 MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) subElement;
-                MetadataDefinition metadataDefinition = _getMetadataDefinition(contentType, parentMetadataDefinition, metadataDefRef.getMetadataName());
+                MetadataDefinition metadataDefinition = _getMetadataDefinition(content, parentMetadataDefinition, metadataDefRef.getMetadataName());
                 
                 String subMetadataPath = metadataPath + metadataDefinition.getName();
                 
-                if (contentType.canWrite(content, subMetadataPath))
+                if (_contentTypesHelper.canWrite(content, subMetadataPath))
                 {
-                    _synchronizeMetadata(contentType, content, metadata, form, allErrors, login, subElement, metadataDefinition, subMetadataPath);
+                    _synchronizeMetadata(content, metadata, form, allErrors, login, subElement, metadataDefinition, subMetadataPath);
                 }
             }
             else
             {
-                _synchronizeMetadataSetElement(contentType, content, metadata, form, allErrors, login, subElement, parentMetadataDefinition, metadataPath);
+                _synchronizeMetadataSetElement(content, metadata, form, allErrors, login, subElement, parentMetadataDefinition, metadataPath);
             }
         }
     }
@@ -460,16 +461,16 @@
     /**
      * Retrieves a sub metadata definition from a content type or
      * a parent metadata definition. 
-     * @param contentType the content type.
+     * @param content the content.
      * @param parentMetadataDefinition the parent metadata definition.
      * @param metadataName the metadata name.
      * @return the metadata definition found or <code>null</code> otherwise.
      */
-    protected MetadataDefinition _getMetadataDefinition(ContentType contentType, MetadataDefinition parentMetadataDefinition, String metadataName)
+    protected MetadataDefinition _getMetadataDefinition(Content content, MetadataDefinition parentMetadataDefinition, String metadataName)
     {
         if (parentMetadataDefinition == null)
         {
-            return contentType.getMetadataDefinition(metadataName);
+            return _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
         }
         else
         {
@@ -479,7 +480,6 @@
 
     /**
      * Bind and validate a form.
-     * @param contentType the content type.
      * @param content the content.
      * @param form the form.
      * @param allErrors the errors.
@@ -490,12 +490,12 @@
      * @param metadataPath the metadata path.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateMetadata(ContentType contentType, Content content, Form form, AllErrors allErrors, AbstractMetadataSetElement metadataSetElement, Map<String, Object> rawValues, Map<String, Object> rawComments, MetadataDefinition metadataDefinition, String metadataPath) throws WorkflowException
+    protected void _bindAndValidateMetadata(Content content, Form form, AllErrors allErrors, AbstractMetadataSetElement metadataSetElement, Map<String, Object> rawValues, Map<String, Object> rawComments, MetadataDefinition metadataDefinition, String metadataPath) throws WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         MetadataType type = metadataDefinition.getType();
         
-        if (!contentType.canWrite(content, metadataPath))
+        if (!_contentTypesHelper.canWrite(content, metadataPath))
         {
             throw new WorkflowException("Current user has no right to edit metadata " + metadataName);
         }
@@ -506,46 +506,46 @@
         {
             case STRING:
             case USER:
-                _bindAndValidateStringMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateStringMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case DATE:
-                _bindAndValidateDateMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateDateMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case DATETIME:
-                _bindAndValidateDateTimeMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateDateTimeMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case LONG:
-                _bindAndValidateLongMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateLongMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case GEOCODE:
-                _bindAndValidateGeocodeMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateGeocodeMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case DOUBLE:
-                _bindAndValidateDoubleMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateDoubleMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case BOOLEAN:
-                _bindAndValidateBooleanMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateBooleanMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case BINARY:
-                _bindAndValidateBinaryMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateBinaryMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case FILE:
-                _bindAndValidateFileMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateFileMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case RICH_TEXT:
-                _bindAndValidateRichText(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateRichText(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case COMPOSITE:
-                _bindAndValidateCompositeMetadata(allErrors, form, contentType, content, metadataName, metadataSetElement, metadataDefinition, metadataPath, metadataValues, rawValues, rawComments);
+                _bindAndValidateCompositeMetadata(allErrors, form, content, metadataName, metadataSetElement, metadataDefinition, metadataPath, metadataValues, rawValues, rawComments);
                 break;
             case REFERENCE:
-                _bindAndValidateReferenceMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateReferenceMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case CONTENT:
-                _bindAndValidateContentReferenceMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues);
+                _bindAndValidateContentReferenceMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues);
                 break;
             case SUB_CONTENT:
-                _bindAndValidateSubContentMetadata(allErrors, form, contentType, metadataDefinition, metadataPath, metadataValues, rawValues);
+                _bindAndValidateSubContentMetadata(allErrors, form, content, metadataDefinition, metadataPath, metadataValues, rawValues);
                 break;
             default:
                 throw new WorkflowException("Unsupported type: " + type);   
@@ -595,7 +595,7 @@
             }
             else
             {
-                metadataValues.add((String) rawValue);
+                metadataValues.add(String.valueOf(rawValue));
             }
         }
         
@@ -639,7 +639,6 @@
      * @param allErrors for storing validation errors.
      * @param form the form. 
      * @param metadataDefinition the metadata definition.
-     * @param contentType the content type
      * @param content the current content
      * @param metadataName the metadata name
      * @param metadataSetElement 
@@ -650,23 +649,22 @@
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateCompositeMetadata(AllErrors allErrors, Form form, ContentType contentType, Content content, String metadataName, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues, Map<String, Object> rawValues, Map<String, Object> rawComments) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateCompositeMetadata(AllErrors allErrors, Form form, Content content, String metadataName, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues, Map<String, Object> rawValues, Map<String, Object> rawComments) throws AmetysRepositoryException, WorkflowException
     {
         if (metadataDefinition instanceof RepeaterDefinition)
         {
-            _bindAndValidateRepeater(contentType, content, form, allErrors, metadataSetElement, rawValues, rawComments, (RepeaterDefinition) metadataDefinition, metadataPath);
+            _bindAndValidateRepeater(content, form, allErrors, metadataSetElement, rawValues, rawComments, (RepeaterDefinition) metadataDefinition, metadataPath);
         }
         else 
         {
             Form compositeForm = new Form();
-            _bindAndValidateMetadataSetElement(contentType, content, compositeForm, allErrors, metadataSetElement, rawValues, rawComments, metadataDefinition, metadataPath + "/");
+            _bindAndValidateMetadataSetElement(content, compositeForm, allErrors, metadataSetElement, rawValues, rawComments, metadataDefinition, metadataPath + "/");
             form.setCompositeField(metadataName, compositeForm);
         }
     }
 
     /**
      * Bind and validate a repeater.
-     * @param contentType the content type.
      * @param content the content.
      * @param form the form.
      * @param allErrors the errors.
@@ -677,7 +675,7 @@
      * @param metadataPath the metadata path.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateRepeater(ContentType contentType, Content content, Form form, AllErrors allErrors, AbstractMetadataSetElement metadataSetElement, Map<String, Object> rawValues, Map<String, Object> rawComments, RepeaterDefinition repeaterDefinition, String metadataPath) throws WorkflowException
+    protected void _bindAndValidateRepeater(Content content, Form form, AllErrors allErrors, AbstractMetadataSetElement metadataSetElement, Map<String, Object> rawValues, Map<String, Object> rawComments, RepeaterDefinition repeaterDefinition, String metadataPath) throws WorkflowException
     {
         String metadataName = repeaterDefinition.getName();
         int repeaterSizeValue;
@@ -727,7 +725,7 @@
             }
             
             // Bind and validate each entry
-            _bindAndValidateMetadataSetElement(contentType, content, repeaterEntry, allErrors, metadataSetElement, rawValues, rawComments, repeaterDefinition, metadataPath + "/" + i + "/");
+            _bindAndValidateMetadataSetElement(content, repeaterEntry, allErrors, metadataSetElement, rawValues, rawComments, repeaterDefinition, metadataPath + "/" + i + "/");
             
             repeaterField.addEntry(repeaterEntry);
         }
@@ -763,17 +761,17 @@
      * @param allErrors for storing validation errors.
      * @param form the form. 
      * @param metadataDefinition the metadata definition.
-     * @param cType the content type
+     * @param content the content
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateStringMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateStringMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, metadataValues))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, metadataValues))
         {
             if (metadataDefinition.isMultiple())
             {
@@ -793,14 +791,14 @@
      * Bind and validate a date metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateDateMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateDateMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         List<Date> dateValues = new ArrayList<Date>();
@@ -824,7 +822,7 @@
         
         Date[] dateArray = dateValues.toArray(new Date[dateValues.size()]);
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, dateArray))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, dateArray))
         {
             if (metadataDefinition.isMultiple())
             {
@@ -844,14 +842,14 @@
      * Bind and validate a date time metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateDateTimeMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateDateTimeMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         List<Date> dateValues = new ArrayList<Date>();
@@ -875,7 +873,7 @@
         
         Date[] dateArray = dateValues.toArray(new Date[dateValues.size()]);
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, dateArray))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, dateArray))
         {
             if (metadataDefinition.isMultiple())
             {
@@ -895,14 +893,14 @@
      * Bind and validate a long metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateLongMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateLongMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         long[] longValues = new long[metadataValues.length];
@@ -924,7 +922,7 @@
             }
         }
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, longValues))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, longValues))
         {
             if (metadataDefinition.isMultiple())
             {
@@ -944,14 +942,14 @@
      * Bind and validate a geocode metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateGeocodeMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateGeocodeMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         Map<String, Object> geocodeValues = Collections.emptyMap();
@@ -976,14 +974,14 @@
      * Bind and validate a double metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateDoubleMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateDoubleMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         double[] doubleValues = new double[metadataValues.length];
@@ -1005,7 +1003,7 @@
             }
         }
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, doubleValues))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, doubleValues))
         {
             if (metadataDefinition.isMultiple())
             {
@@ -1025,14 +1023,14 @@
      * Bind and validate a boolean metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateBooleanMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateBooleanMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         boolean[] booleanValues = new boolean[metadataValues.length];
@@ -1051,7 +1049,7 @@
             }
         }
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, booleanValues))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, booleanValues))
         {
             if (metadataDefinition.isMultiple())
             {
@@ -1071,18 +1069,18 @@
      * Bind and validate a binary metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateBinaryMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateBinaryMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, metadataValues))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, metadataValues))
         {
             if (metadataValues.length > 0 && !metadataValues[0].equals(""))
             {
@@ -1095,14 +1093,14 @@
      * Bind and validate a file metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateFileMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateFileMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         Map<String, Object> fileValues = Collections.emptyMap();
         if (metadataValues.length > 0)
@@ -1116,12 +1114,12 @@
         
         if (EXPLORER_FILE.equals(fileType))
         {
-            _bindAndValidateStringMetadata(allErrors, form, cType, metadataDefinition, metadataPath, values);
+            _bindAndValidateStringMetadata(allErrors, form, content, metadataDefinition, metadataPath, values);
             form.setField(metadataDefinition.getName() + "#type", new String[]{EXPLORER_FILE});
         }
         else if (METADATA_FILE.equals(fileType))
         {
-            _bindAndValidateBinaryMetadata(allErrors, form, cType, metadataDefinition, metadataPath, values);
+            _bindAndValidateBinaryMetadata(allErrors, form, content, metadataDefinition, metadataPath, values);
             form.setField(metadataDefinition.getName() + "#type", new String[]{METADATA_FILE});
         }
        
@@ -1131,18 +1129,18 @@
      * Bind and validate a rich text metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateRichText(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateRichText(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, metadataValues))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, metadataValues))
         {
             if (metadataValues.length > 0 && !metadataValues[0].equals(""))
             {
@@ -1155,18 +1153,18 @@
      * Bind and validate a reference metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateReferenceMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateReferenceMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, metadataValues))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, metadataValues))
         {
             if (metadataDefinition.isMultiple())
             {
@@ -1186,14 +1184,14 @@
      * Bind and validate a content reference metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type.
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
      * @throws AmetysRepositoryException if an error occurs.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _bindAndValidateContentReferenceMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateContentReferenceMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         List<Content> contentList = new ArrayList<Content>(metadataValues.length);
@@ -1217,7 +1215,7 @@
                     {
                         Content contentMeta = (Content) ao;
                         
-                        if (cTypeId != null && !validContentTypes.contains(contentMeta.getType()))
+                        if (cTypeId != null && CollectionUtils.intersection(validContentTypes, Arrays.asList(contentMeta.getTypes())).isEmpty())
                         {
                             Errors parseErrors = new Errors();
                             parseErrors.addError(new I18nizableText("The content " + contentMeta + " must be typed " + cTypeId + " or one of its sub-types."));
@@ -1246,7 +1244,7 @@
         
         Content[] contentValues = contentList.toArray(new Content[contentList.size()]);
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, contentValues))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, contentValues))
         {
             if (metadataDefinition.isMultiple() || contentValues.length > 0)
             {
@@ -1259,7 +1257,7 @@
      * Bind and validate a content metadata.
      * @param allErrors for storing validation errors.
      * @param form the form. 
-     * @param cType the content type.
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path from the content.
      * @param metadataValues the values.
@@ -1268,7 +1266,7 @@
      * @throws WorkflowException if an error occurs.
      */
     @SuppressWarnings("unchecked")
-    protected void _bindAndValidateSubContentMetadata(AllErrors allErrors, Form form, ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues, Map<String, Object> rawValues) throws AmetysRepositoryException, WorkflowException
+    protected void _bindAndValidateSubContentMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, String[] metadataValues, Map<String, Object> rawValues) throws AmetysRepositoryException, WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         String cTypeId = metadataDefinition.getContentType();
@@ -1286,7 +1284,7 @@
             contentValues.add(_jsonUtils.convertJsonToMap(metadataValues[i]));
         }
         
-        if (_validateMetadata(cType, metadataDefinition, metadataPath, allErrors, contentValues))
+        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, contentValues))
         {
             if (metadataDefinition.isMultiple() || !contentValues.isEmpty())
             {
@@ -1331,7 +1329,7 @@
     
     /**
      * Validate a metadata value.
-     * @param cType the content type
+     * @param content the content
      * @param metadataDefinition the metadata definition.
      * @param metadataPath the metadata path.
      * @param allErrors the errors.
@@ -1340,7 +1338,7 @@
      *         <code>false</code> otherwise.
      * @throws WorkflowException if an error occurs.
      */
-    protected boolean _validateMetadata(ContentType cType, MetadataDefinition metadataDefinition, String metadataPath, AllErrors allErrors, Object value) throws WorkflowException
+    protected boolean _validateMetadata(Content content, MetadataDefinition metadataDefinition, String metadataPath, AllErrors allErrors, Object value) throws WorkflowException
     {
         Validator validator = metadataDefinition.getValidator();
         
@@ -1376,7 +1374,6 @@
 
     /**
      * Synchronize a metadata with a composite metadata.
-     * @param contentType the content type.
      * @param content the content.
      * @param metadata the composite metadata to synchronize.
      * @param form the form.
@@ -1387,7 +1384,7 @@
      * @param metadataPath the metadata path.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _synchronizeMetadata(ContentType contentType, Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, String login, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath) throws WorkflowException
+    protected void _synchronizeMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, String login, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath) throws WorkflowException
     {
         MetadataType type = metadataDefinition.getType();
         
@@ -1433,7 +1430,7 @@
         }
         else if (type.equals(MetadataType.SUB_CONTENT))
         {
-            _synchronizeSubContentMetadata(contentType, content, metadata, form, allErrors, login, metadataDefinition, metadataPath);
+            _synchronizeSubContentMetadata(content, metadata, form, allErrors, login, metadataDefinition, metadataPath);
         }
         else if (type.equals(MetadataType.GEOCODE))
         {
@@ -1441,7 +1438,7 @@
         }
         else if (type.equals(MetadataType.COMPOSITE))
         {
-            _synchronizeCompositeMetadata(contentType, content, metadata, form, allErrors, login, metadataSetElement, metadataDefinition, metadataPath);
+            _synchronizeCompositeMetadata(content, metadata, form, allErrors, login, metadataSetElement, metadataDefinition, metadataPath);
         }
         else
         {
@@ -1499,7 +1496,6 @@
 
     /**
      * Synchronize a composite-typed metadata with a a composite metadata.
-     * @param contentType the content type.
      * @param content the content.
      * @param metadata the composite metadata to synchronize.
      * @param form the form.
@@ -1510,13 +1506,13 @@
      * @param metadataPath the metadata path.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _synchronizeCompositeMetadata(ContentType contentType, Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, String login, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath) throws WorkflowException
+    protected void _synchronizeCompositeMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, String login, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath) throws WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         
         if (metadataDefinition instanceof RepeaterDefinition)
         {
-            _synchronizeRepeater(contentType, content, metadata, form, allErrors, login, metadataSetElement, (RepeaterDefinition) metadataDefinition, metadataPath);
+            _synchronizeRepeater(content, metadata, form, allErrors, login, metadataSetElement, (RepeaterDefinition) metadataDefinition, metadataPath);
         }
         else 
         {
@@ -1525,7 +1521,7 @@
             if (compositeForm != null)
             {
                 ModifiableCompositeMetadata subMetadata = metadata.getCompositeMetadata(metadataName, true);
-                _synchronizeMetadataSetElement(contentType, content, subMetadata, compositeForm, allErrors, login, metadataSetElement, metadataDefinition, metadataPath + "/");
+                _synchronizeMetadataSetElement(content, subMetadata, compositeForm, allErrors, login, metadataSetElement, metadataDefinition, metadataPath + "/");
             }
             else
             {
@@ -1536,7 +1532,6 @@
     
     /**
      * Synchronize a repeater with a composite metadata.
-     * @param contentType the content type.
      * @param content the content.
      * @param metadata the composite metadata to synchronize.
      * @param form the form.
@@ -1547,7 +1542,7 @@
      * @param metadataPath the metadata path.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _synchronizeRepeater(ContentType contentType, Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, String login, AbstractMetadataSetElement metadataSetElement, RepeaterDefinition repeaterDefinition, String metadataPath) throws WorkflowException
+    protected void _synchronizeRepeater(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, String login, AbstractMetadataSetElement metadataSetElement, RepeaterDefinition repeaterDefinition, String metadataPath) throws WorkflowException
     {
         String metadataName = repeaterDefinition.getName();
         RepeaterField repeaterField = form.getRepeaterField(metadataName);
@@ -1625,7 +1620,7 @@
                     {
                         // Synchronization
                         ModifiableCompositeMetadata entryMetadata = repeaterMetadata.getCompositeMetadata(entryName);
-                        _synchronizeMetadataSetElement(contentType, content, entryMetadata, repeaterEntry, allErrors, login, metadataSetElement,
+                        _synchronizeMetadataSetElement(content, entryMetadata, repeaterEntry, allErrors, login, metadataSetElement,
                                                        repeaterDefinition, metadataPath + "/" + entryName + "/");
                     }
                 }
@@ -1641,7 +1636,7 @@
             {
                 // New entry
                 ModifiableCompositeMetadata entryMetadata = repeaterMetadata.getCompositeMetadata(String.valueOf(i), true);
-                _synchronizeMetadataSetElement(contentType, content, entryMetadata, repeaterEntry, allErrors, login, metadataSetElement,
+                _synchronizeMetadataSetElement(content, entryMetadata, repeaterEntry, allErrors, login, metadataSetElement,
                                                repeaterDefinition, metadataPath + "/" + i + "/");
             }
         }
@@ -2086,7 +2081,6 @@
     /**
      * Synchronize a sub-content metadata from a field.
      * @param content The Content.
-     * @param contentType The content type.
      * @param metadata the metadata.
      * @param form the form containing the field.
      * @param allErrors the errors.
@@ -2095,7 +2089,7 @@
      * @param metadataPath the current metadata path.
      * @throws WorkflowException if an error occurs.
      */
-    protected void _synchronizeSubContentMetadata(ContentType contentType, Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, String login, MetadataDefinition metadataDefinition, String metadataPath) throws WorkflowException
+    protected void _synchronizeSubContentMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, String login, MetadataDefinition metadataDefinition, String metadataPath) throws WorkflowException
     {
         String metadataName = metadataDefinition.getName();
         String[] values = form.getStringArray(metadataName);
Index: main/plugin-cms/src/org/ametys/cms/workflow/WorkflowTasksComponent.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/workflow/WorkflowTasksComponent.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/workflow/WorkflowTasksComponent.java	(working copy)
@@ -46,8 +46,8 @@
 import org.xml.sax.ContentHandler;
 import org.xml.sax.SAXException;
 
-import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.repository.Content;
 import org.ametys.cms.repository.DefaultContent;
 import org.ametys.cms.repository.WorkflowAwareContent;
@@ -58,9 +58,9 @@
 import org.ametys.plugins.repository.query.SortCriteria;
 import org.ametys.plugins.repository.query.expression.AndExpression;
 import org.ametys.plugins.repository.query.expression.Expression;
+import org.ametys.plugins.repository.query.expression.Expression.Operator;
 import org.ametys.plugins.repository.query.expression.OrExpression;
 import org.ametys.plugins.repository.query.expression.StringExpression;
-import org.ametys.plugins.repository.query.expression.Expression.Operator;
 import org.ametys.plugins.workflow.Workflow;
 import org.ametys.runtime.right.RightsManager;
 import org.ametys.runtime.right.RightsManager.RightResult;
@@ -109,6 +109,9 @@
     /** The content type extension point. */
     protected ContentTypeExtensionPoint _cTypeEP;
     
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
+    
     /** The {@link Workflow} */
     protected Workflow _workflow;
     
@@ -117,6 +120,7 @@
     
     /** Allow user querying ? */
     protected boolean _allowUserQuery;
+
     
     public void service(ServiceManager manager) throws ServiceException
     {
@@ -124,6 +128,7 @@
         _rightsManager = (RightsManager) manager.lookup(RightsManager.ROLE);
         _usersManager = (UsersManager) manager.lookup(UsersManager.ROLE);
         _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
         _workflow = (Workflow) manager.lookup(Workflow.ROLE);
     }
     
@@ -294,15 +299,13 @@
     protected void _saxContent(ContentHandler ch, Content content) throws SAXException, RepositoryException
     {
         User authorUser = _usersManager.getUser(content.getLastContributor());
-        String cType = content.getType();
-        ContentType contentType = _cTypeEP.getExtension(cType);
         
         AttributesImpl attrs = new AttributesImpl();
         attrs.addCDATAAttribute("id", content.getId());
         attrs.addCDATAAttribute("name", content.getName());
         attrs.addCDATAAttribute("title", content.getTitle());
         attrs.addCDATAAttribute("lastModified", _DATE_FORMAT.format(content.getLastModified()));
-        attrs.addCDATAAttribute("contentType", cType);
+        attrs.addCDATAAttribute("contentType", StringUtils.join(content.getTypes(), ','));
         
         if (authorUser != null)
         {
@@ -311,9 +314,9 @@
         
         _saxAdditionalAttributes (content, attrs);
         
-        attrs.addCDATAAttribute("smallIcon", contentType != null ? contentType.getSmallIcon() : "/plugins/cms/resources/img/contenttype/unknown-small.png");
-        attrs.addCDATAAttribute("mediumIcon", contentType != null ? contentType.getMediumIcon() : "/plugins/cms/resources/img/contenttype/unknown-medium.png");
-        attrs.addCDATAAttribute("largeIcon", contentType != null ? contentType.getLargeIcon() : "/plugins/cms/resources/img/contenttype/unknown-large.png");
+        attrs.addCDATAAttribute("smallIcon", _contentTypesHelper.getSmallIcon(content));
+        attrs.addCDATAAttribute("mediumIcon", _contentTypesHelper.getMediumIcon(content));
+        attrs.addCDATAAttribute("largeIcon", _contentTypesHelper.getLargeIcon(content));
         
         XMLUtils.startElement(ch, "content", attrs);
         if (content instanceof WorkflowAwareContent)
Index: main/plugin-cms/src/org/ametys/cms/workflow/ValidateMetadataCondition.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/workflow/ValidateMetadataCondition.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/workflow/ValidateMetadataCondition.java	(working copy)
@@ -57,19 +57,24 @@
     {
         Content content = getContent(transientVars);
         
-        ContentType contentType = _cTypeEP.getExtension(content.getType());
-        
-        Set<String> metadataNames = contentType.getMetadataNames();
-        for (String metadataName : metadataNames)
+        String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
+        for (String cTypeId : allContentTypes)
         {
-            // FIXME API Check modifiable
-            MetadataDefinition metadataDef = contentType.getMetadataDefinition(metadataName);
-            if (!validateMetadata((ModifiableCompositeMetadata) content.getMetadataHolder(), metadataDef, metadataName, new Errors()))
+            ContentType contentType = _cTypeEP.getExtension(cTypeId);
+            
+            Set<String> metadataNames = contentType.getMetadataNames();
+            for (String metadataName : metadataNames)
             {
-                return false;
+                // FIXME API Check modifiable
+                MetadataDefinition metadataDef = contentType.getMetadataDefinition(metadataName);
+                if (!validateMetadata((ModifiableCompositeMetadata) content.getMetadataHolder(), metadataDef, metadataName, new Errors()))
+                {
+                    return false;
+                }
             }
         }
         
+        
         return true;
     }
     
Index: main/plugin-cms/src/org/ametys/cms/workflow/CreateContentRightCondition.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/workflow/CreateContentRightCondition.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/workflow/CreateContentRightCondition.java	(working copy)
@@ -43,7 +43,7 @@
     @Override
     public boolean passesCondition(Map transientVars, Map args, PropertySet ps) throws WorkflowException
     {
-        String right = getContentType(transientVars).getRight();
+        String right = getRight(transientVars);
         args.put(__RIGHT_KEY, right);
         
         return right != null ? super.passesCondition(transientVars, args, ps) : true;
@@ -52,17 +52,25 @@
     @Override
     protected boolean _checkRights(Map transientVars, Map args, String login, String rightNeeded) throws WorkflowException
     {
-        return  _rightsManager.hasRight(login, rightNeeded, "/contributor") == RightResult.RIGHT_OK
-                || _rightsManager.hasRight(login, rightNeeded, "/contents") == RightResult.RIGHT_OK;
+        String[] rights = rightNeeded.split("|");
+        
+        for (String right : rights)
+        {
+            if (_rightsManager.hasRight(login, right, "/contributor") == RightResult.RIGHT_OK || _rightsManager.hasRight(login, right, "/contents") == RightResult.RIGHT_OK)
+            {
+                return true;
+            }
+        }
+        return false;
     }
     
     /**
-     * Retrieve the content type of the content to create.
-     * @param transientVars the parameters from the call.
-     * @return the content.
-     * @throws WorkflowException if the content is not found.
+     * Get the needed right for creation
+     * @param transientVars variables that will not be persisted.
+     * @return The required right
+     * @throws WorkflowException
      */
-    protected ContentType getContentType(Map transientVars) throws WorkflowException
+    protected String getRight (Map transientVars) throws WorkflowException
     {
         String contentTypeId = (String) transientVars.get(CreateContentFunction.CONTENT_TYPE_KEY);
         ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
@@ -72,8 +80,9 @@
             throw new WorkflowException("Unable to retrieve content the content type");
         }
         
-        return contentType;
+        return contentType.getRight();
     }
+   
     
     @Override
     public void dispose()
Index: main/plugin-cms/src/org/ametys/cms/workflow/CreateContentByCopyRightCondition.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/workflow/CreateContentByCopyRightCondition.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/workflow/CreateContentByCopyRightCondition.java	(working copy)
@@ -15,8 +15,12 @@
  */
 package org.ametys.cms.workflow;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
+import org.apache.commons.lang.StringUtils;
+
 import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.repository.Content;
 import org.ametys.cms.workflow.copy.CreateContentByCopyFunction;
@@ -40,7 +44,7 @@
     }
     
     @Override
-    protected ContentType getContentType(Map transientVars) throws WorkflowException
+    protected String getRight(Map transientVars) throws WorkflowException
     {
         Content baseContent = (Content) transientVars.get(CreateContentByCopyFunction.BASE_CONTENT_KEY);
         if (baseContent == null)
@@ -48,15 +52,21 @@
             throw new WorkflowException("Unable to retrieve the base content for the duplication.");
         }
         
-        String contentTypeId = baseContent.getType();
-        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
+        List<String> rights = new ArrayList<String>();
         
-        if (contentType == null)
+        String[] cTypes = baseContent.getTypes();
+        
+        for (String id : cTypes)
         {
-            throw new WorkflowException("Unable to retrieve content the content type");
+            ContentType contentType = _contentTypeExtensionPoint.getExtension(id);
+            String right = contentType.getRight();
+            if (StringUtils.isNotEmpty(right))
+            {
+                rights.add(right);
+            }
         }
         
-        return contentType;
+        return rights.size() > 0 ? StringUtils.join(rights.toArray(new String[rights.size()]), '&') : null;
     }
     
     @Override
Index: main/plugin-cms/src/org/ametys/cms/search/SearchModel.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/SearchModel.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/search/SearchModel.java	(working copy)
@@ -81,7 +81,7 @@
 public abstract class SearchModel extends AbstractLogEnabled implements Serviceable, Contextualizable, Configurable, Disposable
 {
     /** The valid system properties */
-    public static final String[] SYSTEM_PROPERTY_NAMES = {"creator", "contributor", "lastModified", "comments", "workflowStep", "contentLanguage", "contentType", "fulltext"};
+    public static final String[] SYSTEM_PROPERTY_NAMES = {"creator", "contributor", "lastModified", "comments", "workflowStep", "contentLanguage", "contentType", "mixin", "contentTypeOrMixin", "fulltext"};
     /** Prefix for id of metadata search criteria */
     public static final String SEARCH_CRITERIA_METADATA_PREFIX = "metadata-";
     /** Prefix for id of system property search criteria */
@@ -182,6 +182,10 @@
                     customEnumerator.addChild(cTypeConf);
                 }
                 
+                DefaultConfiguration excludeConf = new DefaultConfiguration("exclude-mixin");
+                excludeConf.setValue(true);
+                customEnumerator.addChild(excludeConf);
+                
                 enumConf.addChild(customEnumerator);
                 conf.addChild(enumConf);
                 
@@ -194,6 +198,56 @@
             
             try
             {
+                DefaultConfiguration conf = new DefaultConfiguration("criteria");
+                
+                DefaultConfiguration enumConf = new DefaultConfiguration("enumeration");
+                
+                DefaultConfiguration customEnumerator = new DefaultConfiguration("custom-enumerator");
+                customEnumerator.setAttribute("class", ContentTypeEnumerator.class.getName());
+                
+                Set<String> contentTypes = getContentTypes();
+                if (contentTypes.size() > 0)
+                {
+                    DefaultConfiguration cTypeConf = new DefaultConfiguration("strict-content-types");
+                    cTypeConf.setValue(StringUtils.join(contentTypes, ","));
+                    customEnumerator.addChild(cTypeConf);
+                }
+                
+                enumConf.addChild(customEnumerator);
+                conf.addChild(enumConf);
+                
+                _enumeratorManager.addComponent("cms", null, ContentTypeEnumerator.class.getName() + ".ContentTypeAndMixin", ContentTypeEnumerator.class, conf);
+            }
+            catch (Exception e)
+            {
+                throw new ConfigurationException("Unable to instantiate enumerator for class: " + ContentTypeEnumerator.class.getName(), e);
+            }
+            
+            try
+            {
+                DefaultConfiguration conf = new DefaultConfiguration("criteria");
+                
+                DefaultConfiguration enumConf = new DefaultConfiguration("enumeration");
+                
+                DefaultConfiguration customEnumerator = new DefaultConfiguration("custom-enumerator");
+                customEnumerator.setAttribute("class", ContentTypeEnumerator.class.getName());
+                
+                DefaultConfiguration includeConf = new DefaultConfiguration("include-mixin-only");
+                includeConf.setValue(true);
+                customEnumerator.addChild(includeConf);
+                
+                enumConf.addChild(customEnumerator);
+                conf.addChild(enumConf);
+                
+                _enumeratorManager.addComponent("cms", null, ContentTypeEnumerator.class.getName() + ".Mixin", ContentTypeEnumerator.class, conf);
+            }
+            catch (Exception e)
+            {
+                throw new ConfigurationException("Unable to instantiate enumerator for class: " + ContentTypeEnumerator.class.getName(), e);
+            }
+            
+            try
+            {
                 _enumeratorManager.addComponent("cms", null, LanguageEnumerator.class.getName(), LanguageEnumerator.class, new DefaultConfiguration("enumeration"));
             }
             catch (Exception e)
@@ -307,6 +361,12 @@
             expressions.add(cTypeExpr);
         }
         
+        Expression mixinExpr = createMixinTypeExpressions(values);
+        if (mixinExpr != null)
+        {
+            expressions.add(mixinExpr);
+        }
+        
         expressions.addAll(getCriteriaExpressions(values));
         
         if (excludeSubContents)
@@ -546,7 +606,35 @@
                 getLogger().error("Unable to lookup enumerator role: '" + ContentTypeEnumerator.class.getName() + "'", e);
             }
         }
-        if (propertyName.equals("fulltext"))
+        else if (propertyName.equals("mixin"))
+        {
+            systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_MIXIN"));
+            systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_MIXIN"));
+            systemSC.setType(MetadataType.STRING);
+            try
+            {
+                systemSC.setEnumerator(_enumeratorManager.lookup(ContentTypeEnumerator.class.getName() + ".Mixin"));
+            }
+            catch (Exception e)
+            {
+                getLogger().error("Unable to lookup enumerator role: '" + ContentTypeEnumerator.class.getName() + "'", e);
+            }
+        }
+        else if (propertyName.equals("contentTypeOrMixin"))
+        {
+            systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_CONTENT_TYPE"));
+            systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_CONTENT_TYPE"));
+            systemSC.setType(MetadataType.STRING);
+            try
+            {
+                systemSC.setEnumerator(_enumeratorManager.lookup(ContentTypeEnumerator.class.getName() + ".ContentTypeAndMixin"));
+            }
+            catch (Exception e)
+            {
+                getLogger().error("Unable to lookup enumerator role: '" + ContentTypeEnumerator.class.getName() + "'", e);
+            }
+        }
+        else if (propertyName.equals("fulltext"))
         {
             systemSC.setLabel(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_FULLTEXT"));
             systemSC.setDescription(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_FULLTEXT"));
@@ -624,6 +712,20 @@
             column.setWidth(150);
             column.setMapping("content-type");
         }
+        else if (propertyName.equals("mixin"))
+        {
+            column.setTitle(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_MIXIN"));
+            column.setType(MetadataType.STRING);
+            column.setWidth(150);
+            column.setMapping("mixin");
+        }
+        else if (propertyName.equals("contentTypeOrMixin"))
+        {
+            column.setTitle(new I18nizableText("plugin.cms", "UITOOL_SEARCH_CONTENT_CONTENT_TYPE"));
+            column.setType(MetadataType.STRING);
+            column.setWidth(150);
+            column.setMapping("content-type-mixin");
+        }
         return column;
     }
     
@@ -659,13 +761,50 @@
         
         Object cTypeParam = values.get(SEARCH_CRITERIA_SYSTEM_PREFIX + "contentType-eq");
         
-        if (cTypeParam instanceof String && StringUtils.isNotEmpty((String) cTypeParam))
+        if (cTypeParam == null)
+        {
+            return createContentTypeOrMixinExpressions(values);
+        }
+        else
+        {
+            if (cTypeParam instanceof String && StringUtils.isNotEmpty((String) cTypeParam))
+            {
+                return _cTypeEP.createHierarchicalCTExpression((String) cTypeParam);
+            }
+            else if (cTypeParam != null && cTypeParam instanceof List<?>)
+            {
+                return _cTypeEP.createHierarchicalCTExpression(((List<String>) cTypeParam).toArray(new String[cTypes.size()]));
+            }
+            else if (cTypes != null && cTypes.size() > 0)
+            {
+                return _cTypeEP.createHierarchicalCTExpression(cTypes.toArray(new String[cTypes.size()]));
+            }
+            else
+            {
+                return null;
+            }
+        }
+    }
+    
+    /**
+     * Create expression on content type
+     * @param values The submitted values
+     * @return The content type expression or null if no
+     */
+    @SuppressWarnings("unchecked")
+    protected Expression createContentTypeOrMixinExpressions(Map<String, Object> values)
+    {
+        Set<String> cTypes = getContentTypes();
+        
+        Object cTypeOrMixinParam = values.get(SEARCH_CRITERIA_SYSTEM_PREFIX + "contentTypeOrMixin-eq");
+        
+        if (cTypeOrMixinParam instanceof String && StringUtils.isNotEmpty((String) cTypeOrMixinParam))
         {
-            return _cTypeEP.createHierarchicalCTExpression((String) cTypeParam);
+            return _cTypeEP.createHierarchicalContentTypeOrMixinExpression((String) cTypeOrMixinParam);
         }
-        else if (cTypeParam != null && cTypeParam instanceof List<?>)
+        else if (cTypeOrMixinParam != null && cTypeOrMixinParam instanceof List<?>)
         {
-            return _cTypeEP.createHierarchicalCTExpression(((List<String>) cTypeParam).toArray(new String[cTypes.size()]));
+            return _cTypeEP.createHierarchicalContentTypeOrMixinExpression(((List<String>) cTypeOrMixinParam).toArray(new String[cTypes.size()]));
         }
         else if (cTypes != null && cTypes.size() > 0)
         {
@@ -678,6 +817,32 @@
     }
     
     /**
+     * Create expression on mixin
+     * @param values The submitted values
+     * @return The content type expression or null if no
+     */
+    @SuppressWarnings("unchecked")
+    protected Expression createMixinTypeExpressions(Map<String, Object> values)
+    {
+        Set<String> cTypes = getContentTypes();
+        
+        Object mixinParam = values.get(SEARCH_CRITERIA_SYSTEM_PREFIX + "mixin-eq");
+        
+        if (mixinParam instanceof String && StringUtils.isNotEmpty((String) mixinParam))
+        {
+            return _cTypeEP.createHierarchicalMixinExpression((String) mixinParam);
+        }
+        else if (mixinParam != null && mixinParam instanceof List<?>)
+        {
+            return _cTypeEP.createHierarchicalCTExpression(((List<String>) mixinParam).toArray(new String[cTypes.size()]));
+        }
+        else
+        {
+            return null;
+        }
+    }
+    
+    /**
      * Returns the expression to execute
      * @param values The submitted values
      * @return The expression to execute
Index: main/plugin-cms/src/org/ametys/cms/search/generators/SearchGenerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/search/generators/SearchGenerator.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/search/generators/SearchGenerator.java	(working copy)
@@ -16,6 +16,7 @@
 package org.ametys.cms.search.generators;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -37,6 +38,7 @@
 import org.ametys.cms.contenttype.AbstractMetadataSetElement;
 import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.contenttype.MetadataDefinitionReference;
 import org.ametys.cms.contenttype.MetadataManager;
 import org.ametys.cms.contenttype.MetadataSet;
@@ -54,6 +56,7 @@
 import org.ametys.plugins.workflow.Workflow;
 import org.ametys.runtime.user.User;
 import org.ametys.runtime.user.UsersManager;
+import org.ametys.runtime.util.I18nUtils;
 import org.ametys.runtime.util.I18nizableText;
 import org.ametys.runtime.util.parameter.ParameterHelper;
 
@@ -84,11 +87,14 @@
     protected Workflow _workflow;
     /** The user manager */
     protected UsersManager _usersManager;
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
+    /** I18N utils */
+    protected I18nUtils _i18nUtils;
     
     /** The cache for users' name */
     protected Map<String, String> _userCache;
     
-    
     @Override
     public void service(ServiceManager smanager) throws ServiceException
     {
@@ -100,6 +106,8 @@
         _metadataManager = (MetadataManager) manager.lookup(MetadataManager.ROLE);
         _workflow = (Workflow) manager.lookup(Workflow.ROLE);
         _usersManager = (UsersManager) manager.lookup(UsersManager.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
+        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
     }
     
     @Override
@@ -189,11 +197,9 @@
         
         AttributesImpl attrs = new AttributesImpl();
         
-        ContentType contentType =  _contentTypeExtensionPoint.getExtension(content.getType());
-        
-        attrs.addCDATAAttribute("smallIcon", contentType != null ? contentType.getSmallIcon() : "/plugins/cms/resources/img/contenttype/unknown-small.png");
-        attrs.addCDATAAttribute("mediumIcon", contentType != null ? contentType.getMediumIcon() : "/plugins/cms/resources/img/contenttype/unknown-medium.png");
-        attrs.addCDATAAttribute("largeIcon", contentType != null ? contentType.getLargeIcon() : "/plugins/cms/resources/img/contenttype/unknown-large.png");
+        attrs.addCDATAAttribute("smallIcon", _contentTypesHelper.getSmallIcon(content));
+        attrs.addCDATAAttribute("mediumIcon", _contentTypesHelper.getMediumIcon(content));
+        attrs.addCDATAAttribute("largeIcon", _contentTypesHelper.getLargeIcon(content));
         attrs.addCDATAAttribute("id", content.getId());
         attrs.addCDATAAttribute("name", content.getName());
         attrs.addCDATAAttribute("title", content.getTitle());
@@ -202,11 +208,11 @@
         XMLUtils.startElement(contentHandler, "content", attrs);
 
         // System properties
-        saxSystemProperties(columns, content, contentType);
+        saxSystemProperties(columns, content);
         
         // Metadata
         XMLUtils.startElement(contentHandler, "metadata");
-        MetadataSet metadataSet = getMetadataToSAX(model, model.getResultColumns(), contentType);
+        MetadataSet metadataSet = getMetadataToSAX(model, model.getResultColumns(), content);
         _metadataManager.saxReadableMetadata(contentHandler, content, metadataSet);
         XMLUtils.endElement(contentHandler, "metadata");
         
@@ -215,13 +221,12 @@
     
     /**
      * SAX the system properties
-     * @param columns The
-     * @param content
-     * @param cType
+     * @param columns The search columns
+     * @param content The content
      * @throws SAXException
      * @throws RepositoryException
      */
-    protected void saxSystemProperties (List<SearchColumn> columns, Content content, ContentType cType) throws SAXException, RepositoryException
+    protected void saxSystemProperties (List<SearchColumn> columns, Content content) throws SAXException, RepositoryException
     {
         for (SearchColumn searchColumn : columns)
         {
@@ -231,9 +236,45 @@
             {
                 saxContentCurrentState((WorkflowAwareContent) content);
             }
-            else if ("contentType".equals(id) && cType != null)
+            else if ("contentType".equals(id))
             {
-                cType.getLabel().toSAX(contentHandler, "content-type");
+                List<String> labels = new ArrayList<String>();
+                
+                for (String cTypeId : content.getTypes())
+                {
+                    ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
+                    labels.add(_i18nUtils.translate(cType.getLabel(), content.getLanguage()));
+                }
+                XMLUtils.createElement(contentHandler, "content-type", StringUtils.join(labels, ", "));
+            }
+            else if ("mixin".equals(id))
+            {
+                List<String> labels = new ArrayList<String>();
+                
+                for (String cTypeId : content.getMixinTypes())
+                {
+                    ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
+                    labels.add(_i18nUtils.translate(cType.getLabel(), content.getLanguage()));
+                }
+                XMLUtils.createElement(contentHandler, "mixin", StringUtils.join(labels, ", "));
+            }
+            else if ("contentTypeOrMixin".equals(id))
+            {
+                List<String> labels = new ArrayList<String>();
+                
+                for (String cTypeId : content.getTypes())
+                {
+                    ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
+                    labels.add(_i18nUtils.translate(cType.getLabel(), content.getLanguage()));
+                }
+                
+                for (String cTypeId : content.getMixinTypes())
+                {
+                    ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
+                    labels.add(_i18nUtils.translate(cType.getLabel(), content.getLanguage()));
+                }
+                
+                XMLUtils.createElement(contentHandler, "content-type-mixin", StringUtils.join(labels, ", "));
             }
             else if ("creator".equals(id))
             {
@@ -275,10 +316,10 @@
      * Extracts the metadata to SAX
      * @param model The search model
      * @param columns The result columns
-     * @param contentType The content type
+     * @param content The content
      * @return the metadata to SAX in a {@link MetadataSet}
      */
-    protected MetadataSet getMetadataToSAX (SearchModel model, List<SearchColumn> columns, ContentType contentType)
+    protected MetadataSet getMetadataToSAX (SearchModel model, List<SearchColumn> columns, Content content)
     {
         MetadataSet metadataSet = new MetadataSet();
         
@@ -291,7 +332,7 @@
             {
                 String[] path = id.split("\\/|\\.");
                 
-                if (contentType.getMetadataDefinition(path[0]) != null)
+                if (_contentTypesHelper.getMetadataDefinition(path[0], content.getTypes(), content.getMixinTypes()) != null)
                 {
                     for (String metadataName : path)
                     {
Index: main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypeDescriptor.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypeDescriptor.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypeDescriptor.java	(revision 0)
@@ -0,0 +1,114 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.contenttype;
+
+import java.util.Set;
+
+import org.ametys.runtime.util.I18nizableText;
+
+/**
+ * This class represents a descriptor for content type
+ */
+public interface ContentTypeDescriptor
+{
+    /**
+     * Retrieves the id of the content type.
+     * @return the id.
+     */
+    String getId();
+    
+    /**
+     * Retrieves the name of the plugin declaring this content type.
+     * @return the name of the plugin.
+     */
+    String getPluginName();
+    
+    /**
+     * Retrieves the label of the content type.
+     * @return the label.
+     */
+    I18nizableText getLabel();
+    
+    /**
+     * Retrieves the description of the content type.
+     * @return the description.
+     */
+    I18nizableText getDescription();
+    
+    /**
+     * Retrieves the default title of the content type.
+     * @return the default title.
+     */
+    I18nizableText getDefaultTitle();
+    
+    /**
+     * Retrieves the category of the content type.
+     * @return the category.
+     */
+    I18nizableText getCategory();
+
+    /**
+     * Retrieves the super type's ids.
+     * @return the super type's ids, or empty if this content type doesn't extend a specific content type.
+     */
+    String[] getSupertypeIds();
+    
+    /**
+     * Retrieves the URL of the icon without the context path.
+     * @return the icon URL for the small image 16x16.
+     */
+    String getSmallIcon();
+
+    /**
+     * Retrieves the URL of the icon without the context path.
+     * @return the icon URL for the medium sized image 32x32.
+     */
+    String getMediumIcon();
+
+    /**
+     * Retrieves the URL of the icon without the context path.
+     * @return the icon URL for the large image 48x48.
+     */
+    String getLargeIcon();
+    
+    /**
+     * Returns all names of "view" metadataSets.
+     * @param includeInternal if the result should include internal metadataSets.
+     * @return all names of "view" metadataSets.
+     */
+    Set<String> getViewMetadataSetNames(boolean includeInternal);
+    
+    /**
+     * Returns all names of "edition" metadataSets.
+     * @param includeInternal if the result should include internal metadataSets.
+     * @return all names of "edition" metadataSets.
+     */
+    Set<String> getEditionMetadataSetNames(boolean includeInternal);
+    
+    /**
+     * Retrieves the metadata set name for view.
+     * @param metadataSetName the metadata set name.
+     * @return the metadata definition.
+     */
+    MetadataSet getMetadataSetForView(String metadataSetName);
+    
+    /**
+     * Retrieves the metadata set name for edition.
+     * @param metadataSetName the metadata set name.
+     * @return the metadata set.
+     */
+    MetadataSet getMetadataSetForEdition(String metadataSetName);
+}
Index: main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypeEnumerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypeEnumerator.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypeEnumerator.java	(working copy)
@@ -53,9 +53,13 @@
     protected boolean _excludeSimple;
     /** True to exclude abstract content types */
     protected boolean _excludeAbstract;
-    /** The parent content type */
+    /** True to exclude mixin content types */
+    protected boolean _excludeMixin;
+    /** True to include only mixin content types */
+    protected boolean _mixinOnly;
+    /** The parent content types */
     protected String[] _contentTypes;
-    /** The parent content type */
+    /** The strict content types */
     protected String[] _strictContentTypes;
     
     @Override
@@ -95,6 +99,20 @@
         }
         _excludeAbstract = abstractConf != null ? Boolean.valueOf(abstractConf.getValue("false")) : false;
         
+        Configuration mixinConf = configuration.getChild("exclude-mixin", false);
+        if (mixinConf == null)
+        {
+            mixinConf = configuration.getChild("enumeration").getChild("custom-enumerator").getChild("exclude-mixin", false);
+        }
+        _excludeMixin = mixinConf != null ? Boolean.valueOf(mixinConf.getValue("false")) : false;
+        
+        Configuration mixinOnlyConf = configuration.getChild("mixin-only", false);
+        if (mixinOnlyConf == null)
+        {
+            mixinOnlyConf = configuration.getChild("enumeration").getChild("custom-enumerator").getChild("include-mixin-only", false);
+        }
+        _mixinOnly = mixinOnlyConf != null ? Boolean.valueOf(mixinOnlyConf.getValue("false")) : false;
+        
         String strictCTypes = configuration.getChild("strict-content-types").getValue(null);
         if (strictCTypes == null)
         {
@@ -224,6 +242,16 @@
             return false;
         }
         
+        if (_excludeMixin && cType.isMixin())
+        {
+            return false;
+        }
+        
+        if (_mixinOnly && !cType.isMixin())
+        {
+            return false;
+        }
+        
         if (_excludeAbstract && cType.isAbstract())
         {
             return false;
Index: main/plugin-cms/src/org/ametys/cms/contenttype/DefaultContentIndexer.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/DefaultContentIndexer.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/DefaultContentIndexer.java	(working copy)
@@ -42,7 +42,8 @@
     
     /** The associated content type. */
     protected ContentTypeExtensionPoint _contentTypeEP;
-    
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
     /** The metadata indexer. */
     protected MetadataIndexer _metadataIndexer;
     
@@ -51,6 +52,7 @@
     {
         _metadataIndexer = (MetadataIndexer) manager.lookup(MetadataIndexer.ROLE);
         _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
     }
     
     @Override
@@ -60,7 +62,10 @@
         document.add(new Field(FieldNames.CONTENT_NAMES, content.getName(), Store.YES, Index.NOT_ANALYZED));
         
         // Index the content type.
-        document.add(new Field(FieldNames.CONTENT_TYPES, content.getType(), Store.YES, Index.NOT_ANALYZED));
+        for (String cTypeId : content.getTypes())
+        {
+            document.add(new Field(FieldNames.CONTENT_TYPES, cTypeId, Store.YES, Index.NOT_ANALYZED));
+        }
         
         // Index the content metadata.
         indexContentMetadata(content, document);
@@ -75,13 +80,12 @@
     protected void indexContentMetadata(Content content, Document document) throws Exception
     {
         CompositeMetadata metadata = content.getMetadataHolder();
-        ContentType contentType = _contentTypeEP.getExtension(content.getType());
         
-        MetadataSet metadataSet = contentType.getMetadataSetForView("index");
+        MetadataSet metadataSet = _contentTypesHelper.getMetadataSetForView("index", content.getTypes(), content.getMixinTypes());
         
         if (metadataSet != null)
         {
-            _metadataIndexer.indexMetadataSet(metadataSet, metadata, null, contentType, document);
+            _metadataIndexer.indexMetadataSet(metadataSet, metadata, null, content.getTypes(), content.getMixinTypes(), document);
         }
         else
         {
Index: main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypesHelper.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypesHelper.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypesHelper.java	(working copy)
@@ -16,13 +16,16 @@
 package org.ametys.cms.contenttype;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 
 import org.apache.avalon.framework.component.Component;
 import org.apache.avalon.framework.configuration.ConfigurationException;
@@ -32,6 +35,11 @@
 import org.apache.avalon.framework.service.Serviceable;
 import org.apache.avalon.framework.thread.ThreadSafe;
 import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang.ArrayUtils;
+
+import org.ametys.cms.repository.Content;
+import org.ametys.plugins.repository.AmetysRepositoryException;
+import org.ametys.runtime.util.I18nizableText;
 
 /**
  * Helper for manipulating {@link ContentType}s
@@ -43,11 +51,55 @@
     public static final String ROLE = ContentTypesHelper.class.getName();
     
     private ContentTypeExtensionPoint _cTypeEP;
+
+    private DynamicContentTypeDescriptorExtentionPoint _dynamicContentTypeManager;
     
     @Override
     public void service(ServiceManager smanager) throws ServiceException
     {
         _cTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
+        _dynamicContentTypeManager = (DynamicContentTypeDescriptorExtentionPoint) smanager.lookup(DynamicContentTypeDescriptorExtentionPoint.ROLE);
+    }
+    
+    /**
+     * Determines if a content is a instance of given content type id
+     * @param content The content
+     * @param cTypeId The id of content type or mixin
+     * @return <code>true</code> if the content is an instance of content type
+     */
+    public boolean isInstanceOf (Content content, String cTypeId)
+    {
+        String[] types = content.getTypes();
+        if (ArrayUtils.contains(types, cTypeId))
+        {
+            return true;
+        }
+        
+        String[] mixins = content.getMixinTypes();
+        if (ArrayUtils.contains(mixins, cTypeId))
+        {
+            return true;
+        }
+        
+        return _containsContentType((String[]) ArrayUtils.addAll(types, mixins), cTypeId);
+    }
+    
+    private boolean _containsContentType (String[] cTypesId, String cTypeId)
+    {
+        for (String id : cTypesId)
+        {
+            ContentType cType = _cTypeEP.getExtension(id);
+            
+            if (ArrayUtils.contains(cType.getSupertypeIds(), cTypeId))
+            {
+                return true;
+            }
+            else if (_containsContentType(cType.getSupertypeIds(), cTypeId))
+            {
+                return true;
+            }
+        }
+        return false;
     }
     
     /**
@@ -120,36 +172,38 @@
     }
     
     /**
-     * Get the metadata set names for view resulting of the concatenation of metadata sets of given content types.
-     * @param metadataSetName the name of metadata set to retrieve
+     * Retrieves the metadata names resulting of the intersection of metadata names of given content types and mixins
      * @param cTypes The id of content types
-     * @return The list of metadata set names.
+     * @param mixins The id of mixins
+     * @return the metadata names.
      */
-    public List<String> getMetadataSetNamesForView (String metadataSetName, String[] cTypes)
+    public Set<String> getMetadataNames(String[] cTypes, String[] mixins)
     {
-        // TODO
-        return new ArrayList<String>();
-    }
-    
-    /**
-     * Get the metadata set names for edition resulting of the concatenation of metadata sets of given content types.
-     * @param metadataSetName the name of metadata set to retrieve
-     * @param cTypes The id of content types
-     * @return The list of metadata set names.
-     */
-    public List<String> getMetadataSetNamesForEdition (String metadataSetName, String[] cTypes)
-    {
-        // TODO
-        return new ArrayList<String>();
+        Set<String> metadataNames = new HashSet<String>();
+        
+        for (String id : cTypes)
+        {
+            ContentType cType = _cTypeEP.getExtension(id);
+            metadataNames.addAll(cType.getMetadataNames());
+        }
+        
+        for (String id : mixins)
+        {
+            ContentType cType = _cTypeEP.getExtension(id);
+            metadataNames.addAll(cType.getMetadataNames());
+        }
+        
+        return metadataNames;
     }
     
     /**
-     * Get all metadata sets for view resulting of the concatenation of metadata sets of given content types.
+     * Get all metadata sets for view resulting of the concatenation of metadata sets of given content types and mixins.
      * @param cTypes The id of content types
+     * @param mixins The id of mixins
      * @param includeInternal <code>true</code> True to include internal metadata sets
      * @return The metadata sets
      */
-    public Map<String, MetadataSet> getMetadataSetsForView (String[] cTypes, boolean includeInternal)
+    public Map<String, MetadataSet> getMetadataSetsForView (String[] cTypes, String[] mixins, boolean includeInternal)
     {
         Map<String, MetadataSet> metadataSets = new HashMap<String, MetadataSet>();
         
@@ -168,19 +222,20 @@
         
         for (String viewMetadataSetName : viewMetadataSetNames)
         {
-            metadataSets.put(viewMetadataSetName, getMetadataSetForView(viewMetadataSetName, cTypes));
+            metadataSets.put(viewMetadataSetName, getMetadataSetForView(viewMetadataSetName, cTypes, mixins));
         }
         
         return metadataSets;
     }
     
     /**
-     * Get all metadata sets for edition resulting of the concatenation of metadata sets of given content types.
+     * Get all metadata sets for edition resulting of the concatenation of metadata sets of given content types and mixins.
      * @param cTypes The id of content types
+     * @param mixins The id of mixins
      * @param includeInternal <code>true</code> True to include internal metadata sets
      * @return The metadata sets
      */
-    public Map<String, MetadataSet> getMetadataSetsForEdition (String[] cTypes, boolean includeInternal)
+    public Map<String, MetadataSet> getMetadataSetsForEdition (String[] cTypes, String[] mixins, boolean includeInternal)
     {
         Map<String, MetadataSet> metadataSets = new HashMap<String, MetadataSet>();
         
@@ -199,19 +254,20 @@
         
         for (String editionMetadataSetName : editionMetadataSetNames)
         {
-            metadataSets.put(editionMetadataSetName, getMetadataSetForEdition(editionMetadataSetName, cTypes));
+            metadataSets.put(editionMetadataSetName, getMetadataSetForEdition(editionMetadataSetName, cTypes, mixins));
         }
         
         return metadataSets;
     }
     
     /**
-     * Get the metadata set for view resulting of the concatenation of metadata sets of given content types.
+     * Get the metadata set for view resulting of the concatenation of metadata sets of given content types and mixins.
      * @param metadataSetName the name of metadata set to retrieve
      * @param cTypes The id of content types
+     * @param mixins The id of mixins
      * @return The list of metadata set names.
      */
-    public MetadataSet getMetadataSetForView (String metadataSetName, String[] cTypes)
+    public MetadataSet getMetadataSetForView (String metadataSetName, String[] cTypes, String[] mixins)
     {
         MetadataSet joinMetadataSet = new MetadataSet();
         
@@ -234,16 +290,25 @@
             }
         }
         
+        for (String id : mixins)
+        {
+            ContentType mixin = _cTypeEP.getExtension(id);
+            
+            MetadataSet metadataSet = mixin.getMetadataSetForView(metadataSetName);
+            copyMetadataSetElementsIfNotExist (metadataSet, joinMetadataSet);
+        }
+        
         return joinMetadataSet;
     }
     
     /**
-     * Get the metadata set for edition resulting of the concatenation of metadata sets of given content types.
+     * Get the metadata set for edition resulting of the concatenation of metadata sets of given content types and mixins.
      * @param metadataSetName the name of metadata set to retrieve
      * @param cTypes The id of content types
+     * @param mixins The id of mixins
      * @return The metadata set
      */
-    public MetadataSet getMetadataSetForEdition (String metadataSetName, String[] cTypes)
+    public MetadataSet getMetadataSetForEdition (String metadataSetName, String[] cTypes, String[] mixins)
     {
         MetadataSet joinMetadataSet = new MetadataSet();
         
@@ -266,6 +331,14 @@
             }
         }
         
+        for (String id : mixins)
+        {
+            ContentType mixin = _cTypeEP.getExtension(id);
+            
+            MetadataSet metadataSet = mixin.getMetadataSetForView(metadataSetName);
+            copyMetadataSetElementsIfNotExist (metadataSet, joinMetadataSet);
+        }
+        
         return joinMetadataSet;
         
     }
@@ -302,7 +375,10 @@
                 
                 _copyMetadataSetElementsIfNotExist(elmt, fieldset, dest);
                 
-                dest.addElement(fieldset);
+                if (fieldset.getElements().size() > 0)
+                {
+                    dest.addElement(fieldset);
+                }
             }
         }
     }
@@ -311,9 +387,10 @@
      * Retrieves the definition of a given metadata.
      * @param metadataName the metadata name.
      * @param cTypes The id of content types
+     * @param mixins The id of mixins
      * @return the metadata definition or <code>null</code> if not found
      */
-    public MetadataDefinition getMetadataDefinition (String metadataName, String[] cTypes)
+    public MetadataDefinition getMetadataDefinition (String metadataName, String[] cTypes, String[] mixins)
     {
         for (String id : cTypes)
         {
@@ -324,10 +401,45 @@
                 return cType.getMetadataDefinition(metadataName);
             }
         }
+        
+        for (String id : mixins)
+        {
+            ContentType cType = _cTypeEP.getExtension(id);
+            
+            if (cType.hasMetadataDefinition(metadataName))
+            {
+                return cType.getMetadataDefinition(metadataName);
+            }
+        }
+        
         return null;
     }
     
     /**
+     * Determines if the given content type can be added to content
+     * @param content The content
+     * @param cTypeId The id of content type
+     * @return <code>true</code> if the content type is compatible with content
+     */
+    public boolean isCompatibleContentType (Content content, String cTypeId)
+    {
+        String[] currentContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
+        
+        ArrayList<String> cTypes = new ArrayList<String>(Arrays.asList(currentContentTypes));
+        cTypes.add(cTypeId);
+        
+        try
+        {
+            getMetadataDefinitions (cTypes.toArray(new String[cTypes.size()]));
+            return true;
+        }
+        catch (ConfigurationException e)
+        {
+            return false;
+        }
+    }
+    
+    /**
      * Retrieves all definitions of a metadata resulting of the concatenation of metadata of given content types.
      * @param cTypes The id of content types
      * @return the metadata definitions
@@ -362,4 +474,251 @@
         return metadata;
     }
     
+    /**
+     * Determine whether a metadata can be read at this time.
+     * @param metadataPath the path to the given metadata.
+     * @param content The content where metadata is to be read on.
+     * @return <code>true</code> if the current user is allowed to read the metadata of this content.
+     * @throws AmetysRepositoryException if an error occurs while accessing the content.
+     */
+    public boolean canRead (Content content, String metadataPath)
+    {
+        for (String id : content.getTypes())
+        {
+            ContentType cType = _cTypeEP.getExtension(id);
+            
+            if (cType.getMetadataDefinitionByPath(metadataPath) != null)
+            {
+                return cType.canRead(content, metadataPath);
+            }
+        }
+        
+        for (String id : content.getMixinTypes())
+        {
+            ContentType cType = _cTypeEP.getExtension(id);
+            
+            if (cType.getMetadataDefinitionByPath(metadataPath) != null)
+            {
+                return cType.canRead(content, metadataPath);
+            }
+        }
+        
+        return false;
+    }
+    
+    /**
+     * Determine whether a metadata can be read at this time.
+     * @param metadataPath the path to the given metadata.
+     * @param content The content where metadata is to be read on.
+     * @return <code>true</code> if the current user is allowed to read the metadata of this content.
+     * @throws AmetysRepositoryException if an error occurs while accessing the content.
+     */
+    public boolean canWrite (Content content, String metadataPath)
+    {
+        for (String id : content.getTypes())
+        {
+            ContentType cType = _cTypeEP.getExtension(id);
+            
+            if (cType.getMetadataDefinitionByPath(metadataPath) != null)
+            {
+                return cType.canWrite(content, metadataPath);
+            }
+        }
+        
+        for (String id : content.getMixinTypes())
+        {
+            ContentType cType = _cTypeEP.getExtension(id);
+            
+            if (cType.getMetadataDefinitionByPath(metadataPath) != null)
+            {
+                return cType.canWrite(content, metadataPath);
+            }
+        }
+        
+        return false;
+    }
+    
+    /**
+     * Get the id of content type to use for rendering
+     * @param content The content
+     * @return the id of dynamic or standard content type
+     */
+    public String getContentTypeIdForRendering (Content content)
+    {
+        DynamicContentTypeDescriptor dynamicContentType = _dynamicContentTypeManager.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
+        if (dynamicContentType != null)
+        {
+            return dynamicContentType.getId();
+        }
+        
+        return getFirstContentType(content).getId();
+    }
+    
+    /**
+     * Get the plugin name of content type to use for rendering
+     * @param content The content
+     * @return the plugin name of dynamic or standard content type
+     */
+    public String getContentTypePluginForRendering (Content content)
+    {
+        DynamicContentTypeDescriptor dynamicContentType = _dynamicContentTypeManager.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
+        if (dynamicContentType != null)
+        {
+            return dynamicContentType.getPluginName();
+        }
+        
+        return getFirstContentType(content).getPluginName();
+    }
+    
+    /**
+     * Retrieves the label of the content type.
+     * @param content The content
+     * @return the label.
+     */
+    public I18nizableText getContentTypeLabel (Content content)
+    {
+        DynamicContentTypeDescriptor dynamicContentType = _dynamicContentTypeManager.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
+        if (dynamicContentType != null)
+        {
+            return dynamicContentType.getLabel();
+        }
+        
+        return getFirstContentType(content).getLabel();
+    }
+    
+    /**
+     * Retrieves the description of the content type.
+     * @param content The content
+     * @return the label.
+     */
+    public I18nizableText getContentTypeDescription (Content content)
+    {
+        DynamicContentTypeDescriptor dynamicContentType = _dynamicContentTypeManager.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
+        if (dynamicContentType != null)
+        {
+            return dynamicContentType.getDescription();
+        }
+        
+        return getFirstContentType(content).getDescription();
+    }
+    
+    /**
+     * Retrieves the default title of the content type.
+     * @param content The content
+     * @return the label.
+     */
+    public I18nizableText getContentTypeDefaultTitle (Content content)
+    {
+        DynamicContentTypeDescriptor dynamicContentType = _dynamicContentTypeManager.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
+        if (dynamicContentType != null)
+        {
+            return dynamicContentType.getDefaultTitle();
+        }
+        
+        return getFirstContentType(content).getDefaultTitle();
+    }
+    
+    /**
+     * Retrieves the category of the content type.
+     * @param content The content
+     * @return the label.
+     */
+    public I18nizableText getContentTypeCategory (Content content)
+    {
+        DynamicContentTypeDescriptor dynamicContentType = _dynamicContentTypeManager.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
+        if (dynamicContentType != null)
+        {
+            return dynamicContentType.getCategory();
+        }
+        
+        return getFirstContentType(content).getCategory();
+    }
+    
+    /**
+     * Retrieves the URL of the icon without the context path.
+     * @param content The content
+     * @return the icon URL for the small image 16x16.
+     */
+    public String getSmallIcon (Content content)
+    {
+        DynamicContentTypeDescriptor dynamicContentType = _dynamicContentTypeManager.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
+        if (dynamicContentType != null)
+        {
+            return dynamicContentType.getSmallIcon();
+        }
+        
+        return getFirstContentType(content).getSmallIcon();
+    }
+    
+    /**
+     * Retrieves the URL of the icon without the context path.
+     * @param content The content
+     * @return the icon URL for the medium image 32x32.
+     */
+    public String getMediumIcon (Content content)
+    {
+        DynamicContentTypeDescriptor dynamicContentType = _dynamicContentTypeManager.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
+        if (dynamicContentType != null)
+        {
+            return dynamicContentType.getMediumIcon();
+        }
+        
+        return getFirstContentType(content).getMediumIcon();
+    }
+    
+    /**
+     * Retrieves the URL of the icon without the context path.
+     * @param content The content
+     * @return the icon URL for the large image 48x48.
+     */
+    public String getLargeIcon (Content content)
+    {
+        DynamicContentTypeDescriptor dynamicContentType = _dynamicContentTypeManager.getMatchingDescriptor(content.getTypes(), content.getMixinTypes());
+        if (dynamicContentType != null)
+        {
+            return dynamicContentType.getLargeIcon();
+        }
+        
+        return getFirstContentType(content).getLargeIcon();
+    }
+    
+    /**
+     * Get the content type which determines the content icons and rendering
+     * @param content The content
+     * @return The main content type
+     */
+    public ContentType getFirstContentType (Content content)
+    {
+        TreeSet<ContentType> treeSet = new TreeSet<ContentType>(new ContentTypeComparator());
+        
+        for (String id : content.getTypes())
+        {
+            ContentType contentType = _cTypeEP.getExtension(id);
+            treeSet.add(contentType);
+        }
+        return treeSet.first();
+    }
+    
+    class ContentTypeComparator implements Comparator<ContentType>
+    {
+        @Override
+        public int compare(ContentType c1, ContentType c2)
+        {
+            I18nizableText t1 = c1.getLabel();
+            I18nizableText t2 = c2.getLabel();
+            
+            String str1 = t1.isI18n() ? t1.getKey() : t1.getLabel();
+            String str2 = t2.isI18n() ? t2.getKey() : t2.getLabel();
+            
+            int compareTo = str1.toString().compareTo(str2.toString());
+            if (compareTo == 0)
+            {
+                // Content types have same keys but there are not equals, so do not return 0 to add it in TreeSet
+                // Indeed, in a TreeSet implementation two elements that are equal by the method compareTo are, from the standpoint of the set, equal 
+                return 1;
+            }
+            return compareTo;
+        }
+    }
+    
 }
Index: main/plugin-cms/src/org/ametys/cms/contenttype/GetMetadataSetDefinitionAction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/GetMetadataSetDefinitionAction.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/GetMetadataSetDefinitionAction.java	(working copy)
@@ -53,6 +53,8 @@
     protected CurrentUserProvider _userProvider;
     /** The rights manager */
     protected RightsManager _rightsManager;
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
     
     @Override
     public void service(ServiceManager smanager) throws ServiceException
@@ -62,6 +64,7 @@
         _contentTypeExtensionPoint = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
         _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
         _rightsManager = (RightsManager) smanager.lookup(RightsManager.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
     }
     
     @Override
@@ -71,16 +74,23 @@
         
         String contentTypeId = StringUtils.defaultString(request.getParameter("contentType"));
         String contentId = StringUtils.defaultString(request.getParameter("contentId"));
+        String metadataSetName = StringUtils.defaultIfEmpty(request.getParameter("metadataSetName"), "main");
+        String metadataSetMode = StringUtils.defaultIfEmpty(request.getParameter("metadataSetMode"), "edition");
         
-        ContentType cType = null;
+        String[] contentTypes;
+        String[] mixins = new String[0];
+        
+        MetadataSet metadataSet = null;
+        Content content = null;
         if (StringUtils.isNotEmpty(contentTypeId))
         {
-            cType = _contentTypeExtensionPoint.getExtension(contentTypeId);
+            contentTypes = contentTypeId.split(",");
         }
         else if (StringUtils.isNotEmpty(contentId))
         {
-            Content content = _resolver.resolveById(contentId);
-            cType = _contentTypeExtensionPoint.getExtension(content.getType());
+            content = _resolver.resolveById(contentId);
+            contentTypes = content.getTypes();
+            mixins = content.getMixinTypes();
         }
         else
         {
@@ -89,32 +99,36 @@
             throw new IllegalArgumentException(errorMsg);
         }
         
-        if (cType == null)
+        for (String id : contentTypes)
         {
-            String errorMsg = String.format("Unable to get metadata set definition: unknown content type '" + contentTypeId + "'");
-            getLogger().error(errorMsg);
-            throw new IllegalArgumentException(errorMsg);
+            ContentType cType = _contentTypeExtensionPoint.getExtension(id);
+            
+            if (cType == null)
+            {
+                String errorMsg = String.format("Unable to get metadata set definition: unknown content type '" + id + "'");
+                getLogger().error(errorMsg);
+                throw new IllegalArgumentException(errorMsg);
+            }
         }
         
-        List<Object> metadataSetProperties = new LinkedList<Object>();
+        metadataSet = "view".equals(metadataSetMode) ? _contentTypesHelper.getMetadataSetForView(metadataSetName, contentTypes, mixins) : _contentTypesHelper.getMetadataSetForEdition(metadataSetName, contentTypes, mixins);
         
-        String metadataSetName = StringUtils.defaultIfEmpty(request.getParameter("metadataSetName"), "main");
-        String metadataSetMode = StringUtils.defaultIfEmpty(request.getParameter("metadataSetMode"), "edition");
-        MetadataSet metadataSet = "view".equals(metadataSetMode) ? cType.getMetadataSetForView(metadataSetName) : cType.getMetadataSetForEdition(metadataSetName);
+        
+        List<Object> metadataSetProperties = new LinkedList<Object>();
         
         if (metadataSet == null)
         {
-            String errorMsg = String.format("Unknown metadata set '%s' of type '%s' for content type '%s'", metadataSetName, metadataSetMode, cType.getId());
+            String errorMsg = String.format("Unknown metadata set '%s' of type '%s' for content types '%s'", metadataSetName, metadataSetMode, StringUtils.join(contentTypes, ','));
             getLogger().error(errorMsg);
             
-            metadataSetProperties.add(getMetadataErrorProperties(cType, metadataSetName, metadataSetMode));
+            metadataSetProperties.add(metadataError2JsonObject(contentTypes, metadataSetName, metadataSetMode));
         }
         else
         {
             // Getting the metadata set properties.
             for (AbstractMetadataSetElement element : metadataSet.getElements())
             {
-                Map<String, Object> childProperties = getMetadataSetElementInformation(cType, element);
+                Map<String, Object> childProperties = metadataSetElement2JsonObject(contentTypes, mixins, element);
                 if (childProperties != null)
                 {
                     metadataSetProperties.add(childProperties);
@@ -129,24 +143,24 @@
     
     /**
      * Get some error properties when the requested metadata set is not found. 
-     * @param contentType 
-     * @param metadataSetName 
-     * @param metadataSetMode 
+     * @param contentTypes The content types 
+     * @param metadataSetName The metadata set name
+     * @param metadataSetMode The metadata set mode
      * @return A map containing the properties.
      */
-    protected Map<String, Object> getMetadataErrorProperties(ContentType contentType, String metadataSetName, String metadataSetMode)
+    protected Map<String, Object> metadataError2JsonObject(String[] contentTypes, String metadataSetName, String metadataSetMode)
     {
-        HashMap<String, Object> properties = new LinkedHashMap<String, Object>();
+        HashMap<String, Object> jsonObject = new LinkedHashMap<String, Object>();
         
-        properties.put("type", "error");
-        properties.put("errorType", "metadataSet");
-        properties.put("contentType", contentType.getId());
-        properties.put("metadataSetName", metadataSetName);
-        properties.put("metadataSetMode", metadataSetMode);
+        jsonObject.put("type", "error");
+        jsonObject.put("errorType", "metadataSet");
+        jsonObject.put("contentType", StringUtils.join(contentTypes, ','));
+        jsonObject.put("metadataSetName", metadataSetName);
+        jsonObject.put("metadataSetMode", metadataSetMode);
         
-        properties.put("children", Collections.EMPTY_LIST);
+        jsonObject.put("children", Collections.EMPTY_LIST);
         
-        return properties;
+        return jsonObject;
     }
     
     /* --------------------------------------------------------------------- */
@@ -156,45 +170,48 @@
     /**
      * Get properties of the {@link AbstractMetadataSetElement} and its child element.
      * This method is the entry point to retrieves information of a MetadataSetElement.
-     * @param contentType 
+     * @param contentTypes The content types 
+     * @param mixins The mixins
      * @param metadataSetElement The metadata set element
      * @return A map containing the properties of this  {@link AbstractMetadataSetElement}.
      */
-    protected Map<String, Object> getMetadataSetElementInformation(ContentType contentType, AbstractMetadataSetElement metadataSetElement)
+    protected Map<String, Object> metadataSetElement2JsonObject(String[] contentTypes, String[] mixins, AbstractMetadataSetElement metadataSetElement)
     {
-        return getMetadataSetElementInformation(contentType, null, metadataSetElement, true);
+        return metadataSet2JsonObject(contentTypes, mixins, null, metadataSetElement, true);
     }
     
     /**
      * Get properties of the {@link AbstractMetadataSetElement}
      * This method is the entry point to retrieves information of a MetadataSetElement.
-     * @param contentType
+     * @param contentTypes The content types 
+     * @param mixins The mixins
      * @param metadataSetElement The metadata set element
      * @param recurse If true, also retrieves the properties of the child elements.
      * @return A map containing the properties of this  {@link AbstractMetadataSetElement}.
      */
-    protected Map<String, Object> getMetadataSetElementInformation(ContentType contentType, AbstractMetadataSetElement metadataSetElement, boolean recurse)
+    protected Map<String, Object> getMetadataSetElementInformation(String[] contentTypes, String[] mixins, AbstractMetadataSetElement metadataSetElement, boolean recurse)
     {
-        return getMetadataSetElementInformation(contentType, null, metadataSetElement, recurse);
+        return metadataSet2JsonObject(contentTypes, mixins, null, metadataSetElement, recurse);
     }
     
     /**
      * Get properties of the {@link AbstractMetadataSetElement}
-     * @param contentType 
+     * @param contentTypes The content types 
+     * @param mixins The mixins
      * @param metadataDefinition 
      * @param metadataSetElement The metadata set element
      * @param recurse If true, also retrieves the properties of the child elements.
      * @return A map containing the properties of this  {@link AbstractMetadataSetElement}.
      */
-    protected Map<String, Object> getMetadataSetElementInformation(ContentType contentType, MetadataDefinition metadataDefinition, AbstractMetadataSetElement metadataSetElement, boolean recurse)
+    protected Map<String, Object> metadataSet2JsonObject(String[] contentTypes, String[] mixins, MetadataDefinition metadataDefinition, AbstractMetadataSetElement metadataSetElement, boolean recurse)
     {
         if (metadataSetElement instanceof MetadataDefinitionReference)
         {
-            return getMetadataInformation(contentType, metadataDefinition, (MetadataDefinitionReference) metadataSetElement, recurse);
+            return metadata2JsonObject(contentTypes, mixins, metadataDefinition, (MetadataDefinitionReference) metadataSetElement, recurse);
         }
         else if (metadataSetElement instanceof Fieldset)
         {
-            return getFieldsetInformation(contentType, metadataDefinition, (Fieldset) metadataSetElement, recurse);
+            return fieldset2JsonObject(contentTypes, mixins, metadataDefinition, (Fieldset) metadataSetElement, recurse);
         }
         
         throw new IllegalArgumentException(
@@ -206,17 +223,18 @@
     /**
      * Add informations in a map of properties for the children of a metadata set element.
      * @param properties The map of properties to populate
-     * @param contentType
+     * @param contentTypes The content types
+     * @param mixins The mixins 
      * @param metadataDefinition
      * @param metadataSetElement
      */
-    protected void addChildrenInformation(Map<String, Object> properties, ContentType contentType, MetadataDefinition metadataDefinition, AbstractMetadataSetElement metadataSetElement)
+    protected void addChildrenInformation(Map<String, Object> properties, String[] contentTypes, String[] mixins, MetadataDefinition metadataDefinition, AbstractMetadataSetElement metadataSetElement)
     {
         List<Object> children = new LinkedList<Object>();
         
         for (AbstractMetadataSetElement child : metadataSetElement.getElements())
         {
-            Map<String, Object> childProperties = getMetadataSetElementInformation(contentType, metadataDefinition, child, true);
+            Map<String, Object> childProperties = metadataSet2JsonObject(contentTypes, mixins, metadataDefinition, child, true);
             if (childProperties != null)
             {
                 children.add(childProperties);
@@ -233,26 +251,27 @@
     
     /**
      * Get the properties of a {@link Fieldset}
-     * @param contentType 
+     * @param contentTypes The content types  
+     * @param mixins The mixins
      * @param metadataDefinition 
      * @param fieldset
      * @param recurse 
      * @return A map containing the {@link Fieldset} properties.
      */
-    protected Map<String, Object> getFieldsetInformation(ContentType contentType, MetadataDefinition metadataDefinition, Fieldset fieldset, boolean recurse)
+    protected Map<String, Object> fieldset2JsonObject(String[] contentTypes, String[] mixins, MetadataDefinition metadataDefinition, Fieldset fieldset, boolean recurse)
     {
-        HashMap<String, Object> properties = new LinkedHashMap<String, Object>();
+        HashMap<String, Object> jsonObject = new LinkedHashMap<String, Object>();
         
-        properties.put("type", "fieldset");
-        properties.put("label", fieldset.getLabel());
-        properties.put("role", fieldset.getRole());
+        jsonObject.put("type", "fieldset");
+        jsonObject.put("label", fieldset.getLabel());
+        jsonObject.put("role", fieldset.getRole());
         
         if (recurse)
         {
-            addChildrenInformation(properties, contentType, metadataDefinition, fieldset);
+            addChildrenInformation(jsonObject, contentTypes, mixins, metadataDefinition, fieldset);
         }
         
-        return properties;
+        return jsonObject;
     }
     
     
@@ -263,47 +282,48 @@
     
     /**
      * Get properties of the {@link MetadataDefinition} through its {@link MetadataDefinitionReference}
-     * @param contentType 
+     * @param contentTypes The content types 
+     * @param mixins The mixins 
      * @param parentMetadataDef 
      * @param metadataDefRef
      * @param recurse 
      * @return A map containing the {@link MetadataDefinitionReference} properties.
      */
-    protected Map<String, Object> getMetadataInformation(ContentType contentType, MetadataDefinition parentMetadataDef, MetadataDefinitionReference metadataDefRef, boolean recurse)
+    protected Map<String, Object> metadata2JsonObject(String[] contentTypes, String[] mixins, MetadataDefinition parentMetadataDef, MetadataDefinitionReference metadataDefRef, boolean recurse)
     {
-        MetadataDefinition metadataDefinition = _getMetadataDefinition(contentType, parentMetadataDef, metadataDefRef);
+        MetadataDefinition metadataDefinition = _getMetadataDefinition(contentTypes, mixins, parentMetadataDef, metadataDefRef);
         MetadataType type = metadataDefinition.getType();
         
-        HashMap<String, Object> properties = new LinkedHashMap<String, Object>();
+        HashMap<String, Object> jsonObject = new LinkedHashMap<String, Object>();
         
-        properties.put("type", "metadata");
-        properties.put("metadataName", metadataDefRef.getMetadataName());
-        properties.put("metadataType", _getMetadataType(metadataDefinition));
+        jsonObject.put("type", "metadata");
+        jsonObject.put("metadataName", metadataDefRef.getMetadataName());
+        jsonObject.put("metadataType", _getMetadataType(metadataDefinition));
         
-        properties.put("label", metadataDefinition.getLabel());
-        properties.put("description", metadataDefinition.getDescription());
-        properties.put("mandatory", _isMandatory(metadataDefinition));
+        jsonObject.put("label", metadataDefinition.getLabel());
+        jsonObject.put("description", metadataDefinition.getDescription());
+        jsonObject.put("mandatory", _isMandatory(metadataDefinition));
         
         boolean isContentReference = MetadataType.CONTENT.equals(type);
         boolean isSubContent = MetadataType.SUB_CONTENT.equals(type);
         if (isSubContent || isContentReference)
         {
-            properties.put("contentType", metadataDefinition.getContentType());
-            properties.put("metadataSetName", metadataDefRef.getMetadataSetName());
+            jsonObject.put("contentType", metadataDefinition.getContentType());
+            jsonObject.put("metadataSetName", metadataDefRef.getMetadataSetName());
         }
         
         if (isContentReference)
         {
-            properties.put("canCreate", _hasRight(metadataDefinition.getContentType()));
+            jsonObject.put("canCreate", _hasRight(metadataDefinition.getContentType()));
         }
         
         // Children information are not added in case of a metadata of type content.
-        if (properties != null && recurse && !isSubContent && !isContentReference)
+        if (jsonObject != null && recurse && !isSubContent && !isContentReference)
         {
-            addChildrenInformation(properties, contentType, metadataDefinition, metadataDefRef);
+            addChildrenInformation(jsonObject, contentTypes, mixins, metadataDefinition, metadataDefRef);
         }
         
-        return properties;
+        return jsonObject;
     }
 
     /**
@@ -400,13 +420,13 @@
      * @param metadataDefRef
      * @return The metadata definition found
      */
-    private MetadataDefinition _getMetadataDefinition(ContentType contentType, MetadataDefinition parentMetadataDef, MetadataDefinitionReference metadataDefRef)
+    private MetadataDefinition _getMetadataDefinition(String[] contentTypes, String[] mixins, MetadataDefinition parentMetadataDef, MetadataDefinitionReference metadataDefRef)
     {
         String metadataName = metadataDefRef.getMetadataName();
         
         if (parentMetadataDef == null)
         {
-            return contentType.getMetadataDefinition(metadataName);
+            return _contentTypesHelper.getMetadataDefinition(metadataName, contentTypes, mixins);
         }
         else
         {
Index: main/plugin-cms/src/org/ametys/cms/contenttype/DynamicContentTypeDescriptorExtentionPoint.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/DynamicContentTypeDescriptorExtentionPoint.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/DynamicContentTypeDescriptorExtentionPoint.java	(revision 0)
@@ -0,0 +1,240 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.contenttype;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilenameFilter;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.ConfigurationException;
+import org.apache.avalon.framework.configuration.DefaultConfiguration;
+import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.Constants;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringUtils;
+
+import org.ametys.runtime.plugin.PluginsManager;
+import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
+
+/**
+ * This class is in charge of handling dynamic content type descriptor extension point.<br>
+ * This point handles the pool of available dynamic content type descriptors.
+ */
+public class DynamicContentTypeDescriptorExtentionPoint extends AbstractThreadSafeComponentExtensionPoint<DynamicContentTypeDescriptor>
+{
+    /** Avalon Role */
+    public static final String ROLE = "org.ametys.cms.contenttype.DynamicContentTypeExtentionPoint"; // FIXME RUNTIME-1023 DynamicContentTypeDescriptorExtentionPoint.class.getName();
+    
+    private ServiceManager _smanager;
+    private ContentTypeExtensionPoint _contentTypeEP;
+    private ContentTypesHelper _contentTypesHelper;
+    
+    private Map<String, String> _cache = new HashMap<String, String>();
+    
+    @Override
+    public void service(ServiceManager manager) throws ServiceException
+    {
+        super.service(manager);
+        _smanager = manager;
+        _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
+    }
+    
+    @Override
+    public void initializeExtensions() throws Exception
+    {
+        org.apache.cocoon.environment.Context cocoonContext = (org.apache.cocoon.environment.Context) _context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
+        final Set<String> pluginNames = PluginsManager.getInstance().getPluginNames();
+        
+        File rootFile = new File(cocoonContext.getRealPath("WEB-INF/param/content-types/_dynamic"));
+        File[] plugins = rootFile.listFiles(new FilenameFilter()
+        {
+            @Override
+            public boolean accept(File dir, String name)
+            {
+                return pluginNames.contains(name);
+            }
+        });
+        
+        if (plugins != null)
+        {
+            for (File pluginDir : plugins)
+            {
+                String pluginName = pluginDir.getName();
+                
+                File[] files = pluginDir.listFiles(new FilenameFilter()
+                {
+                    @Override
+                    public boolean accept(File dir, String name)
+                    {
+                        return name.endsWith(".xml");
+                    }
+                });
+                
+                for (File file : files)
+                {
+                    InputStream is = null;
+                    Configuration configuration = null;
+                    try
+                    {
+                        is = new FileInputStream(file); 
+                        configuration = new DefaultConfigurationBuilder(true).build(is);
+                        
+                        String id = "dynamic-content-type." + file.getName().substring(0, file.getName().lastIndexOf('.'));
+                        
+                        DefaultConfiguration conf = new DefaultConfiguration("extension");
+                        conf.setAttribute("id", id);
+                        conf.addChild(configuration);
+                        
+                        addComponent(pluginName, "unknown", id, DynamicContentTypeDescriptor.class, conf);
+                    }
+                    catch (Exception ex)
+                    {
+                        throw new ConfigurationException("Unable to parse dynamic content type configuration at WEB-INF/param/content-types/dynamic/" + file.getName() + ".xml", configuration, ex);
+                    }
+                    finally
+                    {
+                        IOUtils.closeQuietly(is);
+                    }
+                }
+            }
+        }
+        
+        super.initializeExtensions();
+    }
+    
+    /**
+     * Get the matching dynamic descriptor for given content types and mixins
+     * @param contentTypes The content types
+     * @param mixinTypes The mixins
+     * @return The matching dynamic descriptor or <code>null</code> if not found
+     */
+    public DynamicContentTypeDescriptor getMatchingDescriptor (String[] contentTypes, String[] mixinTypes)
+    {
+        String cacheId = _getCacheIdentifier(contentTypes, mixinTypes);
+        
+        if (_cache.containsKey(cacheId))
+        {
+            return getExtension(_cache.get(cacheId));
+        }
+        
+        DynamicContentTypeDescriptor matchingDynamicCType = null;
+        
+        int bestCTypeScore = 0;
+        int bestMixinScore = 0;
+        
+        for (String id : getExtensionsIds())
+        {
+            int cTypeScore = 0;
+            int mixinScore = 0;
+            
+            DynamicContentTypeDescriptor dynamicContentType = getExtension(id);
+            
+            boolean dynamicTypesMatch = true;
+            String[] dynamicCTypes = dynamicContentType.getSupertypeIds();
+            Set<String> cTypesAndAncestors = _getContentTypesAndAncestor(contentTypes, mixinTypes);
+            for (String cTypeId : dynamicCTypes)
+            {
+                if (!cTypesAndAncestors.contains(cTypeId))
+                {
+                    // Dynamic types do not include all content types, ignore
+                    dynamicTypesMatch = false;
+                    break;
+                }
+            }
+            
+            if (dynamicTypesMatch)
+            {
+                for (String cTypeId : dynamicContentType.getSupertypeIds())
+                {
+                    ContentType cType = _contentTypeEP.getExtension(cTypeId);
+                    
+                    if (!cType.isMixin() && ArrayUtils.contains(contentTypes, cTypeId))
+                    {
+                        cTypeScore += 1;
+                    }
+                    else if (cType.isMixin() && ArrayUtils.contains(mixinTypes, cTypeId))
+                    {
+                        mixinScore += 1;
+                    }
+                }
+                
+                if (cTypeScore > bestCTypeScore || (cTypeScore == bestCTypeScore && mixinScore > bestMixinScore))
+                {
+                    bestCTypeScore = cTypeScore;
+                    bestMixinScore = mixinScore;
+
+                    matchingDynamicCType = dynamicContentType;
+                    _cache.put(cacheId, id);
+                }
+            }
+        }
+        
+        return matchingDynamicCType;
+    }
+    
+    private Set<String> _getContentTypesAndAncestor (String[] contentTypes, String[] mixins)
+    {
+        Set<String> cTypesAndAncestors = new HashSet<String>();
+        
+        if (_contentTypesHelper == null)
+        {
+            try
+            {
+                _contentTypesHelper = (ContentTypesHelper) _smanager.lookup(ContentTypesHelper.ROLE);
+            }
+            catch (ServiceException e)
+            {
+                throw new IllegalStateException(e);
+            }
+        }
+        
+        String[] allContentTypes = (String[]) ArrayUtils.addAll(contentTypes, mixins);
+        
+        for (String id : allContentTypes)
+        {
+            cTypesAndAncestors.add(id);
+            cTypesAndAncestors.addAll(_contentTypesHelper.getAncestors(id));
+        }
+        
+        return cTypesAndAncestors;
+        
+    }
+    
+    private String _getCacheIdentifier (String[] contentTypes, String[] mixins)
+    {
+        Arrays.sort(contentTypes);
+        String name = StringUtils.join(contentTypes, ";");
+        
+        if (mixins.length > 0)
+        {
+            Arrays.sort(mixins);
+            name += ";";
+            name += StringUtils.join(mixins, ";");
+        }
+        
+        return name;
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/contenttype/MetadataManager.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/MetadataManager.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/MetadataManager.java	(working copy)
@@ -68,6 +68,8 @@
     
     /** Content type extension point. */
     protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
     /** Users manager */
     protected UsersManager _usersManager;
     /** The JSON conversion utilities. */
@@ -88,6 +90,7 @@
         _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
         _usersManager = (UsersManager) manager.lookup(UsersManager.ROLE);
         _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
     }
     
     /**
@@ -256,15 +259,7 @@
         {
             if (content != null)
             {
-                String contentTypeId = content.getType();
-                ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
-                
-                if (contentType == null)
-                {
-                    throw new AmetysRepositoryException("Unknown content type: " + contentTypeId);
-                }
-                
-                metadataDefinition =  contentType.getMetadataDefinition(metadataName);
+                metadataDefinition =  _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
             }
         }
         else
@@ -887,21 +882,13 @@
         
         if (metadataSetName != null)
         {
-            String contentTypeId = content.getType();
-            
-            ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
-            if (contentType == null)
-            {
-                throw new AmetysRepositoryException("Unknown content type: " + contentTypeId);
-            }
-            
             if (editionRendering)
             {
-                metadataSet = contentType.getMetadataSetForEdition(metadataSetName);
+                metadataSet = _contentTypesHelper.getMetadataSetForEdition(metadataSetName, content.getTypes(), content.getMixinTypes());
             }
             else
             {
-                metadataSet = contentType.getMetadataSetForView(metadataSetName);
+                metadataSet = _contentTypesHelper.getMetadataSetForView(metadataSetName, content.getTypes(), content.getMixinTypes());
             }
         }
         else
@@ -1198,14 +1185,6 @@
      */
     protected boolean _canRead(Content content, String metadataPath) throws AmetysRepositoryException
     {
-        String contentTypeId = content.getType();
-        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
-        
-        if (contentType == null)
-        {
-            throw new AmetysRepositoryException("Unknown content type: " + contentTypeId);
-        }
-        
-        return contentType.canRead(content, metadataPath);
+        return _contentTypesHelper.canRead(content, metadataPath);
     }
 }
Index: main/plugin-cms/src/org/ametys/cms/contenttype/DynamicContentTypeDescriptor.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/DynamicContentTypeDescriptor.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/DynamicContentTypeDescriptor.java	(revision 0)
@@ -0,0 +1,49 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.contenttype;
+
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.ConfigurationException;
+
+/**
+ * This class represents a dynamic content type descriptor
+ */
+public class DynamicContentTypeDescriptor extends AbstractContentTypeDescriptor
+{
+    @Override
+    protected Configuration getOverridenConfiguration() throws ConfigurationException
+    {
+        return null;
+    }
+    
+    @Override
+    protected Configuration getRootConfiguration(Configuration configuration)
+    {
+        return configuration.getChild("dynamic-content-type-descriptor");
+    }
+    
+    @Override
+    protected String _getDefaultCatalogue()
+    {
+        return "application";
+    }
+    
+    @Override
+    protected String _getIconPath(String pluginName)
+    {
+        return "/plugins/cms/dynamic-content-types_resources/" + pluginName + "/resources/";
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypeExtensionPoint.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypeExtensionPoint.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/ContentTypeExtensionPoint.java	(working copy)
@@ -32,6 +32,8 @@
 import org.apache.commons.io.IOUtils;
 
 import org.ametys.cms.repository.ContentTypeExpression;
+import org.ametys.cms.repository.ContentTypeOrMixinTypeExpression;
+import org.ametys.cms.repository.MixinTypeExpression;
 import org.ametys.plugins.repository.query.expression.Expression.Operator;
 import org.ametys.runtime.plugin.PluginsManager;
 import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
@@ -143,6 +145,46 @@
     }
     
     /**
+     * Create a hierarchical {@link ContentTypeExpression} which returns
+     * all contents of the specified mixin and all its descendant types.
+     * @param mixinIds the mixin ids
+     * @return the hierarchical content type expression.
+     */
+    public MixinTypeExpression createHierarchicalMixinExpression(String... mixinIds)
+    {
+        Set<String> typesToSearch = new HashSet<String>();
+        
+        for (String mixinId : mixinIds)
+        {
+            typesToSearch.add(mixinId);
+            typesToSearch.addAll(getSubTypes(mixinId));
+        }
+        
+        return new MixinTypeExpression(Operator.EQ, typesToSearch.toArray(new String[typesToSearch.size()]));
+    }
+    
+    /**
+     * Create a hierarchical {@link ContentTypeExpression} which returns
+     * all contents of the specified type or mixin and all its descendant types.
+     * @param contentIds the mixin ids
+     * @return the hierarchical content type expression.
+     */
+    public ContentTypeOrMixinTypeExpression createHierarchicalContentTypeOrMixinExpression(String... contentIds)
+    {
+        Set<String> typesToSearch = new HashSet<String>();
+        
+        for (String contentTypeId : contentIds)
+        {
+            typesToSearch.add(contentTypeId);
+            typesToSearch.addAll(getSubTypes(contentTypeId));
+        }
+        
+        return new ContentTypeOrMixinTypeExpression(Operator.EQ, typesToSearch.toArray(new String[typesToSearch.size()]));
+    }
+    
+    
+    
+    /**
      * Get all the descendant types of a given {@link ContentType}.
      * @param contentTypeId the content type id.
      * @return a collection of the ids of all the ContentType's descendants.
Index: main/plugin-cms/src/org/ametys/cms/contenttype/CommonMetadataSetNamesFromContentsGenerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/CommonMetadataSetNamesFromContentsGenerator.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/CommonMetadataSetNamesFromContentsGenerator.java	(working copy)
@@ -21,6 +21,7 @@
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
 import org.apache.cocoon.environment.Request;
+import org.apache.commons.lang.ArrayUtils;
 
 import org.ametys.cms.repository.Content;
 import org.ametys.plugins.repository.AmetysObjectResolver;
@@ -49,7 +50,12 @@
         for (String contentId : contentIds)
         {
             Content content = _resolver.resolveById(contentId);
-            cTypeIds.add(content.getType());
+            
+            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
+            for (String id : allContentTypes)
+            {
+                cTypeIds.add(id);
+            }
         }
         
         return cTypeIds.toArray(new String[cTypeIds.size()]);
Index: main/plugin-cms/src/org/ametys/cms/contenttype/DefaultContentType.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/DefaultContentType.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/DefaultContentType.java	(working copy)
@@ -20,7 +20,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -37,7 +36,6 @@
 import org.apache.avalon.framework.context.Context;
 import org.apache.avalon.framework.context.ContextException;
 import org.apache.avalon.framework.context.Contextualizable;
-import org.apache.avalon.framework.logger.AbstractLogEnabled;
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
 import org.apache.avalon.framework.service.Serviceable;
@@ -46,14 +44,12 @@
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.excalibur.source.Source;
-import org.apache.excalibur.source.SourceResolver;
 import org.xml.sax.SAXException;
 
 import org.ametys.cms.repository.Content;
 import org.ametys.cms.repository.WorkflowAwareContent;
 import org.ametys.cms.transformation.RichTextTransformer;
 import org.ametys.cms.transformation.docbook.DocbookTransformer;
-import org.ametys.plugins.explorer.dublincore.DublinCoreMetadataProvider;
 import org.ametys.plugins.repository.AmetysRepositoryException;
 import org.ametys.plugins.workflow.Workflow;
 import org.ametys.runtime.plugin.component.PluginAware;
@@ -85,43 +81,15 @@
  * &nbsp;&nbsp;&lt;/restrict-to><br>
  * </code>
  */
-public class DefaultContentType extends AbstractLogEnabled implements ContentType, PluginAware, Serviceable, Contextualizable, Configurable, ThreadSafe, Disposable
+public class DefaultContentType extends AbstractContentTypeDescriptor implements ContentType, PluginAware, Serviceable, Contextualizable, Configurable, ThreadSafe, Disposable
 {
     /** Global validator role. */
     protected static final String __GLOBAL_VALIDATOR_ROLE = "_global";
     
     static Pattern __annotationNamePattern;
     
-    /** Plugin name. */
-    protected String _pluginName;
-    /** Content type id. */
-    protected String _id;
-    /** This content-type's supertype ids (can be empty). */
-    protected String[] _superTypeIds;
-    /** Label. */
-    protected I18nizableText _label;
-    /** Description. */
-    protected I18nizableText _description;
-    /** Default title. */
-    protected I18nizableText _defaultTitle;
-    /** Category. */
-    protected I18nizableText _category;
-    /** Small icon URI 16x16. */
-    protected String _smallIcon;
-    /** Medium icon URI 32x32. */
-    protected String _mediumIcon;
-    /** Large icon URI 48x48. */
-    protected String _largeIcon;
     /** Metadata definitions. */
     protected Map<String, MetadataDefinition> _metadata = new LinkedHashMap<String, MetadataDefinition>();
-    /** All metadata sets for view. */
-    protected Map<String, MetadataSet> _allMetadataSetsForView = new LinkedHashMap<String, MetadataSet>();
-    /** Non-internal metadata sets for view. */
-    protected Map<String, MetadataSet> _metadataSetsForView = new LinkedHashMap<String, MetadataSet>();
-    /** All metadata sets for edition. */
-    protected Map<String, MetadataSet> _allMetadataSetsForEdition = new LinkedHashMap<String, MetadataSet>();
-    /** Non-internal metadata sets for edition. */
-    protected Map<String, MetadataSet> _metadataSetsForEdition = new LinkedHashMap<String, MetadataSet>();
     /** The right needed to create a content of this type, or null if no right is needed. */
     protected String _right;
     /** The abstract property */
@@ -134,8 +102,6 @@
     protected Context _context;
     /** Cocoon Context */
     protected org.apache.cocoon.environment.Context _cocoonContext;
-    /** The content type extension point. */
-    protected ContentTypeExtensionPoint _cTypeEP;
     /** The workflow. */
     protected Workflow _workflow;
     /** The rights manager. */
@@ -152,12 +118,6 @@
     protected RichTextUpdater _richTextUpdater;
     /** The content indexer. */
     protected ContentIndexer _contentIndexer;
-    /** The source resolver */
-    protected SourceResolver _srcResolver;
-    /** The DublinCore metadata provider */
-    protected DublinCoreMetadataProvider _dcProvider;
-    /** The content types helper */
-    protected ContentTypesHelper _contentTypesHelper;
     
     // ComponentManager pour les Validator
     private ThreadSafeComponentManager<Validator> _validatorManager;
@@ -165,32 +125,20 @@
     //ComponentManager pour les Enumerator
     private ThreadSafeComponentManager<Enumerator> _enumeratorManager;
 
-    
-    
-    @Override
-    public void setPluginInfo(String pluginName, String featureName)
-    {
-        _pluginName = pluginName;
-    }
-    
     @Override
     public void service(ServiceManager manager) throws ServiceException
     {
+        super.service(manager);
         _manager = manager;
-        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
         _workflow = (Workflow) manager.lookup(Workflow.ROLE);
         _rightsManager = (RightsManager) manager.lookup(RightsManager.ROLE);
         _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
         _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE);
         _consistencyExtractor = (ConsistencyExtractor) manager.lookup(DefaultConsistencyExtractor.ROLE);
         _richTextUpdater = (RichTextUpdater) manager.lookup(DocbookRichTextUpdater.ROLE);
-        _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
-        _dcProvider = (DublinCoreMetadataProvider) manager.lookup(DublinCoreMetadataProvider.ROLE);
         _contentIndexer = (DefaultContentIndexer) manager.lookup(DefaultContentIndexer.ROLE);
-        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
     }
 
-    @Override
     public void contextualize(Context context) throws ContextException
     {
         _context = context;
@@ -208,6 +156,40 @@
     }
 
     @Override
+    protected Configuration getRootConfiguration(Configuration configuration)
+    {
+        return configuration.getChild("content-type");
+    }
+    
+    @Override
+    protected Configuration getOverridenConfiguration() throws ConfigurationException
+    {
+        Configuration overridenConf = null;
+        File ctFile = new File(_cocoonContext.getRealPath("WEB-INF/param/content-types/_override/" + _id + ".xml"));
+        
+        if (ctFile.exists())
+        {
+            InputStream is = null;
+            
+            try
+            {
+                is = new FileInputStream(ctFile); 
+                overridenConf = new DefaultConfigurationBuilder(true).build(is);
+            }
+            catch (Exception ex)
+            {
+                throw new ConfigurationException("Unable to parse overriden configuration for content type '" + _id + "' at WEB-INF/param/content-types/_override/" + _id + ".xml", overridenConf, ex);
+            }
+            finally
+            {
+                IOUtils.closeQuietly(is);
+            }
+        }
+        
+        return overridenConf;
+    }
+    
+    @Override
     public void configure(Configuration configuration) throws ConfigurationException
     {
         try
@@ -229,110 +211,49 @@
         
         _id = configuration.getAttribute("id");
         
-        Configuration contentTypeConfiguration = configuration.getChild("content-type");
+        Configuration rootConfiguration = getRootConfiguration(configuration);
         
-        String extendedCTypes = contentTypeConfiguration.getAttribute("extends", null);
-        List<String> superTypeIds = new ArrayList<String>();
-        if (extendedCTypes != null)
-        {
-            String[] superIds = extendedCTypes.split(",");
-            for (String id : superIds)
-            {
-                superTypeIds.add(StringUtils.trim(id));
-            }
-        }
+        _abstract = rootConfiguration.getAttributeAsBoolean("abstract", false);
         
-        _superTypeIds = superTypeIds.toArray(new String[superTypeIds.size()]);
-        _label = _parseI18nizableText(contentTypeConfiguration, "label");
-        _description = _parseI18nizableText(contentTypeConfiguration, "description");
-        _defaultTitle = _parseI18nizableText(contentTypeConfiguration, "default-title");
-        _category = _parseI18nizableText(contentTypeConfiguration, "category");
+        _configureSuperTypes(rootConfiguration);
         
-        _smallIcon = _parseIcon(contentTypeConfiguration.getChild("icons"), "small");
-        _mediumIcon = _parseIcon(contentTypeConfiguration.getChild("icons"), "medium");
-        _largeIcon = _parseIcon(contentTypeConfiguration.getChild("icons"), "large");
-        _right = contentTypeConfiguration.getChild("right").getValue(null);
-        _abstract = contentTypeConfiguration.getAttributeAsBoolean("abstract", false);
+        _configureLabels(rootConfiguration);
+        _configureIcons(rootConfiguration);
         
-        Configuration overridenConf = null;
-        File ctFile = new File(_cocoonContext.getRealPath("WEB-INF/param/content-types/_override/" + _id + ".xml"));
-        
-        if (ctFile.exists())
-        {
-            InputStream is = null;
-            
-            try
-            {
-                is = new FileInputStream(ctFile); 
-                overridenConf = new DefaultConfigurationBuilder(true).build(is);
-            }
-            catch (Exception ex)
-            {
-                throw new ConfigurationException("Unable to parse overriden configuration for content type '" + _id + "' at WEB-INF/param/content-types/_override/" + _id + ".xml", overridenConf, ex);
-            }
-            finally
-            {
-                IOUtils.closeQuietly(is);
-            }
-        }
-        
-        List<ContentType> superTypes = new ArrayList<ContentType>();
+        // Tags
+        _tags = new HashSet<String>();
         
-        // Retrieve the super types, if applicable.
-        // TODO CMS-5262 Check super ctypes can be concatenated
-        for (String superTypeId : _superTypeIds)
+        if (rootConfiguration.getChild("tags", false) != null)
         {
-            if (_cTypeEP.hasExtension(superTypeId))
+            if (rootConfiguration.getChild("tags").getAttributeAsBoolean("inherited", false))
             {
-                ContentType superType = _cTypeEP.getExtension(superTypeId);
-                
-                if (_abstract && !superType.isAbstract())
+                // Get tags from super types
+                for (String superTypeId : _superTypeIds)
                 {
-                    String message = "The abstract content-type '" + _id + "' declares '" + superTypeId + "' as a super-type, but the super-type is not abstract. An abstract content-type can not extend a non-abstract content-type.";
-                    throw new ConfigurationException(message, configuration);
+                    ContentType superType = _cTypeEP.getExtension(superTypeId);
+                    _tags.addAll(superType.getTags());
                 }
-                
-                superTypes.add(superType);
             }
-            else
-            {
-                String message = "The content-type '" + _id + "' declares '" + superTypeId + "' as a super-type, but it doesn't exist or isn't properly initialized.";
-                throw new ConfigurationException(message, configuration);
-            }
+            _tags.addAll(_parseTags (rootConfiguration.getChild("tags")));
         }
         
-        // Tags
-        if (contentTypeConfiguration.getChild("tags", false) != null)
-        {
-            _tags = _parseTags (contentTypeConfiguration.getChild("tags"));
-        }
-        else
-        {
-            _tags = new HashSet<String>();
-            
-            // Get tags from super type if they are not overridden 
-            for (String superTypeId : _superTypeIds)
-            {
-                ContentType superType = _cTypeEP.getExtension(superTypeId);
-                _tags.addAll(superType.getTags());
-            }
-        }
+        // Rights
+        _right = rootConfiguration.getChild("right").getValue(null);
         
         // Metadata definitions
-        _configureMetadataDefinitions (contentTypeConfiguration, overridenConf);
+        _configureMetadataDefinitions (rootConfiguration);
         
         // Metadata sets
-        _configureMetadataSets (contentTypeConfiguration, overridenConf);
+        _configureMetadataSets (rootConfiguration);
     }
     
     /**
      * Configure metadata definitions
      * @param mainConfig The content type configuration
-     * @param overriddenConfig The overridden configuration
      * @throws ConfigurationException
      */
     @SuppressWarnings("unchecked")
-    protected void _configureMetadataDefinitions (Configuration mainConfig, Configuration overriddenConfig) throws ConfigurationException
+    protected void _configureMetadataDefinitions (Configuration mainConfig) throws ConfigurationException
     {
         MetadataAndRepeaterDefinitionParser defParser = new MetadataAndRepeaterDefinitionParser(_enumeratorManager, _validatorManager);
         
@@ -343,6 +264,7 @@
         
         _getApplicableMetadata(mainConfig, metadataConfiguration, false);
         
+        Configuration overriddenConfig = getOverridenConfiguration();
         if (overriddenConfig != null)
         {
             _getApplicableMetadata(overriddenConfig, metadataConfiguration, true);
@@ -392,35 +314,6 @@
     }
     
     /**
-     * Configure metadata sets
-     * @param mainConfig The content type configuration
-     * @param overriddenConfig The overridden configuration
-     * @throws ConfigurationException
-     */
-    protected void _configureMetadataSets (Configuration mainConfig, Configuration overriddenConfig) throws ConfigurationException
-    {
-        // First, fill metadata-set from super types
-        _allMetadataSetsForView.putAll(_contentTypesHelper.getMetadataSetsForView(_superTypeIds, true));
-        _allMetadataSetsForEdition.putAll(_contentTypesHelper.getMetadataSetsForEdition(_superTypeIds, true));
-        _metadataSetsForView.putAll(_contentTypesHelper.getMetadataSetsForView(_superTypeIds, false));
-        _metadataSetsForEdition.putAll(_contentTypesHelper.getMetadataSetsForEdition(_superTypeIds, false));
-        
-        Map<String, Configuration> metadataSetViewConfs = new LinkedHashMap<String, Configuration>();
-        Map<String, Configuration> metadataSetEditConfs = new LinkedHashMap<String, Configuration>();
-        
-        _getApplicableMetadataSets(mainConfig, metadataSetViewConfs, metadataSetEditConfs, false);
-        
-        if (overriddenConfig != null)
-        {
-            _getApplicableMetadataSets(overriddenConfig, metadataSetViewConfs, metadataSetEditConfs, true);
-        }
-        
-        // Then parse own metadata sets
-        _parseMetadataSets(metadataSetViewConfs, _metadataSetsForView, _allMetadataSetsForView, _superTypeIds);
-        _parseMetadataSets(metadataSetEditConfs, _metadataSetsForEdition, _allMetadataSetsForEdition, _superTypeIds);
-    }
-    
-    /**
      * Fill a map of the applicable metadata configurations.
      * @param config the content type configuration.
      * @param metadataConfigurations the Map of metadata {@link Configuration}, indexed by name.
@@ -550,7 +443,6 @@
                 Configuration configuration = new DefaultConfigurationBuilder(true).build(is);
                 
                 MetadataDefinition metadataDefinition = new MetadataDefinition();
-                metadataDefinition.setReferenceContentType(_id);
                 metadataDefinition.setId("/dc"); // FIXME ?
                 metadataDefinition.setLabel(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_LABEL"));
                 metadataDefinition.setDescription(new I18nizableText("plugin.cms", "PLUGINS_CMS_DUBLINCORE_DESC"));
@@ -565,7 +457,6 @@
                     {
                         MetadataDefinition metaDef = defParser.parseParameter(_manager, _pluginName, childConfiguration);
                         String metadataName = metaDef.getName();
-                        metadataDefinition.setReferenceContentType(_id);
                         metaDef.setId("/dc/" + metadataName);  // FIXME ?
                         
                         if (metaDef.getEnumerator() == null && _dcProvider.isEnumerated(metadataName))
@@ -611,124 +502,6 @@
     }
     
     /**
-     * Compute the applicable metadata-sets from their configurations.
-     * @param config The content type configuration
-     * @param metadataSetViewConfs the view metadata-set configurations, indexed by name.
-     * @param metadataSetEditConfs the edition metadata-set configurations, indexed by name.
-     * @param allowOverride if true, encountering a metadata-set which has already been declared (based on its name) will replace it. Otherwise, an exception will be thrown. 
-     * @throws ConfigurationException if the configuration is invalid
-     */
-    protected void _getApplicableMetadataSets(Configuration config, Map<String, Configuration> metadataSetViewConfs, Map<String, Configuration> metadataSetEditConfs, boolean allowOverride) throws ConfigurationException
-    {
-        for (Configuration metadataSetConfig : config.getChildren("metadata-set"))
-        {
-            String name = metadataSetConfig.getAttribute("name");
-            String type = metadataSetConfig.getAttribute("type");
-            
-            if (type.equals("view"))
-            {
-                if (!allowOverride && metadataSetViewConfs.containsKey(name))
-                {
-                    throw new ConfigurationException("The view metadata-set '" + name + "' is already defined.", metadataSetConfig);
-                }
-                metadataSetViewConfs.put(name, metadataSetConfig);
-            }
-            else if (type.equals("edition"))
-            {
-                if (!allowOverride && metadataSetEditConfs.containsKey(name))
-                {
-                    throw new ConfigurationException("The edition metadata-set '" + name + "' is already defined.", metadataSetConfig);
-                }
-                metadataSetEditConfs.put(name, metadataSetConfig);
-            }
-            else
-            {
-                throw new ConfigurationException("Invalid type '" + type + "' for metadata set '" + name + "'", metadataSetConfig);
-            }
-        }
-    }
-    
-    /**
-     * Parse the metadata-set configurations
-     * @param metadataSetConfs the metadata-set configurations, indexed by name.
-     * @param metadataSets the Map of "public" {@link MetadataSet} objects to fill, indexed by name
-     * @param allMetadataSets the Map of {@link MetadataSet} objects to fill (including internal ones), indexed by name.
-     * @param superTypeIds the super content-types
-     * @throws ConfigurationException if the configuration is invalid
-     */
-    protected void _parseMetadataSets(Map<String, Configuration> metadataSetConfs, Map<String, MetadataSet> metadataSets, Map<String, MetadataSet> allMetadataSets, String[] superTypeIds) throws ConfigurationException
-    {
-        for (Configuration metadataSetConfig : metadataSetConfs.values())
-        {
-            String name = metadataSetConfig.getAttribute("name");
-            String type = metadataSetConfig.getAttribute("type");
-            boolean isInternal = metadataSetConfig.getAttributeAsBoolean("internal", false);
-            boolean isEdition = type.equals("edition");
-            
-            MetadataSet metadataSet = _parseMetadataSet(metadataSetConfig, name, isEdition, isInternal, superTypeIds);
-            
-            allMetadataSets.put(name, metadataSet);
-            
-            if (!isInternal)
-            {
-                metadataSets.put(name, metadataSet);
-            }
-        }
-    }
-    
-    /**
-     * Parse an i18n text.
-     * @param config the configuration to use.
-     * @param name the child name.
-     * @return the i18n text.
-     * @throws ConfigurationException if the configuration is not valid.
-     */
-    protected I18nizableText _parseI18nizableText(Configuration config, String name) throws ConfigurationException
-    {
-        return _parseI18nizableText(config, name, "");
-    }
-    
-    /**
-     * Parse an i18n text.
-     * @param config the configuration to use.
-     * @param name the child name.
-     * @param defaultValue the default value if no present
-     * @return the i18n text.
-     * @throws ConfigurationException if the configuration is not valid.
-     */
-    protected I18nizableText _parseI18nizableText(Configuration config, String name, String defaultValue) throws ConfigurationException
-    {
-        Configuration textConfig = config.getChild(name);
-        boolean i18nSupported = textConfig.getAttributeAsBoolean("i18n", false);
-        String text = textConfig.getValue(defaultValue);
-        
-        if (i18nSupported)
-        {
-            String catalogue = textConfig.getAttribute("catalogue", null);
-            
-            if (catalogue == null)
-            {
-                catalogue = _getDefaultCatalogue();
-            }
-            
-            return new I18nizableText(catalogue, text);
-        }
-        else
-        {
-            return new I18nizableText(text);
-        }
-    }
-    
-    /**
-     * Returns the default i18n catalogue for this content type.
-     * @return the default i18n catalogue for this content type.
-     */
-    protected String _getDefaultCatalogue()
-    {
-        return "plugin." + _pluginName;
-    }
-    
-    /**
      * Parse the tags
      * @param configuration the configuration to use
      * @return the tags
@@ -747,301 +520,6 @@
         return tags;
     }
     
-    
-    /**
-     * Parse an icon path
-     * @param configuration the configuration to use
-     * @param name the child name.
-     * @return The icon path
-     * @throws ConfigurationException if the configuration is not valid.
-     */
-    protected String _parseIcon (Configuration configuration, String name) throws ConfigurationException
-    {
-        return _parseIcon(configuration, name, "/plugins/cms/resources/img/contenttype/unknown-" + name + ".png");
-    }
-    
-    /**
-     * Parse an icon path
-     * @param configuration the configuration to use
-     * @param name the child name.
-     * @param defaultValue the default value.
-     * @return The icon path
-     * @throws ConfigurationException if the configuration is not valid.
-     */
-    protected String _parseIcon(Configuration configuration, String name, String defaultValue) throws ConfigurationException
-    {
-        Configuration iconConfig = configuration.getChild(name, false);
-        if (iconConfig != null)
-        {
-            String pluginName = iconConfig.getAttribute("plugin", _pluginName);
-            return _getIconPath(pluginName) + iconConfig.getValue();
-        }
-        
-        return defaultValue;
-    }
-    
-    /**
-     * Returns the path for icons
-     * @param pluginName the configured plugin
-     * @return the path for icons
-     */
-    protected String _getIconPath(String pluginName)
-    {
-        return "/plugins/" + pluginName + "/resources/";
-    }
-    
-    /**
-     * Parse a metadata-set configuration to create a {@link MetadataSet} object.
-     * @param metadataSetConfig the metadata set configuration to use.
-     * @param metadataSetName the metadata set name.
-     * @param isEdition the metadata set edition status.
-     * @param isInternal the metadata internal status.
-     * @param superTypeIds the super types.
-     * @return the metadata set
-     * @throws ConfigurationException if the configuration is not valid.
-     */
-    protected MetadataSet _parseMetadataSet(Configuration metadataSetConfig, String metadataSetName, boolean isEdition, boolean isInternal, String[] superTypeIds) throws ConfigurationException
-    {
-        MetadataSet metadataSet = new MetadataSet();
-        
-        I18nizableText label = _parseI18nizableText(metadataSetConfig, "label", metadataSetName);
-        I18nizableText description = _parseI18nizableText(metadataSetConfig, "description");
-        
-        Configuration iconConf = metadataSetConfig.getChild("icons");
-        
-        String smallIcon = _parseMetadataSetIcon(iconConf, "small", metadataSetName);
-        String mediumIcon = _parseMetadataSetIcon(iconConf, "medium", metadataSetName);
-        String largeIcon = _parseMetadataSetIcon(iconConf, "large", metadataSetName);
-        
-        metadataSet.setName(metadataSetName);
-        metadataSet.setLabel(label);
-        metadataSet.setDescription(description);
-        metadataSet.setSmallIcon(smallIcon);
-        metadataSet.setMediumIcon(mediumIcon);
-        metadataSet.setLargeIcon(largeIcon);
-        metadataSet.setEdition(isEdition);
-        
-        MetadataSet superMetadataSet = null;
-        if (superTypeIds.length > 0)
-        {
-            if (isEdition)
-            {
-                superMetadataSet = _contentTypesHelper.getMetadataSetForEdition(metadataSetName, superTypeIds);
-            }
-            else
-            {
-                superMetadataSet = _contentTypesHelper.getMetadataSetForView(metadataSetName, superTypeIds);
-            }
-        }
-        
-        _fillMetadataSetElement(metadataSet, metadataSetConfig, metadataSet, isEdition, superMetadataSet);
-        
-        return metadataSet;
-    }
-    
-    /**
-     * Parse a metadata set icon path.
-     * @param configuration the configuration to use.
-     * @param size the icon size (large, medium or small).
-     * @param metadataSetName the metadata set name.
-     * @return The icon path.
-     * @throws ConfigurationException if the configuration is not valid.
-     */
-    protected String _parseMetadataSetIcon(Configuration configuration, String size, String metadataSetName) throws ConfigurationException
-    {
-        String defaultMetadataSetName = "unknown";
-        
-        if ("main".equals(metadataSetName) || "abstract".equals(metadataSetName) || "link".equals(metadataSetName))
-        {
-            defaultMetadataSetName = metadataSetName;
-        }
-        
-        String defaultPath = "/plugins/cms/resources/img/contenttype/metadataset/default-" + defaultMetadataSetName + "-" + size + ".png";
-        
-        return _parseIcon(configuration, size, defaultPath);
-    }
-    
-    /**
-     * Parse the DublinCore  metadata set.
-     * @param metadataSetElement the metadata set element to fill.
-     * @throws ConfigurationException if the configuration is not valid.
-     */
-    protected void _fillMetadataSetDublinCore(AbstractMetadataSetElement metadataSetElement) throws ConfigurationException
-    {
-        MetadataDefinitionReference dcMetaDefRef = new MetadataDefinitionReference("dc");
-        
-        Source src = null;
-        InputStream is = null;
-        
-        try
-        {
-            src = _srcResolver.resolveURI("resource://org/ametys/cms/dublincore/dublincore.xml");
-            
-            if (src.exists())
-            {
-                is = src.getInputStream();
-                Configuration configuration = new DefaultConfigurationBuilder(true).build(is);
-                
-                Configuration metadataSetConf = configuration.getChild("metadata-set");
-                
-                for (Configuration childConfiguration : metadataSetConf.getChildren("metadata-ref"))
-                {
-                    dcMetaDefRef.addElement(new MetadataDefinitionReference(childConfiguration.getAttribute("name")));
-                }
-                
-                IOUtils.closeQuietly(is);
-            }
-        }
-        catch (IOException e)
-        {
-            throw new ConfigurationException("Unable to parse Dublin Core metadata set", e);
-        }
-        catch (SAXException e)
-        {
-            throw new ConfigurationException("Unable to parse Dublin Core metadata set", e);
-        }
-        finally
-        {
-            if (src != null)
-            {
-                _srcResolver.release(src);
-            }
-        }
-        
-        metadataSetElement.addElement(dcMetaDefRef);
-    }
-
-    /**
-     * Fill child elements for a metadata set element.
-     * @param rootMetadataSet The root metadata set
-     * @param metadataSetConfig the metadata set configuration to use.
-     * @param metadataSetElement the metadata set element to fill.
-     * @param isEdition true if it is the edition mode
-     * @param superMetadataSet the overridden metadata-set if applicable, null otherwise.
-     * @throws ConfigurationException if the configuration is not valid.
-     */
-    protected void _fillMetadataSetElement(MetadataSet rootMetadataSet, Configuration metadataSetConfig, AbstractMetadataSetElement metadataSetElement, boolean isEdition, MetadataSet superMetadataSet) throws ConfigurationException
-    {
-        for (Configuration elementConfig : metadataSetConfig.getChildren())
-        {
-            String elementConfigName = elementConfig.getName();
-            
-            if (elementConfigName.equals("metadata-ref"))
-            {
-                String metadataName = elementConfig.getAttribute("name");
-                String metadataSetName = elementConfig.getAttribute("metadata-set", null);
-                
-                MetadataDefinitionReference metadataDefRef = new MetadataDefinitionReference(metadataName, metadataSetName);
-                
-                if ("metadata-set".equals(metadataSetConfig.getName()) && rootMetadataSet.hasMetadataDefinitionReference(metadataName))
-                {
-                    throw new ConfigurationException("The metadata '" + metadataName + "' is already referenced by a super metadata-set or by the metadata-set '" + rootMetadataSet.getName() + "' itself.", elementConfig);
-                }
-                metadataSetElement.addElement(metadataDefRef);
-                
-                _fillMetadataSetElement(rootMetadataSet, elementConfig, metadataDefRef, isEdition, superMetadataSet);
-            }
-            else if (elementConfigName.equals("fieldset"))
-            {
-                Fieldset fieldset = new Fieldset();
-                fieldset.setRole(elementConfig.getAttribute("role", "tab"));
-                fieldset.setLabel(_parseI18nizableText(elementConfig, "label"));
-                
-                _fillMetadataSetElement(rootMetadataSet, elementConfig, fieldset, isEdition, superMetadataSet);
-                
-                metadataSetElement.addElement(fieldset);
-            }
-            else if (elementConfigName.equals("dublin-core"))
-            {
-                _fillMetadataSetDublinCore(metadataSetElement);
-            }
-            else if (elementConfigName.equals("include"))
-            {
-                String supertype = elementConfig.getAttribute("from-supertype");
-                if ("true".equals(supertype))
-                {
-                    if (superMetadataSet == null)
-                    {
-                        throw new ConfigurationException("The metadata-set element includes the super metadata-set without its content type referencing a super-type.", elementConfig);
-                    }
-                    
-                    _contentTypesHelper.copyMetadataSetElementsIfNotExist(superMetadataSet, metadataSetElement);
-                }
-                else
-                {
-                    ContentType superType = _cTypeEP.getExtension(supertype);
-                    if (superType == null)
-                    {
-                        throw new ConfigurationException("The metadata-set element includes the super metadata-set of an unknown super-type '" + supertype + "'.", elementConfig);
-                    }
-                    
-                    MetadataSet metadataSetToInclude = isEdition ? superType.getMetadataSetForEdition(rootMetadataSet.getName()) : superType.getMetadataSetForEdition(rootMetadataSet.getName());
-                    _contentTypesHelper.copyMetadataSetElementsIfNotExist(metadataSetToInclude, metadataSetElement);
-                }
-            }
-        }
-    }
-    
-    @Override
-    public String getId()
-    {
-        return _id;
-    }
-    
-    @Override
-    public String getPluginName()
-    {
-        return _pluginName;
-    }
-
-    @Override
-    public I18nizableText getLabel()
-    {
-        return _label;
-    }
-    
-    @Override
-    public I18nizableText getDescription()
-    {
-        return _description;
-    }
-    
-    @Override
-    public I18nizableText getDefaultTitle()
-    {
-        return _defaultTitle;
-    }
-    
-    @Override
-    public String[] getSupertypeIds()
-    {
-        return _superTypeIds;
-    }
-    
-    @Override
-    public I18nizableText getCategory()
-    {
-        return _category;
-    }
-
-    @Override
-    public String getSmallIcon()
-    {
-        return _smallIcon;
-    }
-    
-    @Override
-    public String getMediumIcon()
-    {
-        return _mediumIcon;
-    }
-    
-    @Override
-    public String getLargeIcon()
-    {
-        return _largeIcon;
-    }
-    
     @Override
     public Validator getGlobalValidator()
     {
@@ -1085,41 +563,23 @@
     }
     
     @Override
-    public Set<String> getEditionMetadataSetNames(boolean includeInternal)
-    {
-        if (includeInternal)
-        {
-            return Collections.unmodifiableSet(_allMetadataSetsForEdition.keySet());
-        }
-        else
-        {
-            return Collections.unmodifiableSet(_metadataSetsForEdition.keySet());
-        }
-    }
-    
-    @Override
-    public Set<String> getViewMetadataSetNames(boolean includeInternal)
+    public MetadataDefinition getMetadataDefinitionByPath (String metadataPath)
     {
-        if (includeInternal)
+        String[] pathSegments = metadataPath.split("/+");
+        
+        if (pathSegments.length == 0)
         {
-            return Collections.unmodifiableSet(_allMetadataSetsForView.keySet());
+            return null;
         }
-        else
+        
+        MetadataDefinition metadataDef = _metadata.get(pathSegments[0]);
+
+        for (int i = 1;  i < pathSegments.length && metadataDef != null; i++)
         {
-            return Collections.unmodifiableSet(_metadataSetsForView.keySet());
+            metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]);
         }
-    }
 
-    @Override
-    public MetadataSet getMetadataSetForView(String metadataSetName)
-    {
-        return _allMetadataSetsForView.get(metadataSetName);
-    }
-
-    @Override
-    public MetadataSet getMetadataSetForEdition(String metadataSetName)
-    {
-        return _allMetadataSetsForEdition.get(metadataSetName);
+        return metadataDef;
     }
     
     @Override
@@ -1223,6 +683,12 @@
     }
     
     @Override
+    public boolean isMixin()
+    {
+        return hasTag(TAG_MIXIN);
+    }
+    
+    @Override
     public String getRight()
     {
         return _right;
@@ -1235,26 +701,6 @@
         
     }
     
-    @Override
-    public MetadataDefinition getMetadataDefinitionByPath(String metadataPath) 
-    { 
-        String[] pathSegments = metadataPath.split("/+"); 
-         
-        if (pathSegments.length == 0) 
-        { 
-            return null; 
-        } 
-         
-        MetadataDefinition metadataDef = _metadata.get(pathSegments[0]); 
- 
-        for (int i = 1;  i < pathSegments.length && metadataDef != null; i++) 
-        { 
-            metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]); 
-        } 
- 
-        return metadataDef; 
-    }
-    
     /**
      * Retrieves the restrictions for a given path.
      * @param metadataPath the metadata path.
@@ -1262,19 +708,7 @@
      */
     protected Restrictions _getRestrictionsForPath(String metadataPath)
     {
-        String[] pathSegments = metadataPath.split("/+");
-        
-        if (pathSegments.length == 0)
-        {
-            return null;
-        }
-        
-        MetadataDefinition metadataDef = _metadata.get(pathSegments[0]);
-
-        for (int i = 1;  i < pathSegments.length && metadataDef != null; i++)
-        {
-            metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]);
-        }
+        MetadataDefinition metadataDef = getMetadataDefinitionByPath (metadataPath);
 
         if (metadataDef != null && metadataDef instanceof RestrictedDefinition)
         {
@@ -1539,7 +973,6 @@
             super._additionalParsing(manager, pluginName, metadataConfiguration, metadataId, metadataDefinition);
             String metadataName = metadataConfiguration.getAttribute("name");
             
-            metadataDefinition.setReferenceContentType(_id);
             metadataDefinition.setName(metadataName);
             metadataDefinition.setMultiple(metadataConfiguration.getAttributeAsBoolean("multiple", false));
             // Use default transformer (docbook)
Index: main/plugin-cms/src/org/ametys/cms/contenttype/ContentType.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/ContentType.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/ContentType.java	(working copy)
@@ -22,13 +22,12 @@
 
 import org.ametys.cms.repository.Content;
 import org.ametys.plugins.repository.AmetysRepositoryException;
-import org.ametys.runtime.util.I18nizableText;
 import org.ametys.runtime.util.parameter.Validator;
 
 /**
  * This class represents a type of content.
  */
-public interface ContentType
+public interface ContentType extends ContentTypeDescriptor
 {
     /** Tag for private content type */
     public static final String TAG_PRIVATE = "private";
@@ -36,67 +35,10 @@
     /** Tag for simple content type */
     public static final String TAG_SIMPLE = "simple";
     
-    /**
-     * Retrieves the id of the content type.
-     * @return the id.
-     */
-    String getId();
-    
-    /**
-     * Retrieves the name of the plugin declaring this content type.
-     * @return the name of the plugin.
-     */
-    String getPluginName();
-    
-    /**
-     * Retrieves the label of the content type.
-     * @return the label.
-     */
-    I18nizableText getLabel();
-    
-    /**
-     * Retrieves the description of the content type.
-     * @return the description.
-     */
-    I18nizableText getDescription();
-    
-    /**
-     * Retrieves the default title of the content type.
-     * @return the default title.
-     */
-    I18nizableText getDefaultTitle();
-    
-    /**
-     * Retrieves the category of the content type.
-     * @return the category.
-     */
-    I18nizableText getCategory();
-
-    /**
-     * Retrieves the super type's ids.
-     * @return the super type's ids, or empty if this content type doesn't extend a specific content type.
-     */
-    String[] getSupertypeIds();
+    /** Tag for simple content type */
+    public static final String TAG_MIXIN = "mixin";
     
     /**
-     * Retrieves the URL of the icon without the context path.
-     * @return the icon URL for the small image 16x16.
-     */
-    String getSmallIcon();
-
-    /**
-     * Retrieves the URL of the icon without the context path.
-     * @return the icon URL for the medium sized image 32x32.
-     */
-    String getMediumIcon();
-
-    /**
-     * Retrieves the URL of the icon without the context path.
-     * @return the icon URL for the large image 48x48.
-     */
-    String getLargeIcon();
-
-    /**
      * Retrieves the potential global validator.
      * @return the global validator or <code>null</code> if none.
      */
@@ -127,9 +69,9 @@
     Set<String> getMetadataNames();
     
     /**
-     * Retrieves the definition of a given metadata.
+     * Retrieves the definition of a given metadata by its name
      * @param metadataName the metadata name.
-     * @return the metadata definition.
+     * @return the metadata definition or <code>null</code> if not found
      */
     MetadataDefinition getMetadataDefinition(String metadataName);
     
@@ -148,34 +90,6 @@
     boolean hasMetadataDefinition(String metadataName);
     
     /**
-     * Returns all names of "view" metadataSets.
-     * @param includeInternal if the result should include internal metadataSets.
-     * @return all names of "view" metadataSets.
-     */
-    Set<String> getViewMetadataSetNames(boolean includeInternal);
-    
-    /**
-     * Returns all names of "edition" metadataSets.
-     * @param includeInternal if the result should include internal metadataSets.
-     * @return all names of "edition" metadataSets.
-     */
-    Set<String> getEditionMetadataSetNames(boolean includeInternal);
-    
-    /**
-     * Retrieves the metadata set name for view.
-     * @param metadataSetName the metadata set name.
-     * @return the metadata definition.
-     */
-    MetadataSet getMetadataSetForView(String metadataSetName);
-    
-    /**
-     * Retrieves the metadata set name for edition.
-     * @param metadataSetName the metadata set name.
-     * @return the metadata set.
-     */
-    MetadataSet getMetadataSetForEdition(String metadataSetName);
-    
-    /**
      * Determine whether a metadata can be read at this time.
      * @param metadataPath the path to the given metadata.
      * @param content The content where metadata is to be read on.
@@ -230,6 +144,12 @@
     public boolean isAbstract();
     
     /**
+     * Get whether the content type is a mixin, i.e. should used to add metadata to a existing content.
+     * @return true if the content is mixin, false otherwise.
+     */
+    public boolean isMixin();
+    
+    /**
      * Get the right needed to create a content of this type.
      * @return the right needed to create a content of this type. If null is returned, no right is needed.
      */
Index: main/plugin-cms/src/org/ametys/cms/contenttype/AbstractContentTypeDescriptor.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/contenttype/AbstractContentTypeDescriptor.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/contenttype/AbstractContentTypeDescriptor.java	(revision 0)
@@ -0,0 +1,658 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.contenttype;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.avalon.framework.configuration.Configurable;
+import org.apache.avalon.framework.configuration.Configuration;
+import org.apache.avalon.framework.configuration.ConfigurationException;
+import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
+import org.apache.avalon.framework.logger.AbstractLogEnabled;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.avalon.framework.service.Serviceable;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.excalibur.source.Source;
+import org.apache.excalibur.source.SourceResolver;
+import org.xml.sax.SAXException;
+
+import org.ametys.plugins.explorer.dublincore.DublinCoreMetadataProvider;
+import org.ametys.runtime.plugin.component.PluginAware;
+import org.ametys.runtime.util.I18nizableText;
+
+/**
+ * This abstract class represents a content type descriptor
+ */
+public abstract class AbstractContentTypeDescriptor extends AbstractLogEnabled implements ContentTypeDescriptor, Configurable, PluginAware, Serviceable
+{
+    /** Plugin name. */
+    protected String _pluginName;
+    
+    /** Content type id. */
+    protected String _id;
+    /** Label. */
+    protected I18nizableText _label;
+    /** Description. */
+    protected I18nizableText _description;
+    /** Default title. */
+    protected I18nizableText _defaultTitle;
+    /** Category. */
+    protected I18nizableText _category;
+    /** Small icon URI 16x16. */
+    protected String _smallIcon;
+    /** Medium icon URI 32x32. */
+    protected String _mediumIcon;
+    /** Large icon URI 48x48. */
+    protected String _largeIcon;
+    
+    /** This content-type's supertype ids (can be empty). */
+    protected String[] _superTypeIds;
+    /** All metadata sets for view. */
+    protected Map<String, MetadataSet> _allMetadataSetsForView = new LinkedHashMap<String, MetadataSet>();
+    /** Non-internal metadata sets for view. */
+    protected Map<String, MetadataSet> _metadataSetsForView = new LinkedHashMap<String, MetadataSet>();
+    /** All metadata sets for edition. */
+    protected Map<String, MetadataSet> _allMetadataSetsForEdition = new LinkedHashMap<String, MetadataSet>();
+    /** Non-internal metadata sets for edition. */
+    protected Map<String, MetadataSet> _metadataSetsForEdition = new LinkedHashMap<String, MetadataSet>();
+    
+    /** The content type extension point. */
+    protected ContentTypeExtensionPoint _cTypeEP;
+    /** The content types helper */
+    protected ContentTypesHelper _contentTypesHelper;
+    /** The source resolver */
+    protected SourceResolver _srcResolver;
+    /** The DublinCore metadata provider */
+    protected DublinCoreMetadataProvider _dcProvider;
+    
+    @Override
+    public void service(ServiceManager smanager) throws ServiceException
+    {
+        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
+        _srcResolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE);
+        _dcProvider = (DublinCoreMetadataProvider) smanager.lookup(DublinCoreMetadataProvider.ROLE);
+        _cTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
+    }
+    
+    public void setPluginInfo(String pluginName, String featureName)
+    {
+        _pluginName = pluginName;
+    }
+    
+    /**
+     * Get the root configuration
+     * @param configuration The configuration 
+     * @return The main configuration
+     */
+    protected abstract Configuration getRootConfiguration (Configuration configuration);
+    
+    /**
+     * Get the overriden configuration
+     * @return the overriden configuration or null
+     * @throws ConfigurationException
+     */
+    protected abstract Configuration getOverridenConfiguration() throws ConfigurationException;
+    
+    @Override
+    public void configure(Configuration configuration) throws ConfigurationException
+    {
+        _id = configuration.getAttribute("id");
+        
+        Configuration rootConfiguration = getRootConfiguration(configuration);
+        _configureSuperTypes(rootConfiguration);
+        
+        _configureLabels(rootConfiguration);
+        _configureIcons(rootConfiguration);
+        
+        // Metadata sets
+        _configureMetadataSets (rootConfiguration);
+    }
+    
+    /**
+     * Configures the super types
+     * @param mainConfig The main configuration
+     */
+    protected void _configureSuperTypes (Configuration mainConfig)
+    {
+        String extendedCTypes = mainConfig.getAttribute("extends", null);
+        List<String> superTypeIds = new ArrayList<String>();
+        if (extendedCTypes != null)
+        {
+            String[] superIds = extendedCTypes.split(",");
+            for (String id : superIds)
+            {
+                superTypeIds.add(StringUtils.trim(id));
+            }
+        }
+        
+        _superTypeIds = superTypeIds.toArray(new String[superTypeIds.size()]);
+    }
+    
+    /**
+     * Configures the labels
+     * @param mainConfig The main configuration
+     * @throws ConfigurationException
+     */
+    protected void _configureLabels (Configuration mainConfig)  throws ConfigurationException
+    {
+        _label = _parseI18nizableText(mainConfig, "label");
+        _description = _parseI18nizableText(mainConfig, "description");
+        _defaultTitle = _parseI18nizableText(mainConfig, "default-title");
+        _category = _parseI18nizableText(mainConfig, "category");
+    }
+    
+    /**
+     * Configures the icons
+     * @param mainConfig The main configuration
+     * @throws ConfigurationException
+     */
+    protected void _configureIcons (Configuration mainConfig)  throws ConfigurationException
+    {
+        _smallIcon = _parseIcon(mainConfig.getChild("icons"), "small");
+        _mediumIcon = _parseIcon(mainConfig.getChild("icons"), "medium");
+        _largeIcon = _parseIcon(mainConfig.getChild("icons"), "large");
+    }
+
+    /**
+     * Configure metadata sets
+     * @param mainConfig The content type configuration
+     * @throws ConfigurationException
+     */
+    protected void _configureMetadataSets (Configuration mainConfig) throws ConfigurationException
+    {
+        // First, fill metadata-set from super types
+        _allMetadataSetsForView.putAll(_contentTypesHelper.getMetadataSetsForView(_superTypeIds, new String[0], true));
+        _allMetadataSetsForEdition.putAll(_contentTypesHelper.getMetadataSetsForEdition(_superTypeIds, new String[0], true));
+        _metadataSetsForView.putAll(_contentTypesHelper.getMetadataSetsForView(_superTypeIds, new String[0], false));
+        _metadataSetsForEdition.putAll(_contentTypesHelper.getMetadataSetsForEdition(_superTypeIds, new String[0], false));
+        
+        Map<String, Configuration> metadataSetViewConfs = new LinkedHashMap<String, Configuration>();
+        Map<String, Configuration> metadataSetEditConfs = new LinkedHashMap<String, Configuration>();
+        
+        _getApplicableMetadataSets(mainConfig, metadataSetViewConfs, metadataSetEditConfs, false);
+        
+        Configuration overriddenConfig = getOverridenConfiguration();
+        if (overriddenConfig != null)
+        {
+            _getApplicableMetadataSets(overriddenConfig, metadataSetViewConfs, metadataSetEditConfs, true);
+        }
+        
+        // Then parse own metadata sets
+        _parseMetadataSets(metadataSetViewConfs, _metadataSetsForView, _allMetadataSetsForView, _superTypeIds);
+        _parseMetadataSets(metadataSetEditConfs, _metadataSetsForEdition, _allMetadataSetsForEdition, _superTypeIds);
+    }
+    
+    /**
+     * Compute the applicable metadata-sets from their configurations.
+     * @param config The content type configuration
+     * @param metadataSetViewConfs the view metadata-set configurations, indexed by name.
+     * @param metadataSetEditConfs the edition metadata-set configurations, indexed by name.
+     * @param allowOverride if true, encountering a metadata-set which has already been declared (based on its name) will replace it. Otherwise, an exception will be thrown. 
+     * @throws ConfigurationException if the configuration is invalid
+     */
+    protected void _getApplicableMetadataSets(Configuration config, Map<String, Configuration> metadataSetViewConfs, Map<String, Configuration> metadataSetEditConfs, boolean allowOverride) throws ConfigurationException
+    {
+        for (Configuration metadataSetConfig : config.getChildren("metadata-set"))
+        {
+            String name = metadataSetConfig.getAttribute("name");
+            String type = metadataSetConfig.getAttribute("type");
+            
+            if (type.equals("view"))
+            {
+                if (!allowOverride && metadataSetViewConfs.containsKey(name))
+                {
+                    throw new ConfigurationException("The view metadata-set '" + name + "' is already defined.", metadataSetConfig);
+                }
+                metadataSetViewConfs.put(name, metadataSetConfig);
+            }
+            else if (type.equals("edition"))
+            {
+                if (!allowOverride && metadataSetEditConfs.containsKey(name))
+                {
+                    throw new ConfigurationException("The edition metadata-set '" + name + "' is already defined.", metadataSetConfig);
+                }
+                metadataSetEditConfs.put(name, metadataSetConfig);
+            }
+            else
+            {
+                throw new ConfigurationException("Invalid type '" + type + "' for metadata set '" + name + "'", metadataSetConfig);
+            }
+        }
+    }
+    
+    /**
+     * Parse the metadata-set configurations
+     * @param metadataSetConfs the metadata-set configurations, indexed by name.
+     * @param metadataSets the Map of "public" {@link MetadataSet} objects to fill, indexed by name
+     * @param allMetadataSets the Map of {@link MetadataSet} objects to fill (including internal ones), indexed by name.
+     * @param superTypeIds the super content-types
+     * @throws ConfigurationException if the configuration is invalid
+     */
+    protected void _parseMetadataSets(Map<String, Configuration> metadataSetConfs, Map<String, MetadataSet> metadataSets, Map<String, MetadataSet> allMetadataSets, String[] superTypeIds) throws ConfigurationException
+    {
+        for (Configuration metadataSetConfig : metadataSetConfs.values())
+        {
+            String name = metadataSetConfig.getAttribute("name");
+            String type = metadataSetConfig.getAttribute("type");
+            boolean isInternal = metadataSetConfig.getAttributeAsBoolean("internal", false);
+            boolean isEdition = type.equals("edition");
+            
+            MetadataSet metadataSet = _parseMetadataSet(metadataSetConfig, name, isEdition, isInternal, superTypeIds);
+            
+            allMetadataSets.put(name, metadataSet);
+            
+            if (!isInternal)
+            {
+                metadataSets.put(name, metadataSet);
+            }
+        }
+    }
+    
+    /**
+     * Parse a metadata-set configuration to create a {@link MetadataSet} object.
+     * @param metadataSetConfig the metadata set configuration to use.
+     * @param metadataSetName the metadata set name.
+     * @param isEdition the metadata set edition status.
+     * @param isInternal the metadata internal status.
+     * @param superTypeIds the super types.
+     * @return the metadata set
+     * @throws ConfigurationException if the configuration is not valid.
+     */
+    protected MetadataSet _parseMetadataSet(Configuration metadataSetConfig, String metadataSetName, boolean isEdition, boolean isInternal, String[] superTypeIds) throws ConfigurationException
+    {
+        MetadataSet metadataSet = new MetadataSet();
+        
+        I18nizableText label = _parseI18nizableText(metadataSetConfig, "label", metadataSetName);
+        I18nizableText description = _parseI18nizableText(metadataSetConfig, "description");
+        
+        Configuration iconConf = metadataSetConfig.getChild("icons");
+        
+        String smallIcon = _parseMetadataSetIcon(iconConf, "small", metadataSetName);
+        String mediumIcon = _parseMetadataSetIcon(iconConf, "medium", metadataSetName);
+        String largeIcon = _parseMetadataSetIcon(iconConf, "large", metadataSetName);
+        
+        metadataSet.setName(metadataSetName);
+        metadataSet.setLabel(label);
+        metadataSet.setDescription(description);
+        metadataSet.setSmallIcon(smallIcon);
+        metadataSet.setMediumIcon(mediumIcon);
+        metadataSet.setLargeIcon(largeIcon);
+        metadataSet.setEdition(isEdition);
+        
+        MetadataSet superMetadataSet = null;
+        if (superTypeIds.length > 0)
+        {
+            if (isEdition)
+            {
+                superMetadataSet = _contentTypesHelper.getMetadataSetForEdition(metadataSetName, superTypeIds, new String[0]);
+            }
+            else
+            {
+                superMetadataSet = _contentTypesHelper.getMetadataSetForView(metadataSetName, superTypeIds, new String[0]);
+            }
+        }
+        
+        _fillMetadataSetElement(metadataSet, metadataSetConfig, metadataSet, isEdition, superMetadataSet);
+        
+        return metadataSet;
+    }
+    
+    /**
+     * Parse a metadata set icon path.
+     * @param configuration the configuration to use.
+     * @param size the icon size (large, medium or small).
+     * @param metadataSetName the metadata set name.
+     * @return The icon path.
+     * @throws ConfigurationException if the configuration is not valid.
+     */
+    protected String _parseMetadataSetIcon(Configuration configuration, String size, String metadataSetName) throws ConfigurationException
+    {
+        String defaultMetadataSetName = "unknown";
+        
+        if ("main".equals(metadataSetName) || "abstract".equals(metadataSetName) || "link".equals(metadataSetName))
+        {
+            defaultMetadataSetName = metadataSetName;
+        }
+        
+        String defaultPath = "/plugins/cms/resources/img/contenttype/metadataset/default-" + defaultMetadataSetName + "-" + size + ".png";
+        
+        return _parseIcon(configuration, size, defaultPath);
+    }
+    
+    
+    /**
+     * Fill child elements for a metadata set element.
+     * @param rootMetadataSet The root metadata set
+     * @param metadataSetConfig the metadata set configuration to use.
+     * @param metadataSetElement the metadata set element to fill.
+     * @param isEdition true if it is the edition mode
+     * @param superMetadataSet the overridden metadata-set if applicable, null otherwise.
+     * @throws ConfigurationException if the configuration is not valid.
+     */
+    protected void _fillMetadataSetElement(MetadataSet rootMetadataSet, Configuration metadataSetConfig, AbstractMetadataSetElement metadataSetElement, boolean isEdition, MetadataSet superMetadataSet) throws ConfigurationException
+    {
+        for (Configuration elementConfig : metadataSetConfig.getChildren())
+        {
+            String elementConfigName = elementConfig.getName();
+            
+            if (elementConfigName.equals("metadata-ref"))
+            {
+                String metadataName = elementConfig.getAttribute("name");
+                String metadataSetName = elementConfig.getAttribute("metadata-set", null);
+                
+                MetadataDefinitionReference metadataDefRef = new MetadataDefinitionReference(metadataName, metadataSetName);
+                
+                if ("metadata-set".equals(metadataSetConfig.getName()) && rootMetadataSet.hasMetadataDefinitionReference(metadataName))
+                {
+                    throw new ConfigurationException("The metadata '" + metadataName + "' is already referenced by a super metadata-set or by the metadata-set '" + rootMetadataSet.getName() + "' itself.", elementConfig);
+                }
+                metadataSetElement.addElement(metadataDefRef);
+                
+                _fillMetadataSetElement(rootMetadataSet, elementConfig, metadataDefRef, isEdition, superMetadataSet);
+            }
+            else if (elementConfigName.equals("fieldset"))
+            {
+                Fieldset fieldset = new Fieldset();
+                fieldset.setRole(elementConfig.getAttribute("role", "tab"));
+                fieldset.setLabel(_parseI18nizableText(elementConfig, "label"));
+                
+                _fillMetadataSetElement(rootMetadataSet, elementConfig, fieldset, isEdition, superMetadataSet);
+                
+                metadataSetElement.addElement(fieldset);
+            }
+            else if (elementConfigName.equals("dublin-core"))
+            {
+                _fillMetadataSetDublinCore(metadataSetElement);
+            }
+            else if (elementConfigName.equals("include"))
+            {
+                String supertype = elementConfig.getAttribute("from-supertype");
+                if ("true".equals(supertype))
+                {
+                    if (superMetadataSet == null)
+                    {
+                        throw new ConfigurationException("The metadata-set element includes the super metadata-set without its content type referencing a super-type.", elementConfig);
+                    }
+                    
+                    _contentTypesHelper.copyMetadataSetElementsIfNotExist(superMetadataSet, metadataSetElement);
+                }
+                else
+                {
+                    ContentType superType = _cTypeEP.getExtension(supertype);
+                    if (superType == null)
+                    {
+                        throw new ConfigurationException("The metadata-set element includes the super metadata-set of an unknown super-type '" + supertype + "'.", elementConfig);
+                    }
+                    
+                    MetadataSet metadataSetToInclude = isEdition ? superType.getMetadataSetForEdition(rootMetadataSet.getName()) : superType.getMetadataSetForEdition(rootMetadataSet.getName());
+                    _contentTypesHelper.copyMetadataSetElementsIfNotExist(metadataSetToInclude, metadataSetElement);
+                }
+            }
+        }
+    }
+    
+    /**
+     * Parse the DublinCore  metadata set.
+     * @param metadataSetElement the metadata set element to fill.
+     * @throws ConfigurationException if the configuration is not valid.
+     */
+    protected void _fillMetadataSetDublinCore(AbstractMetadataSetElement metadataSetElement) throws ConfigurationException
+    {
+        MetadataDefinitionReference dcMetaDefRef = new MetadataDefinitionReference("dc");
+        
+        Source src = null;
+        InputStream is = null;
+        
+        try
+        {
+            src = _srcResolver.resolveURI("resource://org/ametys/cms/dublincore/dublincore.xml");
+            
+            if (src.exists())
+            {
+                is = src.getInputStream();
+                Configuration configuration = new DefaultConfigurationBuilder(true).build(is);
+                
+                Configuration metadataSetConf = configuration.getChild("metadata-set");
+                
+                for (Configuration childConfiguration : metadataSetConf.getChildren("metadata-ref"))
+                {
+                    dcMetaDefRef.addElement(new MetadataDefinitionReference(childConfiguration.getAttribute("name")));
+                }
+                
+                IOUtils.closeQuietly(is);
+            }
+        }
+        catch (IOException e)
+        {
+            throw new ConfigurationException("Unable to parse Dublin Core metadata set", e);
+        }
+        catch (SAXException e)
+        {
+            throw new ConfigurationException("Unable to parse Dublin Core metadata set", e);
+        }
+        finally
+        {
+            if (src != null)
+            {
+                _srcResolver.release(src);
+            }
+        }
+        
+        metadataSetElement.addElement(dcMetaDefRef);
+    }
+    /**
+     * Parse an i18n text.
+     * @param config the configuration to use.
+     * @param name the child name.
+     * @return the i18n text.
+     * @throws ConfigurationException if the configuration is not valid.
+     */
+    protected I18nizableText _parseI18nizableText(Configuration config, String name) throws ConfigurationException
+    {
+        return _parseI18nizableText(config, name, "");
+    }
+    
+    /**
+     * Parse an i18n text.
+     * @param config the configuration to use.
+     * @param name the child name.
+     * @param defaultValue the default value if no present
+     * @return the i18n text.
+     * @throws ConfigurationException if the configuration is not valid.
+     */
+    protected I18nizableText _parseI18nizableText(Configuration config, String name, String defaultValue) throws ConfigurationException
+    {
+        Configuration textConfig = config.getChild(name);
+        boolean i18nSupported = textConfig.getAttributeAsBoolean("i18n", false);
+        String text = textConfig.getValue(defaultValue);
+        
+        if (i18nSupported)
+        {
+            String catalogue = textConfig.getAttribute("catalogue", null);
+            
+            if (catalogue == null)
+            {
+                catalogue = _getDefaultCatalogue();
+            }
+            
+            return new I18nizableText(catalogue, text);
+        }
+        else
+        {
+            return new I18nizableText(text);
+        }
+    }
+    
+    /**
+     * Returns the default i18n catalogue for this content type.
+     * @return the default i18n catalogue for this content type.
+     */
+    protected String _getDefaultCatalogue()
+    {
+        return "plugin." + _pluginName;
+    }
+    
+    /**
+     * Parse an icon path
+     * @param configuration the configuration to use
+     * @param name the child name.
+     * @return The icon path
+     * @throws ConfigurationException if the configuration is not valid.
+     */
+    protected String _parseIcon (Configuration configuration, String name) throws ConfigurationException
+    {
+        return _parseIcon(configuration, name, "/plugins/cms/resources/img/contenttype/unknown-" + name + ".png");
+    }
+    
+    /**
+     * Parse an icon path
+     * @param configuration the configuration to use
+     * @param name the child name.
+     * @param defaultValue the default value.
+     * @return The icon path
+     * @throws ConfigurationException if the configuration is not valid.
+     */
+    protected String _parseIcon(Configuration configuration, String name, String defaultValue) throws ConfigurationException
+    {
+        Configuration iconConfig = configuration.getChild(name, false);
+        if (iconConfig != null)
+        {
+            String pluginName = iconConfig.getAttribute("plugin", _pluginName);
+            return _getIconPath(pluginName) + iconConfig.getValue();
+        }
+        
+        return defaultValue;
+    }
+    
+    /**
+     * Returns the path for icons
+     * @param pluginName the configured plugin
+     * @return the path for icons
+     */
+    protected String _getIconPath(String pluginName)
+    {
+        return "/plugins/" + pluginName + "/resources/";
+    }
+
+    @Override
+    public String getPluginName()
+    {
+        return _pluginName;
+    }
+
+    @Override
+    public String getId()
+    {
+        return _id;
+    }
+    
+    @Override
+    public I18nizableText getLabel()
+    {
+        return _label;
+    }
+    
+    @Override
+    public I18nizableText getDescription()
+    {
+        return _description;
+    }
+    
+    @Override
+    public I18nizableText getCategory()
+    {
+        return _category;
+    }
+
+    @Override
+    public I18nizableText getDefaultTitle()
+    {
+        return _defaultTitle;
+    }
+    
+    @Override
+    public String getSmallIcon()
+    {
+        return _smallIcon;
+    }
+    
+    @Override
+    public String getMediumIcon()
+    {
+        return _mediumIcon;
+    }
+    
+    @Override
+    public String getLargeIcon()
+    {
+        return _largeIcon;
+    }
+    
+    @Override
+    public String[] getSupertypeIds()
+    {
+        return _superTypeIds;
+    }
+    
+    @Override
+    public Set<String> getEditionMetadataSetNames(boolean includeInternal)
+    {
+        if (includeInternal)
+        {
+            return Collections.unmodifiableSet(_allMetadataSetsForEdition.keySet());
+        }
+        else
+        {
+            return Collections.unmodifiableSet(_metadataSetsForEdition.keySet());
+        }
+    }
+    
+    @Override
+    public Set<String> getViewMetadataSetNames(boolean includeInternal)
+    {
+        if (includeInternal)
+        {
+            return Collections.unmodifiableSet(_allMetadataSetsForView.keySet());
+        }
+        else
+        {
+            return Collections.unmodifiableSet(_metadataSetsForView.keySet());
+        }
+    }
+
+    @Override
+    public MetadataSet getMetadataSetForView(String metadataSetName)
+    {
+        return _allMetadataSetsForView.get(metadataSetName);
+    }
+
+    @Override
+    public MetadataSet getMetadataSetForEdition(String metadataSetName)
+    {
+        return _allMetadataSetsForEdition.get(metadataSetName);
+    }
+    
+}
+
Index: main/plugin-cms/src/org/ametys/cms/content/ConsistencyGenerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/ConsistencyGenerator.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/ConsistencyGenerator.java	(working copy)
@@ -27,6 +27,7 @@
 import org.apache.cocoon.generation.ServiceableGenerator;
 import org.apache.cocoon.xml.AttributesImpl;
 import org.apache.cocoon.xml.XMLUtils;
+import org.apache.commons.lang.StringUtils;
 import org.xml.sax.SAXException;
 
 import org.ametys.cms.repository.Content;
@@ -74,7 +75,7 @@
                 attrs.addCDATAAttribute("title", content.getTitle());
                 attrs.addCDATAAttribute("name", content.getName());
                 attrs.addCDATAAttribute("path", content.getPath());
-                attrs.addCDATAAttribute("type", content.getType()); // CMS-5261
+                attrs.addCDATAAttribute("type", StringUtils.join(content.getTypes(), ','));
                 attrs.addCDATAAttribute("lang", content.getLanguage());
 
                 XMLUtils.startElement(contentHandler, "content", attrs);
Index: main/plugin-cms/src/org/ametys/cms/content/CopyContentMetadataComponent.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/CopyContentMetadataComponent.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/CopyContentMetadataComponent.java	(working copy)
@@ -22,6 +22,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Properties;
 
 import javax.jcr.Node;
@@ -59,6 +60,7 @@
 import org.ametys.cms.contenttype.ConsistencyExtractor;
 import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.contenttype.MetadataDefinition;
 import org.ametys.cms.contenttype.MetadataDefinitionReference;
 import org.ametys.cms.contenttype.MetadataSet;
@@ -160,6 +162,9 @@
     /** Content type extension point. */
     protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
     
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
+    
     @Override
     public void service(ServiceManager manager) throws ServiceException
     {
@@ -167,6 +172,7 @@
         _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
         _saxParser = (SAXParser) manager.lookup(SAXParser.ROLE);
         _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
     }
     
     /**
@@ -278,14 +284,17 @@
      */
     protected CopyReport _copyContent(Content baseContent, String title, Map<String, Object> copyMap, String metadataSetName, String metadataSetType, String targetContentId, String parentMetadataPath)
     {
-        ContentType contentType = null;
         String baseContentId = baseContent.getId();
         CopyReport report = new CopyReport(baseContentId, baseContent.getTitle(), true, metadataSetName, metadataSetType, CopyMode.CREATION);
         try
         {
-            contentType = _contentTypeExtensionPoint.getExtension(baseContent.getType());
+            boolean simple = false;
+            for (String cTypeId : baseContent.getTypes())
+            {
+                simple = simple || _contentTypeExtensionPoint.getExtension(cTypeId).isSimple();
+            }
             
-            report.setSimple(contentType.isSimple());
+            report.setSimple(simple);
             
             Map<String, Object> internalCopyMap = copyMap;
             if (internalCopyMap == null)
@@ -393,8 +402,7 @@
      */
     protected Map<String, Object> _buildCopyMap(Content baseContent, String metadataSetName, String metadataSetType)
     {
-        ContentType contentType = _contentTypeExtensionPoint.getExtension(baseContent.getType());
-        MetadataSet metadataSet = "view".equals(metadataSetType) ? contentType.getMetadataSetForView(metadataSetName) : contentType.getMetadataSetForEdition(metadataSetName);
+        MetadataSet metadataSet = "view".equals(metadataSetType) ? _contentTypesHelper.getMetadataSetForView(metadataSetName, baseContent.getTypes(), baseContent.getMixinTypes()) : _contentTypesHelper.getMetadataSetForEdition(metadataSetName, baseContent.getTypes(), baseContent.getMixinTypes());
         return _buildCopyMap(metadataSet, null);
     }
     
@@ -449,14 +457,11 @@
     public CopyReport editContent(String baseContentId, String targetContentId, Map<String, Object> copyMap, String metadataSetName, String metadataSetType)
     {
         Content baseContent = null;
-        ContentType contentType = null;
         CopyReport report = null;
         try
         {
             baseContent = _resolver.resolveById(baseContentId);
-            contentType = _contentTypeExtensionPoint.getExtension(baseContent.getType());
-            
-            report = new CopyReport(baseContentId, baseContent.getTitle(), contentType.isSimple(), metadataSetName, metadataSetType, CopyMode.EDITION);
+            report = new CopyReport(baseContentId, baseContent.getTitle(), _isSimple(baseContent), metadataSetName, metadataSetType, CopyMode.EDITION);
             
             Map<String, Object> internalCopyMap = copyMap;
             if (internalCopyMap == null)
@@ -499,13 +504,7 @@
             }
             else
             {
-                boolean isSimple = true;
-                if (contentType != null)
-                {
-                    isSimple = contentType.isSimple();
-                }
-                
-                report = new CopyReport(baseContentId, isSimple, metadataSetName, metadataSetType, CopyMode.EDITION);
+                report = new CopyReport(baseContentId, _isSimple(baseContent), metadataSetName, metadataSetType, CopyMode.EDITION);
                 report.notifyContentCopyError();
             }
         }
@@ -513,6 +512,18 @@
         return report;
     }
     
+    private boolean _isSimple (Content content)
+    {
+        for (String cTypeId : content.getTypes())
+        {
+            ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
+            if (!cType.isSimple())
+            {
+                return false;
+            }
+        }
+        return true;
+    }
     /**
      * Retrieve the target content from its id.
      * Also ensure that it is a workflow aware content.
@@ -574,26 +585,26 @@
     
     /**
      * Copy the specified set of metadata from a base content to a target content by iterating over the copyMap.
-     * @param baseContent
+     * @param baseContent The original content
      * @param targetContent
      * @param copyMap
      * @param copyReport The copy report being populated during the copy.
      */
     public void copyMetadataMap(Content baseContent, ModifiableContent targetContent, Map<String, Object> copyMap, CopyReport copyReport)
     {
-        ContentType contentType = _contentTypeExtensionPoint.getExtension(baseContent.getType());
-        copyReport.notifyContentCreation(targetContent.getId(), targetContent.getTitle(), contentType.isSimple());
+        copyReport.notifyContentCreation(targetContent.getId(), targetContent.getTitle(), _isSimple(baseContent));
         
-        _copyMetadataMap(baseContent.getMetadataHolder(), targetContent.getMetadataHolder(), "", contentType, copyMap, copyReport);
+        _copyMetadataMap(baseContent, baseContent.getMetadataHolder(), targetContent.getMetadataHolder(), "", null, copyMap, copyReport);
         
         copyReport.setTargetContentTitle(targetContent.getTitle());
         
-        _updateRichTextMetadata(contentType, baseContent, targetContent, copyReport);
+        _updateRichTextMetadata(baseContent, targetContent, copyReport);
         _buildConsistencies(targetContent);
     }
     
     /**
      * Copy the specified set of metadata from a base composite metadata to a target composite metadata by iterating over the copyMap.
+     * @param baseContent The original content
      * @param baseMetaHolder 
      * @param targetMetaHolder 
      * @param metaPrefix The metadata prefix.
@@ -601,13 +612,14 @@
      * @param copyMap
      * @param copyReport The copy report being populated during the copy.
      */
-    public void copyMetadataMap(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metaPrefix, ContentType contentType, Map<String, Object> copyMap, CopyReport copyReport)
+    public void copyMetadataMap(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metaPrefix, ContentType contentType, Map<String, Object> copyMap, CopyReport copyReport)
     {
-        _copyMetadataMap(baseMetaHolder, targetMetaHolder, metaPrefix, contentType, copyMap, copyReport);
+        _copyMetadataMap(baseContent, baseMetaHolder, targetMetaHolder, metaPrefix, null, copyMap, copyReport);
     }
     
     /**
      * Copy the specified set of metadata from a base composite metadata to a target composite metadata by iterating over the copyMap.
+     * @param baseContent The original content
      * @param baseMetaHolder 
      * @param targetMetaHolder 
      * @param metaPrefix The metadata prefix.
@@ -615,21 +627,22 @@
      * @param copyMap
      * @param copyReport The copy report being populated during the copy.
      */
-    public void copyMetadataMap(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metaPrefix, MetadataDefinition metadataDefinition, Map<String, Object> copyMap, CopyReport copyReport)
+    public void copyMetadataMap(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metaPrefix, MetadataDefinition metadataDefinition, Map<String, Object> copyMap, CopyReport copyReport)
     {
-        _copyMetadataMap(baseMetaHolder, targetMetaHolder, metaPrefix, metadataDefinition, copyMap, copyReport);
+        _copyMetadataMap(baseContent, baseMetaHolder, targetMetaHolder, metaPrefix, metadataDefinition, copyMap, copyReport);
     }
     
     /**
      * Copy the specified set of metadata from a base composite metadata to a target composite metadata by iterating over the copyMap.
-     * @param baseMetaHolder 
+     * @param baseContent The original content
+     * @param baseMetaHolder
      * @param targetMetaHolder 
      * @param metaPrefix The metadata prefix.
-     * @param contentTypeOrMetadataDef Either a contentType (is at the root level of content metadata holder), or a metadata definition.
+     * @param parentMetadataDefinition The parent metadata definition. Can be null
      * @param copyMap
      * @param copyReport The copy report being populated during the copy.
      */
-    protected void _copyMetadataMap(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metaPrefix, Object contentTypeOrMetadataDef, Map<String, Object> copyMap, CopyReport copyReport)
+    protected void _copyMetadataMap(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metaPrefix, MetadataDefinition parentMetadataDefinition, Map<String, Object> copyMap, CopyReport copyReport)
     {
         if (copyMap == null)
         {
@@ -647,10 +660,10 @@
             MetadataDefinition metadataDefinition = null;
             try
             {
-                metadataDefinition = _getMetadataDefinition(contentTypeOrMetadataDef, metadataName);
+                metadataDefinition = _getMetadataDefinition(baseContent, parentMetadataDefinition, metadataName);
                 if (metadataDefinition != null)
                 {
-                    copyMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metaPrefix, (Map<String, Object>) copyMap.get(metadataName), copyReport);
+                    copyMetadata(baseContent, baseMetaHolder, targetMetaHolder, metadataDefinition, metaPrefix, (Map<String, Object>) copyMap.get(metadataName), copyReport);
                 }
             }
             catch (Exception e)
@@ -662,26 +675,22 @@
     
     /**
      * Retrieves a {@link MetadataDefinition} through its
-     * parent defintion or the {@link ContentType} of the
+     * parent definition or the {@link ContentType} of the
      * current content if at the root level of the metadataset.
-     * @param contentTypeOrMetadataDef
-     * @param childMetadataName
-     * @return The retrieved metadata defintion.
-     * @throws IllegalArgumentException If the metadata could not be retrieved.
+     * @param content The content
+     * @param metadataDefinition The parent metadata definition. Can be null.
+     * @param metadataName The metadata name
+     * @return The retrieved metadata defintion or null if not found
      */
-    protected MetadataDefinition _getMetadataDefinition(Object contentTypeOrMetadataDef, String childMetadataName) throws IllegalArgumentException
+    protected MetadataDefinition _getMetadataDefinition(Content content, MetadataDefinition metadataDefinition, String metadataName)
     {
-        if (contentTypeOrMetadataDef instanceof MetadataDefinition)
+        if (metadataDefinition != null)
         {
-            return ((MetadataDefinition) contentTypeOrMetadataDef).getMetadataDefinition(childMetadataName);
-        }
-        else if (contentTypeOrMetadataDef instanceof ContentType)
-        {
-            return ((ContentType) contentTypeOrMetadataDef).getMetadataDefinition(childMetadataName);
+            return metadataDefinition.getMetadataDefinition(metadataName);
         }
         else
         {
-            throw new IllegalArgumentException("Unable to retrieve metadata definition for metadata '" + childMetadataName + "'.");
+            return _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
         }
     }
     
@@ -706,6 +715,7 @@
     
     /**
      * Copy the specified metadata from a base composite metadata to a target composite metadata.
+     * @param baseContent The original content
      * @param baseMetaHolder 
      * @param targetMetaHolder 
      * @param metadataDefinition 
@@ -713,14 +723,12 @@
      * @param copyMap 
      * @param copyReport 
      */
-    public void copyMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDefinition, String metaPrefix, Map<String, Object> copyMap, CopyReport copyReport)
+    public void copyMetadata(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDefinition, String metaPrefix, Map<String, Object> copyMap, CopyReport copyReport)
     {
         String metadataName = metadataDefinition.getName();
         String metadataPath = metaPrefix + "/" + metadataName;
-        MetadataType type = metadataDefinition.getType();
-        boolean multiple = metadataDefinition.isMultiple();
         
-        switch (type)
+        switch (metadataDefinition.getType())
         {
             case GEOCODE:
                 copyGeocodeMetadata(baseMetaHolder, targetMetaHolder, metadataName);
@@ -729,10 +737,10 @@
                 copyBinaryMetadata(baseMetaHolder, targetMetaHolder, metadataName);
                 break;
             case FILE:
-                copyFileMetadata(baseMetaHolder, targetMetaHolder, type, metadataName, multiple);
+                copyFileMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName);
                 break;
             case RICH_TEXT:
-                copyRichTextMetadata(baseMetaHolder, targetMetaHolder, metadataName, copyReport);
+                copyRichTextMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName, copyReport);
                 break;
             case CONTENT:
                 copyContentReferenceMetadata(baseMetaHolder, targetMetaHolder, metadataName, copyMap, copyReport);
@@ -741,16 +749,16 @@
                 copySubContentMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataPath, copyMap, copyReport);
                 break;
             case COMPOSITE:
-                copyCompositeMetadata(baseMetaHolder, targetMetaHolder, metadataPath, metadataDefinition, copyMap, copyReport);
+                copyCompositeMetadata(baseContent, baseMetaHolder, targetMetaHolder, metadataPath, metadataDefinition, copyMap, copyReport);
                 break;
             default:
-                copyBasicMetadata(baseMetaHolder, targetMetaHolder, type, metadataName, multiple);
+                copyBasicMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName);
                 break;
         }
     }
     
     /**
-     * Duplicate a basic metadata (nothing special to do).
+     * Copy a 'basic' metadata.
      * This is used to copy metadata of type :
      * {@link MetadataType#STRING}, {@link MetadataType#DATE}, {@link MetadataType#DATETIME},
      * {@link MetadataType#DOUBLE}, {@link MetadataType#LONG}, {@link MetadataType#BOOLEAN},
@@ -758,34 +766,33 @@
      * 
      * @param baseMetaHolder 
      * @param targetMetaHolder 
-     * @param type 
-     * @param metadataName 
-     * @param isMultiple 
+     * @param metadataDef The metadata definition 
+     * @param metadataName The metadata name 
      */
-    public void copyBasicMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataType type, String metadataName, Boolean isMultiple)
+    public void copyBasicMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDef, String metadataName)
     {
         if (baseMetaHolder.hasMetadata(metadataName))
         {
-            switch (type)
+            switch (metadataDef.getType())
             {
                 case DATE:
                 case DATETIME:
-                    _setDateMetadata(baseMetaHolder, targetMetaHolder, metadataName, isMultiple);
+                    _setDateMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple());
                     break;
                 case DOUBLE:
-                    _setDoubleMetadata(baseMetaHolder, targetMetaHolder, metadataName, isMultiple);
+                    _setDoubleMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple());
                     break;
                 case LONG:
-                    _setLongMetadata(baseMetaHolder, targetMetaHolder, metadataName, isMultiple);
+                    _setLongMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple());
                     break;
                 case BOOLEAN:
-                    _setBooleanMetadata(baseMetaHolder, targetMetaHolder, metadataName, isMultiple);
+                    _setBooleanMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple());
                     break;
                 case STRING:
                 case USER:
                 case REFERENCE:
                 default:
-                    _setStringMetadata(baseMetaHolder, targetMetaHolder, metadataName, isMultiple);
+                    _setStringMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple());
                     break;
             }
         }
@@ -938,14 +945,13 @@
     }
     
     /**
-     * Duplicate a metadata of type {@link MetadataType#FILE}.
-     * @param baseMetaHolder 
-     * @param targetMetaHolder 
-     * @param type 
-     * @param metadataName 
-     * @param isMultiple 
+     * Copy a file metadata
+     * @param baseMetaHolder The copied composite metadata
+     * @param targetMetaHolder The target composite metadata
+     * @param metadataDef The metadata definition
+     * @param metadataName The metadata name
      */
-    public void copyFileMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataType type, String metadataName, Boolean isMultiple)
+    public void copyFileMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDef, String metadataName)
     {
         if (baseMetaHolder.hasMetadata(metadataName))
         {
@@ -957,7 +963,7 @@
             else
             {
                 targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getString(metadataName));
-                copyBasicMetadata(baseMetaHolder, targetMetaHolder, type, metadataName, isMultiple);
+                copyBasicMetadata(baseMetaHolder, targetMetaHolder, metadataDef, metadataName);
             }
         }
         else if (targetMetaHolder.hasMetadata(metadataName))
@@ -967,13 +973,14 @@
     }
     
     /**
-     * Duplicate a metadata of type {@link MetadataType#RICH_TEXT}.
-     * @param baseMetaHolder 
-     * @param targetMetaHolder 
-     * @param metadataName 
-     * @param copyReport 
+     * Copy a rich-text metadata
+     * @param baseMetaHolder The copied composite metadata
+     * @param targetMetaHolder The target composite metadata
+     * @param metadataName The metadata name
+     * @param metadataDef The metadata definition
+     * @param copyReport The copy report
      */
-    protected void copyRichTextMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, CopyReport copyReport)
+    protected void copyRichTextMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDef, String metadataName, CopyReport copyReport)
     {
         if (baseMetaHolder.hasMetadata(metadataName))
         {
@@ -981,7 +988,7 @@
             ModifiableRichText targetRichText = targetMetaHolder.getRichText(metadataName, true);
             
             // Notify the report that a rich text has been copied.
-            copyReport.addRichText(targetRichText);
+            copyReport.addRichText(targetRichText, metadataDef);
             
             targetRichText.setEncoding(baseRichText.getEncoding());
             targetRichText.setMimeType(baseRichText.getMimeType());
@@ -1235,19 +1242,20 @@
     }
     
     /**
-     * Duplicate a metadata of type {@link MetadataType#COMPOSITE}. 
-     * @param baseMetaHolder 
-     * @param targetMetaHolder 
-     * @param metadataPath 
-     * @param metadataDefinition
-     * @param copyMap 
-     * @param copyReport 
+     * Copy composite metadata
+     * @param baseContent The copied content
+     * @param baseMetaHolder The original composite metadata
+     * @param targetMetaHolder The target composite metadata
+     * @param metadataPath The metadata path
+     * @param metadataDefinition The metadata definition
+     * @param copyMap The map for copy
+     * @param copyReport The copy report
      */
-    public void copyCompositeMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataPath, MetadataDefinition metadataDefinition, Map<String, Object> copyMap, CopyReport copyReport)
+    public void copyCompositeMetadata(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataPath, MetadataDefinition metadataDefinition, Map<String, Object> copyMap, CopyReport copyReport)
     {
         if (metadataDefinition instanceof RepeaterDefinition)
         {
-            _copyRepeater(baseMetaHolder, targetMetaHolder, metadataPath, metadataDefinition, copyMap, copyReport);
+            _copyRepeater(baseContent, baseMetaHolder, targetMetaHolder, metadataPath, metadataDefinition, copyMap, copyReport);
         }
         else
         {
@@ -1258,7 +1266,7 @@
                 CompositeMetadata baseCompositeMetadata = baseMetaHolder.getCompositeMetadata(metadataName);
                 ModifiableCompositeMetadata targetCompositeMetadata = targetMetaHolder.getCompositeMetadata(metadataName, true);
                 
-                _copyMetadataMap(baseCompositeMetadata, targetCompositeMetadata, metadataPath, metadataDefinition, copyMap, copyReport);
+                _copyMetadataMap(baseContent, baseCompositeMetadata, targetCompositeMetadata, metadataPath, metadataDefinition, copyMap, copyReport);
             }
             else if (targetMetaHolder.hasMetadata(metadataName))
             {
@@ -1268,15 +1276,16 @@
     }
     
     /**
-     * Duplicate a repeater 
-     * @param baseMetaHolder 
-     * @param targetMetaHolder 
-     * @param metadataPath 
-     * @param metadataDefinition
-     * @param copyMap 
-     * @param copyReport 
+     * Copy a repeater
+     * @param baseContent The copied content
+     * @param baseMetaHolder The original composite metadata
+     * @param targetMetaHolder The target composite metadata
+     * @param metadataPath The metadata path
+     * @param metadataDefinition The metadata definition
+     * @param copyMap The map for copy
+     * @param copyReport The copy report
      */
-    protected void _copyRepeater(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataPath, MetadataDefinition metadataDefinition, Map<String, Object> copyMap, CopyReport copyReport)
+    protected void _copyRepeater(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataPath, MetadataDefinition metadataDefinition, Map<String, Object> copyMap, CopyReport copyReport)
     {
         String metadataName = metadataDefinition.getName();
         
@@ -1304,7 +1313,7 @@
                     
                     String metaPrefix = metadataPath + "/" + entryName;
                     
-                    _copyMetadataMap(baseRepeaterEntry, targetRepeaterEntry, metaPrefix, metadataDefinition, copyMap, copyReport);
+                    _copyMetadataMap(baseContent, baseRepeaterEntry, targetRepeaterEntry, metaPrefix, metadataDefinition, copyMap, copyReport);
                 }
             }
         }
@@ -1321,55 +1330,61 @@
      */
     protected void _buildConsistencies(ModifiableContent content)
     {
-        String contentTypeId = content.getType();
-        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
-        ConsistencyExtractor consistencyExtractor = contentType.getConsistencyExtractor();
-        if (consistencyExtractor != null)
+        Map<String, List<String>> elements = new HashMap<String, List<String>>();
+        for (String cTypeId : content.getTypes())
         {
-            Map<String, List<String>> elements = consistencyExtractor.getConsistencyElement(content);
-            
-            content.setConsistencyElements(elements);
+            ContentType contentType = _contentTypeExtensionPoint.getExtension(cTypeId);
+            ConsistencyExtractor consistencyExtractor = contentType.getConsistencyExtractor();
+            if (consistencyExtractor != null)
+            {
+                elements.putAll(consistencyExtractor.getConsistencyElement(content));
+            }
         }
+        content.setConsistencyElements(elements);
     }
     
 
     /**
      * This method analyzes content rich texts and update their content to
      * ensure consistency. (link to image, attachments...)
-     * @param contentType 
-     * @param baseContent 
-     * @param targetContent 
-     * @param copyReport 
+     * @param baseContent The copied content
+     * @param targetContent The target content
+     * @param copyReport  The copy report
      */
-    protected void _updateRichTextMetadata(ContentType contentType, final Content baseContent, final ModifiableContent targetContent, CopyReport copyReport)
+    protected void _updateRichTextMetadata(final Content baseContent, final ModifiableContent targetContent, CopyReport copyReport)
     {
         try
         {
-            RichTextUpdater richTextUpdater = contentType.getRichTextUpdater();
-            List<ModifiableRichText> richTexts = copyReport.getCopiedRichTexts();
+            Map<ModifiableRichText, MetadataDefinition> copiedRichTexts = copyReport.getCopiedRichTexts();
             
-            // Update rich text content
-            if (!richTexts.isEmpty() && richTextUpdater != null)
+            for (Entry<ModifiableRichText, MetadataDefinition> entry : copiedRichTexts.entrySet())
             {
-                Map<String, Object> params = new HashMap<String, Object>();
-                params.put("initialContent", baseContent);
-                params.put("createdContent", targetContent);
+                ModifiableRichText richText = entry.getKey();
+                MetadataDefinition metadataDef = entry.getValue();
                 
-                // create the transformer instance
-                TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
+                String referenceContentType = metadataDef.getReferenceContentType();
+                ContentType contentType = _contentTypeExtensionPoint.getExtension(referenceContentType);
                 
-                // create the format of result
-                Properties format = new Properties();
-                format.put(OutputKeys.METHOD, "xml");
-                format.put(OutputKeys.INDENT, "yes");
-                format.put(OutputKeys.ENCODING, "UTF-8");
-                format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
-                th.getTransformer().setOutputProperties(format);
-                
-                // Update rich text contents
-                // Copy the needed original attachments.
-                for (ModifiableRichText richText : richTexts)
+                RichTextUpdater richTextUpdater = contentType.getRichTextUpdater();
+                if (richTextUpdater != null)
                 {
+                    Map<String, Object> params = new HashMap<String, Object>();
+                    params.put("initialContent", baseContent);
+                    params.put("createdContent", targetContent);
+                    
+                    // create the transformer instance
+                    TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
+                    
+                    // create the format of result
+                    Properties format = new Properties();
+                    format.put(OutputKeys.METHOD, "xml");
+                    format.put(OutputKeys.INDENT, "yes");
+                    format.put(OutputKeys.ENCODING, "UTF-8");
+                    format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
+                    th.getTransformer().setOutputProperties(format);
+                    
+                    // Update rich text contents
+                    // Copy the needed original attachments.
                     InputStream is = null;
                     OutputStream os = null;
                     try
Index: main/plugin-cms/src/org/ametys/cms/content/AddOrRemoveContentTypeAction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/AddOrRemoveContentTypeAction.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/content/AddOrRemoveContentTypeAction.java	(revision 0)
@@ -0,0 +1,165 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.content;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.avalon.framework.parameters.Parameters;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.acting.ServiceableAction;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Redirector;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.environment.SourceResolver;
+
+import org.ametys.cms.ObservationConstants;
+import org.ametys.cms.contenttype.ContentType;
+import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
+import org.ametys.cms.repository.Content;
+import org.ametys.cms.repository.ModifiableContent;
+import org.ametys.cms.repository.WorkflowAwareContent;
+import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
+import org.ametys.plugins.repository.AmetysObjectResolver;
+import org.ametys.plugins.workflow.Workflow;
+import org.ametys.runtime.cocoon.JSonReader;
+import org.ametys.runtime.observation.Event;
+import org.ametys.runtime.observation.ObservationManager;
+import org.ametys.runtime.user.CurrentUserProvider;
+
+/**
+ * Action to add or remove a content type to an existing content
+ *
+ */
+public class AddOrRemoveContentTypeAction extends ServiceableAction
+{
+    private AmetysObjectResolver _resolver;
+    private Workflow _workflow;
+    private ObservationManager _observationManager;
+    private CurrentUserProvider _currentUserProvider;
+    private ContentTypesHelper _contentTypesHelper;
+    private ContentTypeExtensionPoint _contentTypeEP;
+    
+    @Override
+    public void service(ServiceManager smanager) throws ServiceException
+    {
+        super.service(smanager);
+        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
+        _workflow = (Workflow) smanager.lookup(Workflow.ROLE);
+        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
+        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
+        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
+    }
+    
+    @Override
+    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
+    {
+        Map<String, Object> result = new HashMap<String, Object>();
+        
+        Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
+        
+        String contentId = (String) jsParameters.get("contentId");
+        String contentTypeId = (String) jsParameters.get("contentType");
+        
+        int actionId = (Integer) jsParameters.get("actionId");
+        boolean deleteMode = parameters.getParameterAsBoolean("remove", false);
+        
+        Content content = _resolver.resolveById(contentId);
+
+        if (content instanceof ModifiableContent)
+        {
+            ModifiableContent modifiableContent = (ModifiableContent) content;
+            
+            List<String> currentTypes = new ArrayList<String>(Arrays.asList(content.getTypes()));
+            
+            boolean hasChange = false;
+            if (deleteMode)
+            {
+                if (currentTypes.size() > 1)
+                {
+                    hasChange = currentTypes.remove(contentTypeId);
+                }
+                else
+                {
+                    result.put("failure", true);
+                    result.put("msg", "empty-list");
+                }
+            }
+            else if (!currentTypes.contains(contentTypeId))
+            {
+                ContentType cType = _contentTypeEP.getExtension(contentTypeId);
+                if (cType.isMixin())
+                {
+                    result.put("failure", true);
+                    result.put("msg", "no-content-type");
+                    getLogger().error("Content type '" + contentTypeId + "' is a mixin type. It can not be added as content type.");
+                }
+                else if (!_contentTypesHelper.isCompatibleContentType(content, contentTypeId))
+                {
+                    result.put("failure", true);
+                    result.put("msg", "invalid-content-type");
+                    getLogger().error("Content type '" + contentTypeId + "' is incompatible with content '" + contentId + "'.");
+                }
+                else
+                {
+                    currentTypes.add(contentTypeId);
+                    hasChange = true;
+                }
+            }
+            
+            if (hasChange)
+            {
+                // TODO check if the content type is compatible
+                modifiableContent.setTypes(currentTypes.toArray(new String[currentTypes.size()]));
+                modifiableContent.saveChanges();
+                
+                if (content instanceof WorkflowAwareContent)
+                {
+                    Map<String, Object> inputs = new HashMap<String, Object>();
+                    
+                    inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
+                    inputs.put(AbstractContentWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
+                    
+                    _workflow.doAction(((WorkflowAwareContent) content).getWorkflowId(), actionId, inputs);
+                }
+                
+                result.put("success", true);
+                
+                Map<String, Object> eventParams = new HashMap<String, Object>();
+                eventParams.put(ObservationConstants.ARGS_CONTENT, modifiableContent);
+                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
+            }
+        }
+        else
+        {
+            result.put("failure", true);
+            result.put("msg", "no-modifiable-content");
+            getLogger().error("Can not modified content types to a non-modifiable content '" + content.getId() + "'.");
+        }
+        
+        Request request = ObjectModelHelper.getRequest(objectModel);
+        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
+        
+        return EMPTY_MAP;
+    }
+
+}
Index: main/plugin-cms/src/org/ametys/cms/content/GetContentInformationAction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/GetContentInformationAction.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/GetContentInformationAction.java	(working copy)
@@ -116,7 +116,8 @@
         infos.put("name", content.getName());
         infos.put("title", content.getTitle());
         infos.put("path", content.getPath());
-        infos.put("type", content.getType());
+        infos.put("types", content.getTypes());
+        infos.put("mixins", content.getMixinTypes());
         infos.put("lang", content.getLanguage());
         infos.put("creator", content.getCreator());
         infos.put("lastContributor", content.getLastContributor());
Index: main/plugin-cms/src/org/ametys/cms/content/ContentGenerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/ContentGenerator.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/ContentGenerator.java	(working copy)
@@ -40,6 +40,7 @@
 
 import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.contenttype.MetadataManager;
 import org.ametys.cms.contenttype.MetadataSet;
 import org.ametys.cms.languages.Language;
@@ -76,7 +77,8 @@
     protected LanguagesManager _languageManager;
     /** The workflow */
     protected Workflow _workflow;
-    
+    /** Helper for content types */
+    protected ContentTypesHelper _cTypesHelper; 
     
     @Override
     public void service(ServiceManager serviceManager) throws ServiceException
@@ -86,6 +88,7 @@
         _metadataManager = (MetadataManager) serviceManager.lookup(MetadataManager.ROLE);
         _languageManager = (LanguagesManager) serviceManager.lookup(LanguagesManager.ROLE);
         _workflow = (Workflow) serviceManager.lookup(Workflow.ROLE);
+        _cTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
     }
     
     public void generate() throws IOException, SAXException, ProcessingException
@@ -147,10 +150,9 @@
         attrs.addCDATAAttribute("lastContributor", content.getLastContributor());
         attrs.addCDATAAttribute("commentable", Boolean.toString(content instanceof CommentableContent));
         
-        ContentType cType = _contentTypeExtensionPoint.getExtension(content.getType());
-        attrs.addCDATAAttribute("smallIcon", cType.getSmallIcon());
-        attrs.addCDATAAttribute("mediumIcon", cType.getMediumIcon());
-        attrs.addCDATAAttribute("largeIcon", cType.getMediumIcon());
+        attrs.addCDATAAttribute("smallIcon", _cTypesHelper.getSmallIcon(content));
+        attrs.addCDATAAttribute("mediumIcon", _cTypesHelper.getMediumIcon(content));
+        attrs.addCDATAAttribute("largeIcon", _cTypesHelper.getLargeIcon(content));
         
         XMLUtils.startElement(contentHandler, "content", attrs);
 
@@ -166,7 +168,12 @@
             XMLUtils.endElement(contentHandler, "comments");
         }
         
-        cType.saxContentTypeAdditionalData(contentHandler, content);
+        String[] cTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
+        for (String cTypeId : cTypes)
+        {
+            ContentType cType = _contentTypeExtensionPoint.getExtension(cTypeId);
+            cType.saxContentTypeAdditionalData(contentHandler, content);
+        }
         
         // FIXME CMS-3057
         Request request = ObjectModelHelper.getRequest(objectModel);
@@ -177,7 +184,6 @@
             _saxWorkflowStep(content);
         }
         
-        _saxContentType(cType);
         _saxLanguage (content);
         
         _saxDublinCoreMetadata(content);
@@ -248,24 +254,6 @@
     }
     
     /**
-     * SAX teh content type
-     * @param cType The content type
-     * @throws SAXException if an error occurs while SAXing.
-     */
-    protected void _saxContentType (ContentType cType) throws SAXException
-    {
-        AttributesImpl attrs = new AttributesImpl();
-        attrs.addCDATAAttribute("id", cType.getId());
-        attrs.addCDATAAttribute("icon-small", cType.getSmallIcon());
-        attrs.addCDATAAttribute("icon-medium", cType.getMediumIcon());
-        attrs.addCDATAAttribute("icon-large", cType.getLargeIcon());
-        
-        XMLUtils.startElement(contentHandler, "content-type", attrs);
-        cType.getLabel().toSAX(contentHandler);
-        XMLUtils.endElement(contentHandler, "content-type");
-        
-    }
-    /**
      * SAX the content language
      * @param content The content
      * @throws SAXException if an error occurs while SAXing.
@@ -464,7 +452,6 @@
      */
     protected MetadataSet _getMetadataSet(Content content) throws ProcessingException
     {
-        ContentType contentType = _contentTypeExtensionPoint.getExtension(content.getType());
         boolean isEditionMetadataSet = parameters.getParameterAsBoolean("isEditionMetadataSet", false);
         String metadataSetName = parameters.getParameter("metadataSetName", "");
         if (StringUtils.isBlank(metadataSetName))
@@ -476,17 +463,17 @@
         
         if (isEditionMetadataSet)
         {
-            metadataSet = contentType.getMetadataSetForEdition(metadataSetName);
+            metadataSet = _cTypesHelper.getMetadataSetForEdition(metadataSetName, content.getTypes(), content.getMixinTypes());
         }
         else
         {
-            metadataSet = contentType.getMetadataSetForView(metadataSetName);
+            metadataSet = _cTypesHelper.getMetadataSetForView(metadataSetName, content.getTypes(), content.getMixinTypes());
         }
         
         if (metadataSet == null)
         {
-            throw new ProcessingException(String.format("Unknown metadata set '%s' of type '%s' for content type '%s'",
-                                          metadataSetName, isEditionMetadataSet ? "edition" : "view", contentType.getId())); 
+            throw new ProcessingException(String.format("Unknown metadata set '%s' of type '%s' for content type(s) '%s'",
+                                          metadataSetName, isEditionMetadataSet ? "edition" : "view", StringUtils.join(content.getTypes(), ','))); 
         }
         
         return metadataSet;
Index: main/plugin-cms/src/org/ametys/cms/content/CopyReport.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/CopyReport.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/CopyReport.java	(working copy)
@@ -16,9 +16,12 @@
 package org.ametys.cms.content;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
+import org.ametys.cms.contenttype.MetadataDefinition;
 import org.ametys.plugins.repository.metadata.ModifiableRichText;
 import org.ametys.runtime.util.I18nizableText;
 
@@ -53,7 +56,7 @@
     protected List<CopyReport> _innerReports;
     
     /** List of copied rich text metadata during the duplication process */ 
-    protected List<ModifiableRichText> _copiedRichTexts;
+    protected Map<ModifiableRichText, MetadataDefinition> _copiedRichTexts;
     /**
      * List of paths (relative to the root attachment node of the target
      * content) of the copied attachments during the duplication
@@ -119,7 +122,7 @@
         _mode = copyMode;
         
         _innerReports = new LinkedList<CopyReport>();
-        _copiedRichTexts = new LinkedList<ModifiableRichText>();
+        _copiedRichTexts = new HashMap<ModifiableRichText, MetadataDefinition>();
         _copiedAttachments = new LinkedList<String>();
         _metadataCopyErrors = new LinkedList<I18nizableText>();
         
@@ -195,20 +198,21 @@
     
     /**
      * Add a rich text to the list of copied rich texts.
-     * @param richText
+     * @param richText the rich text
+     * @param definition the metadata definition
      */
-    protected void addRichText(ModifiableRichText richText)
+    protected void addRichText(ModifiableRichText richText, MetadataDefinition definition)
     {
-        _copiedRichTexts.add(richText);
+        _copiedRichTexts.put(richText, definition);
     }
     
     /**
      * Get the list of copied rich text metadata.
      * @return List of {@link ModifiableRichText}
      */
-    protected List<ModifiableRichText> getCopiedRichTexts()
+    protected Map<ModifiableRichText, MetadataDefinition> getCopiedRichTexts()
     {
-        return Collections.unmodifiableList(_copiedRichTexts);
+        return Collections.unmodifiableMap(_copiedRichTexts);
     }
     
     /**
Index: main/plugin-cms/src/org/ametys/cms/content/AddOrRemoveMixinAction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/AddOrRemoveMixinAction.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/content/AddOrRemoveMixinAction.java	(revision 0)
@@ -0,0 +1,157 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.content;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.avalon.framework.parameters.Parameters;
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.acting.ServiceableAction;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Redirector;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.environment.SourceResolver;
+
+import org.ametys.cms.ObservationConstants;
+import org.ametys.cms.contenttype.ContentType;
+import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
+import org.ametys.cms.repository.Content;
+import org.ametys.cms.repository.ModifiableContent;
+import org.ametys.cms.repository.WorkflowAwareContent;
+import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
+import org.ametys.plugins.repository.AmetysObjectResolver;
+import org.ametys.plugins.workflow.Workflow;
+import org.ametys.runtime.cocoon.JSonReader;
+import org.ametys.runtime.observation.Event;
+import org.ametys.runtime.observation.ObservationManager;
+import org.ametys.runtime.user.CurrentUserProvider;
+
+/**
+ * Action to add or remove a content type to an existing content
+ *
+ */
+public class AddOrRemoveMixinAction extends ServiceableAction
+{
+    private AmetysObjectResolver _resolver;
+    private Workflow _workflow;
+    private ObservationManager _observationManager;
+    private CurrentUserProvider _currentUserProvider;
+    private ContentTypesHelper _contentTypesHelper;
+    private ContentTypeExtensionPoint _contentTypeEP;
+    
+    @Override
+    public void service(ServiceManager smanager) throws ServiceException
+    {
+        super.service(smanager);
+        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
+        _workflow = (Workflow) smanager.lookup(Workflow.ROLE);
+        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
+        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
+        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
+    }
+    
+    @Override
+    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
+    {
+        Map<String, Object> result = new HashMap<String, Object>();
+        
+        Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
+        
+        String contentId = (String) jsParameters.get("contentId");
+        String mixinId = (String) jsParameters.get("mixin");
+        
+        int actionId = (Integer) jsParameters.get("actionId");
+        boolean deleteMode = parameters.getParameterAsBoolean("remove", false);
+        
+        Content content = _resolver.resolveById(contentId);
+
+        if (content instanceof ModifiableContent)
+        {
+            ModifiableContent modifiableContent = (ModifiableContent) content;
+            
+            List<String> currentMixins = new ArrayList<String>(Arrays.asList(content.getMixinTypes()));
+            
+            boolean hasChange = false;
+            if (deleteMode)
+            {
+                hasChange = currentMixins.remove(mixinId);
+            }
+            else if (!currentMixins.contains(mixinId))
+            {
+                ContentType cType = _contentTypeEP.getExtension(mixinId);
+                if (!cType.isMixin())
+                {
+                    result.put("failure", true);
+                    result.put("msg", "no-mixin");
+                    getLogger().error("The content type '" + mixinId + "' is not a mixin type, it can be not be added as a mixin.");
+                }
+                else if (!_contentTypesHelper.isCompatibleContentType(content, mixinId))
+                {
+                    result.put("failure", true);
+                    result.put("msg", "invalid-mixin");
+                    getLogger().error("Mixin '" + mixinId + "' is incompatible with content '" + contentId + "'.");
+                }
+                else
+                {
+                    currentMixins.add(mixinId);
+                    hasChange = true;
+                }
+            }
+            
+            if (hasChange)
+            {
+                // TODO check if the content type is compatible
+                modifiableContent.setMixinTypes(currentMixins.toArray(new String[currentMixins.size()]));
+                modifiableContent.saveChanges();
+                
+                if (content instanceof WorkflowAwareContent)
+                {
+                    Map<String, Object> inputs = new HashMap<String, Object>();
+                    
+                    inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
+                    inputs.put(AbstractContentWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
+                    
+                    _workflow.doAction(((WorkflowAwareContent) content).getWorkflowId(), actionId, inputs);
+                }
+                
+                result.put("success", true);
+                
+                Map<String, Object> eventParams = new HashMap<String, Object>();
+                eventParams.put(ObservationConstants.ARGS_CONTENT, modifiableContent);
+                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
+            }
+        }
+        else
+        {
+            result.put("failure", true);
+            result.put("msg", "no-modifiable-content");
+            getLogger().error("Can not modified mixins to a non-modifiable content '" + content.getId() + "'.");
+        }
+        
+        Request request = ObjectModelHelper.getRequest(objectModel);
+        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
+        
+        return EMPTY_MAP;
+    }
+
+}
Index: main/plugin-cms/src/org/ametys/cms/content/ContentInformationGenerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/ContentInformationGenerator.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/ContentInformationGenerator.java	(working copy)
@@ -28,6 +28,7 @@
 import org.apache.cocoon.generation.ServiceableGenerator;
 import org.apache.cocoon.xml.AttributesImpl;
 import org.apache.cocoon.xml.XMLUtils;
+import org.apache.commons.lang.StringUtils;
 import org.xml.sax.SAXException;
 
 import org.ametys.cms.repository.Content;
@@ -98,7 +99,7 @@
                 attrs.addCDATAAttribute("title", content.getTitle());
                 attrs.addCDATAAttribute("name", content.getName());
                 attrs.addCDATAAttribute("path", content.getPath());
-                attrs.addCDATAAttribute("type", content.getType());
+                attrs.addCDATAAttribute("type", StringUtils.join(content.getTypes(), ','));
                 attrs.addCDATAAttribute("lang", content.getLanguage());
                 attrs.addCDATAAttribute("creator", content.getCreator());
                 attrs.addCDATAAttribute("lastContributor", content.getLastContributor());
Index: main/plugin-cms/src/org/ametys/cms/content/GetMetadataSetDefAction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/GetMetadataSetDefAction.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/GetMetadataSetDefAction.java	(working copy)
@@ -33,8 +33,8 @@
 import org.apache.commons.lang.StringUtils;
 
 import org.ametys.cms.contenttype.AbstractMetadataSetElement;
-import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.contenttype.Fieldset;
 import org.ametys.cms.contenttype.MetadataDefinition;
 import org.ametys.cms.contenttype.MetadataDefinitionReference;
@@ -44,7 +44,6 @@
 import org.ametys.cms.contenttype.RichTextMetadataDefinition;
 import org.ametys.cms.contenttype.SemanticAnnotation;
 import org.ametys.cms.repository.Content;
-import org.ametys.plugins.repository.AmetysRepositoryException;
 import org.ametys.runtime.cocoon.JSonReader;
 import org.ametys.runtime.util.I18nizableText;
 import org.ametys.runtime.util.parameter.Enumerator;
@@ -59,12 +58,15 @@
 {
     /** Content type extension point. */
     protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
+    /** Helper for content type */
+    protected ContentTypesHelper _contentTypesHelper;
     
     @Override
     public void service(ServiceManager serviceManager) throws ServiceException
     {
         super.service(serviceManager);
         _contentTypeExtensionPoint = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
     }
     
     @Override
@@ -98,26 +100,24 @@
         jsonObject.put("id", content.getId());
         jsonObject.put("name", content.getName());
         jsonObject.put("title", content.getTitle());
-        jsonObject.put("type", content.getType());
+        jsonObject.put("type", content.getTypes());
         jsonObject.put("language", content.getLanguage());
         
-        ContentType contentType = _contentTypeExtensionPoint.getExtension(content.getType());
-        
         MetadataSet metadataSet = null;
         
         if (isEditionMetadataSet)
         {
-            metadataSet = contentType.getMetadataSetForEdition(metadataSetName);
+            metadataSet = _contentTypesHelper.getMetadataSetForEdition(metadataSetName, content.getTypes(), content.getMixinTypes());
         }
         else
         {
-            metadataSet = contentType.getMetadataSetForView(metadataSetName);
+            metadataSet = _contentTypesHelper.getMetadataSetForView(metadataSetName, content.getTypes(), content.getMixinTypes());
         }
         
         if (metadataSet == null)
         {
-            throw new ProcessingException(String.format("Unknown metadata set '%s' of type '%s' for content type '%s'",
-                                          metadataSetName, isEditionMetadataSet ? "edition" : "view", contentType.getId())); 
+            throw new ProcessingException(String.format("Unknown metadata set '%s' of type '%s' for content type(s) '%s'",
+                                          metadataSetName, isEditionMetadataSet ? "edition" : "view", StringUtils.join(content.getTypes(), ','))); 
         }
         
         jsonObject.put("metadataSetName", metadataSetName); 
@@ -151,12 +151,10 @@
                     throw new IllegalArgumentException("Unable to get the metadata definition of metadata \"" + metadataName + "\"");
                 }
                 
-                ContentType contentType = _contentTypeExtensionPoint.getExtension(content.getType());
-                
                 String metadataPath = StringUtils.stripStart(metaDef.getId(), "/");
-                if (contentType.canRead(content, metadataPath))
+                if (_contentTypesHelper.canRead(content, metadataPath))
                 {
-                    jsonObject.put(metaDef.getName(), metadataDefinition2JsonObject(contentType, content, metadataDefRef, metaDef, metadataPath));
+                    jsonObject.put(metaDef.getName(), metadataDefinition2JsonObject(content, metadataDefRef, metaDef, metadataPath));
                 }
             }
             else
@@ -184,7 +182,6 @@
     
     /**
      * Convert a metadata to JSON object
-     * @param contentType The content type
      * @param content The content 
      * @param metadataSetElement The metadataset element
      * @param metaDef The metadata definition
@@ -192,7 +189,7 @@
      * @return The JSON object representing the metadata
      * @throws ProcessingException
      */
-    protected Map<String, Object> metadataDefinition2JsonObject(ContentType contentType, Content content, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metaDef, String metadataPath) throws ProcessingException
+    protected Map<String, Object> metadataDefinition2JsonObject(Content content, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metaDef, String metadataPath) throws ProcessingException
     {
         Map<String, Object> jsonObject = new LinkedHashMap<String, Object>();
         
@@ -261,7 +258,7 @@
         }
 
         
-        if (!contentType.canWrite(content, metadataPath))
+        if (!_contentTypesHelper.canWrite(content, metadataPath))
         {
             jsonObject.put("can-not-write", true);
         }
@@ -358,15 +355,7 @@
         {
             if (content != null)
             {
-                String contentTypeId = content.getType();
-                ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
-                
-                if (contentType == null)
-                {
-                    throw new AmetysRepositoryException("Unknown content type: " + contentTypeId);
-                }
-                
-                metadataDefinition =  contentType.getMetadataDefinition(metadataName);
+                metadataDefinition =  _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
             }
         }
         else
Index: main/plugin-cms/src/org/ametys/cms/content/consistency/GlobalContentConsistencyGenerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/consistency/GlobalContentConsistencyGenerator.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/consistency/GlobalContentConsistencyGenerator.java	(working copy)
@@ -31,14 +31,14 @@
 import org.apache.cocoon.xml.XMLUtils;
 import org.xml.sax.SAXException;
 
-import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.repository.Content;
 import org.ametys.cms.repository.ContentQueryHelper;
 import org.ametys.cms.repository.DefaultContent;
 import org.ametys.cms.transformation.ConsistencyChecker;
-import org.ametys.cms.transformation.ConsistencyCheckerExtensionPoint;
 import org.ametys.cms.transformation.ConsistencyChecker.CHECK;
+import org.ametys.cms.transformation.ConsistencyCheckerExtensionPoint;
 import org.ametys.plugins.repository.AmetysObjectIterable;
 import org.ametys.plugins.repository.AmetysObjectResolver;
 import org.ametys.plugins.repository.RepositoryConstants;
@@ -70,10 +70,13 @@
     protected UsersManager _usersManager;
     /** The content type extension point */
     protected ContentTypeExtensionPoint _cTypeExtPt;
+    /** Helper for content types */
+    protected ContentTypesHelper _cTypesHelper;
     
     /** The name cache */
     protected Map<String, String> _nameCache;
     
+    
     @Override
     public void service(ServiceManager serviceManager) throws ServiceException
     {
@@ -82,6 +85,7 @@
         _consistencyCheckerExtensionPoint = (ConsistencyCheckerExtensionPoint) serviceManager.lookup(ConsistencyCheckerExtensionPoint.ROLE);
         _cTypeExtPt = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
         _usersManager = (UsersManager) serviceManager.lookup(UsersManager.ROLE);
+        _cTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
     }
     
     @Override
@@ -177,12 +181,10 @@
      */
     protected void _saxContentConsistency(Content content, int successCount, int unknownCount, int failureCount) throws SAXException
     {
-        ContentType contentType =  _cTypeExtPt.getExtension(content.getType());
-        
         AttributesImpl atts = new AttributesImpl();
         atts.addCDATAAttribute("id", content.getId());
         atts.addCDATAAttribute("title", content.getTitle());
-        atts.addCDATAAttribute("smallIcon", contentType != null ? contentType.getSmallIcon() : "/plugins/cms/resources/img/contenttype/unknown-small.png");
+        atts.addCDATAAttribute("smallIcon", _cTypesHelper.getSmallIcon(content));
         
         atts.addCDATAAttribute("failure-count", Integer.toString(failureCount));
         atts.addCDATAAttribute("unknown-count", Integer.toString(unknownCount));
Index: main/plugin-cms/src/org/ametys/cms/content/AdditionalContentPropertiesGenerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/AdditionalContentPropertiesGenerator.java	(revision 0)
+++ main/plugin-cms/src/org/ametys/cms/content/AdditionalContentPropertiesGenerator.java	(revision 0)
@@ -0,0 +1,237 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.ametys.cms.content;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.ProcessingException;
+import org.apache.cocoon.environment.ObjectModelHelper;
+import org.apache.cocoon.environment.Request;
+import org.apache.cocoon.generation.ServiceableGenerator;
+import org.apache.cocoon.xml.AttributesImpl;
+import org.apache.cocoon.xml.XMLUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.xml.sax.SAXException;
+
+import org.ametys.cms.contenttype.ContentType;
+import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.repository.Content;
+import org.ametys.plugins.repository.AmetysObjectResolver;
+import org.ametys.plugins.repository.version.VersionableAmetysObject;
+import org.ametys.runtime.util.I18nUtils;
+import org.ametys.runtime.util.I18nizableText;
+
+/**
+ * Generates addition information on content
+ */
+public class AdditionalContentPropertiesGenerator extends ServiceableGenerator
+{
+    /** The AmetysObject resolver. */
+    protected AmetysObjectResolver _resolver;
+    private ContentTypeExtensionPoint _contentTypeEP;
+    private I18nUtils _i18nUtils;
+    
+    @Override
+    public void service(ServiceManager serviceManager) throws ServiceException
+    {
+        super.service(serviceManager);
+        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
+        _contentTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
+        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
+    }
+    
+    @Override
+    public void generate() throws IOException, SAXException, ProcessingException
+    {
+        Request request = ObjectModelHelper.getRequest(objectModel);
+        
+        String contentId = parameters.getParameter("contentId", request.getParameter("contentId"));
+        
+        Content content = null;
+        if (StringUtils.isNotEmpty(contentId))
+        {
+            content = _resolver.resolveById(contentId);
+        }
+        else
+        {
+            content = (Content) request.getAttribute(Content.class.getName());
+        }
+        
+        contentHandler.startDocument();
+        
+        AttributesImpl attrs = new AttributesImpl();
+        attrs.addCDATAAttribute("id", content.getId());
+        XMLUtils.startElement(contentHandler, "content", attrs);
+        
+        // Take the HEAD revision
+        String revision = null;
+        if (content instanceof VersionableAmetysObject)
+        {
+            revision = ((VersionableAmetysObject) content).getRevision();
+            ((VersionableAmetysObject) content).switchToRevision(null);
+        }
+        
+        _saxContentTypesHierarchy (content);
+        _saxMixinsHierarchy(content);
+        _saxReferencingContents(content);
+        
+        if (content instanceof VersionableAmetysObject && revision != null)
+        {
+            ((VersionableAmetysObject) content).switchToRevision(revision);
+        }
+        
+        XMLUtils.endElement(contentHandler, "content");
+        contentHandler.endDocument();
+    }
+    
+    /**
+     * Sax the referencing contents
+     * @param content The content
+     * @throws SAXException if an error occurred while saxing
+     */
+    protected void _saxReferencingContents (Content content) throws SAXException
+    {
+        XMLUtils.startElement(contentHandler, "referencing-contents");
+        
+        for (Content referrerContent : content.getReferencingContents())
+        {
+            AttributesImpl refererAttrs = new AttributesImpl();
+            refererAttrs.addCDATAAttribute("id", referrerContent.getId());
+            refererAttrs.addCDATAAttribute("name", referrerContent.getName());
+            refererAttrs.addCDATAAttribute("title", referrerContent.getTitle());
+            
+            XMLUtils.createElement(contentHandler, "referencing-content", refererAttrs);
+        }
+        
+        XMLUtils.endElement(contentHandler, "referencing-contents");
+    }
+    
+    /**
+     * SAX the content types hierarchy
+     * @param content The content type
+     * @throws SAXException if an error occurred while saxing
+     */
+    protected void _saxContentTypesHierarchy (Content content) throws SAXException
+    {
+        // Sort content types
+        TreeSet<ContentType> treeSet = new TreeSet<ContentType>(new ContentTypeComparator());
+        for (String id : content.getTypes())
+        {
+            ContentType contentType = _contentTypeEP.getExtension(id);
+            treeSet.add(contentType);
+        }
+        
+        XMLUtils.startElement(contentHandler, "content-types");
+        for (ContentType cType : treeSet)
+        {
+            _saxContentType (cType, content.getLanguage());
+        }
+        XMLUtils.endElement(contentHandler, "content-types");
+    }
+    
+    /**
+     * SAX the content types hierarchy
+     * @param content The content type
+     * @throws SAXException if an error occurred while saxing
+     */
+    protected void _saxMixinsHierarchy (Content content) throws SAXException
+    {
+        XMLUtils.startElement(contentHandler, "mixins");
+        
+        String[] contentTypes = content.getMixinTypes();
+        for (String id : contentTypes)
+        {
+            ContentType cType = _contentTypeEP.getExtension(id);
+            _saxMixin(cType, content.getLanguage());
+        }
+        
+        XMLUtils.endElement(contentHandler, "mixins");
+    }
+    
+    /**
+     * SAX the content type hierarchy
+     * @param cType The content type
+     * @param lang The language
+     * @throws SAXException if an error occurred while saxing
+     */
+    protected void _saxContentType (ContentType cType, String lang) throws SAXException
+    {
+        AttributesImpl attrs = new AttributesImpl();
+        
+        attrs.addCDATAAttribute("id", cType.getId());
+        attrs.addCDATAAttribute("icon", cType.getSmallIcon());
+        attrs.addCDATAAttribute("label", _i18nUtils.translate(cType.getLabel(), lang));
+        
+        XMLUtils.startElement(contentHandler, "content-type", attrs);
+        
+        for (String id : cType.getSupertypeIds())
+        {
+            ContentType superType = _contentTypeEP.getExtension(id);
+            _saxContentType(superType, lang);
+        }
+        XMLUtils.endElement(contentHandler, "content-type");
+    }
+    
+    /**
+     * SAX the mixin hierarchy
+     * @param mixin The mixin type
+     * @param lang The language
+     * @throws SAXException if an error occurred while saxing
+     */
+    protected void _saxMixin (ContentType mixin, String lang) throws SAXException
+    {
+        AttributesImpl attrs = new AttributesImpl();
+        
+        attrs.addCDATAAttribute("id", mixin.getId());
+        attrs.addCDATAAttribute("icon", mixin.getSmallIcon());
+        attrs.addCDATAAttribute("label", _i18nUtils.translate(mixin.getLabel(), lang));
+        
+        XMLUtils.startElement(contentHandler, "mixin", attrs);
+        
+        for (String id : mixin.getSupertypeIds())
+        {
+            ContentType superType = _contentTypeEP.getExtension(id);
+            _saxMixin(superType, lang);
+        }
+        XMLUtils.endElement(contentHandler, "mixin");
+    }
+
+    class ContentTypeComparator implements Comparator<ContentType>
+    {
+        @Override
+        public int compare(ContentType c1, ContentType c2)
+        {
+            I18nizableText t1 = c1.getLabel();
+            I18nizableText t2 = c2.getLabel();
+            
+            String str1 = t1.isI18n() ? t1.getKey() : t1.getLabel();
+            String str2 = t2.isI18n() ? t2.getKey() : t2.getLabel();
+            
+            int compareTo = str1.toString().compareTo(str2.toString());
+            if (compareTo == 0)
+            {
+                // Content types have same keys but there are not equals, so do not return 0 to add it in TreeSet
+                // Indeed, in a TreeSet implementation two elements that are equal by the method compareTo are, from the standpoint of the set, equal 
+                return 1;
+            }
+            return compareTo;
+        }
+    }
+}
Index: main/plugin-cms/src/org/ametys/cms/content/MetadataSetDefGenerator.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/MetadataSetDefGenerator.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/MetadataSetDefGenerator.java	(working copy)
@@ -32,8 +32,8 @@
 import org.xml.sax.SAXException;
 
 import org.ametys.cms.contenttype.AbstractMetadataSetElement;
-import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.contenttype.Fieldset;
 import org.ametys.cms.contenttype.MetadataDefinition;
 import org.ametys.cms.contenttype.MetadataDefinitionReference;
@@ -44,7 +44,6 @@
 import org.ametys.cms.contenttype.RichTextMetadataDefinition;
 import org.ametys.cms.contenttype.SemanticAnnotation;
 import org.ametys.cms.repository.Content;
-import org.ametys.plugins.repository.AmetysRepositoryException;
 import org.ametys.runtime.util.I18nizableText;
 import org.ametys.runtime.util.parameter.Enumerator;
 import org.ametys.runtime.util.parameter.ParameterHelper;
@@ -59,6 +58,8 @@
     protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
     /** Metadata manager. */
     protected MetadataManager _metadataManager;
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypesHelper;
     
     @Override
     public void service(ServiceManager serviceManager) throws ServiceException
@@ -66,6 +67,7 @@
         super.service(serviceManager);
         _contentTypeExtensionPoint = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
         _metadataManager = (MetadataManager) serviceManager.lookup(MetadataManager.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
     }
     
     @Override
@@ -108,23 +110,21 @@
      */
     protected void _saxMetadataSet(Content content, String metadataSetName, boolean isEditionMetadataSet) throws SAXException, ProcessingException, IOException
     {
-        ContentType contentType = _contentTypeExtensionPoint.getExtension(content.getType());
-        
         MetadataSet metadataSet = null;
         
         if (isEditionMetadataSet)
         {
-            metadataSet = contentType.getMetadataSetForEdition(metadataSetName);
+            metadataSet = _contentTypesHelper.getMetadataSetForEdition(metadataSetName, content.getTypes(), content.getMixinTypes());
         }
         else
         {
-            metadataSet = contentType.getMetadataSetForView(metadataSetName);
+            metadataSet = _contentTypesHelper.getMetadataSetForView(metadataSetName, content.getTypes(), content.getMixinTypes());
         }
         
         if (metadataSet == null)
         {
             throw new ProcessingException(String.format("Unknown metadata set '%s' of type '%s' for content type '%s'",
-                                          metadataSetName, isEditionMetadataSet ? "edition" : "view", contentType.getId())); 
+                                          metadataSetName, isEditionMetadataSet ? "edition" : "view", StringUtils.join(content.getTypes(), ',')));
         }
         
         _saxMetadataSetElement(content, null, metadataSet);
@@ -152,12 +152,10 @@
                     throw new IllegalArgumentException("Unable to get the metadata definition of metadata \"" + metadataName + "\"");
                 }
                 
-                ContentType contentType = _contentTypeExtensionPoint.getExtension(content.getType());
-                
                 String metadataPath = StringUtils.stripStart(metaDef.getId(), "/");
-                if (contentType.canRead(content, metadataPath))
+                if (_contentTypesHelper.canRead(content, metadataPath))
                 {
-                    _saxMetadataDefinition(contentType, content, metadataDefRef, metaDef, metadataPath);
+                    _saxMetadataDefinition(content, metadataDefRef, metaDef, metadataPath);
                 }
             }
             else
@@ -191,15 +189,7 @@
         {
             if (content != null)
             {
-                String contentTypeId = content.getType();
-                ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
-                
-                if (contentType == null)
-                {
-                    throw new AmetysRepositoryException("Unknown content type: " + contentTypeId);
-                }
-                
-                metadataDefinition =  contentType.getMetadataDefinition(metadataName);
+                metadataDefinition = _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
             }
         }
         else
@@ -212,14 +202,13 @@
 
     /**
      * Sax the metadata definition
-     * @param contentType The content type
      * @param content The content
      * @param metadataSetElement The metadata set element
      * @param metaDef The metadata definition
      * @param metadataPath Path of the metadata. Can not be null.
      * @throws SAXException if an error occurs while SAXing.
      */
-    protected void _saxMetadataDefinition(ContentType contentType, Content content, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metaDef, String metadataPath) throws SAXException
+    protected void _saxMetadataDefinition(Content content, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metaDef, String metadataPath) throws SAXException
     {
         XMLUtils.startElement(contentHandler, metaDef.getName());
         
@@ -315,7 +304,7 @@
             XMLUtils.endElement(contentHandler, "enumeration");
         }
 
-        if (!contentType.canWrite(content, metadataPath))
+        if (!_contentTypesHelper.canWrite(content, metadataPath))
         {
             XMLUtils.createElement(contentHandler, "can-not-write", "true");
         }
Index: main/plugin-cms/src/org/ametys/cms/content/GetWrapperContextAction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/GetWrapperContextAction.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/GetWrapperContextAction.java	(working copy)
@@ -16,7 +16,9 @@
 package org.ametys.cms.content;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.avalon.framework.parameters.Parameters;
@@ -30,6 +32,7 @@
 import org.apache.excalibur.source.Source;
 import org.apache.excalibur.source.SourceResolver;
 
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.repository.Content;
 
 /**
@@ -42,11 +45,13 @@
 {
     /** The source resolver */
     protected SourceResolver _srcResolver;
+    private ContentTypesHelper _contentTypesHelper;
     
     @Override
     public void service(ServiceManager manager) throws ServiceException
     {
         _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
+        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
     }
     
     @Override
@@ -71,38 +76,51 @@
      */
     protected String getWrapperURI(Request request, String uri) throws IOException
     {
-        String wrapperURI = null;
-        
-        Content content = (Content) request.getAttribute(Content.class.getName());
-        String cType = content.getType();
-        
-        // Ex : context://WEB-INF/stylesheets/content/_wrapper/content-[cType].xsl
-        int index = uri.indexOf(".xsl");
-        wrapperURI = "context://WEB-INF/" + uri.substring(0, index) + "-" + cType + ".xsl";
-        Source src = _srcResolver.resolveURI(wrapperURI);
-        
-        if (!src.exists())
+        for (String wrapperURI : getSourceURIs(request, uri))
         {
-            if (getLogger().isDebugEnabled())
-            {
-                getLogger().debug("Failed to find a file at '" + wrapperURI + "'");
-            }
-            
-            // Ex : context://WEB-INF/stylesheets/content/_wrapper/content.xsl
-            wrapperURI = "context://WEB-INF/" + uri;
-            src = _srcResolver.resolveURI(wrapperURI);
+            Source src = _srcResolver.resolveURI(wrapperURI);
             if (!src.exists())
             {
                 if (getLogger().isDebugEnabled())
                 {
-                    getLogger().debug("Failed to find a file at '" + wrapperURI + "'");
+                    getLogger().debug("Failed to find a stylesheet at '" + wrapperURI + "'.");
                 }
-                
-                // Ex : worspace:workspaceName://stylesheets/content/_wrapper/content.xsl
-                wrapperURI = "workspace:" + request.getAttribute("workspaceName") + "://" + uri;
+            }
+            else
+            {
+                return wrapperURI;
             }
         }
         
-        return wrapperURI;
+        return null;
+    }
+    
+    /**
+     * Get the source URI for wrapped content
+     * @param request The request
+     * @param requestedURI The requested URI
+     * @return the wrapped URI
+     */
+    protected List<String> getSourceURIs (Request request, String requestedURI)
+    {
+        Content content = (Content) request.getAttribute(Content.class.getName());
+        
+        List<String> sourceURIs = new ArrayList<String>();
+        
+        // Ex: context://WEB-INF/stylesheets/content/_wrapper/content-[cType].xsl 
+        int index = requestedURI.indexOf(".xsl");
+        
+        String cTypeId = _contentTypesHelper.getContentTypeIdForRendering(content);
+        sourceURIs.add("context://WEB-INF/" + requestedURI.substring(0, index) + "-" + cTypeId + ".xsl");
+        
+        cTypeId = _contentTypesHelper.getFirstContentType(content).getId();
+        sourceURIs.add("context://WEB-INF/" + requestedURI.substring(0, index) + "-" + cTypeId + ".xsl");
+        
+        sourceURIs.add("context://WEB-INF/" + requestedURI);
+        
+        sourceURIs.add("workspace:" + request.getAttribute("workspaceName") + "://" + requestedURI);
+        
+        return sourceURIs;
+        
     }
 }
Index: main/plugin-cms/src/org/ametys/cms/content/GetContentAction.java
===================================================================
--- main/plugin-cms/src/org/ametys/cms/content/GetContentAction.java	(revision 25585)
+++ main/plugin-cms/src/org/ametys/cms/content/GetContentAction.java	(working copy)
@@ -27,8 +27,8 @@
 import org.apache.cocoon.environment.Request;
 import org.apache.cocoon.environment.SourceResolver;
 
-import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
+import org.ametys.cms.contenttype.ContentTypesHelper;
 import org.ametys.cms.repository.Content;
 import org.ametys.plugins.repository.AmetysObjectResolver;
 import org.ametys.plugins.repository.version.VersionableAmetysObject;
@@ -51,12 +51,16 @@
     protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
     /** The ametys object resolver */
     protected AmetysObjectResolver _resolver;
+    /** Helper for content types */
+    protected ContentTypesHelper _contentTypeshelper;
+    
     @Override
     public void service(ServiceManager serviceManager) throws ServiceException
     {
         super.service(serviceManager);
         _contentTypeExtensionPoint = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
         _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
+        _contentTypeshelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
     }
 
     public Map<String, String> act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
@@ -97,11 +101,10 @@
             result.put(RESULT_RENDERINGLANGUAGE, (String) request.getAttribute(RESULT_RENDERINGLANGUAGE));
         }
 
-        String contentTypeId = content.getType();
-        ContentType cType = _contentTypeExtensionPoint.getExtension(contentTypeId);
-
+        String contentTypeId = _contentTypeshelper.getContentTypeIdForRendering(content);
+        
         result.put(RESULT_CONTENTTYPE, contentTypeId);
-        result.put(RESULT_PLUGINNAME, cType.getPluginName());
+        result.put(RESULT_PLUGINNAME, _contentTypeshelper.getContentTypePluginForRendering(content));
         result.put(RESULT_CONTENTVERSION, revision);
         
         return result;
Index: main/plugin-cms/stylesheets/default-content.xsl
===================================================================
--- main/plugin-cms/stylesheets/default-content.xsl	(revision 25585)
+++ main/plugin-cms/stylesheets/default-content.xsl	(working copy)
@@ -99,11 +99,19 @@
     
     <xsl:template name="display-content-metadata">
     	<xsl:variable name="contentId" select="@id" />
-    	<xsl:variable name="contentTypeId" select="ametys:contentType($contentId)" />
+    	<xsl:variable name="contentTypeIds" select="ametys:contentTypes($contentId)" />
     	
-    	<xsl:variable name="contentTypeTags" select="ametys:contentTypeTags($contentTypeId)" />
+    	<xsl:variable name="isSimple">
+	    	<xsl:for-each select="ametys:contentTypes($contentId)">
+	    		<xsl:choose>
+		    		<xsl:when test="ametys:contentTypeTags(@id)[@name = 'simple']">true</xsl:when>
+		    		<xsl:otherwise></xsl:otherwise>
+		    	</xsl:choose>
+	    	</xsl:for-each>
+    	</xsl:variable>
+    	
     	<xsl:choose>
-    		<xsl:when test="$contentTypeTags[@name = 'simple']">
+    		<xsl:when test="$isSimple != ''">
     			<xsl:value-of select="@title"/>
     		</xsl:when>
     		<xsl:otherwise>
Index: main/plugin-cms/pages/properties/view.xsl
===================================================================
--- main/plugin-cms/pages/properties/view.xsl	(revision 25585)
+++ main/plugin-cms/pages/properties/view.xsl	(working copy)
@@ -77,45 +77,82 @@
 				<span style="background: url('{$contextPath}{content/workflow-step/@icon-small}') left center no-repeat; padding-left: 20px; line-height: 16px; display: inline-block;"><xsl:value-of select="content/workflow-step"/></span>
 			</p>
 			
-			<p>
-				<span class="title"><i18n:text i18n:key="UITOOL_DETAILS_CONTENT_CONTENT_TYPE"/></span>
-			</p>
+			<xsl:call-template name="content-types"/>
+			<xsl:call-template name="mixins"/>
 			
-			<ul>
+		</div>
+	</xsl:template>
+	
+	<!-- Content Types -->
+	<xsl:template name="content-types">
+		<p>
+			<span class="title">
+				<xsl:choose>
+					<xsl:when test="count(content/content-types/content-type) = 1"><i18n:text i18n:key="UITOOL_DETAILS_CONTENT_CONTENT_TYPE"/></xsl:when>
+					<xsl:otherwise><i18n:text i18n:key="UITOOL_DETAILS_CONTENT_CONTENT_TYPES"/></xsl:otherwise>
+				</xsl:choose>
+			</span>
+			
+			<xsl:if test="content/content-types/content-type/content-type">
+				<xsl:text> </xsl:text><a class="showhide" href="javascript:Ametys.plugins.cms.content.tool.PropertiesTool.showHideSubTypes()">(...)</a>
+			</xsl:if>
+		</p>
+		
+		<ul>
+			<xsl:for-each select="content/content-types/content-type">
 				<li>
-					<img src="{$contextPath}{content/content-type/@icon-small}" alt="" title="{content/content-type}"/><xsl:value-of select="content/content-type"/>
-					<xsl:if test="content/super-types/super-type">
-						<ul>
-							<xsl:apply-templates select="content/super-types/super-type">
-								<xsl:with-param name="level" select="1"/>
-							</xsl:apply-templates>
+					<img src="{$contextPath}{@icon}" alt="" title="{@label}"/><xsl:value-of select="@label"/>
+					<xsl:if test="content-type">
+						<ul class="subtypes hidden">
+							<xsl:apply-templates select="content-type"/>
 						</ul>
 					</xsl:if>
 				</li>
+			</xsl:for-each>
+		</ul>
+	</xsl:template>
+	
+	<!-- Mixins -->
+	<xsl:template name="mixins">
+		<xsl:if test="content/mixins/mixin">
+			<p>
+				<span class="title">
+					<xsl:choose>
+						<xsl:when test="count(content/mixins/mixin) = 1"><i18n:text i18n:key="UITOOL_DETAILS_CONTENT_MIXIN"/></xsl:when>
+						<xsl:otherwise><i18n:text i18n:key="UITOOL_DETAILS_CONTENT_MIXINS"/></xsl:otherwise>
+					</xsl:choose>
+				</span>
+				
+				<xsl:if test="content/mixins/mixin/mixin">
+					<xsl:text> </xsl:text><a class="showhide" href="javascript:Ametys.plugins.cms.content.tool.PropertiesTool.showHideMixins()">(...)</a>
+				</xsl:if>
+			</p>
+			<ul>
+				<xsl:for-each select="content/mixins/mixin">
+					<li>
+						<img src="{$contextPath}{@icon}" alt="" title="{@label}"/><xsl:value-of select="@label"/>
+						<xsl:if test="mixin">
+							<ul class="mixins hidden">
+								<xsl:apply-templates select="mixin"/>
+							</ul>
+						</xsl:if>
+					</li>
+				</xsl:for-each>
 			</ul>
-		</div>
+		</xsl:if>
 	</xsl:template>
 	
-	<xsl:template match="super-type">
-		<xsl:param name="level"/>
-		
-		<xsl:variable name="padding" select="$level * 20"/>
-		<xsl:variable name="bgposition" select="($level - 1) * 20"/>
-		
-		<!-- <li style="padding-left: {$padding}px; background-position: {$bgposition}px center"> -->
+	<xsl:template match="content-type|mixin">
 		<li>
 			<xsl:attribute name="class">content-type<xsl:if test="position() = last()"> last</xsl:if></xsl:attribute>
 			<img src="{$contextPath}{@icon}" alt="" title="{@label}"/><xsl:value-of select="@label"/>
 			
-			<xsl:if test="super-type">
+			<xsl:if test="content-type|mixin">
 				<ul>
-					<xsl:apply-templates select="super-type">
-						<xsl:with-param name="level" select="$level + 1"/>
-					</xsl:apply-templates>
+					<xsl:apply-templates select="content-type|mixin"/>
 				</ul>
 			</xsl:if>
 		</li>
-					
 	</xsl:template>
 	
 	<xsl:template match="*" mode="metadata-set">
@@ -167,10 +204,18 @@
 		<xsl:choose>
 			<xsl:when test="$type = 'CONTENT' or $type = 'SUB_CONTENT'">
 				<xsl:variable name="contentId" select="@id" />
-    			<xsl:variable name="contentTypeId" select="ametys:contentType($contentId)" />
-				<xsl:variable name="contentTypeTags" select="ametys:contentTypeTags($contentTypeId)" />
+				
+				<xsl:variable name="isSimple">
+			    	<xsl:for-each select="ametys:contentTypes($contentId)">
+			    		<xsl:choose>
+				    		<xsl:when test="ametys:contentTypeTags(@id)[@name = 'simple']">true</xsl:when>
+				    		<xsl:otherwise></xsl:otherwise>
+				    	</xsl:choose>
+			    	</xsl:for-each>
+		    	</xsl:variable>
+    	
 		    	<xsl:choose>
-		    		<xsl:when test="$contentTypeTags[@name = 'simple']">
+		    		<xsl:when test="$isSimple != ''">
 		    			<xsl:value-of select="@title"/>
 		    		</xsl:when>
 		    		<xsl:otherwise>
Index: main/plugin-cms/plugin.xml
===================================================================
--- main/plugin-cms/plugin.xml	(revision 25585)
+++ main/plugin-cms/plugin.xml	(working copy)
@@ -92,6 +92,12 @@
             <!-- Extension point for defining a content type -->
         </extension-point>
         
+        <!-- FIXME RUNTIME-1023 -->
+        <extension-point id="org.ametys.cms.contenttype.DynamicContentTypeExtentionPoint"
+                         class="org.ametys.cms.contenttype.DynamicContentTypeDescriptorExtentionPoint">
+        </extension-point>
+        
+        
         <extension-point id="org.ametys.cms.transformation.URIResolverExtensionPoint"
                          class="org.ametys.cms.transformation.URIResolverExtensionPoint">
             <!-- Extension point for resolving links from the inline editor -->
@@ -507,6 +513,7 @@
 					<file>js/Ametys/cms/form/widget/SelectContent.i18n.js</file>
 					<file>js/Ametys/cms/form/widget/SelectSimpleContent.js</file>
 					<file>js/Ametys/cms/form/widget/ComposeContent.i18n.js</file>
+					<file>js/Ametys/cms/form/widget/FlipFlap.i18n.js</file>
 					<file>js/Ametys/cms/uihelper/ChooseLocation.i18n.js</file>
 					<file>js/Ametys/cms/uihelper/ChooseLocation/LoadGoogleMaps.i18n.js</file>
 					
@@ -4446,7 +4453,7 @@
     	<extensions>
     		<extension id="org.ametys.cms.CreateContentMenu" 
     				   point="org.ametys.cms.ribbon.RibbonControlsManager"
-    				   class="org.ametys.cms.clientsideelement.CreateContentMenu">
+    				   class="org.ametys.cms.clientsideelement.ContentTypesGallery">
 				<class name="Ametys.ribbon.element.ui.ButtonController">
 					<label i18n="true">PLUGINS_CMS_CONTENT_CREATECONTENTMENU_LABEL</label>
 					<description i18n="true">PLUGINS_CMS_CONTENT_CREATECONTENTMENU_DESCRIPTION</description>
@@ -5473,6 +5480,13 @@
         </extensions>
     </feature>
     
+    <feature name="cliensideelement.helper">
+    	<components>
+    		<component class="org.ametys.cms.clientsideelement.content.SmartContentClientSideElementHelper" 
+    				   role="org.ametys.cms.clientsideelement.content.SmartContentClientSideElementHelper"/>
+    	</components>
+    </feature>
+    
     <feature name="linksresolver">
     	<components>
     		<component class="org.ametys.cms.transformation.xslt.ResolveURIComponent" role="org.ametys.cms.transformation.xslt.ResolveURIComponent"/>
Index: main/plugin-cms/i18n/messages_en.xml
===================================================================
--- main/plugin-cms/i18n/messages_en.xml	(revision 25585)
+++ main/plugin-cms/i18n/messages_en.xml	(working copy)
@@ -116,6 +116,7 @@
     <message key="UITOOL_SEARCH_CONTENT_LANGUAGE">Language</message>
     <message key="UITOOL_SEARCH_CONTENT_FULLTEXT">Full text</message>
     <message key="UITOOL_SEARCH_CONTENT_CONTENT_TYPE">Content type</message>
+    <message key="UITOOL_SEARCH_CONTENT_MIXIN">Mixin</message>
     <message key="UITOOL_SEARCH_CONTENT_LASTMODIFIED">Last modified</message>
     <message key="UITOOL_SEARCH_CONTENT_LASTMODIFIED_BEFORE">Modified before</message>
     <message key="UITOOL_SEARCH_CONTENT_LASTMODIFIED_AFTER">Modified after</message>
@@ -690,6 +691,55 @@
     <message key="CONTENT_UNLOCK_LABEL">Content locked</message>
     <message key="CONTENT_UNLOCK_DESCRIPTION">Not yet implemented.</message>
 
+	<!-- ADD / REMOVE CONTENT TYPE -->
+	<message key="CONTENT_SET_CONTENT_TYPE_LABEL">Content type</message>
+    
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_NOCONTENT">This button is disabled because it allows you update content types and no content is currently selected.</message>
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_REFRESHING">Please wait... On-going operations on server.</message>
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_MANYCONTENT">Action inactive as current selection include more than one content.&lt;br/&gt;Please select only one content.</message>
+    
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_ERROR">This button is disabled.</message>
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_START">Click here to add or delete a content type to the content </message>
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_CONTENT">'{0}'</message>
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_END">.</message>
+
+	<message key="CONTENT_ADD_CONTENT_TYPE_ERROR_TITLE">Unable to add the content type</message>
+    <message key="CONTENT_ADD_CONTENT_TYPE_ERROR_NON_MODIFIABLE_CONTENT">Unable to add content type: content is not editable</message>
+    <message key="CONTENT_ADD_CONTENT_TYPE_ERROR_INVALID_CONTENT_TYPE">The selected content type is not compatible with the selected content: the content type declares at least one metadata already defined in the content through one of its content types or mixins.</message>
+    <message key="CONTENT_ADD_CONTENT_TYPE_ERROR_NO_CONTENT_TYPE">The selected content type is a 'mixin', it can not be added as a content type.</message>
+    <message key="CONTENT_ADD_CONTENT_TYPE_ERROR">An error has occurred: unable to add content type.</message>
+    
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_CONFIRM_TITLE">Delete content type</message>
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_CONFIRM">Are you sure you want to delete this content type?</message>
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_ERROR_TITLE">Failed to delete</message>
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_ERROR">An error has occurred: can not delete content type</message>
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_ERROR_NON_MODIFIABLE_CONTENT">Unable to delete content type: content is not editable</message>
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_ERROR_EMPTY_LIST">Unable to delete content type: content must be linked to at least one type of content</message>
+    
+    <!-- ADD / REMOVE CONTENT TYPE -->
+	<message key="CONTENT_SET_MIXIN_LABEL">Mixin</message>
+    
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_NOCONTENT">This button is disabled because it allows you update mixin types and no content is currently selected.</message>
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_REFRESHING">Please wait... On-going operations on server.</message>
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_MANYCONTENT">Action inactive as current selection include more than one content.&lt;br/&gt;Please select only one content.</message>
+    
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_ERROR">This button is disabled.</message>
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_START">Click here to add or delete a mixin type to the content </message>
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_CONTENT">'{0}'</message>
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_END">.</message>
+
+	<message key="CONTENT_ADD_MIXIN_ERROR_TITLE">Unable to add mixin</message>
+    <message key="CONTENT_ADD_MIXIN_ERROR_NON_MODIFIABLE_CONTENT">Unable to add mixin: content is not editable</message>
+    <message key="CONTENT_ADD_MIXIN_ERROR_INVALID_MIXIN">The selected mixin is not compatible with the selected content: the mixin declares at least one metadata already defined in the content through one of its content types or mixins.</message>
+    <message key="CONTENT_ADD_MIXIN_ERROR_NO_MIXIN">The selected content type is not 'mixin', it can not be added as a mixin.</message>
+    <message key="CONTENT_ADD_MIXIN_ERROR">An error has occurred: unable to add mixin.</message>
+    
+    <message key="CONTENT_REMOVE_MIXIN_CONFIRM_TITLE">Delete mixin</message>
+    <message key="CONTENT_REMOVE_MIXIN_CONFIRM">Are you sure you want to delete this mixin?</message>
+    <message key="CONTENT_REMOVE_MIXIN_ERROR_TITLE">Failed to delete</message>
+    <message key="CONTENT_REMOVE_MIXIN_ERROR">An error has occurred: can not delete mixin</message>
+    <message key="CONTENT_REMOVE_MIXIN_ERROR_NON_MODIFIABLE_CONTENT">Unable to delete mixin: content is not editable</message>
+    
 	<!-- DELETE -->
 	<message key="CONTENT_DELETE_LABEL">Delete</message>
     
Index: main/plugin-cms/i18n/messages_fr.xml
===================================================================
--- main/plugin-cms/i18n/messages_fr.xml	(revision 25585)
+++ main/plugin-cms/i18n/messages_fr.xml	(working copy)
@@ -116,6 +116,7 @@
     <message key="UITOOL_SEARCH_CONTENT_LANGUAGE">Langue</message>
     <message key="UITOOL_SEARCH_CONTENT_FULLTEXT">Texte libre</message>
     <message key="UITOOL_SEARCH_CONTENT_CONTENT_TYPE">Type de contenu</message>
+    <message key="UITOOL_SEARCH_CONTENT_MIXIN">Rôle</message>
     <message key="UITOOL_SEARCH_CONTENT_LASTMODIFIED">Dernière modification</message>
     <message key="UITOOL_SEARCH_CONTENT_LASTMODIFIED_BEFORE">Modifié avant le</message>
     <message key="UITOOL_SEARCH_CONTENT_LASTMODIFIED_AFTER">Modifié après le</message>
@@ -403,6 +404,9 @@
     <message key="UITOOL_DETAILS_CONTENT_LANGUAGE">Langue</message>
     <message key="UITOOL_DETAILS_CONTENT_WORKFLOW_STEP">Cycle de vie</message>
     <message key="UITOOL_DETAILS_CONTENT_CONTENT_TYPE">Type de contenu</message>
+    <message key="UITOOL_DETAILS_CONTENT_CONTENT_TYPES">Types de contenu</message>
+    <message key="UITOOL_DETAILS_CONTENT_MIXIN">Rôle</message>
+    <message key="UITOOL_DETAILS_CONTENT_MIXINS">Rôles</message>
     <message key="UITOOL_DETAILS_ERROR">Une erreur est survenue : elle a empêché d'afficher les propriétés du contenu</message>
     <message key="UITOOL_DETAILS_DATE_FORMAT">j F Y à H:i</message>
     <message key="UITOOL_DETAILS_BOOLEAN_TRUE">Oui</message>
@@ -689,6 +693,55 @@
     <message key="CONTENT_UNLOCK_LABEL">Contenu verrouillé</message>
     <message key="CONTENT_UNLOCK_DESCRIPTION">Ce bouton est désactivé dans cette version de développement.</message>
 
+	<!-- ADD / REMOVE CONTENT TYPE -->
+	<message key="CONTENT_SET_CONTENT_TYPE_LABEL">Type de contenu</message>
+    
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_NOCONTENT">Ce bouton est désactivé car il permet d'ajouter des types de contenus et actuellement aucun contenu n'est sélectionné.</message>
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_REFRESHING">Veuillez patienter... ce bouton est désactivé pendant que des vérifications sont en cours sur le serveur.</message>
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_MANYCONTENT">Ce bouton est désactivé car plusieurs contenus sont actuellement sélectionnés.&lt;br/&gt;Veuillez ne sélectionner qu'un seul contenu pour continuer.</message>
+    
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_ERROR">Ce bouton est désactivé.</message>
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_START">Cliquez ici pour ajouter ou supprimer un type de contenu pour le contenu </message>
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_CONTENT">'{0}'</message>
+    <message key="CONTENT_SET_CONTENT_TYPE_DESCRIPTION_END">.</message>
+
+	<message key="CONTENT_ADD_CONTENT_TYPE_ERROR_TITLE">Impossible d'ajouter le type de contenu</message>
+    <message key="CONTENT_ADD_CONTENT_TYPE_ERROR_NON_MODIFIABLE_CONTENT">Impossible d'ajouter le type de contenu: le contenu n'est pas modifiable</message>
+    <message key="CONTENT_ADD_CONTENT_TYPE_ERROR_INVALID_CONTENT_TYPE">Le type de contenu choisi n'est pas compatible avec le contenu sélectionné: le type de contenu possède au moins une métadonnée déjà définit sur le contenu au travers l'un de ces types de contenus ou rôles.</message>
+    <message key="CONTENT_ADD_CONTENT_TYPE_ERROR_NO_CONTENT_TYPE">Le type de contenu choisit est un 'rôle', il ne peut pas être ajouté en tant que type de contenu.</message>
+    <message key="CONTENT_ADD_CONTENT_TYPE_ERROR">Une erreur est survenue : impossible d'ajouter le type de contenu</message>
+    
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_CONFIRM_TITLE">Supprimer le type de contenu</message>
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_CONFIRM">Etes-vous sûrs de vouloir supprimer ce type de contenu ?</message>
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_ERROR_TITLE">Echec de la suppression</message>
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_ERROR">Une erreur est survenue : impossible de supprimer le type de contenu</message>
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_ERROR_NON_MODIFIABLE_CONTENT">Impossible de supprimer le type de contenu: le contenu n'est pas modifiable</message>
+    <message key="CONTENT_REMOVE_CONTENT_TYPE_ERROR_EMPTY_LIST">Impossible de supprimer le type de contenu: le contenu doit être lié à au moins un type de contenu</message>
+    
+    <!-- ADD / REMOVE CONTENT TYPE -->
+	<message key="CONTENT_SET_MIXIN_LABEL">Rôle</message>
+    
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_NOCONTENT">Ce bouton est désactivé car il permet d'ajouter des rôles d'un contenu et actuellement aucun contenu n'est sélectionné.</message>
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_REFRESHING">Veuillez patienter... ce bouton est désactivé pendant que des vérifications sont en cours sur le serveur.</message>
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_MANYCONTENT">Ce bouton est désactivé car plusieurs contenus sont actuellement sélectionnés.&lt;br/&gt;Veuillez ne sélectionner qu'un seul contenu pour continuer.</message>
+    
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_ERROR">Ce bouton est désactivé.</message>
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_START">Cliquez ici pour ajouter ou supprimer un rôle pour le contenu </message>
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_CONTENT">'{0}'</message>
+    <message key="CONTENT_SET_MIXIN_DESCRIPTION_END">.</message>
+
+	<message key="CONTENT_ADD_MIXIN_ERROR_TITLE">Impossible d'ajouter le rôle</message>
+    <message key="CONTENT_ADD_MIXIN_ERROR_NON_MODIFIABLE_CONTENT">Impossible d'ajouter le rôle: le contenu n'est pas modifiable</message>
+    <message key="CONTENT_ADD_MIXIN_ERROR_INVALID_MIXIN">Le rôle choisi n'est pas compatible avec le contenu sélectionné: le rôle possède au moins une métadonnée déjà définit sur le contenu au travers l'un de ces types de contenus ou rôles.</message>
+    <message key="CONTENT_ADD_MIXIN_ERROR_NO_MIXIN">Le type de contenu choisit n'est pas un 'rôle', il ne peut pas être ajouté en tant que 'rôle'.</message>
+    <message key="CONTENT_ADD_MIXIN_ERROR">Une erreur est survenue : impossible d'ajouter le rôle</message>
+    
+    <message key="CONTENT_REMOVE_MIXIN_CONFIRM_TITLE">Supprimer le rôle</message>
+    <message key="CONTENT_REMOVE_MIXIN_CONFIRM">Etes-vous sûrs de vouloir supprimer ce rôle ?</message>
+    <message key="CONTENT_REMOVE_MIXIN_ERROR_TITLE">Echec de la suppression</message>
+    <message key="CONTENT_REMOVE_MIXIN_ERROR">Une erreur est survenue : impossible de supprimer le rôle</message>
+    <message key="CONTENT_REMOVE_MIXIN_ERROR_NON_MODIFIABLE_CONTENT">Impossible de supprimer le rôle: le contenu n'est pas modifiable</message>
+    
 	<!-- DELETE -->
 	<message key="CONTENT_DELETE_LABEL">Supprimer</message>
     
@@ -2155,6 +2208,7 @@
 	<message key="RIBBON_TABS_TAB_CONTENT_LABEL">Contenu</message>
 	<message key="RIBBON_TABS_TAB_CONTENT_GROUPS_GROUP_TOOL_LABEL">Outils</message>
 	<message key="RIBBON_TABS_TAB_CONTENT_GROUPS_GROUP_ACTION_LABEL">Actions</message>
+	<message key="RIBBON_TABS_TAB_CONTENT_GROUPS_GROUP_CONTENT_TYPE_LABEL">Type</message>
 	<message key="RIBBON_TABS_TAB_CONTENT_GROUPS_GROUP_WORKFLOW_LABEL">Cycle de vie</message>
     <message key="RIBBON_TABS_TAB_CONTENT_GROUPS_GROUP_CLIPBOARD_LABEL">Presse-papiers</message>
 	
Index: main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/ContentTypesGalleryController.i18n.js
===================================================================
--- main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/ContentTypesGalleryController.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/ContentTypesGalleryController.i18n.js	(revision 0)
@@ -0,0 +1,74 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.
+ */
+
+/**
+ * @private
+ * This class control a ribbon menu representing the workflow state.
+ * It HAS to be used in conjunction with the java org.ametys.cms.clientsideelement.WorkflowMenu
+ */
+Ext.define('Ametys.plugins.cms.content.controller.ContentTypesGalleryController', {
+	extend: 'Ametys.plugins.cms.content.controller.SmartContentController',
+	
+	/**
+	 * @inheritdoc
+	 * Add a listener on 'menushow' event
+	 */
+	createUI: function ()
+	{
+		var elt = this.callParent(arguments);
+		elt.on ('menushow', this._onMenuShow, this);
+		return elt;
+	},
+	
+	/**
+	 * Listener on 'menushow' event<br>
+	 * @param {Ext.button.Button} btn The button
+	 * @param {Ext.menu.Menu} menu The menu
+	 * @private
+	 */
+	_onMenuShow: function (btn, menu)
+	{
+		var target = this.getMatchingTargets()[0];
+		
+		this._mask = Ext.create('Ext.LoadMask', {
+			target: menu.items.get(0),
+			msg: "<i18n:text i18n:key='KERNEL_LOADMASK_DEFAULT_MESSAGE' i18n:catalogue='kernel'/>"
+		});
+		this._mask.show();
+		
+		// Current content types can not be up-to-date, send request to update
+		var content = Ametys.cms.content.ContentDAO.getContent(target.getParameters().id, Ext.bind(this._fillGallery, this, [menu], 1));
+	},
+	
+	_fillGallery: function (content, menu)
+	{
+		var cTypes = content.getTypes();
+		
+		var menuGallery = menu.items.get(0);
+		
+		menuGallery.items.get(0).items.each(function (item) {
+			var cTypeId = Ametys.ribbon.RibbonManager.getElement(item.controlId).getInitialConfig('contentTypes');
+			var state = Ext.Array.contains(cTypes, cTypeId);
+			item.toggle(state, true);
+		});
+		
+		/*if (this._mask)
+		{
+			this._mask.hide();
+			this._mask = null;
+		}*/
+	}
+});
\ No newline at end of file
Index: main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/MixinGalleryController.i18n.js
===================================================================
--- main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/MixinGalleryController.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/MixinGalleryController.i18n.js	(revision 0)
@@ -0,0 +1,74 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.
+ */
+
+/**
+ * @private
+ * This class control a ribbon menu representing the workflow state.
+ * It HAS to be used in conjunction with the java org.ametys.cms.clientsideelement.WorkflowMenu
+ */
+Ext.define('Ametys.plugins.cms.content.controller.MixinGalleryController', {
+	extend: 'Ametys.plugins.cms.content.controller.SmartContentController',
+	
+	/**
+	 * @inheritdoc
+	 * Add a listener on 'menushow' event
+	 */
+	createUI: function ()
+	{
+		var elt = this.callParent(arguments);
+		elt.on ('menushow', this._onMenuShow, this);
+		return elt;
+	},
+	
+	/**
+	 * Listener on 'menushow' event<br>
+	 * @param {Ext.button.Button} btn The button
+	 * @param {Ext.menu.Menu} menu The menu
+	 * @private
+	 */
+	_onMenuShow: function (btn, menu)
+	{
+		var target = this.getMatchingTargets()[0];
+		
+		this._mask = Ext.create('Ext.LoadMask', {
+			target: menu.items.get(0),
+			msg: "<i18n:text i18n:key='KERNEL_LOADMASK_DEFAULT_MESSAGE' i18n:catalogue='kernel'/>"
+		});
+		this._mask.show();
+		
+		// Current content types can not be up-to-date, send request to update
+		var content = Ametys.cms.content.ContentDAO.getContent(target.getParameters().id, Ext.bind(this._fillGallery, this, [menu], 1));
+	},
+	
+	_fillGallery: function (content, menu)
+	{
+		var mixins = content.getMixins();
+		
+		var menuGallery = menu.items.get(0);
+		
+		menuGallery.items.get(0).items.each(function (item) {
+			var mixinId = Ametys.ribbon.RibbonManager.getElement(item.controlId).getInitialConfig('contentTypes');
+			var state = Ext.Array.contains(mixins, mixinId);
+			item.toggle(state, true);
+		});
+		
+		if (this._mask)
+		{
+			this._mask.hide();
+			this._mask = null;
+		}
+	}
+});
\ No newline at end of file
Index: main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/ContentTypeGalleryController.i18n.js
===================================================================
--- main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/ContentTypeGalleryController.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/ContentTypeGalleryController.i18n.js	(revision 0)
@@ -0,0 +1,74 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.
+ */
+
+/**
+ * @private
+ * This class control a ribbon menu representing the workflow state.
+ * It HAS to be used in conjunction with the java org.ametys.cms.clientsideelement.WorkflowMenu
+ */
+Ext.define('Ametys.plugins.cms.content.controller.ContentTypeGalleryController', {
+	extend: 'Ametys.plugins.cms.content.controller.SmartContentController',
+	
+	/**
+	 * @inheritdoc
+	 * Add a listener on 'menushow' event
+	 */
+	createUI: function ()
+	{
+		var elt = this.callParent(arguments);
+		elt.on ('menushow', this._onMenuShow, this);
+		return elt;
+	},
+	
+	/**
+	 * Listener on 'menushow' event<br>
+	 * @param {Ext.button.Button} btn The button
+	 * @param {Ext.menu.Menu} menu The menu
+	 * @private
+	 */
+	_onMenuShow: function (btn, menu)
+	{
+		var target = this.getMatchingTargets()[0];
+		
+		this._mask = Ext.create('Ext.LoadMask', {
+			target: menu.items.get(0),
+			msg: "<i18n:text i18n:key='KERNEL_LOADMASK_DEFAULT_MESSAGE' i18n:catalogue='kernel'/>"
+		});
+		this._mask.show();
+		
+		// Current content types can not be up-to-date, send request to update
+		var content = Ametys.cms.content.ContentDAO.getContent(target.getParameters().id, Ext.bind(this._fillGallery, this, [menu], 1));
+	},
+	
+	_fillGallery: function (content, menu)
+	{
+		var cTypes = content.getTypes();
+		
+		var menuGallery = menu.items.get(0);
+		
+		menuGallery.items.get(0).items.each(function (item) {
+			var cTypeId = Ametys.ribbon.RibbonManager.getElement(item.controlId).getInitialConfig('contentTypes');
+			var state = Ext.Array.contains(cTypes, cTypeId);
+			item.toggle(state, true);
+		});
+		
+		if (this._mask)
+		{
+			this._mask.hide();
+			this._mask = null;
+		}
+	}
+});
\ No newline at end of file
Index: main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/MixinsGalleryController.i18n.js
===================================================================
--- main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/MixinsGalleryController.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/Ametys/plugins/cms/content/controller/MixinsGalleryController.i18n.js	(revision 0)
@@ -0,0 +1,74 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.
+ */
+
+/**
+ * @private
+ * This class control a ribbon menu representing the workflow state.
+ * It HAS to be used in conjunction with the java org.ametys.cms.clientsideelement.WorkflowMenu
+ */
+Ext.define('Ametys.plugins.cms.content.controller.MixinsGalleryController', {
+	extend: 'Ametys.plugins.cms.content.controller.SmartContentController',
+	
+	/**
+	 * @inheritdoc
+	 * Add a listener on 'menushow' event
+	 */
+	createUI: function ()
+	{
+		var elt = this.callParent(arguments);
+		elt.on ('menushow', this._onMenuShow, this);
+		return elt;
+	},
+	
+	/**
+	 * Listener on 'menushow' event<br>
+	 * @param {Ext.button.Button} btn The button
+	 * @param {Ext.menu.Menu} menu The menu
+	 * @private
+	 */
+	_onMenuShow: function (btn, menu)
+	{
+		var target = this.getMatchingTargets()[0];
+		
+		this._mask = Ext.create('Ext.LoadMask', {
+			target: menu.items.get(0),
+			msg: "<i18n:text i18n:key='KERNEL_LOADMASK_DEFAULT_MESSAGE' i18n:catalogue='kernel'/>"
+		});
+		this._mask.show();
+		
+		// Current content types can not be up-to-date, send request to update
+		var content = Ametys.cms.content.ContentDAO.getContent(target.getParameters().id, Ext.bind(this._fillGallery, this, [menu], 1));
+	},
+	
+	_fillGallery: function (content, menu)
+	{
+		var mixins = content.getMixins();
+		
+		var menuGallery = menu.items.get(0);
+		
+		menuGallery.items.get(0).items.each(function (item) {
+			var cTypeId = Ametys.ribbon.RibbonManager.getElement(item.controlId).getInitialConfig('contentTypes');
+			var state = Ext.Array.contains(mixins, cTypeId);
+			item.toggle(state, true);
+		});
+		
+		if (this._mask)
+		{
+			this._mask.hide();
+			this._mask = null;
+		}
+	}
+});
\ No newline at end of file
Index: main/plugin-cms/resources/js/Ametys/plugins/cms/content/tool/PropertiesTool.i18n.js
===================================================================
--- main/plugin-cms/resources/js/Ametys/plugins/cms/content/tool/PropertiesTool.i18n.js	(revision 25585)
+++ main/plugin-cms/resources/js/Ametys/plugins/cms/content/tool/PropertiesTool.i18n.js	(working copy)
@@ -28,6 +28,30 @@
 	 * @property {String} _metadataSetName The metadata set to display. See #cfg-metadata-set-name.
 	 */
 	
+	statics: {
+		
+		showHideSubTypes: function ()
+		{
+			var show = false;
+			
+			Ext.select('.details-view-card ul.subtypes').each (function (ul) {
+				show = Ext.get(ul).hasCls('hidden');
+				Ext.get(ul).toggleCls('hidden');
+			});
+			
+			Ext.select('.details-view-card a.showhide-subtypes', true).each (function (a){
+				a.update(show ? 'replier tout' : 'deplier tout')
+			})
+		},
+
+		showHideMixins: function ()
+		{
+			Ext.select('.details-view-card ul.mixins').each (function (ul) {
+				Ext.get(ul).toggleCls('hidden');
+			});
+		}
+	},
+	
 	constructor: function(config)
 	{
 		this.callParent(arguments);
Index: main/plugin-cms/resources/js/Ametys/plugins/cms/content/actions/ContentTypeActions.i18n.js
===================================================================
--- main/plugin-cms/resources/js/Ametys/plugins/cms/content/actions/ContentTypeActions.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/Ametys/plugins/cms/content/actions/ContentTypeActions.i18n.js	(revision 0)
@@ -0,0 +1,455 @@
+/*
+ *  Copyright 2011 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.
+ */
+
+/**
+ * Singleton class to create a new content. 
+ * @private
+ */
+Ext.define('Ametys.plugins.cms.content.actions.ContentTypeActions', {
+	singleton: true,
+
+	/**
+	 * The default workflow action id
+	 * @private
+	 */
+	_DEFAULT_WORKFLOW_ACTION_ID: 220,
+	
+	/**
+	 * Action function to be called by the controller.
+	 * Add or remove the selected content type on concerned content
+	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller calling this function
+	 */
+	setContentType: function (controller)
+	{
+		var btnConfig = controller.getInitialConfig();
+		
+		var parentController = Ametys.ribbon.RibbonManager.getElement(btnConfig.controllerParentId);
+		var contentIds = parentController.getContentIds();
+		
+		if (parentController.getContentIds().length > 0)
+		{
+			var contentType = btnConfig['contentTypes'] || btnConfig['contentType'];
+			var actionId = btnConfig['workflowActionId'] ? parseInt(btnConfig['workflowActionId']) : this._DEFAULT_WORKFLOW_ACTION_ID;
+			var contentId = parentController.getContentIds()[0];
+			
+			if (controller.getUIControls().get(0).pressed)
+			{
+				this.removeContentType(contentId, contentType, actionId);
+			}
+			else
+			{
+				this.addContentType(contentId, contentType, actionId);
+			}
+		}
+	},
+	
+	/**
+	 * Add a content type on concerned content
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} contentType The content type to add
+	 * @param {Number} actionId The workflow action id
+	 */
+	addContentType: function (contentId, contentType, actionId)
+	{
+		Ametys.data.ServerComm.send({
+            plugin: 'cms', 
+            url: 'content/add-content-type', 
+            parameters: {
+            	contentId: contentId,
+            	contentType: contentType,
+            	actionId: actionId,
+            },
+            priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
+            callback: {
+                scope: this,
+                handler: this._addContentTypeCb,
+                arguments: {
+                	contentId: contentId
+                }
+            },
+            responseType: 'text'
+        });
+	},
+	
+	/**
+	 * Remove a content type on concerned content
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} contentType The content type to remove
+	 * @param {Number} actionId The workflow action id
+	 */
+	removeContentType: function (contentId, contentType, actionId)
+	{
+		Ext.Msg.confirm("<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_CONFIRM_TITLE'/>", 
+				"<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_CONFIRM'/>",
+				Ext.bind(this._confirmRemoveContentType, this, [contentType, contentId, actionId], 1)
+		);
+	},
+	
+	/**
+	 * Callback function invoked after the #removeContentType confirm box is closed
+	 * @param {String} answer Id of the button that was clicked
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} contentType The content type to remove
+	 * @param {Number} actionId The workflow action id
+	 * @private
+	 */
+	_confirmRemoveContentType: function (answer, contentType, contentId, actionId)
+	{
+		if (answer == 'yes')
+		{
+			Ametys.data.ServerComm.send({
+	            plugin: 'cms', 
+	            url: 'content/remove-content-type', 
+	            parameters: {
+	            	contentId: contentId,
+	            	contentType: contentType,
+	            	actionId: actionId,
+	            },
+	            priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
+	            callback: {
+	                scope: this,
+	                handler: this._removeContentTypeCb,
+	                arguments: {
+	                	contentId: contentId
+	                }
+	            },
+	            responseType: 'text'
+	        });
+		}
+	},
+	
+	/**
+	 * @private
+	 * Callback function called after adding a content type
+	 * @param {HTMLElement} response The XML document.
+	 * @param {Array} params The callback arguments.
+	 */
+	_addContentTypeCb: function (response, params)
+	{
+		if (Ametys.data.ServerComm.handleBadResponse("<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR'/>", response, 'Ametys.plugins.cms.content.actions.ContentActions'))
+        {
+            return;
+        }
+		
+		var result = Ext.JSON.decode(Ext.dom.Query.selectValue("", response));
+		
+		if (result.failure)
+		{
+			var errorMsg = "";
+			
+			var msg = result.msg;
+			if (msg == 'non-modifiable-content')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR_NON_MODIFIABLE_CONTENT'/>";
+			}
+			else if (msg == 'no-content-type')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR_NO_CONTENT_TYPE'/>";
+			}
+			else if (msg == 'invalid-content-type')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR_INVALID_CONTENT_TYPE'/>";
+			}
+			else
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR'/>";
+			}
+			
+			Ext.Msg.show({
+				title: "<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR_TITLE'/>",
+				msg: errorMsg,
+				buttons: Ext.Msg.OK,
+				icon: Ext.Msg.ERROR
+			});
+		}	
+		
+		if (result.success)
+		{
+			Ametys.cms.content.ContentDAO.getContent(params.contentId, Ext.bind(this._fireMessages, this));
+		}
+	},
+	
+	/**
+	 * @private
+	 * Callback function called after removing a content type
+	 * @param {HTMLElement} response The XML document.
+	 * @param {Array} params The callback arguments.
+	 */
+	_removeContentTypeCb: function (response, params)
+	{
+		if (Ametys.data.ServerComm.handleBadResponse("<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_ERROR'/>", response, 'Ametys.plugins.cms.content.actions.ContentActions'))
+        {
+            return;
+        }
+		
+		var result = Ext.JSON.decode(Ext.dom.Query.selectValue("", response));
+		if (result.failure)
+		{
+			var errorMsg = "";
+			
+			var msg = result.msg;
+			if (msg == 'non-modifiable-content')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_ERROR_NON_MODIFIABLE_CONTENT'/>";
+			}
+			else if (msg == 'empty-list')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_ERROR_EMPTY_LIST'/>";
+			}
+			else
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_ERROR'/>";
+			}
+			
+			Ext.Msg.show({
+				title: "<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_ERROR_TITLE'/>",
+				msg: errorMsg,
+				buttons: Ext.Msg.OK,
+				icon: Ext.Msg.ERROR
+			});
+		}	
+		
+		if (result.success)
+		{
+			Ametys.cms.content.ContentDAO.getContent(params.contentId, Ext.bind(this._fireMessages, this));
+		}
+	},
+	
+	/**
+	 * Action function to be called by the controller.
+	 * Add or remove the selected mixin on concerned content
+	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller calling this function
+	 */
+	setMixinType: function (controller)
+	{
+		var btnConfig = controller.getInitialConfig();
+		
+		var parentController = Ametys.ribbon.RibbonManager.getElement(btnConfig.controllerParentId);
+		var contentIds = parentController.getContentIds();
+		
+		if (parentController.getContentIds().length > 0)
+		{
+			var mixin = btnConfig['contentTypes'] || btnConfig['contentType'];
+			var actionId = btnConfig['workflowActionId'] ? parseInt(btnConfig['workflowActionId']) : this.self._DEFAULT_WORKFLOW_ACTION_ID;
+			var contentId = parentController.getContentIds()[0];
+			
+			if (controller.getUIControls().get(0).pressed)
+			{
+				this.removeMixinType(contentId, mixin, actionId);
+			}
+			else
+			{
+				this.addMixinType(contentId, mixin, actionId);
+			}
+		}
+	},
+	
+	/**
+	 * Add a mixin on concerned content
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} mixin The mixin to add
+	 * @param {Number} actionId The workflow action id
+	 */
+	addMixinType: function (contentId, mixin, actionId)
+	{
+		Ametys.data.ServerComm.send({
+            plugin: 'cms', 
+            url: 'content/add-mixin', 
+            parameters: {
+            	contentId: contentId,
+            	mixin: mixin,
+            	actionId: actionId,
+            },
+            priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
+            callback: {
+                scope: this,
+                handler: this._addMixinTypeCb,
+                arguments: {
+                	contentId: contentId
+                }
+            },
+            responseType: 'text'
+        });
+	},
+	
+	/**
+	 * Remove a mixin on concerned content
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} mixin The mixin to remove
+	 * @param {Number} actionId The workflow action id
+	 */
+	removeMixinType: function (contentId, mixin, actionId)
+	{
+		Ext.Msg.confirm("<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_CONFIRM_TITLE'/>", 
+				"<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_CONFIRM'/>",
+				Ext.bind(this._confirmRemoveMixin, this, [mixin, contentId, actionId], 1)
+		);
+	},
+	
+	/**
+	 * Callback function invoked after the #removeMixin confirm box is closed
+	 * @param {String} answer Id of the button that was clicked
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} mixin The mixin to remove
+	 * @param {Number} actionId The workflow action id
+	 * @private
+	 */
+	_confirmRemoveMixin: function (answer, mixin, contentId, actionId)
+	{
+		if (answer == 'yes')
+		{
+			Ametys.data.ServerComm.send({
+	            plugin: 'cms', 
+	            url: 'content/remove-mixin', 
+	            parameters: {
+	            	contentId: contentId,
+	            	mixin: mixin,
+	            	actionId: actionId,
+	            },
+	            priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
+	            callback: {
+	                scope: this,
+	                handler: this._removeMixinTypeCb,
+	                arguments: {
+	                	contentId: contentId
+	                }
+	            },
+	            responseType: 'text'
+	        });
+		}
+	},
+	
+	/**
+	 * @private
+	 * Callback function called after adding a content type
+	 * @param {HTMLElement} response The XML document.
+	 * @param {Array} params The callback arguments.
+	 */
+	_addMixinTypeCb: function (response, params)
+	{
+		if (Ametys.data.ServerComm.handleBadResponse("<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR'/>", response, 'Ametys.plugins.cms.content.actions.ContentActions'))
+        {
+            return;
+        }
+		
+		var result = Ext.JSON.decode(Ext.dom.Query.selectValue("", response));
+		
+		if (result.failure)
+		{
+			var errorMsg = "";
+			
+			var msg = result.msg;
+			if (msg == 'non-modifiable-content')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR_NON_MODIFIABLE_CONTENT'/>";
+			}
+			else if (msg == 'no-mixin')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR_NO_MIXIN'/>";
+			}
+			else if (msg == 'invalid-mixin')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR_INVALID_MIXIN'/>";
+			}
+			else
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR'/>";
+			}
+			
+			Ext.Msg.show({
+				title: "<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR_TITLE'/>",
+				msg: errorMsg,
+				buttons: Ext.Msg.OK,
+				icon: Ext.Msg.ERROR
+			});
+		}	
+		
+		if (result.success)
+		{
+			Ametys.cms.content.ContentDAO.getContent(params.contentId, Ext.bind(this._fireMessages, this));
+		}
+	},
+	
+	/**
+	 * @private
+	 * Callback function called after removing a content type
+	 * @param {HTMLElement} response The XML document.
+	 * @param {Array} params The callback arguments.
+	 */
+	_removeMixinTypeCb: function (response, params)
+	{
+		if (Ametys.data.ServerComm.handleBadResponse("<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_ERROR'/>", response, 'Ametys.plugins.cms.content.actions.ContentActions'))
+        {
+            return;
+        }
+		
+		var result = Ext.JSON.decode(Ext.dom.Query.selectValue("", response));
+		if (result.failure)
+		{
+			var errorMsg = "";
+			
+			var msg = result.msg;
+			if (msg == 'non-modifiable-content')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_ERROR_NON_MODIFIABLE_CONTENT'/>";
+			}
+			else
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_ERROR'/>";
+			}
+			
+			Ext.Msg.show({
+				title: "<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_ERROR_TITLE'/>",
+				msg: errorMsg,
+				buttons: Ext.Msg.OK,
+				icon: Ext.Msg.ERROR
+			});
+		}	
+		
+		if (result.success)
+		{
+			Ametys.cms.content.ContentDAO.getContent(params.contentId, Ext.bind(this._fireMessages, this));
+		}
+	},
+	
+	/**
+	 * Fire {@link Ametys.message.Message#MODIFIED} and {@link Ametys.message.Message#WORKFLOW_CHANGED} on content
+	 * @param {Ametys.cms.content.Content} content The concerned content
+	 * @private
+	 */
+	_fireMessages: function (content)
+	{
+		Ext.create("Ametys.message.Message", {
+			type: Ametys.message.Message.MODIFIED,
+			
+			targets: {
+				type: Ametys.message.MessageTarget.CONTENT,
+				parameters: { contents: [content] }
+			}
+		});
+		
+		Ext.create("Ametys.message.Message", {
+			type: Ametys.message.Message.WORKFLOW_CHANGED,
+			
+			targets: {
+				type: Ametys.message.MessageTarget.CONTENT,
+				parameters: { contents: [content] }
+			}
+		});
+	}
+	
+	
+});
Index: main/plugin-cms/resources/js/Ametys/plugins/cms/content/actions/ContentTypeActionss.i18n.js
===================================================================
--- main/plugin-cms/resources/js/Ametys/plugins/cms/content/actions/ContentTypeActionss.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/Ametys/plugins/cms/content/actions/ContentTypeActionss.i18n.js	(revision 0)
@@ -0,0 +1,447 @@
+/*
+ *  Copyright 2011 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.
+ */
+
+/**
+ * Singleton class to create a new content. 
+ * @private
+ */
+Ext.define('Ametys.plugins.cms.content.actions.ContentActions', {
+	singleton: true,
+
+	/**
+	 * The default workflow action id
+	 * @private
+	 */
+	_DEFAULT_WORKFLOW_ACTION_ID: 220,
+	
+	/**
+	 * Action function to be called by the controller.
+	 * Add or remove the selected content type on concerned content
+	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller calling this function
+	 */
+	setContentType: function (controller)
+	{
+		var btnConfig = controller.getInitialConfig();
+		
+		var parentController = Ametys.ribbon.RibbonManager.getElement(btnConfig.controllerParentId);
+		var contentIds = parentController.getContentIds();
+		
+		if (parentController.getContentIds().length > 0)
+		{
+			var contentType = btnConfig['contentTypes'] || btnConfig['contentType'];
+			var actionId = btnConfig['workflowActionId'] ? parseInt(btnConfig['workflowActionId']) : this.self._DEFAULT_WORKFLOW_ACTION_ID;
+			var contentId = parentController.getContentIds()[0];
+			
+			if (controller.getUIControls().get(0).pressed)
+			{
+				this.removeContentType(contentId, contentType, actionId);
+			}
+			else
+			{
+				this.addContentType(contentId, contentType, actionId);
+			}
+		}
+	},
+	
+	/**
+	 * Add a content type on concerned content
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} contentType The content type to add
+	 * @param {Number} actionId The workflow action id
+	 */
+	addContentType: function (contentId, contentType, actionId)
+	{
+		Ametys.data.ServerComm.send({
+            plugin: 'cms', 
+            url: 'content/add-content-type', 
+            parameters: {
+            	contentId: contentId,
+            	contentType: contentType,
+            	actionId: actionId,
+            },
+            priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
+            callback: {
+                scope: this,
+                handler: this._addContentTypeCb,
+                arguments: {
+                	contentId: contentId
+                }
+            },
+            responseType: 'text'
+        });
+	},
+	
+	/**
+	 * Remove a content type on concerned content
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} contentType The content type to remove
+	 * @param {Number} actionId The workflow action id
+	 */
+	removeContentType: function (contentId, contentType, actionId)
+	{
+		Ext.Msg.confirm("<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_CONFIRM_TITLE'/>", 
+				"<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_CONFIRM'/>",
+				Ext.bind(this._confirmRemoveContentType, this, [contentType, contentId, actionId], 1)
+		);
+	},
+	
+	/**
+	 * Callback function invoked after the #removeContentType confirm box is closed
+	 * @param {String} answer Id of the button that was clicked
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} mixin The mixin to remove
+	 * @param {Number} actionId The workflow action id
+	 * @private
+	 */
+	_confirmRemoveContentType: function (answer, contentType, contentId, actionId)
+	{
+		if (answer == 'yes')
+		{
+			Ametys.data.ServerComm.send({
+	            plugin: 'cms', 
+	            url: 'content/remove-content-type', 
+	            parameters: {
+	            	contentId: contentId,
+	            	contentType: contentType,
+	            	actionId: actionId,
+	            },
+	            priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
+	            callback: {
+	                scope: this,
+	                handler: this._removeContentTypeCb,
+	                arguments: {
+	                	contentId: contentId
+	                }
+	            },
+	            responseType: 'text'
+	        });
+		}
+	},
+	
+	/**
+	 * @private
+	 * Callback function called after adding a content type
+	 * @param {HTMLElement} response The XML document.
+	 * @param {Array} params The callback arguments.
+	 */
+	_addContentTypeCb: function (response, params)
+	{
+		if (Ametys.data.ServerComm.handleBadResponse("<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR'/>", response, 'Ametys.plugins.cms.content.actions.ContentActions'))
+        {
+            return;
+        }
+		
+		var result = Ext.JSON.decode(Ext.dom.Query.selectValue("", response));
+		
+		if (result.failure)
+		{
+			var errorMsg = "";
+			
+			var msg = result.msg;
+			if (msg == 'non-modifiable-content')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR_NON_MODIFIABLE_CONTENT'/>";
+			}
+			else if (msg = 'invalid-content-type')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR_INVALID_CONTENT_TYPE'/>";
+			}
+			else
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR'/>";
+			}
+			
+			Ext.Msg.show({
+				title: "<i18n:text i18n:key='CONTENT_ADD_CONTENT_TYPE_ERROR_TITLE'/>",
+				msg: errorMsg,
+				buttons: Ext.Msg.OK,
+				icon: Ext.Msg.ERROR
+			});
+		}	
+		
+		if (result.success)
+		{
+			Ametys.cms.content.ContentDAO.getContent(params.contentId, Ext.bind(this._fireMessages, this));
+		}
+	},
+	
+	/**
+	 * @private
+	 * Callback function called after removing a content type
+	 * @param {HTMLElement} response The XML document.
+	 * @param {Array} params The callback arguments.
+	 */
+	_removeContentTypeCb: function (response, params)
+	{
+		if (Ametys.data.ServerComm.handleBadResponse("<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_ERROR'/>", response, 'Ametys.plugins.cms.content.actions.ContentActions'))
+        {
+            return;
+        }
+		
+		if (result.failure)
+		{
+			var errorMsg = "";
+			
+			var msg = result.msg;
+			if (msg == 'non-modifiable-content')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_ERROR_NON_MODIFIABLE_CONTENT'/>";
+			}
+			else if (msg = 'empty-list')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_ERROR_EMPTY_LIST'/>";
+			}
+			else
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_ERROR'/>";
+			}
+			
+			Ext.Msg.show({
+				title: "<i18n:text i18n:key='CONTENT_REMOVE_CONTENT_TYPE_ERROR_TITLE'/>",
+				msg: errorMsg,
+				buttons: Ext.Msg.OK,
+				icon: Ext.Msg.ERROR
+			});
+		}	
+		
+		var result = Ext.JSON.decode(Ext.dom.Query.selectValue("", response));
+		if (result.success)
+		{
+			Ametys.cms.content.ContentDAO.getContent(params.contentId, Ext.bind(this._fireMessages, this));
+		}
+	},
+	
+	/**
+	 * Action function to be called by the controller.
+	 * Add or remove the selected mixin on concerned content
+	 * @param {Ametys.ribbon.element.ui.ButtonController} controller The controller calling this function
+	 */
+	setMixin: function (controller)
+	{
+		var btnConfig = controller.getInitialConfig();
+		
+		var parentController = Ametys.ribbon.RibbonManager.getElement(btnConfig.controllerParentId);
+		var contentIds = parentController.getContentIds();
+		
+		if (parentController.getContentIds().length > 0)
+		{
+			var mixin = btnConfig['contentTypes'] || btnConfig['contentType'];
+			var actionId = btnConfig['workflowActionId'] ? parseInt(btnConfig['workflowActionId']) : this.self._DEFAULT_WORKFLOW_ACTION_ID;
+			var contentId = parentController.getContentIds()[0];
+			
+			if (controller.getUIControls().get(0).pressed)
+			{
+				this.removeMixin(contentId, mixin, actionId);
+			}
+			else
+			{
+				this.addMixin(contentId, mixin, actionId);
+			}
+		}
+	},
+	
+	/**
+	 * Add a mixin on concerned content
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} mixin The mixin to add
+	 * @param {Number} actionId The workflow action id
+	 */
+	addMixin: function (contentId, mixin, actionId)
+	{
+		Ametys.data.ServerComm.send({
+            plugin: 'cms', 
+            url: 'content/add-mixin', 
+            parameters: {
+            	contentId: contentId,
+            	mixin: mixin,
+            	actionId: actionId,
+            },
+            priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
+            callback: {
+                scope: this,
+                handler: this._addMixinCb,
+                arguments: {
+                	contentId: contentId
+                }
+            },
+            responseType: 'text'
+        });
+	},
+	
+	/**
+	 * Remove a mixin on concerned content
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} mixin The mixin to remove
+	 * @param {Number} actionId The workflow action id
+	 */
+	removeMixin: function (contentId, mixin, actionId)
+	{
+		Ext.Msg.confirm("<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_CONFIRM_TITLE'/>", 
+				"<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_CONFIRM'/>",
+				Ext.bind(this._confirmRemoveMixin, this, [mixin, contentId, actionId], 1)
+		);
+	},
+	
+	/**
+	 * Callback function invoked after the #removeMixin confirm box is closed
+	 * @param {String} answer Id of the button that was clicked
+	 * @param {String} contentId The id of concerned content
+	 * @param {String} mixin The mixin to remove
+	 * @param {Number} actionId The workflow action id
+	 * @private
+	 */
+	_confirmRemoveMixin: function (answer, mixin, contentId, actionId)
+	{
+		if (answer == 'yes')
+		{
+			Ametys.data.ServerComm.send({
+	            plugin: 'cms', 
+	            url: 'content/remove-mixin', 
+	            parameters: {
+	            	contentId: contentId,
+	            	mixin: mixin,
+	            	actionId: actionId,
+	            },
+	            priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
+	            callback: {
+	                scope: this,
+	                handler: this._removeMixinCb,
+	                arguments: {
+	                	contentId: contentId
+	                }
+	            },
+	            responseType: 'text'
+	        });
+		}
+	},
+	
+	/**
+	 * @private
+	 * Callback function called after adding a content type
+	 * @param {HTMLElement} response The XML document.
+	 * @param {Array} params The callback arguments.
+	 */
+	_addMixinCb: function (response, params)
+	{
+		if (Ametys.data.ServerComm.handleBadResponse("<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR'/>", response, 'Ametys.plugins.cms.content.actions.ContentActions'))
+        {
+            return;
+        }
+		
+		var result = Ext.JSON.decode(Ext.dom.Query.selectValue("", response));
+		
+		if (result.failure)
+		{
+			var errorMsg = "";
+			
+			var msg = result.msg;
+			if (msg == 'non-modifiable-content')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR_NON_MODIFIABLE_CONTENT'/>";
+			}
+			else if (msg = 'invalid-mixin')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR_INVALID_CONTENT_TYPE'/>";
+			}
+			else
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR'/>";
+			}
+			
+			Ext.Msg.show({
+				title: "<i18n:text i18n:key='CONTENT_ADD_MIXIN_ERROR_TITLE'/>",
+				msg: errorMsg,
+				buttons: Ext.Msg.OK,
+				icon: Ext.Msg.ERROR
+			});
+		}	
+		
+		if (result.success)
+		{
+			Ametys.cms.content.ContentDAO.getContent(params.contentId, Ext.bind(this._fireMessages, this));
+		}
+	},
+	
+	/**
+	 * @private
+	 * Callback function called after removing a content type
+	 * @param {HTMLElement} response The XML document.
+	 * @param {Array} params The callback arguments.
+	 */
+	_removeMixinCb: function (response, params)
+	{
+		if (Ametys.data.ServerComm.handleBadResponse("<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_ERROR'/>", response, 'Ametys.plugins.cms.content.actions.ContentActions'))
+        {
+            return;
+        }
+		
+		if (result.failure)
+		{
+			var errorMsg = "";
+			
+			var msg = result.msg;
+			if (msg == 'non-modifiable-content')
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_ERROR_NON_MODIFIABLE_CONTENT'/>";
+			}
+			else
+			{
+				errorMsg = "<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_ERROR'/>";
+			}
+			
+			Ext.Msg.show({
+				title: "<i18n:text i18n:key='CONTENT_REMOVE_MIXIN_ERROR_TITLE'/>",
+				msg: errorMsg,
+				buttons: Ext.Msg.OK,
+				icon: Ext.Msg.ERROR
+			});
+		}	
+		
+		var result = Ext.JSON.decode(Ext.dom.Query.selectValue("", response));
+		if (result.success)
+		{
+			Ametys.cms.content.ContentDAO.getContent(params.contentId, Ext.bind(this._fireMessages, this));
+		}
+	},
+	
+	/**
+	 * Fire {@link Ametys.message.Message#MODIFIED} and {@link Ametys.message.Message#WORKFLOW_CHANGED} on content
+	 * @param {Ametys.cms.content.Content} content The concerned content
+	 * @private
+	 */
+	_fireMessages: function (content)
+	{
+		Ext.create("Ametys.message.Message", {
+			type: Ametys.message.Message.MODIFIED,
+			
+			targets: {
+				type: Ametys.message.MessageTarget.CONTENT,
+				parameters: { contents: [content] }
+			}
+		});
+		
+		Ext.create("Ametys.message.Message", {
+			type: Ametys.message.Message.WORKFLOW_CHANGED,
+			
+			targets: {
+				type: Ametys.message.MessageTarget.CONTENT,
+				parameters: { contents: [content] }
+			}
+		});
+	}
+	
+	
+});
Index: main/plugin-cms/resources/js/Ametys/cms/form/widget/FlipFlap.i18n.js
===================================================================
--- main/plugin-cms/resources/js/Ametys/cms/form/widget/FlipFlap.i18n.js	(revision 0)
+++ main/plugin-cms/resources/js/Ametys/cms/form/widget/FlipFlap.i18n.js	(revision 0)
@@ -0,0 +1,56 @@
+/*
+ *  Copyright 2013 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  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.
+ */
+
+/**
+ * This class provides a control that allows selection of between two list controls.<br>
+ * 
+ * This widget is registered for enumerated and multiple fields of type Ametys.cms.form.WidgetManager#TYPE_STRING.<br>
+ */
+Ext.define('Ametys.cms.form.widget.FlipFlap', {
+    extend: 'Ext.ux.form.ItemSelector',
+    
+    alias: ['widget.edition.flipflap'],
+    
+    constructor: function(config)
+    {
+    	var storeCfg = {
+			id: 0,
+            fields: [ 'value', {name: 'text', sortType: Ext.data.SortTypes.asNonAccentedUCString}],
+            data: config.enumeration
+        };
+    		
+		config.naturalOrder = Ext.isBoolean(config.naturalOrder) ? config.naturalOrder : config.naturalOrder == 'true';
+		if (!config.naturalOrder)
+		{
+			storeCfg.sorters = [{property: 'text', direction:'ASC'}]; // default order
+		}
+    		
+        config = Ext.apply(config, {
+        	cls: 'ametys',
+        	
+            store: new Ext.data.SimpleStore(storeCfg),
+            
+            valueField: 'value',
+            displayField: 'text',
+            
+            allowBlank: this.allowBlank
+        });
+        
+        this.callParent(arguments);
+    }
+});
+
+Ametys.cms.form.WidgetManager.registerWidget('edition.flipflap', Ametys.cms.form.WidgetManager.TYPE_STRING, true /*enumeration*/, true /*multiple*/, false /*default*/);
\ No newline at end of file
Index: main/plugin-cms/resources/js/Ametys/cms/content/Content.i18n.js
===================================================================
--- main/plugin-cms/resources/js/Ametys/cms/content/Content.i18n.js	(revision 25585)
+++ main/plugin-cms/resources/js/Ametys/cms/content/Content.i18n.js	(working copy)
@@ -67,14 +67,23 @@
 			/** @ignore */
 			name: null,
 			/**
-			 * @cfg {String} type The content type
+			 * @cfg {String[]} types The content types
 			 */
 			/**
-			 * @method getType Get the #cfg-type
-			 * @return {String} The type
+			 * @method getTypes Get the #cfg-types
+			 * @return {String[]} The types
 			 */
 			/** @ignore */
-			type: null,
+			types: [],
+			/**
+			 * @cfg {String[]} mixins The mixins
+			 */
+			/**
+			 * @method getMixins Get the #cfg-mixins
+			 * @return {String[]} The mixins
+			 */
+			/** @ignore */
+			mixins: [],
 			/**
 			 * @cfg {String} workflowName The identifier of the workflow used for this content
 			 */
@@ -416,7 +425,8 @@
 					title: this.title,
 					path: this.path,
 					lang: this.lang,
-					type: this.type,
+					types: this.types,
+					mixins: this.types,
 					workflowName : this.workflowName,
 					isModifiable : this.isModifiable,
 					rights: this.rights
Index: main/plugin-cms/resources/css/detail.css
===================================================================
--- main/plugin-cms/resources/css/detail.css	(revision 25585)
+++ main/plugin-cms/resources/css/detail.css	(working copy)
@@ -28,6 +28,10 @@
 	text-overflow: ellipsis;
 }
 
+.details-view .details-view-card .showhide {
+	color: #777777;
+}
+
 .details-view .details-view-card ul {
 	list-style-type: none;
 	padding-left: 0px;
@@ -49,6 +53,10 @@
 	margin-right:0px;
 }
 
+.details-view .details-view-card ul.hidden {
+	display: none;
+}
+
 .details-view ul li.content-type {
 	font-style:italic;
 	color: #444;