/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.component.workflow.execution.api; import java.io.File; import java.io.FileNotFoundException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import de.rcenvironment.core.communication.common.InstanceNodeSessionId; import de.rcenvironment.core.communication.common.LogicalNodeId; import de.rcenvironment.core.component.api.ComponentUtils; import de.rcenvironment.core.component.api.DistributedComponentKnowledge; import de.rcenvironment.core.component.model.api.ComponentInstallation; import de.rcenvironment.core.component.workflow.model.api.WorkflowDescription; import de.rcenvironment.core.component.workflow.model.api.WorkflowNode; import de.rcenvironment.core.utils.common.StringUtils; /** * Utility methods used during workflow execution. * * @author Doreen Seider * @author Robert Mischke */ public final class WorkflowExecutionUtils { /** * Common error message template for {@link #resolveWorkflowOrPlaceholderFileLocation(String, String)}. */ public static final String DEFAULT_ERROR_MESSAGE_TEMPLATE_CANNOT_READ_PLACEHOLDER_FILE = "Placeholder file %s does not exist or it can not be read"; /** * Common error message template for {@link #resolveWorkflowOrPlaceholderFileLocation(String, String)}. */ public static final String DEFAULT_ERROR_MESSAGE_TEMPLATE_CANNOT_READ_WORKFLOW_FILE = "Workflow file %s does not exist or it can not be read"; private static final int WORKFLOW_SUFFIX_NUMBER_MODULO = 100; private static final int PATTERN_DATETIME_LENGTH = 19; /** * Structure of this regular expression:<br> * 1. year<br> * \d{4} -> random number between 0 and 9 exactly 4 times. \- -> hyphen 2. month<br> * [01]{1} -> 0 or 1 (a year only has 12 months) \d{1} -> random number between 0 and 9 exactly once. \- -> hyphen 3. day<br> * [0123]{1} \d{1} -> random number between 0 and 9 exactly once. \_ -> underline character 4. hour<br> * [012]{1} \d -> random number between 0 and 9 exactly once. \: -> colon 5. minutes<br> * [0123456]{1} -> random number between 0 and 6 exactly once (a hour only has 60min) \d{1} -> random number between 0 and 9 exactly * once. \: -> colon 6. seconds<br> * [0123456]{1} -> random number between 0 and 6 exactly once (a minute only has 60sec) \d{1} -> random number between 0 and 9 exactly * once. * */ private static final String PATTERN_DATETIME_STRING = "\\d{4}\\-[01]{1}\\d{1}\\-[0123]{1}\\d{1}\\_[012]{1}\\d\\:[0123456]{1}\\d{1}\\:[0123456]{1}\\d{1}"; private static final int PATTERN_NUMBER_LENGTH = 3; private static final String PATTERN_NUMBER_STRING = "^\\_\\d{2}"; private static final AtomicInteger GLOBAL_WORKFLOW_SUFFIX_SEQUENCE_COUNTER = new AtomicInteger(); private WorkflowExecutionUtils() {}; /** * Generates a new default name for the workflow, which will be executed now. * * @param filename *.wf file * @param workflowDescription {@link WorkflowDescription} of the workflow * @return the generated default name including the timestamp */ public static String generateDefaultNameforExecutingWorkflow(String filename, WorkflowDescription workflowDescription) { String wfDescriptionName = workflowDescription.getName(); if (wfDescriptionName == null) { return generateWorkflowName(filename); } Pattern pattern = Pattern.compile(PATTERN_DATETIME_STRING); Matcher matcher = pattern.matcher(wfDescriptionName); if (!matcher.find()) { return wfDescriptionName; } int position = matcher.start(); String dateAndTime = wfDescriptionName.substring(position, position + PATTERN_DATETIME_LENGTH); try { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss"); dateFormat.setLenient(false); dateFormat.parse(dateAndTime); } catch (ParseException e) { return wfDescriptionName; } String newName = wfDescriptionName.substring(0, position) + generateTimestampString(); String rest = wfDescriptionName.substring(position + PATTERN_DATETIME_LENGTH); pattern = Pattern.compile(PATTERN_NUMBER_STRING); matcher = pattern.matcher(rest); if (matcher.find()) { newName = StringUtils.format("%s_%02d", newName, generateNewSuffixNumber()); newName += rest.substring(PATTERN_NUMBER_LENGTH); } else { newName += rest; } return newName; } private static String generateWorkflowName(String filename) { String storedWorkflowName = filename; if (storedWorkflowName.toLowerCase().endsWith(".wf")) { storedWorkflowName = storedWorkflowName.substring(0, storedWorkflowName.length() - 3); } // make the last two digits sequentially increasing to reduce the likelihood of timestamp collisions return StringUtils.format("%s_%s_%02d", storedWorkflowName, generateTimestampString(), generateNewSuffixNumber()); } private static int generateNewSuffixNumber() { return GLOBAL_WORKFLOW_SUFFIX_SEQUENCE_COUNTER.incrementAndGet() % WORKFLOW_SUFFIX_NUMBER_MODULO; } /** * Resolves a user-given path to a workflow file to its actual location. This is mostly intended for workflow execution via console * commands (including headless batch execution using "--exec"). * * @param filename the given workflow filename/path * @param errorString the template for the exception message if the file can not be read * @return an absolute {@link File} pointing to the resolved workflow file, if it exists * @throws FileNotFoundException if the given file cannot be resolved (usually, because it does not exist), or if it cannot be read */ public static File resolveWorkflowOrPlaceholderFileLocation(String filename, String errorString) throws FileNotFoundException { // TODO trivial implementation; improve with paths relative to workspace etc. - misc_ro File file = new File(filename).getAbsoluteFile(); // validate if (!file.isFile() || !file.canRead()) { throw new FileNotFoundException(StringUtils.format(errorString, StringUtils.format("\"%s\" (resolved to \"%s\")", filename, file.getAbsolutePath()))); } return file; } private static String generateTimestampString() { // format: full date and time, connected with underscore SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss"); return dateFormat.format(new Date()); } /** * Replaces null {@link InstanceNodeSessionId} for controller and components with local {@link InstanceNodeSessionId}. * * @param wfDescription {@link WorkflowDescription} * @param localNodeId local {@link InstanceNodeSessionId} * @param compKnowledge latest {@link DistributedComponentKnowledge} * @throws WorkflowExecutionException if a component affected is not installed locally */ public static void replaceNullNodeIdentifiersWithActualNodeIdentifier(WorkflowDescription wfDescription, LogicalNodeId localNodeId, DistributedComponentKnowledge compKnowledge) throws WorkflowExecutionException { for (WorkflowNode node : wfDescription.getWorkflowNodes()) { // replace null (representing localhost) with the actual host name // TODO review: can this actually still be null at this point? if (node.getComponentDescription().getNode() == null) { Collection<ComponentInstallation> installations = compKnowledge.getLocalInstallations(); final String componentIdentifier = node.getComponentDescription().getIdentifier(); ComponentInstallation installation = ComponentUtils.getExactMatchingComponentInstallationForNode( componentIdentifier, installations, localNodeId); if (installation == null) { throw new WorkflowExecutionException(StringUtils.format("Component '%s' (%s) not installed on node %s ", node.getName(), componentIdentifier, node.getComponentDescription().getNode())); } node.getComponentDescription().setComponentInstallationAndUpdateConfiguration(installation); node.getComponentDescription().setIsNodeIdTransient(true); } } if (wfDescription.getControllerNode() == null) { wfDescription.setControllerNode(localNodeId); wfDescription.setIsControllerNodeIdTransient(true); } } /** * Set {@link InstanceNodeSessionId}s to transient if they point to the local node. * * @param wfDescription {@link WorkflowDescription} * @param localNodeId local {@link InstanceNodeSessionId} */ public static void setNodeIdentifiersToTransientInCaseOfLocalOnes(WorkflowDescription wfDescription, LogicalNodeId localNodeId) { for (WorkflowNode node : wfDescription.getWorkflowNodes()) { node.getComponentDescription().setIsNodeIdTransient(node.getComponentDescription().getNode() == null || node.getComponentDescription().getNode().equals(localNodeId)); } wfDescription.setIsControllerNodeIdTransient(wfDescription.getControllerNode() == null || wfDescription.getControllerNode().equals(localNodeId)); } /** * Removed disabled workflow nodes from given {@link WorkflowDescription}. * * @param workflowDescription to remove the disabled {@link WorkflowNode}s from * @return {@link WorkflowDescription} without disabled {@link WorkflowNode}s */ public static WorkflowDescription removeDisabledWorkflowNodesWithoutNotify(WorkflowDescription workflowDescription) { List<WorkflowNode> disabledWorkflowNodes = getDisabledWorkflowNodes(workflowDescription); if (!disabledWorkflowNodes.isEmpty()) { workflowDescription.removeWorkflowNodesAndRelatedConnectionsWithoutNotify(disabledWorkflowNodes); } return workflowDescription; } /** * Returns the workflow nodes that are disabled from given {@link WorkflowDescription}. * * @param workflowDescription to remove the disabled {@link WorkflowNode}s from * @return list of {@link WorkflowNode}s disabled */ public static List<WorkflowNode> getDisabledWorkflowNodes(WorkflowDescription workflowDescription) { List<WorkflowNode> nodes = new ArrayList<>(); for (WorkflowNode node : workflowDescription.getWorkflowNodes()) { if (!node.isEnabled()) { nodes.add(node); } } return nodes; } /** * @param wfNodes list of {@link WorkflowNode}s to check * @return <code>true</code> if one of the {@link WorkflowNode}s is considered as referring to a non-available component, otherwise * <code>false</code> */ public static boolean hasMissingWorkflowNode(List<WorkflowNode> wfNodes) { for (WorkflowNode wfNode : wfNodes) { if (wfNode.getComponentDescription().getIdentifier().startsWith(ComponentUtils.MISSING_COMPONENT_PREFIX)) { return true; } } return false; } /** * @param wfExeCtx {@link WorkflowExecutionContext} of related workflow * @return text containing workflow instance name and workflow execution id that can be used in log messages */ public static String substituteWorkflowNameAndExeId(WorkflowExecutionContext wfExeCtx) { return StringUtils.format("workflow '%s' (%s)", wfExeCtx.getInstanceName(), wfExeCtx.getExecutionIdentifier()); } }