From 9c02802042aa01b8d7154cd223656e892c3009bc Mon Sep 17 00:00:00 2001
From: bourgesl <bourges.laurent@gmail.com>
Date: Mon, 20 Feb 2017 13:54:41 +0100
Subject: [PATCH] major update: generate redirections + datacite client +
 entity refactoring

---
 doc/application.yml                           |   1 +
 src/main/java/fr/osug/DOIApplication.java     |  61 +-
 src/main/java/fr/osug/doi/Const.java          |   1 -
 src/main/java/fr/osug/doi/CsvData.java        |   1 -
 src/main/java/fr/osug/doi/CsvUtil.java        |  73 +-
 src/main/java/fr/osug/doi/DOIConfig.java      |  45 +-
 src/main/java/fr/osug/doi/DataciteConfig.java |  60 ++
 src/main/java/fr/osug/doi/DoiCsvData.java     |   1 -
 src/main/java/fr/osug/doi/DoiCsvSplitter.java |   1 -
 src/main/java/fr/osug/doi/DoiCsvToXml.java    |  18 -
 src/main/java/fr/osug/doi/DoiTemplates.java   |   1 -
 .../java/fr/osug/doi/GeneratePipeline.java    | 828 +++++++-----------
 .../fr/osug/doi/GenerateRedirectPipeline.java | 208 +++++
 src/main/java/fr/osug/doi/IndexUtil.java      |   3 +-
 src/main/java/fr/osug/doi/PathConfig.java     | 281 +-----
 src/main/java/fr/osug/doi/Paths.java          |  21 +-
 .../java/fr/osug/doi/PipelineCommonData.java  |  20 +
 src/main/java/fr/osug/doi/PipelineConfig.java |  12 +
 .../java/fr/osug/doi/ProcessPipeline.java     | 453 +++-------
 .../java/fr/osug/doi/ProcessPipelineData.java |  35 +-
 .../fr/osug/doi/ProcessPipelineDoiData.java   |  37 +-
 src/main/java/fr/osug/doi/ProjectConfig.java  | 301 +++++--
 src/main/java/fr/osug/doi/domain/Doi.java     |  45 +-
 .../java/fr/osug/doi/domain/DoiCommon.java    | 139 +++
 .../java/fr/osug/doi/domain/DoiPublic.java    | 112 +--
 .../java/fr/osug/doi/domain/DoiStaging.java   | 113 +--
 .../java/fr/osug/doi/domain/IndexEntry.java   |   3 +-
 .../java/fr/osug/doi/domain/NameEntry.java    |  44 +-
 src/main/java/fr/osug/doi/domain/Project.java |  29 +-
 .../java/fr/osug/doi/domain/model/dbmodel.jpa |  38 +-
 .../doi/repository/DoiBaseRepository.java     |  25 +-
 .../doi/repository/DoiPublicRepository.java   |  16 +-
 .../doi/repository/DoiStagingRepository.java  |  14 +-
 .../doi/repository/ProjectRepository.java     |   5 +
 .../fr/osug/doi/service/DataciteClient.java   | 242 +++++
 .../java/fr/osug/doi/service/DoiService.java  |  77 +-
 .../osug/doi/validation/ValidationUtil.java   |  93 +-
 .../osug/doi/validation/WarningMessage.java   |   1 -
 src/main/java/fr/osug/util/FileUtils.java     |  41 +-
 src/main/java/fr/osug/xml/XmlFactory.java     |   6 +-
 .../resources/config/application-default.yml  |  14 +-
 .../resources/config/application-test.yml     |  13 +-
 src/main/resources/config/application.yml     |  14 +-
 src/main/resources/db/migration/V1__init.sql  |  22 +-
 src/main/resources/logback-spring.xml         |   5 +-
 src/test/java/fr/osug/doi/DOIServiceTest.java |  30 +-
 .../java/fr/osug/doi/DOITextToXmlTest.java    |  22 +-
 47 files changed, 1925 insertions(+), 1700 deletions(-)
 create mode 100644 src/main/java/fr/osug/doi/DataciteConfig.java
 create mode 100644 src/main/java/fr/osug/doi/GenerateRedirectPipeline.java
 create mode 100644 src/main/java/fr/osug/doi/PipelineCommonData.java
 create mode 100644 src/main/java/fr/osug/doi/PipelineConfig.java
 create mode 100644 src/main/java/fr/osug/doi/domain/DoiCommon.java
 create mode 100644 src/main/java/fr/osug/doi/service/DataciteClient.java

diff --git a/doc/application.yml b/doc/application.yml
index c05c9de..125b5cf 100644
--- a/doc/application.yml
+++ b/doc/application.yml
@@ -6,6 +6,7 @@ osug:
     doi:
         prefix: 10.17178
         domain: http://doi.osug.fr
+        dataciteClientEnabled: true
 
 datacite:
     user: INIST.OSUG
diff --git a/src/main/java/fr/osug/DOIApplication.java b/src/main/java/fr/osug/DOIApplication.java
index dc87d99..63a7bc9 100644
--- a/src/main/java/fr/osug/DOIApplication.java
+++ b/src/main/java/fr/osug/DOIApplication.java
@@ -6,6 +6,7 @@ package fr.osug;
 import ch.qos.logback.classic.Level;
 import fr.osug.doi.ProcessPipeline;
 import fr.osug.doi.DOIConfig;
+import fr.osug.doi.GeneratePipeline;
 import fr.osug.doi.Paths;
 import fr.osug.doi.ProjectConfig;
 import java.io.IOException;
@@ -24,11 +25,14 @@ import org.springframework.boot.ApplicationRunner;
 import org.springframework.boot.ExitCodeGenerator;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration;
+import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
 
