com.formkiq.core.service.workflow.WorkflowServiceImpl.java Source code

Java tutorial

Introduction

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

Source

/*
 * Copyright (C) 2017 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.workflow;

import static com.formkiq.core.webflow.FlowStateType.DEFAULT;
import static com.formkiq.core.webflow.FlowStateType.END;
import static com.formkiq.core.webflow.FlowStateType.START;
import static com.formkiq.core.webflow.FlowTransition.BACKWARD;
import static com.formkiq.core.webflow.FlowTransition.FORWARD;
import static org.springframework.util.StringUtils.hasText;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.ModelAndView;

import com.formkiq.core.domain.QueueMessage;
import com.formkiq.core.form.dto.ArchiveDTO;
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.FormJSONFieldType;
import com.formkiq.core.form.dto.FormJSONRequiredType;
import com.formkiq.core.form.dto.FormJSONSection;
import com.formkiq.core.form.dto.Workflow;
import com.formkiq.core.form.service.FormCalculatorService;
import com.formkiq.core.form.service.FormEventHandler;
import com.formkiq.core.form.service.FormValidatorService;
import com.formkiq.core.service.ArchiveService;
import com.formkiq.core.service.FolderService;
import com.formkiq.core.service.PreconditionFailedException;
import com.formkiq.core.service.SpringSecurityService;
import com.formkiq.core.service.notification.NotificationService;
import com.formkiq.core.service.queue.DocSignQueueMessage;
import com.formkiq.core.service.sign.PrintRenderer;
import com.formkiq.core.service.sign.SigningService;
import com.formkiq.core.webflow.FlowEndedException;
import com.formkiq.core.webflow.FlowEventProcessor;
import com.formkiq.core.webflow.FlowManager;
import com.formkiq.core.webflow.FlowNotFoundException;
import com.formkiq.core.webflow.FlowNotSupportedException;
import com.formkiq.core.webflow.FlowState;
import com.formkiq.core.webflow.FlowStateType;
import com.formkiq.core.webflow.WebFlow;
import com.google.common.collect.ImmutableMap;

/**
 * Implementation of Workflow Service.
 *
 */
public class WorkflowServiceImpl implements WorkflowService {

    /** Logger. */
    private static final Logger LOG = Logger.getLogger(WorkflowServiceImpl.class.getName());

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

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

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

    /** FolderService. */
    @Autowired
    private FolderService folderService;

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

    /** PrintRenderer. */
    @Autowired
    private PrintRenderer printRenderer;

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

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

    /** ValidatorService. */
    @Autowired
    private FormValidatorService validatorService;

    @Override
    public ModelAndView actions(final HttpServletRequest request, final HttpServletResponse response)
            throws IOException {

        WebFlow flow = FlowManager.get(request);

        Map<String, String[]> parameterMap = request.getParameterMap();

        if (parameterMap.containsKey("_eventId_print")) {
            return getModelAndView(flow, DEFAULT_PDF_VIEW);
        }

        byte[] bytes = generatePDF(request, flow);

        response.setContentType("application/pdf");
        response.setContentLengthLong(bytes.length);
        IOUtils.write(bytes, response.getOutputStream());
        response.getOutputStream().flush();

        return null;
    }

    @Override
    public Map<String, String> applyValuesToState(final HttpServletRequest request) throws IOException {

        Map<String, String> map = Collections.emptyMap();
        WebFlow flow = FlowManager.get(request);
        ArchiveDTO archive = flow.getData();
        FlowState current = flow.getCurrentState();

        if (current.getData() instanceof FormJSON) {
            FormJSON form = current.getData();

            // make sure applying values to correct form
            if (form.getUUID().equals(request.getParameter("formuuid"))) {
                map = applyValuesToState(request, archive, form);
            }
        }

        return map;
    }

    /**
     * Apply Field Values to Form.
     * @param request {@link HttpServletRequest}
     * @param archive {@link ArchiveDTO}
     * @param form {@link FormJSON}
     * @return {@link Map}
     * @throws IOException IOException
     */
    @Deprecated
    private Map<String, String> applyValuesToState(final HttpServletRequest request, final ArchiveDTO archive,
            final FormJSON form) throws IOException {

        return this.calculatorService.applyFieldValues(archive, form, request);
    }

    @Override
    public List<FlowState> createStates(final ArchiveDTO archive) {
        return createStates(archive, archive.getWorkflow().getSteps());
    }

