com.formkiq.core.service.FolderServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.formkiq.core.service.FolderServiceImpl.java

Source

/*
 * Copyright (C) 2016 FormKiQ Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.formkiq.core.service;

import static com.formkiq.core.domain.type.FolderPermission.PERM_FORM_ADMIN;
import static com.formkiq.core.domain.type.FolderPermission.PERM_FORM_DESIGN;
import static com.formkiq.core.domain.type.FolderPermission.PERM_FORM_ENTRY;
import static com.formkiq.core.domain.type.FolderPermission.PERM_FORM_RESULTS;
import static com.formkiq.core.form.FormFinder.findSectionAndField;
import static com.formkiq.core.form.FormFinder.findValueByKey;
import static com.formkiq.core.form.dto.WorkflowOutputDocumentType.FORM;
import static com.formkiq.core.form.dto.WorkflowOutputDocumentType.PDF;
import static com.formkiq.core.util.Strings.extractLabelAndValue;
import static org.springframework.util.StringUtils.hasText;
import static org.springframework.util.StringUtils.isEmpty;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import com.fasterxml.jackson.core.JsonParseException;
import com.formkiq.core.dao.FolderDao;
import com.formkiq.core.dao.QueueDao;
import com.formkiq.core.domain.Folder;
import com.formkiq.core.domain.FolderAccess;
import com.formkiq.core.domain.FolderForm;
import com.formkiq.core.domain.User;
import com.formkiq.core.domain.type.ClientFormType;
import com.formkiq.core.domain.type.FolderDTO;
import com.formkiq.core.domain.type.FolderFormLedgerListDTO;
import com.formkiq.core.domain.type.FolderFormStatus;
import com.formkiq.core.domain.type.FolderFormsListDTO;
import com.formkiq.core.domain.type.FolderFormsSearchCriteria;
import com.formkiq.core.domain.type.FolderListDTO;
import com.formkiq.core.domain.type.FolderStatus;
import com.formkiq.core.domain.type.FolderUserListDTO;
import com.formkiq.core.domain.type.FormDTO;
import com.formkiq.core.form.JSONService;
import com.formkiq.core.form.dto.ArchiveDTO;
import com.formkiq.core.form.dto.DateAuditable;
import com.formkiq.core.form.dto.FormBuiltInType;
import com.formkiq.core.form.dto.FormJSON;
import com.formkiq.core.form.dto.FormJSONField;
import com.formkiq.core.form.dto.FormJSONSection;
import com.formkiq.core.form.dto.Workflow;
import com.formkiq.core.form.dto.WorkflowOutput;
import com.formkiq.core.form.dto.WorkflowOutputForm;
import com.formkiq.core.form.dto.WorkflowOutputPdf;
import com.formkiq.core.form.dto.WorkflowRoute;
import com.formkiq.core.form.dto.WorkflowRouteNotification;
import com.formkiq.core.form.dto.WorkflowStatus;
import com.formkiq.core.form.service.FormCalculatorService;
import com.formkiq.core.form.service.FormValidatorService;
import com.formkiq.core.service.dto.FormSaveResult;
import com.formkiq.core.service.generator.WorkflowOutputGenerator;
import com.formkiq.core.service.notification.FolderOnSaveEvent;
import com.formkiq.core.service.notification.NotificationService;
import com.formkiq.core.service.sign.PrintRenderer;
import com.formkiq.core.service.sign.SigningService;
import com.formkiq.core.util.DateService;
import com.formkiq.core.util.Strings;
import com.formkiq.core.util.Zips;
import com.google.common.collect.ImmutableMap;

/**
 * FormService implementation.
 *
 */
public class FolderServiceImpl implements FolderService {

    /** ArchiveService. */
    @Autowired
    private ArchiveService archiveService;

    /** AssetServiceRouter. */
    @Autowired
    private AssetServiceRouter assetService;

    /** ApplicationContext. */
    @Autowired
    private ApplicationContext context;

    /** {@link DateService}. */
    @Autowired
    private DateService dateService;

    /** FolderDao. */
    @Autowired
    private FolderDao folderDao;

    /** FormCalculatorService. */
    @Autowired
    private FormCalculatorService formcalcService;

    /** Jackson2ObjectMapperBuilder. */
    /** JSONService. */
    @Autowired
    private JSONService jsonService;

    /** NotificationService. */
    @Autowired
    private NotificationService notificationService;

    /** {@link PrintRenderer}. */
    @Autowired
    private PrintRenderer printRenderer;

    /** ApplicationEventPublisher. */
    @Autowired
    private ApplicationEventPublisher publisher;

    /** SpringSecurityService. */
    @Autowired
    private SpringSecurityService securityService;

    /** SigningService. */
    @Autowired
    private SigningService signingService;

    /** QueueDao. */
    @Autowired
    private QueueDao queueDao;

    /** Validator Service. */
    @Autowired
    private FormValidatorService validator;

    @Override
    public UUID addFolder(final String foldername) {
        Folder f = new Folder();
        f.setName(foldername);
        this.folderDao.saveFolder(f);
        return f.getFolderid();
    }

