Index: .settings/org.eclipse.jdt.core.prefs
===================================================================
--- .settings/org.eclipse.jdt.core.prefs	(revision 0)
+++ .settings/org.eclipse.jdt.core.prefs	(revision 0)
@@ -0,0 +1,11 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.8
Index: main/plugin-odf-migration-40/README.txt
===================================================================
--- main/plugin-odf-migration-40/README.txt	(revision 53499)
+++ main/plugin-odf-migration-40/README.txt	(working copy)
@@ -11,20 +11,24 @@
 En fonction des erreurs/warning levés lors de la migration, vous serez sans doute amené à personnaliser puis recommencer la migration plusieurs fois
 
 --------------------------------------------------------------- 
---			ETAPE 2 : MIGRATION GENERALE DES DONNEES		 --
----------------------------------------------------------------
+--                  ETAPE 1.1 : Repository                   --
+--------------------------------------------------------------- 
 
-Appliquez le script de migration des données hors ODF 3.8.x vers 4.0.x
-https://wiki.ametys.org/pages/viewpage.action?pageId=31129652#Migrationdesdonn%C3%A9es3.8vers4.0-Repository
+Supprimez le custom-nodetypes.xml
+
+Si vous avez pris le fichier WEB-INF/param/repository.xml du template (ce qui est conseillé):
+
+déplacez le dossier ametys_cms_home/data/repository/version/db vers ametys_cms_home/data/repository/versions/db
+supprimez le dossier ametys_cms_home/data/repository/version
 
 --------------------------------------------------------------- 
---			ETAPE 3 : INSTALLATION PLUGIN MIGRATION			 --
+--			ETAPE 2 : INSTALLATION PLUGIN MIGRATION			 --
 ---------------------------------------------------------------
 
 Copiez le plugin "odf-migration-40" dans votre projet ODF
 
 --------------------------------------------------------------- 
---		ETAPE 3.1 : PERSONNALISATION TABLE DE REFERENCE		 --
+--		ETAPE 2.1 : PERSONNALISATION TABLE DE REFERENCE		 --
 ---------------------------------------------------------------
 
 Personnalisez les tables de références si besoin :
@@ -36,7 +40,7 @@
 NB: si vous utilisez les tables de références par défaut fournit par Ametys, vous n'avez rien à faire pour cette étape.
 
 --------------------------------------------------------------- 
---		ETAPE 3.2 : NOUVELLES TABLES DE REFERENCE			 --
+--		ETAPE 2.2 : NOUVELLES TABLES DE REFERENCE			 --
 ---------------------------------------------------------------
 Personnalisez les nouvelles tables de références (modèle v4) :
 
@@ -52,7 +56,7 @@
 - Langue d'enseignement (language.txt)
 
 --------------------------------------------------------------- 
---					ETAPE 3.3 : MAPPING						 --
+--					ETAPE 2.3 : MAPPING						 --
 ---------------------------------------------------------------
 Personnalisez le fichier mapping.xml:
 Dans ce fichier, pour chaque type de contenu ODF, listez l'ensemble des métadonnées pointant vers une table de référence.
@@ -61,7 +65,7 @@
 NB: si vous n'avez pas surchargé le modèle de données des contenus ODF, vous n'avez rien à faire ici pour cette étape.
 
 --------------------------------------------------------------- 
---			ETAPE 4 : LANCEMENT DE LA MIGRATION				 --
+--			ETAPE 3 : LANCEMENT DE LA MIGRATION				 --
 ---------------------------------------------------------------
 Redémarrez l'application.
 
@@ -79,6 +83,13 @@
 - positionner les droits de lecture sur les contenus ODF depuis le back-office
 
 --------------------------------------------------------------- 
+--          ETAPE 4 : MIGRATION GENERALE DES DONNEES         --
+---------------------------------------------------------------
+
+Appliquez le script de migration des données hors ODF 3.8.x vers 4.0.x
+https://docs.ametys.org/fr/dev/doc-incubator/ametys-4-0/manuels-de-migration/migration-des-donnees-3-8-vers-4-0.html%3Fsearch-keywords=migration-des-donnees-3-8-vers-4-0.html
+
+--------------------------------------------------------------- 
 --				PERSONNALISATION AVANCEE					 --
 ---------------------------------------------------------------
 Si le projet nécessite une migration spécifique plus avancée, vous pouvez modifier la classe OdfMigrationSchedulable.
Index: main/plugin-odf-migration-40/src/org/ametys/plugins/odfmigration40/MigrateNewModelComponent.java
===================================================================
--- main/plugin-odf-migration-40/src/org/ametys/plugins/odfmigration40/MigrateNewModelComponent.java	(revision 53499)
+++ main/plugin-odf-migration-40/src/org/ametys/plugins/odfmigration40/MigrateNewModelComponent.java	(working copy)
@@ -219,7 +219,12 @@
      */
     public boolean migrateProgram(Program program)
     {
-        return _migrationAbstractProgam(program);
+        boolean hasChanges = _migrationAbstractProgam(program);
+        
+        // Effectifs
+        hasChanges = _convertStringToRichText(program, AbstractProgram.NUMBER_OF_STUDENTS) || hasChanges;
+        
+        return hasChanges;
     }
     
     /**
@@ -248,7 +253,12 @@
      */
     public boolean migrateSubProgram(SubProgram subprogram)
     {
-        return _migrationAbstractProgam(subprogram);
+        boolean hasChanges = _migrationAbstractProgam(subprogram);
+        
+        // Effectifs
+        hasChanges = _convertLongToRichText(subprogram, AbstractProgram.NUMBER_OF_STUDENTS) || hasChanges;
+        
+        return hasChanges;
     }
     
     private boolean _migrationAbstractProgam(AbstractProgram program)
@@ -258,9 +268,6 @@
         // Domaine multiple
         hasChanges = _convertStringSingleToMultiple(program, AbstractProgram.DOMAIN, null) || hasChanges;
         
-        // Effectifs
-        hasChanges = _convertStringToRichText(program, AbstractProgram.NUMBER_OF_STUDENTS) || hasChanges;
-        
         // ECTS
         hasChanges = _convertStringToReferenceTable(program, AbstractProgram.ECTS, OdfReferenceTableHelper.ECTS) || hasChanges;
 
@@ -434,12 +441,15 @@
             }
             catch (UnknownMetadataException e) 
             {
-                String[] oldValues = metadataHolder.getStringArray(personInChargeMetadataName);
+                String[] oldValues = metadataHolder.getStringArray(personInChargeMetadataName, ArrayUtils.EMPTY_STRING_ARRAY);
                 metadataHolder.removeMetadata(personInChargeMetadataName);
                 
-                ModifiableCompositeMetadata compositeMetadata = metadataHolder.getCompositeMetadata(personInChargeMetadataName, true);
-                ModifiableCompositeMetadata entry = compositeMetadata.getCompositeMetadata("1", true);
-                entry.setMetadata("persons", oldValues);
+                if (oldValues.length > 0)
+                {
+                    ModifiableCompositeMetadata compositeMetadata = metadataHolder.getCompositeMetadata(personInChargeMetadataName, true);
+                    ModifiableCompositeMetadata entry = compositeMetadata.getCompositeMetadata("1", true);
+                    entry.setMetadata("persons", oldValues);
+                }
                 
                 return true;
             }