    @Override
    public List<FlowState> createStates(final ArchiveDTO archive, final List<String> formUUIDs) {

        List<FlowState> states = new ArrayList<>();
        states.add(new FlowState(FlowStateType.START));

        states.add(new FlowState(FlowStateType.DEFAULT, archive.getWorkflow()));

        if (!CollectionUtils.isEmpty(formUUIDs)) {

            for (String formUUID : formUUIDs) {
                states.add(new FlowState(FlowStateType.DEFAULT, archive.getForm(formUUID)));
            }
        }

        states.add(new FlowState(FlowStateType.END));
        return states;
    }

    /**
     * Create FSM States.
     * @param dto {@link ArchiveDTO}
     * @param types {@link FormJSONRequiredType}
     * @return {@link List}
     */
    private List<FlowState> createStates(final ArchiveDTO dto, final FormJSONRequiredType... types) {

        boolean addAll = false;
        List<FlowState> states = new ArrayList<>();
        states.add(new FlowState(FlowStateType.START));

        Workflow workflow = dto.getWorkflow();

        for (String uuid : workflow.getSteps()) {

            FormJSON form = dto.getForm(uuid);
            boolean skipForm = isSkipForm(form);

            if (!skipForm && hasText(form.getBuiltintype()) && !FormBuiltInType.contains(form.getBuiltintype())) {
                throw new FlowNotSupportedException();
            }

            if (!skipForm) {

                Map<String, String> errors = isValidatedCurrentForm(dto, form, types);
                // TODO fix if all form has no required fields.
                // if form doesn't have any required fields, it doesn't display
                if (!errors.isEmpty() || addAll) {

                    FormBuiltInType builtIn = FormBuiltInType.find(form.getBuiltintype());

                    if (builtIn != null && !FormBuiltInType.DOCSIGN.equals(builtIn)
                            && !FormBuiltInType.EQUIFAX_CREDIT_CHECK.equals(builtIn)) {
                        states.add(new FlowState(FlowStateType.DEFAULT, form, "flow/" + builtIn.getCode()));
                    } else {
                        states.add(new FlowState(FlowStateType.DEFAULT, form, "flow/workflow"));
                    }

                    addAll = true;
                }
            }
        }

        states.add(new FlowState(FlowStateType.END, null, "flow/complete"));

        return states;
    }

    /**
     * Ends Flow.
     * @param request {@link HttpServletRequest}
     * @return boolean whether flow was ended
     * @throws IOException IOException
     */
    private Pair<ModelAndView, WebFlow> endFlow(final HttpServletRequest request) throws IOException {

        Pair<ModelAndView, WebFlow> result = null;
        WebFlow flow = FlowManager.get(request);
        ArchiveDTO archive = flow.getData();

        FormJSON documentSignResult = null;
        DocSignQueueMessage qmsg = flow.getParameter("docsignmsg");

        if (qmsg != null) {

            Workflow workflow = archive.getWorkflow();
            this.securityService.login(qmsg.getUserid());

            documentSignResult = this.signingService.createDocsignResult(request, workflow);
            workflow.getSteps().add(documentSignResult.getUUID());
            archive.addForm(documentSignResult);

            byte[] bytes = generatePDF(request, flow);
            archive.addPDF(workflow.getUUID() + ".pdf", bytes);
        }

        boolean rolledback = false;
        try {
            String folder = flow.getParameter("folder");
            Map<String, String> errors = this.folderService.saveForm(folder, archive, null, false).getErrors();

            if (!errors.isEmpty()) {
                result = rollbackStep(request, archive, documentSignResult);
                rolledback = true;
                result.getRight().getCurrentState().setFielderrors(errors);
            }

        } catch (Exception ex) {

            LOG.log(Level.WARNING, ex.getMessage(), ex);

            if (!rolledback) {
                result = rollbackStep(request, archive, documentSignResult);
            }

        } finally {

            if (qmsg != null) {
                this.securityService.logout();
            }
        }

        return result;
    }

    @Override
    public byte[] generatePDF(final HttpServletRequest request, final WebFlow flow) throws IOException {

        Map<String, Object> map = ImmutableMap.of("flow", flow);
        String html = this.printRenderer.generateHTML(request, DEFAULT_PDF_VIEW, map);
        byte[] bytes = this.printRenderer.createPDF(html);
        return bytes;
    }

    @Override
    public ModelAndView getModelAndView(final WebFlow flow, final String viewName) {

        Map<String, Object> mavmap = new HashMap<>();
        mavmap.put("flow", flow);

        return new ModelAndView(viewName, mavmap);
    }

