package is.idega.idegaweb.egov.bpm.cases.email.business; import is.idega.idegaweb.egov.bpm.cases.email.bean.BPMEmailMessage; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import org.jbpm.JbpmContext; import org.jbpm.JbpmException; import org.jbpm.context.exe.VariableInstance; import org.jbpm.context.exe.variableinstance.StringInstance; import org.jbpm.graph.exe.ProcessInstance; import org.jbpm.taskmgmt.exe.TaskInstance; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.support.WebApplicationContextUtils; import com.idega.block.email.bean.FoundMessagesInfo; import com.idega.block.email.bean.MessageParserType; import com.idega.block.email.business.EmailsParsersProvider; import com.idega.block.email.client.business.ApplicationEmailEvent; import com.idega.block.email.client.business.EmailParams; import com.idega.block.email.parser.EmailParser; import com.idega.block.process.variables.Variable; import com.idega.block.process.variables.VariableDataType; import com.idega.bpm.BPMConstants; import com.idega.core.converter.util.StringConverterUtility; import com.idega.core.messaging.EmailMessage; import com.idega.idegaweb.IWMainApplication; import com.idega.idegaweb.IWMainApplicationSettings; import com.idega.jbpm.BPMContext; import com.idega.jbpm.JbpmCallback; import com.idega.jbpm.exe.BPMFactory; import com.idega.jbpm.exe.ProcessInstanceW; import com.idega.jbpm.exe.TaskInstanceW; import com.idega.jbpm.view.View; import com.idega.jbpm.view.ViewSubmission; import com.idega.util.ArrayUtil; import com.idega.util.CoreConstants; import com.idega.util.CoreUtil; import com.idega.util.FileUtil; import com.idega.util.IOUtil; import com.idega.util.ListUtil; import com.idega.util.StringHandler; import com.idega.util.StringUtil; import com.idega.util.datastructures.map.MapUtil; import com.idega.util.expression.ELUtil; /** * Resolves messages to attach and attaches * * @author <a href="mailto:valdas@idega.com">Valdas Žemaitis</a> * @version $Revision: 1.1 $ Last modified: $Date: 2009/04/22 12:56:21 $ by $Author: valdas $ */ @Service @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class EmailMessagesAttacherWorker implements Runnable { private static final Logger LOGGER = Logger.getLogger(EmailMessagesAttacherWorker.class.getName()); private static final String FETCH_EMAILS_TASK_NAME = "Email"; @Autowired private BPMContext idegaJbpmContext; @Autowired private BPMFactory bpmFactory; private ApplicationEmailEvent emailEvent; public EmailMessagesAttacherWorker(ApplicationEmailEvent emailEvent) { this.emailEvent = emailEvent; ELUtil.getInstance().autowire(this); } @Override public void run() { parseAndAttachMessages(); } private void parseAndAttachMessages() { Map<String, FoundMessagesInfo> messagesToParse = new HashMap<String, FoundMessagesInfo>(); Map<String, FoundMessagesInfo> messagesInfo = emailEvent.getMessages(); if (messagesInfo != null && !messagesInfo.isEmpty()) { for (Entry<String, FoundMessagesInfo> entry: messagesInfo.entrySet()) { if (entry.getValue().getParserType() == MessageParserType.BPM) { messagesToParse.put(entry.getKey(), entry.getValue()); } } } Map<?, ?> parsersProviders = null; try { parsersProviders = WebApplicationContextUtils.getWebApplicationContext(IWMainApplication.getDefaultIWMainApplication() .getServletContext()).getBeansOfType(EmailsParsersProvider.class); } catch(BeansException e) { LOGGER.log(Level.SEVERE, "Error getting beans of type: " + EmailsParsersProvider.class, e); } if (parsersProviders == null || parsersProviders.isEmpty()) { return; } if (!IWMainApplication.getDefaultIWMainApplication().getSettings().getBoolean("bpm.attach_emails_to_case", Boolean.TRUE)) { return; } EmailParams params = emailEvent.getEmailParams(); Collection<BPMEmailMessage> allParsedMessages = new ArrayList<BPMEmailMessage>(); for (Object bean: parsersProviders.values()) { if (bean instanceof EmailsParsersProvider) { for (EmailParser parser: ((EmailsParsersProvider) bean).getAllParsers()) { Collection<? extends EmailMessage> parsedMessages = parser.getParsedMessagesCollection(messagesToParse, params); addParsedMessages(allParsedMessages, parsedMessages); parsedMessages = parser.getParsedMessages(emailEvent); addParsedMessages(allParsedMessages, parsedMessages); } } } if (ListUtil.isEmpty(allParsedMessages)) { LOGGER.info("No emails were parsed"); return; } for (final BPMEmailMessage message: allParsedMessages) { try { if (!doAttachMessageIfNeeded(message)) { // TODO: save message and try attach later? } } catch (Exception e) { LOGGER.log(Level.WARNING, "Error attaching message " + message, e); } } } private void addParsedMessages(Collection<BPMEmailMessage> allParsedMessages, Collection<? extends EmailMessage> parsedMessages) { if (ListUtil.isEmpty(parsedMessages)) return; for (EmailMessage message: parsedMessages) { if (message instanceof BPMEmailMessage && !allParsedMessages.contains(message)) { allParsedMessages.add((BPMEmailMessage) message); } } } private Map<Long, Map<Long, Map<String, String>>> getEmailsValues(List<Long> subProcInstIds, List<String> names) { if (ListUtil.isEmpty(subProcInstIds) || ListUtil.isEmpty(names)) { return null; } // Proc. inst. ID -> task inst. ID -> name -> value Map<Long, Map<Long, Map<String, String>>> results = new HashMap<Long, Map<Long, Map<String, String>>>(); for (Long subProcInstId: subProcInstIds) { List<Long> tiIds = getBpmFactory().getBPMDAO().getIdsOfFinishedTaskInstancesForTask(subProcInstId, FETCH_EMAILS_TASK_NAME); if (ListUtil.isEmpty(tiIds)) { continue; } Map<Long, Map<String, String>> dataForSubProcInst = new HashMap<Long, Map<String, String>>(); for (Long tiId: tiIds) { for (String name: names) { String value = getVariableValue(tiId, name); if (StringUtil.isEmpty(value)) { continue; } Map<String, String> taskData = dataForSubProcInst.get(tiId); if (taskData == null) { taskData = new HashMap<String, String>(); dataForSubProcInst.put(tiId, taskData); } taskData.put(name, value); } } if (MapUtil.isEmpty(dataForSubProcInst)) { continue; } results.put(subProcInstId, dataForSubProcInst); } return results; } private String getVariableValue(final Long tiId, final String name) { return getIdegaJbpmContext().execute(new JbpmCallback() { @Override public String doInJbpm(JbpmContext context) throws JbpmException { TaskInstance ti = context.getTaskInstance(tiId); Object value = ti.getVariableLocally(name); if (value instanceof String) { return (String) value; } value = ti.getVariable(name); if (value instanceof String) { return (String) value; } VariableInstance variable = ti.getVariableInstance(name); if (variable instanceof StringInstance) { return (String) ((StringInstance) variable).getValue(); } return null; } }); } public boolean doAttachMessageIfNeeded(BPMEmailMessage message) { if (message == null || message.isParsed()) { return true; } Long procInstId = message.getProcessInstanceId(); if (procInstId == null) { LOGGER.warning("Proc. inst. ID is unknown for message " + message); return false; } IWMainApplicationSettings settings = IWMainApplication.getDefaultIWMainApplication().getSettings(); List<Long> subProcInstIds = null; if (settings.getBoolean("bpm.email_find_all_sub_proc", Boolean.TRUE)) { ProcessInstanceW piW = getBpmFactory().getProcessInstanceW(procInstId); subProcInstIds = piW.getIdsOfSubProcesses(procInstId); } else { subProcInstIds = getBpmFactory().getBPMDAO().getSubProcInstIdsByParentProcInstIdAndProcDefName( procInstId, EmailMessagesAttacher.email_fetch_process_name ); } if (ListUtil.isEmpty(subProcInstIds)) { LOGGER.warning("No sub-proc. inst. IDs were found for proc. inst. ID " + procInstId + " and proc. def. name: " + EmailMessagesAttacher.email_fetch_process_name + ". Can not verify if message (" + message + ") is already attached"); return false; } String subject = message.getSubject(); String text = message.getBody(); if (text == null) { text = CoreConstants.EMPTY; } String senderPersonalName = message.getSenderName(); String fromAddress = message.getFromAddress(); // Proc. inst. ID -> task inst. ID -> variable name -> variable value Map<Long, Map<Long, Map<String, String>>> groupedVars = getEmailsValues( subProcInstIds, Arrays.asList(BPMConstants.VAR_SUBJECT, BPMConstants.VAR_TEXT, BPMConstants.VAR_FROM, BPMConstants.VAR_FROM_ADDRESS) ); boolean foundExisting = false; if (!MapUtil.isEmpty(groupedVars)) { // Checking if current message is not attached already Map<String, Boolean> subjectsComparisons = new HashMap<String, Boolean>(), fromComparisons = new HashMap<String, Boolean>(), addressesComparisons = new HashMap<String, Boolean>(); String[] patterns = settings.getProperty("bpm.emails_cont_rep_patt", "…" + CoreConstants.COMMA + "¿").split(CoreConstants.COMMA); String[] encodedPatterns = settings.getProperty("bpm.emails_cont_rep_enc_patt", "u2026" + CoreConstants.COMMA + "u00BF").split(CoreConstants.COMMA); boolean printComparison = settings.getBoolean("bpm.emails_print_comparison", Boolean.FALSE); try { for (Iterator<Long> subProcInstIdsIter = groupedVars.keySet().iterator(); (subProcInstIdsIter.hasNext() && !foundExisting);) { Long subProcInstId = subProcInstIdsIter.next(); Map<Long, Map<String, String>> tasksVariables = groupedVars.get(subProcInstId); if (MapUtil.isEmpty(tasksVariables)) { continue; } for (Iterator<Long> taskInstIdsIter = tasksVariables.keySet().iterator(); (taskInstIdsIter.hasNext() && !foundExisting);) { Long tiId = taskInstIdsIter.next(); Map<String, String> existingValues = tasksVariables.get(tiId); if (MapUtil.isEmpty(existingValues)) { continue; } String subjectVarValue = existingValues.get(BPMConstants.VAR_SUBJECT); String textVarValue = existingValues.get(BPMConstants.VAR_TEXT); String fromVarValue = existingValues.get(BPMConstants.VAR_FROM); String fromAddressVarValue = existingValues.get(BPMConstants.VAR_FROM_ADDRESS); boolean subjectsMatch = !StringUtil.isEmpty(subjectVarValue) && subject.equals(subjectVarValue); boolean textsMatch = !StringUtil.isEmpty(textVarValue) && text.equals(textVarValue); boolean fromMatch = (fromVarValue == null && senderPersonalName == null) || (!StringUtil.isEmpty(fromVarValue) && !StringUtil.isEmpty(senderPersonalName) && senderPersonalName.equals(fromVarValue)); boolean addressesMatch = !StringUtil.isEmpty(fromAddressVarValue) && fromAddress.equals(fromAddressVarValue); if (subjectsMatch && textsMatch && fromMatch && addressesMatch) { message.setParsed(true); foundExisting = true; return true; } else { subjectsComparisons.put(tiId + "_:_ '" + subjectVarValue + "'", subjectsMatch); if (fromVarValue != null) { fromComparisons.put(tiId + "_:_ '" + fromVarValue + "'", fromMatch); } addressesComparisons.put(tiId + "_:_ '" + fromAddressVarValue + "'", addressesMatch); if (subjectsMatch && fromMatch && addressesMatch) { LOGGER.info("Will compare texts again for '" + subject + "', because all other fields match. Proc. inst. ID: " + procInstId + ", sub-proc. inst. ID: " + subProcInstId + ", task inst. ID: " + tiId); // Will write texts to files and will compare content of these files try { File dir = new File(System.getProperty("java.io.tmpdir") + File.separator + "bpm_emails"); if (!dir.exists()) { dir.mkdir(); } String toAttachName = "to_attach_" + subject + "_" + tiId + ".txt"; toAttachName = StringHandler.removeWhiteSpace(toAttachName); File toAttach = new File(dir.getAbsolutePath() + File.separator + toAttachName); if (!toAttach.exists()) { toAttach.createNewFile(); } FileUtil.streamToFile(StringHandler.getStreamFromString(text), toAttach); String toCompareName = "to_compare_" + subjectVarValue + "_" + tiId + ".txt"; toCompareName = StringHandler.removeWhiteSpace(toCompareName); File toCompare = new File(dir.getAbsolutePath() + File.separator + toCompareName); if (!toCompare.exists()) { toCompare.createNewFile(); } FileUtil.streamToFile(StringHandler.getStreamFromString(textVarValue), toCompare); textsMatch = isContentOfFilesEqual(toAttach, toCompare, patterns, encodedPatterns, subject, printComparison); if (textsMatch) { message.setParsed(true); foundExisting = true; return true; } else { String msg = "Wrote files " + toAttach.getName() + " and " + toCompare.getName() + " to " + dir.getAbsolutePath() + ". Content is not the same of these files while subject ('" + subject + "'), sender address and name are the same. " + "Proc. inst. ID: " + procInstId + ", sub-proc. inst. ID: " + subProcInstId + ", task inst. ID: " + tiId; LOGGER.warning(msg); if (settings.getBoolean("bpm.email_send_comparisons", false)) { CoreUtil.sendExceptionNotification(msg, null, toAttach, toCompare); } } } catch (Exception e) { e.printStackTrace(); } } } } } } finally { if (foundExisting) { message.setParsed(true); return true; } else { LOGGER.info("Email with subject '" + subject + "' (comparisons:\n" + subjectsComparisons + ")\nsender: '" + senderPersonalName + "' (comparisons:\n" + fromComparisons + ")\nfrom: '" + fromAddress + "' (comparisons:\n" + addressesComparisons + ")\n is not attached to proc. inst. ID: " + procInstId + ", sub-proc. inst IDs: " + subProcInstIds + ", need to attach it"); } } } if (foundExisting) { message.setParsed(true); return true; } List<Long> fetchEmailsSubProcInstIds = getBpmFactory().getBPMDAO().getSubProcInstIdsByParentProcInstIdAndProcDefName( procInstId, EmailMessagesAttacher.email_fetch_process_name ); if (ListUtil.isEmpty(fetchEmailsSubProcInstIds)) { LOGGER.warning("No sub-proc. inst. IDs were found for proc. inst. ID " + procInstId + " and proc. def. name: " + EmailMessagesAttacher.email_fetch_process_name + ". Do not know where to attach message " + message); return false; } Map<String, InputStream> attachments = message.getAttachments(); if (attachments == null) { attachments = new HashMap<String, InputStream>(1); } boolean result = doAttachEmailToProcess( fetchEmailsSubProcInstIds.get(0), subject, text, senderPersonalName, fromAddress, attachments, message.getAttachedFiles() ); message.setParsed(result); return result; } private boolean isContentOfLinesEqual(String identifier, List<String> lines1, List<String> lines2, boolean printComparison) { if (ListUtil.isEmpty(lines1)) { LOGGER.warning("Lines1 are not provided"); return false; } if (ListUtil.isEmpty(lines2)) { LOGGER.warning("Lines2 are not provided"); return false; } boolean numberOfLinesIsTheSame = lines1.size() == lines2.size(); if (!numberOfLinesIsTheSame) { LOGGER.warning("Number of lines is not the same: lines1: " + lines1.size() + " vs. lines2: " + lines2.size()); return false; } for (int i = 0; i < lines1.size(); i++) { String line1 = lines1.get(i); String line2 = lines2.get(i); if (!line1.equals(line2)) { if (printComparison) { LOGGER.warning("Line number: " + i + ": 'Line 1' (length: " + line1.length() + "):\n'" + line1+ "'\n'Line 2' (length: " + line2.length() + "):\n'" + line2 + "'\n. They are not equal. Identifier: " + identifier); } return false; } } return true; } private boolean isContentOfFilesEqual(File file1, File file2, String[] patterns, String[] encodedPatterns, String identifier, boolean printComparison) { boolean sameContent = false; BufferedReader bfr1 = null, bfr2 = null; List<String> content1 = null, content2 = null; try { bfr1 = new BufferedReader(new FileReader(file1)); String content = null; content1 = new ArrayList<String>(); while ((content = bfr1.readLine()) != null) { content = getRidOfInvalidSymbols(content, patterns, encodedPatterns); content1.add(content); } bfr2 = new BufferedReader(new FileReader(file2)); content2 = new ArrayList<String>(); while ((content = bfr2.readLine()) != null) { content = getRidOfInvalidSymbols(content, patterns, encodedPatterns); content2.add(content); } sameContent = isContentOfLinesEqual(identifier, content1, content2, printComparison); return sameContent; } catch (Exception e) { LOGGER.log(Level.WARNING, "Error while comparing content of files " + file1 + " and " + file2, e); } finally { if (sameContent) { if (file1 != null) { file1.delete(); } if (file2 != null) { file2.delete(); } } else if (content1 != null && content2 != null && printComparison) { LOGGER.warning("Content 1 (lines: " + content1.size() + "):\n" + content1 + "\nis not the same as content 2 (lines: " + content2.size() + ")\n" + content2); } IOUtil.close(bfr1); IOUtil.close(bfr2); } return false; } private String getRidOfInvalidSymbols(String content, String[] patterns, String[] encodedPatterns) { if (content == null || ArrayUtil.isEmpty(patterns)) { return content; } String tmp = content; for (String pattern: patterns) { content = StringHandler.replace(content, pattern, CoreConstants.EMPTY); } if (tmp.equals(content)) { String encoded = StringConverterUtility.saveConvert(content, false); for (String encodedPattern: encodedPatterns) { encoded = StringHandler.replace(encoded, CoreConstants.BACK_SLASH + encodedPattern, CoreConstants.EMPTY); } content = StringConverterUtility.loadConvert(encoded); } return content; } @Transactional private Boolean doAttachEmailToProcess( final Long subProcInstId, final String subject, final String text, final String senderPersonalName, final String fromAddress, final Map<String, InputStream> attachments, final Collection<File> attachedFiles ) { Boolean result = getIdegaJbpmContext().execute(new JbpmCallback() { @Override public Boolean doInJbpm(JbpmContext context) throws JbpmException { try { ProcessInstance subPI = context.getProcessInstance(subProcInstId); TaskInstance ti = subPI.getTaskMgmtInstance().createStartTaskInstance(); ti.setName(subject); Map<String, Object> newVars = new HashMap<String, Object>(2); newVars.put(BPMConstants.VAR_SUBJECT, subject); newVars.put(BPMConstants.VAR_TEXT, text); newVars.put(BPMConstants.VAR_FROM, senderPersonalName); newVars.put(BPMConstants.VAR_FROM_ADDRESS, fromAddress); // taking here view for new task instance long tiId = ti.getId(); View view = getBpmFactory().takeView(tiId, false, null); if (view == null) { return Boolean.FALSE; } long pdId = ti.getProcessInstance().getProcessDefinition().getId(); ViewSubmission emailViewSubmission = getBpmFactory().getViewSubmission(); emailViewSubmission.populateVariables(newVars); TaskInstanceW taskInstance = bpmFactory.getProcessManager(pdId).getTaskInstance(ti.getId()); taskInstance.submit(emailViewSubmission, false); LOGGER.info("Task instance ID for email message to attach: " + tiId + "\nSubmitted task instance " + taskInstance.getTaskInstanceId() + " with data from email message (sender: " + fromAddress + ", subject: " + subject + ") for process instance: " + taskInstance.getProcessInstanceW().getProcessInstanceId()); if (!ListUtil.isEmpty(attachedFiles)) { for (File attachedFile: attachedFiles) { if (attachedFile != null) { attachments.put(attachedFile.getName(), new FileInputStream(attachedFile)); } } } if (!MapUtil.isEmpty(attachments)) { Variable variable = new Variable("attachments", VariableDataType.FILES); InputStream fileStream = null; for (String fileName: attachments.keySet()) { fileStream = attachments.get(fileName); try { taskInstance.addAttachment(variable, fileName, fileName, fileStream); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Unable to set binary variable for task instance: " + ti.getId(), e); } finally { IOUtil.closeInputStream(fileStream); if (!ListUtil.isEmpty(attachedFiles)) { for (File attachedFile: attachedFiles) { attachedFile.delete(); } } } } } return true; } catch (Exception e) { LOGGER.log(Level.SEVERE, "Exception while attaching email msg (subject: " + subject + ", text: " + text + "). Sub-process ID: " + subProcInstId, e); } return false; } }); return result; } public BPMContext getIdegaJbpmContext() { return idegaJbpmContext; } public void setIdegaJbpmContext(BPMContext idegaJbpmContext) { this.idegaJbpmContext = idegaJbpmContext; } public BPMFactory getBpmFactory() { return bpmFactory; } public void setBpmFactory(BPMFactory bpmFactory) { this.bpmFactory = bpmFactory; } }