@@ -748,6 +758,49 @@
         
         return null;
     }
+    
+    /**
+     * Convert long metadata to rich text metadata
+     * @param content the content
+     * @param metadataName the metadata name
+     * @return true if changes were made
+     */
+    protected boolean _convertLongToRichText(ModifiableContent content, String metadataName)
+    {
+        boolean hasChanges = false;
+        ModifiableCompositeMetadata metadataHolder = content.getMetadataHolder();
+        
+        if (metadataHolder.hasMetadata(metadataName))
+        {
+            if (metadataHolder.getType(metadataName).equals(MetadataType.LONG))
+            {
+                Long oldValue = metadataHolder.getLong(metadataName);
+                metadataHolder.removeMetadata(metadataName);
+                
+                String docbook = _textToDocbook(String.valueOf(oldValue));
+            
+                ByteArrayInputStream is =  new ByteArrayInputStream(docbook.getBytes(StandardCharsets.UTF_8));
+                ModifiableRichText richText = metadataHolder.getRichText(metadataName, true);
+                richText.setInputStream(is);
+                richText.setMimeType("text/xml");
+                richText.setLastModified(new Date());
+            }
+            else
+            {
+                metadataHolder.removeMetadata(metadataName);
+            }
+            
+            hasChanges = true;
+        }
+        
+        if (!metadataName.endsWith(ExternalizableMetadataHelper.ALTERNATIVE_SUFFIX))
+        {
+            hasChanges = _convertLongToRichText(content, metadataName + ExternalizableMetadataHelper.ALTERNATIVE_SUFFIX) || hasChanges;
+        }
+        
+        return hasChanges;
+    }
+    
     /**
      * Convert string metadata to rich text metadata
      * @param content the content
Index: main/plugin-odf-migration-40/src/org/ametys/plugins/odfmigration40/OdfMigrationSchedulable.java
===================================================================
--- main/plugin-odf-migration-40/src/org/ametys/plugins/odfmigration40/OdfMigrationSchedulable.java	(revision 53499)
+++ main/plugin-odf-migration-40/src/org/ametys/plugins/odfmigration40/OdfMigrationSchedulable.java	(working copy)
@@ -5,6 +5,7 @@
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -26,12 +27,15 @@
 import javax.jcr.ValueFormatException;
 import javax.jcr.lock.LockException;
 import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.version.OnParentVersionAction;
 import javax.jcr.version.VersionException;
+import javax.jcr.version.VersionHistory;
 import javax.xml.parsers.SAXParserFactory;
 
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
 import org.apache.cocoon.util.HashUtil;
+import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.excalibur.source.SourceResolver;
@@ -80,6 +84,7 @@
 import org.ametys.odf.program.ProgramPart;
 import org.ametys.odf.program.SubProgram;
 import org.ametys.odf.program.SubProgramFactory;
+import org.ametys.odf.program.TraversableProgramPart;
 import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollection;
 import org.ametys.plugins.contentio.synchronize.SynchronizableContentsCollectionDAO;
 import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable;
@@ -124,8 +129,12 @@
  */
 public class OdfMigrationSchedulable extends AbstractStaticSchedulable
 {
+    /** Properties that are auto-created or protected, which mustn't be copied when copying a node. */
+    protected static final List<String> PROTECTED_PROPERTIES = Arrays.asList("jcr:uuid",
+            "jcr:primaryType", "jcr:predecessors", "jcr:versionHistory", "jcr:baseVersion");
+
     private static final String __V4_LABEL = "AmetysODF_v4.0.0";
-    
+
     private ContentWorkflowHelper _workflowHelper;
     private I18nUtils _i18nUtils;
     private SourceResolver _srcResolver;
@@ -141,8 +150,8 @@
     
     private Map<String, Map<String, String>> _tableRefCache;
     private String _sccType;
-
-
+    private Map<String, Node> _oldContentToNewContentNode;
+    
     @Override
     public void service(ServiceManager manager) throws ServiceException
     {
@@ -165,10 +174,10 @@
     public void execute(JobExecutionContext context) throws Exception
     {
         getLogger().info("Start ODF migration ...");
-        
         try
         {
             _tableRefCache = new HashMap<>();
+            _oldContentToNewContentNode = new HashMap<>();
             
             JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
             _sccType = (String) jobDataMap.get(Scheduler.PARAM_VALUES_PREFIX + "sccType");
@@ -180,8 +189,9 @@
             
             _migrateODFRoot();
             
-            // Migrate ODF enumeration
+            // Migrate ODF enumeration            
             _createTableRefs();
+            
             // New reference tables
             _newModelMigration.createNewReferenceTables();
             
@@ -299,17 +309,10 @@
         
         Map<String, Map<String, String>> mapping = _readTableRefMapping();
 
-        // Set catalog before migrate structure
-        _migrateCatalog();
+        List<WorkflowAwareContent> toValidate = _migrateStructure(mapping);
         
-        List<WorkflowAwareContent> toValidate = _migrateStructure();
-        
-        _migratePrograms(mapping.get(ProgramFactory.PROGRAM_CONTENT_TYPE));
-        _migrateSubPrograms(mapping.get(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE));
-        _migrateContainers(mapping.get(ContainerFactory.CONTAINER_CONTENT_TYPE));
         _migrateCourses(mapping.get(CourseFactory.COURSE_CONTENT_TYPE));
         _migrateOrgunits(mapping.get(OrgUnitFactory.ORGUNIT_CONTENT_TYPE));
-        
         _migratePerson();
 
         for (WorkflowAwareContent waContent : toValidate)
@@ -320,21 +323,6 @@
         // TODO Do a migration workflow step ?
     }
     
-    private void _migrateCatalog()
-    {
-        AmetysObjectIterable<SubProgram> subprograms = _odfHelper.getProgramItems(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, null, null, null);
-        for (SubProgram subprogram : subprograms)
-        {
-            _setCatalog(subprogram);
-        }
-        
-        AmetysObjectIterable<Container> containers = _odfHelper.getProgramItems(ContainerFactory.CONTAINER_CONTENT_TYPE, null, null, null);
-        for (Container container : containers)
-        {
-            _setCatalog(container);
-        }
-        
-    }
     private void _migratePages() throws ValueFormatException, PathNotFoundException, RepositoryException
     {
         getLogger().info("Migrate virtual pages...");
@@ -361,57 +349,7 @@
         }
     }
     
-    private void _migratePrograms(Map<String, String> tableRefMapping) throws UnsupportedRepositoryOperationException, RepositoryException
-    {
-        getLogger().info("Start migration of programs...");
-        
-        int count = 0;
-        
-        ContentType cType = _cTypeExtensionPoint.getExtension(ProgramFactory.PROGRAM_CONTENT_TYPE);
-        
-        AmetysObjectIterable<Program> programs = _odfHelper.getProgramItems(ProgramFactory.PROGRAM_CONTENT_TYPE, null, null, null);
-        for (Program program : programs)
-        {
-            if (program.isLocked())
-            {
-                program.unlock();
-            }
-            
-            String[] allLabels = program.getAllLabels();
-            String[] currentLabels = program.getLabels();
-            
-            boolean hasLiveVersion = ArrayUtils.contains(allLabels, "Live");
-            boolean currentVersionIsLive = ArrayUtils.contains(currentLabels, "Live");
-            
-            if (hasLiveVersion && !currentVersionIsLive)
-            {
-                String currentVersion = program.getNode().getBaseVersion().getName();
-                
-                // switching to old live version
-                program.restoreFromLabel("Live");
-                // migrate old live version
-                _migrateProgram(program, cType, tableRefMapping);
-                program.addLabel("Live", true); 
-                
-                // restore current version
-                program.restoreFromRevision(currentVersion);
-            }
-            
-            // Applying migration on head version
-            _migrateProgram(program, cType, tableRefMapping);
-            
-            if (currentVersionIsLive)
-            {
-                program.addLabel("Live", true);
-            }
-
-            count++;
-        }
-        
-        getLogger().info("{} programs have been migrated", count);
-    }
-    
-    private void _migrateProgram(Program program, ContentType cType, Map<String, String> tableRefMapping)
+    private void _migrateProgram(Program program, ContentType cType, Map<String, String> tableRefMapping, boolean addTag)
     {
         // Migrate synchronized metadata
         MigrateSynchronizedMetadataHelper.migrateSynchronizedMetadata(cType, program);
@@ -427,29 +365,33 @@
         // New model
         _newModelMigration.migrateProgram(program);
         
-        _endMigration(program);
+        _endMigration(program, addTag);
     }
     
-    private List<WorkflowAwareContent> _migrateStructure() throws RepositoryException, AmetysRepositoryException, WorkflowException
+    private List<WorkflowAwareContent> _migrateStructure(Map<String, Map<String, String>> tableRefMapping) throws RepositoryException, AmetysRepositoryException, WorkflowException
     {
-        // ---------------------  Programs ----------------------------
-        getLogger().info("Start migration of programs' structure...");
-        
         List<WorkflowAwareContent> toValidate = new ArrayList<>();
+
+        // ---------------------  Programs Live ----------------------------
+        getLogger().info("Start migration of programs' live structure...");
         
-        AmetysObjectIterable<Program> programs = _odfHelper.getProgramItems(ProgramFactory.PROGRAM_CONTENT_TYPE, null, null, null);
-        for (Program program : programs)
-        {
-            boolean currentProgramVersionIsLive = ArrayUtils.contains(program.getAllLabels(), "Live");
-            _migrateSubStructure(program.getNode(), currentProgramVersionIsLive, toValidate);
-            // program.saveChanges();
-        }
+        // Migrate the live structure
+        _migrateProgramsLiveStructure(tableRefMapping);
+        
+        // ---------------------  Programs ----------------------------
+        getLogger().info("Start migration of programs'  structure...");
+
+        // Migrate content types attributes before migrate structure
+        _migrateContentTypesForODFContent();
+        
+        // Migrate the structure
+        _migrateProgramsStructure(toValidate, tableRefMapping);
         
         // ---------------------  Courses ----------------------------
         getLogger().info("Start migration of courses' structure...");
         
-        AmetysObjectIterable<Course> courses = _odfHelper.getProgramItems(CourseFactory.COURSE_CONTENT_TYPE, null, null, null);
-        for (Course course : courses)
+        AmetysObjectIterable<Course> coursesToMigrate = _odfHelper.getProgramItems(CourseFactory.COURSE_CONTENT_TYPE, null, null, null);
+        for (Course course : coursesToMigrate)
         {
             boolean currentCourseVersionIsLive = ArrayUtils.contains(course.getAllLabels(), "Live");
             NodeIterator nodesIt = course.getNode().getNodes();
@@ -477,56 +419,103 @@
         getLogger().info("Start migration of orgunits' structure...");
         
         OrgUnit rootOU = _ouRootProvider.getRoot();
+        
+        /// migrate content type, workflow
+        _migrateV3ContentToV4(rootOU);
+        
+        // migrate structure
         _migrateOrgunitStructure(rootOU);
         
         return toValidate;
     }
-    
-    private void _validateContent(WorkflowAwareContent waContent, int validateStepId)
+
+    private void _migrateContentTypesForODFContent()
     {
-        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
-        AmetysStep currentStep = (AmetysStep) workflow.getCurrentSteps(waContent.getWorkflowId()).iterator().next();
-        
-        currentStep.setStepId(validateStepId);
-        
-        waContent.setCurrentStepId(validateStepId);
-        waContent.saveChanges();
-        
-        if (waContent instanceof VersionableAmetysObject)
+        List<DefaultContent> contents = new ArrayList<DefaultContent>();
+        CollectionUtils.addAll(contents, _odfHelper.getProgramItems(ProgramFactory.PROGRAM_CONTENT_TYPE, null, null, null));
+        CollectionUtils.addAll(contents, _odfHelper.getProgramItems(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, null, null, null));
+        CollectionUtils.addAll(contents, _odfHelper.getProgramItems(ContainerFactory.CONTAINER_CONTENT_TYPE, null, null, null));
+        CollectionUtils.addAll(contents, _odfHelper.getProgramItems(CourseFactory.COURSE_CONTENT_TYPE, null, null, null));
+        CollectionUtils.addAll(contents, _odfHelper.getProgramItems(CourseListFactory.COURSE_LIST_CONTENT_TYPE, null, null, null));
+        CollectionUtils.addAll(contents, _odfHelper.getProgramItems(OrgUnitFactory.ORGUNIT_CONTENT_TYPE, null, null, null));
+        CollectionUtils.addAll(contents, _odfHelper.getProgramItems(PersonFactory.PERSON_CONTENT_TYPE, null, null, null));
+
+        for (DefaultContent content : contents)
         {
-            ((VersionableAmetysObject) waContent).checkpoint();
-            ((VersionableAmetysObject) waContent).addLabel("Live", true);
+            _migrateContentType(content);
         }
     }
     
-    private void _migrateOrgunitStructure(OrgUnit ou) throws RepositoryException
+    private void _migrateProgramsLiveStructure(Map<String, Map<String, String>> tableRefMapping) throws RepositoryException, AmetysRepositoryException, WorkflowException
     {
-        if (ou.getMetadataHolder().hasMetadata("orgunitsReferences"))
+        AmetysObjectIterable<Program> programs = _odfHelper.getProgramItems(ProgramFactory.PROGRAM_CONTENT_TYPE, null, null, null);
+        for (Program program : programs)
         {
-            // orgunitsReferences renamed to childOrgUnits
-            String[] ouRefIds = ou.getMetadataHolder().getStringArray("orgunitsReferences");
-            ou.getMetadataHolder().setMetadata(OrgUnit.METADATA_CHILD_ORGUNITS, ouRefIds);
+            if (program.isLocked())
+            {
+                program.unlock();
+            }
             
-            ValueFactory valueFactory = ou.getNode().getSession().getValueFactory();
-            Value[] jcrValues = new Value[ouRefIds.length];
-            for (int i = 0; i < ouRefIds.length; i++)
+            boolean hasLiveVersion = ArrayUtils.contains(program.getAllLabels(), "Live");
+            boolean currentVersionIsLive = ArrayUtils.contains(program.getLabels(), "Live");
+            
+            // Migrate structure only if the program has a an old live version and he is not validated
+            if (hasLiveVersion && !currentVersionIsLive)
             {
-                OrgUnit childOU = _resolver.resolveById(ouRefIds[i]);
-                jcrValues[i] = valueFactory.createValue(childOU.getNode());
+                String currentVersion = program.getNode().getBaseVersion().getName();
                 
-                // invert relation
-                _setRelation(childOU.getNode(), ou.getNode(), OrgUnit.METADATA_PARENT_ORGUNIT);
+                Node programNode = program.getNode();
                 
-                // browse child orgunits recursively
-                _migrateOrgunitStructure(childOU);
+                // switching to old live version
+                _restoreFromLabel("Live", programNode);
+
+                // migrate content types for program live structure
+                _migrateContentTypeForStructure(programNode);
+                 
+                // migrate workflow
+                _migrateWorkflow(program);
+                
+                // migrate old live version
+                _migrateProgramLiveSubStructure(program.getNode(), tableRefMapping);
+                
+                // migrate program
+                ContentType cType = _cTypeExtensionPoint.getExtension(ProgramFactory.PROGRAM_CONTENT_TYPE);
+                _migrateProgram(program, cType, tableRefMapping.get(ProgramFactory.PROGRAM_CONTENT_TYPE), true);
+                
+                // add label live
+                program.addLabel("Live", true); 
+                
+                // restore current version
+                program.restoreFromRevision(currentVersion);
+                
+                // migrate content types for program head structure
+                _migrateContentTypeForStructure(program.getNode());
             }
-            
-            ou.getMetadataHolder().removeMetadata("orgunitsReferences"); 
-            // ou.saveChanges();
+        }
+    }
+    
+    private void _migrateContentTypeForStructure(Node node) throws RepositoryException
+    {
+        if (node.hasProperty("ametys-internal:contentType"))  
+        {  
+            Property property = node.getProperty("ametys-internal:contentType");  
+            if (!property.getDefinition().isMultiple())  
+            {  
+                String value = property.getValue().getString();  
+                property.remove();  
+                node.setProperty("ametys-internal:contentType", new String[] {value});  
+            }  
+        }
+        
+        NodeIterator nodesIt2 = node.getNodes();
+        while (nodesIt2.hasNext())
+        {
+            Node childNode = nodesIt2.nextNode();
+            _migrateContentTypeForStructure(childNode);
         }
     }
     
-    private void _migrateSubStructure(Node parentNode, boolean currentRootVersionIsLive, List<WorkflowAwareContent> subContentsToValidate) throws RepositoryException, AmetysRepositoryException, WorkflowException
+    private void _migrateProgramLiveSubStructure(Node parentNode, Map<String, Map<String, String>> tableRefMapping) throws RepositoryException, AmetysRepositoryException, WorkflowException
     {
         NodeIterator nodesIt = parentNode.getNodes();
         
@@ -537,6 +526,11 @@
             
             if (childNodeType.equals("ametys:container") || childNodeType.equals("ametys:subProgramContent"))
             {
+                // Migrate catalog
+                Content content = _resolver.resolve(childNode, false);
+                _setCatalog((ProgramItem) content);
+                
+                // generate new node name
                 long hash = Math.abs(HashUtil.hash(childNode.getName()));
                 String hashStr = Long.toString(hash, 16);
                 hashStr = StringUtils.leftPad(hashStr, 4, '0');
@@ -556,27 +550,203 @@
                     }
                 }
                 
-                childNode.getSession().move(childNode.getPath(), contextNode.getPath() + "/" + childNode.getName());
+                String childNodeName = childNode.getName();
+                int index = 2;
+                while (contextNode.hasNode(childNodeName))
+                {
+                    childNodeName = childNode.getName() + "_" + index;
+                    index++;
+                }
+                
+                // move the content node to new path
+                childNode.getSession().move(childNode.getPath(), contextNode.getPath() + "/" + childNodeName);
                 
-                _setInvertRelation(childNode, parentNode, ProgramPart.METADATA_CHILD_PROGRAM_PARTS, ProgramPart.METADATA_PARENT_PROGRAM_PARTS);
+                // record relation relation between old node and new node
+                _oldContentToNewContentNode.put(childNode.getName(), childNode);
                 
-                if (currentRootVersionIsLive)
+                // set invert relation
+                _setInvertRelation(childNode, parentNode, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, ProgramPart.METADATA_PARENT_PROGRAM_PARTS);
+
+                // migrate workflow for restored content
+                _migrateWorkflow((DefaultContent) content);
+                
+                // migrate live sub structure
+                _migrateProgramLiveSubStructure(childNode, tableRefMapping);
+                
+                // migrate child content
+                if (content instanceof SubProgram)
                 {
-                    // Validate content if parent program is validated
-                    String aoId = StringUtils.substringAfter(childNode.getPrimaryNodeType().getName(), "ametys:") + "://" + childNode.getIdentifier();
-                    WorkflowAwareContent waContent = _resolver.resolveById(aoId);
-                    subContentsToValidate.add(waContent);
+                    ContentType cType = _cTypeExtensionPoint.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE);
+                    _migrateSubProgram((SubProgram) content, cType, tableRefMapping.get(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE), true);
+                }
+                else if (content instanceof Container)
+                {
+                    ContentType cType = _cTypeExtensionPoint.getExtension(ContainerFactory.CONTAINER_CONTENT_TYPE);
+                    _migrateContainer((Container) content, cType, tableRefMapping.get(ContainerFactory.CONTAINER_CONTENT_TYPE), true);
                 }
+                
+                // Add label live
+                ((DefaultContent) content).addLabel("Live", true);
+            }
+        }
+    }
+    
+    private void _migrateProgramsStructure(List<WorkflowAwareContent> toValidate, Map<String, Map<String, String>> tableRefMapping) throws RepositoryException, AmetysRepositoryException, WorkflowException
+    {
+        AmetysObjectIterable<Program> programs = _odfHelper.getProgramItems(ProgramFactory.PROGRAM_CONTENT_TYPE, null, null, null);
+        for (Program program : programs)
+        {
+            if (program.isLocked())
+            {
+                program.unlock();
+            }
+            
+            boolean currentProgramVersionIsLive = ArrayUtils.contains(program.getLabels(), "Live");
+            
+            // migrate workflow for program
+            _migrateWorkflow(program);
+            
+            // migrate program structure
+            _migrateProgramSubStructure(program.getNode(), currentProgramVersionIsLive, toValidate, tableRefMapping);
+            
+            // migrate program
+            ContentType cType = _cTypeExtensionPoint.getExtension(ProgramFactory.PROGRAM_CONTENT_TYPE);
+            boolean hasLiveVersion = ArrayUtils.contains(program.getAllLabels(), "Live");
+            boolean currentVersionIsLive = ArrayUtils.contains(program.getLabels(), "Live");
+            _migrateProgram(program, cType, tableRefMapping.get(ProgramFactory.PROGRAM_CONTENT_TYPE), !hasLiveVersion || currentVersionIsLive);
+            
+            if (currentProgramVersionIsLive)
+            {
+                program.addLabel("Live", true);
+            }
+        }
+    }
+    
+    private void _migrateProgramSubStructure(Node parentNode, boolean currentProgramVersionIsLive, List<WorkflowAwareContent> subContentsToValidate, Map<String, Map<String, String>> tableRefMapping) throws RepositoryException, AmetysRepositoryException, WorkflowException
+    {
+        NodeIterator nodesIt = parentNode.getNodes();
+        
+        while (nodesIt.hasNext())
+        {
+            Node childNode = nodesIt.nextNode();
+            String childNodeType = childNode.getPrimaryNodeType().getName();
+            
+            if (childNodeType.equals("ametys:container") || childNodeType.equals("ametys:subProgramContent"))
+            {
+                // Migrate catalog
+                Content content = _resolver.resolve(childNode, false);
+                _setCatalog((ProgramItem) content);
+                
+                boolean hasLiveVersion = _oldContentToNewContentNode.containsKey(childNode.getName());
+                if (hasLiveVersion)
+                {
+                    // Get live version node
+                    Node liveVersionContentNode = _oldContentToNewContentNode.get(childNode.getName());
+                    
+                    // Migrate sub structure
+                    _migrateProgramSubStructure(childNode, currentProgramVersionIsLive, subContentsToValidate, tableRefMapping);
+                    
+                    // Restore draft version
+                    restoreFromNode(childNode, liveVersionContentNode, false);
 
-                _migrateSubStructure(childNode, currentRootVersionIsLive, subContentsToValidate);
+                    // set invert relation
+                    _setInvertRelation(liveVersionContentNode, parentNode, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, ProgramPart.METADATA_PARENT_PROGRAM_PARTS);
+                    
+                    // Migrate content
+                    Content liveVersionContent = _resolver.resolve(liveVersionContentNode, false);
+                    if (liveVersionContent instanceof SubProgram)
+                    {
+                        ContentType cType = _cTypeExtensionPoint.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE);
+                        _migrateSubProgram((SubProgram) liveVersionContent, cType, tableRefMapping.get(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE), false);
+                    }
+                    else if (liveVersionContent instanceof Container)
+                    {
+                        ContentType cType = _cTypeExtensionPoint.getExtension(ContainerFactory.CONTAINER_CONTENT_TYPE);
+                        _migrateContainer((Container) liveVersionContent, cType, tableRefMapping.get(ContainerFactory.CONTAINER_CONTENT_TYPE), false);
+                    }
+                    
+                    // Remove node and workflow reference
+                    if (childNode.hasProperty("ametys-internal:workflowRef"))
+                    {
+                        Node node = childNode.getProperty("ametys-internal:workflowRef").getNode();
+                        if (node.hasProperty("ametys-internal:contentRef"))
+                        {
+                            node.getProperty("ametys-internal:contentRef").remove();
+                        }
+                    }
+                    
+                    childNode.remove();
+                }
+                else
+                {
+                    // generate new node name
+                    long hash = Math.abs(HashUtil.hash(childNode.getName()));
+                    String hashStr = Long.toString(hash, 16);
+                    hashStr = StringUtils.leftPad(hashStr, 4, '0');
+                    
+                    String[] hashPath = new String[]{hashStr.substring(0, 2), hashStr.substring(2, 4)};
+                    
+                    Node contextNode = _odfHelper.getRootContent().getNode();
+                    for (String hashPart : hashPath)
+                    {
+                        if (!contextNode.hasNode(hashPart))
+                        {
+                            contextNode = contextNode.addNode(hashPart, AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE);
+                        }
+                        else
+                        {
+                            contextNode = contextNode.getNode(hashPart); 
+                        }
+                    }
+                    
+                    String childNodeName = childNode.getName();
+                    int index = 2;
+                    while (contextNode.hasNode(childNodeName))
+                    {
+                        childNodeName = childNode.getName() + "_" + index;
+                        index++;
+                    }
+                    
+                    // move the content node to new path
+                    childNode.getSession().move(childNode.getPath(), contextNode.getPath() + "/" + childNodeName);
+                    
+                    // set invert relation
+                    _setInvertRelation(childNode, parentNode, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, ProgramPart.METADATA_PARENT_PROGRAM_PARTS);
+                    
+                    // Validate content if parent program is validated
+                    if (currentProgramVersionIsLive)
+                    {
+                        String aoId = StringUtils.substringAfter(childNode.getPrimaryNodeType().getName(), "ametys:") + "://" + childNode.getIdentifier();
+                        WorkflowAwareContent waContent = _resolver.resolveById(aoId);
+                        subContentsToValidate.add(waContent);
+                    }
+                    
+                    // migrate workflow for content
+                    _migrateWorkflow((DefaultContent) content);
+                    
+                    // migrate sub structure
+                    _migrateProgramSubStructure(childNode, currentProgramVersionIsLive, subContentsToValidate, tableRefMapping);
+                    
+                    // migrate child content
+                    if (content instanceof SubProgram)
+                    {
+                        ContentType cType = _cTypeExtensionPoint.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE);
+                        _migrateSubProgram((SubProgram) content, cType, tableRefMapping.get(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE), true);
+                    }
+                    else if (content instanceof Container)
+                    {
+                        ContentType cType = _cTypeExtensionPoint.getExtension(ContainerFactory.CONTAINER_CONTENT_TYPE);
+                        _migrateContainer((Container) content, cType, tableRefMapping.get(ContainerFactory.CONTAINER_CONTENT_TYPE), true);
+                    }
+                }
             }
             else if (childNodeType.equals("ametys:courseList"))
             {
                 DefaultContent content = _migrateCourseList(childNode);
                 
-                _setInvertRelation(content.getNode(), parentNode, ProgramPart.METADATA_CHILD_PROGRAM_PARTS, ProgramPart.METADATA_PARENT_PROGRAM_PARTS);
+                _setInvertRelation(content.getNode(), parentNode, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS, ProgramPart.METADATA_PARENT_PROGRAM_PARTS);
                 
-                if (currentRootVersionIsLive)
+                if (currentProgramVersionIsLive)
                 {
                     subContentsToValidate.add((WorkflowAwareContent) content);
                 }
@@ -585,6 +755,50 @@
         }
     }
     