    /**
     * Whether to skip form.
     * @param form {@link FormJSON}
     * @return boolean
     */
    private boolean isSkipForm(final FormJSON form) {
        return FormBuiltInType.ID_BARCODE.name().equals(form.getBuiltintype());
    }

    /**
     * Validate Current Form.
     * @param archive {@link ArchiveDTO}
     * @param form {@link FormJSON}
     * @param types {@link FormJSONRequiredType}
     * @return {@link Map}
     */
    private Map<String, String> isValidatedCurrentForm(final ArchiveDTO archive, final FormJSON form,
            final FormJSONRequiredType... types) {

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

        if (form != null) {
            errors = this.validatorService.validateFormJSON(archive, form, types);
        }

        return errors;
    }

    /**
     * Transition to next event.
     * @param request {@link HttpServletRequest}
     * @return {@link Pair}
     * @throws IOException IOException
     */
    private Pair<ModelAndView, WebFlow> next(final HttpServletRequest request) throws IOException {

        Pair<ModelAndView, WebFlow> result = null;
        WebFlow flow = FlowManager.get(request);
        ArchiveDTO archive = flow.getData();
        FlowState state = flow.getCurrentState();
        FormJSON form = state.getData();

        applyValuesToState(request, archive, form);

        Map<String, String> errors = isValidatedCurrentForm(archive, form);
        state.setFielderrors(errors);

        if (errors.isEmpty()) {

            if (form != null && !CollectionUtils.isEmpty(form.getEvents())) {

                for (Map.Entry<String, String> e : form.getEvents().entrySet()) {
                    handleFormEvents(form, e.getKey());
                }
            }

            flow = FlowManager.transition(request, FORWARD);
        }

        state = flow.getCurrentState();
        if (FlowStateType.END.equals(state.getType())) {
            result = endFlow(request);
        }

        if (result == null) {
            result = Pair.of(new ModelAndView("redirect:/flow/workflow?execution=" + flow.getSessionKey()), flow);
        }

        return result;
    }

    /**
     * Transition to previous state.
     * @param request {@link HttpServletRequest}
     * @return {@link Pair}
     */
    private Pair<ModelAndView, WebFlow> previous(final HttpServletRequest request) {
        WebFlow flow = FlowManager.transition(request, BACKWARD);
        return Pair.of(new ModelAndView("redirect:/flow/workflow?execution=" + flow.getSessionKey()), flow);
    }

    /**
     * Rollback to previous step.
     * @param request {@link HttpServletRequest}
     * @param archive {@link ArchiveDTO}
     * @param documentSignResult {@link FormJSON}
     * @return {@link Pair}
     */
    private Pair<ModelAndView, WebFlow> rollbackStep(final HttpServletRequest request, final ArchiveDTO archive,
            final FormJSON documentSignResult) {

        Pair<ModelAndView, WebFlow> result;
        result = previous(request);

        if (documentSignResult != null) {
            Workflow workflow = archive.getWorkflow();
            workflow.getSteps().remove(documentSignResult.getUUID());
            archive.removeForm(documentSignResult);
        }
        return result;
    }

    @Transactional
    @Override
    public <T extends FlowEventProcessor> Pair<ModelAndView, WebFlow> start(final HttpServletRequest request,
            final Class<T> eventProcessor, final String folder, final String uuid) throws IOException {

        ArchiveDTO dto = this.archiveService.get(folder, uuid, true).getLeft();

        if (dto == null) {
            throw new FlowNotFoundException();
        }

        List<FlowState> states = createStates(dto, FormJSONRequiredType.BEFORE_SUBMIT,
                FormJSONRequiredType.IMMEDIATE);

        WebFlow flow = startFlow(request, folder, null, dto, eventProcessor, states);
        return Pair.of(new ModelAndView("redirect:/flow/workflow?execution=" + flow.getSessionKey()), flow);
    }

    @Override
    public <T extends FlowEventProcessor> WebFlow startFlow(final HttpServletRequest request, final String folder,
            final String sha1hash, final ArchiveDTO dto, final Class<T> eventProcessor,
            final List<FlowState> states) {

        WebFlow flow = FlowManager.start(request, eventProcessor, states);
        flow.setParameter("sha1hash", sha1hash);
        flow.setParameter("folder", folder);
        flow.setData(dto);

        return flow;
    }

