content: /*
   * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
   *
   * 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 org.activiti.engine.test.image;
  
  import static java.util.Arrays.asList;
  import static java.util.Collections.emptyList;
  import static org.assertj.core.api.Assertions.assertThat;
  import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
  
  import java.io.File;
  import java.io.InputStream;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.stream.Collectors;
  import org.activiti.bpmn.model.BpmnModel;
  import org.activiti.engine.ProcessEngineConfiguration;
  import org.activiti.engine.ProcessEngines;
  import org.activiti.engine.impl.ProcessEngineImpl;
  import org.activiti.engine.impl.test.PluggableActivitiTestCase;
  import org.activiti.engine.task.Task;
  import org.activiti.engine.test.Deployment;
  import org.activiti.image.ProcessDiagramGenerator;
  import org.activiti.image.exception.ActivitiImageException;
  import org.activiti.image.exception.ActivitiInterchangeInfoNotFoundException;
  import org.activiti.image.impl.DefaultProcessDiagramGenerator;
  import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
  import org.apache.batik.anim.dom.SVGOMDocument;
  import org.apache.batik.util.XMLResourceDescriptor;
  import org.apache.commons.io.IOUtils;
  import org.springframework.util.ResourceUtils;
  import org.springframework.util.StreamUtils;
  import org.xmlunit.assertj3.XmlAssert;
  import org.xmlunit.diff.Comparison;
  import org.xmlunit.diff.ComparisonResult;
  import org.xmlunit.diff.ComparisonType;
  import org.xmlunit.diff.DifferenceEvaluator;
  
  public class ProcessDiagramGeneratorTest extends PluggableActivitiTestCase {
  
      public static final String TEST_OUTPUT_RESULT_PATH =
          "classpath:org/activiti/engine/test/image/" + "ProcessDiagramGeneratorTest.testOutput.result.svg";
  
      private final ProcessDiagramGenerator imageGenerator = new DefaultProcessDiagramGenerator();
  
      private final String activityFontName = imageGenerator.getDefaultActivityFontName();
  
      private final String labelFontName = imageGenerator.getDefaultLabelFontName();
  
      private final String annotationFontName = imageGenerator.getDefaultAnnotationFontName();
  
      @Override
      protected void initializeProcessEngine() {
          ProcessEngines.destroy();
          processEngine = ProcessEngineConfiguration.createStandaloneProcessEngineConfiguration()
              .setDatabaseSchemaUpdate("drop-create")
              .setJdbcDriver("org.h2.Driver")
              .setJdbcUrl("jdbc:h2:mem:activiti;DB_CLOSE_DELAY=1000")
              .setJdbcPassword("")
              .setJdbcUsername("sa")
              .buildProcessEngine();
  
          cachedProcessEngine = processEngine;
          processEngineConfiguration = ((ProcessEngineImpl) processEngine).getProcessEngineConfiguration();
      }
  
      @Deployment
      public void testHighLighted() throws Exception {
          runtimeService.startProcessInstanceByKey("myProcess");
          List<Task> tasks = taskService.createTaskQuery().list();
          for (Task task : tasks) {
              taskService.complete(task.getId());
          }
          Task task = taskService.createTaskQuery().taskDefinitionKey("usertask4").singleResult();
          taskService.complete(task.getId());
  
          List<String> activityIds = runtimeService.getActiveActivityIds(task.getProcessInstanceId());
          InputStream diagram = imageGenerator.generateDiagram(
              repositoryService.getBpmnModel(task.getProcessDefinitionId()),
              activityIds
          );
          assertThat(diagram).isNotNull();
  
          List<String> highLightedFlows = asList("flow1", "flow2", "flow3", "flow4", "flow5", "flow6");
          diagram = imageGenerator.generateDiagram(
              repositoryService.getBpmnModel(task.getProcessDefinitionId()),
              activityIds,
              highLightedFlows
          );
          assertThat(diagram).isNotNull();
  
          diagram = imageGenerator.generateDiagram(
              repositoryService.getBpmnModel(task.getProcessDefinitionId()),
              activityIds,
              highLightedFlows,
              activityFontName,
              labelFontName,
              annotationFontName
          );
          assertThat(diagram).isNotNull();
      }
  
      @Deployment
      public void testSmallBoxLabels() throws Exception {
          String id = repositoryService
              .createProcessDefinitionQuery()
              .processDefinitionKey("myProcess")
              .singleResult()
              .getId();
  
          List<String> activityIds = new ArrayList<>();
          List<String> highLightedFlows = new ArrayList<>();
          InputStream diagram = imageGenerator.generateDiagram(
              repositoryService.getBpmnModel(id),
              activityIds,
              highLightedFlows
          );
          assertThat(diagram).isNotNull();
  
          diagram = imageGenerator.generateDiagram(
              repositoryService.getBpmnModel(id),
              activityIds,
              highLightedFlows,
              activityFontName,
              labelFontName,
              annotationFontName
          );
          assertThat(diagram).isNotNull();
      }
  
      @Deployment
      public void testTransactionElements() throws Exception {
          String id = repositoryService
              .createProcessDefinitionQuery()
              .processDefinitionKey("transactionSubRequest")
              .singleResult()
              .getId();
  
          List<String> activityIds = new ArrayList<>();
          List<String> highLightedFlows = new ArrayList<>();
          InputStream diagram = imageGenerator.generateDiagram(
              repositoryService.getBpmnModel(id),
              activityIds,
              highLightedFlows
          );
          assertThat(diagram).isNotNull();
  
          diagram = imageGenerator.generateDiagram(
              repositoryService.getBpmnModel(id),
              activityIds,
              highLightedFlows,
              activityFontName,
              labelFontName,
              annotationFontName
          );
          assertThat(diagram).isNotNull();
      }
  
      @Deployment
      public void testAllElements() throws Exception {
          String id = repositoryService
              .createProcessDefinitionQuery()
              .processDefinitionKey("myProcess")
              .singleResult()
              .getId();
          BpmnModel bpmnModel = repositoryService.getBpmnModel(id);
          try (
              final InputStream resourceStream = imageGenerator.generateDiagram(
                  bpmnModel,
                  activityFontName,
                  labelFontName,
                  annotationFontName
              )
          ) {
              SVGOMDocument svg = parseXml(resourceStream);
              List<String> startEventIdList = asList(
                  "startevent1",
                  "errorstartevent1",
                  "signalstartevent1",
                  "messagestartevent1",
                  "timerstartevent1"
              );
              checkDiagramElements(startEventIdList, svg);
              List<String> userTaskIdList = asList(
                  "usertask1",
                  "usertask2",
                  "usertask3",
                  "usertask4",
                  "usertask5",
                  "usertask6",
                  "usertask7",
                  "usertask8",
                  "usertask9",
                  "usertask10",
                  "usertask11",
                  "usertask12",
                  "usertask13",
                  "usertask14",
                  "usertask15",
                  "usertask16",
                  "usertask17",
                  "usertask18",
                  "usertask19"
              );
              checkDiagramElements(userTaskIdList, svg);
              List<String> scriptTaskIdList = asList("scripttask1", "scripttask2", "scripttask3");
              checkDiagramElements(scriptTaskIdList, svg);
              List<String> otherTaskIdList = asList(
                  "servicetask1",
                  "mailtask1",
                  "manualtask1",
                  "receivetask1",
                  "callactivity1"
              );
              checkDiagramElements(otherTaskIdList, svg);
              List<String> intermediateEvent = asList(
                  "timerintermediatecatchevent1",
                  "signalintermediatecatchevent1",
                  "messageintermediatecatchevent1",
                  "signalintermediatethrowevent1",
                  "compensationintermediatethrowevent1",
                  "noneintermediatethrowevent1"
              );
              checkDiagramElements(intermediateEvent, svg);
              List<String> gatewayIdList = asList(
                  "parallelgateway1",
                  "parallelgateway2",
                  "exclusivegateway1",
                  "exclusivegateway3",
                  "inclusivegateway1",
                  "inclusivegateway2",
                  "eventgateway1"
              );
              checkDiagramElements(gatewayIdList, svg);
              List<String> containerIdList = asList(
                  "subprocess1",
                  "eventsubprocess1",
                  "pool1",
                  "pool2",
                  "pool3",
                  "lane1",
                  "lane2",
                  "lane3",
                  "lane4"
              );
              checkDiagramElements(containerIdList, svg);
              List<String> endEventIdList = asList(
                  "errorendevent1",
                  "endevent1",
                  "endevent2",
                  "endevent3",
                  "endevent4",
                  "endevent5",
                  "endevent6",
                  "endevent7",
                  "endevent8",
                  "endevent9",
                  "endevent10",
                  "endevent11",
                  "endevent12"
              );
              checkDiagramElements(endEventIdList, svg);
          }
      }
  
      @Deployment
      public void testOutput() throws Exception {
          String id = repositoryService
              .createProcessDefinitionQuery()
              .processDefinitionKey("big-process")
              .singleResult()
              .getId();
          BpmnModel bpmnModel = repositoryService.getBpmnModel(id);
          try (final InputStream resourceStream = imageGenerator.generateDiagram(bpmnModel, "", "", "")) {
              byte[] bytes = StreamUtils.copyToByteArray(resourceStream);
              String resultSvgContent = new String(bytes, StandardCharsets.UTF_8);
              File expectedResultFile = ResourceUtils.getFile(TEST_OUTPUT_RESULT_PATH);
              XmlAssert.assertThat(resultSvgContent)
                  .and(expectedResultFile)
                  .ignoreWhitespace()
                  .withNodeFilter(node -> !"path".equals(node.getNodeName()))
                  .withDifferenceEvaluator(new StyleAttributeEvaluator())
                  .areIdentical();
          }
      }
  
      /**
       * Test that when the diagram is generated for a model without graphic info
       * then the default diagram image is returned
       * or the ActivitiInterchangeInfoNotFoundException is thrown
       * depending on the value of the generateDefaultDiagram parameter.
       *
       */
      @Deployment
      public void testGenerateDefaultDiagram() throws Exception {
          //GIVEN
          String id = repositoryService
              .createProcessDefinitionQuery()
              .processDefinitionKey("fixSystemFailure")
              .singleResult()
              .getId();
          BpmnModel bpmnModel = repositoryService.getBpmnModel(id);
  
          //WHEN
          try (
              final InputStream resourceStream = imageGenerator.generateDiagram(
                  bpmnModel,
                  emptyList(),
                  emptyList(),
                  activityFontName,
                  labelFontName,
                  annotationFontName,
                  true
              )
          ) {
              //THEN
              assertThat(resourceStream).isNotNull();
              byte[] diagram = IOUtils.toByteArray(resourceStream);
              assertThat(diagram).isNotNull();
  
              try (
                  InputStream imageStream = getClass().getResourceAsStream(
                      imageGenerator.getDefaultDiagramImageFileName()
                  )
              ) {
                  assertThat(diagram).isEqualTo(IOUtils.toByteArray(imageStream));
              }
          }
  
          //THEN
          assertThatExceptionOfType(ActivitiInterchangeInfoNotFoundException.class)
              .isThrownBy(
                  //WHEN
                  () ->
                      imageGenerator.generateDiagram(
                          bpmnModel,
                          emptyList(),
                          emptyList(),
                          activityFontName,
                          labelFontName,
                          annotationFontName,
                          false
                      )
              )
              .withMessage("No interchange information found.");
  
          //THEN
          assertThatExceptionOfType(ActivitiImageException.class)
              .isThrownBy(
                  //WHEN
                  () ->
                      imageGenerator.generateDiagram(
                          bpmnModel,
                          emptyList(),
                          emptyList(),
                          emptyList(),
                          emptyList(),
                          activityFontName,
                          labelFontName,
                          annotationFontName,
                          true,
                          "invalid-file-name"
                      )
              )
              .withMessage("Error occurred while getting default diagram image from file: invalid-file-name");
      }
  
      private void checkDiagramElements(List<String> elementIdList, SVGOMDocument svg) {
          for (String elementId : elementIdList) {
              assertThat(svg.getElementById(elementId)).isNotNull();
          }
      }
  
      private SVGOMDocument parseXml(InputStream resourceStream) throws Exception {
          String parser = XMLResourceDescriptor.getXMLParserClassName();
          SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(parser);
          return (SVGOMDocument) factory.createDocument(null, resourceStream);
      }
  
      private static class StyleAttributeEvaluator implements DifferenceEvaluator {
  
          @Override
          public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) {
              if (outcome != ComparisonResult.EQUAL && isStyleAttribute(comparison)) {
                  List<String> controlStream = split(comparison.getControlDetails());
                  List<String> testStream = split(comparison.getTestDetails());
                  if (controlStream.size() == testStream.size() && controlStream.containsAll(testStream)) {
                      return ComparisonResult.EQUAL;
                  }
              }
              return outcome;
          }
  
          private boolean isStyleAttribute(Comparison comparison) {
              return (
                  comparison.getType() == ComparisonType.ATTR_VALUE &&
                  "style".equals(comparison.getControlDetails().getTarget().getNodeName())
              );
          }
  
          private List<String> split(Comparison.Detail comparison) {
              return Arrays.stream(((String) comparison.getValue()).split(";"))
                  .map(String::trim)
                  .collect(Collectors.toList());
          }
      }
  }
  