+    private void _validateContent(WorkflowAwareContent waContent, int validateStepId)
+    {
+        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
+        AmetysStep currentStep = (AmetysStep) workflow.getCurrentSteps(waContent.getWorkflowId()).iterator().next();
+        
+        currentStep.setStepId(validateStepId);
+        
+        waContent.setCurrentStepId(validateStepId);
+        waContent.saveChanges();
+        
+        if (waContent instanceof VersionableAmetysObject)
+        {
+            ((VersionableAmetysObject) waContent).checkpoint();
+            ((VersionableAmetysObject) waContent).addLabel("Live", true);
+        }
+    }
+    
+    private void _migrateOrgunitStructure(OrgUnit ou) throws RepositoryException
+    {
+        if (ou.getMetadataHolder().hasMetadata("orgunitsReferences"))
+        {
+            // orgunitsReferences renamed to childOrgUnits
+            String[] ouRefIds = ou.getMetadataHolder().getStringArray("orgunitsReferences");
+            ou.getMetadataHolder().setMetadata(OrgUnit.METADATA_CHILD_ORGUNITS, ouRefIds);
+            
+            ValueFactory valueFactory = ou.getNode().getSession().getValueFactory();
+            Value[] jcrValues = new Value[ouRefIds.length];
+            for (int i = 0; i < ouRefIds.length; i++)
+            {
+                OrgUnit childOU = _resolver.resolveById(ouRefIds[i]);
+                jcrValues[i] = valueFactory.createValue(childOU.getNode());
+                
+                // invert relation
+                _setRelation(childOU.getNode(), ou.getNode(), OrgUnit.METADATA_PARENT_ORGUNIT);
+                
+                // browse child orgunits recursively
+                _migrateOrgunitStructure(childOU);
+            }
+            
+            ou.getMetadataHolder().removeMetadata("orgunitsReferences"); 
+            // ou.saveChanges();
+        }
+    }
+    
     private void _migrateSCC(DefaultContent content, String syncCode)
     {
         SynchronizableContentsCollection collection = _getSCC(content);
@@ -724,17 +938,20 @@
         ContentType cType = _cTypeExtensionPoint.getExtension(CourseListFactory.COURSE_LIST_CONTENT_TYPE);
         MigrateSynchronizedMetadataHelper.migrateSynchronizedMetadata(cType, (ModifiableContent) content);
         
-        String[] courseRefIds = content.getMetadataHolder().getStringArray("coursesReferences");
-        content.getMetadataHolder().setMetadata(CourseList.METADATA_CHILD_COURSES, courseRefIds);
-        
-        ValueFactory valueFactory = content.getNode().getSession().getValueFactory();
-        Value[] jcrValues = new Value[courseRefIds.length];
-        for (int i = 0; i < courseRefIds.length; i++)
+        if (content.getMetadataHolder().hasMetadata("coursesReferences"))
         {
-            Course course = _resolver.resolveById(courseRefIds[i]);
-            jcrValues[i] = valueFactory.createValue(course.getNode());
+            String[] courseRefIds = content.getMetadataHolder().getStringArray("coursesReferences");
+            content.getMetadataHolder().setMetadata(CourseList.METADATA_CHILD_COURSES, courseRefIds);
             
-            _addRelation(course.getNode(), contentNode, Course.METADATA_PARENT_COURSE_LISTS);
+            ValueFactory valueFactory = content.getNode().getSession().getValueFactory();
+            Value[] jcrValues = new Value[courseRefIds.length];
+            for (int i = 0; i < courseRefIds.length; i++)
+            {
+                Course course = _resolver.resolveById(courseRefIds[i]);
+                jcrValues[i] = valueFactory.createValue(course.getNode());
+                
+                _addRelation(course.getNode(), contentNode, Course.METADATA_PARENT_COURSE_LISTS);
+            }
         }
         
         String syncCode = null;
@@ -762,7 +979,11 @@
         _migrateSCC(content, syncCode);
         
         content.getNode().setProperty("ametys:catalog", courseListNode.getParent().getProperty("ametys:catalog").getString());
-        content.getMetadataHolder().removeMetadata("coursesReferences");  
+        
+        if (content.getMetadataHolder().hasMetadata("coursesReferences"))
+        {
+            content.getMetadataHolder().removeMetadata("coursesReferences");  
+        }
         
         _newModelMigration.migrateCourseList((CourseList) content);
         
@@ -772,56 +993,7 @@
         return content;
     }
     
