ref #6755 first preliminary version of DwC-A export web service
authorAndreas Müller <a.mueller@bgbm.org>
Wed, 28 Jun 2017 20:06:37 +0000 (22:06 +0200)
committerAndreas Müller <a.mueller@bgbm.org>
Wed, 28 Jun 2017 20:07:01 +0000 (22:07 +0200)
cdmlib-io/src/main/java/eu/etaxonomy/cdm/io/dwca/out/DwcaExportBase.java
cdmlib-persistence/src/main/java/eu/etaxonomy/cdm/persistence/dto/TaxonNodeDto.java
cdmlib-remote/src/main/java/eu/etaxonomy/cdm/remote/controller/checklist/DwcaExportController.java [new file with mode: 0644]
src/site/apt/remote/dwca-tax-export-default.apt [new file with mode: 0644]

index de11f42c89b90266a87fa078f34078d7987967a2..ae22fbe9789387066da1f93b22bd7f524e250f19 100644 (file)
@@ -29,7 +29,10 @@ import javax.xml.stream.XMLStreamWriter;
 \r
 import org.apache.commons.lang.StringUtils;\r
 import org.apache.log4j.Logger;\r
+import org.springframework.beans.factory.annotation.Autowired;\r
 \r
+import eu.etaxonomy.cdm.api.service.IClassificationService;\r
+import eu.etaxonomy.cdm.api.service.ITaxonNodeService;\r
 import eu.etaxonomy.cdm.common.CdmUtils;\r
 import eu.etaxonomy.cdm.io.common.CdmExportBase;\r
 import eu.etaxonomy.cdm.io.common.ICdmExport;\r
@@ -61,6 +64,12 @@ public abstract class DwcaExportBase
 \r
     protected static final boolean IS_CORE = true;\r
 \r
