Skip to content

Commit

Permalink
Merge pull request #6013 from matthias-ronge/issue-5980
Browse files Browse the repository at this point in the history
Run Kitodo Script commands via Active MQ
  • Loading branch information
solth authored Aug 23, 2024
2 parents 335afc0 + d52fcee commit d838f8e
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,10 @@ public enum ParameterCore implements ParameterInterface {

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

ACTIVE_MQ_KITODO_SCRIPT_ALLOW(new Parameter<UndefinedParameter>("activeMQ.kitodoScript.allow")),

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,71 @@
/*
* (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.Arrays;
import java.util.Collection;
import java.util.List;

import javax.jms.JMSException;

import org.apache.commons.lang3.ArrayUtils;
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 {
final String[] allowedCommands = ConfigCore.getStringArrayParameter(
ParameterCore.ACTIVE_MQ_KITODO_SCRIPT_ALLOW);
try {
String script = ticket.getMandatoryString("script");
int space = script.indexOf(' ');
if (!ArrayUtils.contains(allowedCommands, script.substring(7, space >= 0 ? space : script.length()))) {
throw new IllegalArgumentException((space >= 0 ? script.substring(0, space) : script)
+ " is not allowed");
}
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());
}

/**
* 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
7 changes: 7 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,13 @@ 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

# The Kitodo Script commands authorized to be executed must be named here:
activeMQ.kitodoScript.allow=createFolders&export&searchForMedia


# -----------------------------------
# Elasticsearch properties
# -----------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* (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.junit.jupiter.api.Assertions.assertThrows;
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 {

@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");
}

@Test
public void shouldNotExecuteKitodoScript() throws Exception {
// test data
MapMessageObjectReader mockedMessage = mock(MapMessageObjectReader.class);
when(mockedMessage.getMandatoryString("script")).thenReturn("action:other");

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

// carry out test
IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, () -> underTest
.process(mockedMessage));
assertEquals("action:other is not allowed", illegalArgumentException.getMessage(),
"should report that the action is not permitted");
}
}
2 changes: 2 additions & 0 deletions Kitodo/src/test/resources/kitodo_config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ metsEditor.lockingTime=2

#copyData.onExport=/@GoobiIdentifier \= $process.id;

activeMQ.kitodoScript.allow=test&test2

LongTermPreservationValidation.mapping.FALSE.FALSE=ERROR
LongTermPreservationValidation.mapping.FALSE.TRUE=ERROR
LongTermPreservationValidation.mapping.FALSE.UNDETERMINED=ERROR
Expand Down

0 comments on commit d838f8e

Please sign in to comment.