-    private void _migrateSubPrograms(Map<String, String> tableRefMapping) throws UnsupportedRepositoryOperationException, RepositoryException
-    {
-        getLogger().info("Start migration of subprograms...");
-        
-        int count = 0;
-        ContentType cType = _cTypeExtensionPoint.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE);
-        AmetysObjectIterable<SubProgram> subprograms = _odfHelper.getProgramItems(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE, null, null, null);
-
-        for (SubProgram subprogram : subprograms)
-        {
-            if (subprogram.isLocked())
-            {
-                subprogram.unlock();
-            }
-            
-            String[] allLabels = subprogram.getAllLabels();
-            String[] currentLabels = subprogram.getLabels();
-            
-            boolean hasLiveVersion = ArrayUtils.contains(allLabels, "Live");
-            boolean currentVersionIsLive = ArrayUtils.contains(currentLabels, "Live");
-            
-            if (hasLiveVersion && !currentVersionIsLive)
-            {
-                String currentVersion = subprogram.getNode().getBaseVersion().getName();
-                
-                // switching to old live version
-                subprogram.restoreFromLabel("Live");
-                // migrate old live version
-                _migrateSubProgram(subprogram, cType, tableRefMapping);
-                subprogram.addLabel("Live", true); 
-                
-                // restore current version
-                subprogram.restoreFromRevision(currentVersion);
-            }
-            
-            // Applying migration on head version
-            _migrateSubProgram(subprogram, cType, tableRefMapping);
-            
-            if (currentVersionIsLive)
-            {
-                subprogram.addLabel("Live", true);
-            }
-
-            count++;
-        }
-        
-        getLogger().info("{} subprograms have been migrated", count);
-    }
-    
-    private void _migrateSubProgram(SubProgram subprogram, ContentType cType, Map<String, String> tableRefMapping) throws PathNotFoundException, RepositoryException
+    private void _migrateSubProgram(SubProgram subprogram, ContentType cType, Map<String, String> tableRefMapping, boolean addTag) throws PathNotFoundException, RepositoryException
     {
         MigrateSynchronizedMetadataHelper.migrateSynchronizedMetadata(cType, subprogram);
         
@@ -835,58 +1007,10 @@
         
         _newModelMigration.migrateSubProgram(subprogram);
         
-        _endMigration(subprogram);
-    }
-    
-    private void _migrateContainers(Map<String, String> tableRefMapping) throws UnsupportedRepositoryOperationException, RepositoryException
-    {
-        getLogger().info("Start migration of containers...");
-        
-        int count = 0;
-        ContentType cType = _cTypeExtensionPoint.getExtension(ContainerFactory.CONTAINER_CONTENT_TYPE);
-        AmetysObjectIterable<Container> containers = _odfHelper.getProgramItems(ContainerFactory.CONTAINER_CONTENT_TYPE, null, null, null);
-        for (Container container : containers)
-        {
-            if (container.isLocked())
-            {
-                container.unlock();
-            }
-            
-            String[] allLabels = container.getAllLabels();
-            String[] currentLabels = container.getLabels();
-            
-            boolean hasLiveVersion = ArrayUtils.contains(allLabels, "Live");
-            boolean currentVersionIsLive = ArrayUtils.contains(currentLabels, "Live");
-            
-            if (hasLiveVersion && !currentVersionIsLive)
-            {
-                String currentVersion = container.getNode().getBaseVersion().getName();
-                
-                // switching to old live version
-                container.restoreFromLabel("Live");
-                // migrate old live version
-                _migrateContainer(container, cType, tableRefMapping);
-                container.addLabel("Live", true); 
-                
-                // restore current version
-                container.restoreFromRevision(currentVersion);
-            }
-            
-            // Applying migration on head version
-            _migrateContainer(container, cType, tableRefMapping);
-            
-            if (currentVersionIsLive)
-            {
-                container.addLabel("Live", true);
-            }
-
-            count++;
-        }
-        
-        getLogger().info("{} containers have been migrated", count);
+        _endMigration(subprogram, addTag);
     }
     