+    @Autowired\r
+    private IClassificationService classificationService;\r
+\r
+    @Autowired\r
+    private ITaxonNodeService taxonNodeService;\r
+\r
 \r
     @Override\r
     public int countSteps(DwcaTaxExportState state) {\r
@@ -104,38 +113,61 @@ public abstract class DwcaExportBase
 \r
     private void makeAllNodes(DwcaTaxExportState state, Set<UUID> subtreeSet) {\r
 \r
-        boolean doSynonyms = false;\r
-        boolean recursive = true;\r
-        Set<UUID> uuidSet = new HashSet<>();\r
+        try {\r
+            boolean doSynonyms = false;\r
+            boolean recursive = true;\r
+            Set<UUID> uuidSet = new HashSet<>();\r
+\r
+            for (UUID subtreeUuid : subtreeSet){\r
+                UUID tnUuuid = taxonNodeUuid(subtreeUuid);\r
+                uuidSet.add(tnUuuid);\r
+                List<TaxonNodeDto> records = getTaxonNodeService().pageChildNodesDTOs(tnUuuid,\r
+                        recursive, doSynonyms, null, null, null).getRecords();\r
+                for (TaxonNodeDto dto : records){\r
+                    uuidSet.add(dto.getUuid());\r
+                }\r
+            }\r
+            List<TaxonNode> allNodes =  getTaxonNodeService().find(uuidSet);\r
 \r
-        for (UUID subtreeUuid : subtreeSet){\r
-            uuidSet.add(subtreeUuid);\r
-            List<TaxonNodeDto> records = getTaxonNodeService().pageChildNodesDTOs(subtreeUuid,\r
-                    recursive, doSynonyms, null, null, null).getRecords();\r
-            for (TaxonNodeDto dto : records){\r
-                uuidSet.add(dto.getUuid());\r
+            List<TaxonNode> result = new ArrayList<>();\r
+            for (TaxonNode node : allNodes){\r
+                if(node.getParent()== null){  //root (or invalid) node\r
+                    continue;\r
+                }\r
+                node = CdmBase.deproxy(node);\r
+                Taxon taxon = CdmBase.deproxy(node.getTaxon());\r
+                if (taxon == null){\r
+                    String message = "There is a taxon node without taxon. id=" + node.getId();\r
+                    state.getResult().addWarning(message);\r
+                    continue;\r
+                }\r
+                result.add(node);\r
             }\r
+            state.setAllNodes(result);\r
+        } catch (Exception e) {\r
+            String message = "Unexpected exception when trying to compute all taxon nodes";\r
+            state.getResult().addException(e, message);\r
         }\r
-        List<TaxonNode> allNodes =  getTaxonNodeService().find(uuidSet);\r
+    }\r
 \r
-        List<TaxonNode> result = new ArrayList<>();\r
-        for (TaxonNode node : allNodes){\r
-            if(node.getParent()== null){  //root (or invalid) node\r
-                continue;\r
-            }\r
-            node = CdmBase.deproxy(node);\r
-            Taxon taxon = CdmBase.deproxy(node.getTaxon());\r
-            if (taxon == null){\r
-                String message = "There is a taxon node without taxon. id=" + node.getId();\r
-                state.getResult().addWarning(message);\r
-                continue;\r
+\r
+    /**\r
+     * @param subtreeUuid\r
+     * @return\r
+     */\r
+    private UUID taxonNodeUuid(UUID subtreeUuid) {\r
+        TaxonNode node = taxonNodeService.find(subtreeUuid);\r
+        if (node == null){\r
+            Classification classification = classificationService.find(subtreeUuid);\r
+            if (classification != null){\r
+                node = classification.getRootNode();\r
+            }else{\r
+                throw new IllegalArgumentException("Subtree identifier does not exist: " + subtreeUuid);\r
             }\r
-            result.add(node);\r
         }\r
-        state.setAllNodes(result);\r
+        return node.getUuid();\r
     }\r
 \r
-\r
     /**\r
      * Creates the locationId, locality, countryCode triple\r
      * @param record\r
index 8feb3960e208e2d607ee48b4b64ca8a6ff28e9f8..2d26f9b1df61b8878b0deb71d717f0360d092049 100644 (file)
@@ -78,13 +78,13 @@ public class TaxonNodeDto {
         uuid = taxonNode.getUuid();
         taxonomicChildrenCount = taxonNode.getCountChildren();
         Taxon taxon = taxonNode.getTaxon();
-        secUuid = taxon.getSec().getUuid();
+        secUuid = taxon.getSec() != null ? taxon.getSec().getUuid() : null;
         taxonUuid = taxon.getUuid();
-        titleCache = taxon.getName().getTitleCache();
-        taggedTitle = taxon.getName().getTaggedName();
+        titleCache = taxon.getName() != null ? taxon.getName().getTitleCache() : taxon.getTitleCache();
+        taggedTitle = taxon.getName() != null? taxon.getName().getTaggedName() : taxon.getTaggedTitle();
         unplaced = taxonNode.isUnplaced();
         excluded = taxonNode.isExcluded();
-        rankLabel = taxon.getName().getRank().getLabel();
+        rankLabel = taxon.getNullSafeRank() != null ? taxon.getNullSafeRank().getLabel() : null;
         status = TaxonStatus.Accepted;
     }
 
diff --git a/cdmlib-remote/src/main/java/eu/etaxonomy/cdm/remote/controller/checklist/DwcaExportController.java b/cdmlib-remote/src/main/java/eu/etaxonomy/cdm/remote/controller/checklist/DwcaExportController.java
new file mode 100644 (file)
index 0000000..126457e
--- /dev/null
@@ -0,0 +1,402 @@
+/*
+* Copyright  EDIT
+* European Distributed Institute of Taxonomy
+* http://www.e-taxonomy.eu
+*
+* The contents of this file are subject to the Mozilla Public License Version 1.1
+* See LICENSE.TXT at the top of this package for the full license terms.
+*/
+package eu.etaxonomy.cdm.remote.controller.checklist;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ResourceLoaderAware;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.WebDataBinder;
+import org.springframework.web.bind.annotation.InitBinder;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.servlet.ModelAndView;
+
+import eu.etaxonomy.cdm.api.service.IClassificationService;
+import eu.etaxonomy.cdm.api.service.IService;
+import eu.etaxonomy.cdm.api.service.ITaxonNodeService;
+import eu.etaxonomy.cdm.common.CdmUtils;
+import eu.etaxonomy.cdm.common.DocUtils;
+import eu.etaxonomy.cdm.common.monitor.IRestServiceProgressMonitor;
+import eu.etaxonomy.cdm.io.common.CdmApplicationAwareDefaultExport;
+import eu.etaxonomy.cdm.io.dwca.out.DwcaEmlRecord;
+import eu.etaxonomy.cdm.io.dwca.out.DwcaTaxExportConfigurator;
+import eu.etaxonomy.cdm.model.description.Feature;
+import eu.etaxonomy.cdm.model.taxon.Classification;
+import eu.etaxonomy.cdm.model.taxon.Taxon;
+import eu.etaxonomy.cdm.model.taxon.TaxonNode;
+import eu.etaxonomy.cdm.remote.controller.AbstractController;
+import eu.etaxonomy.cdm.remote.controller.ProgressMonitorController;
+import eu.etaxonomy.cdm.remote.controller.util.ProgressMonitorUtil;
+import eu.etaxonomy.cdm.remote.editor.UUIDListPropertyEditor;
+import eu.etaxonomy.cdm.remote.editor.UuidList;
+import eu.etaxonomy.cdm.remote.view.FileDownloadView;
+import eu.etaxonomy.cdm.remote.view.HtmlView;
+
+/**
+ * @author a.mueller
+ * @created 28.06.2017
+ * <p>
+ *  This controller exports taxonomies via Darwin Core Archive
+ *  (https://en.wikipedia.org/wiki/Darwin_Core_Archive).
+ */
+@Controller
+@RequestMapping(value = { "/dwca" })
+public class DwcaExportController extends AbstractController implements ResourceLoaderAware{
+
+
+    private static final String DWCA_TAX_EXPORT_DOC_RESSOURCE = "classpath:eu/etaxonomy/cdm/doc/remote/apt/dwca-tax-export-default.apt";
+
+    @Autowired
+    private ApplicationContext appContext;
+
+    @Autowired
+    private IClassificationService classificationService;
+
+    @Autowired
+    private ITaxonNodeService taxonNodeService;
+
+    @Autowired
+    public ProgressMonitorController progressMonitorController;
+
+    private ResourceLoader resourceLoader;
+
+
+    /**
+     * There should only be one processes operating on the export
+     * therefore the according progress monitor uuid is stored in this static
+     * field.
+     */
+    private static UUID indexMonitorUuid = null;
+
+    private final static long DAY_IN_MILLIS = 86400000;
+
+
+
+    private static final Logger logger = Logger.getLogger(DwcaExportController.class);
+
+    /**
+     * Helper method, which allows to convert strings directly into uuids.
+     *
+     * @param binder Special DataBinder for data binding from web request parameters to JavaBean objects.
+     */
+    @InitBinder
+    public void initBinder(WebDataBinder binder) {
+        binder.registerCustomEditor(UuidList.class, new UUIDListPropertyEditor());
+//        binder.registerCustomEditor(NamedArea.class, new TermBaseListPropertyEditor<>(termService));
+//        binder.registerCustomEditor(UUID.class, new UUIDEditor());
+    }
+
+
+
+
+    /**
+     * Documentation webservice for this controller.
+     *
+     * @param response unused
+     * @param request unused
+     * @return
+     * @throws IOException
+     */
+    @RequestMapping(value = {""}, method = { RequestMethod.GET})
+    public ModelAndView exportGetExplanation(HttpServletResponse response,
+            HttpServletRequest request) throws IOException{
+        ModelAndView mv = new ModelAndView();
+        // Read apt documentation file.
+        Resource resource = resourceLoader.getResource(DWCA_TAX_EXPORT_DOC_RESSOURCE);
+        // using input stream as this works for both files in the classes directory
+        // as well as files inside jars
+        InputStream aptInputStream = resource.getInputStream();
+        // Build Html View
+        Map<String, String> modelMap = new HashMap<>();
+        // Convert Apt to Html
+        modelMap.put("html", DocUtils.convertAptToHtml(aptInputStream));
+        mv.addAllObjects(modelMap);
+
+        HtmlView hv = new HtmlView();
+        mv.setView(hv);
+        return mv;
+    }
+
+    /**
+     * This service endpoint is for generating the documentation site.
+     * If any request of the other endpoint below is incomplete or false
+     * then this method will be triggered.
+     *
+     * @param response
+     * @param request
+     * @return
+     * @throws IOException
+     */
+    public ModelAndView exportGetExplanation(HttpServletResponse response,
+            HttpServletRequest request, Resource res) throws IOException{
+        ModelAndView mv = new ModelAndView();
+        // Read apt documentation file.
+        Resource resource = (res!= null) ? res : resourceLoader.getResource(DWCA_TAX_EXPORT_DOC_RESSOURCE);
+        // using input stream as this works for both files in the classes directory
+        // as well as files inside jars
+        InputStream aptInputStream = resource.getInputStream();
+        // Build Html View
+        Map<String, String> modelMap = new HashMap<>();
+        // Convert Apt to Html
+        modelMap.put("html", DocUtils.convertAptToHtml(aptInputStream));
+        mv.addAllObjects(modelMap);
+
+        HtmlView hv = new HtmlView();
+        mv.setView(hv);
+        return mv;
+    }
+
+
+
+    /**
+     *
+     * This Service endpoint will offer a csv file. It caches the csv-file in the system temp directory
+     * and will only generate a new one after 24 hours. Or if explicitly triggerd by noCache parameter.
+     *
+     * @param featureUuids List of uuids to download/select {@link Feature feature}features
+     * @param clearCache will trigger export and avoids cached file
+     * @param classificationUUID Selected {@link Classification classification} to iterate the {@link Taxon}
+     * @param response HttpServletResponse which returns the ByteArrayOutputStream
+     * @throws Exception
+     */
+    @RequestMapping(value = { "dwcaTaxExport" }, method = { RequestMethod.GET })
+    public synchronized ModelAndView doDwcaTaxExport(
+            @RequestParam(value = "subtrees", required = false) final UuidList subtreeUuids,
+            @RequestParam(value = "clearCache", required = false) final boolean clearCache,
+//            @RequestParam(value = "demoExport", required = false) final boolean demoExport,
+//            @RequestParam(value = "conceptExport", required = false) final boolean conceptExport,
+//            @RequestParam(value = "classification", required = false) final String classificationUUID,
+//            @RequestParam(value = "area", required = false) final UuidList areas,
+            @RequestParam(value = "downloadTokenValueId", required = false) final String downloadTokenValueId,
+            @RequestParam(value = "priority", required = false) Integer priority,
+            final HttpServletResponse response,
+            final HttpServletRequest request) throws Exception {
+        /**
+         * ========================================
+         * progress monitor & new thread for export
+         * ========================================
+         */
+        try{
+            ModelAndView mv = new ModelAndView();
+
+            String fileName = makeFileName(response, subtreeUuids);
+
+            final File cacheFile = new File(new File(System.getProperty("java.io.tmpdir")), fileName);
+            final String origin = request.getRequestURL().append('?')
+                    .append(request.getQueryString()).toString();
+
+            Long result = null;
+            if(cacheFile.exists()){
+                result = System.currentTimeMillis() - cacheFile.lastModified();
+            }
+            //if file exists return file instantly
+            //timestamp older than one day?
+            if(clearCache == false && result != null){ //&& result < 7*(DAY_IN_MILLIS)
+                logger.info("result of calculation: " + result);
+                Map<String, File> modelMap = new HashMap<>();
+                modelMap.put("file", cacheFile);
+                mv.addAllObjects(modelMap);
+                //application/zip
+                FileDownloadView fdv = new FileDownloadView("application/octet-stream", fileName, "zip", "UTF-8");
+                mv.setView(fdv);
+                return mv;
+            }else{//trigger progress monitor and performExport()
+                String processLabel = "Exporting ...";
+                final String frontbaseUrl = null;
+                ProgressMonitorUtil progressUtil = new ProgressMonitorUtil(progressMonitorController);
+                if (!progressMonitorController.isMonitorRunning(indexMonitorUuid)) {
+                    indexMonitorUuid = progressUtil.registerNewMonitor();
+                    Thread subThread = new Thread() {
+                        @Override
+                        public void run() {
+                            try {
+                                cacheFile.createNewFile();
+                            } catch (IOException e) {
+                                logger.info("Could not create file "+ e);
+                            }
+                            performExport(cacheFile, progressMonitorController.getMonitor(indexMonitorUuid),
+                                    subtreeUuids, downloadTokenValueId, origin, response);
+                        }
+                    };
+                    if (priority == null) {
+                        priority = AbstractController.DEFAULT_BATCH_THREAD_PRIORITY;
+                    }
+                    subThread.setPriority(priority);
+                    subThread.start();
+                }
+                mv = progressUtil.respondWithMonitorOrDownload(frontbaseUrl, origin, processLabel, indexMonitorUuid, false, request, response);
+            }
+            return mv;
+        }catch(Exception e){
+            //TODO: Write an specific documentation for this service endpoint
+           Resource resource = resourceLoader.getResource(DWCA_TAX_EXPORT_DOC_RESSOURCE);
+           return exportGetExplanation(response, request, resource);
+        }
+    }
+
+
+
+    //=========== Helper Methods ===============//
+
+    /**
+     *
+     * This private methods finally triggers the export back in the io-package and will create a cache file
+     * in system temp directory.
+     *
+     * @param downloadTokenValueId
+     * @param response
+     * @param byteArrayOutputStream
+     * @param config
+     * @param defaultExport
+     */
+    private void performExport(File cacheFile, IRestServiceProgressMonitor progressMonitor,
+            UuidList featureUuids, String downloadTokenValueId, String origin,
+            HttpServletResponse response
+            ) {
+
+        progressMonitor.subTask("configure export");
+        DwcaTaxExportConfigurator config = setDwcaTaxExportConfigurator(cacheFile, progressMonitor, featureUuids);
+        @SuppressWarnings("unchecked")
+        CdmApplicationAwareDefaultExport<DwcaTaxExportConfigurator> defaultExport =
+                (CdmApplicationAwareDefaultExport<DwcaTaxExportConfigurator>)appContext.getBean("defaultExport");
+        progressMonitor.subTask("invoke export");
+        defaultExport.invoke(config);  //triggers export
+        progressMonitor.subTask("wrote results to cache");
+        progressMonitor.done();
+        progressMonitor.setOrigin(origin);
+    }
+
+    /**
+     * Cofiguration method to set the configuration details for the defaultExport in the application context.
+     * @param cacheFile
+     *
+     * @param classificationUUID pass-through the selected {@link Classification classification}
+     * @param featureUuids pass-through the selected {@link Feature feature} of a {@link Taxon}, in order to fetch it.
+     * @param areas
+     * @param byteArrayOutputStream pass-through the stream to write out the data later.
+     * @param progressMonitor
+     * @param conceptExport
+     * @param demoExport
+     * @return the CsvTaxExportConfiguratorRedlist config
+     */
+    private DwcaTaxExportConfigurator setDwcaTaxExportConfigurator(File cacheFile, IRestServiceProgressMonitor progressMonitor,
+            UuidList subtreeUuids) {
+
+        if(cacheFile == null){
+            String destination = System.getProperty("java.io.tmpdir");
+            cacheFile = new File(destination);
+        }
+
+        DwcaEmlRecord emlRecord = null;
+        DwcaTaxExportConfigurator config = DwcaTaxExportConfigurator.NewInstance(
+                null, cacheFile, emlRecord);
+
+        Set<UUID> subtreeSet = new HashSet<>(subtreeUuids);
+        config.setProgressMonitor(progressMonitor);
+        config.setSubtreeUuids(subtreeSet);
+
+//        config.setHasHeaderLines(true);
+//        config.setFieldsTerminatedBy("\t");
+//        config.setClassificationUuids(classificationUUIDS);
+
+//        if(demoExport == false && conceptExport == false){
+//             config.createPreSelectedExport(false, true);
+//        }else{
+//             config.createPreSelectedExport(demoExport, conceptExport);
+//        }
+
+        return config;
+    }
+
+
+    /**
+     * @param response
+     * @param subtreeUuids
+     * @throws IOException
+     */
+    private String makeFileName(HttpServletResponse response, UuidList subtreeUuids) throws IOException {
+        String fileName;
+        if (subtreeUuids != null && ! subtreeUuids.isEmpty()){
+            UUID firstUuid = subtreeUuids.get(0);
+            TaxonNode node = taxonNodeService.find(firstUuid);
+            if (node != null && node.getTaxon() != null){
+                if (node.getTaxon().getName() != null){
+                    fileName = node.getTaxon().getName().getTitleCache();
+                }else{
+                    fileName = node.getTaxon().getTitleCache();
+                }
+            }else if (node != null){
+                fileName = node.getClassification().getTitleCache();
+            }else{
+                Classification classification = classificationService.find(firstUuid);
+                if (classification != null){
+                    fileName = classification.getTitleCache();
+                }else{
+                    //handle via repso
+                    response.sendError(404, "Subtree uuid does not exist: " + firstUuid);
+                    fileName = "Error";
+                }
+            }
+        }else{
+            List<Classification> classificationList = classificationService.list(null, 1, null
+                    , null, null);
+            if (!classificationList.isEmpty()){
+                fileName = classificationList.get(0).getTitleCache();
+            }else{
+               //handle via repso
+                response.sendError(404, "No classification found");
+                fileName = "Error";
+            }
+        }
+
+
+        fileName = fileName + "_" + uuidListToString(subtreeUuids, 40) + ".zip";
+
+        return fileName;
+
+    }
+
+    private String uuidListToString(UuidList uuidList, Integer truncate) {
+        String result = null;
+        for (UUID uuid : uuidList){
+            result = CdmUtils.concat("_", uuid.toString());
+        }
+        if (result != null && result.length() > truncate){
+            result = result.substring(0, truncate);
+        }
+        return result;
+    }
+
+    @Override
+    public void setService(IService service) {}
+
+    @Override
+    public void setResourceLoader(ResourceLoader resourceLoader) {
+        this.resourceLoader = resourceLoader;
+    }
+
+}
diff --git a/src/site/apt/remote/dwca-tax-export-default.apt b/src/site/apt/remote/dwca-tax-export-default.apt
new file mode 100644 (file)
index 0000000..9ebe30f
--- /dev/null
@@ -0,0 +1,13 @@
+                                   --------------------------
+                                    DWCA TAXON EXPORT API
+                                    --------------------------
+
+{CDM DWCA TAXON API}
+
+    TODO
+
+       
+*      The request parameters are :    
+       
+       
+