    /**
     * Check the Sync Status.
     * @param form {@link FolderForm}
     * @param formName {@link String}
     * @param parentuuid {@link String}
     * @param lastSha1hash {@link String}
     * @param isAdmin boolean
     */
    private void checkSyncStatus(final FolderForm form, final String formName, final String parentuuid,
            final String lastSha1hash, final boolean isAdmin) {

        if (form != null) {

            if (!isAdmin && isEmpty(parentuuid) && isEmpty(lastSha1hash)) {

                throw new FormNotFoundException("sha1hash parameter required");
            }

            if (hasText(lastSha1hash) && !lastSha1hash.equals(form.getSha1hash())) {

                throw new FormNotFoundException(formName + " has been modified. 'Sync' and try again.");
            }
        }
    }

    @Override
    public void createWorkflowOutput(final ArchiveDTO archive) throws IOException {

        Workflow workflow = archive.getWorkflow();

        List<WorkflowOutput> outputs = !CollectionUtils.isEmpty(workflow.getOutputs()) ? workflow.getOutputs()
                : new ArrayList<>();

        if (!CollectionUtils.isEmpty(workflow.getPrintsteps())) {
            WorkflowOutputPdf pdf = new WorkflowOutputPdf();
            pdf.setForms(workflow.getPrintsteps());
            outputs.add(pdf);
        }

        if (!CollectionUtils.isEmpty(outputs)) {

            for (WorkflowOutput wo : outputs) {

                if (FORM.equals(wo.getInputDocumentType())) {

                    WorkflowOutputForm wof = (WorkflowOutputForm) wo;

                    String uuid = extractLabelAndValue(wof.getForm()).getRight();
                    this.formcalcService.calculate(archive, uuid);

                } else if (PDF.equals(wo.getInputDocumentType())) {

                    byte[] pdf = this.printRenderer.createPDF(archive, (WorkflowOutputPdf) wo);
                    archive.addPDF(workflow.getUUID() + ".pdf", pdf);

                } else {

                    for (WorkflowOutputGenerator wog : this.context.getBeansOfType(WorkflowOutputGenerator.class)
                            .values()) {
                        if (wog.isSupported(wo)) {
                            wog.addOutputDocument(archive, wo);
                        }
                    }
                }
            }
        }
    }

    @Override
    public void deleteFolder(final UserDetails ud, final String folder) {

        User user = (User) ud;
        FolderAccess access = this.folderDao.findFolderAccess(user, folder);

        if (!this.securityService.hasPermission(access, PERM_FORM_ADMIN)) {
            throw new FormAccessDeniedException();
        }

        if (this.folderDao.getFolderAccessCount(folder) > 1) {
            throw new PreconditionFailedException("User permissions need to be removed from folder");
        }

        if (!this.folderDao.hasFiles(folder)) {

            this.folderDao.deleteFolder(folder);

        } else {

            throw new PreconditionFailedException("Forms need to be deleted first");
        }
    }

    @Override
    public void deleteFolderAccess(final UserDetails ud, final String folder) {
        User user = (User) ud;
        FolderAccess fa = this.folderDao.findFolderAccess(user, folder);
        this.folderDao.deleteFolderAccess(fa);
    }

    @Override
    public void deleteFolderFile(final UserDetails ud, final String folder, final String uuid,
            final boolean isparentuuid) {

        User user = (User) ud;
        FolderAccess access = this.folderDao.findFolderAccess(user, folder);

        if (!this.securityService.hasPermission(access, PERM_FORM_ADMIN)) {
            throw new FormAccessDeniedException();
        }

        if (isparentuuid) {

            this.folderDao.deleteForm(folder, uuid, isparentuuid);

        } else if (!this.folderDao.hasFormChildren(folder, uuid)) {

            this.folderDao.deleteForm(folder, uuid, isparentuuid);

            Map<String, String> map = ImmutableMap.<String, String>builder().put("folderid", folder)
                    .put("formid", uuid).build();

            this.queueDao.delete(map);

        } else {
            throw new PreconditionFailedException("Sub records need to be deleted first");
        }
    }

    @Override
    public FolderDTO findFolder(final UserDetails ud, final String folder) {
        User user = (User) ud;
        FolderDTO dto = this.folderDao.findFolderDTO(user, folder);

        if (dto == null) {
            throw new FormNotFoundException("Folder not found");
        }

        return dto;
    }

    @Override
    public FolderUserListDTO findFolderUsers(final String folder, final String token) {
        return this.folderDao.findFolderUsers(folder, token);
    }

    @Override
    public FormDTO findForm(final UserDetails ud, final String folder, final String uuid) {

        FormDTO dto = null;
        User user = (User) ud;

        FolderAccess access = this.folderDao.findFolderAccess(user, folder);

        if (access != null) {
            dto = this.folderDao.findFormDTO(folder, uuid);
        }

        if (dto == null || FolderFormStatus.DELETED.name().equals(dto.getStatus())) {
            throw new FormNotFoundException("Form " + uuid + " not found in folder " + folder);
        }

        return dto;
    }