-    private void _migrateContainer(Container container, ContentType cType, Map<String, String> tableRefMapping) throws UnsupportedRepositoryOperationException, RepositoryException
+    private void _migrateContainer(Container container, ContentType cType, Map<String, String> tableRefMapping, boolean addTag) throws UnsupportedRepositoryOperationException, RepositoryException
     {
         MigrateSynchronizedMetadataHelper.migrateSynchronizedMetadata(cType, container);
         
@@ -900,7 +1024,7 @@
         
         _newModelMigration.migrateContainer(container);
         
-        _endMigration(container);
+        _endMigration(container, addTag);
     }
     
     private void _migrateWorkflowEntries(AbstractProgramPart content, String workflowName) throws PathNotFoundException, RepositoryException
@@ -956,6 +1080,10 @@
                 
                 // switching to old live version
                 course.restoreFromLabel("Live");
+                
+                // migrate content type, workflow
+                _migrateV3ContentToV4(course);
+                
                 // migrate old live version
                 _migrateCourse(course, cType, tableRefMapping);
                 course.addLabel("Live", true); 
@@ -964,6 +1092,12 @@
                 course.restoreFromRevision(currentVersion);
             }
             
+            // migrate workflow
+            _migrateWorkflow(course);
+            
+            // migrate content types for course head structure
+            _migrateContentTypeForStructure(course.getNode());
+            
             // Applying migration on head version
             _migrateCourse(course, cType, tableRefMapping);
             
