Java tutorial
/* * 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; } }