    @Override
    public Pair<byte[], String> findFormData(final String folder, final String uuid) throws IOException {
        Pair<byte[], FolderForm> p = findFormDataInternal(folder, uuid);

        if (p.getRight() == null || FolderFormStatus.DELETED.equals(p.getRight().getStatus())) {
            throw new FormNotFoundException("Form " + uuid + " not found in folder " + folder);
        }

        return Pair.of(p.getLeft(), p.getRight().getSha1hash());
    }

    /**
     * Finds Form Data.
     * @param folder {@link String}
     * @param uuid {@link String}
     * @return {@link Pair}
     * @throws IOException IOException
     */
    private Pair<byte[], FolderForm> findFormDataInternal(final String folder, final String uuid)
            throws IOException {

        User user = (User) this.securityService.getUserDetails();
        Pair<FolderForm, FolderAccess> pair = this.folderDao.findForm(user, folder, uuid);

        FolderForm form = pair.getKey();

        if (form != null) {
            byte[] data = this.assetService.findAsset(folder, form.getAssetid().toString());
            return Pair.of(data, form);
        }

        throw new FormNotFoundException("form " + uuid + " not found");
    }

    @Override
    public FolderFormLedgerListDTO findFormLedger(final String folder, final String uuid) {

        FolderFormLedgerListDTO dto = new FolderFormLedgerListDTO();
        dto.setLedgers(this.folderDao.findFormLedger(folder, uuid));

        return dto;
    }

    @Override
    public List<WorkflowOutput> findFormOutputs(final String folder, final String uuid) throws IOException {

        User user = (User) this.securityService.getUserDetails();
        FolderAccess access = this.folderDao.findFolderAccess(user, folder);

        if (access != null) {

            if (!this.securityService.hasPermission(access, PERM_FORM_RESULTS)) {
                throw new FormAccessDeniedException();
            }

            String json = this.folderDao.findFormData(folder, uuid);
            Workflow wf = this.jsonService.readValue(json, Workflow.class);

            return wf.getOutputs();
        }

        throw new FormNotFoundException("Folder not found");
    }

    @Override
    public FolderFormsListDTO findForms(final String folder, final String uuid,
            final FolderFormsSearchCriteria criteria, final String token) {

        User user = (User) this.securityService.getUserDetails();
        FolderAccess access = this.folderDao.findFolderAccess(user, folder);

        if (access != null) {

            if (!this.securityService.hasPermission(access, PERM_FORM_RESULTS, PERM_FORM_ENTRY)) {
                throw new FormAccessDeniedException();
            }

            return this.folderDao.findForms(folder, uuid, criteria, token);
        }

        throw new FormNotFoundException("Folder not found");
    }

    /**
     * Finds the main FolderForm. If workflow is not null then
     * that is it. Otherwise forms should only have 1 record and
     * that is the main one.
     * @param workflow {@link FolderForm}
     * @param forms {@link List}
     * @return {@link FolderForm}
     */
    private FolderForm findMainFolderForm(final FolderForm workflow, final List<FolderForm> forms) {

        if (workflow != null) {
            return workflow;
        }

        if (forms.size() == 1) {
            return forms.iterator().next();
        }

        throw new PreconditionFailedException("Invalid Data");
    }

    /**
     * Generate SHA 256 for {@link FolderForm}.
     * @param user {@link User}
     * @param form {@link FolderForm}
     * @return {@link String}
     */
    private String generateSha256(final User user, final FolderForm form) {

        SimpleDateFormat sdf = new SimpleDateFormat(JSONService.DEFAULT_DATE_FORMAT);

        String data = user.getEmail() + sdf.format(form.getUpdatedDate()) + form.getFolderid() + form.getUUID()
                + form.getData();

        String sha256hash = DigestUtils.sha256Hex(data);
        return sha256hash;
    }

    /**
     * Get {@link FolderFormStatus}.
     * @param archive {@link ArchiveDTO}
     * @param currentStep {@link String}
     * @param errors {@link Map}
     * @return {@link FolderFormStatus}
     */
    private FolderFormStatus getFolderFormStatus(final ArchiveDTO archive, final String currentStep,
            final Map<String, String> errors) {

        FolderFormStatus status = FolderFormStatus.ACTIVE;

        if (archive.getWorkflow().isAllowinprocess() && !errors.isEmpty()) {
            status = FolderFormStatus.IN_PROCESS;
        } else if (isRoutingStep(currentStep, archive)) {
            status = FolderFormStatus.ROUTED;
        }

        return status;
    }

    @Override
    public FolderListDTO getFolderList(final String text, final String token) {
        return this.folderDao.findFolderList(text, token);
    }

    @Override
    public FolderListDTO getFolderList(final UserDetails ud, final String text, final String token) {
        User user = (User) ud;
        return this.folderDao.findFolderList(user, text, token);
    }