@@ -991,7 +1125,7 @@
         
         _newModelMigration.migrateCourse(course);
         
-        _endMigration(course);
+        _endMigration(course, true);
     }
     
     private void _migratePerson() throws UnsupportedRepositoryOperationException, RepositoryException
@@ -1020,14 +1154,27 @@
                 
                 // switching to old live version
                 person.restoreFromLabel("Live");
+                
+                // migrate content type, workflow
+                _migrateV3ContentToV4(person);
+                
                 // migrate old live version
                 _migratePerson(person, cType);
                 person.addLabel("Live", true); 
                 
                 // restore current version
                 person.restoreFromRevision(currentVersion);
+                
+                // migrate content type
+                _migrateContentTypeForStructure(person.getNode());
             }
             
+            // migrate workflow
+            _migrateWorkflow(person);
+            
+            // migrate content types for person head structure
+            _migrateContentTypeForStructure(person.getNode());
+            
             // Applying migration on head version
             _migratePerson(person, cType);
             
@@ -1046,6 +1193,17 @@
     {
         MigrateSynchronizedMetadataHelper.migrateSynchronizedMetadata(cType, person);
         
+        // TODO Choisir méthode 1 ou méthode 2 selon les données
+        
+        /* Methode 1 :
+         * A effectuer pour construire le titre si personTitle est bien utilisé
+        */
+        person.getMetadataHolder().setMetadata("title", person.getGivenName() + " " + person.getLastName());
+        
+        /* Methode 2 :
+         * A effectuer si le personTitle ne correspond pas à un role ou une civilité (Mr, Mme, Dr, Professeur, etc ...)
+        */
+        /*         
         if (person.getMetadataHolder().hasMetadata("personTitle"))
         {
             String personTitle = person.getMetadataHolder().getString("personTitle");
@@ -1074,9 +1232,9 @@
                 person.getMetadataHolder().setMetadata("title" + ExternalizableMetadataHelper.STATUS_SUFFIX, personTitleStatus);
             }
             person.getMetadataHolder().removeMetadata("personTitle" + ExternalizableMetadataHelper.STATUS_SUFFIX);
-        }
+        } */
         
-        _endMigration(person);
+        _endMigration(person, true);
     }
     
     private void _migrateOrgunits(Map<String, String> tableRefMapping) throws UnsupportedRepositoryOperationException, RepositoryException