    @Transactional
    @Override
    public Pair<ModelAndView, WebFlow> startSigning(final HttpServletRequest request, final String document,
            final String stoken) throws IOException {

        Triple<DocSignQueueMessage, ArchiveDTO, QueueMessage> p = this.signingService.findAssets(document, stoken);

        DocSignQueueMessage msg = p.getLeft();

        ArchiveDTO dto = p.getMiddle();

        updateSignaturesToRequired(dto.getForms().values());

        List<FlowState> states = new ArrayList<>();
        states.add(new FlowState(START));
        FlowState s1 = new FlowState(DEFAULT, null, "flow/summary");
        s1.setParameter("geoLocation", Boolean.TRUE);
        states.add(s1);
        states.add(new FlowState(END, null, "flow/complete"));

        WebFlow flow = startFlow(request, msg.getFolderid(), null, dto, WorkflowEditorServiceImpl.class, states);
        flow.setParameter("docsignmsg", msg);

        return Pair.of(new ModelAndView("redirect:/flow/workflow?execution=" + flow.getSessionKey()), flow);
    }

    @Override
    public Pair<ModelAndView, WebFlow> state(final HttpServletRequest request) throws IOException {

        ModelAndView mv;
        WebFlow flow = FlowManager.get(request);
        FlowState state = flow.getCurrentState();

        if (flow.getEventId() != FlowManager.getEventId(request)) {
            mv = new ModelAndView("redirect:/flow/workflow?execution=" + flow.getSessionKey());
        } else {

            mv = getModelAndView(flow, state.getViewName());
        }

        return Pair.of(mv, flow);
    }

    @Deprecated
    @Override
    public Pair<ModelAndView, WebFlow> transition(final HttpServletRequest request) throws IOException {

        Map<String, String[]> parameterMap = request.getParameterMap();
        // TODO change to FlowEventService
        if (parameterMap.containsKey("_eventId_next")) {
            return next(request);
        } else if (parameterMap.containsKey("_eventId_prev")) {
            return previous(request);
        } else if (parameterMap.containsKey("execution")) {
            WebFlow flow = FlowManager.get(request);
            return Pair.of(
                    new ModelAndView("redirect:/flow/workflow?execution=" + request.getParameter("execution")),
                    flow);
        }

        throw new FlowEndedException();
    }

    /**
     * Update Signatures from before_submit to immediate.
     * @param forms {@link Collection}
     */
    private void updateSignaturesToRequired(final Collection<FormJSON> forms) {

        for (FormJSON form : forms) {
            for (FormJSONSection section : form.getSections()) {
                for (FormJSONField field : section.getFields()) {
                    if (FormJSONFieldType.SIGNATURE.equals(field.getType())) {
                        if (FormJSONRequiredType.BEFORE_SUBMIT.equals(field.getRequired())) {
                            field.setRequired(FormJSONRequiredType.IMMEDIATE);
                        }
                    }
                }
            }
        }
    }

    /**
     * Gets PDF from {@link ArchiveDTO}.
     * @param request {@link HttpServletRequest}
     * @param archive {@link ArchiveDTO}
     * @return byte[]
     * @throws IOException IOException
     */
    private byte[] getPDF(final HttpServletRequest request, final ArchiveDTO archive) throws IOException {

        byte[] bytes = null;
        if (!CollectionUtils.isEmpty(archive.getPDF())) {
            bytes = archive.getPDF().values().iterator().next();
        } else {

            List<FlowState> states = createStates(archive);
            WebFlow flow = startFlow(request, null, null, archive, WorkflowEditorServiceImpl.class, states);

            Map<String, Object> map = ImmutableMap.of("flow", flow);
            String html = this.printRenderer.generateHTML(request, DEFAULT_PDF_VIEW, map);

            bytes = this.printRenderer.createPDF(html);
        }

        return bytes;
    }

    @Override
    public void emailForm(final HttpServletRequest request, final String folder, final String uuid, final String to,
            final String subject, final String body) throws IOException {

        ArchiveDTO dto = this.archiveService.get(folder, uuid, false).getLeft();

        byte[] pdf = getPDF(request, dto);

        Map<String, byte[]> attachments = new HashMap<>();
        attachments.put(dto.getWorkflow().getName() + ".pdf", pdf);

        this.notificationService.sendEmail(to, subject, null, body, attachments);
    }

    @Override
    public FormJSON handleFormEvents(final FormJSON form, final String type) throws IOException {

        FormJSON response = null;

        for (FormEventHandler handler : this.context.getBeansOfType(FormEventHandler.class).values()) {

            if (handler.getType().eq(form.getBuiltintype())) {
                response = handler.handleEvent(form, type);
                break;
            }
        }

        if (response == null) {
            throw new PreconditionFailedException("Unable to find Form Handler for " + form.getBuiltintype());
        }
        return response;
    }
}