    /**
     * @param archive {@link ArchiveDTO}
     * @param labelform {@link String}
     * @param labelfield {@link String}
     * @return {@link String}
     */
    private String getLabel(final ArchiveDTO archive, final String labelform, final String labelfield) {

        String label = null;

        if (hasText(labelform) && hasText(labelfield)) {
            String formUUID = extractLabelAndValue(labelform).getRight();
            int id = Integer.parseInt(extractLabelAndValue(labelfield).getRight());
            Optional<FormJSON> form = archive.getForms().values().stream()
                    .filter(s -> formUUID.equals(s.getSourceFormUUID())).findFirst();

            if (form.isPresent()) {
                Optional<Pair<FormJSONSection, FormJSONField>> f = findSectionAndField(form.get(), id);
                if (f.isPresent()) {
                    label = f.get().getRight().getValue();
                }
            }
        }

        return label;
    }

    /**
     * Get Save Form Steps.
     * @param archive {@link ArchiveDTO}
     * @param formsMap {@link Map}
     * @return {@link List}
     */
    private List<String> getSaveFormsSteps(final ArchiveDTO archive, final Map<String, FolderForm> formsMap) {

        int saveCount = 0;
        List<String> results = new ArrayList<>();

        Workflow workflow = archive.getWorkflow();

        if (workflow != null) {

            if (hasText(workflow.getParentUUID())) {

                List<String> steps = workflow.getSteps();

                for (String step : steps) {

                    if (!formsMap.containsKey(step)) {

                        if (!isRoutingStep(step, archive)) {
                            results.add(step);
                            saveCount++;
                        } else if (saveCount > 0) {
                            break;
                        }
                    }
                }
            }

        } else {

            List<String> list = archive.getForms().values().stream().map(s -> s.getUUID())
                    .collect(Collectors.toList());
            results.addAll(list);
        }

        return results;
    }

    /**
     * Get Validation Save Workflow.
     * @param archive {@link ArchiveDTO}
     * @return {@link Workflow}
     */
    private Workflow getValidationSaveWorkflow(final ArchiveDTO archive) {
        Workflow workflow = archive.getWorkflow();

        if (workflow == null) {
            workflow = new Workflow();

            List<String> steps = archive.getForms().values().stream().map(s -> s.getUUID())
                    .collect(Collectors.toList());

            workflow.setSteps(steps);

            Optional<String> first = steps.stream().findFirst();
            if (first.isPresent()) {
                workflow.setParentUUID(archive.getForms().get(first.get()).getParentUUID());
            }
        }
        return workflow;
    }

    /**
     * Whether Archvie DTO has a Parent UUID.
     * @param archive {@link ArchiveDTO}
     * @return boolean
     */
    private boolean hasParentUUID(final ArchiveDTO archive) {

        Workflow workflow = archive.getWorkflow();

        int workflowParentCount = workflow != null && hasText(workflow.getParentUUID()) ? 1 : 0;

        Collection<FormJSON> forms = archive.getForms().values();

        int formCount = forms.size();
        long formParentCount = forms.stream().filter(e -> hasText(e.getParentUUID())).count();

        Collection<WorkflowRoute> routes = archive.getRoutes().values();
        int routeCount = routes.size();
        long routeParentCount = routes.stream().filter(e -> hasText(e.getParentUUID())).count();

        if (formParentCount > 0 && formCount != formParentCount) {
            throw new PreconditionFailedException("Not all forms have parent uuid set");
        }

        if (routeParentCount > 0 && routeCount != routeParentCount) {
            throw new PreconditionFailedException("Not all routes have parent uuid set");
        }

        return workflowParentCount > 0 || formParentCount > 0 || routeParentCount > 0;
    }