@@ -1105,14 +1263,24 @@
                 
                 // switching to old live version
                 orgunit.restoreFromLabel("Live");
+                
+                // migrate content type, workflow
+                _migrateV3ContentToV4(orgunit);
+                
                 // migrate old live version
                 _migrateOrgunit(orgunit, cType, tableRefMapping);
                 orgunit.addLabel("Live", true); 
                 
                 // restore current version
                 orgunit.restoreFromRevision(currentVersion);
+                
+                // migrate content type
+                _migrateContentTypeForStructure(orgunit.getNode());
             }
             
+            // migrate workflow
+            _migrateWorkflow(orgunit);
+            
             // Applying migration on head version
             _migrateOrgunit(orgunit, cType, tableRefMapping);
             
@@ -1136,7 +1304,7 @@
             _contentTableRef(cType, orgunit, tableRefMapping);
         }
         
-        _endMigration(orgunit);
+        _endMigration(orgunit, true);
     }
     
     private void _setCatalog(ProgramItem part)
@@ -1151,6 +1319,15 @@
                     part.setCatalog(((Program) ao).getCatalog());
                     return;
                 }
+                else if (ao instanceof SubProgram || ao instanceof Container)
+                {
+                    ProgramItem parentPart = (ProgramItem) ao;
+                    if (StringUtils.isNotBlank(parentPart.getCatalog()))
+                    {
+                        part.setCatalog(parentPart.getCatalog());
+                        return;
+                    }
+                }
                 ao = ao.getParent();
             }
         }
@@ -1310,17 +1487,23 @@
         return convertedIds.toArray(new String[convertedIds.size()]);
     }
     
-    private void _endMigration(DefaultContent content)
+    private void _endMigration(DefaultContent content, boolean addTag)
     {
         if (content.needsSave())
         {
-            // Add POST_V4 label
-            content.addLabel("NotCompatible", true);
+            if (addTag)
+            {
+                // Add POST_V4 label
+                content.addLabel("NotCompatible", true);
+            }
             
             content.saveChanges();
             content.checkpoint();
             
-            content.addLabel(__V4_LABEL, true);
+            if (addTag)
+            {
+                content.addLabel(__V4_LABEL, true);
+            }
         }
     }
     
@@ -1563,4 +1746,224 @@
         }
         return entries;
     }