-@SpringBootApplication
+@SpringBootApplication(exclude = {EmbeddedServletContainerAutoConfiguration.class, WebMvcAutoConfiguration.class})
+@EnableTransactionManagement
 public class DOIApplication {
 
     private final static Logger logger = LoggerFactory.getLogger(DOIApplication.class.getName());
@@ -117,6 +121,9 @@ public class DOIApplication {
                 case "process":
                     doProcess(aa);
                     break;
+                case "generate":
+                    doGenerate(aa);
+                    break;
                 default:
                     if (action != null) {
                         fail("Unsupported action: " + action);
@@ -124,22 +131,17 @@ public class DOIApplication {
             }
         }
 
-//        @Transactional
-        public void doProcess(ApplicationArguments aa) {
-
+        private void doProcess(ApplicationArguments aa) {
             // Required values:
             final String project = getRequiredValue("project", aa);
-
+            // Optional values:
             final boolean saveCSV = aa.containsOption("csv");
 
             try {
-                final ProjectConfig projectConfig = new ProjectConfig(project);
+                final ProjectConfig projectConfig = new ProjectConfig(doiConfig.getPathConfig(), project);
                 projectConfig.setSaveCSV(saveCSV);
 
-                final ProcessPipeline pipeline = new ProcessPipeline(
-                        doiConfig,
-                        projectConfig);
-
+                final ProcessPipeline pipeline = new ProcessPipeline(doiConfig, projectConfig);
                 pipeline.process();
 
             } catch (IOException ioe) {
@@ -148,20 +150,40 @@ public class DOIApplication {
             }
         }
 
+        private void doGenerate(ApplicationArguments aa) {
+            // Optional values:
+            final String project = getOptionalValue("project", aa);
+            // mode (staging / public)
+            final String mode = getOptionalValue("mode", aa);
+
+            final boolean doStaging = (mode == null) || "staging".equalsIgnoreCase(mode) || "all".equalsIgnoreCase(mode);
+            final boolean doPublic = "public".equalsIgnoreCase(mode) || "all".equalsIgnoreCase(mode);
+
+            try {
+                final GeneratePipeline pipeline = new GeneratePipeline(doiConfig, doStaging, doPublic);
+                pipeline.process(project);
+
+            } catch (IOException ioe) {
+                // will rollback transaction:
+                throw new RuntimeException(ioe);
+            }
+        }
+
         /** Show command arguments help. */
         public static void showArgumentsHelp() {
             System.out.println("---------------------------------- Arguments help ------------------------------");
             System.out.println("| Key          Value           Description                                     |");
             System.out.println("|------------------------------------------------------------------------------|");
-            System.out.println("| --action=[help|process]       Define the command to perform                  |");
+            System.out.println("| --action=[help|process|generate] Define the command to perform               |");
             System.out.println("| [--v=0|1|2|3|4|5]             Define console logging level                   |");
             System.out.println("|                                                                              |");
             System.out.println("| LOG LEVELS : 0 = OFF, 1 = ERROR, 2 = WARNING, 3 = INFO, 4 = DEBUG, 5 = ALL   |");
             System.out.println("|------------------------------------------------------------------------------|");
             System.out.println("| Action [process]:                                                            |");
-            System.out.println("| --project=<dir>               Project folder name (case sensitive)           |");
+            System.out.println("| --project=<name>              Project folder name (case sensitive)           |");
             System.out.println("| [--csv]                       CSV Output format                              |");
-            System.out.println("| [--xml]                       XML Output format (default)                    |");
+            System.out.println("| Action [generate]:                                                           |");
+            System.out.println("| --project=<name>              Optional Project folder name (case sensitive)  |");
             System.out.println("|------------------------------------------------------------------------------|\n");
         }
 
@@ -180,7 +202,14 @@ public class DOIApplication {
             this.exitCode = exitCode;
         }
 
-        private String getRequiredValue(String opt, final ApplicationArguments aa) {
+        private String getOptionalValue(final String opt, final ApplicationArguments aa) {
+            if (aa.containsOption(opt)) {
+                return getSingleValue(opt, aa);
+            }
+            return null;
+        }
+
+        private String getRequiredValue(final String opt, final ApplicationArguments aa) {
             if (!aa.containsOption(opt)) {
                 fail("Missing argument '" + opt + "' !");
                 return null;
@@ -188,7 +217,7 @@ public class DOIApplication {
             return getSingleValue(opt, aa);
         }
 
-        private static String getSingleValue(String opt, final ApplicationArguments aa) {
+        private static String getSingleValue(final String opt, final ApplicationArguments aa) {
             final List<String> values = aa.getOptionValues(opt);
             if (values.size() != 1) {
                 throw new IllegalArgumentException("Too many values for argument '" + opt + "': " + values);
diff --git a/src/main/java/fr/osug/doi/Const.java b/src/main/java/fr/osug/doi/Const.java
index 9ba607d..0e44f5d 100644
--- a/src/main/java/fr/osug/doi/Const.java
+++ b/src/main/java/fr/osug/doi/Const.java
@@ -5,7 +5,6 @@ package fr.osug.doi;
 
 /**
  *
- * @author bourgesl
  */
 public interface Const {
     
diff --git a/src/main/java/fr/osug/doi/CsvData.java b/src/main/java/fr/osug/doi/CsvData.java
index 571b5cd..940c82c 100644
--- a/src/main/java/fr/osug/doi/CsvData.java
+++ b/src/main/java/fr/osug/doi/CsvData.java
@@ -9,7 +9,6 @@ import org.slf4j.LoggerFactory;
 
 /**
  * Generic CSV Data holder
- * @author bourgesl
  */
 public class CsvData {
 
diff --git a/src/main/java/fr/osug/doi/CsvUtil.java b/src/main/java/fr/osug/doi/CsvUtil.java
index afdfa5e..7c98c13 100644
--- a/src/main/java/fr/osug/doi/CsvUtil.java
+++ b/src/main/java/fr/osug/doi/CsvUtil.java
@@ -7,23 +7,22 @@ import com.opencsv.CSVReader;
 import com.opencsv.CSVWriter;
 import fr.osug.util.FileUtils;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.FilenameFilter;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.Writer;
-import java.nio.charset.StandardCharsets;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  *
- * @author bourgesl
  */
 public final class CsvUtil {
 
@@ -36,7 +35,7 @@ public final class CsvUtil {
     }
 
     public static CsvData read(final File csvFile) throws IOException {
-        final CSVReader reader = new CSVReader(getTextReader(csvFile), Const.SEPARATOR);
+        final CSVReader reader = new CSVReader(FileUtils.getTextReader(csvFile), Const.SEPARATOR);
         return new CsvData(csvFile.getAbsolutePath(), reader.readAll());
     }
 
@@ -48,7 +47,7 @@ public final class CsvUtil {
     }
 
     public static void write(final CsvData data, final OutputStream out) throws IOException {
-        final CSVWriter writer = new CSVWriter(getTextWriter(out), Const.SEPARATOR);
+        final CSVWriter writer = new CSVWriter(FileUtils.getTextWriter(out), Const.SEPARATOR);
         try {
             writer.writeAll(data.getRows(), false);
         } finally {
@@ -56,22 +55,14 @@ public final class CsvUtil {
         }
     }
 
-    public static Reader getTextReader(final File file) throws IOException {
-        return new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
-    }
-
-    public static Writer getTextWriter(final File file) throws IOException {
-        return getTextWriter(new FileOutputStream(file));
-    }
-
-    public static Writer getTextWriter(final OutputStream out) throws IOException {
-        return new OutputStreamWriter(out, StandardCharsets.UTF_8);
-    }
-
     public static File[] findCSVFiles(final String dirPath) throws IOException {
         final File inputDir = FileUtils.getRequiredDirectory(dirPath).getCanonicalFile();
-        // find CSV files:
-        return CsvUtil.getSortedFiles(inputDir, ".*\\.csv");
+        return findCSVFiles(inputDir);
+    }
+
+    public static File[] findCSVFiles(final File dir) throws IOException {
+        logger.debug("findCSVFiles: {}", dir);
+        return CsvUtil.getSortedFiles(dir, ".*\\.csv");
     }
 
     public static File[] getSortedFiles(final File directory, final String regexp) {
@@ -87,4 +78,40 @@ public final class CsvUtil {
 
         return dataFiles;
     }
+
+    public static Map<String, String> loadUrlMapping(final File csvFile) throws IOException {
+        Map<String, String> urlMapping = Collections.emptyMap();
+
+        if (csvFile.exists()) {
+            final CsvData data = CsvUtil.read(csvFile);
+            logger.debug("CSV data: {}", data);
+
+            final List<String[]> rows = data.getRows();
+
+            urlMapping = new LinkedHashMap<String, String>(rows.size());
+
+            // Get mappings [DOI|URL]
+            for (String[] cols : rows) {
+                if (cols == null || cols.length != 2) {
+                    continue;
+                }
+                String key = cols[0].trim();
+                if (key.isEmpty() || key.charAt(0) == Const.COMMENT) {
+                    continue;
+                }
+                String url = cols[1].trim();
+                if (url.isEmpty() || !url.startsWith("http")) {
+                    continue;
+                }
+                try {
+                    // should detect repeated keys or values ?
+                    urlMapping.put(key, new URL(url).toString());
+                } catch (MalformedURLException mue) {
+                    logger.info("invalid URL: {}", url);
+                }
+            }
+        }
+        logger.debug("urlMapping: {}", urlMapping);
+        return urlMapping;
+    }
 }
diff --git a/src/main/java/fr/osug/doi/DOIConfig.java b/src/main/java/fr/osug/doi/DOIConfig.java
index 49b71bc..5516897 100644
--- a/src/main/java/fr/osug/doi/DOIConfig.java
+++ b/src/main/java/fr/osug/doi/DOIConfig.java
@@ -3,9 +3,11 @@
  ******************************************************************************/
 package fr.osug.doi;
 
+import fr.osug.doi.service.DataciteClient;
 import fr.osug.doi.service.DoiService;
 import fr.osug.xml.XmlFactory;
 import fr.osug.xml.validator.XmlValidatorFactory;
+import java.io.IOException;
 import java.util.Arrays;
 import javax.annotation.PostConstruct;
 import org.slf4j.Logger;
@@ -14,22 +16,16 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 /**
  *
- * @author bourgesl
  */
 @Configuration
 @ConfigurationProperties(prefix = "osug.doi")
-@EnableTransactionManagement
 public class DOIConfig {
 
     private final static Logger logger = LoggerFactory.getLogger(DOIConfig.class.getName());
 
-    /* debug flag */
-    private boolean debug = false;
-
     @Autowired
     private ApplicationContext appCtx;
 
@@ -40,20 +36,43 @@ public class DOIConfig {
 
     @Autowired
     private DoiService doiService;
-    
+
+    @Autowired
+    private DataciteConfig dataciteConfig;
+
+    /** path config */
+    private PathConfig pathConfig;
+
+    /** debug flag */
+    private boolean debug = false;
+
     /** DOI Prefix (registered in datacite) */
     private String prefix;
 
     /** DOI Domain (registered in datacite) */
     private String domain;
 
+    /** enable/disable the Datacite API client (REST) */
+    private boolean dataciteClientEnabled;
+
     @PostConstruct
     public void initialize() {
+        try {
+            this.pathConfig = new PathConfig(Paths.DIR_WEB);
+        } catch (IOException ioe) {
+            throw new IllegalStateException("Invalid paths: ", ioe);
+        }
+
         this.debug = Arrays.asList(appCtx.getEnvironment().getActiveProfiles()).contains("debug");
 
         logger.info("debug mode: {}", debug);
         logger.info("doi prefix: {}", prefix);
         logger.info("doi domain: {}", domain);
+        logger.info("Enable Datacite Client: {}", isDataciteClientEnabled());
+    }
+
+    public PathConfig getPathConfig() {
+        return pathConfig;
     }
 
     public boolean isDebug() {
@@ -76,6 +95,10 @@ public class DOIConfig {
         return doiService;
     }
 
+    public DataciteClient getDataciteClient() {
+        return dataciteConfig.getClient();
+    }
+
     public String getPrefix() {
         return prefix;
     }
@@ -92,4 +115,12 @@ public class DOIConfig {
         this.domain = domain;
     }
 
+    public boolean isDataciteClientEnabled() {
+        return dataciteClientEnabled;
+    }
+
+    public void setDataciteClientEnabled(final boolean dataciteClientEnabled) {
+        this.dataciteClientEnabled = dataciteClientEnabled;
+    }
+
 }
diff --git a/src/main/java/fr/osug/doi/DataciteConfig.java b/src/main/java/fr/osug/doi/DataciteConfig.java
new file mode 100644
index 0000000..383c0c0
--- /dev/null
+++ b/src/main/java/fr/osug/doi/DataciteConfig.java
@@ -0,0 +1,60 @@
+/*******************************************************************************
+ * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
+ ******************************************************************************/
+package fr.osug.doi;
+
+import fr.osug.doi.service.DataciteClient;
+import javax.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ *
+ */
+@Configuration
+@ConfigurationProperties(prefix = "datacite")
+public class DataciteConfig {
+    
+    private final static Logger logger = LoggerFactory.getLogger(DataciteConfig.class.getName());
+
+    /** User account (registered in datacite) */
+    private String user;
+
+    /** User password (registered in datacite) */
+    private String password;
+    
+    @Autowired
+    private RestTemplateBuilder restTemplateBuilder;
+    
+    private DataciteClient dcClient;
+    
+    @PostConstruct
+    public void initialize() {
+        logger.info("Datacite user: [{}]", user);
+        if (user != null) {
+            restTemplateBuilder = restTemplateBuilder.basicAuthorization(user, password);
+        }
+        dcClient = new DataciteClient(restTemplateBuilder);
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public DataciteClient getClient() {
+        return dcClient;
+    }
+    
+}
diff --git a/src/main/java/fr/osug/doi/DoiCsvData.java b/src/main/java/fr/osug/doi/DoiCsvData.java
index 60a818d..2284a21 100644
--- a/src/main/java/fr/osug/doi/DoiCsvData.java
+++ b/src/main/java/fr/osug/doi/DoiCsvData.java
@@ -16,7 +16,6 @@ import java.util.Set;
 
 /**
  *
- * @author bourgesl
  */
 public final class DoiCsvData extends CsvData {
 
diff --git a/src/main/java/fr/osug/doi/DoiCsvSplitter.java b/src/main/java/fr/osug/doi/DoiCsvSplitter.java
index d60796f..7e416ae 100644
--- a/src/main/java/fr/osug/doi/DoiCsvSplitter.java
+++ b/src/main/java/fr/osug/doi/DoiCsvSplitter.java
@@ -10,7 +10,6 @@ import java.util.List;
 
 /**
  *
- * @author bourgesl
  */
 public class DoiCsvSplitter {
 
diff --git a/src/main/java/fr/osug/doi/DoiCsvToXml.java b/src/main/java/fr/osug/doi/DoiCsvToXml.java
index ab9c6b4..7d7d2e4 100644
--- a/src/main/java/fr/osug/doi/DoiCsvToXml.java
+++ b/src/main/java/fr/osug/doi/DoiCsvToXml.java
@@ -3,9 +3,6 @@
  ******************************************************************************/
 package fr.osug.doi;
 
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
 import org.slf4j.Logger;
@@ -13,7 +10,6 @@ import org.slf4j.LoggerFactory;
 
 /**
  *
- * @author bourgesl
  */
 public class DoiCsvToXml {
 
@@ -85,20 +81,6 @@ public class DoiCsvToXml {
         }
     }
 
-    public static void writeXml(final String content, final File xmlFile) throws IOException {
-        logger.info("Writing: {}", xmlFile.getAbsolutePath());
-
-        BufferedWriter writer = null;
-        try {
-            writer = new BufferedWriter(CsvUtil.getTextWriter(xmlFile), content.length());
-            writer.write(content);
-        } finally {
-            if (writer != null) {
-                writer.close();
-            }
-        }
-    }
-
     public static void xmlEscapeText(final String value, final StringBuilder sb) {
         if (value != null) {
             for (int i = 0, len = value.length(); i < len; i++) {
diff --git a/src/main/java/fr/osug/doi/DoiTemplates.java b/src/main/java/fr/osug/doi/DoiTemplates.java
index 04d9740..7d91818 100644
--- a/src/main/java/fr/osug/doi/DoiTemplates.java
+++ b/src/main/java/fr/osug/doi/DoiTemplates.java
@@ -12,7 +12,6 @@ import org.slf4j.LoggerFactory;
 
 /**
  *
- * @author bourgesl
  */
 public final class DoiTemplates {
 
diff --git a/src/main/java/fr/osug/doi/GeneratePipeline.java b/src/main/java/fr/osug/doi/GeneratePipeline.java
index bf3fbb1..601fe15 100644
--- a/src/main/java/fr/osug/doi/GeneratePipeline.java
+++ b/src/main/java/fr/osug/doi/GeneratePipeline.java
@@ -7,51 +7,37 @@ import fr.osug.doi.domain.Doi;
 import fr.osug.doi.domain.DoiStaging;
 import fr.osug.doi.domain.Project;
 import fr.osug.doi.domain.IndexEntry;
-import fr.osug.doi.domain.NameEntry;
 import fr.osug.doi.repository.DoiStagingRepository;
 import fr.osug.doi.repository.ProjectRepository;
 import fr.osug.doi.service.DoiService;
-import fr.osug.doi.validation.ValidationUtil;
 import fr.osug.util.FileUtils;
-import fr.osug.xml.validator.ErrorMessage;
-import fr.osug.xml.validator.ValidationResult;
-import fr.osug.xml.validator.XmlValidator;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.HashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
 import javax.xml.transform.stream.StreamResult;
 import javax.xml.transform.stream.StreamSource;
-import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.transaction.annotation.Transactional;
+import fr.osug.doi.domain.DoiCommon;
+import fr.osug.doi.repository.DoiBaseRepository;
+import java.util.Arrays;
 
 /**
  *
- * @author bourgesl
  */
-public final class ProcessPipeline {
+public class GeneratePipeline {
 
-    private final static Logger logger = LoggerFactory.getLogger(ProcessPipeline.class.getName());
+    private final static Logger logger = LoggerFactory.getLogger(GeneratePipeline.class.getName());
 
     public final static String HTML_EXT = ".html";
     public final static String HTML_INDEX = "index" + HTML_EXT;
     public final static String HTML_REPORT = "report" + HTML_EXT;
     public final static String HTML_ERROR = "error" + HTML_EXT;
 
-    public final static String XSD_DOI_SCHEMA = "file://" + Paths.DIR_XSD_SCHEMA + "/metadata.xsd";
-
-    private final static String XSLT_XML_TO_DOI = "xml2doi.xsl";
     private final static String XSLT_DOI_TO_LANDING_PAGE = "doi2landing.xsl";
 
     private final static String XSLT_PROJECT_INDEX = "project_index.xsl";
@@ -78,54 +64,93 @@ public final class ProcessPipeline {
     private final static String XSLT_PARAM_CURRENT = "current";
 
     // members
-    private Date _now = null;
     private final DOIConfig doiConfig;
-    private final ProjectConfig projectConfig;
-    // temporary data
-    private final PipelineGlobalData globalData = new PipelineGlobalData();
+    private final boolean doStaging;
+    private final boolean doPublic;
+    // temporary data (process pipeline)
+    private final PipelineCommonData globalData;
+    /** child pipeline */
+    private final GenerateRedirectPipeline pipe;
+
+    public GeneratePipeline(final DOIConfig doiConfig,
+                            final boolean doStaging,
+                            final boolean doPublic) {
+        this(doiConfig, doStaging, doPublic, null);
+    }
 
-    public ProcessPipeline(final DOIConfig doiConfig,
-                           final ProjectConfig projectConfig) throws IOException {
+    GeneratePipeline(final DOIConfig doiConfig,
+                     final boolean doStaging,
+                     final boolean doPublic,
+                     final PipelineCommonData globalData) {
 
         this.doiConfig = doiConfig;
-        this.projectConfig = projectConfig;
+        this.doStaging = doStaging;
+        this.doPublic = doPublic;
+        this.globalData = (globalData != null) ? globalData : new PipelineCommonData();
+        // child pipeline:
+        this.pipe = new GenerateRedirectPipeline(doiConfig);
     }
 
-    public void process() throws IOException {
+    public void process(final String project) throws IOException {
+        logger.info("doStaging: {}", doStaging);
+        logger.info("doPublic:  {}", doPublic);
 
-        // 1 - Process complete datasets (csv):
-        for (File inputCsvFile : projectConfig.getMetadataCsvFiles()) {
-            processCsvFile(inputCsvFile, false);
-        }
+        final DoiService doiService = doiConfig.getDoiService();
+        final ProjectRepository pr = doiService.getProjectRepository();
 
-        // 2 - Process partial datasets (csv to be merged with templates):
-        for (File inputCsvFile : projectConfig.getInputCsvFiles()) {
-            processCsvFile(inputCsvFile, true);
+        // all or project by project:
+        final List<String> projectNames;
+        if (project == null) {
+            projectNames = pr.findAllNamesOrderByNameAsc();
+        } else {
+            projectNames = Arrays.asList(new String[]{project});
         }
 
-        // TODO: 3 - process complete datasets as XML files (full metadata)
-        // global validation of references
-        validateRefs();
+        logger.info("projectNames: {}", projectNames);
 
-        // 4 - Landing pages
-        generateLandingPages();
+        // 1 - Landing pages for selected project(s):
+        for (String projectName : projectNames) {
+            // get existing project:
+            final Project p = pr.findOneByName(projectName);
 
-        // TODO: test publication datacite ?
-        // Update database:
-        updateDatabase();
+            if (p == null) {
+                logger.warn("Unknown Project [{}]", projectName);
+            } else {
+                final ProjectConfig projectConfig = new ProjectConfig(doiConfig.getPathConfig(), p.getName());
+                generateLandingPages(projectConfig);
+            }
+        }
 
-        // Update index pages:
-        generateIndexPagesStaging();
+        // 2 - Update index pages:
+        generateIndexPages();
     }
 
-    private void generateLandingPages() throws IOException {
-
-        final File webStaging = projectConfig.getWebStagingProjectDir();
-        logger.info("Generating landing pages into {}", webStaging);
+    public void generateLandingPages(final ProjectConfig projectConfig) throws IOException {
+        if (doStaging) {
+            generateLandingPages(projectConfig, true);
+        }
+        if (doPublic) {
+            generateLandingPages(projectConfig, false);
+        }
+    }
 
-        final File webStagingEmbed = projectConfig.getWebStagingProjectEmbedDir();
-        final File webStagingXml = projectConfig.getWebStagingProjectXmlDir();
+    private void generateLandingPages(final ProjectConfig projectConfig,
+                                      final boolean isStaging) throws IOException {
+        final File webDir;
+        final File webEmbedDir;
+        final File webXmlDir;
+        if (isStaging) {
+            webDir = projectConfig.getWebStagingProjectDir();
+            webEmbedDir = projectConfig.getWebStagingProjectEmbedDir();
+            webXmlDir = projectConfig.getWebStagingProjectXmlDir();
+        } else {
+            webDir = projectConfig.getWebPublicProjectDir();
+            webEmbedDir = projectConfig.getWebPublicProjectEmbedDir();
+            webXmlDir = projectConfig.getWebPublicProjectXmlDir();
+        }
+        logger.info("Generating landing pages into {}", webDir);
 
+        // Note: these fields should be in project (db) to avoid inconsistency accross staging then public runs:
         final File dataAccessFileLocation = new File(projectConfig.getProjectConf(), CONFIG_ACCESS_INSTRUCTIONS);
         final File dataAccessFile = FileUtils.getFile(dataAccessFileLocation);
         if (dataAccessFile == null) {
@@ -134,127 +159,198 @@ public final class ProcessPipeline {
 
         // TODO: use mapping file [DOI:dataAccessUrl] like url mapping:
         final String defaultDataAccessUrl = projectConfig.getProperty(ProjectConfig.CONF_KEY_DATA_ACCESS_URL);
+        if (defaultDataAccessUrl == null) {
+            logger.warn("Missing default data access url");
+        }
 
         final Map<String, Object> xslParameters = new HashMap<String, Object>(8);
-        xslParameters.put(XSLT_PARAM_DATE, now().toString());
+        xslParameters.put(XSLT_PARAM_DATE, globalData.now.toString());
         if (dataAccessFile != null) {
             xslParameters.put(XSLT_PARAM_DATA_ACCESS_HTML, dataAccessFile.toURI().toString());
         }
-        if (defaultDataAccessUrl != null) {
-            xslParameters.put(XSLT_PARAM_DATA_ACCESS_URL, defaultDataAccessUrl);
-        }
 
-        for (PipelineDoiData doiData : globalData.getDoiDatas().values()) {
-            final String doiSuffix = doiData.getDoiSuffix();
-            logger.info("processing {}", doiSuffix);
+        if (globalData instanceof ProcessPipelineData) {
+            for (ProcessPipelineDoiData doiData : ((ProcessPipelineData) globalData).getDoiDatas().values()) {
+                final String doiSuffix = doiData.getDoiSuffix();
+                logger.info("processing {}", doiSuffix);
 
-            // Get external Landing page URL:
-            final String landingPageUrl = projectConfig.getLandingPageUrl(doiSuffix);
-            if (landingPageUrl != null) {
-                logger.info("landingPageUrl: {}", landingPageUrl);
-                xslParameters.put(XSLT_PARAM_LANDING_PAGE_URL, landingPageUrl);
+                // Get specific data access URL:
+                String dataAccessUrl = projectConfig.getUrlDataAccess(doiSuffix);
+                if (dataAccessUrl == null) {
+                    dataAccessUrl = defaultDataAccessUrl;
+                }
+                if (dataAccessUrl != null) {
+                    logger.debug("dataAccessUrl: {}", dataAccessUrl);
+                    doiData.setDataAccessUrl(dataAccessUrl);
+                    xslParameters.put(XSLT_PARAM_DATA_ACCESS_URL, dataAccessUrl);
+                } else {
+                    xslParameters.remove(XSLT_PARAM_DATA_ACCESS_URL);
+                }
 
-                doiData.setLandingPageUrl(landingPageUrl);
-            } else {
-                xslParameters.remove(XSLT_PARAM_LANDING_PAGE_URL);
-            }
+                // Get external Landing page URL:
+                final String landingPageExtUrl = projectConfig.getUrlLandingPage(doiSuffix);
+                if (landingPageExtUrl != null) {
+                    logger.debug("landingPageUrl: {}", landingPageExtUrl);
+                    doiData.setLandingPageExtUrl(landingPageExtUrl);
+                    xslParameters.put(XSLT_PARAM_LANDING_PAGE_URL, landingPageExtUrl);
+                } else {
+                    xslParameters.remove(XSLT_PARAM_LANDING_PAGE_URL);
+                }
 
-            // add warnings:
-            if (dataAccessFile == null) {
-                doiData.addError("Missing data access instructions (" + CONFIG_ACCESS_INSTRUCTIONS + ")");
-            }
-            if (StringUtils.isEmpty(defaultDataAccessUrl)) {
-                doiData.addError("Missing data access url");
+                // add warnings:
+                if (dataAccessFile == null) {
+                    doiData.addError("Missing data access instructions (" + CONFIG_ACCESS_INSTRUCTIONS + ")");
+                }
+                if (dataAccessUrl == null) {
+                    doiData.addError("Missing data access url");
+                }
+
+                final File stagingFileDOI = doiData.getStagingFileDOI();
+
+                if (stagingFileDOI == null) {
+                    logger.info("Ignoring {} (no xml)", doiSuffix);
+                } else {
+                    final String landingPageFilePath = generateLandingPages(projectConfig, stagingFileDOI, xslParameters, webDir, webEmbedDir, webXmlDir);
+
+                    // Update landing local url (staging):
+                    doiData.setLandingLocUrl(extractUrlPath(landingPageFilePath));
+                }
             }
+        } else {
+            // generate from DB:
 
-            final File stagingFileDOI = doiData.getStagingFileDOI();
+            // Get Public|Staging directory:
+            final File doiDir = (isStaging) ? projectConfig.getStagingDir() : projectConfig.getPublicDir();
 
-            if (stagingFileDOI == null) {
-                logger.info("Ignoring {} (no xml)", doiSuffix);
-            } else {
-                final String filenameNoExt = FileUtils.getFileNameWithoutExtension(stagingFileDOI);
+            final String projectName = projectConfig.getProjectName();
 
-                // main landing page:
-                xslParameters.remove(XSLT_PARAM_EMBEDDED);
-                xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../");
-                File output = new File(webStaging, filenameNoExt + HTML_EXT);
-                generateLandingPage(xslParameters, stagingFileDOI, output);
-
-                if (projectConfig.getPropertyBoolean(ProjectConfig.CONF_KEY_GENERATE_EMBEDDED)) {
-                    // embedded mode:
-                    xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../../");
-
-                    // embedded mode - full:
-                    output = new File(webStagingEmbed, filenameNoExt + HTML_EXT);
-                    xslParameters.put(XSLT_PARAM_EMBEDDED, XSLT_PARAM_EMBEDDED_FULL);
-                    generateLandingPage(xslParameters, stagingFileDOI, output);
-
-                    // embedded mode - header:
-                    output = new File(webStagingEmbed, filenameNoExt + "-header" + HTML_EXT);
-                    xslParameters.put(XSLT_PARAM_EMBEDDED, XSLT_PARAM_EMBEDDED_HEADER);
-                    generateLandingPage(xslParameters, stagingFileDOI, output);
-
-                    // embedded mode - meta:
-                    output = new File(webStagingEmbed, filenameNoExt + "-meta" + HTML_EXT);
-                    xslParameters.put(XSLT_PARAM_EMBEDDED, XSLT_PARAM_EMBEDDED_META);
-                    generateLandingPage(xslParameters, stagingFileDOI, output);
+            final DoiService doiService = doiConfig.getDoiService();
+            final DoiBaseRepository<?> dbr = doiService.getDoiCommonRepository(isStaging);
+
+            final List<? extends DoiCommon> dList = dbr.findByProject(projectName);
+
+            logger.debug("DoiCommon list for project[{}]: {}", projectName, dList);
+
+            for (DoiCommon d : dList) {
+                logger.debug("DoiCommon: {}", d);
+                final Doi doi = d.getDoi();
+
+                final String doiSuffix = doi.getIdentifier();
+                logger.info("processing {}", doiSuffix);
+
+                // Get specific data access URL:
+                final String dataAccessUrl = doi.getDataAccessUrl();
+                if (dataAccessUrl != null) {
+                    logger.debug("dataAccessUrl: {}", dataAccessUrl);
+                    xslParameters.put(XSLT_PARAM_DATA_ACCESS_URL, dataAccessUrl);
+                } else {
+                    xslParameters.remove(XSLT_PARAM_DATA_ACCESS_URL);
+                }
+
+                // Get external Landing page URL:
+                final String landingPageUrl = doi.getLandingExtUrl();
+                if (landingPageUrl != null) {
+                    logger.debug("landingPageUrl: {}", landingPageUrl);
+                    xslParameters.put(XSLT_PARAM_LANDING_PAGE_URL, landingPageUrl);
+                } else {
+                    xslParameters.remove(XSLT_PARAM_LANDING_PAGE_URL);
                 }
 
-                // Xml DOI:
-                output = new File(webStagingXml, stagingFileDOI.getName());
-                FileUtils.copy(stagingFileDOI, output);
+                final File stagingFileDOI = new File(doiDir, doiSuffix + Const.FILE_EXT_XML);
+
+                if (!stagingFileDOI.exists()) {
+                    logger.warn("Missing {} (no xml)", stagingFileDOI);
+                } else {
+                    generateLandingPages(projectConfig, stagingFileDOI, xslParameters, webDir, webEmbedDir, webXmlDir);
+                }
             }
         }
+        logger.info("Generating landing pages: done");
     }
 
-    private void generateLandingPage(final Map<String, Object> xslParameters,
-                                     final File stagingFileDOI, final File output) {
-
-        logger.info("generateLandingPage: {}", output);
+    private String generateLandingPages(final ProjectConfig projectConfig,
+                                        final File xmlFileDOI,
+                                        final Map<String, Object> xslParameters,
+                                        final File webDir,
+                                        final File webEmbedDir,
+                                        final File webXmlDir) throws IOException {
+
+        final String filenameNoExt = FileUtils.getFileNameWithoutExtension(xmlFileDOI);
+
+        // main landing page:
+        // [public/staging]/PROJECT/DOI_SUFFIX.html
+        xslParameters.remove(XSLT_PARAM_EMBEDDED);
+        xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../");
+        File outputFile = new File(webDir, filenameNoExt + HTML_EXT);
+        generateLandingPage(xslParameters, xmlFileDOI, outputFile);
+
+        // Store reference of the main landing page:
+        final String landingPageFilePath = outputFile.getAbsolutePath();
+
+        if (projectConfig.getPropertyBoolean(ProjectConfig.CONF_KEY_GENERATE_EMBEDDED)) {
+            // embedded mode:
+            xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../../");
+
+            // embedded mode - full:
+            // [public/staging]/PROJECT/embed/DOI_SUFFIX[.html]
+            outputFile = new File(webEmbedDir, filenameNoExt + HTML_EXT);
+            xslParameters.put(XSLT_PARAM_EMBEDDED, XSLT_PARAM_EMBEDDED_FULL);
+            generateLandingPage(xslParameters, xmlFileDOI, outputFile);
+
+            // embedded mode - header:
+            outputFile = new File(webEmbedDir, filenameNoExt + "-header" + HTML_EXT);
+            xslParameters.put(XSLT_PARAM_EMBEDDED, XSLT_PARAM_EMBEDDED_HEADER);
+            generateLandingPage(xslParameters, xmlFileDOI, outputFile);
+
+            // embedded mode - meta:
+            outputFile = new File(webEmbedDir, filenameNoExt + "-meta" + HTML_EXT);
+            xslParameters.put(XSLT_PARAM_EMBEDDED, XSLT_PARAM_EMBEDDED_META);
+            generateLandingPage(xslParameters, xmlFileDOI, outputFile);
+        }
 
-        xslParameters.put(XSLT_PARAM_METADATA_FILE, stagingFileDOI.getName());
-        logger.debug("xslParameters: {}", xslParameters);
+        // Xml DOI:
+        outputFile = new File(webXmlDir, xmlFileDOI.getName());
+        FileUtils.copy(xmlFileDOI, outputFile);
 
-        // use XSLT to generate landing page from the doi xml file:
-        doiConfig.getXmlFactory().transform(
-                new StreamSource(stagingFileDOI),
-                Paths.DIR_XSL + XSLT_DOI_TO_LANDING_PAGE,
-                xslParameters, true,
-                new StreamResult(output)
-        );
+        return landingPageFilePath;
     }
 
-    @Transactional
-    /* public needed by Transactional */
-    public void updateDatabase() {
-        final String projectName = projectConfig.getProjectName();
+    private void generateLandingPage(final Map<String, Object> xslParameters,
+                                     final File inputFileDOI, final File outputFile) {
+
+        xslParameters.put(XSLT_PARAM_METADATA_FILE, inputFileDOI.getName());
 
-        final Date now = now();
+        // use XSLT to generate landing page from the doi xml file:
+        transform(inputFileDOI, Paths.DIR_XSL + XSLT_DOI_TO_LANDING_PAGE, xslParameters, outputFile);
+    }
 
-        // Get or create the project:
-        final DoiService doiService = doiConfig.getDoiService();
-        final Project project = doiService.getOrCreateProject(projectName, now);
-
-        // Update Dois:
-        for (PipelineDoiData doiData : globalData.getDoiDatas().values()) {
-            // Update DOI in staging phase:
-            doiService.createOrUpdateStagingDoi(project, doiData.getDoiSuffix(),
-                    doiData.getTitle(), doiData.getLandingPageUrl(),
-                    doiData.isValid(), doiData.getMetadataMd5(),
-                    doiData.messagesToString(), now);
+    public void generateIndexPages() throws IOException {
+        if (doStaging) {
+            generateIndexPages(true);
+        }
+        if (doPublic) {
+            generateIndexPages(false);
         }
 
-        // Update Project:
-        doiService.updateProjectDate(projectName, now);
+        // generate .htaccess (for apache2)
+        pipe.generateRedirects();
     }
 
-    @Transactional
-    /* public needed by Transactional */
-    public void generateIndexPagesStaging() {
-        final File webStaging = projectConfig.getWebStagingDir();
+    private void generateIndexPages(final boolean isStaging) {
+        final String folderType;
+        final File webDir;
+        if (isStaging) {
+            folderType = Paths.DIR_WEB_STAGING;
+            webDir = doiConfig.getPathConfig().getWebStagingDir();
+        } else {
+            folderType = Paths.DIR_WEB_PUBLIC;
+            webDir = doiConfig.getPathConfig().getWebPublicDir();
+        }
+        logger.info("Generating index pages into {}", webDir);
 
         final DoiService doiService = doiConfig.getDoiService();
         final ProjectRepository pr = doiService.getProjectRepository();
+        final DoiBaseRepository<?> dbr = doiService.getDoiCommonRepository(isStaging);
         final DoiStagingRepository dsr = doiService.getDoiStagingRepository();
 
         final List<Project> projects = pr.findAllByOrderByNameAsc();
@@ -262,430 +358,136 @@ public final class ProcessPipeline {
         // Project listing:
         List<IndexEntry> entries = new ArrayList<IndexEntry>();
 
+        // index.html:
         for (Project p : projects) {
             final String projectName = p.getName();
-            final Long countDoi = dsr.countByProject(projectName);
-            final IndexEntry ie = new IndexEntry(projectName, p.getDescription(), p.getUpdateDate(), countDoi);
+            final Long countDoi = dbr.countByProject(projectName);
 
-            logger.debug("IndexEntry: {}", ie);
-            entries.add(ie);
+            if (countDoi > 0L) {
+                final IndexEntry ie = new IndexEntry(projectName, p.getDescription(), p.getUpdateDate(), countDoi);
+
+                logger.debug("IndexEntry: {}", ie);
+                entries.add(ie);
+            }
         }
-        // serialize entries and transform in the /staging/index.html
+        // serialize entries and transform in the /[staging|public]/index.html
         IndexUtil.write(entries, globalData.buffer);
 
         String xmlDoc = globalData.buffer.toString();
         logger.debug("xmlDoc(project index): \n{}", xmlDoc);
 
         final Map<String, Object> xslParameters = new HashMap<String, Object>(8);
-        xslParameters.put(XSLT_PARAM_DATE, now().toString());
-        xslParameters.put(XSLT_PARAM_CURRENT, "staging");
+        xslParameters.put(XSLT_PARAM_DATE, globalData.now.toString());
+        xslParameters.put(XSLT_PARAM_CURRENT, folderType);
         xslParameters.put(XSLT_PARAM_WEB_ROOT, "../");
-        File output = new File(webStaging, HTML_INDEX);
+        File outputFile = new File(webDir, HTML_INDEX);
 
         // use XSLT to generate the index page:
-        doiConfig.getXmlFactory().transform(
-                new StreamSource(new StringReader(xmlDoc)),
-                Paths.DIR_XSL_ROOT + XSLT_PROJECT_INDEX,
-                xslParameters, true,
-                new StreamResult(output)
-        );
+        transform(xmlDoc, Paths.DIR_XSL_ROOT + XSLT_PROJECT_INDEX, xslParameters, outputFile);
 
         // DOI listing per project:
         for (Project p : projects) {
             final String projectName = p.getName();
-            final List<DoiStaging> dsList = dsr.findByProject(projectName);
-
-            logger.debug("DoiStaging list for project[{}]: {}", projectName, dsList);
 
-            // index.html:
+            // PROJECT/index.html:
             entries.clear();
 
-            for (DoiStaging ds : dsList) {
-                logger.debug("DoiStaging: {}", ds);
+            final List<? extends DoiCommon> dList = dbr.findByProject(projectName);
 
-                final Doi doi = ds.getDoi();
+            logger.debug("DoiCommon list for project[{}]: {}", projectName, dList);
 
-                final IndexEntry ie = new IndexEntry(doi.getIdentifier(), doi.getDescription(), ds.getUpdateDate());
+            if (!dList.isEmpty()) {
+                for (DoiCommon d : dList) {
+                    logger.debug("DoiBase: {}", d);
+                    final Doi doi = d.getDoi();
+                    final IndexEntry ie = new IndexEntry(doi.getIdentifier(), doi.getDescription(), d.getUpdateDate());
 
-                logger.debug("IndexEntry: {}", ie);
-                entries.add(ie);
-            }
-            // serialize entries and transform in the /staging/PROJECT/index.html
-            IndexUtil.write(entries, globalData.buffer);
-
-            xmlDoc = globalData.buffer.toString();
-            logger.debug("xmlDoc(doi index): \n{}", xmlDoc);
-
-            xslParameters.put(XSLT_PARAM_PARENT, "staging");
-            xslParameters.put(XSLT_PARAM_CURRENT, projectName);
-            xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../");
-            // suppose child directory is the project folder:
-            output = new File(new File(webStaging, projectName), HTML_INDEX);
-
-            // use XSLT to generate the index page:
-            doiConfig.getXmlFactory().transform(
-                    new StreamSource(new StringReader(xmlDoc)),
-                    Paths.DIR_XSL_ROOT + XSLT_PROJECT_INDEX,
-                    xslParameters, true,
-                    new StreamResult(output)
-            );
-
-            // serialize doi staging list and transform in the /staging/PROJECT/report.html
-            IndexUtil.writeReport(dsList, globalData.buffer);
-
-            xmlDoc = globalData.buffer.toString();
-            logger.debug("xmlDoc(doi report): \n{}", xmlDoc);
-
-            xslParameters.put(XSLT_PARAM_PARENT, "staging");
-            xslParameters.put(XSLT_PARAM_CURRENT, projectName);
-            xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../");
-            // suppose child directory is the project folder:
-            output = new File(new File(webStaging, projectName), HTML_REPORT);
-
-            // use XSLT to generate the report page:
-            doiConfig.getXmlFactory().transform(
-                    new StreamSource(new StringReader(xmlDoc)),
-                    Paths.DIR_XSL_ROOT + XSLT_PROJECT_REPORT,
-                    xslParameters, true,
-                    new StreamResult(output)
-            );
-
-            // serialize doi staging error list and transform in the /staging/PROJECT/error.html
-            final List<DoiStaging> dsErrorList = dsr.findErrorByProject(projectName);
-            logger.debug("DoiStaging error list for project[{}]: {}", projectName, dsErrorList);
-
-            IndexUtil.writeReport(dsErrorList, globalData.buffer);
-
-            xmlDoc = globalData.buffer.toString();
-            logger.debug("xmlDoc(error report): \n{}", xmlDoc);
-
-            xslParameters.put(XSLT_PARAM_PARENT, "staging");
-            xslParameters.put(XSLT_PARAM_CURRENT, projectName);
-            xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../");
-            // suppose child directory is the project folder:
-            output = new File(new File(webStaging, projectName), HTML_ERROR);
-
-            // use XSLT to generate the report page:
-            doiConfig.getXmlFactory().transform(
-                    new StreamSource(new StringReader(xmlDoc)),
-                    Paths.DIR_XSL_ROOT + XSLT_PROJECT_REPORT,
-                    xslParameters, true,
-                    new StreamResult(output)
-            );
-        }
-        logger.info("generateIndexPagesStaging: done.");
-    }
+                    logger.debug("IndexEntry: {}", ie);
+                    entries.add(ie);
+                }
+                // serialize entries and transform in the /[staging|public]/PROJECT/index.html
+                IndexUtil.write(entries, globalData.buffer);
 
-    private void validateRefs() throws IOException {
-        logger.debug("validateRefs");
+                xmlDoc = globalData.buffer.toString();
+                logger.debug("xmlDoc(doi index): \n{}", xmlDoc);
 
-        final DoiService doiService = doiConfig.getDoiService();
-        final String publicPrefix = doiConfig.getPrefix();
-
-        for (String doiId : globalData.getDoiIds()) {
-            logger.debug("checking refs for [{}]", doiId);
-
-            final Set<String> refs = globalData.getDoiRefs(doiId);
-            if (refs != null) {
-                for (String refId : refs) {
-                    // Only check DOI references with Prefix Test or Public:
-                    if (ValidationUtil.isTestPrefix(refId) || refId.startsWith(publicPrefix)) {
-                        logger.debug("validateRefs: check ref[{}]", refId);
-
-                        boolean found = false;
-
-                        // Lookup first in processed identifiers:
-                        if (globalData.hasDoiId(refId)) {
-                            found = true;
-                        } else {
-                            // Then in database (previous data):
-                            final String doiSuffix = DoiCsvData.getDoiSuffix(refId);
-                            if (doiService.getDoi(doiSuffix) != null) {
-                                found = true;
-                            }
-                        }
-                        if (!found) {
-                            final PipelineDoiData doiData = globalData.getDoiData(doiId);
-                            if (doiData != null) {
-                                doiData.addError("Invalid " + Const.KEY_REL_ID_START + " '" + refId + "' found.");
-                            }
-                        }
-                    }
-                }
-            }
-        }
+                // inherit XSLT_PARAM_DATE:
+                xslParameters.put(XSLT_PARAM_PARENT, folderType);
+                xslParameters.put(XSLT_PARAM_CURRENT, projectName);
+                xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../");
 
-        // Check name references:
-        final TreeSet<NameEntry> entries = globalData.getNameRefs();
-        if (logger.isDebugEnabled()) {
-            logger.debug("name references:\n{}", entries);
-        }
+                // suppose child directory is the project folder:
+                final File webProjectDir = new File(webDir, projectName);
+                outputFile = new File(webProjectDir, HTML_INDEX);
 
-        // Collect unique attributes:
-        final Set<String> keys = new LinkedHashSet<String>();
-        for (NameEntry e : entries) {
-            if (e.hasAttributes()) {
-                for (String k : e.getAttributes().keySet()) {
-                    keys.add(k);
-                }
-            }
-        }
+                // use XSLT to generate the PROJECT index page:
+                transform(xmlDoc, Paths.DIR_XSL_ROOT + XSLT_PROJECT_INDEX, xslParameters, outputFile);
 
-        final int nCols = keys.size() + 1;
+                if (isStaging) {
+                    @SuppressWarnings("unchecked")
+                    final List<DoiStaging> dsList = (List<DoiStaging>) dList;
 
-        final List<String[]> rows = new ArrayList<String[]>();
-        // header:
-        String[] cols = new String[nCols];
-        int i = 0;
-        cols[i++] = "#Name";
-        for (String k : keys) {
-            cols[i++] = k;
-        }
-        rows.add(cols);
-        // data:
-        for (NameEntry e : entries) {
-            if (e.hasAttributes()) {
-                cols = new String[nCols];
-                i = 0;
-                cols[i++] = e.getName();
-                for (String k : keys) {
-                    cols[i++] = e.getAttribute(k);
-                }
-            } else {
-                cols = new String[]{e.getName()};
-            }
-            rows.add(cols);
-        }
-        final CsvData data = new CsvData(rows);
-        if (logger.isDebugEnabled()) {
-            logger.debug("name reference table:\n{}", data);
-        }
+                    // serialize doi staging list and transform in the /staging/PROJECT/report.html
+                    IndexUtil.writeReport(dsList, globalData.buffer);
 
-        // Save file as CSV:
-        final File fileCSV = new File(projectConfig.getTmpDir(), "names" + Const.FILE_EXT_CSV);
-        CsvUtil.write(data, fileCSV);
-    }
+                    xmlDoc = globalData.buffer.toString();
+                    logger.debug("xmlDoc(doi report): \n{}", xmlDoc);
 
-    private void processCsvFile(final File inputCsvFile,
-                                final boolean doMerge) throws IOException {
-        /*
-        Spliter et produire 1 fichier XML dataCite par jeu.
-        Ex: AMMA-CATCH.CL.Run_O.xml
-         */
-        final List<DoiCsvData> csvDatas = DoiCsvSplitter.split(inputCsvFile);
-
-        for (DoiCsvData data : csvDatas) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("process: data:\n{}", data);
-            }
-            if (!data.isEmpty()) {
-                final String doiId = data.getDoiId();
+                    // inherit XSLT_PARAM_DATE, XSLT_PARAM_PARENT, XSLT_PARAM_CURRENT, XSLT_PARAM_WEB_ROOT:
+                    // suppose child directory is the project folder:
+                    outputFile = new File(webProjectDir, HTML_REPORT);
 
-                // check identifier (missing) ?
-                if (ValidationUtil.checkIdentifier(doiId, data)) {
-                    final String doiSuffix = data.getDoiSuffix();
-                    logger.info("process: dataset[{}].", doiSuffix);
+                    // use XSLT to generate the PROJECT report page:
+                    transform(xmlDoc, Paths.DIR_XSL_ROOT + XSLT_PROJECT_REPORT, xslParameters, outputFile);
 
-                    final PipelineDoiData doiData = globalData.newDoiData(doiId, doiSuffix, data.getTitle());
+                    // serialize doi staging error list and transform in the /staging/PROJECT/error.html
+                    final List<DoiStaging> dsErrorList = dsr.findErrorByProject(projectName);
+                    logger.debug("DoiStaging error list for project[{}]: {}", projectName, dsErrorList);
 
-                    // ignore duplicates
-                    if (doiData != null) {
-                        // validate identifier (format)
-                        ValidationUtil.validateIdentifier(doiId, doiData);
+                    IndexUtil.writeReport(dsErrorList, globalData.buffer);
 
-                        if (doMerge) {
-                            mergeCSV(data, doiData);
-                        }
+                    xmlDoc = globalData.buffer.toString();
+                    logger.debug("xmlDoc(error report): \n{}", xmlDoc);
 
-                        // Validate metadata:
-                        validateData(data, doiData);
+                    // inherit XSLT_PARAM_DATE, XSLT_PARAM_PARENT, XSLT_PARAM_CURRENT, XSLT_PARAM_WEB_ROOT:
+                    // suppose child directory is the project folder:
+                    outputFile = new File(webProjectDir, HTML_ERROR);
 
-                        // Save metadata for dataset:
-                        saveData(data, doiData);
-                    }
+                    // use XSLT to generate the PROJECT error report page:
+                    transform(xmlDoc, Paths.DIR_XSL_ROOT + XSLT_PROJECT_REPORT, xslParameters, outputFile);
                 }
             }
         }
+        logger.info("Generating index pages: done");
     }
 
-    private void mergeCSV(final DoiCsvData data, final PipelineDoiData doiData) {
-
-        final DoiTemplates templates = projectConfig.getTemplates();
-
-        /*                
-        ### 2/ Fusionner avec template observatoire : template_obs.csv
-
-        Informations disjointes
-         */
-        DoiCsvData t = templates.getBase();
-        if (t == null) {
-            doiData.addError("Missing base template for project '" + projectConfig.getProjectName() + "'");
-        } else {
-            data.mergeFrom(t);
-        }
-
-        final String doiId = data.getDoiId(); // not null
-
-        /*
-        ### 3/ Traitement spécifiques: fusionner avec template pays
-
-        Fusionner avec template pays (template_benin.csv):
-
-        Trouver le template du pays qui contient geoLocationPlace;Benin
-         */
-        final String geoLocationPlace = data.getFirstGeoLocationPlace();
-        if (geoLocationPlace == null) {
-            doiData.addWarning("Missing ''" + Const.KEY_GEO_LOCATION_PLACE + "'' in dataset '" + doiId + "'");
-        } else {
-            t = templates.getByGeoLocationPlace(geoLocationPlace);
-            if (t == null) {
-                doiData.addError("Missing country template for '" + geoLocationPlace + "'");
-            } else {
-                logger.info("Using country template '{}'", geoLocationPlace);
-                data.mergeFrom(t);
-            }
-        }
-
-        /*
-        ### 4/ Traitement spécifiques: fusionner avec template jeu
-
-        Trouver le nom du jeu dans le XML:
-        <identifier identifierType="DOI">10.5072/AMMA-CATCH.CL.Run_O</identifier>
-
-        Trouver le template du jeu qui contient identifier:DOI;10.5072/AMMA-CATCH.CL.Run_O
-         */
-        t = templates.getByIdentifier(doiId);
-        if (t == null) {
-            doiData.addWarning("Missing dataset template for id '" + doiId + "'");
-        } else {
-            logger.info("Using dataset template for id '{}'", doiId);
-            data.mergeFrom(t);
-        }
+    private void transform(final File src, final String xslFilePath,
+                           final Map<String, Object> xslParameters, final File outputFile) {
+        transform(new StreamSource(src), xslFilePath, xslParameters, outputFile);
     }
 
-    private void validateData(final DoiCsvData data, final PipelineDoiData doiData) {
-        logger.debug("validateData [{}]", doiData.getDoiId());
-        // TODO: check title ?
-
-        // check all values:
-        final List<String[]> rows = data.getRows();
-        for (ListIterator<String[]> it = rows.listIterator(); it.hasNext();) {
-            final String[] cols = it.next();
-            if (cols != null && cols.length >= 2) {
-                if (cols[1].toLowerCase().contains("todo")) {
-                    doiData.addWarning("TODO detected for key [" + cols[0] + ']');
-                }
-
-                // Collect name references:
-                final String key = cols[0];
-                if (key.startsWith(Const.KEY_CREATOR_NAME) || key.startsWith(Const.KEY_CONTRIBUTOR_NAME)) {
-                    globalData.addNameRef(cols);
-                }
-            }
-        }
-
-        final Set<String> refs = data.getReferences();
-        if (refs != null) {
-            logger.debug("References: {}", refs);
-        }
-        // anyway: add the doi entry:
-        globalData.addRefs(doiData.getDoiId(), refs);
+    private void transform(final String src, final String xslFilePath,
+                           final Map<String, Object> xslParameters, final File outputFile) {
+        transform(new StreamSource(new StringReader(src)), xslFilePath, xslParameters, outputFile);
     }
 
-    private void saveData(final DoiCsvData data, final PipelineDoiData doiData) throws IOException {
-        // Sort keys:
-        data.sort();
-
-        // Get temporary directory:
-        final File tmpDoiDir = projectConfig.getTmpDoiDir();
-        // Get Staging directory:
-        final File stagingDir = projectConfig.getStagingDir();
-
-        // File base name:
-        final String baseName = data.getDoiSuffix();
-
-        // Save file as XML:
-        DoiCsvToXml.write(data, globalData.buffer);
-
-        final String xmlDoc = globalData.buffer.toString();
-        logger.debug("xmlDoc: \n{}", xmlDoc);
-
-        boolean forceSaveCSV = false;
-        boolean forceSaveXML = false;
-
-        // check non empty file:
-        if (xmlDoc.length() == 0) {
-            forceSaveCSV = true;
-            doiData.addError("No XML output");
-        } else {
-            globalData.doiDoc.reset();
-
-            // use XSLT to write doi xml file:
-            doiConfig.getXmlFactory().transform(
-                    new StreamSource(new StringReader(xmlDoc)),
-                    Paths.DIR_XSL + XSLT_XML_TO_DOI, true,
-                    new StreamResult(globalData.doiDoc)); // no xslt-param
-
-            final String doiDoc = globalData.doiDoc.toString();
-            logger.debug("doiDoc: \n{}", doiDoc);
-
-            if (doiDoc.length() == 0) {
-                forceSaveXML = true;
-                doiData.addError("No Metadata XML output");
-            } else {
-                final File fileDOI = new File(tmpDoiDir, baseName + Const.FILE_EXT_XML);
-                DoiCsvToXml.writeXml(doiDoc, fileDOI);
-
-                // compute MD5 on metadata:
-                final String xmlMd5 = FileUtils.MD5(new FileInputStream(fileDOI));
-                doiData.setMetadataMd5(xmlMd5);
-                logger.debug("DOI metadata [{}] MD5: {}", fileDOI, xmlMd5);
-
-                // Validate:
-                final XmlValidator validator = doiConfig.getXmlValidatorFactory().getInstance(XSD_DOI_SCHEMA);
-
-                final ValidationResult vr = new ValidationResult();
-                validator.validate(new StreamSource(fileDOI), vr);
-
-                logger.info("XSD validation [{}]: {}", fileDOI.getAbsolutePath(), vr.isValid());
-
-                if (!vr.isValid()) {
-                    doiData.addError("Metadata XML validation failed:");
-
-                    for (ErrorMessage msg : vr.getMessages()) {
-                        doiData.addError(msg.toString());
-                    }
-                }
-
-                // Anyway copy the doi metadata into staging:
-                final File stagingFileDOI = new File(stagingDir, fileDOI.getName());
-
-                logger.debug("Copy [{}] to [{}]", fileDOI, stagingFileDOI);
+    private void transform(final StreamSource src, final String xslFilePath,
+                           final Map<String, Object> xslParameters, final File outputFile) {
 
-                FileUtils.copy(fileDOI, stagingFileDOI);
+        logger.info("generate: {}", outputFile);
 
-                // to be processed at next step:
-                doiData.setStagingFileDOI(stagingFileDOI);
-            }
-        }
-
-        // Always save intermediate results in case of failure:
-        if (forceSaveCSV || projectConfig.isSaveCSV()) {
-            // Save file as CSV:
-            final File fileCSV = new File(tmpDoiDir, baseName + Const.FILE_EXT_CSV);
-            CsvUtil.write(data, fileCSV);
-        }
-
-        if (forceSaveXML || doiConfig.isDebug()) {
-            final File fileXML = new File(tmpDoiDir, baseName + "-xml" + Const.FILE_EXT_XML);
-            DoiCsvToXml.writeXml(xmlDoc, fileXML);
-        }
+        doiConfig.getXmlFactory().transform(src, xslFilePath,
+                xslParameters, true, new StreamResult(outputFile)
+        );
     }
 
-    private Date now() {
-        if (_now == null) {
-            _now = new Date();
+    private String extractUrlPath(final String landingPageFilePath) {
+        final String webRootPath = doiConfig.getPathConfig().getWebRootPath();
+        logger.debug("webRootPath: {}", webRootPath);
+
+        if (!landingPageFilePath.startsWith(webRootPath)) {
+            throw new IllegalStateException("Invalid web root path in file: " + landingPageFilePath);
         }
-        return _now;
+        return landingPageFilePath.substring(webRootPath.length() - 1);
     }
 }
diff --git a/src/main/java/fr/osug/doi/GenerateRedirectPipeline.java b/src/main/java/fr/osug/doi/GenerateRedirectPipeline.java
new file mode 100644
index 0000000..fd07ddf
--- /dev/null
+++ b/src/main/java/fr/osug/doi/GenerateRedirectPipeline.java
@@ -0,0 +1,208 @@
+/*******************************************************************************
+ * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
+ ******************************************************************************/
+package fr.osug.doi;
+
+import fr.osug.doi.domain.Doi;
+import fr.osug.doi.domain.DoiPublic;
+import fr.osug.doi.domain.DoiStaging;
+import fr.osug.doi.domain.Project;
+import fr.osug.doi.domain.Status;
+import fr.osug.doi.repository.DoiPublicRepository;
+import fr.osug.doi.repository.DoiRepository;
+import fr.osug.doi.repository.DoiStagingRepository;
+import fr.osug.doi.repository.ProjectRepository;
+import fr.osug.doi.service.DoiService;
+import fr.osug.util.FileUtils;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ */
+public class GenerateRedirectPipeline {
+
+    private final static Logger logger = LoggerFactory.getLogger(GenerateRedirectPipeline.class.getName());
+
+    public final static String FILE_HT_ACCESS = ".htaccess";
+
+    // members
+    private final DOIConfig doiConfig;
+    // temporary data (process pipeline)
+    private final PipelineCommonData globalData;
+
+    public GenerateRedirectPipeline(final DOIConfig doiConfig) {
+        this.doiConfig = doiConfig;
+        this.globalData = new PipelineCommonData();
+    }
+
+    public void process() throws IOException {
+        generateRedirects();
+    }
+
+    public void generateRedirects() throws IOException {
+        generateDOIUrls();
+        generateEmbedUrls();
+    }
+
+    private void generateDOIUrls() throws IOException {
+        final StringBuilder sb = prepareBuffer();
+
+        sb.append("\n# Redirect DOI to Landing page:");
+        sb.append("\n# [DOI_SUFFIX] => [landing page Ext]");
+        sb.append("\n#              or [/public/<PROJECT>/<DOI_SUFFIX>.html]     (public)");
+        sb.append("\n#              or [/staging/<PROJECT>/<DOI_SUFFIX>.html]    (staging)");
+
+        final DoiService doiService = doiConfig.getDoiService();
+        final ProjectRepository pr = doiService.getProjectRepository();
+        final DoiRepository dr = doiService.getDoiRepository();
+        final DoiPublicRepository dpr = doiService.getDoiPublicRepository();
+        final DoiStagingRepository dsr = doiService.getDoiStagingRepository();
+
+        final List<Project> projects = pr.findAllByOrderByNameAsc();
+
+        // DOI listing per project:
+        for (Project p : projects) {
+            final String projectName = p.getName();
+
+            sb.append("\n# Project ").append(projectName);
+
+            final List<Doi> dois = dr.findByProject(projectName);
+
+            logger.info("Dois for project[{}]: {}", projectName, dois.size());
+
+            for (Doi doi : dois) {
+                final String doiSuffix = doi.getIdentifier();
+
+                if (doi.getStatus() == Status.PUBLIC) {
+                    final DoiPublic dp = dpr.findOneByDoi(doi);
+
+                    if (dp == null) {
+                        logger.warn("Missing DoiPublic for [{}]; skipping Redirect", doiSuffix);
+                    } else {
+                        sb.append("\nRedirect \"/").append(doiSuffix).append("\" \"");
+
+                        if (dp.isActiveExtUrl() && doi.getLandingExtUrl() != null) {
+                            /*
+                            # [landing page Ext] case:
+                            # note: complete pattern match
+                            Redirect "/AMMA-CATCH.CE.RainD_Nc" "http://www.amma-catch.org/spip.php?article220"
+                             */
+                            sb.append(doi.getLandingExtUrl());
+                        } else {
+                            /*
+                            # [/public/<PROJECT>/<DOI_SUFFIX>.html] (public) case:
+                            Redirect "/AMMA-CATCH.all" "/public/AMMA-CATCH/AMMA-CATCH.all.html"
+                             */
+                            sb.append(dp.getLandingLocUrl());
+                        }
+                        sb.append('\"');
+                    }
+                } else {
+                    final DoiStaging ds = dsr.findOneByDoi(doi);
+
+                    if (ds == null) {
+                        logger.warn("Missing DoiStaging for [{}]; skipping Redirect", doiSuffix);
+                    } else {
+                        sb.append("\nRedirect \"/").append(doiSuffix).append("\" \"");
+
+                        // Note: no test on the active flag (public only):
+                        if (doi.getLandingExtUrl() != null) {
+                            /*
+                            # [landing page Ext] case:
+                            # note: complete pattern match
+                            Redirect "/AMMA-CATCH.CE.RainD_Nc" "http://www.amma-catch.org/spip.php?article220"
+                             */
+                            sb.append(doi.getLandingExtUrl());
+                        } else {
+                            /*
+                            # [/public/<PROJECT>/<DOI_SUFFIX>.html] (public) case:
+                            Redirect "/AMMA-CATCH.all" "/public/AMMA-CATCH/AMMA-CATCH.all.html"
+                             */
+                            sb.append(ds.getLandingLocUrl());
+                        }
+                        sb.append('\"');
+                    }
+                }
+            }
+            sb.append('\n');
+        } // projects
+
+        final String redirectRules = sb.toString();
+
+        FileUtils.writeFile(redirectRules,
+                new File(doiConfig.getPathConfig().getWebRedirectDir(), FILE_HT_ACCESS));
+
+        logger.debug("generateDOIUrls:\n{}", redirectRules);
+    }
+
+    private void generateEmbedUrls() throws IOException {
+        final StringBuilder sb = prepareBuffer();
+
+        sb.append("\n# Redirections to embbedded page fragments:");
+        sb.append("\n# note: RedirectMatch uses regexp (fuzzy match)");
+
+        final DoiService doiService = doiConfig.getDoiService();
+        final ProjectRepository pr = doiService.getProjectRepository();
+        final DoiRepository dr = doiService.getDoiRepository();
+        final DoiPublicRepository dpr = doiService.getDoiPublicRepository();
+        final DoiStagingRepository dsr = doiService.getDoiStagingRepository();
+
+        final List<Project> projects = pr.findAllByOrderByNameAsc();
+
+        // DOI listing per project:
+        for (Project p : projects) {
+            final String projectName = p.getName();
+
+            final ProjectConfig projectConfig = new ProjectConfig(doiConfig.getPathConfig(), projectName);
+
+            if (projectConfig.getPropertyBoolean(ProjectConfig.CONF_KEY_GENERATE_EMBEDDED)) {
+                // // [public/staging]/PROJECT/embed/DOI_SUFFIX[.html]
+                sb.append("\n# Project ").append(projectName);
+
+                /*
+                RedirectMatch "^/embed/AMMA-CATCH.CE.RainD_Nc([\.-])(.*)" "/staging/AMMA-CATCH/embed/AMMA-CATCH.CE.RainD_Nc$1$2"
+                 */
+                final List<Doi> dois = dr.findByProject(projectName);
+
+                logger.info("Dois for project[{}]: {}", projectName, dois.size());
+
+                for (Doi doi : dois) {
+                    final String doiSuffix = doi.getIdentifier();
+
+                    sb.append("\nRedirectMatch \"^/embed/").append(doiSuffix).append("([\\.-])(.*)\" \"");
+
+                    if (doi.getStatus() == Status.PUBLIC) {
+                        sb.append('/').append(Paths.DIR_WEB_PUBLIC).append('/');
+                    } else {
+                        sb.append('/').append(Paths.DIR_WEB_STAGING).append('/');
+                    }
+                    sb.append(projectName).append('/').append(Paths.DIR_WEB_EMBED).append('/').append(doiSuffix);
+                    sb.append("$1$2\"");
+                }
+                sb.append('\n');
+            }
+        }
+        sb.append('\n');
+
+        final String redirectRules = sb.toString();
+
+        FileUtils.writeFile(redirectRules,
+                new File(doiConfig.getPathConfig().getWebRedirectEmbedDir(), FILE_HT_ACCESS));
+
+        logger.debug("generateEmbedUrls:\n{}", redirectRules);
+    }
+
+    private StringBuilder prepareBuffer() {
+        final StringBuilder sb = globalData.buffer;
+        sb.setLength(0);
+
+        // htaccess Header:
+        sb.append("# Generated at ").append(globalData.now);
+        sb.append("\n# Need AllowOverride FileInfo\n");
+        return sb;
+    }
+}
diff --git a/src/main/java/fr/osug/doi/IndexUtil.java b/src/main/java/fr/osug/doi/IndexUtil.java
index 6825c11..736fd3b 100644
--- a/src/main/java/fr/osug/doi/IndexUtil.java
+++ b/src/main/java/fr/osug/doi/IndexUtil.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * JMMC project ( http://www.jmmc.fr ) - Copyright (C) CNRS.
+ * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
  ******************************************************************************/
 package fr.osug.doi;
 
@@ -12,7 +12,6 @@ import org.slf4j.LoggerFactory;
 
 /**
  *
- * @author bourgesl
  */
 public class IndexUtil {
 
diff --git a/src/main/java/fr/osug/doi/PathConfig.java b/src/main/java/fr/osug/doi/PathConfig.java
index 3bfc08f..113610d 100644
--- a/src/main/java/fr/osug/doi/PathConfig.java
+++ b/src/main/java/fr/osug/doi/PathConfig.java
@@ -6,288 +6,87 @@ package fr.osug.doi;
 import fr.osug.util.FileUtils;
 import java.io.File;
 import java.io.IOException;
-import java.io.Reader;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.Properties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  *
- * @author bourgesl
  */
-public final class ProjectConfig {
+public final class PathConfig {
 
-    public final static String CONF_PROJECT = "project.properties";
+    private final static Logger logger = LoggerFactory.getLogger(PathConfig.class.getName());
 
-    public final static String CONF_DOI_URL_MAPPING = "doi_url_landing_page.csv";
-
-    public final static String CONF_KEY_DATA_ACCESS_URL = "dataAccessUrl";
-    public final static String CONF_KEY_GENERATE_EMBEDDED = "generate_embedded";
-
-    private final static File[] EMPTY = new File[0];
-
-    private final static Logger logger = LoggerFactory.getLogger(ProjectConfig.class.getName());
-
-    private final String projectName;
-    private final File projectConf;
-
-    private final Properties properties;
-
-    private final Map<String, String> landingPageUrlMapping;
-
-    private final DoiTemplates templates;
-    private File[] metadataCsvFiles = EMPTY;
-    private File[] inputCsvFiles = EMPTY;
-    private File tmpDir;
-    private File tmpDoiDir;
-    private File stagingDir;
+    /* members */
+    private String webRootPath;
+    private File webRootDir;
     private File webStagingDir;
-    private File webStagingProjectDir;
-    private File webStagingProjectEmbedDir;
-    private File webStagingProjectXmlDir;
     private File webPublicDir;
-    private File webPublicProjectDir;
-
-    private boolean saveCSV = false;
-
-    public ProjectConfig(final String projectName) throws IOException {
-        this.projectName = projectName;
-
-        // may fail:
-        this.projectConf = FileUtils.getRequiredDirectory(Paths.DIR_CONFIG + projectName).getCanonicalFile();
-        logger.info("Project Config: {}", projectConf);
-
-        this.properties = loadProperties(projectConf);
-
-        this.landingPageUrlMapping = loadUrlMapping(new File(this.projectConf, CONF_DOI_URL_MAPPING));
-
-        this.templates = new DoiTemplates(
-                new File(this.projectConf, Paths.DIR_PROJECT_TEMPLATES).getAbsolutePath()
-        );
-
-        initMetadataDir(
-                new File(this.projectConf, Paths.DIR_PROJECT_METADATA).getAbsolutePath()
-        );
-        initInputDir(
-                new File(this.projectConf, Paths.DIR_PROJECT_INPUTS).getAbsolutePath()
-        );
-        initTmpDir(
-                new File(Paths.DIR_TMP, projectName).getAbsolutePath()
-        );
-        initTmpDoiDir(
-                new File(tmpDir, Paths.DIR_TMP_DOI).getAbsolutePath()
-        );
-
-        initStagingDir(
-                new File(Paths.DIR_STAGING, projectName).getAbsolutePath()
-        );
+    private File webRedirectDir;
+    private File webRedirectEmbedDir;
 
-        // /www/staging directories:
-        initWebStagingDir(Paths.DIR_WEB_STAGING);
-        initWebStagingProjectDir(
-                new File(Paths.DIR_WEB_STAGING, projectName).getAbsolutePath()
-        );
-        initWebStagingProjectEmbedDir(
-                new File(webStagingProjectDir, Paths.DIR_WEB_EMBED).getAbsolutePath()
-        );
-        initWebStagingProjectXmlDir(
-                new File(webStagingProjectDir, Paths.DIR_WEB_XML).getAbsolutePath()
-        );
-        // /www/public directories:
-        initWebPublicDir(Paths.DIR_WEB_PUBLIC);
-        initWebPublicProjectDir(
-                new File(Paths.DIR_WEB_PUBLIC, projectName).getAbsolutePath()
-        );
+    public PathConfig(final String webRootDir) throws IOException {
+        initWebRoot(webRootDir);
     }
 
-    public void initMetadataDir(final String dirPath) throws IOException {
-        this.metadataCsvFiles = CsvUtil.findCSVFiles(dirPath);
+    public void initWebRoot(final String webRootPath) throws IOException {
+        logger.info("web root: {}", webRootPath);
+        
+        this.webRootPath = webRootPath;
+        // /www/ directories:
+        initWebRootDir(webRootPath);
+        // /www/staging directory:
+        initWebStagingDir(webRootPath + Paths.DIR_WEB_STAGING);
+        // /www/public directory:
+        initWebPublicDir(webRootPath + Paths.DIR_WEB_PUBLIC);
+        // Redirect directories:
+        // /www/r directory:
+        initWebRedirectDir(webRootPath + Paths.DIR_WEB_REDIRECT);
+        // /www/embed directory:
+        initWebRedirectEmbedDir(webRootPath + Paths.DIR_WEB_EMBED);
     }
 
-    // Used by junit test
-    public void initInputDir(final String dirPath) throws IOException {
-        this.inputCsvFiles = CsvUtil.findCSVFiles(dirPath);
+    private void initWebRootDir(final String path) throws IOException {
+        this.webRootDir = FileUtils.createDirectories(path);
     }
 
-    public void initTmpDir(final String path) throws IOException {
-        this.tmpDir = FileUtils.createDirectories(path);
-    }
-
-    public void initTmpDoiDir(final String path) throws IOException {
-        this.tmpDoiDir = FileUtils.createDirectories(path);
-    }
-
-    public void initStagingDir(final String path) throws IOException {
-        this.stagingDir = FileUtils.createDirectories(path);
-    }
-
-    public void initWebStagingDir(final String path) throws IOException {
+    private void initWebStagingDir(final String path) throws IOException {
         this.webStagingDir = FileUtils.createDirectories(path);
     }
 
-    public void initWebStagingProjectDir(final String path) throws IOException {
-        this.webStagingProjectDir = FileUtils.createDirectories(path);
-    }
-
-    public void initWebStagingProjectEmbedDir(final String path) throws IOException {
-        this.webStagingProjectEmbedDir = FileUtils.createDirectories(path);
-    }
-
-    public void initWebStagingProjectXmlDir(final String path) throws IOException {
-        this.webStagingProjectXmlDir = FileUtils.createDirectories(path);
-    }
-
-    public void initWebPublicDir(final String path) throws IOException {
+    private void initWebPublicDir(final String path) throws IOException {
         this.webPublicDir = FileUtils.createDirectories(path);
     }
 
-    public void initWebPublicProjectDir(final String path) throws IOException {
-        this.webPublicProjectDir = FileUtils.createDirectories(path);
+    private void initWebRedirectDir(final String path) throws IOException {
+        this.webRedirectDir = FileUtils.createDirectories(path);
     }
 
-    public String getProjectName() {
-        return projectName;
+    private void initWebRedirectEmbedDir(final String path) throws IOException {
+        this.webRedirectEmbedDir = FileUtils.createDirectories(path);
     }
 
-    public File getProjectConf() {
-        return projectConf;
+    public String getWebRootPath() {
+        return webRootPath;
     }
 
-    public String getProperty(final String key) {
-        final String val = properties.getProperty(key);
-        if (val != null) {
-            return val.trim();
-        }
-        return null;
-    }
-
-    public boolean getPropertyBoolean(final String key) {
-        return Boolean.valueOf(properties.getProperty(key, "false"));
-    }
-
-    public String getLandingPageUrl(final String doiSuffix) {
-        return landingPageUrlMapping.get(doiSuffix);
-    }
-
-    public DoiTemplates getTemplates() {
-        return templates;
-    }
-
-    public File[] getMetadataCsvFiles() {
-        return metadataCsvFiles;
-    }
-
-    public File[] getInputCsvFiles() {
-        return inputCsvFiles;
-    }
-
-    public File getTmpDir() {
-        return tmpDir;
-    }
-
-    public File getTmpDoiDir() {
-        return tmpDoiDir;
-    }
-
-    public File getStagingDir() {
-        return stagingDir;
+    public File getWebRootDir() {
+        return webRootDir;
     }
 
     public File getWebStagingDir() {
         return webStagingDir;
     }
 
-    public File getWebStagingProjectDir() {
-        return webStagingProjectDir;
-    }
-
-    public File getWebStagingProjectEmbedDir() {
-        return webStagingProjectEmbedDir;
-    }
-
-    public File getWebStagingProjectXmlDir() {
-        return webStagingProjectXmlDir;
-    }
-
     public File getWebPublicDir() {
         return webPublicDir;
     }
 
-    public File getWebPublicProjectDir() {
-        return webPublicProjectDir;
-    }
-
-    public boolean isSaveCSV() {
-        return saveCSV;
-    }
-
-    public void setSaveCSV(boolean saveCSV) {
-        this.saveCSV = saveCSV;
+    public File getWebRedirectDir() {
+        return webRedirectDir;
     }
 
-    private static Properties loadProperties(final File projectConf) {
-        final File propFile = new File(projectConf, CONF_PROJECT);
-        logger.info("Loading {}", propFile);
-
-        final Properties props = new Properties();
-        Reader r = null;
-        try {
-            r = FileUtils.reader(propFile);
-            props.load(r);
-        } catch (IOException ioe) {
-            throw new RuntimeException("loadProperties failed:" + propFile.getAbsolutePath(), ioe);
-        } finally {
-            FileUtils.closeFile(r);
-        }
-        logger.info("properties: {}", props);
-        return props;
+    public File getWebRedirectEmbedDir() {
+        return webRedirectEmbedDir;
     }
 
-    private static Map<String, String> loadUrlMapping(final File csvFile) throws IOException {
-        final Map<String, String> urlMapping;
-
-        if (csvFile.exists()) {
-            final CsvData data = CsvUtil.read(csvFile);
-            logger.debug("CSV data: {}", data);
-
-            final List<String[]> rows = data.getRows();
-
-            urlMapping = new LinkedHashMap<String, String>(rows.size());
-
-            // Get mappings [DOI|URL]
-            for (ListIterator<String[]> it = rows.listIterator(); it.hasNext();) {
-                final String[] cols = it.next();
-                if (cols == null || cols.length != 2) {
-                    // remove line:
-                    it.remove();
-                    continue;
-                }
-                String key = cols[0].trim();
-                if (key.isEmpty() || key.charAt(0) == Const.COMMENT) {
-                    continue;
-                }
-                String url = cols[1].trim();
-                if (url.isEmpty() || !url.startsWith("http")) {
-                    continue;
-                }
-                try {
-                    // should detect repeated keys or values ?
-                    urlMapping.put(key, new URL(url).toString());
-                } catch (MalformedURLException mue) {
-                    logger.info("invalid URL: {}", url);
-                }
-            }
-        } else {
-            urlMapping = Collections.emptyMap();
-        }
-        logger.info("urlMapping: {}", urlMapping);
-        return urlMapping;
-    }
 }
diff --git a/src/main/java/fr/osug/doi/Paths.java b/src/main/java/fr/osug/doi/Paths.java
index 78f42c6..0ecc530 100644
--- a/src/main/java/fr/osug/doi/Paths.java
+++ b/src/main/java/fr/osug/doi/Paths.java
@@ -11,7 +11,6 @@ import org.slf4j.LoggerFactory;
 
 /**
  *
- * @author bourgesl
  */
 public final class Paths {
 
@@ -45,15 +44,21 @@ public final class Paths {
     /* [base]/tmp/[PROJECT]/doi/ */
     public final static String DIR_TMP_DOI = "doi/";
 
-    /* [base]/staging directory (DOI) */
-    public final static String DIR_STAGING = Paths.DIR_BASE + "staging/";
+    /* [base]/data directory (DOI) */
+    public final static String DIR_DATA = Paths.DIR_BASE + "data/";
+    /* [base]/data/staging directory (DOI) */
+    public final static String DIR_STAGING = DIR_DATA + "staging/";
+    /* [base]/data/public directory (DOI) */
+    public final static String DIR_PUBLIC = DIR_DATA + "public/";
 
-    /* [base]/www directory (web) */
+    /* [base]/www directory = web root */
     public final static String DIR_WEB = Paths.DIR_BASE + "www/";
-    /* [base]/www/staging/ */
-    public final static String DIR_WEB_STAGING = DIR_WEB + "staging/";
-    /* [base]/www/public/ */
-    public final static String DIR_WEB_PUBLIC = DIR_WEB + "public/";
+    /* web sub-directory /r */
+    public final static String DIR_WEB_REDIRECT = "r";
+    /* web sub-directory /staging */
+    public final static String DIR_WEB_STAGING = "staging";
+    /* web sub-directory /public */
+    public final static String DIR_WEB_PUBLIC = "public";
     /* web sub-directory /embed */
     public final static String DIR_WEB_EMBED = "embed";
     /* web sub-directory /xml */
diff --git a/src/main/java/fr/osug/doi/PipelineCommonData.java b/src/main/java/fr/osug/doi/PipelineCommonData.java
new file mode 100644
index 0000000..1268939
--- /dev/null
+++ b/src/main/java/fr/osug/doi/PipelineCommonData.java
@@ -0,0 +1,20 @@
+/*******************************************************************************
+ * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
+ ******************************************************************************/
+package fr.osug.doi;
+
+import java.util.Date;
+
+/**
+ *
+ */
+public class PipelineCommonData {
+
+    public static final int BUFFER_SIZE = 4 * 1024;
+
+    /* members */
+    final Date now = new Date();
+    /** temporary StringBuilder to hold xml document */
+    final StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
+
+}
diff --git a/src/main/java/fr/osug/doi/PipelineConfig.java b/src/main/java/fr/osug/doi/PipelineConfig.java
new file mode 100644
index 0000000..288fa70
--- /dev/null
+++ b/src/main/java/fr/osug/doi/PipelineConfig.java
@@ -0,0 +1,12 @@
+/*******************************************************************************
+ * JMMC project ( http://www.jmmc.fr ) - Copyright (C) CNRS.
+ ******************************************************************************/
+package fr.osug.doi;
+
+/**
+ *
+ * @author bourgesl
+ */
+public class PipelineConfig {
+
+}
diff --git a/src/main/java/fr/osug/doi/ProcessPipeline.java b/src/main/java/fr/osug/doi/ProcessPipeline.java
index bf3fbb1..22e9d5a 100644
--- a/src/main/java/fr/osug/doi/ProcessPipeline.java
+++ b/src/main/java/fr/osug/doi/ProcessPipeline.java
@@ -4,12 +4,9 @@
 package fr.osug.doi;
 
 import fr.osug.doi.domain.Doi;
-import fr.osug.doi.domain.DoiStaging;
-import fr.osug.doi.domain.Project;
-import fr.osug.doi.domain.IndexEntry;
 import fr.osug.doi.domain.NameEntry;
-import fr.osug.doi.repository.DoiStagingRepository;
-import fr.osug.doi.repository.ProjectRepository;
+import fr.osug.doi.domain.Status;
+import fr.osug.doi.service.DataciteClient;
 import fr.osug.doi.service.DoiService;
 import fr.osug.doi.validation.ValidationUtil;
 import fr.osug.util.FileUtils;
@@ -22,358 +19,118 @@ import java.io.IOException;
 import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.ListIterator;
-import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
 import javax.xml.transform.stream.StreamResult;
 import javax.xml.transform.stream.StreamSource;
-import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.transaction.annotation.Transactional;
 
 /**
  *
- * @author bourgesl
  */
-public final class ProcessPipeline {
+public class ProcessPipeline {
 
     private final static Logger logger = LoggerFactory.getLogger(ProcessPipeline.class.getName());
 
-    public final static String HTML_EXT = ".html";
-    public final static String HTML_INDEX = "index" + HTML_EXT;
-    public final static String HTML_REPORT = "report" + HTML_EXT;
-    public final static String HTML_ERROR = "error" + HTML_EXT;
-
     public final static String XSD_DOI_SCHEMA = "file://" + Paths.DIR_XSD_SCHEMA + "/metadata.xsd";
 
     private final static String XSLT_XML_TO_DOI = "xml2doi.xsl";
-    private final static String XSLT_DOI_TO_LANDING_PAGE = "doi2landing.xsl";
-
-    private final static String XSLT_PROJECT_INDEX = "project_index.xsl";
-    private final static String XSLT_PROJECT_REPORT = "project_report.xsl";
-
-    private final static String CONFIG_ACCESS_INSTRUCTIONS = "access_instruction.html";
-
-    // xslt parameters (common):
-    private final static String XSLT_PARAM_DATE = "date";
-    private final static String XSLT_PARAM_WEB_ROOT = "www_root";
 
-    // xslt parameters (doi2landing):
-    private final static String XSLT_PARAM_DATA_ACCESS_HTML = "data_access_html";
-    private final static String XSLT_PARAM_DATA_ACCESS_URL = "data_access_url";
-    private final static String XSLT_PARAM_EMBEDDED = "embedded";
-    private final static String XSLT_PARAM_EMBEDDED_FULL = "full";
-    private final static String XSLT_PARAM_EMBEDDED_HEADER = "header";
-    private final static String XSLT_PARAM_EMBEDDED_META = "meta";
-    private final static String XSLT_PARAM_LANDING_PAGE_URL = "landing_page_url";
-    private final static String XSLT_PARAM_METADATA_FILE = "metadataFile";
-
-    // xslt parameters (index_project):
-    private final static String XSLT_PARAM_PARENT = "parent";
-    private final static String XSLT_PARAM_CURRENT = "current";
+    private final static String FILE_NAMES_CSV = "names" + Const.FILE_EXT_CSV;
 
     // members
-    private Date _now = null;
     private final DOIConfig doiConfig;
     private final ProjectConfig projectConfig;
     // temporary data
-    private final PipelineGlobalData globalData = new PipelineGlobalData();
+    private final ProcessPipelineData globalData = new ProcessPipelineData();
+    /** child pipeline */
+    private final GeneratePipeline pipe;
 
     public ProcessPipeline(final DOIConfig doiConfig,
                            final ProjectConfig projectConfig) throws IOException {
 
         this.doiConfig = doiConfig;
         this.projectConfig = projectConfig;
+
+        // preload all needed data:        
+        projectConfig.prepareProcess();
+
+        // child pipeline:
+        this.pipe = new GeneratePipeline(doiConfig, true, false, globalData);
     }
 
     public void process() throws IOException {
+        File[] files;
 
         // 1 - Process complete datasets (csv):
-        for (File inputCsvFile : projectConfig.getMetadataCsvFiles()) {
+        files = CsvUtil.findCSVFiles(projectConfig.getMetadataDir());
+        for (File inputCsvFile : files) {
             processCsvFile(inputCsvFile, false);
         }
 
         // 2 - Process partial datasets (csv to be merged with templates):
-        for (File inputCsvFile : projectConfig.getInputCsvFiles()) {
+        files = CsvUtil.findCSVFiles(projectConfig.getInputDir());
+        for (File inputCsvFile : files) {
             processCsvFile(inputCsvFile, true);
         }
 
         // TODO: 3 - process complete datasets as XML files (full metadata)
+        // use case ?
         // global validation of references
         validateRefs();
 
-        // 4 - Landing pages
-        generateLandingPages();
+        // 4 - Landing pages:
+        pipe.generateLandingPages(projectConfig);
+
+        // test publication datacite ?
+        publishDatacite();
 
-        // TODO: test publication datacite ?
         // Update database:
         updateDatabase();
 
         // Update index pages:
-        generateIndexPagesStaging();
+        pipe.generateIndexPages();
     }
 
-    private void generateLandingPages() throws IOException {
-
-        final File webStaging = projectConfig.getWebStagingProjectDir();
-        logger.info("Generating landing pages into {}", webStaging);
-
-        final File webStagingEmbed = projectConfig.getWebStagingProjectEmbedDir();
-        final File webStagingXml = projectConfig.getWebStagingProjectXmlDir();
-
-        final File dataAccessFileLocation = new File(projectConfig.getProjectConf(), CONFIG_ACCESS_INSTRUCTIONS);
-        final File dataAccessFile = FileUtils.getFile(dataAccessFileLocation);
-        if (dataAccessFile == null) {
-            logger.warn("Missing dataAccessFile: {}", dataAccessFileLocation);
+    private void publishDatacite() throws IOException {
+        if (!doiConfig.isDataciteClientEnabled()) {
+            logger.warn("Datacite client disabled.");
+            return;
         }
 
-        // TODO: use mapping file [DOI:dataAccessUrl] like url mapping:
-        final String defaultDataAccessUrl = projectConfig.getProperty(ProjectConfig.CONF_KEY_DATA_ACCESS_URL);
+        final DataciteClient dc = doiConfig.getDataciteClient();
 
-        final Map<String, Object> xslParameters = new HashMap<String, Object>(8);
-        xslParameters.put(XSLT_PARAM_DATE, now().toString());
-        if (dataAccessFile != null) {
-            xslParameters.put(XSLT_PARAM_DATA_ACCESS_HTML, dataAccessFile.toURI().toString());
-        }
-        if (defaultDataAccessUrl != null) {
-            xslParameters.put(XSLT_PARAM_DATA_ACCESS_URL, defaultDataAccessUrl);
-        }
+        logger.info("Datacite DOI list:\n{}", dc.getDois());
 
-        for (PipelineDoiData doiData : globalData.getDoiDatas().values()) {
+        for (ProcessPipelineDoiData doiData : globalData.getDoiDatas().values()) {
             final String doiSuffix = doiData.getDoiSuffix();
-            logger.info("processing {}", doiSuffix);
 
-            // Get external Landing page URL:
-            final String landingPageUrl = projectConfig.getLandingPageUrl(doiSuffix);
-            if (landingPageUrl != null) {
-                logger.info("landingPageUrl: {}", landingPageUrl);
-                xslParameters.put(XSLT_PARAM_LANDING_PAGE_URL, landingPageUrl);
+            final String doi = Const.DOI_PREFIX_TEST + '/' + doiSuffix;
+            // short url:
+            final String url = doiConfig.getDomain() + '/' + doiSuffix;
 
-                doiData.setLandingPageUrl(landingPageUrl);
-            } else {
-                xslParameters.remove(XSLT_PARAM_LANDING_PAGE_URL);
-            }
-
-            // add warnings:
-            if (dataAccessFile == null) {
-                doiData.addError("Missing data access instructions (" + CONFIG_ACCESS_INSTRUCTIONS + ")");
-            }
-            if (StringUtils.isEmpty(defaultDataAccessUrl)) {
-                doiData.addError("Missing data access url");
-            }
-
-            final File stagingFileDOI = doiData.getStagingFileDOI();
+            logger.info("publish DOI [{}]", doi);
 
-            if (stagingFileDOI == null) {
-                logger.info("Ignoring {} (no xml)", doiSuffix);
-            } else {
-                final String filenameNoExt = FileUtils.getFileNameWithoutExtension(stagingFileDOI);
-
-                // main landing page:
-                xslParameters.remove(XSLT_PARAM_EMBEDDED);
-                xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../");
-                File output = new File(webStaging, filenameNoExt + HTML_EXT);
-                generateLandingPage(xslParameters, stagingFileDOI, output);
-
-                if (projectConfig.getPropertyBoolean(ProjectConfig.CONF_KEY_GENERATE_EMBEDDED)) {
-                    // embedded mode:
-                    xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../../");
-
-                    // embedded mode - full:
-                    output = new File(webStagingEmbed, filenameNoExt + HTML_EXT);
-                    xslParameters.put(XSLT_PARAM_EMBEDDED, XSLT_PARAM_EMBEDDED_FULL);
-                    generateLandingPage(xslParameters, stagingFileDOI, output);
-
-                    // embedded mode - header:
-                    output = new File(webStagingEmbed, filenameNoExt + "-header" + HTML_EXT);
-                    xslParameters.put(XSLT_PARAM_EMBEDDED, XSLT_PARAM_EMBEDDED_HEADER);
-                    generateLandingPage(xslParameters, stagingFileDOI, output);
-
-                    // embedded mode - meta:
-                    output = new File(webStagingEmbed, filenameNoExt + "-meta" + HTML_EXT);
-                    xslParameters.put(XSLT_PARAM_EMBEDDED, XSLT_PARAM_EMBEDDED_META);
-                    generateLandingPage(xslParameters, stagingFileDOI, output);
-                }
+            // Push DOI in staging phase:
+            final String metadata = FileUtils.readFile(doiData.getStagingFileDOI());
 
-                // Xml DOI:
-                output = new File(webStagingXml, stagingFileDOI.getName());
-                FileUtils.copy(stagingFileDOI, output);
-            }
+            dc.publishDoi(doi, url, metadata);
         }
     }
 
-    private void generateLandingPage(final Map<String, Object> xslParameters,
-                                     final File stagingFileDOI, final File output) {
-
-        logger.info("generateLandingPage: {}", output);
-
-        xslParameters.put(XSLT_PARAM_METADATA_FILE, stagingFileDOI.getName());
-        logger.debug("xslParameters: {}", xslParameters);
-
-        // use XSLT to generate landing page from the doi xml file:
-        doiConfig.getXmlFactory().transform(
-                new StreamSource(stagingFileDOI),
-                Paths.DIR_XSL + XSLT_DOI_TO_LANDING_PAGE,
-                xslParameters, true,
-                new StreamResult(output)
-        );
-    }
-
-    @Transactional
-    /* public needed by Transactional */
-    public void updateDatabase() {
+    private void updateDatabase() {
+        logger.info("updateDatabase ...");
         final String projectName = projectConfig.getProjectName();
+        final Date now = globalData.now;
 
-        final Date now = now();
-
-        // Get or create the project:
-        final DoiService doiService = doiConfig.getDoiService();
-        final Project project = doiService.getOrCreateProject(projectName, now);
-
-        // Update Dois:
-        for (PipelineDoiData doiData : globalData.getDoiDatas().values()) {
-            // Update DOI in staging phase:
-            doiService.createOrUpdateStagingDoi(project, doiData.getDoiSuffix(),
-                    doiData.getTitle(), doiData.getLandingPageUrl(),
-                    doiData.isValid(), doiData.getMetadataMd5(),
-                    doiData.messagesToString(), now);
-        }
-
-        // Update Project:
-        doiService.updateProjectDate(projectName, now);
-    }
-
-    @Transactional
-    /* public needed by Transactional */
-    public void generateIndexPagesStaging() {
-        final File webStaging = projectConfig.getWebStagingDir();
-
-        final DoiService doiService = doiConfig.getDoiService();
-        final ProjectRepository pr = doiService.getProjectRepository();
-        final DoiStagingRepository dsr = doiService.getDoiStagingRepository();
-
-        final List<Project> projects = pr.findAllByOrderByNameAsc();
-
-        // Project listing:
-        List<IndexEntry> entries = new ArrayList<IndexEntry>();
-
-        for (Project p : projects) {
-            final String projectName = p.getName();
-            final Long countDoi = dsr.countByProject(projectName);
-            final IndexEntry ie = new IndexEntry(projectName, p.getDescription(), p.getUpdateDate(), countDoi);
-
-            logger.debug("IndexEntry: {}", ie);
-            entries.add(ie);
-        }
-        // serialize entries and transform in the /staging/index.html
-        IndexUtil.write(entries, globalData.buffer);
-
-        String xmlDoc = globalData.buffer.toString();
-        logger.debug("xmlDoc(project index): \n{}", xmlDoc);
-
-        final Map<String, Object> xslParameters = new HashMap<String, Object>(8);
-        xslParameters.put(XSLT_PARAM_DATE, now().toString());
-        xslParameters.put(XSLT_PARAM_CURRENT, "staging");
-        xslParameters.put(XSLT_PARAM_WEB_ROOT, "../");
-        File output = new File(webStaging, HTML_INDEX);
-
-        // use XSLT to generate the index page:
-        doiConfig.getXmlFactory().transform(
-                new StreamSource(new StringReader(xmlDoc)),
-                Paths.DIR_XSL_ROOT + XSLT_PROJECT_INDEX,
-                xslParameters, true,
-                new StreamResult(output)
-        );
-
-        // DOI listing per project:
-        for (Project p : projects) {
-            final String projectName = p.getName();
-            final List<DoiStaging> dsList = dsr.findByProject(projectName);
-
-            logger.debug("DoiStaging list for project[{}]: {}", projectName, dsList);
-
-            // index.html:
-            entries.clear();
-
-            for (DoiStaging ds : dsList) {
-                logger.debug("DoiStaging: {}", ds);
-
-                final Doi doi = ds.getDoi();
-
-                final IndexEntry ie = new IndexEntry(doi.getIdentifier(), doi.getDescription(), ds.getUpdateDate());
+        // To encapsulate all write operations in a single transaction (all or nothing):
+        doiConfig.getDoiService().importDoiStagingCollection(projectName, now, globalData.getDoiDatas().values());
 
-                logger.debug("IndexEntry: {}", ie);
-                entries.add(ie);
-            }
-            // serialize entries and transform in the /staging/PROJECT/index.html
-            IndexUtil.write(entries, globalData.buffer);
-
-            xmlDoc = globalData.buffer.toString();
-            logger.debug("xmlDoc(doi index): \n{}", xmlDoc);
-
-            xslParameters.put(XSLT_PARAM_PARENT, "staging");
-            xslParameters.put(XSLT_PARAM_CURRENT, projectName);
-            xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../");
-            // suppose child directory is the project folder:
-            output = new File(new File(webStaging, projectName), HTML_INDEX);
-
-            // use XSLT to generate the index page:
-            doiConfig.getXmlFactory().transform(
-                    new StreamSource(new StringReader(xmlDoc)),
-                    Paths.DIR_XSL_ROOT + XSLT_PROJECT_INDEX,
-                    xslParameters, true,
-                    new StreamResult(output)
-            );
-
-            // serialize doi staging list and transform in the /staging/PROJECT/report.html
-            IndexUtil.writeReport(dsList, globalData.buffer);
-
-            xmlDoc = globalData.buffer.toString();
-            logger.debug("xmlDoc(doi report): \n{}", xmlDoc);
-
-            xslParameters.put(XSLT_PARAM_PARENT, "staging");
-            xslParameters.put(XSLT_PARAM_CURRENT, projectName);
-            xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../");
-            // suppose child directory is the project folder:
-            output = new File(new File(webStaging, projectName), HTML_REPORT);
-
-            // use XSLT to generate the report page:
-            doiConfig.getXmlFactory().transform(
-                    new StreamSource(new StringReader(xmlDoc)),
-                    Paths.DIR_XSL_ROOT + XSLT_PROJECT_REPORT,
-                    xslParameters, true,
-                    new StreamResult(output)
-            );
-
-            // serialize doi staging error list and transform in the /staging/PROJECT/error.html
-            final List<DoiStaging> dsErrorList = dsr.findErrorByProject(projectName);
-            logger.debug("DoiStaging error list for project[{}]: {}", projectName, dsErrorList);
-
-            IndexUtil.writeReport(dsErrorList, globalData.buffer);
-
-            xmlDoc = globalData.buffer.toString();
-            logger.debug("xmlDoc(error report): \n{}", xmlDoc);
-
-            xslParameters.put(XSLT_PARAM_PARENT, "staging");
-            xslParameters.put(XSLT_PARAM_CURRENT, projectName);
-            xslParameters.put(XSLT_PARAM_WEB_ROOT, "../../");
-            // suppose child directory is the project folder:
-            output = new File(new File(webStaging, projectName), HTML_ERROR);
-
-            // use XSLT to generate the report page:
-            doiConfig.getXmlFactory().transform(
-                    new StreamSource(new StringReader(xmlDoc)),
-                    Paths.DIR_XSL_ROOT + XSLT_PROJECT_REPORT,
-                    xslParameters, true,
-                    new StreamResult(output)
-            );
-        }
-        logger.info("generateIndexPagesStaging: done.");
+        logger.info("updateDatabase: done");
     }
 
     private void validateRefs() throws IOException {
@@ -388,8 +145,9 @@ public final class ProcessPipeline {
             final Set<String> refs = globalData.getDoiRefs(doiId);
             if (refs != null) {
                 for (String refId : refs) {
+                    final boolean isPublic = refId.startsWith(publicPrefix);
                     // Only check DOI references with Prefix Test or Public:
-                    if (ValidationUtil.isTestPrefix(refId) || refId.startsWith(publicPrefix)) {
+                    if (ValidationUtil.isTestPrefix(refId) || isPublic) {
                         logger.debug("validateRefs: check ref[{}]", refId);
 
                         boolean found = false;
@@ -400,12 +158,19 @@ public final class ProcessPipeline {
                         } else {
                             // Then in database (previous data):
                             final String doiSuffix = DoiCsvData.getDoiSuffix(refId);
-                            if (doiService.getDoi(doiSuffix) != null) {
-                                found = true;
+
+                            final Doi doi = doiService.getDoi(doiSuffix);
+                            if (doi != null) {
+                                if (isPublic && doi.getStatus() != Status.PUBLIC) {
+                                    logger.warn("validateRefs: DOI[{}] is not public yet (use test prefix instead)", refId);
+                                    found = false;
+                                } else {
+                                    found = true;
+                                }
                             }
                         }
                         if (!found) {
-                            final PipelineDoiData doiData = globalData.getDoiData(doiId);
+                            final ProcessPipelineDoiData doiData = globalData.getDoiData(doiId);
                             if (doiData != null) {
                                 doiData.addError("Invalid " + Const.KEY_REL_ID_START + " '" + refId + "' found.");
                             }
@@ -462,7 +227,7 @@ public final class ProcessPipeline {
         }
 
         // Save file as CSV:
-        final File fileCSV = new File(projectConfig.getTmpDir(), "names" + Const.FILE_EXT_CSV);
+        final File fileCSV = new File(projectConfig.getTmpDir(), FILE_NAMES_CSV);
         CsvUtil.write(data, fileCSV);
     }
 
@@ -486,7 +251,7 @@ public final class ProcessPipeline {
                     final String doiSuffix = data.getDoiSuffix();
                     logger.info("process: dataset[{}].", doiSuffix);
 
-                    final PipelineDoiData doiData = globalData.newDoiData(doiId, doiSuffix, data.getTitle());
+                    final ProcessPipelineDoiData doiData = globalData.newDoiData(doiId, doiSuffix, data.getTitle());
 
                     // ignore duplicates
                     if (doiData != null) {
@@ -508,7 +273,7 @@ public final class ProcessPipeline {
         }
     }
 
-    private void mergeCSV(final DoiCsvData data, final PipelineDoiData doiData) {
+    private void mergeCSV(final DoiCsvData data, final ProcessPipelineDoiData doiData) throws IOException {
 
         final DoiTemplates templates = projectConfig.getTemplates();
 
@@ -526,59 +291,82 @@ public final class ProcessPipeline {
 
         final String doiId = data.getDoiId(); // not null
 
-        /*
-        ### 3/ Traitement spécifiques: fusionner avec template pays
+        if (projectConfig.getPropertyBoolean(ProjectConfig.CONF_KEY_USE_TEMPLATE_COUNTRY)) {
+            /*
+            ### 3/ Traitement spécifiques: fusionner avec template pays
 
-        Fusionner avec template pays (template_benin.csv):
+            Fusionner avec template pays (template_benin.csv):
 
-        Trouver le template du pays qui contient geoLocationPlace;Benin
-         */
-        final String geoLocationPlace = data.getFirstGeoLocationPlace();
-        if (geoLocationPlace == null) {
-            doiData.addWarning("Missing ''" + Const.KEY_GEO_LOCATION_PLACE + "'' in dataset '" + doiId + "'");
-        } else {
-            t = templates.getByGeoLocationPlace(geoLocationPlace);
-            if (t == null) {
-                doiData.addError("Missing country template for '" + geoLocationPlace + "'");
+            Trouver le template du pays qui contient geoLocationPlace;Benin
+             */
+            final String geoLocationPlace = data.getFirstGeoLocationPlace();
+            if (geoLocationPlace == null) {
+                doiData.addWarning("Missing ''" + Const.KEY_GEO_LOCATION_PLACE + "'' in dataset '" + doiId + "'");
             } else {
-                logger.info("Using country template '{}'", geoLocationPlace);
-                data.mergeFrom(t);
+                t = templates.getByGeoLocationPlace(geoLocationPlace);
+                if (t == null) {
+                    doiData.addError("Missing country template for '" + geoLocationPlace + "'");
+                } else {
+                    logger.info("Using country template '{}'", geoLocationPlace);
+                    data.mergeFrom(t);
+                }
             }
         }
 
-        /*
-        ### 4/ Traitement spécifiques: fusionner avec template jeu
+        if (projectConfig.getPropertyBoolean(ProjectConfig.CONF_KEY_USE_TEMPLATE_DOI)) {
+            /*
+            ### 4/ Traitement spécifiques: fusionner avec template jeu
 
-        Trouver le nom du jeu dans le XML:
-        <identifier identifierType="DOI">10.5072/AMMA-CATCH.CL.Run_O</identifier>
+            Trouver le nom du jeu dans le XML:
+            <identifier identifierType="DOI">10.5072/AMMA-CATCH.CL.Run_O</identifier>
 
-        Trouver le template du jeu qui contient identifier:DOI;10.5072/AMMA-CATCH.CL.Run_O
-         */
-        t = templates.getByIdentifier(doiId);
-        if (t == null) {
-            doiData.addWarning("Missing dataset template for id '" + doiId + "'");
-        } else {
-            logger.info("Using dataset template for id '{}'", doiId);
-            data.mergeFrom(t);
+            Trouver le template du jeu qui contient identifier:DOI;10.5072/AMMA-CATCH.CL.Run_O
+             */
+            t = templates.getByIdentifier(doiId);
+            if (t == null) {
+                doiData.addWarning("Missing dataset template for id '" + doiId + "'");
+            } else {
+                logger.info("Using dataset template for id '{}'", doiId);
+                data.mergeFrom(t);
+            }
         }
     }
 
-    private void validateData(final DoiCsvData data, final PipelineDoiData doiData) {
+    private void validateData(final DoiCsvData data, final ProcessPipelineDoiData doiData) throws IOException {
         logger.debug("validateData [{}]", doiData.getDoiId());
-        // TODO: check title ?
 
-        // check all values:
+        // check title
+        if (data.getTitle().isEmpty()) {
+            doiData.addWarning("Empty title.");
+        }
+
         final List<String[]> rows = data.getRows();
+
+        // check all values:
         for (ListIterator<String[]> it = rows.listIterator(); it.hasNext();) {
-            final String[] cols = it.next();
+            String[] cols = it.next();
             if (cols != null && cols.length >= 2) {
                 if (cols[1].toLowerCase().contains("todo")) {
                     doiData.addWarning("TODO detected for key [" + cols[0] + ']');
                 }
 
-                // Collect name references:
+                // Collect name references and override specific name entries:
                 final String key = cols[0];
                 if (key.startsWith(Const.KEY_CREATOR_NAME) || key.startsWith(Const.KEY_CONTRIBUTOR_NAME)) {
+                    final String name = cols[1];
+                    final String simpleName = ValidationUtil.simplifyName(name);
+
+                    // Check in override names:
+                    final NameEntry override = projectConfig.getOverrideNameEntry(simpleName);
+
+                    if (override != null) {
+                        logger.debug("override: {}", override);
+                        cols = NameEntry.toCsv(override);
+                        cols[0] = key;
+
+                        it.set(cols);
+                    }
+
                     globalData.addNameRef(cols);
                 }
             }
@@ -592,7 +380,7 @@ public final class ProcessPipeline {
         globalData.addRefs(doiData.getDoiId(), refs);
     }
 
-    private void saveData(final DoiCsvData data, final PipelineDoiData doiData) throws IOException {
+    private void saveData(final DoiCsvData data, final ProcessPipelineDoiData doiData) throws IOException {
         // Sort keys:
         data.sort();
 
@@ -634,7 +422,7 @@ public final class ProcessPipeline {
                 doiData.addError("No Metadata XML output");
             } else {
                 final File fileDOI = new File(tmpDoiDir, baseName + Const.FILE_EXT_XML);
-                DoiCsvToXml.writeXml(doiDoc, fileDOI);
+                FileUtils.writeFile(doiDoc, fileDOI);
 
                 // compute MD5 on metadata:
                 final String xmlMd5 = FileUtils.MD5(new FileInputStream(fileDOI));
@@ -678,14 +466,7 @@ public final class ProcessPipeline {
 
         if (forceSaveXML || doiConfig.isDebug()) {
             final File fileXML = new File(tmpDoiDir, baseName + "-xml" + Const.FILE_EXT_XML);
-            DoiCsvToXml.writeXml(xmlDoc, fileXML);
-        }
-    }
-
-    private Date now() {
-        if (_now == null) {
-            _now = new Date();
+            FileUtils.writeFile(xmlDoc, fileXML);
         }
-        return _now;
     }
 }
diff --git a/src/main/java/fr/osug/doi/ProcessPipelineData.java b/src/main/java/fr/osug/doi/ProcessPipelineData.java
index 970e7c8..e012075 100644
--- a/src/main/java/fr/osug/doi/ProcessPipelineData.java
+++ b/src/main/java/fr/osug/doi/ProcessPipelineData.java
@@ -1,12 +1,11 @@
 /*******************************************************************************
- * JMMC project ( http://www.jmmc.fr ) - Copyright (C) CNRS.
+ * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
  ******************************************************************************/
 package fr.osug.doi;
 
 import fr.osug.doi.domain.NameEntry;
 import fr.osug.util.StringBuilderWriter;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
@@ -14,45 +13,40 @@ import java.util.TreeSet;
 
 /**
  *
- * @author bourgesl
  */
-public final class PipelineGlobalData {
+public final class ProcessPipelineData extends PipelineCommonData {
 
     private final static Set<String> NO_REFS = Collections.emptySet();
 
-    private static final int BUFFER_SIZE = 4 * 1024;
-
     /* members */
-    /** temporary StringBuilder to hold xml document */
-    final StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
     /** temporary StringBuilderWriter to hold doi document */
     final StringBuilderWriter doiDoc = new StringBuilderWriter(BUFFER_SIZE);
     // References:
     private final Map<String, Set<String>> doiRefs = new LinkedHashMap<String, Set<String>>();
     /* doi data (insertion order) */
-    private final Map<String, PipelineDoiData> datas = new LinkedHashMap<String, PipelineDoiData>();
+    private final Map<String, ProcessPipelineDoiData> datas = new LinkedHashMap<String, ProcessPipelineDoiData>();
     // Name references:
     private final TreeSet<NameEntry> nameRefs = new TreeSet<NameEntry>(NameEntry.COMPARATOR);
 
-    public Map<String, PipelineDoiData> getDoiDatas() {
+    public Map<String, ProcessPipelineDoiData> getDoiDatas() {
         return datas;
     }
 
-    PipelineDoiData newDoiData(final String doiId, final String doiSuffix, final String title) {
-        PipelineDoiData doiData = getDoiData(doiId);
+    ProcessPipelineDoiData newDoiData(final String doiId, final String doiSuffix, final String title) {
+        ProcessPipelineDoiData doiData = getDoiData(doiId);
         // ensure unicity:
         if (doiData != null) {
             doiData.addError("Duplicate detected for DOI: " + doiSuffix);
             // ignore later
             doiData = null;
         } else {
-            doiData = new PipelineDoiData(doiId, doiSuffix, title);
+            doiData = new ProcessPipelineDoiData(doiId, doiSuffix, title);
             datas.put(doiId, doiData);
         }
         return doiData;
     }
 
-    PipelineDoiData getDoiData(final String doiId) {
+    ProcessPipelineDoiData getDoiData(final String doiId) {
         return datas.get(doiId);
     }
 
@@ -73,18 +67,7 @@ public final class PipelineGlobalData {
     }
 
     void addNameRef(final String[] cols) {
-        final String name = cols[1];
-        final NameEntry entry;
-        if (cols.length == 2) {
-            entry = new NameEntry(name);
-        } else {
-            final Map<String, String> attributes = new HashMap<String, String>();
-            for (int i = 2; i < cols.length; i += 2) {
-                attributes.put(cols[i], cols[i + 1]);
-            }
-            entry = new NameEntry(name, attributes);
-        }
-        nameRefs.add(entry);
+        nameRefs.add(NameEntry.fromCsv(cols));
     }
 
     public TreeSet<NameEntry> getNameRefs() {
diff --git a/src/main/java/fr/osug/doi/ProcessPipelineDoiData.java b/src/main/java/fr/osug/doi/ProcessPipelineDoiData.java
index 346e794..c9ceaa5 100644
--- a/src/main/java/fr/osug/doi/ProcessPipelineDoiData.java
+++ b/src/main/java/fr/osug/doi/ProcessPipelineDoiData.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * JMMC project ( http://www.jmmc.fr ) - Copyright (C) CNRS.
+ * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
  ******************************************************************************/
 package fr.osug.doi;
 
@@ -12,9 +12,8 @@ import org.slf4j.LoggerFactory;
 
 /**
  *
- * @author bourgesl
  */
-public final class PipelineDoiData {
+public final class ProcessPipelineDoiData {
 
     private final static Logger logger = LoggerFactory.getLogger(ProcessPipeline.class.getName());
 
@@ -31,10 +30,14 @@ public final class PipelineDoiData {
     private String metadataMd5;
     /** xml metadata */
     private File stagingFileDOI;
+    /* data access URL */
+    private String dataAccessUrl;
     /* external Landing page URL */
-    private String landingPageUrl;
+    private String landingPageExtUrl;
+    /* Local Landing page URL (staging) */
+    private String landingLocUrl;
 
-    PipelineDoiData(final String doiId, final String doiSuffix, final String title) {
+    ProcessPipelineDoiData(final String doiId, final String doiSuffix, final String title) {
         this.doiId = doiId;
         this.doiSuffix = doiSuffix;
         this.title = title;
@@ -115,12 +118,28 @@ public final class PipelineDoiData {
         this.stagingFileDOI = stagingFileDOI;
     }
 
-    public String getLandingPageUrl() {
-        return landingPageUrl;
+    public String getDataAccessUrl() {
+        return dataAccessUrl;
     }
 
-    public void setLandingPageUrl(final String landingPageUrl) {
-        this.landingPageUrl = landingPageUrl;
+    public void setDataAccessUrl(final String dataAccessUrl) {
+        this.dataAccessUrl = dataAccessUrl;
+    }
+
+    public String getLandingPageExtUrl() {
+        return landingPageExtUrl;
+    }
+
+    public void setLandingPageExtUrl(final String landingPageUrl) {
+        this.landingPageExtUrl = landingPageUrl;
+    }
+
+    public String getLandingLocUrl() {
+        return landingLocUrl;
+    }
+
+    public void setLandingLocUrl(final String landingLocUrl) {
+        this.landingLocUrl = landingLocUrl;
     }
 
 }
diff --git a/src/main/java/fr/osug/doi/ProjectConfig.java b/src/main/java/fr/osug/doi/ProjectConfig.java
index 3bfc08f..5fad004 100644
--- a/src/main/java/fr/osug/doi/ProjectConfig.java
+++ b/src/main/java/fr/osug/doi/ProjectConfig.java
@@ -3,16 +3,17 @@
  ******************************************************************************/
 package fr.osug.doi;
 
+import fr.osug.doi.domain.NameEntry;
+import fr.osug.doi.validation.ValidationUtil;
 import fr.osug.util.FileUtils;
 import java.io.File;
 import java.io.IOException;
 import java.io.Reader;
-import java.net.MalformedURLException;
-import java.net.URL;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Map;
 import java.util.Properties;
 import org.slf4j.Logger;
@@ -20,57 +21,64 @@ import org.slf4j.LoggerFactory;
 
 /**
  *
- * @author bourgesl
  */
 public final class ProjectConfig {
 
+    private final static Logger logger = LoggerFactory.getLogger(ProjectConfig.class.getName());
+
     public final static String CONF_PROJECT = "project.properties";
 
-    public final static String CONF_DOI_URL_MAPPING = "doi_url_landing_page.csv";
+    public final static String CONF_URL_MAP_DATA_ACCESS = "doi_url_data_access.csv";
+    public final static String CONF_URL_MAP_LANDING_PAGE = "doi_url_landing_page.csv";
 
-    public final static String CONF_KEY_DATA_ACCESS_URL = "dataAccessUrl";
-    public final static String CONF_KEY_GENERATE_EMBEDDED = "generate_embedded";
+    private final static String CONF_OVERRIDE_NAMES_CSV = "names_override" + Const.FILE_EXT_CSV;
 
-    private final static File[] EMPTY = new File[0];
+    /* project.properties */
+    /** use templates for countries = "use_template_country" */
+    public final static String CONF_KEY_USE_TEMPLATE_COUNTRY = "use_template_country";
+    /** use templates for each DOI = "use_template_doi" */
+    public final static String CONF_KEY_USE_TEMPLATE_DOI = "use_template_doi";
 
-    private final static Logger logger = LoggerFactory.getLogger(ProjectConfig.class.getName());
+    /** general data access url = "dataAccessUrl" */
+    public final static String CONF_KEY_DATA_ACCESS_URL = "dataAccessUrl";
+    /** generate embedded fragments = "generate_embedded" */
+    public final static String CONF_KEY_GENERATE_EMBEDDED = "generate_embedded";
 
+    /* members */
     private final String projectName;
     private final File projectConf;
 
-    private final Properties properties;
-
-    private final Map<String, String> landingPageUrlMapping;
-
-    private final DoiTemplates templates;
-    private File[] metadataCsvFiles = EMPTY;
-    private File[] inputCsvFiles = EMPTY;
+    // lazy:
+    private Properties properties = null;
+    private Map<String, String> urlMapDataAccess = null;
+    private Map<String, String> urlMapLandingPages = null;
+    // Directories:
+    private File metadataDir = null;
+    private File inputDir = null;
     private File tmpDir;
     private File tmpDoiDir;
     private File stagingDir;
-    private File webStagingDir;
+    private File publicDir;
     private File webStagingProjectDir;
     private File webStagingProjectEmbedDir;
     private File webStagingProjectXmlDir;
-    private File webPublicDir;
     private File webPublicProjectDir;
-
+    private File webPublicProjectEmbedDir;
+    private File webPublicProjectXmlDir;
+    // Process state:
+    private DoiTemplates templates = null;
+    private Map<String, NameEntry> overrideNameEntryMap = null;
     private boolean saveCSV = false;
 
-    public ProjectConfig(final String projectName) throws IOException {
+    public ProjectConfig(final PathConfig pathConfig, final String projectName) throws IOException {
         this.projectName = projectName;
 
         // may fail:
         this.projectConf = FileUtils.getRequiredDirectory(Paths.DIR_CONFIG + projectName).getCanonicalFile();
         logger.info("Project Config: {}", projectConf);
 
-        this.properties = loadProperties(projectConf);
-
-        this.landingPageUrlMapping = loadUrlMapping(new File(this.projectConf, CONF_DOI_URL_MAPPING));
-
-        this.templates = new DoiTemplates(
-                new File(this.projectConf, Paths.DIR_PROJECT_TEMPLATES).getAbsolutePath()
-        );
+        // preload properties to ensure the folder exists:
+        getProperties();
 
         initMetadataDir(
                 new File(this.projectConf, Paths.DIR_PROJECT_METADATA).getAbsolutePath()
@@ -88,11 +96,13 @@ public final class ProjectConfig {
         initStagingDir(
                 new File(Paths.DIR_STAGING, projectName).getAbsolutePath()
         );
+        initPublicDir(
+                new File(Paths.DIR_PUBLIC, projectName).getAbsolutePath()
+        );
 
         // /www/staging directories:
-        initWebStagingDir(Paths.DIR_WEB_STAGING);
         initWebStagingProjectDir(
-                new File(Paths.DIR_WEB_STAGING, projectName).getAbsolutePath()
+                new File(pathConfig.getWebStagingDir(), projectName).getAbsolutePath()
         );
         initWebStagingProjectEmbedDir(
                 new File(webStagingProjectDir, Paths.DIR_WEB_EMBED).getAbsolutePath()
@@ -101,19 +111,31 @@ public final class ProjectConfig {
                 new File(webStagingProjectDir, Paths.DIR_WEB_XML).getAbsolutePath()
         );
         // /www/public directories:
-        initWebPublicDir(Paths.DIR_WEB_PUBLIC);
         initWebPublicProjectDir(
-                new File(Paths.DIR_WEB_PUBLIC, projectName).getAbsolutePath()
+                new File(pathConfig.getWebPublicDir(), projectName).getAbsolutePath()
+        );
+        initWebPublicProjectEmbedDir(
+                new File(webPublicProjectDir, Paths.DIR_WEB_EMBED).getAbsolutePath()
         );
+        initWebPublicProjectXmlDir(
+                new File(webPublicProjectDir, Paths.DIR_WEB_XML).getAbsolutePath()
+        );
+    }
+
+    public void prepareProcess() throws IOException {
+        // preload needed data for the process pipeline:
+        getTemplates();
+        getUrlMapDataAccess();
+        getUrlMapLandingPage();
+        getOverrideNameMap();
     }
 
     public void initMetadataDir(final String dirPath) throws IOException {
-        this.metadataCsvFiles = CsvUtil.findCSVFiles(dirPath);
+        this.metadataDir = FileUtils.getRequiredDirectory(dirPath).getCanonicalFile();
     }
 
-    // Used by junit test
     public void initInputDir(final String dirPath) throws IOException {
-        this.inputCsvFiles = CsvUtil.findCSVFiles(dirPath);
+        this.inputDir = FileUtils.getRequiredDirectory(dirPath).getCanonicalFile();
     }
 
     public void initTmpDir(final String path) throws IOException {
@@ -128,8 +150,8 @@ public final class ProjectConfig {
         this.stagingDir = FileUtils.createDirectories(path);
     }
 
-    public void initWebStagingDir(final String path) throws IOException {
-        this.webStagingDir = FileUtils.createDirectories(path);
+    public void initPublicDir(final String path) throws IOException {
+        this.publicDir = FileUtils.createDirectories(path);
     }
 
     public void initWebStagingProjectDir(final String path) throws IOException {
@@ -144,14 +166,18 @@ public final class ProjectConfig {
         this.webStagingProjectXmlDir = FileUtils.createDirectories(path);
     }
 
-    public void initWebPublicDir(final String path) throws IOException {
-        this.webPublicDir = FileUtils.createDirectories(path);
-    }
-
     public void initWebPublicProjectDir(final String path) throws IOException {
         this.webPublicProjectDir = FileUtils.createDirectories(path);
     }
 
+    public void initWebPublicProjectEmbedDir(final String path) throws IOException {
+        this.webPublicProjectEmbedDir = FileUtils.createDirectories(path);
+    }
+
+    public void initWebPublicProjectXmlDir(final String path) throws IOException {
+        this.webPublicProjectXmlDir = FileUtils.createDirectories(path);
+    }
+
     public String getProjectName() {
         return projectName;
     }
@@ -160,32 +186,90 @@ public final class ProjectConfig {
         return projectConf;
     }
 
+    /* properties */
+    private Properties getProperties() {
+        if (properties == null) {
+            properties = loadProperties(new File(projectConf, CONF_PROJECT));
+        }
+        return properties;
+    }
+
     public String getProperty(final String key) {
-        final String val = properties.getProperty(key);
+        String val = getProperties().getProperty(key);
         if (val != null) {
-            return val.trim();
+            val = val.trim();
+            if (val.length() != 0) {
+                return val;
+            }
         }
         return null;
     }
 
     public boolean getPropertyBoolean(final String key) {
-        return Boolean.valueOf(properties.getProperty(key, "false"));
+        return getPropertyBoolean(key, false);
     }
 
-    public String getLandingPageUrl(final String doiSuffix) {
-        return landingPageUrlMapping.get(doiSuffix);
+    public boolean getPropertyBoolean(final String key, final boolean def) {
+        final String val = getProperties().getProperty(key);
+        if (val != null) {
+            return Boolean.valueOf(val);
+        }
+        return def;
+    }
+
+    /* DataAccess Url Mapping */
+    private Map<String, String> getUrlMapDataAccess() throws IOException {
+        if (urlMapDataAccess == null) {
+            urlMapDataAccess = CsvUtil.loadUrlMapping(new File(this.projectConf, CONF_URL_MAP_DATA_ACCESS));
+        }
+        return urlMapDataAccess;
+    }
+
+    public String getUrlDataAccess(final String doiSuffix) throws IOException {
+        return getUrlMapDataAccess().get(doiSuffix);
+    }
+
+    /* LandingPage Url Mapping */
+    private Map<String, String> getUrlMapLandingPage() throws IOException {
+        if (urlMapLandingPages == null) {
+            urlMapLandingPages = CsvUtil.loadUrlMapping(new File(this.projectConf, CONF_URL_MAP_LANDING_PAGE));
+        }
+        return urlMapLandingPages;
+    }
+
+    public String getUrlLandingPage(final String doiSuffix) throws IOException {
+        return getUrlMapLandingPage().get(doiSuffix);
     }
 
-    public DoiTemplates getTemplates() {
+    /* templates */
+    public DoiTemplates getTemplates() throws IOException {
+        if (templates == null) {
+            templates = new DoiTemplates(
+                    new File(this.projectConf, Paths.DIR_PROJECT_TEMPLATES).getAbsolutePath()
+            );
+        }
         return templates;
     }
 
-    public File[] getMetadataCsvFiles() {
-        return metadataCsvFiles;
+    /* names override */
+    private Map<String, NameEntry> getOverrideNameMap() throws IOException {
+        if (overrideNameEntryMap == null) {
+            overrideNameEntryMap = loadOverrideNames(new File(this.projectConf, CONF_OVERRIDE_NAMES_CSV));
+        }
+        return overrideNameEntryMap;
+    }
+
+    public NameEntry getOverrideNameEntry(final String simpleName) throws IOException {
+        return getOverrideNameMap().get(simpleName);
+    }
+
+    /* Directories */
+    public File getMetadataDir() {
+        return metadataDir;
     }
 
-    public File[] getInputCsvFiles() {
-        return inputCsvFiles;
+    public File getInputDir() {
+        return inputDir;
     }
 
     public File getTmpDir() {
@@ -200,8 +284,8 @@ public final class ProjectConfig {
         return stagingDir;
     }
 
-    public File getWebStagingDir() {
-        return webStagingDir;
+    public File getPublicDir() {
+        return publicDir;
     }
 
     public File getWebStagingProjectDir() {
@@ -216,14 +300,18 @@ public final class ProjectConfig {
         return webStagingProjectXmlDir;
     }
 
-    public File getWebPublicDir() {
-        return webPublicDir;
-    }
-
     public File getWebPublicProjectDir() {
         return webPublicProjectDir;
     }
 
+    public File getWebPublicProjectEmbedDir() {
+        return webPublicProjectEmbedDir;
+    }
+
+    public File getWebPublicProjectXmlDir() {
+        return webPublicProjectXmlDir;
+    }
+
     public boolean isSaveCSV() {
         return saveCSV;
     }
@@ -232,8 +320,7 @@ public final class ProjectConfig {
         this.saveCSV = saveCSV;
     }
 
-    private static Properties loadProperties(final File projectConf) {
-        final File propFile = new File(projectConf, CONF_PROJECT);
+    private static Properties loadProperties(final File propFile) {
         logger.info("Loading {}", propFile);
 
         final Properties props = new Properties();
@@ -250,44 +337,82 @@ public final class ProjectConfig {
         return props;
     }
 
-    private static Map<String, String> loadUrlMapping(final File csvFile) throws IOException {
-        final Map<String, String> urlMapping;
+    private static Map<String, NameEntry> loadOverrideNames(final File csvFile) throws IOException {
+        Map<String, NameEntry> nameEntryMap = Collections.emptyMap();
 
         if (csvFile.exists()) {
             final CsvData data = CsvUtil.read(csvFile);
             logger.debug("CSV data: {}", data);
 
             final List<String[]> rows = data.getRows();
-
-            urlMapping = new LinkedHashMap<String, String>(rows.size());
-
-            // Get mappings [DOI|URL]
-            for (ListIterator<String[]> it = rows.listIterator(); it.hasNext();) {
-                final String[] cols = it.next();
-                if (cols == null || cols.length != 2) {
-                    // remove line:
-                    it.remove();
-                    continue;
-                }
-                String key = cols[0].trim();
-                if (key.isEmpty() || key.charAt(0) == Const.COMMENT) {
-                    continue;
-                }
-                String url = cols[1].trim();
-                if (url.isEmpty() || !url.startsWith("http")) {
-                    continue;
-                }
-                try {
-                    // should detect repeated keys or values ?
-                    urlMapping.put(key, new URL(url).toString());
-                } catch (MalformedURLException mue) {
-                    logger.info("invalid URL: {}", url);
+            final int nbRows = rows.size();
+
+            if (nbRows > 1) {
+                // Parse header:
+                String[] keys = null;
+                String[] cols = rows.get(0);
+
+                if (cols == null || cols.length < 1) {
+                    logger.error("Missing header in {}", csvFile);
+                } else {
+                    keys = cols;
+                    // Check keys:
+                    for (int i = 0; i < keys.length; i++) {
+                        String key = keys[i];
+
+                        if (key.length() > 1) {
+                            if (key.charAt(0) == Const.COMMENT) {
+                                key = key.substring(1);
+                            }
+                            key = key.trim();
+                        }
+                        if (key.isEmpty()) {
+                            logger.error("Missing column name at {}", i);
+                            key = null; // ignore
+                        }
+                        keys[i] = key;
+                    }
+
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("keys: {}", Arrays.toString(keys));
+                    }
+
+                    nameEntryMap = new LinkedHashMap<String, NameEntry>(nbRows - 1);
+
+                    // Get mappings [Name|...]
+                    for (int i = 1; i < nbRows; i++) {
+                        cols = rows.get(i);
+
+                        if (cols == null || cols.length < 1) {
+                            continue;
+                        }
+                        String name = cols[0].trim();
+                        if (name.isEmpty() || name.charAt(0) == Const.COMMENT) {
+                            continue;
+                        }
+
+                        Map<String, String> attributes = null;
+
+                        if (cols.length > 1) {
+                            attributes = new HashMap<String, String>(4);
+
+                            for (int j = 1; j < cols.length; j++) {
+                                String key = keys[j];
+                                String val = cols[j];
+                                if (key != null && !val.trim().isEmpty()) {
+                                    attributes.put(key, val);
+                                }
+                            }
+                        }
+
+                        final String simpleName = ValidationUtil.simplifyName(name);
+
+                        nameEntryMap.put(simpleName, new NameEntry(name, attributes));
+                    }
                 }
             }
-        } else {
-            urlMapping = Collections.emptyMap();
         }
-        logger.info("urlMapping: {}", urlMapping);
-        return urlMapping;
+        logger.debug("nameEntryMap: {}", nameEntryMap);
+        return nameEntryMap;
     }
 }
diff --git a/src/main/java/fr/osug/doi/domain/Doi.java b/src/main/java/fr/osug/doi/domain/Doi.java
index 2cfd58c..2d509b0 100644
--- a/src/main/java/fr/osug/doi/domain/Doi.java
+++ b/src/main/java/fr/osug/doi/domain/Doi.java
@@ -26,7 +26,7 @@ public class Doi implements Persistable<Long> {
     private static final long serialVersionUID = 1L;
 
     @Id
-    @SequenceGenerator(name = "doi_id_seq", sequenceName = "doi_id_seq", allocationSize = 10)
+    @SequenceGenerator(name = "doi_id_seq", sequenceName = "doi_id_seq", initialValue = 1, allocationSize = 10)
     @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "doi_id_seq")
     private Long id;
 
@@ -36,12 +36,15 @@ public class Doi implements Persistable<Long> {
     @Column(name = "identifier", unique = true, nullable = false)
     private String identifier;
 
+    @Column(name = "description")
+    private String description;
+
     @Enumerated(EnumType.STRING)
     @Column(name = "status")
     private Status status = Status.STAGING;
 
-    @Column(name = "description")
-    private String description;
+    @Column(name = "data_access_url")
+    private String dataAccessUrl;
 
     @Column(name = "landing_ext_url")
     private String landingExtUrl;
@@ -92,6 +95,14 @@ public class Doi implements Persistable<Long> {
         this.status = status;
     }
 
+    public String getDataAccessUrl() {
+        return dataAccessUrl;
+    }
+
+    public void setDataAccessUrl(String dataAccessUrl) {
+        this.dataAccessUrl = dataAccessUrl;
+    }
+
     public String getLandingExtUrl() {
         return landingExtUrl;
     }
@@ -101,23 +112,28 @@ public class Doi implements Persistable<Long> {
     }
 
     @Override
-    public boolean equals(Object o) {
-        if (this == o) {
+    public int hashCode() {
+        int hash = 3;
+        hash = 17 * hash + Objects.hashCode(this.identifier);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
             return true;
         }
-        if (o == null || getClass() != o.getClass()) {
+        if (obj == null) {
             return false;
         }
-        Doi doi = (Doi) o;
-        if (doi.getId() == null || id == null) {
+        if (getClass() != obj.getClass()) {
             return false;
         }
-        return Objects.equals(id, doi.getId());
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(id);
+        final Doi other = (Doi) obj;
+        if (!Objects.equals(this.identifier, other.identifier)) {
+            return false;
+        }
+        return true;
     }
 
     @Override
@@ -128,6 +144,7 @@ public class Doi implements Persistable<Long> {
                 + ", identifier='" + identifier + "'"
                 + ", description='" + description + "'"
                 + ", status='" + status + "'"
+                + ", dataAccessUrl='" + dataAccessUrl + "'"
                 + ", landingExtUrl='" + landingExtUrl + "'"
                 + '}';
     }
diff --git a/src/main/java/fr/osug/doi/domain/DoiCommon.java b/src/main/java/fr/osug/doi/domain/DoiCommon.java
new file mode 100644
index 0000000..aba2e3c
--- /dev/null
+++ b/src/main/java/fr/osug/doi/domain/DoiCommon.java
@@ -0,0 +1,139 @@
+/*******************************************************************************
+ * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
+ ******************************************************************************/
+package fr.osug.doi.domain;
+
+import java.util.Date;
+import java.util.Objects;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Inheritance;
+import javax.persistence.InheritanceType;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToOne;
+import javax.persistence.SequenceGenerator;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+import org.springframework.data.domain.Persistable;
+
+/**
+ *
+ */
+@Entity
+@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
+public abstract class DoiCommon implements Persistable<Long> {
+
+    private static final long serialVersionUID = 1L;
+
+    @Id
+    @Column(name = "id")
+    @SequenceGenerator(name = "doi_common_id_seq", sequenceName = "doi_common_id_seq", initialValue = 1, allocationSize = 10)
+    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "doi_common_id_seq")
+    private Long id;
+
+    @OneToOne
+    @JoinColumn(name = "doi_id")
+    private Doi doi;
+
+    @Column(name = "metadata_md5")
+    private String metadataMd5;
+
+    @Column(name = "landing_loc_url")
+    private String landingLocUrl;
+
+    @Column(name = "create_date")
+    @Temporal(TemporalType.TIMESTAMP)
+    private Date createDate;
+
+    @Column(name = "update_date")
+    @Temporal(TemporalType.TIMESTAMP)
+    private Date updateDate;
+
+    @Override
+    public final Long getId() {
+        return id;
+    }
+
+    @Override
+    public final boolean isNew() {
+        return id == null;
+    }
+
+    public final Doi getDoi() {
+        return doi;
+    }
+
+    public final void setDoi(Doi doi) {
+        this.doi = doi;
+    }
+
+    public final String getMetadataMd5() {
+        return metadataMd5;
+    }
+
+    public final void setMetadataMd5(final String metadataMd5) {
+        this.metadataMd5 = metadataMd5;
+    }
+
+    public final String getLandingLocUrl() {
+        return landingLocUrl;
+    }
+
+    public final void setLandingLocUrl(final String landingLocUrl) {
+        this.landingLocUrl = landingLocUrl;
+    }
+
+    public final Date getCreateDate() {
+        return createDate;
+    }
+
+    public final void setCreateDate(Date createDate) {
+        this.createDate = createDate;
+    }
+
+    public final Date getUpdateDate() {
+        return updateDate;
+    }
+
+    public final void setUpdateDate(final Date updateDate) {
+        this.updateDate = updateDate;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 47 * hash + Objects.hashCode(this.doi);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final DoiCommon other = (DoiCommon) obj;
+        if (!Objects.equals(this.doi, other.doi)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "id=" + id
+                + ", doi=[" + doi + "]"
+                + ", metadataMd5='" + metadataMd5 + "'"
+                + ", landingLocUrl='" + landingLocUrl + "'"
+                + ", createDate='" + createDate + "'"
+                + ", updateDate='" + updateDate + "'";
+    }
+}
diff --git a/src/main/java/fr/osug/doi/domain/DoiPublic.java b/src/main/java/fr/osug/doi/domain/DoiPublic.java
index be8d3a4..1e1c59a 100644
--- a/src/main/java/fr/osug/doi/domain/DoiPublic.java
+++ b/src/main/java/fr/osug/doi/domain/DoiPublic.java
@@ -3,91 +3,22 @@
  ******************************************************************************/
 package fr.osug.doi.domain;
 
-import java.util.Date;
-import java.util.Objects;
 import javax.persistence.Column;
 import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.OneToOne;
-import javax.persistence.SequenceGenerator;
 import javax.persistence.Table;
-import javax.persistence.Temporal;
-import javax.persistence.TemporalType;
-import org.springframework.data.domain.Persistable;
 
 /**
  * A Doi in the public phase
  */
 @Entity
 @Table(name = "doi_public")
-public class DoiPublic implements Persistable<Long> {
+public class DoiPublic extends DoiCommon {
 
     private static final long serialVersionUID = 1L;
 
-    @Id
-    @Column(name = "doi_id")
-    @SequenceGenerator(name = "doi_public_id_seq", sequenceName = "doi_public_id_seq", allocationSize = 10)
-    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "doi_public_id_seq")
-    private Long id;
-
-    @OneToOne
-    @JoinColumn(name = "doi_id")
-    private Doi doi;
-
-    @Column(name = "metadata_md5")
-    private String metadataMd5;
-
-    @Column(name = "landing_loc_url")
-    private String landingLocUrl;
-
     @Column(name = "active_ext_url")
     private boolean activeExtUrl;
 
-    @Column(name = "create_date")
-    @Temporal(TemporalType.TIMESTAMP)
-    private Date createDate;
-
-    @Column(name = "update_date")
-    @Temporal(TemporalType.TIMESTAMP)
-    private Date updateDate;
-
-    @Override
-    public Long getId() {
-        return id;
-    }
-
-    @Override
-    public boolean isNew() {
-        return id == null;
-    }
-
-    public Doi getDoi() {
-        return doi;
-    }
-
-    public void setDoi(Doi doi) {
-        this.doi = doi;
-    }
-
-    public String getMetadataMd5() {
-        return metadataMd5;
-    }
-
-    public void setMetadataMd5(String metadataMd5) {
-        this.metadataMd5 = metadataMd5;
-    }
-
-    public String getLandingLocUrl() {
-        return landingLocUrl;
-    }
-
-    public void setLandingLocUrl(String landingLocUrl) {
-        this.landingLocUrl = landingLocUrl;
-    }
-
     public boolean isActiveExtUrl() {
         return activeExtUrl;
     }
@@ -96,50 +27,11 @@ public class DoiPublic implements Persistable<Long> {
         this.activeExtUrl = activeExtUrl;
     }
 
-    public Date getCreateDate() {
-        return createDate;
-    }
-
-    public void setCreateDate(Date createDate) {
-        this.createDate = createDate;
-    }
-
-    public Date getUpdateDate() {
-        return updateDate;
-    }
-
-    public void setUpdateDate(Date updateDate) {
-        this.updateDate = updateDate;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        DoiPublic doi = (DoiPublic) o;
-        if (doi.getId() == null || getId() == null) {
-            return false;
-        }
-        return Objects.equals(getId(), doi.getId());
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(getId());
-    }
-
     @Override
     public String toString() {
         return "DoiPublic{"
-                + "doi=" + doi
+                + super.toString()
                 + ", activeExtUrl='" + activeExtUrl + "'"
-                + ", landingLocUrl='" + landingLocUrl + "'"
-                + ", createDate='" + createDate + "'"
-                + ", updateDate='" + updateDate + "'"
                 + '}';
     }
 }
diff --git a/src/main/java/fr/osug/doi/domain/DoiStaging.java b/src/main/java/fr/osug/doi/domain/DoiStaging.java
index a06d8f1..b348a19 100644
--- a/src/main/java/fr/osug/doi/domain/DoiStaging.java
+++ b/src/main/java/fr/osug/doi/domain/DoiStaging.java
@@ -3,77 +3,25 @@
  ******************************************************************************/
 package fr.osug.doi.domain;
 
-import java.util.Date;
-import java.util.Objects;
 import javax.persistence.Column;
 import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.OneToOne;
-import javax.persistence.SequenceGenerator;
 import javax.persistence.Table;
-import javax.persistence.Temporal;
-import javax.persistence.TemporalType;
-import org.springframework.data.domain.Persistable;
 
 /**
  * A Doi in the staging phase
  */
 @Entity
 @Table(name = "doi_staging")
-public class DoiStaging implements Persistable<Long> {
+public class DoiStaging extends DoiCommon {
 
     private static final long serialVersionUID = 1L;
 
-    @Id
-    @SequenceGenerator(name = "doi_staging_id_seq", sequenceName = "doi_staging_id_seq", allocationSize = 10)
-    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "doi_staging_id_seq")
-    private Long id;
-
-    @OneToOne
-    @JoinColumn(name = "doi_id")
-    private Doi doi;
-
     @Column(name = "valid")
     private boolean valid;
 
-    @Column(name = "metadata_md5")
-    private String metadataMd5;
-
     @Column(name = "log")
     private String log;
 
-    @Column(name = "landing_loc_url")
-    private String landingLocUrl;
-
-    @Column(name = "create_date")
-    @Temporal(TemporalType.TIMESTAMP)
-    private Date createDate;
-
-    @Column(name = "update_date")
-    @Temporal(TemporalType.TIMESTAMP)
-    private Date updateDate;
-
-    @Override
-    public Long getId() {
-        return id;
-    }
-
-    @Override
-    public boolean isNew() {
-        return id == null;
-    }
-
-    public Doi getDoi() {
-        return doi;
-    }
-
-    public void setDoi(Doi doi) {
-        this.doi = doi;
-    }
-
     public boolean isValid() {
         return valid;
     }
@@ -82,14 +30,6 @@ public class DoiStaging implements Persistable<Long> {
         this.valid = valid;
     }
 
-    public String getMetadataMd5() {
-        return metadataMd5;
-    }
-
-    public void setMetadataMd5(String metadataMd5) {
-        this.metadataMd5 = metadataMd5;
-    }
-
     public String getLog() {
         return log;
     }
@@ -98,60 +38,11 @@ public class DoiStaging implements Persistable<Long> {
         this.log = log;
     }
 
-    public String getLandingLocUrl() {
-        return landingLocUrl;
-    }
-
-    public void setLandingLocUrl(String landingLocUrl) {
-        this.landingLocUrl = landingLocUrl;
-    }
-
-    public Date getCreateDate() {
-        return createDate;
-    }
-
-    public void setCreateDate(Date createDate) {
-        this.createDate = createDate;
-    }
-
-    public Date getUpdateDate() {
-        return updateDate;
-    }
-
-    public void setUpdateDate(Date updateDate) {
-        this.updateDate = updateDate;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        DoiStaging doi = (DoiStaging) o;
-        if (doi.getId() == null || getId() == null) {
-            return false;
-        }
-        return Objects.equals(getId(), doi.getId());
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(getId());
-    }
-
     @Override
     public String toString() {
         return "DoiStaging{"
-                + "id=" + id
-                + ", doi=" + doi
+                + super.toString()
                 + ", valid='" + valid + "'"
-                + ", metadataMd5='" + metadataMd5 + "'"
-                + ", landingLocUrl='" + landingLocUrl + "'"
-                + ", createDate='" + createDate + "'"
-                + ", updateDate='" + updateDate + "'"
                 + ", log=[\n" + log + "\n]"
                 + '}';
     }
diff --git a/src/main/java/fr/osug/doi/domain/IndexEntry.java b/src/main/java/fr/osug/doi/domain/IndexEntry.java
index c7f5db5..5867ffb 100644
--- a/src/main/java/fr/osug/doi/domain/IndexEntry.java
+++ b/src/main/java/fr/osug/doi/domain/IndexEntry.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * JMMC project ( http://www.jmmc.fr ) - Copyright (C) CNRS.
+ * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
  ******************************************************************************/
 package fr.osug.doi.domain;
 
@@ -7,7 +7,6 @@ import java.util.Date;
 
 /**
  *
- * @author bourgesl
  */
 public final class IndexEntry {
 
diff --git a/src/main/java/fr/osug/doi/domain/NameEntry.java b/src/main/java/fr/osug/doi/domain/NameEntry.java
index 478b863..92d33f1 100644
--- a/src/main/java/fr/osug/doi/domain/NameEntry.java
+++ b/src/main/java/fr/osug/doi/domain/NameEntry.java
@@ -1,15 +1,16 @@
 /*******************************************************************************
- * JMMC project ( http://www.jmmc.fr ) - Copyright (C) CNRS.
+ * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
  ******************************************************************************/
 package fr.osug.doi.domain;
 
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Objects;
 
 /**
  *
- * @author bourgesl
  */
 public final class NameEntry {
 
@@ -26,15 +27,42 @@ public final class NameEntry {
         }
     };
 
-    private final String name;
-    private final Map<String, String> attributes;
+    public static NameEntry fromCsv(final String[] cols) {
+        final String name = cols[1];
+        final NameEntry entry;
+        if (cols.length == 2) {
+            entry = new NameEntry(name, null);
+        } else {
+            final Map<String, String> attributes = new HashMap<String, String>(4);
+            for (int i = 2; i < cols.length; i += 2) {
+                attributes.put(cols[i], cols[i + 1]);
+            }
+            entry = new NameEntry(name, attributes);
+        }
+        return entry;
+    }
 
-    public NameEntry(String name) {
-        this.name = name;
-        this.attributes = null;
+    public static String[] toCsv(final NameEntry entry) {
+        if (!entry.hasAttributes()) {
+            return new String[]{null, entry.getName()};
+        }
+        final Map<String, String> attributes = entry.getAttributes();
+        final String[] cols = new String[(1 + attributes.size()) * 2];
+        cols[1] = entry.getName();
+        int i = 2;
+        for (Entry<String, String> e : attributes.entrySet()) {
+            cols[i] = e.getKey();
+            cols[i + 1] = e.getValue();
+            i += 2;
+        }
+        return cols;
     }
 
-    public NameEntry(String name, Map<String, String> attributes) {
+    // members:
+    private final String name;
+    private final Map<String, String> attributes;
+
+    public NameEntry(final String name, final Map<String, String> attributes) {
         this.name = name;
         this.attributes = attributes;
     }
diff --git a/src/main/java/fr/osug/doi/domain/Project.java b/src/main/java/fr/osug/doi/domain/Project.java
index c43210b..e202af3 100644
--- a/src/main/java/fr/osug/doi/domain/Project.java
+++ b/src/main/java/fr/osug/doi/domain/Project.java
@@ -26,7 +26,7 @@ public class Project implements Persistable<Long> {
     private static final long serialVersionUID = 1L;
 
     @Id
-    @SequenceGenerator(name = "project_id_seq", sequenceName = "project_id_seq", allocationSize = 1)
+    @SequenceGenerator(name = "project_id_seq", sequenceName = "project_id_seq", initialValue = 1, allocationSize = 10)
     @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "project_id_seq")
     private Long id;
 
@@ -102,23 +102,28 @@ public class Project implements Persistable<Long> {
     }
 
     @Override
-    public boolean equals(Object o) {
-        if (this == o) {
+    public int hashCode() {
+        int hash = 7;
+        hash = 97 * hash + Objects.hashCode(this.name);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
             return true;
         }
-        if (o == null || getClass() != o.getClass()) {
+        if (obj == null) {
             return false;
         }
-        Project project = (Project) o;
-        if (project.getId() == null || id == null) {
+        if (getClass() != obj.getClass()) {
             return false;
         }
-        return Objects.equals(id, project.getId());
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(id);
+        final Project other = (Project) obj;
+        if (!Objects.equals(this.name, other.name)) {
+            return false;
+        }
+        return true;
     }
 
     @Override
diff --git a/src/main/java/fr/osug/doi/domain/model/dbmodel.jpa b/src/main/java/fr/osug/doi/domain/model/dbmodel.jpa
index 2feb2c4..39d6ec2 100644
--- a/src/main/java/fr/osug/doi/domain/model/dbmodel.jpa
+++ b/src/main/java/fr/osug/doi/domain/model/dbmodel.jpa
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<jpa:entity-mappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:java="http://jcp.org/en/jsr/detail?id=270" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jpa="http://java.sun.com/xml/ns/persistence/orm" v="2.6.5" status="GENERATED" sm="false" xs="false" id="_1481192659941462" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_1.xsd">
-    <jpa:entity xre="false" abs="false" class="Project" visibile="true" minimized="false" rootElement="_1481192659941462" id="_1481192659941463">
+<jpa:entity-mappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:java="http://jcp.org/en/jsr/detail?id=270" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jpa="http://java.sun.com/xml/ns/persistence/orm" v="2.6.5" sm="false" xs="false" id="_1481192659941462" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_1.xsd">
+    <jpa:entity xre="false" compositePrimaryKeyClass="ProjectPK" abs="false" class="Project" visibile="true" minimized="false" rootElement="_1481192659941462" id="_1481192659941463">
         <jpa:attributes>
             <jpa:basic optional="false" attribute-type="String" visibile="true" name="name" id="_1481192659941468">
                 <jpa:column name="name" unique="false" nullable="false" insertable="true" updatable="true" table="project" length="32"/>
@@ -19,7 +19,7 @@
                 <jpa:temporal>TIMESTAMP</jpa:temporal>
                 <jpa:column name="update_date" unique="false" nullable="true" insertable="true" updatable="true" table="project"/>
             </jpa:basic>
-            <jpa:one-to-many own="false" connected-entity-id="_1481192659941464" connected-attribute-id="_1481192659941479" visibile="true" name="doiCollection" id="_1481192659941473"/>
+            <jpa:one-to-many own="false" collection-type="java.util.List" connected-entity-id="_1481192659941464" connected-attribute-id="_1481192659941479" visibile="true" name="doiCollection" id="_1481192659941473"/>
             <jpa:id attribute-type="Long" visibile="true" name="id" id="_1481192659941467">
                 <jpa:column name="id" unique="false" nullable="false" insertable="true" updatable="true" table="project"/>
             </jpa:id>
@@ -30,7 +30,7 @@
             </jpa:unique-constraint>
         </jpa:table>
     </jpa:entity>
-    <jpa:entity xre="false" abs="false" class="Doi" visibile="true" minimized="false" rootElement="_1481192659941462" id="_1481192659941464">
+    <jpa:entity xre="false" compositePrimaryKeyClass="DoiPK" abs="false" class="Doi" visibile="true" minimized="false" rootElement="_1481192659941462" id="_1481192659941464">
         <jpa:attributes>
             <jpa:basic optional="false" attribute-type="String" visibile="true" name="identifier" id="_1481192659941475">
                 <jpa:column name="identifier" unique="false" nullable="false" insertable="true" updatable="true" table="doi" length="255"/>
@@ -44,11 +44,11 @@
             <jpa:basic optional="true" attribute-type="String" visibile="true" name="landingExtUrl" id="_1481192659941478">
                 <jpa:column name="landing_ext_url" unique="false" nullable="true" insertable="true" updatable="true" table="doi" length="1024"/>
             </jpa:basic>
-            <jpa:many-to-one optional="false" connected-entity-id="_1481192659941463" connected-attribute-id="_1481192659941473" visibile="true" name="projectId" id="_1481192659941479">
+            <jpa:many-to-one optional="false" primaryKey="false" connected-entity-id="_1481192659941463" connected-attribute-id="_1481192659941473" visibile="true" name="projectId" id="_1481192659941479">
                 <jpa:join-column name="PROJECT_ID" rc="ID" unique="false" nullable="true" insertable="true" updatable="true"/>
             </jpa:many-to-one>
-            <jpa:one-to-many own="false" connected-entity-id="_1481192659941465" connected-attribute-id="_1481192659941488" visibile="true" name="doiPublicCollection" id="_1481192659941480"/>
-            <jpa:one-to-many own="false" connected-entity-id="_1481192659941466" connected-attribute-id="_1481192659941496" visibile="true" name="doiStagingCollection" id="_1481192659941481"/>
+            <jpa:one-to-many own="false" collection-type="java.util.List" connected-entity-id="_1481192659941465" connected-attribute-id="_1481192659941488" visibile="true" name="doiPublicCollection" id="_1481192659941480"/>
+            <jpa:one-to-many own="false" collection-type="java.util.List" connected-entity-id="_1481192659941466" connected-attribute-id="_1481192659941496" visibile="true" name="doiStagingCollection" id="_1481192659941481"/>
             <jpa:id attribute-type="Long" visibile="true" name="id" id="_1481192659941474">
                 <jpa:column name="id" unique="false" nullable="false" insertable="true" updatable="true" table="doi"/>
             </jpa:id>
@@ -59,7 +59,7 @@
             </jpa:unique-constraint>
         </jpa:table>
     </jpa:entity>
-    <jpa:entity xre="false" abs="false" class="DoiPublic" visibile="true" minimized="false" rootElement="_1481192659941462" id="_1481192659941465">
+    <jpa:entity xre="false" compositePrimaryKeyClass="DoiPublicPK" abs="false" class="DoiPublic" visibile="true" minimized="false" rootElement="_1481192659941462" id="_1481192659941465">
         <jpa:attributes>
             <jpa:basic optional="false" attribute-type="String" visibile="true" name="metadataMd5" id="_1481192659941483">
                 <jpa:column name="metadata_md5" unique="false" nullable="false" insertable="true" updatable="true" table="doi_public" length="32"/>
@@ -78,7 +78,7 @@
                 <jpa:temporal>TIMESTAMP</jpa:temporal>
                 <jpa:column name="update_date" unique="false" nullable="true" insertable="true" updatable="true" table="doi_public"/>
             </jpa:basic>
-            <jpa:many-to-one optional="false" connected-entity-id="_1481192659941464" connected-attribute-id="_1481192659941480" visibile="true" name="doiId" id="_1481192659941488">
+            <jpa:many-to-one optional="false" primaryKey="false" connected-entity-id="_1481192659941464" connected-attribute-id="_1481192659941480" visibile="true" name="doiId" id="_1481192659941488">
                 <jpa:join-column name="DOI_ID" rc="ID" unique="false" nullable="true" insertable="true" updatable="true"/>
             </jpa:many-to-one>
             <jpa:id attribute-type="Long" visibile="true" name="id" id="_1481192659941482">
@@ -87,7 +87,7 @@
         </jpa:attributes>
         <jpa:table name="doi_public"/>
     </jpa:entity>
-    <jpa:entity xre="false" abs="false" class="DoiStaging" visibile="true" minimized="false" rootElement="_1481192659941462" id="_1481192659941466">
+    <jpa:entity xre="false" compositePrimaryKeyClass="DoiStagingPK" abs="false" class="DoiStaging" visibile="true" minimized="false" rootElement="_1481192659941462" id="_1481192659941466">
         <jpa:attributes>
             <jpa:basic optional="true" attribute-type="Boolean" visibile="true" name="valid" id="_1481192659941490">
                 <jpa:column name="valid" unique="false" nullable="true" insertable="true" updatable="true" table="doi_staging"/>
@@ -109,7 +109,7 @@
                 <jpa:temporal>TIMESTAMP</jpa:temporal>
                 <jpa:column name="update_date" unique="false" nullable="true" insertable="true" updatable="true" table="doi_staging"/>
             </jpa:basic>
-            <jpa:many-to-one optional="false" connected-entity-id="_1481192659941464" connected-attribute-id="_1481192659941481" visibile="true" name="doiId" id="_1481192659941496">
+            <jpa:many-to-one optional="false" primaryKey="false" connected-entity-id="_1481192659941464" connected-attribute-id="_1481192659941481" visibile="true" name="doiId" id="_1481192659941496">
                 <jpa:join-column name="DOI_ID" rc="ID" unique="false" nullable="true" insertable="true" updatable="true"/>
             </jpa:many-to-one>
             <jpa:id attribute-type="Long" visibile="true" name="id" id="_1481192659941489">
@@ -122,7 +122,21 @@
  * This file was generated by the JPA Modeler
  */</jpa:snp>
     <jpa:inf e="true" n="java.io.Serializable"/>
+    <jpa:c/>
     <jpa:diagram>
-        <plane elementRef="_1481192659941462"/>
+        <plane elementRef="_1481192659941462">
+            <shape elementRef="_1481192659941463">
+                <Bounds x="32.0" y="576.0" width="146.0" height="198.0"/>
+            </shape>
+            <shape elementRef="_1481192659941464">
+                <Bounds x="257.0" y="294.0" width="185.0" height="218.0"/>
+            </shape>
+            <shape elementRef="_1481192659941465">
+                <Bounds x="32.0" y="32.0" width="161.0" height="198.0"/>
+            </shape>
+            <shape elementRef="_1481192659941466">
+                <Bounds x="506.0" y="576.0" width="174.0" height="218.0"/>
+            </shape>
+        </plane>
     </jpa:diagram>
 </jpa:entity-mappings>
diff --git a/src/main/java/fr/osug/doi/repository/DoiBaseRepository.java b/src/main/java/fr/osug/doi/repository/DoiBaseRepository.java
index f4a91f1..fd73914 100644
--- a/src/main/java/fr/osug/doi/repository/DoiBaseRepository.java
+++ b/src/main/java/fr/osug/doi/repository/DoiBaseRepository.java
@@ -4,34 +4,29 @@
 package fr.osug.doi.repository;
 
 import fr.osug.doi.domain.Doi;
-import fr.osug.doi.domain.DoiStaging;
+import fr.osug.doi.domain.DoiCommon;
 import java.util.List;
-import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.NoRepositoryBean;
+import org.springframework.data.repository.Repository;
 import org.springframework.data.repository.query.Param;
-import org.springframework.stereotype.Repository;
 import org.springframework.transaction.annotation.Transactional;
 
 /**
- * Spring Data JPA repository for the DoiStaging entity.
+ * Spring Data JPA repository for the DoiCommon classes.
  */
-@Repository
-@Transactional
-public interface DoiStagingRepository extends JpaRepository<DoiStaging, Long> {
+@NoRepositoryBean
+public interface DoiBaseRepository<T extends DoiCommon> extends Repository<T, Long> {
 
     @Transactional(readOnly = true)
-    DoiStaging findOneByDoi(Doi doi);
+    T findOneByDoi(Doi doi);
 
     @Transactional(readOnly = true)
-    @Query("select ds from DoiStaging ds join ds.doi d join d.project p where (p.name = :projectName) order by d.identifier asc")
-    List<DoiStaging> findByProject(@Param("projectName") String projectName);
-    
-    @Transactional(readOnly = true)
-    @Query("select ds from DoiStaging ds join ds.doi d join d.project p where (p.name = :projectName) and ds.valid = false order by d.identifier asc")
-    List<DoiStaging> findErrorByProject(@Param("projectName") String projectName);
+    @Query("select ds from #{#entityName} ds join ds.doi d join d.project p where (p.name = :projectName) order by d.identifier asc")
+    List<T> findByProject(@Param("projectName") String projectName);
 
     @Transactional(readOnly = true)
-    @Query("select count(ds) from DoiStaging ds join ds.doi d join d.project p where (p.name = :projectName)")
+    @Query("select count(ds) from #{#entityName} ds join ds.doi d join d.project p where (p.name = :projectName)")
     Long countByProject(@Param("projectName") String projectName);
 
 }
diff --git a/src/main/java/fr/osug/doi/repository/DoiPublicRepository.java b/src/main/java/fr/osug/doi/repository/DoiPublicRepository.java
index 6cc0897..0d73034 100644
--- a/src/main/java/fr/osug/doi/repository/DoiPublicRepository.java
+++ b/src/main/java/fr/osug/doi/repository/DoiPublicRepository.java
@@ -3,28 +3,16 @@
  ******************************************************************************/
 package fr.osug.doi.repository;
 
-import fr.osug.doi.domain.Doi;
 import fr.osug.doi.domain.DoiPublic;
-import fr.osug.doi.domain.DoiStaging;
-import java.util.List;
 import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
 import org.springframework.stereotype.Repository;
 import org.springframework.transaction.annotation.Transactional;
 
 /**
- * Spring Data JPA repository for the DoiStaging entity.
+ * Spring Data JPA repository for the DoiPublic entity.
  */
 @Repository
 @Transactional
-public interface DoiPublicRepository extends JpaRepository<DoiPublic, Long> {
-
-    @Transactional(readOnly = true)
-    DoiPublic findOneByDoi(Doi doi);
-
-    @Transactional(readOnly = true)
-    @Query("select dp from DoiPublic dp join dp.doi d join d.project p where (p.name = :projectName) order by d.identifier asc")
-    List<DoiPublic> findByProject(@Param("projectName") String projectName);
+public interface DoiPublicRepository extends DoiBaseRepository<DoiPublic>, JpaRepository<DoiPublic, Long> {
 
 }
diff --git a/src/main/java/fr/osug/doi/repository/DoiStagingRepository.java b/src/main/java/fr/osug/doi/repository/DoiStagingRepository.java
index f4a91f1..6c4f3ee 100644
--- a/src/main/java/fr/osug/doi/repository/DoiStagingRepository.java
+++ b/src/main/java/fr/osug/doi/repository/DoiStagingRepository.java
@@ -3,7 +3,6 @@
  ******************************************************************************/
 package fr.osug.doi.repository;
 
-import fr.osug.doi.domain.Doi;
 import fr.osug.doi.domain.DoiStaging;
 import java.util.List;
 import org.springframework.data.jpa.repository.JpaRepository;
@@ -17,21 +16,10 @@ import org.springframework.transaction.annotation.Transactional;
  */
 @Repository
 @Transactional
-public interface DoiStagingRepository extends JpaRepository<DoiStaging, Long> {
+public interface DoiStagingRepository extends DoiBaseRepository<DoiStaging>, JpaRepository<DoiStaging, Long> {
 
-    @Transactional(readOnly = true)
-    DoiStaging findOneByDoi(Doi doi);
-
-    @Transactional(readOnly = true)
-    @Query("select ds from DoiStaging ds join ds.doi d join d.project p where (p.name = :projectName) order by d.identifier asc")
-    List<DoiStaging> findByProject(@Param("projectName") String projectName);
-    
     @Transactional(readOnly = true)
     @Query("select ds from DoiStaging ds join ds.doi d join d.project p where (p.name = :projectName) and ds.valid = false order by d.identifier asc")
     List<DoiStaging> findErrorByProject(@Param("projectName") String projectName);
 
-    @Transactional(readOnly = true)
-    @Query("select count(ds) from DoiStaging ds join ds.doi d join d.project p where (p.name = :projectName)")
-    Long countByProject(@Param("projectName") String projectName);
-
 }
diff --git a/src/main/java/fr/osug/doi/repository/ProjectRepository.java b/src/main/java/fr/osug/doi/repository/ProjectRepository.java
index 72510ec..77f71f4 100644
--- a/src/main/java/fr/osug/doi/repository/ProjectRepository.java
+++ b/src/main/java/fr/osug/doi/repository/ProjectRepository.java
@@ -6,6 +6,7 @@ package fr.osug.doi.repository;
 import fr.osug.doi.domain.Project;
 import java.util.List;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
 
 import org.springframework.stereotype.Repository;
 import org.springframework.transaction.annotation.Transactional;
@@ -23,4 +24,8 @@ public interface ProjectRepository extends JpaRepository<Project, Long> {
     @Transactional(readOnly = true)
     List<Project> findAllByOrderByNameAsc();
 
+    @Transactional(readOnly = true)
+    @Query("select p.name from Project p order by p.name asc")
+    List<String> findAllNamesOrderByNameAsc();
+    
 }
diff --git a/src/main/java/fr/osug/doi/service/DataciteClient.java b/src/main/java/fr/osug/doi/service/DataciteClient.java
new file mode 100644
index 0000000..1afaac5
--- /dev/null
+++ b/src/main/java/fr/osug/doi/service/DataciteClient.java
@@ -0,0 +1,242 @@
+/*******************************************************************************
+ * OSUG-DOI project ( http://doi.osug.fr ) - Copyright (C) CNRS.
+ ******************************************************************************/
+package fr.osug.doi.service;
+
+import fr.osug.doi.Const;
+import fr.osug.doi.DataciteConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestClientException;
+
+/**
+ *
+ */
+@Service
+public class DataciteClient {
+
+    private final static Logger logger = LoggerFactory.getLogger(DataciteConfig.class.getName());
+
+    public final static boolean RESTRICT_POST_TO_TEST_PREFIX = true;
+
+    public final static int TIMEOUT_CONNECT = 10000;
+    public final static int TIMEOUT_READ = 30000;
+
+    public final static String BASE_URL = "https://mds.datacite.org";
+    /* specific content-types */
+    public final static String PLAIN_UTF8 = "text/plain;charset=UTF-8";
+    public final static String XML_UTF8 = "application/xml;charset=UTF-8";
+
+    private final RestTemplate restTemplate;
+
+    public DataciteClient(RestTemplateBuilder restTemplateBuilder) {
+        this.restTemplate = restTemplateBuilder.rootUri(BASE_URL)
+                .setConnectTimeout(TIMEOUT_CONNECT)
+                .setReadTimeout(TIMEOUT_READ)
+                .build();
+    }
+
+    /**
+        GET (list all DOIs)
+        URI: https://mds.datacite.org/doi
+
+        This request returns a list of all DOIs for the requesting datacentre. There is no guaranteed order.
+
+        Response body
+        If response status is 200: list of DOIs, one DOI per line; empty for 204
+
+        Response statuses
+        200 OK - operation successful
+        204 No Content - no DOIs founds
+    
+        @return 
+     */
+    public String getDois() {
+        String response = null;
+        try {
+            response = this.restTemplate.getForObject("/doi", String.class);
+        } catch (RestClientException rce) {
+            logger.error("getDois: failed", rce);
+        }
+        logger.debug("getDois:\n{}", response);
+        return response;
+    }
+
+    /**
+    Publish a DOI
+    
+    @param doi
+    @param url
+    @param metadata
+    @return 
+     */
+    public boolean publishDoi(final String doi, final String url, final String metadata) {
+        // 1. Metadata
+        final String oldMetadata = getMetadataForDoi(doi);
+
+        if (oldMetadata == null || !oldMetadata.equals(metadata)) {
+            postMetadataForDoi(doi, metadata);
+        }
+
+        // 2. create / update DOI:
+        return setDoi(doi, url);
+    }
+
+    /**
+        POST
+        URI: https://mds.datacite.org/doi
+
+        POST will mint new DOI if specified DOI doesn't exist. 
+        This method will attempt to update URL if you specify existing DOI. 
+        A new record in Datasets will be created.
+        
+        Request headers
+        Content-Type:text/plain;charset=UTF-8
+
+        Request body
+        doi={doi}
+        url={url}
+        where {doi} and {url} have to be replaced by your DOI and URL, UFT-8 encoded.
+        
+        Response body
+        short explanation of status code e.g. CREATED, HANDLE_ALREADY_EXISTS etc
+
+        Response statuses
+        201 Created - operation successful
+        400 Bad Request - request body must be exactly two lines: DOI and URL; wrong domain, wrong prefix
+        401 Unauthorized - no login
+        403 Forbidden - login problem, quota exceeded
+        412 Precondition failed - metadata must be uploaded first
+        500 Internal Server Error - server internal error, try later and if problem persists please contact us
+    
+        @param doi
+        @param url
+        @return 
+     */
+    public boolean setDoi(final String doi, final String url) {
+        if (RESTRICT_POST_TO_TEST_PREFIX && !doi.startsWith(Const.DOI_PREFIX_TEST)) {
+            throw new IllegalStateException("Only Test prefix is allowed !");
+        }
+        logger.debug("setDoi: doi: {} url: {}", doi, url);
+
+        final HttpHeaders headers = new HttpHeaders();
+        headers.set(HttpHeaders.CONTENT_TYPE, PLAIN_UTF8);
+
+        final String parameters = "doi=" + doi + "\nurl=" + url;
+        logger.debug("parameters: {}", parameters);
+
+        final HttpEntity<String> request = new HttpEntity<String>(parameters, headers);
+
+        boolean result = false;
+        try {
+            final String response = this.restTemplate.postForObject("/doi", request, String.class);
+            logger.debug("setDoi: {}", response);
+            result = true;
+        } catch (RestClientException rce) {
+            logger.error("setDoi: failed", rce);
+        }
+        return result;
+    }
+
+    /* Metadata API */
+    /**
+    GET
+    URI: https://mds.datacite.org/metadata/{doi} where {doi} is a specific DOI.
+
+    This request returns the most recent version of metadata associated with a given DOI.
+
+    Request headers
+    Accept:application/xml
+
+    Response headers
+    Content-Type:application/xml
+
+    Response body
+    If response status is 200: XML representing a dataset, otherwise short explanation for non-200 status
+
+    Response statuses
+    200 OK - operation successful
+    401 Unauthorized - no login
+    403 Forbidden - login problem or dataset belongs to another party
+    404 Not Found - DOI does not exist in our database
+    410 Gone - the requested dataset was marked inactive (using DELETE method)
+    500 Internal Server Error - server internal error, try later and if problem persists please contact us
+    
+    @param doi
+    @return 
+     */
+    public String getMetadataForDoi(final String doi) {
+        String response = null;
+        try {
+            response = this.restTemplate.getForObject("/metadata/{doi}", String.class, doi);
+        } catch (HttpClientErrorException hcee) {
+            if (hcee.getStatusCode() == HttpStatus.NOT_FOUND) {
+                logger.debug("getMetadataForDoi: failed", hcee);
+            } else {
+                logger.error("getMetadataForDoi: failed", hcee);
+            }
+        } catch (RestClientException rce) {
+            logger.error("getMetadataForDoi: failed", rce);
+        }
+        logger.debug("getMetadataForDoi:\n{}", response);
+        return response;
+    }
+
+    /**
+    POST
+    URI: https://mds.datacite.org/metadata
+
+    This request stores new version of metadata. The request body must contain valid XML.
+
+    Request headers
+    Content-Type:application/xml;charset=UTF-8
+
+    Request body
+    UFT-8 encoded metadata
+
+    Response body
+    short explanation of status code e.g. CREATED, HANDLE_ALREADY_EXISTS etc
+
+    Response headers
+    Location - URL of the newly stored metadata
+
+    Response statuses
+    201 Created - operation successful
+    400 Bad Request - invalid XML, wrong prefix
+    401 Unauthorized - no login
+    403 Forbidden - login problem, quota exceeded
+    500 Internal Server Error - server internal error, try later and if problem persists please contact us
+    
+    @param doi
+    @param metadata
+    @return 
+     */
+    public boolean postMetadataForDoi(final String doi, final String metadata) {
+        if (RESTRICT_POST_TO_TEST_PREFIX && !doi.startsWith(Const.DOI_PREFIX_TEST)) {
+            throw new IllegalStateException("Only Test prefix is allowed !");
+        }
+        logger.debug("postMetadataForDoi: metadata: {}", metadata);
+
+        final HttpHeaders headers = new HttpHeaders();
+        headers.set(HttpHeaders.CONTENT_TYPE, XML_UTF8);
+
+        final HttpEntity<String> request = new HttpEntity<String>(metadata, headers);
+
+        boolean result = false;
+        try {
+            final String response = this.restTemplate.postForObject("/metadata", request, String.class);
+            logger.debug("postMetadataForDoi: {}", response);
+            result = true;
+        } catch (RestClientException rce) {
+            logger.error("postMetadataForDoi: failed", rce);
+        }
+        return result;
+    }
+}
diff --git a/src/main/java/fr/osug/doi/service/DoiService.java b/src/main/java/fr/osug/doi/service/DoiService.java
index 7654513..c393c35 100644
--- a/src/main/java/fr/osug/doi/service/DoiService.java
+++ b/src/main/java/fr/osug/doi/service/DoiService.java
@@ -3,15 +3,18 @@
  ******************************************************************************/
 package fr.osug.doi.service;
 
+import fr.osug.doi.ProcessPipelineDoiData;
 import fr.osug.doi.domain.Doi;
 import fr.osug.doi.domain.DoiPublic;
 import fr.osug.doi.domain.DoiStaging;
 import fr.osug.doi.domain.Project;
 import fr.osug.doi.domain.Status;
+import fr.osug.doi.repository.DoiBaseRepository;
 import fr.osug.doi.repository.DoiPublicRepository;
 import fr.osug.doi.repository.DoiRepository;
 import fr.osug.doi.repository.DoiStagingRepository;
 import fr.osug.doi.repository.ProjectRepository;
+import java.util.Collection;
 import java.util.Date;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -25,7 +28,7 @@ import org.springframework.transaction.annotation.Transactional;
 @Service
 public class DoiService {
 
-    private final Logger log = LoggerFactory.getLogger(DoiService.class);
+    private final static Logger logger = LoggerFactory.getLogger(DoiService.class);
 
     @Autowired
     private ProjectRepository projectRepository;
@@ -39,6 +42,28 @@ public class DoiService {
     @Autowired
     private DoiPublicRepository doiPublicRepository;
 
+    @Transactional
+    public void importDoiStagingCollection(final String projectName, final Date now,
+                                           final Collection<ProcessPipelineDoiData> doiDataCollection) {
+
+        // Get or create the project:
+        final Project project = getOrCreateProject(projectName, now);
+
+        // Update Dois:
+        for (ProcessPipelineDoiData doiData : doiDataCollection) {
+            // Update DOI in staging phase:
+            createOrUpdateStagingDoi(project,
+                    doiData.getDoiSuffix(), doiData.getTitle(),
+                    doiData.getDataAccessUrl(), doiData.getLandingPageExtUrl(),
+                    doiData.isValid(), doiData.getMetadataMd5(),
+                    doiData.messagesToString(), now,
+                    doiData.getLandingLocUrl());
+        }
+
+        // Update Project:
+        updateProjectDate(projectName, now);
+    }
+
     @Transactional
     public Project getOrCreateProject(final String name, final Date now) {
         Project project = projectRepository.findOneByName(name);
@@ -50,7 +75,7 @@ public class DoiService {
             project.setCreateDate(now);
             project.setUpdateDate(now);
 
-            log.debug("saving: {}", project);
+            logger.debug("saving: {}", project);
 
             projectRepository.save(project);
         }
@@ -67,7 +92,7 @@ public class DoiService {
 
         project.setUpdateDate(now);
 
-        log.debug("saving: {}", project);
+        logger.debug("saving: {}", project);
 
         projectRepository.save(project);
     }
@@ -79,13 +104,8 @@ public class DoiService {
 
     @Transactional
     public Doi createOrUpdateDoi(final Project project, final String identifier,
-                                 final String description, final String landingExtUrl) {
-        return createOrUpdateDoi(project, identifier, Status.STAGING, description, landingExtUrl);
-    }
-
-    @Transactional
-    public Doi createOrUpdateDoi(final Project project, final String identifier,
-                                 final Status status, final String description, final String landingExtUrl) {
+                                 final Status status, final String description,
+                                 final String dataAccessUrl, final String landingExtUrl) {
 
         Doi doi = doiRepository.findOneByIdentifier(identifier);
 
@@ -95,11 +115,15 @@ public class DoiService {
             doi.setIdentifier(identifier);
         }
 
-        doi.setStatus(status);
+        // only update status (PUBLIC > STAGING)
+        if (doi.getStatus() != Status.PUBLIC) {
+            doi.setStatus(status);
+        }
         doi.setDescription(description);
+        doi.setDataAccessUrl(dataAccessUrl);
         doi.setLandingExtUrl(landingExtUrl);
 
-        log.debug("saving: {}", doi);
+        logger.debug("saving: {}", doi);
 
         doiRepository.save(doi);
 
@@ -108,12 +132,13 @@ public class DoiService {
 
     @Transactional
     public DoiStaging createOrUpdateStagingDoi(final Project project, final String identifier,
-                                               final String description, final String landingExtUrl,
+                                               final String description,
+                                               final String dataAccessUrl, final String landingExtUrl,
                                                final boolean valid, final String metadataMd5,
                                                final String processLog,
-                                               final Date now) {
+                                               final Date now, final String landingLocUrl) {
 
-        final Doi doi = createOrUpdateDoi(project, identifier, description, landingExtUrl);
+        final Doi doi = createOrUpdateDoi(project, identifier, Status.STAGING, description, dataAccessUrl, landingExtUrl);
 
         DoiStaging doiStaging = doiStagingRepository.findOneByDoi(doi);
 
@@ -126,17 +151,17 @@ public class DoiService {
         } else {
             // update date only if the MD5 changed:
             if (metadataMd5 != null && !metadataMd5.equals(doiStaging.getMetadataMd5())) {
-                log.info("{} md5 modified: {}", doi.getIdentifier(), metadataMd5);
+                logger.info("{} md5 modified: {}", doi.getIdentifier(), metadataMd5);
                 doiStaging.setUpdateDate(now);
             }
         }
 
         doiStaging.setValid(valid);
         doiStaging.setMetadataMd5(metadataMd5);
-        doiStaging.setLandingLocUrl("http://doi.osug.fr/staging/" + doi.getIdentifier()); // TODO
+        doiStaging.setLandingLocUrl(landingLocUrl);
         doiStaging.setLog(processLog);
 
-        log.debug("saving: {}", doiStaging);
+        logger.debug("saving: {}", doiStaging);
 
         doiStagingRepository.save(doiStaging);
 
@@ -145,7 +170,7 @@ public class DoiService {
 
     @Transactional
     public DoiPublic createOrUpdatePublicDoi(final Project project, final DoiStaging doiStaging,
-                                             final Date now) {
+                                             final Date now, final String landingLocUrl) {
         final Doi doi = doiStaging.getDoi();
 
         DoiPublic doiPublic = doiPublicRepository.findOneByDoi(doi);
@@ -158,11 +183,11 @@ public class DoiService {
         }
 
         doiPublic.setMetadataMd5(doiStaging.getMetadataMd5());
-        doiPublic.setLandingLocUrl("http://doi.osug.fr/public/" + doi.getIdentifier()); // TODO
-        doiPublic.setActiveExtUrl(false); // TODO
+        doiPublic.setLandingLocUrl(landingLocUrl);
+        doiPublic.setActiveExtUrl(true); // TODO: handle external url checks
         doiPublic.setUpdateDate(now);
 
-        log.debug("saving: {}", doiPublic);
+        logger.debug("saving: {}", doiPublic);
 
         doiPublicRepository.save(doiPublic);
 
@@ -177,12 +202,16 @@ public class DoiService {
         return doiRepository;
     }
 
-    public DoiStagingRepository getDoiStagingRepository() {
-        return doiStagingRepository;
+    public DoiBaseRepository<?> getDoiCommonRepository(final boolean isStaging) {
+        return (isStaging) ? doiStagingRepository : doiPublicRepository;
     }
 
     public DoiPublicRepository getDoiPublicRepository() {
         return doiPublicRepository;
     }
 
+    public DoiStagingRepository getDoiStagingRepository() {
+        return doiStagingRepository;
+    }
+
 }
diff --git a/src/main/java/fr/osug/doi/validation/ValidationUtil.java b/src/main/java/fr/osug/doi/validation/ValidationUtil.java
index c715595..d6852f6 100644
--- a/src/main/java/fr/osug/doi/validation/ValidationUtil.java
+++ b/src/main/java/fr/osug/doi/validation/ValidationUtil.java
@@ -4,21 +4,38 @@
 package fr.osug.doi.validation;
 
 import fr.osug.doi.Const;
-import fr.osug.doi.PipelineDoiData;
+import fr.osug.doi.ProcessPipelineDoiData;
 import fr.osug.doi.DoiCsvData;
+import java.text.Normalizer;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.regex.Pattern;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  *
- * @author bourgesl
  */
 public class ValidationUtil {
 
     private final static Logger logger = LoggerFactory.getLogger(ValidationUtil.class.getName());
+    /** Empty String constant '' */
+    public final static String STRING_EMPTY = "";
+    /** String constant containing 1 space character ' ' */
+    public final static String STRING_SPACE = " ";
+
     /** regular expression used to match characters different than alpha/numeric/_/-/. (1..n) */
     private final static Pattern PATTERN_IDENTIFIER = Pattern.compile("[^a-zA-Z0-9\\-_\\.]");
+    /** regular expression used to match characters different than alpha (1..n) */
+    private final static Pattern PATTERN_NON_ALPHA = Pattern.compile("[^a-zA-Z]+");
+
+    /** regular expression used to match characters with accents */
+    private final static Pattern PATTERN_ACCENT_CHARS = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
+
+    /** RegExp expression to match white spaces (1..n) */
+    private final static Pattern PATTERN_WHITE_SPACE_MULTIPLE = Pattern.compile("\\s+");
+    /** simplify name cache */
+    private final static Map<String, String> cacheSimpleNames = new ConcurrentHashMap<String, String>(128);
 
     private ValidationUtil() {
     }
@@ -40,7 +57,7 @@ public class ValidationUtil {
         return true;
     }
 
-    public static void validateIdentifier(final String id, final PipelineDoiData pipeData) {
+    public static void validateIdentifier(final String id, final ProcessPipelineDoiData pipeData) {
         // Check test prefix:
         if (!isTestPrefix(id)) {
             // identifier does not starts with the test prefix
@@ -53,7 +70,7 @@ public class ValidationUtil {
             pipeData.addError("Invalid suffix for ''" + Const.KEY_IDENTIFIER + "'' = '" + suffix + "'");
         }
     }
-    
+
     public static boolean isTestPrefix(final String id) {
         return id.startsWith(Const.DOI_PREFIX_TEST);
     }
@@ -66,4 +83,72 @@ public class ValidationUtil {
     private static boolean checkIdentifier(final String value) {
         return !PATTERN_IDENTIFIER.matcher(value).matches();
     }
+
+    /** string utils */
+    /**
+     * Test if value is empty (null or no chars)
+     * 
+     * @param value string value
+     * @return true if value is empty (null or no chars)
+     */
+    public static boolean isEmpty(final String value) {
+        return value == null || value.length() == 0;
+    }
+
+    /**
+     * Replace any non alpha numeric character by ' ' and remove accents
+     * @param value input value
+     * @return string value
+     */
+    public static String simplifyName(final String value) {
+        String res = cacheSimpleNames.get(value);
+        if (res == null) {
+            res = cleanWhiteSpaces(replaceNonAlphaChars(removeAccents(value), STRING_SPACE));
+            logger.debug("simplifyName: '{}' => '{}'", value, res);
+            cacheSimpleNames.put(value, res);
+        }
+        return res;
+    }
+
+    /**
+     * Replace non alpha numeric characters by the given replacement string
+     * @param value input value
+     * @param replaceBy replacement string
+     * @return string value
+     */
+    private static String replaceNonAlphaChars(final String value, final String replaceBy) {
+        return PATTERN_NON_ALPHA.matcher(value).replaceAll(replaceBy);
+    }
+
+    /**
+     * Remove accents from any character i.e. remove diacritical marks
+     * @param value input value
+     * @return string value
+     */
+    private static String removeAccents(final String value) {
+        // Remove accent from characters (if any) (Java 1.6)
+        final String normalized = Normalizer.normalize(value, Normalizer.Form.NFD);
+
+        return PATTERN_ACCENT_CHARS.matcher(normalized).replaceAll(STRING_EMPTY);
+    }
+
+    /**
+     * Trim and remove redundant white space characters
+     * @param value input value
+     * @return string value
+     */
+    private static String cleanWhiteSpaces(final String value) {
+        return isEmpty(value) ? STRING_EMPTY : replaceWhiteSpaces(value.trim(), STRING_SPACE);
+    }
+
+    /**
+     * Replace white space characters (1..n) by the given replacement string
+     * @param value input value
+     * @param replaceBy replacement string
+     * @return string value
+     */
+    private static String replaceWhiteSpaces(final String value, final String replaceBy) {
+        return PATTERN_WHITE_SPACE_MULTIPLE.matcher(value).replaceAll(replaceBy);
+    }
+
 }
diff --git a/src/main/java/fr/osug/doi/validation/WarningMessage.java b/src/main/java/fr/osug/doi/validation/WarningMessage.java
index 2920418..7ef48b8 100644
--- a/src/main/java/fr/osug/doi/validation/WarningMessage.java
+++ b/src/main/java/fr/osug/doi/validation/WarningMessage.java
@@ -5,7 +5,6 @@ package fr.osug.doi.validation;
 
 /**
  * This class represents a warning message (message + state)
- * @author bourgesl
  */
 public final class WarningMessage {
 
diff --git a/src/main/java/fr/osug/util/FileUtils.java b/src/main/java/fr/osug/util/FileUtils.java
index 9940ee1..db8ec09 100644
--- a/src/main/java/fr/osug/util/FileUtils.java
+++ b/src/main/java/fr/osug/util/FileUtils.java
@@ -16,9 +16,11 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.io.OutputStreamWriter;
 import java.io.Reader;
 import java.io.Writer;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
@@ -355,6 +357,32 @@ public final class FileUtils {
         return null;
     }
 
+    public static Reader getTextReader(final File file) throws IOException {
+        return new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
+    }
+
+    public static Writer getTextWriter(final File file) throws IOException {
+        return getTextWriter(new FileOutputStream(file));
+    }
+
+    public static Writer getTextWriter(final OutputStream out) throws IOException {
+        return new OutputStreamWriter(out, StandardCharsets.UTF_8);
+    }
+
+    public static void writeFile(final String content, final File textFile) throws IOException {
+        log.info("Writing: {}", textFile.getAbsolutePath());
+
+        BufferedWriter writer = null;
+        try {
+            writer = new BufferedWriter(getTextWriter(textFile), content.length());
+            writer.write(content);
+        } finally {
+            if (writer != null) {
+                writer.close();
+            }
+        }
+    }
+
     /**
      * Read a text file from the given file
      *
@@ -470,19 +498,6 @@ public final class FileUtils {
         }
     }
 
-    public static long computeChecksum(final InputStream in) throws IOException {
-
-        final ChecksumOutputStream cs = new ChecksumOutputStream();
-
-        final byte[] buf = new byte[DEFAULT_READ_BUFFER_SIZE];
-
-        int len;
-        while ((len = in.read(buf)) > 0) {
-            cs.write(buf, 0, len);
-        }
-        return cs.getChecksum(); // flush and close streams
-    }
-
     public static String MD5(final InputStream in) throws IOException {
         try {
             final MessageDigest md = MessageDigest.getInstance("MD5");
diff --git a/src/main/java/fr/osug/xml/XmlFactory.java b/src/main/java/fr/osug/xml/XmlFactory.java
index 7a74c9b..f64de2f 100644
--- a/src/main/java/fr/osug/xml/XmlFactory.java
+++ b/src/main/java/fr/osug/xml/XmlFactory.java
@@ -32,7 +32,6 @@ import javax.xml.validation.Schema;
 import javax.xml.validation.SchemaFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.w3c.dom.Document;
 import org.w3c.dom.Node;
@@ -95,7 +94,7 @@ public final class XmlFactory {
             try {
                 transformerFactory = TransformerFactory.newInstance();
 
-                log.info("transformerFactory: {}", transformerFactory);
+                log.debug("transformerFactory: {}", transformerFactory);
 
             } catch (final TransformerFactoryConfigurationError tfce) {
                 throw new IllegalStateException("XmlFactory.getTransformerFactory : failure on TransformerFactory initialisation : ", tfce);
@@ -385,6 +384,9 @@ public final class XmlFactory {
 
             if (tf != null) {
                 if (parameters != null) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("XmlFactory.transform : XSL parameters : {}", parameters);
+                    }
                     for (final Map.Entry<String, Object> entry : parameters.entrySet()) {
                         tf.setParameter(entry.getKey(), entry.getValue());
                     }
diff --git a/src/main/resources/config/application-default.yml b/src/main/resources/config/application-default.yml
index 998c874..944b99b 100644
--- a/src/main/resources/config/application-default.yml
+++ b/src/main/resources/config/application-default.yml
@@ -15,17 +15,21 @@ debug: true
 spring:
     datasource:
 # go into osug-doi/tmp/ folder:
-        url: jdbc:h2:file:../tmp/h2db/doi;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
-        name:
+        url: jdbc:h2:../tmp/h2db/doi;MODE=PostgreSQL
+#;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
+        name: doi
         username: doi
         password:
+        initial-size: 1
+        min-idle: 2
+        max-idle: 4
     h2:
         console:
-            enabled: true
+            enabled: false
     jpa:
         database: H2
         database-platform: org.hibernate.dialect.PostgreSQL9Dialect
         show_sql: true
-        hibernate:
-            ddl-auto: none
+#        hibernate:
+#            ddl-auto: none
 #            ddl-auto: create
diff --git a/src/main/resources/config/application-test.yml b/src/main/resources/config/application-test.yml
index 998c874..d2be6b0 100644
--- a/src/main/resources/config/application-test.yml
+++ b/src/main/resources/config/application-test.yml
@@ -14,18 +14,17 @@ debug: true
 
 spring:
     datasource:
-# go into osug-doi/tmp/ folder:
-        url: jdbc:h2:file:../tmp/h2db/doi;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
-        name:
+        url: jdbc:h2:mem:doi;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
+        name: doi
         username: doi
         password:
+        initial-size: 1
+        min-idle: 2
+        max-idle: 4
     h2:
         console:
-            enabled: true
+            enabled: false
     jpa:
         database: H2
         database-platform: org.hibernate.dialect.PostgreSQL9Dialect
         show_sql: true
-        hibernate:
-            ddl-auto: none
-#            ddl-auto: create
diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml
index 6280ae6..e2a727f 100644
--- a/src/main/resources/config/application.yml
+++ b/src/main/resources/config/application.yml
@@ -24,11 +24,13 @@ spring:
     jpa:
         open-in-view: false
         properties:
-            hibernate.id.new_generator_mappings: false
+            hibernate.id.new_generator_mappings: true
             hibernate.cache.use_second_level_cache: false
             hibernate.cache.use_query_cache: false
             hibernate.generate_statistics: false
-            hibernate.hbm2ddl.auto: none
+            hibernate.hbm2ddl.auto:
+    main:
+        web-environment: false
 
 logging:
     file:   osug-doi.log
@@ -36,4 +38,10 @@ logging:
 osug:
     doi:
         prefix: 10.5072
-        domain: my-domain.fr
+        domain: http://my-domain.fr/
+        dataciteClientEnabled: false
+
+datacite:
+    user: user
+    password: password_to_define
+
diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql
index 8f52947..cbdfce5 100644
--- a/src/main/resources/db/migration/V1__init.sql
+++ b/src/main/resources/db/migration/V1__init.sql
@@ -1,6 +1,6 @@
 
 /* PROJECT */
-create sequence project_id_seq START 1;
+create sequence project_id_seq START 1 INCREMENT 10;
 
 create table project (
     id int8 not null,
@@ -14,7 +14,7 @@ create table project (
 alter table project add constraint uk_project_name unique (name);
 
 /* DOI */
-create sequence doi_id_seq START 1;
+create sequence doi_id_seq  START 1 INCREMENT 10;
 
 create table doi (
     id int8 not null,
@@ -22,39 +22,43 @@ create table doi (
     identifier varchar(255) not null,
     status varchar(16),
     description varchar(512),
+    data_access_url varchar(1024),
     landing_ext_url varchar(1024),
     primary key (id)
 );
 alter table doi add constraint uk_doi_identifier unique (identifier);
 alter table doi add constraint fk_doi_project_id foreign key (project_id) references project;
 
-/* DOI STAGING */
-create sequence doi_staging_id_seq START 1;
+/* DOI COMMON (seq) */
+create sequence doi_common_id_seq  START 1 INCREMENT 10;
 
+/* DOI STAGING */
 create table doi_staging (
+    /* DOI Common fields */
     id int8 not null,
     doi_id int8 not null,
-    valid boolean,
     metadata_md5 varchar(32) not null,
-    log varchar(4096),
     landing_loc_url varchar(512) not null,
     create_date timestamp,
     update_date timestamp,
+    /* specific fields */
+    valid boolean,
+    log varchar(4096),
     primary key (id)
 );
 alter table doi_staging add constraint fk_doi_staging_id foreign key (doi_id) references doi;
 
 /* DOI PUBLIC */
-create sequence doi_public_id_seq START 1;
-
 create table doi_public (
+    /* DOI Common fields */
     id int8 not null,
     doi_id int8 not null,
     metadata_md5 varchar(32) not null,
     landing_loc_url varchar(512) not null,
-    active_ext_url boolean,
     create_date timestamp,
     update_date timestamp,
+    /* specific fields */
+    active_ext_url boolean,
     primary key (id)
 );
 alter table doi_public add constraint fk_doi_public_id foreign key (doi_id) references doi;
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
index fdd305a..a496fae 100644
--- a/src/main/resources/logback-spring.xml
+++ b/src/main/resources/logback-spring.xml
@@ -8,8 +8,11 @@
         <logger name="fr.osug.doi" level="DEBUG" />
 
         <logger name="org.springframework.orm.jpa.JpaTransactionManager" level="DEBUG"/>
-        <logger name="org.hibernate.transaction" level="DEBUG" />
         <logger name="org.springframework.transaction" level="DEBUG" />
+        <logger name="org.springframework.transaction.interceptor" level="TRACE" />
+
+        <logger name="org.springframework.web.client.RestTemplate" level="TRACE" />
+
     </springProfile>
 
     <springProfile name="prod">
diff --git a/src/test/java/fr/osug/doi/DOIServiceTest.java b/src/test/java/fr/osug/doi/DOIServiceTest.java
index e72cc51..c8f5708 100644
--- a/src/test/java/fr/osug/doi/DOIServiceTest.java
+++ b/src/test/java/fr/osug/doi/DOIServiceTest.java
@@ -27,11 +27,10 @@ import org.springframework.transaction.annotation.Transactional;
 
 /**
  *
- * @author bourgesl
  */
 @RunWith(SpringJUnit4ClassRunner.class)
 @SpringBootTest
-@ActiveProfiles("test,default")
+@ActiveProfiles({"test","debug"})
 public class DOIServiceTest {
 
     private final static Logger logger = LoggerFactory.getLogger(DOIServiceTest.class.getName());
@@ -50,14 +49,14 @@ public class DOIServiceTest {
         }
 
         final ProjectRepository pr = doiService.getProjectRepository();
-        /*      
-        final Sort sortByName = new Sort("name");
-        final List<Project> projects = pr.findAll(sortByName);
-         */
+
+        final List<String> names = pr.findAllNamesOrderByNameAsc();
+        logger.info("names: {}", names);
+
         final List<Project> projects = pr.findAllByOrderByNameAsc();
 
         for (Project dp : projects) {
-            logger.info("remove project: " + dp);
+            logger.info("remove project: {}", dp);
             pr.delete(dp);
         }
 
@@ -91,7 +90,7 @@ public class DOIServiceTest {
         final List<Project> projects = pr.findAllByOrderByNameAsc();
 
         for (Project dp : projects) {
-            logger.info("remove project: " + dp);
+            logger.info("remove project: {}", dp);
             pr.delete(dp);
         }
         logger.info("verifyInitialDatabaseState: exit");
@@ -106,7 +105,7 @@ public class DOIServiceTest {
         final List<Project> projects = pr.findAllByOrderByNameAsc();
 
         for (Project dp : projects) {
-            logger.info("project: " + dp);
+            logger.info("project: {}", dp);
         }
 
         Assert.assertTrue(projects.isEmpty());
@@ -118,7 +117,7 @@ public class DOIServiceTest {
     @Test
     public void testCreateProjectAndDoi() throws Exception {
         final String projectName = "AMMA-CATCH";
-        
+
         final Date now = new Date();
 
         final Project project = doiService.getOrCreateProject(projectName, now);
@@ -126,9 +125,10 @@ public class DOIServiceTest {
 
         for (int i = 0; i < 93; i++) {
             Assert.assertNotNull(doiService.createOrUpdateStagingDoi(project,
-                    "AMMA-CATCH.TEST." + (int) (10000.0 * Math.random()),
-                    "description ...", "http://server/ext",
-                    true, "md5", "log:blah\nblah", now));
+                    "AMMA-CATCH.TEST." + (int) (10.0 * Math.random()),
+                    "description ...", "http://server/data/file", "http://server/ext/page",
+                    true, "md5", "log:blah\nblah", now,
+                    "/test/AMMA-CATCH.TEST.html"));
 
             Thread.sleep(20l);
         }
@@ -142,7 +142,7 @@ public class DOIServiceTest {
         logger.info("Dois for project[{}]: {}", projectName, dois.size());
 
         for (Doi d : dois) {
-            logger.info("Doi: " + d);
+            logger.info("Doi: {}", d);
         }
 
         final DoiStagingRepository dsr = doiService.getDoiStagingRepository();
@@ -151,7 +151,7 @@ public class DOIServiceTest {
         logger.info("DoiStaging for project[{}]: {}", projectName, dois.size());
 
         for (DoiStaging d : doiStagings) {
-            logger.info("DoiStaging: " + d);
+            logger.info("DoiStaging: {}", d);
         }
     }
 }
diff --git a/src/test/java/fr/osug/doi/DOITextToXmlTest.java b/src/test/java/fr/osug/doi/DOITextToXmlTest.java
index ad07676..205dc8f 100644
--- a/src/test/java/fr/osug/doi/DOITextToXmlTest.java
+++ b/src/test/java/fr/osug/doi/DOITextToXmlTest.java
@@ -26,11 +26,10 @@ import org.springframework.transaction.annotation.Transactional;
 
 /**
  *
- * @author bourgesl
  */
 @RunWith(SpringJUnit4ClassRunner.class)
 @SpringBootTest
-@ActiveProfiles(profiles = {"test", "debug"})
+@ActiveProfiles(profiles = {"test","debug"})
 public class DOITextToXmlTest {
 
     private final static Logger logger = LoggerFactory.getLogger(DOITextToXmlTest.class.getName());
@@ -43,9 +42,7 @@ public class DOITextToXmlTest {
     private final static String DIR_OUTPUT = Paths.DIR_BASE + "target/test-output/";
     private final static String DIR_STAGING = DIR_OUTPUT + "staging/";
     public final static String DIR_WEB = DIR_OUTPUT + "www/";
-    public final static String DIR_WEB_STAGING = DIR_WEB + "staging/";
-    public final static String DIR_WEB_PUBLIC = DIR_WEB + "public/";
-    
+
     @BeforeClass
     public static void setUpClass() {
     }
@@ -84,22 +81,21 @@ public class DOITextToXmlTest {
     @Transactional
     @Test
     public void testPipeline() throws IOException {
-        
+
         final String project = "AMMA-CATCH";
 
-        ProjectConfig pc = new ProjectConfig(project);
+        final PathConfig pathConfig = doiConfig.getPathConfig();
+        // redirect web:
+        pathConfig.initWebRoot(DIR_WEB);
+
+        final ProjectConfig pc = new ProjectConfig(pathConfig, project);
         pc.setSaveCSV(true);
 
+        // redirect outputs:
         pc.initInputDir(DIR_TEST);
         pc.initTmpDir(DIR_OUTPUT);
 
         pc.initStagingDir(DIR_STAGING);
-        pc.initWebStagingDir(DIR_WEB_STAGING);
-        pc.initWebStagingProjectDir(DIR_WEB_STAGING + project);
-        pc.initWebStagingProjectEmbedDir(DIR_WEB_STAGING + project + '/' + Paths.DIR_WEB_EMBED);
-        pc.initWebStagingProjectXmlDir(DIR_WEB_STAGING + project + '/' + Paths.DIR_WEB_XML);
-        pc.initWebPublicDir(DIR_WEB_PUBLIC);
-        pc.initWebPublicProjectDir(DIR_WEB_PUBLIC + project);
 
         final ProcessPipeline pipeline = new ProcessPipeline(doiConfig, pc);
         pipeline.process();
-- 
GitLab