    /**
     * Whether to publish the folder on save event.
     * @param archive {@link ArchiveDTO}
     * @return boolean
     */
    private boolean isFolderOnSaveEvent(final ArchiveDTO archive) {

        Workflow wf = archive.getWorkflow();
        if (wf != null && hasText(wf.getParentUUID())) {
            return true;
        }

        for (Map.Entry<String, FormJSON> e : archive.getForms().entrySet()) {

            FormJSON form = e.getValue();
            String parentuuid = form.getParentUUID();
            if (hasText(parentuuid)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Is Current Step a routing Step.
     * @param step {@link String}
     * @param archive {@link ArchiveDTO}
     * @return boolean
     */
    private boolean isRoutingStep(final String step, final ArchiveDTO archive) {

        boolean found = false;
        Workflow wf = archive.getWorkflow();

        if (wf != null) {

            if (hasText(step)) {
                found = archive.getRoutes().get(step) != null;

                if (!found) {
                    FormJSON form = archive.getForms().get(step);

                    found = form != null && FormBuiltInType.DOCSIGN.toString().equals(form.getBuiltintype());
                }
            }
        }

        return found;
    }

    /**
     * Loads Folder Forms from database.
     *
     * @param archive {@link ArchiveDTO}
     * @param folder {@link String}
     * @param isAdmin boolean
     * @return {@link Map}
     */
    private Map<String, FolderForm> loadFolderForms(final ArchiveDTO archive, final String folder,
            final boolean isAdmin) {

        Map<String, FolderForm> map = new HashMap<>();

        User user = (User) this.securityService.getUserDetails();

        Workflow workflow = archive.getWorkflow();

        if (workflow != null) {

            FolderForm ff = isAdmin ? this.folderDao.findForm(folder, workflow.getUUID())
                    : this.folderDao.findForm(user, folder, workflow.getUUID()).getLeft();

            if (ff != null) {
                map.put(workflow.getUUID(), ff);
            }
        }

        for (FormJSON formJSON : archive.getForms().values()) {

            FolderForm form = isAdmin ? this.folderDao.findForm(folder, formJSON.getUUID())
                    : this.folderDao.findForm(user, folder, formJSON.getUUID()).getLeft();

            if (form != null) {
                map.put(formJSON.getUUID(), form);
            }
        }

        return map;
    }

    /**
     * Process Workflow Routing.
     * @param folder {@link String}
     * @param route {@link WorkflowRoute}
     * @param archive {@link ArchiveDTO}
     */
    private void processRouting(final String folder, final WorkflowRoute route, final ArchiveDTO archive) {

        for (WorkflowRouteNotification notify : route.getNotifications()) {
            this.notificationService.sendRoutingNotification(folder, notify.getMethod(), notify.getEmail(),
                    archive);
        }
    }

    @Override
    public UUID saveFolder(final UserDetails ud, final String folder, final String foldername) {

        User user = (User) ud;

        if (hasText(folder)) {

            FolderAccess fa = this.folderDao.findFolderAccess(user, folder);

            if (fa != null) {

                if (!this.securityService.hasPermission(fa, PERM_FORM_ADMIN)) {
                    throw new FormAccessDeniedException();
                }

                Folder f = this.folderDao.findFolder(folder);
                f.setName(foldername);
                this.folderDao.saveFolder(f);
                return f.getFolderid();

            }

            throw new FormNotFoundException(foldername);
        }

        UUID folderId = addFolder(foldername);

        FolderAccess fa = new FolderAccess();
        fa.setStatus(FolderStatus.ACTIVE);
        fa.setFolderid(folderId);
        fa.setUserid(user.getUserid());
        fa.setPermissions(Arrays.asList(PERM_FORM_ADMIN));
        this.folderDao.saveFolderAccess(fa);

        return folderId;
    }

    @Override
    public FormSaveResult saveForm(final String folder, final ArchiveDTO archive, final String lastSha1hash,
            final boolean isAdmin) throws IOException {

        if (hasParentUUID(archive)) {
            return saveFormWithParentUUID(folder, archive, lastSha1hash, isAdmin);
        }

        return saveFormWithoutParentUUID(folder, archive, lastSha1hash, isAdmin);
    }

    // Save without parent.. Workflow, Form, Route
    @Override
    public FormSaveResult saveForm(final String folder, final byte[] bytes, final String lastSha1hash,
            final boolean isAdmin) throws IOException {

        ArchiveDTO archive = null;

        try {
            archive = this.jsonService.readValue(bytes, ArchiveDTO.class);
        } catch (JsonParseException e) {
            archive = this.archiveService.extractJSONFromZipFile(bytes);
        }

        return saveForm(folder, archive, lastSha1hash, isAdmin);
    }

    /**
     * Saves Forms.
     * @param archive {@link ArchiveDTO}
     * @param workflow {@link FolderForm}
     * @param folder {@link String}
     * @param formsMap {@link Map}
     * @param status {@link FolderFormStatus}
     * @return {@link String} - SHA1Hash
     * @throws IOException IOException
     */
    private List<FolderForm> saveForms(final ArchiveDTO archive, final FolderForm workflow, final String folder,
            final Map<String, FolderForm> formsMap, final FolderFormStatus status) throws IOException {

        String sha1hash = null;
        List<FolderForm> formlist = new ArrayList<>();
        User user = (User) this.securityService.getUserDetails();

        if (workflow != null && hasText(workflow.getSha1hash())) {
            sha1hash = workflow.getSha1hash();
        }

        Map<String, FormJSON> forms = archive.getForms();
        List<String> steps = getSaveFormsSteps(archive, formsMap);

        for (String step : steps) {

            FormJSON form = forms.get(step);

            String uuid = form.getUUID();
            String parentuuid = form.getParentUUID();
            String data = this.jsonService.writeValueAsString(form);

            if (formsMap.containsKey(uuid) && hasText(parentuuid)) {
                continue;
            }

            FolderForm folderForm = formsMap.containsKey(uuid) ? formsMap.get(uuid) : new FolderForm();

            updateDates(folderForm, form);
            folderForm.setType(ClientFormType.FORM);
            folderForm.setFolderid(UUID.fromString(folder));
            folderForm.setUUID(UUID.fromString(uuid));
            folderForm.setData(data);
            folderForm.setStatus(status);

            folderForm.setInsertedDate(form.getInserteddate());
            folderForm.setUpdatedDate(form.getUpdateddate());

            if (hasText(parentuuid)) {
                folderForm.setParentUUID(UUID.fromString(parentuuid));
            }

            String sha256 = generateSha256(user, folderForm);

            if (isEmpty(parentuuid)) {

                byte[] zipfile = Zips.zipFile(form.getUUID() + ".form", data);

                updateAsset(folderForm, folder, zipfile, sha1hash);
                this.folderDao.saveForm(user, folderForm, sha256);

            } else if (workflow == null) {

                byte[] bytes = this.archiveService.createZipFile(archive);
                updateAsset(folderForm, folder, bytes, null);
                this.folderDao.saveForm(user, folderForm, sha256);

            } else {

                folderForm.setSha1hash(sha1hash);
                folderForm.setType(ClientFormType.WORKFLOW_FORM);
                this.folderDao.saveForm(user, folderForm, sha256);
            }

            if (isEmpty(sha1hash)) {
                sha1hash = folderForm.getSha1hash();
            }

            formlist.add(folderForm);
        }

        return formlist;
    }

    /**
     * Saves ArchiveDTO without Parent UUID.
     * @param folder {@link String}
     * @param archive {@link ArchiveDTO}
     * @param lastSha1hash {@link String}
     * @param isAdmin boolean
     * @return {@link FormSaveResult}
     * @throws IOException IOException
     * @throws FormValidatorException FormValidatorException
     */
    private FormSaveResult saveFormWithoutParentUUID(final String folder, final ArchiveDTO archive,
            final String lastSha1hash, final boolean isAdmin) throws IOException {

        FolderForm workflow = null;
        Map<String, String> errors = Collections.emptyMap();

        if (archive.getRoutes().size() > 1) {
            throw new PreconditionFailedException("Data can only contain 1 route");
        }

        FolderFormStatus status = FolderFormStatus.ACTIVE;

        validateSaveFormFolderAccess(archive, folder, isAdmin);

        Map<String, FolderForm> formsMap = loadFolderForms(archive, folder, isAdmin);

        validateFormsSync(archive, folder, formsMap, lastSha1hash, isAdmin);

        errors = this.validator.validate(archive);

        //        errors = validateFormAndGetCurrentStep(archive, formsMap).getRight();

        if (errors.isEmpty()) {

            Pair<FolderForm, Map<String, String>> pair = saveWorkflow(archive, folder, formsMap, status);
            workflow = pair.getLeft();
            errors = pair.getRight();
        }

        String sha1hash = workflow != null ? workflow.getSha1hash() : null;

        return new FormSaveResult(null, sha1hash, errors);
    }

    /**
     * Saves ArchiveDTO with Parent UUID.
     * @param folder {@link String}
     * @param archive {@link ArchiveDTO}
     * @param lastSha1hash {@link String}
     * @param isAdmin boolean
     * @return {@link FormSaveResult}
     * @throws IOException IOException
     */
    private FormSaveResult saveFormWithParentUUID(final String folder, final ArchiveDTO archive,
            final String lastSha1hash, final boolean isAdmin) throws IOException {

        String sha1hash = null;
        FolderFormStatus status = null;
        Workflow w = archive.getWorkflow();
        boolean allowinprocess = w.isAllowinprocess();

        Map<String, String> errors = Collections.emptyMap();
        validateSaveFormFolderAccess(archive, folder, isAdmin);
        validateFormsParent(archive, folder, lastSha1hash, isAdmin);

        Map<String, FolderForm> formsMap = loadFolderForms(archive, folder, isAdmin);

        updateWorkflowLabels(archive);

        validateFormsSync(archive, folder, formsMap, lastSha1hash, isAdmin);

        Pair<String, Map<String, String>> pair = validateFormAndGetCurrentStep(archive, formsMap);
        String currentStep = pair.getLeft();
        errors = pair.getRight();

        if (errors.isEmpty() || allowinprocess) {

            status = getFolderFormStatus(archive, currentStep, errors);

            w.setStatus(
                    FolderFormStatus.IN_PROCESS.equals(status) ? WorkflowStatus.IN_PROCESS : WorkflowStatus.DONE);

            if (FolderFormStatus.ACTIVE.equals(status)) {
                createWorkflowOutput(archive);
            }

            boolean publishSaveEvent = isFolderOnSaveEvent(archive);

            Pair<FolderForm, Map<String, String>> pworkflow = saveWorkflow(archive, folder, formsMap, status);
            FolderForm workflow = pworkflow.getLeft();
            errors = pworkflow.getRight();

            if (errors.isEmpty()) {

                List<FolderForm> forms = saveForms(archive, workflow, folder, formsMap, status);

                FolderForm mainRecord = findMainFolderForm(workflow, forms);

                WorkflowRoute route = archive.getRoutes().get(currentStep);
                if (route != null) {
                    processRouting(folder, route, archive);
                }

                sendDocsignIfStep(folder, mainRecord, archive, currentStep);
                updateDocsignResultIfStep(folder, archive, currentStep);

                if (publishSaveEvent) {
                    User user = (User) this.securityService.getUserDetails();
                    this.publisher.publishEvent(new FolderOnSaveEvent(user, folder, archive));
                }

                sha1hash = workflow != null ? workflow.getSha1hash() : forms.get(0).getSha1hash();
            }
        }

        return new FormSaveResult(status, sha1hash, errors);
    }

    /**
     * Save Workflow.
     * @param archive {@link ArchiveDTO}
     * @param folder {@link String}
     * @param formsMap {@link Map}
     * @param status {@link FolderFormStatus}
     * @return {@link Pair}
     * @throws IOException IOException
     */
    private Pair<FolderForm, Map<String, String>> saveWorkflow(final ArchiveDTO archive, final String folder,
            final Map<String, FolderForm> formsMap, final FolderFormStatus status) throws IOException {

        Workflow wf = archive.getWorkflow();
        // TODO test..
        Map<String, String> errors = this.validator.validate(wf);

        if (wf != null && errors.isEmpty()) {

            String uuid = wf.getUUID();
            String parentuuid = wf.getParentUUID();

            FolderForm form = formsMap.containsKey(uuid) ? formsMap.get(uuid) : new FolderForm();

            updateDates(form, wf);
            form.setType(ClientFormType.WORKFLOW);
            form.setFolderid(UUID.fromString(folder));
            form.setUUID(UUID.fromString(uuid));

            String workflowJSON = this.jsonService.writeValueAsString(wf);
            form.setData(workflowJSON);
            form.setStatus(status);

            if (hasText(parentuuid)) {
                form.setParentUUID(UUID.fromString(parentuuid));
            }

            byte[] data = this.archiveService.createZipFile(archive);
            updateAsset(form, folder, data, null);

            User user = (User) this.securityService.getUserDetails();
            String sha256 = generateSha256(user, form);

            this.folderDao.saveForm(user, form, sha256);

            return Pair.of(form, errors);
        }

        return Pair.of(null, errors);
    }

    /**
     * Send Email when Docsign is Completed.
     * @param archive {@link ArchiveDTO}
     * @param form {@link FormJSON}
     * @throws IOException IOException
     */
    private void sendDocsignCompleteEmail(final ArchiveDTO archive, final FormJSON form) throws IOException {

        String emailsubject = "Completed signing " + archive.getName();

        String body = IOUtils.toString(
                this.context.getResource("classpath:/templates/email/sign-complete.html").getInputStream(),
                Strings.CHARSET_UTF8);

        String signer1name = findValueByKey(form, "signer1name").get().getValue();
        body = body.replaceAll("\\$\\{signername\\}", signer1name);

        User user = (User) this.securityService.getUserDetails();
        this.notificationService.sendEmail(user.getEmail(), emailsubject, body, null, null);
    }

    /**
     * Send Docsign If Step.
     * @param folder {@link String}
     * @param mainRecord {@link FolderForm}
     * @param archive {@link ArchiveDTO}
     * @param step {@link String}
     * @throws IOException IOException
     */
    private void sendDocsignIfStep(final String folder, final FolderForm mainRecord, final ArchiveDTO archive,
            final String step) throws IOException {

        FormJSON form = archive.getForm(step, FormBuiltInType.DOCSIGN);

        if (form != null) {
            this.signingService.sendSigningRequest(mainRecord, form);
        }
    }

    /**
     * Update Asset on FolderForm.
     * @param form {@link FolderForm}
     * @param folder {@link String}
     * @param bytes byte[]
     * @param sha1hash {@link String}
     */
    private void updateAsset(final FolderForm form, final String folder, final byte[] bytes,
            final String sha1hash) {

        UUID assetid = form.getAssetid();
        if (assetid == null) {
            assetid = UUID.randomUUID();
        }

        if (bytes != null) {
            this.assetService.saveAsset(folder, assetid.toString(), bytes);

            form.setAssetid(assetid);
            form.setSha1hash(DigestUtils.sha1Hex(bytes));
        }

        if (hasText(sha1hash)) {
            form.setSha1hash(sha1hash);
        }
    }

    /**
     * Update {@link Workflow} dates if missing.
     * @param form {@link FolderForm}
     * @param audit {@link DateAuditable}
     */
    private void updateDates(final FolderForm form, final DateAuditable audit) {
        form.setInsertedDate(audit.getInserteddate());
        form.setUpdatedDate(audit.getUpdateddate());

        Date now = this.dateService.now();
        if (form.getInsertedDate() == null) {
            form.setInsertedDate(now);
            audit.setInserteddate(now);
        }

        form.setUpdatedDate(now);
        audit.setUpdateddate(now);
    }

    /**
     * Update Docsign Result Step.
     * @param folder {@link String}
     * @param archive {@link ArchiveDTO}
     * @param step {@link String}
     * @throws IOException IOException
     */
    private void updateDocsignResultIfStep(final String folder, final ArchiveDTO archive, final String step)
            throws IOException {

        FormJSON docsignResult = archive.getForm(step, FormBuiltInType.DOCSIGN_RESULT);

        if (docsignResult != null) {

            Map<String, String> map = ImmutableMap.<String, String>builder().put("folderid", folder)
                    .put("formid", archive.getWorkflow().getUUID()).build();

            this.queueDao.delete(map);

            FormJSON docsign = archive.getForm(FormBuiltInType.DOCSIGN);
            sendDocsignCompleteEmail(archive, docsign);
        }
    }

    /**
     * Update Workflow Labels.
     * @param archive {@link ArchiveDTO}
     */
    private void updateWorkflowLabels(final ArchiveDTO archive) {
        Workflow workflow = archive.getWorkflow();

        if (isEmpty(workflow.getLabel1())) {
            workflow.setLabel1(getLabel(archive, workflow.getLabel1form(), workflow.getLabel1field()));
        }

        if (isEmpty(workflow.getLabel2())) {
            workflow.setLabel2(getLabel(archive, workflow.getLabel2form(), workflow.getLabel2field()));
        }

        if (isEmpty(workflow.getLabel3())) {
            workflow.setLabel3(getLabel(archive, workflow.getLabel3form(), workflow.getLabel3field()));
        }
    }

    /**
     * Validates Forms and returns the current step.
     * @param archive {@link ArchiveDTO}
     * @param formsMap {@link Map}
     * @return {@link Pair}
     */
    private Pair<String, Map<String, String>> validateFormAndGetCurrentStep(final ArchiveDTO archive,
            final Map<String, FolderForm> formsMap) {

        String currentstep = null;
        WorkflowRoute currentRoute = null;
        Map<String, String> errors = Collections.emptyMap();

        Workflow workflow = getValidationSaveWorkflow(archive);

        // validate Entries
        for (String step : workflow.getSteps()) {

            FormJSON form = archive.getForm(step);
            WorkflowRoute route = archive.getRoute(step);

            if (form != null) {

                Map<String, String> errs = this.validator.validateFormJSON(archive, form);

                if (errs.isEmpty()) {
                    validateUserHasAccessToRoute(currentRoute);
                    currentstep = step;
                } else if (currentRoute == null) {
                    errors = errs;
                } else {
                    break;
                }

            } else if (route != null) {
                currentRoute = route;
                currentstep = step;
            } else {
                throw new FormNotFoundException("form " + step + " not found");
            }
        }

        return Pair.of(currentstep, errors);
    }

    /**
     * Validate Form Parent Exists.
     * @param archive {@link ArchiveDTO}
     * @param folder {@link String}
     * @param lastSha1hash {@link String}
     * @param isAdmin boolean
     */
    private void validateFormsParent(final ArchiveDTO archive, final String folder, final String lastSha1hash,
            final boolean isAdmin) {

        User user = (User) this.securityService.getUserDetails();

        for (FormJSON formJSON : archive.getForms().values()) {

            String parentuuid = formJSON.getParentUUID();

            if (!isEmpty(parentuuid)) {

                FolderForm ff = isAdmin ? this.folderDao.findForm(folder, parentuuid)
                        : this.folderDao.findForm(user, folder, parentuuid).getLeft();

                if (ff == null && StringUtils.isEmpty(formJSON.getBuiltintype())) {
                    throw new FormNotFoundException("form " + parentuuid + " not found");
                }
            }
        }
    }

    /**
     * Validate Form with out Parent (IE: top level).
     * @param archive {@link ArchiveDTO}
     * @param folder {@link String}
     * @param formsMap {@link Map}
     * @param lastSha1hash {@link String}
     * @param isAdmin boolean
     * @return {@link Map}
     */
    private Map<String, FolderForm> validateFormsSync(final ArchiveDTO archive, final String folder,
            final Map<String, FolderForm> formsMap, final String lastSha1hash, final boolean isAdmin) {

        Map<String, FolderForm> map = new HashMap<>();

        Workflow workflow = archive.getWorkflow();

        if (workflow != null) {
            FolderForm form = formsMap.get(workflow.getUUID());
            checkSyncStatus(form, workflow.getName(), workflow.getParentUUID(), lastSha1hash, isAdmin);
        }

        for (FormJSON formJSON : archive.getForms().values()) {

            FolderForm form = formsMap.get(formJSON.getUUID());

            checkSyncStatus(form, formJSON.getName(), formJSON.getParentUUID(), lastSha1hash, isAdmin);
        }

        return map;
    }

    /**
     * Validate user has save access to folder.
     * @param archive {@link ArchiveDTO}
     * @param folder {@link String}
     * @param isAdmin boolean
     */
    private void validateSaveFormFolderAccess(final ArchiveDTO archive, final String folder,
            final boolean isAdmin) {

        if (!isAdmin) {
            User user = (User) this.securityService.getUserDetails();
            FolderAccess access = this.folderDao.findFolderAccess(user, folder);

            Workflow workflow = getValidationSaveWorkflow(archive);

            if (access != null) {

                String parentuuid = workflow.getParentUUID();

                if (!isEmpty(parentuuid) && !this.securityService.hasPermission(access, PERM_FORM_ENTRY)) {
                    throw new FormAccessDeniedException();
                }

                if (isEmpty(parentuuid) && !this.securityService.hasPermission(access, PERM_FORM_DESIGN)) {
                    throw new FormAccessDeniedException();
                }

            } else {

                throw new FormAccessDeniedException();
            }
        }
    }

    /**
     * Validates User has access to route.
     * @param wr {@link WorkflowRoute}
     */
    private void validateUserHasAccessToRoute(final WorkflowRoute wr) {

        if (wr != null) {

            User us = (User) this.securityService.getUserDetails();

            boolean match = wr.getNotifications().stream().anyMatch(s -> s.getEmail().equals(us.getEmail()));

            if (!match) {
                throw new PreconditionFailedException("User not allowed to continue form");
            }
        }
    }
}