diff --git a/pom.xml b/pom.xml index 066229e..7783f15 100644 --- a/pom.xml +++ b/pom.xml @@ -100,6 +100,9 @@ 4.13.2 0.8.8 3.9.1.2184 + 1.17.3 + 0.0.9 + 4.2.0 3.10.1 @@ -108,6 +111,7 @@ 2.1.1 3.0.12 3.0.0-M7 + 3.0.0-M7 1.6.13 3.4.1 3.2.1 @@ -118,6 +122,7 @@ https://sonarcloud.io + @@ -132,6 +137,13 @@ jackson-core provided + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + @@ -196,7 +208,24 @@ ch.qos.logback logback-classic - 1.4.0 + 1.2.11 + test + + + org.testcontainers + testcontainers + test + + + org.bonitasoft.web + bonita-java-client + ${bonita-java-client.version} + test + + + org.awaitility + awaitility + ${awaitility.version} test @@ -216,11 +245,21 @@ - + org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + maven-assembly-plugin ${maven-assembly-plugin.version} @@ -252,11 +291,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - org.jacoco jacoco-maven-plugin @@ -355,6 +389,39 @@ + + org.apache.maven.plugins + maven-failsafe-plugin + + + integration-tests-7.13 + + integration-test + + + + 7.13.0 + + + + + integration-tests-7.14 + + integration-test + + + + 7.14.0 + + + + + + verify + + + + diff --git a/src/test/java/org/bonitasoft/connectors/rest/ConnectorTestToolkit.java b/src/test/java/org/bonitasoft/connectors/rest/ConnectorTestToolkit.java new file mode 100644 index 0000000..da6b62d --- /dev/null +++ b/src/test/java/org/bonitasoft/connectors/rest/ConnectorTestToolkit.java @@ -0,0 +1,218 @@ +package org.bonitasoft.connectors.rest; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.bonitasoft.engine.bpm.bar.BarResource; +import org.bonitasoft.engine.bpm.bar.BusinessArchive; +import org.bonitasoft.engine.bpm.bar.BusinessArchiveBuilder; +import org.bonitasoft.engine.bpm.bar.BusinessArchiveFactory; +import org.bonitasoft.engine.bpm.bar.actorMapping.Actor; +import org.bonitasoft.engine.bpm.bar.actorMapping.ActorMapping; +import org.bonitasoft.engine.bpm.connector.ConnectorEvent; +import org.bonitasoft.engine.bpm.process.DesignProcessDefinition; +import org.bonitasoft.engine.bpm.process.impl.ProcessDefinitionBuilder; +import org.bonitasoft.engine.expression.ExpressionBuilder; +import org.bonitasoft.engine.expression.InvalidExpressionException; +import org.bonitasoft.engine.operation.OperationBuilder; +import org.bonitasoft.web.client.BonitaClient; +import org.bonitasoft.web.client.api.ProcessInstanceVariableApi; +import org.bonitasoft.web.client.model.ProcessInstantiationResponse; +import org.bonitasoft.web.client.services.policies.ProcessImportPolicy; + +/** + * Helper for testing connector in a docker image of Bonita studio. + */ +public class ConnectorTestToolkit { + + /** + * Build a connector and then install it into a dummy process with input and output process variables. + * Those variables will help to verify the input and output specified in the implementation of the connector to be tested. + * + * @param connectorId The identifier of the connector specified in the pom.xml and the definition file. + * @param versionId The version of the connector to be tested. + * @param inputs A map of variables with a content specified to be tested. + * @param outputs A map of results variables. + * @param locationJar The Jar containing the class and dependencies used by the connector. + * @return A {@link BusinessArchive} + * @throws Exception + */ + public static BusinessArchive buildConnectorToTest(String connectorId, String versionId, Map inputs, + Map outputs, String locationJar) throws Exception { + + // Building process with connector to setup + var process = buildConnectorInProcess(connectorId, versionId, inputs, outputs); + + // Building business archive with the process and connector + return buildBusinessArchive(process, connectorId, locationJar); + } + + private static BusinessArchive buildBusinessArchive(DesignProcessDefinition process, String connectorId, + String artifactId) throws Exception { + var barBuilder = new BusinessArchiveBuilder(); + barBuilder.createNewBusinessArchive(); + barBuilder.setProcessDefinition(process); + var foundFiles = new File("").getAbsoluteFile().toPath() + .resolve("target") + .toFile() + .listFiles(new FilenameFilter() { + + @Override + public boolean accept(File dir, String name) { + return Pattern.matches(artifactId + "-.*.jar", name) + && !name.endsWith("-sources.jar") + && !name.endsWith("-javadoc.jar"); + } + }); + + assertThat(foundFiles).hasSize(1); + var connectorJar = foundFiles[0]; + assertThat(connectorJar).exists(); + List jarEntries = findJarEntries(connectorJar, + entry -> entry.getName().equals(connectorId + ".impl")); + assertThat(jarEntries).hasSize(1); + var implEntry = jarEntries.get(0); + + byte[] content = null; + try (JarFile jarFile = new JarFile(connectorJar)) { + InputStream inputStream = jarFile.getInputStream(implEntry); + content = inputStream.readAllBytes(); + } + + barBuilder.addConnectorImplementation( + new BarResource(connectorId + ".impl", content)); + barBuilder.addClasspathResource( + new BarResource(connectorJar.getName(), Files.readAllBytes(connectorJar.toPath()))); + ActorMapping actorMapping = new ActorMapping(); + var systemActor = new Actor("system"); + systemActor.addRole("member"); + actorMapping.addActor(systemActor); + barBuilder.setActorMapping(actorMapping); + + return barBuilder.done(); + } + + private static DesignProcessDefinition buildConnectorInProcess(String connectorId, String versionId, + Map inputs, Map outputs) throws Exception { + var processBuilder = new ProcessDefinitionBuilder(); + var expBuilder = new ExpressionBuilder(); + processBuilder.createNewInstance("PROCESS_UNDER_TEST", "1.0"); + processBuilder.addActor("system"); + var connectorBuilder = processBuilder.addConnector("connector-under-test", connectorId, versionId, + ConnectorEvent.ON_ENTER); + inputs.forEach((name, content) -> { + try { + connectorBuilder.addInput(name, expBuilder.createConstantStringExpression( + content)); + } catch (InvalidExpressionException e) { + throw new RuntimeException(e); + } + }); + + if (outputs != null) { + outputs.forEach((name, output) -> { + try { + processBuilder.addData(name, output.getType(), null); + connectorBuilder.addOutput(new OperationBuilder().createSetDataOperation(name, + new ExpressionBuilder().createDataExpression(output.getName(), output.getType()))); + } catch (InvalidExpressionException e) { + throw new RuntimeException(e); + } + }); + } + + // Add a user task to avoid the process to be already completed as soon as it's launched. + processBuilder.addUserTask("waiting task", "system"); + + return processBuilder.done(); + } + + /** + * Import the {@link BusinessArchive} and launch the dummy process containing the connector to be tested. + * + * @param barArchive The file containing the {@link BusinessArchive} + * @param client A {@link BonitaClient} + * @return The process started. + * @throws IOException + */ + public static ProcessInstantiationResponse importAndLaunchProcess(BusinessArchive barArchive, BonitaClient client) + throws IOException { + var process = barArchive.getProcessDefinition(); + File processFile = null; + try { + processFile = Files.createTempFile("process", ".bar").toFile(); + processFile.delete(); + BusinessArchiveFactory.writeBusinessArchiveToFile(barArchive, processFile); + client.login("install", "install"); + client.processes().importProcess(processFile, ProcessImportPolicy.REPLACE_DUPLICATES); + } finally { + if (processFile != null) { + processFile.delete(); + } + } + + var processId = client.processes().getProcess(process.getName(), process.getVersion()).getId(); + return client.processes().startProcess(processId, Map.of()); + + } + + private static List findJarEntries(File file, Predicate entryPredicate) + throws IOException { + try (JarFile jarFile = new JarFile(file)) { + return jarFile.stream() + .filter(entryPredicate) + .collect(Collectors.toList()); + } + } + + /** + * Getting the content value of a specific variable process. + * + * @param client A {@link BonitaClient} + * @param caseId A process instance id. + * @param variableProcessName The name of the variable process, it must have been already declared in the output map of the connector before building the + * connector to test. + * @return The content of the variable. Can be null. + */ + public static String getProcessVariableValue(BonitaClient client, String caseId, String variableProcessName) { + return client.get(ProcessInstanceVariableApi.class).getVariableByProcessInstanceId(caseId, variableProcessName) + .getValue(); + + } + + static class Output { + + private final String name; + private final String type; + + public static Output create(String name, String type) { + return new Output(name, type); + } + + private Output(String name, String type) { + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + } +} diff --git a/src/test/java/org/bonitasoft/connectors/rest/RestConnectorIT.java b/src/test/java/org/bonitasoft/connectors/rest/RestConnectorIT.java new file mode 100644 index 0000000..2071803 --- /dev/null +++ b/src/test/java/org/bonitasoft/connectors/rest/RestConnectorIT.java @@ -0,0 +1,319 @@ +package org.bonitasoft.connectors.rest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Callable; + +import org.bonitasoft.connectors.rest.ConnectorTestToolkit.Output; +import org.bonitasoft.web.client.BonitaClient; +import org.bonitasoft.web.client.api.ArchivedProcessInstanceApi; +import org.bonitasoft.web.client.api.ProcessInstanceApi; +import org.bonitasoft.web.client.exception.NotFoundException; +import org.bonitasoft.web.client.model.ArchivedProcessInstance; +import org.bonitasoft.web.client.services.policies.OrganizationImportPolicy; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class RestConnectorIT { + + private static final String REST_HEAD_DEF_VERSION = "1.0.0"; + private static final String REST_HEAD_DEF_ID = "rest-head"; + + private static final String REST_GET_DEF_VERSION = "1.2.0"; + private static final String REST_GET_DEF_ID = "rest-get"; + + private static final String REST_POST_DEF_VERSION = "1.3.0"; + private static final String REST_POST_DEF_ID = "rest-post"; + + private static final String REST_FILE_POST_DEF_VERSION = "1.0.0"; + private static final String REST_FILE_POST_DEF_ID = "rest-file-post"; + + private static final String REST_PUT_DEF_VERSION = "1.3.0"; + private static final String REST_PUT_DEF_ID = "rest-put"; + + private static final String REST_FILE_PUT_DEF_VERSION = "1.0.0"; + private static final String REST_FILE_PUT_DEF_ID = "rest-file-put"; + + private static final String REST_DELETE_DEF_VERSION = "1.2.0"; + private static final String REST_DELETE_DEF_ID = "rest-delete"; + + private static final Logger LOGGER = LoggerFactory.getLogger(RestConnectorIT.class); + + private static final String ARTIFACT_ID = "bonita-connector-rest"; + private static final String BONITA_VERSION = Objects.requireNonNull(System.getProperty("bonita.version"), "'bonita.version' system property not defined !"); + + @ClassRule + public static GenericContainer BONITA_CONTAINER = new GenericContainer<>( + DockerImageName.parse(String.format("bonita:%s", BONITA_VERSION))) + .withExposedPorts(8080) + .waitingFor(Wait.forHttp("/bonita")) + .withLogConsumer(new Slf4jLogConsumer(LOGGER)); + + @BeforeClass + public static void installOrganization() { + var client = BonitaClient + .builder(String.format("http://%s:%s/bonita", BONITA_CONTAINER.getHost(), + BONITA_CONTAINER.getFirstMappedPort())) + .build(); + client.login("install", "install"); + client.users().importOrganization(new File(RestConnectorIT.class.getResource("/ACME.xml").getFile()), + OrganizationImportPolicy.IGNORE_DUPLICATES); + client.logout(); + } + + private BonitaClient client; + + @Before + public void login() { + client = BonitaClient + .builder(String.format("http://%s:%s/bonita", BONITA_CONTAINER.getHost(), + BONITA_CONTAINER.getFirstMappedPort())) + .build(); + client.login("install", "install"); + } + + @After + public void logout() { + client.logout(); + } + + @Test + public void testRestGetConnectorIntegration() throws Exception { + // Inputs + Map inputsConnector = new HashMap<>(); + inputsConnector.put("url", "https://jsonplaceholder.typicode.com/todos/1"); + + // Outputs + Map outputsConnector = new HashMap<>(); + outputsConnector.put("resultRestGet", Output.create("bodyAsString", String.class.getName())); + + // Building process with connector + var barFile = ConnectorTestToolkit.buildConnectorToTest(REST_GET_DEF_ID, REST_GET_DEF_VERSION, inputsConnector, + outputsConnector, ARTIFACT_ID); + + // Importing and launching the process contained in the business archive + var processResponse = ConnectorTestToolkit.importAndLaunchProcess(barFile, client); + + // Wait until the process launched is started (and not failed) + await().until(pollInstanceState(client, processResponse.getCaseId()), "started"::equals); + + // Getting the result of the rest call. + String resultRestGetResult = (String) ConnectorTestToolkit.getProcessVariableValue(client, + processResponse.getCaseId(), "resultRestGet"); + assertNotNull(resultRestGetResult); + + ObjectMapper mapper = new ObjectMapper(); + var map = mapper.readValue(resultRestGetResult, Map.class); + assertEquals(1, map.get("userId")); + assertEquals(1, map.get("id")); + assertEquals("delectus aut autem", map.get("title")); + assertFalse((boolean) map.get("completed")); + } + + @Test + public void testRestHeadConnectorIntegration() throws Exception { + // Inputs + Map inputsConnector = new HashMap<>(); + inputsConnector.put("url", "https://jsonplaceholder.typicode.com/posts/1"); + + // Outputs + var connectorOuput = Map.ofEntries(entry("headerResult", Output.create("headers", Map.class.getName())), + entry("statusResult", Output.create("status_code", Integer.class.getName()))); + + // Building process with connector + var barFile = ConnectorTestToolkit.buildConnectorToTest(REST_HEAD_DEF_ID, REST_HEAD_DEF_VERSION, inputsConnector, connectorOuput, + ARTIFACT_ID); + + // Importing and launching the process contained in the business archive + var processResponse = ConnectorTestToolkit.importAndLaunchProcess(barFile, client); + + // Wait until the process launched is started (and not failed) + await().until(pollInstanceState(client, processResponse.getCaseId()), "started"::equals); + + // Getting the results of the rest call. + String status = ConnectorTestToolkit.getProcessVariableValue(client, + processResponse.getCaseId(), "statusResult"); + assertThat(Integer.parseInt(status)).isEqualTo(200); + + String headers = ConnectorTestToolkit.getProcessVariableValue(client, + processResponse.getCaseId(), "headerResult"); + assertNotNull(headers); + assertThat(headers).containsIgnoringCase("Content-Type=application/json"); + } + + @Test + public void testRestPostConnectorIntegration() throws Exception { + // Inputs + Map inputsConnector = new HashMap<>(); + inputsConnector.put("url", "https://jsonplaceholder.typicode.com/posts/"); + inputsConnector.put("contentType", "application/json"); + inputsConnector.put("charset", "UTF-8"); + + // Outputs + var connectorOuput = Map.ofEntries(entry("response", Output.create("bodyAsString", String.class.getName())), + entry("statusResult", Output.create("status_code", Integer.class.getName()))); + + // Building process with connector + var barFile = ConnectorTestToolkit.buildConnectorToTest(REST_POST_DEF_ID, REST_POST_DEF_VERSION, inputsConnector, connectorOuput, + ARTIFACT_ID); + + // Importing and launching the process contained in the business archive + var processResponse = ConnectorTestToolkit.importAndLaunchProcess(barFile, client); + + // Wait until the process launched is started (and not failed) + await().until(pollInstanceState(client, processResponse.getCaseId()), "started"::equals); + + String status = ConnectorTestToolkit.getProcessVariableValue(client, + processResponse.getCaseId(), "statusResult"); + assertThat(Integer.parseInt(status)).isEqualTo(201); + String response = ConnectorTestToolkit.getProcessVariableValue(client, + processResponse.getCaseId(), "response"); + ObjectMapper mapper = new ObjectMapper(); + var map = mapper.readValue(response, Map.class); + assertThat(map).containsExactly(entry("id", 101)); + } + + @Test + public void testRestPutConnectorIntegration() throws Exception { + // Inputs + Map inputsConnector = new HashMap<>(); + inputsConnector.put("url", "https://jsonplaceholder.typicode.com/posts/1"); + inputsConnector.put("contentType", "application/json"); + inputsConnector.put("charset", "UTF-8"); + inputsConnector.put("body", "{ \"title\" : \"updated\"}"); + + // Outputs + var connectorOuput = Map.ofEntries(entry("statusResult", Output.create("status_code", Integer.class.getName())), + entry("response", Output.create("bodyAsString", String.class.getName()))); + + // Building process with connector + var barFile = ConnectorTestToolkit.buildConnectorToTest(REST_PUT_DEF_ID, REST_PUT_DEF_VERSION, inputsConnector, connectorOuput, + ARTIFACT_ID); + + // Importing and launching the process contained in the business archive + var processResponse = ConnectorTestToolkit.importAndLaunchProcess(barFile, client); + + // Wait until the process launched is started (and not failed) + await().until(pollInstanceState(client, processResponse.getCaseId()), "started"::equals); + + String status = ConnectorTestToolkit.getProcessVariableValue(client, + processResponse.getCaseId(), "statusResult"); + assertThat(Integer.parseInt(status)).isEqualTo(200); + var response = ConnectorTestToolkit.getProcessVariableValue(client, + processResponse.getCaseId(), "response"); + assertThat(response).contains("updated"); + } + + @Test + public void testRestFilePutConnectorIntegration() throws Exception { + // Inputs + Map inputsConnector = new HashMap<>(); + inputsConnector.put("url", "https://jsonplaceholder.typicode.com/posts/1"); + inputsConnector.put("contentType", "application/json"); + inputsConnector.put("charset", "UTF-8"); + + // Outputs + + // Building process with connector + var barFile = ConnectorTestToolkit.buildConnectorToTest(REST_FILE_PUT_DEF_ID, REST_FILE_PUT_DEF_VERSION, inputsConnector, null, + ARTIFACT_ID); + + // Importing and launching the process contained in the business archive + var processResponse = ConnectorTestToolkit.importAndLaunchProcess(barFile, client); + + // Wait until the process launched is started (and not failed) + await().until(pollInstanceState(client, processResponse.getCaseId()), "started"::equals); + } + + @Test + public void testRestFilePostConnectorIntegration() throws Exception { + // Inputs + Map inputsConnector = new HashMap<>(); + inputsConnector.put("url", "https://jsonplaceholder.typicode.com/posts/1"); + inputsConnector.put("contentType", "application/json"); + inputsConnector.put("charset", "UTF-8"); + + // Outputs + + // Building process with connector + var barFile = ConnectorTestToolkit.buildConnectorToTest(REST_FILE_POST_DEF_ID, REST_FILE_POST_DEF_VERSION, inputsConnector, null, + ARTIFACT_ID); + + // Importing and launching the process contained in the business archive + var processResponse = ConnectorTestToolkit.importAndLaunchProcess(barFile, client); + + // Wait until the process launched is started (and not failed) + await().until(pollInstanceState(client, processResponse.getCaseId()), "started"::equals); + } + + @Test + public void testRestDeleteConnectorIntegration() throws Exception { + // Inputs + Map inputsConnector = new HashMap<>(); + inputsConnector.put("url", "https://jsonplaceholder.typicode.com/posts/1"); + + // Outputs + var connectorOuput = Map.ofEntries(entry("statusResult", Output.create("status_code", Integer.class.getName()))); + + + // Building process with connector + var barFile = ConnectorTestToolkit.buildConnectorToTest(REST_DELETE_DEF_ID, REST_DELETE_DEF_VERSION, inputsConnector, connectorOuput, + ARTIFACT_ID); + + // Importing and launching the process contained in the business archive + var processResponse = ConnectorTestToolkit.importAndLaunchProcess(barFile, client); + + // Wait until the process launched is started (and not failed) + await().until(pollInstanceState(client, processResponse.getCaseId()), "started"::equals); + + String status = ConnectorTestToolkit.getProcessVariableValue(client, + processResponse.getCaseId(), "statusResult"); + assertThat(Integer.parseInt(status)).isEqualTo(200); + } + + private Callable pollInstanceState(BonitaClient client, String id) { + return () -> { + try { + var instance = client.get(ProcessInstanceApi.class).getProcessInstanceById(id, (String) null); + return instance.getState().toLowerCase(); + } catch (NotFoundException e) { + return getCompletedProcess(id).getState().toLowerCase(); + } + }; + } + + private ArchivedProcessInstance getCompletedProcess(String id) { + var archivedInstances = client.get(ArchivedProcessInstanceApi.class) + .searchArchivedProcessInstances( + new ArchivedProcessInstanceApi.SearchArchivedProcessInstancesQueryParams() + .c(1) + .p(0) + .f(List.of("caller=any", "sourceObjectId=" + id))); + if (!archivedInstances.isEmpty()) { + return archivedInstances.get(0); + } + return null; + } + +} diff --git a/src/test/resources/ACME.xml b/src/test/resources/ACME.xml new file mode 100644 index 0000000..78f0b37 --- /dev/null +++ b/src/test/resources/ACME.xml @@ -0,0 +1,869 @@ + + + + + asdasdasd + + + + + + William + Jobs + Mr + Chief Executive Officer + + + william.jobs@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + April + Sanchez + Mrs + Compensation specialist + helen.kelly + + + april.sanchez@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Helen + Kelly + Mrs + Human resource manager + william.jobs + + + helen.kelly@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Walter + Bates + Mr + Human resources benefits + helen.kelly + + + walter.bates@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Zachary + Williamson + Mr + Chief Financial Officer + william.jobs + + + zachary.williamson@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Patrick + Gardenier + Mr + Financial controller + zachary.williamson + + + patrick.gardenier@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Virginie + Jomphe + Mrs + Accountant + zachary.williamson + + + virginie.jomphe@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Thorsten + Hartmann + Mr + Financial planning manager + zachary.williamson + + + thorsten.hartmann@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Jan + Fisher + Mr + Infrastucture specialist + favio.riviera + + + jan.fisher@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Isabel + Bleasdale + Mrs + Product marketing manager + favio.riviera + + + isabel.bleasdale@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Favio + Riviera + Mr + Vice President of Marketing + william.jobs + + + favio.riviera@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Michael + Morrison + Mr + Chief Technical Officer + william.jobs + + + michael.morrison@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Marc + Marseau + Mr + Engineer + michael.morrison + + + marc.marseau@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Joseph + Hovell + Mr + Engineer + michael.morrison + + + joseph.hovell@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Mauro + Zetticci + Mr + Consultant + michael.morrison + + + mauro.zetticci@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Thomas + Wallis + Mr + Consultant + michael.morrison + + + thomas.wallis@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Daniela + Angelo + Mrs + Vice President of Sales + william.jobs + + + daniela.angelo@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Anthony + Nichols + Mr + Account manager + daniela.angelo + + + anthony.nichols@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Misa + Kumagai + Mrs + Account manager + daniela.angelo + + + misa.kumagai@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Norio + Yamazaki + Mr + Account manager + daniela.angelo + + + norio.yamazaki@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+ + Giovanna + Almeida + Mrs + Account manager + daniela.angelo + + + giovanna.almeida@acme.com + 484-302-5000 + 484-302-0000 + 70 +
Renwick Drive
+ 19108 + Philadelphia + PA + United States +
+ + + + + + true + bpm + + + asdasdasd + + + +
+
+ + + Member + + + + + + Acme + This group represents the acme department of the ACME organization + + + Human Resources + This group represents the human resources department of the ACME organization + + + Finance + This group represents the finance department of the ACME organization + + + Infrastructure + This group represents the infrastructure department of the ACME organization + + + Marketing + This group represents the marketing department of the ACME organization + + + Production + This group represents the production department of the ACME organization + + + Sales + This group represents the sales department of the ACME organization + + + Europe + This group represents the europe department of the ACME organization + + + Asia + This group represents the asia department of the ACME organization + + + Latin America + This group represents the latin america department of the ACME organization + + + North America + This group represents the north america department of the ACME organization + + + Research & Development + This group represents the research & development department of the ACME organization + + + Services + This group represents the services department of the ACME organization + + + + + william.jobs + member + acme + + + april.sanchez + member + hr + /acme + + + helen.kelly + member + hr + /acme + + + walter.bates + member + hr + /acme + + + zachary.williamson + member + finance + /acme + + + patrick.gardenier + member + finance + /acme + + + virginie.jomphe + member + finance + /acme + + + thorsten.hartmann + member + finance + /acme + + + jan.fisher + member + it + /acme + + + isabel.bleasdale + member + marketing + /acme + + + favio.riviera + member + marketing + /acme + + + michael.morrison + member + production + /acme + + + marc.marseau + member + rd + /acme/production + + + joseph.hovell + member + rd + /acme/production + + + mauro.zetticci + member + services + /acme/production + + + thomas.wallis + member + services + /acme/production + + + daniela.angelo + member + europe + /acme/sales + + + misa.kumagai + member + asia + /acme/sales + + + norio.yamazaki + member + asia + /acme/sales + + + giovanna.almeida + member + latin_america + /acme/sales + + + anthony.nichols + member + north_america + /acme/sales + + +
\ No newline at end of file