+    
+    private void _migrateV3ContentToV4(DefaultContent content)
+    {
+        _migrateContentType(content);
+        _migrateWorkflow(content);
+    }
+    
+    private void _migrateContentType(DefaultContent content)
+    {
+        Node node = content.getNode();
+        try
+        {
+            if (node.hasProperty("ametys-internal:contentType"))  
+            {  
+                Property property = node.getProperty("ametys-internal:contentType");  
+                if (!property.getDefinition().isMultiple())  
+                {  
+                    String value = property.getValue().getString();  
+                    property.remove();  
+                    node.setProperty("ametys-internal:contentType", new String[] {value});  
+                }  
+            }
+        }
+        catch (RepositoryException e) 
+        {
+            getLogger().error("An error occurred : can't migrate content types for content " + content.getName(), e); 
+        }
+    }
+    
+    private void _migrateWorkflow(DefaultContent content)
+    {
+        Node node = content.getNode();
+        try
+        {
+            if (node.hasProperty("ametys-internal:workflowRef"))  
+            {  
+                // 1 - get entry node + ensure not already local  
+                Node entryNode = node.getProperty("ametys-internal:workflowRef").getNode();
+                String entryName = entryNode.getName();  
+                if (!entryNode.getParent().getParent().getPath().equals(node.getPath()))  
+                {  
+                    Node wfRootNode = _getOrCreateRootNode(node);  
+                    Long entryId = _getAndSetNextEntryId(wfRootNode);  
+                    entryNode.setProperty("oswf:id", entryId);  
+                    entryName = "workflow-" + entryId;  
+                    entryNode.getProperty("ametys-internal:contentRef").remove();
+                    entryNode.getSession().move(entryNode.getPath(), wfRootNode.getPath() + '/' + entryName);  
+                } 
+
+                // 2 - update workflow id if needed  
+                if (node.hasProperty("ametys-internal:workflowId"))  
+                {  
+                    Long contentWorkflowId = node.getProperty("ametys-internal:workflowId").getLong();  
+                    int entryWorkflowId = Integer.parseInt(StringUtils.substring(entryName, 9));  
+                    if (contentWorkflowId != entryWorkflowId)  
+                    {  
+                        node.setProperty("ametys-internal:workflowId", entryWorkflowId);  
+                    }  
+                }
+              
+                // 3 - Remove workflowRef property  
+                node.getProperty("ametys-internal:workflowRef").remove();  
+            }
+        }
+        catch (RepositoryException e) 
+        {
+            getLogger().error("An error occurred : can't migrate workflows for content " + content.getName(), e); 
+        }
+    }
+    
+    private Node _getOrCreateRootNode(Node contentNode) throws RepositoryException 
+    {  
+        if (!contentNode.hasNode("ametys-internal:workflows"))  
+        {  
+            contentNode.addNode("ametys-internal:workflows");  
+        }  
+        return contentNode.getNode("ametys-internal:workflows");  
+    }  
+  
+    private Long _getAndSetNextEntryId(Node wfRootNode) throws RepositoryException  
+    {  
+        Long nextEntryId = wfRootNode.getProperty("oswf:nextEntryId").getLong();  
+        wfRootNode.setProperty("oswf:nextEntryId", nextEntryId + 1);  
+        return nextEntryId;  
+    } 
+    
+    private void _restoreFromLabel(String label, Node baseNode) throws UnknownAmetysObjectException, AmetysRepositoryException
+    {
+        try
+        {
+            VersionHistory history = baseNode.getVersionHistory();
+            Node versionNode = history.getVersionByLabel(label);
+            restoreFromNode(versionNode.getNode("jcr:frozenNode"), baseNode, true);
+        }
+        catch (RepositoryException e)
+        {
+            throw new AmetysRepositoryException("Unable to restore from label: " + label, e);
+        }
+    }
+    
+    /**
+     * Restore from a node
+     * @param node The node to restore
+     * @throws RepositoryException If error occurs
+     */
+    protected void restoreFromNode(Node node, Node baseNode, boolean handleSubContent) throws RepositoryException
+    {
+        // Remove all properties and nodes of the current node (except jcr and OnParentVersion=IGNORE).
+        PropertyIterator propIt = baseNode.getProperties();
+        while (propIt.hasNext())
+        {
+            Property prop = propIt.nextProperty();
+            String propName = prop.getName();
+            if (!propName.startsWith("jcr:") && prop.getDefinition().getOnParentVersion() != OnParentVersionAction.IGNORE)
+            {
+                prop.remove();
+            }
+        }
+        
+        NodeIterator nodeIt = baseNode.getNodes();
+        while (nodeIt.hasNext())
+        {
+            Node childNode = nodeIt.nextNode();
+            String nodeName = childNode.getName();
+            if (!nodeName.startsWith("jcr:") && childNode.getDefinition().getOnParentVersion() != OnParentVersionAction.IGNORE)
+            {
+                if (!childNode.isNodeType("ametys:content"))
+                {
+                    childNode.remove();
+                }
+            }
+        }
+        
+        // Copy all properties and nodes from the given node (except jcr).
+        PropertyIterator newPropIt = node.getProperties();
+        while (newPropIt.hasNext())
+        {
+            Property newProp = newPropIt.nextProperty();
+            
+            if (!newProp.getName().startsWith("jcr:"))
+            {
+                if (newProp.getDefinition().isMultiple())
+                {
+                    baseNode.setProperty(newProp.getName(), newProp.getValues());
+                }
+                else
+                {
+                    baseNode.setProperty(newProp.getName(), newProp.getValue());
+                }
+            }
+        }
+        
+        NodeIterator newNodeIt = node.getNodes();
+        while (newNodeIt.hasNext())
+        {
+            Node newNode = newNodeIt.nextNode();
+            
+            if (!newNode.getName().startsWith("jcr:"))
+            {
+                if (!newNode.isNodeType("ametys:content"))
+                {
+                    copyNode(baseNode, newNode);
+                }
+                else if (handleSubContent)
+                {
+                    restoreFromNode(newNode, baseNode.getNode(newNode.getName()), handleSubContent);
+                }
+            }
+        }
+    }
+    
+    /**
+     * Copy the source node in parent node
+     * @param parentDest The dest node
+     * @param src The source node to copy
+     * @throws RepositoryException If error occurs
+     */
+    protected void copyNode(Node parentDest, Node src) throws RepositoryException
+    {
+        Node dest;
+        if (parentDest.hasNode(src.getName()))
+        {
+            // case of auto created child
+            dest = parentDest.getNode(src.getName());
+        }
+        else if (src.hasProperty("jcr:frozenPrimaryType"))
+        {
+            dest = parentDest.addNode(src.getName(), src.getProperty("jcr:frozenPrimaryType").getString());
+        }
+        else
+        {
+            dest = parentDest.addNode(src.getName(), src.getProperty("jcr:primaryType").getString());
+        }
+        
+        PropertyIterator pit = src.getProperties();
+        while (pit.hasNext())
+        {
+            Property p = pit.nextProperty();
+            String name = p.getName();
+            
+            // Tests for protected and/or autocreated properties
+            if (!PROTECTED_PROPERTIES.contains(name) && !name.startsWith("jcr:frozen") && !dest.hasProperty(name))
+            {
+                if (p.getDefinition().isMultiple())
+                {
+                    dest.setProperty(name, p.getValues());
+                }
+                else
+                {
+                    dest.setProperty(name, p.getValue());
+                }
+            }
+        }
+        
+        NodeIterator nit = src.getNodes();
+        while (nit.hasNext())
+        {
+            copyNode(dest, nit.nextNode());
+        }
+    }
 }
