Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run Kitodo Script commands via Active MQ #6013

Merged
merged 13 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,8 @@ public enum ParameterCore implements ParameterInterface {

ACTIVE_MQ_FINALIZE_STEP_QUEUE(new Parameter<UndefinedParameter>("activeMQ.finalizeStep.queue")),

ACTIVE_MQ_KITODO_SCRIPT_QUEUE(new Parameter<UndefinedParameter>("activeMQ.kitodoScript.queue")),

ACTIVE_MQ_TASK_ACTION_QUEUE(new Parameter<UndefinedParameter>("activeMQ.taskAction.queue")),

ACTIVE_MQ_USER(new Parameter<UndefinedParameter>("activeMQ.user")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public class ActiveMQDirector implements Runnable, ServletContextListener {
private static Collection<? extends ActiveMQProcessor> services;

static {
services = Arrays.asList(new FinalizeStepProcessor(), new TaskActionProcessor());
services = Arrays.asList(new FinalizeStepProcessor(), new TaskActionProcessor(), new KitodoScriptProcessor());
}

private static Connection connection = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* (c) Kitodo. Key to digital objects e. V. <[email protected]>
*
* This file is part of the Kitodo project.
*
* It is licensed under GNU General Public License version 3 or later.
*
* For the full copyright and license information, please read the
* GPL3-License.txt file that was distributed with this source code.
*/

package org.kitodo.production.interfaces.activemq;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.jms.JMSException;

import org.kitodo.config.ConfigCore;
import org.kitodo.config.enums.ParameterCore;
import org.kitodo.data.database.beans.Process;
import org.kitodo.data.database.exceptions.DAOException;
import org.kitodo.data.exceptions.DataException;
import org.kitodo.exceptions.InvalidImagesException;
import org.kitodo.exceptions.MediaNotFoundException;
import org.kitodo.exceptions.ProcessorException;
import org.kitodo.production.services.ServiceManager;
import org.kitodo.production.services.command.KitodoScriptService;
import org.kitodo.production.services.data.ProcessService;

/**
* Executes instructions to start a Kitodo Script command from the Active MQ
* interface. The MapMessage must contain the command statement in the
* {@code script} argument. You pass a list of the process IDs as
* {@code processes}.
*/
public class KitodoScriptProcessor extends ActiveMQProcessor {

private final KitodoScriptService kitodoScriptService = ServiceManager.getKitodoScriptService();
private final ProcessService processService = ServiceManager.getProcessService();

public KitodoScriptProcessor() {
super(ConfigCore.getOptionalString(ParameterCore.ACTIVE_MQ_KITODO_SCRIPT_QUEUE).orElse(null));
}

@Override
protected void process(MapMessageObjectReader ticket) throws ProcessorException, JMSException {
try {
String script = ticket.getMandatoryString("script");
Collection<Integer> processIds = ticket.getCollectionOfInteger("processes");
List<Process> processes = new ArrayList<>(processIds.size());
for (Integer id : processIds) {
processes.add(processService.getById(id));
}
kitodoScriptService.execute(processes, script);
} catch (DAOException | DataException | IOException | InvalidImagesException | MediaNotFoundException e) {
throw new ProcessorException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,20 @@

package org.kitodo.production.interfaces.activemq;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import javax.jms.JMSException;
import javax.jms.MapMessage;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.kitodo.utils.Guard;
Expand Down Expand Up @@ -109,6 +112,51 @@ public String getMandatoryString(String key) throws JMSException {
return mandatoryString;
}

/**
* Fetches a {@code Collection<Integer>} from a MapMessage. This is a loose
* implementation for an optional object with optional content. The
* collection content is filtered through {@code toString()} and split on
* non-digits, dealing generously with list variants and separators. If not
* found, returns an empty collection, never {@code null}.
*
* @param key
* the name of the set to return
* @return the set requested
* @throws JMSException
* can be thrown by MapMessage.getObject(String)
*/
public Collection<Integer> getCollectionOfInteger(String key) throws JMSException {
return getCollectionOfString(key).stream()
.flatMap(string -> Arrays.stream(string.split("\\D+"))).filter(StringUtils::isNumeric)
.map(Integer::valueOf).collect(Collectors.toList());
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved
Dismissed Show dismissed Hide dismissed
}

/**
* Fetches a {@code Collection<String>} from a MapMessage. This is a loose
* implementation for an optional object with optional content. The
* collection content is filtered through {@code toString()}, {@code null}
* objects will be skipped. If not found, returns an empty collection, never
* {@code null}.
*
* @param key
* the name of the set to return
* @return the set requested
* @throws JMSException
* can be thrown by MapMessage.getObject(String)
*/
public Collection<String> getCollectionOfString(String key) throws JMSException {

Object collectionObject = ticket.getObject(key);
if (Objects.isNull(collectionObject)) {
return Collections.emptyList();
}
if (!(collectionObject instanceof Collection<?>)) {
return Collections.singletonList(collectionObject.toString());
}
return ((Collection<?>) collectionObject).stream().filter(Objects::nonNull).map(Object::toString)
.collect(Collectors.toList());
}

/**
* Fetches a String from a MapMessage. This is an access forward to the
* native function of the MapMessage. You may consider to use
Expand Down
3 changes: 3 additions & 0 deletions Kitodo/src/main/resources/kitodo_config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,9 @@ activeMQ.user=testAdmin
# You can provide a queue from which messages are read to process task actions
#activeMQ.taskAction.queue=KitodoProduction.TaskAction.Queue

# You can provide a queue from which messages are read to run a Kitodo Script
#activeMQ.kitodoScript.queue=KitodoProduction.KitodoScript.Queue

# -----------------------------------
# Elasticsearch properties
# -----------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* (c) Kitodo. Key to digital objects e. V. <[email protected]>
*
* This file is part of the Kitodo project.
*
* It is licensed under GNU General Public License version 3 or later.
*
* For the full copyright and license information, please read the
* GPL3-License.txt file that was distributed with this source code.
*/

package org.kitodo.production.interfaces.activemq;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.anyList;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.commons.util.ReflectionUtils;
import org.kitodo.MockDatabase;
import org.kitodo.SecurityTestUtils;
import org.kitodo.data.database.beans.Process;
import org.kitodo.production.services.ServiceManager;
import org.kitodo.production.services.command.KitodoScriptService;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class KitodoScriptProcessorIT {
matthias-ronge marked this conversation as resolved.
Show resolved Hide resolved

@Captor
private ArgumentCaptor<List<Process>> processCaptor;

@Captor
private ArgumentCaptor<String> scriptCaptor;

@BeforeEach
public void prepare() throws Exception {
MockDatabase.startNode();
MockDatabase.insertProcessesForWorkflowFull();
SecurityTestUtils.addUserDataToSecurityContext(ServiceManager.getUserService().getById(1), 1);
}

@AfterEach
public void clean() throws Exception {
MockDatabase.stopNode();
MockDatabase.cleanDatabase();
SecurityTestUtils.cleanSecurityContext();
}

@Test
public void shouldExecuteKitodoScript() throws Exception {

// define test data
MapMessageObjectReader mockedMappedMessageObjectReader = mock(MapMessageObjectReader.class);
when(mockedMappedMessageObjectReader.getMandatoryString("script")).thenReturn("action:test");
when(mockedMappedMessageObjectReader.getCollectionOfInteger("processes")).thenReturn(Collections.singletonList(1));

// the object to be tested
KitodoScriptProcessor underTest = new KitodoScriptProcessor();

// manipulate static field to insert a mocked service
// using MockStatic or other options did not work or too less knowdlegde to manipulate a static field
KitodoScriptService kitodoScriptService = mock(KitodoScriptService.class);
Field field = ReflectionUtils
.findFields(
KitodoScriptProcessor.class, f -> f.getName().equals("kitodoScriptService"),
ReflectionUtils.HierarchyTraversalMode.TOP_DOWN)
.get(0);
field.setAccessible(true);
field.set(underTest, kitodoScriptService);

// capture the method parameters of the execute method
doNothing().when(kitodoScriptService).execute(processCaptor.capture(), scriptCaptor.capture());

// carry out test
underTest.process(mockedMappedMessageObjectReader);

// check executed mocks
verify(mockedMappedMessageObjectReader, times(1)).getMandatoryString("script");
verify(mockedMappedMessageObjectReader, times(1)).getCollectionOfInteger("processes");
verify(kitodoScriptService, times(1)).execute(anyList(), anyString());

// check results
assertEquals("action:test", scriptCaptor.getValue(), "should have passed the script to be executed");
assertEquals(1, processCaptor.getAllValues().size(), "should have passed one process");
assertEquals(1, processCaptor.getAllValues().get(0).get(0).getId(), "should have passed process 1");
}
}