diff --git a/Kitodo/src/main/java/org/kitodo/config/enums/ParameterCore.java b/Kitodo/src/main/java/org/kitodo/config/enums/ParameterCore.java index c89dddbcc6a..29ea0699e9e 100644 --- a/Kitodo/src/main/java/org/kitodo/config/enums/ParameterCore.java +++ b/Kitodo/src/main/java/org/kitodo/config/enums/ParameterCore.java @@ -621,6 +621,10 @@ public enum ParameterCore implements ParameterInterface { ACTIVE_MQ_FINALIZE_STEP_QUEUE(new Parameter("activeMQ.finalizeStep.queue")), + ACTIVE_MQ_KITODO_SCRIPT_ALLOW(new Parameter("activeMQ.kitodoScript.allow")), + + ACTIVE_MQ_KITODO_SCRIPT_QUEUE(new Parameter("activeMQ.kitodoScript.queue")), + ACTIVE_MQ_TASK_ACTION_QUEUE(new Parameter("activeMQ.taskAction.queue")), ACTIVE_MQ_USER(new Parameter("activeMQ.user")), diff --git a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/ActiveMQDirector.java b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/ActiveMQDirector.java index 73a2a316b61..0bcd1ee4fdf 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/ActiveMQDirector.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/ActiveMQDirector.java @@ -57,7 +57,7 @@ public class ActiveMQDirector implements Runnable, ServletContextListener { private static Collection services; static { - services = Arrays.asList(new FinalizeStepProcessor(), new TaskActionProcessor()); + services = Arrays.asList(new FinalizeStepProcessor(), new TaskActionProcessor(), new KitodoScriptProcessor()); } private static Connection connection = null; diff --git a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/KitodoScriptProcessor.java b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/KitodoScriptProcessor.java new file mode 100644 index 00000000000..d9924126585 --- /dev/null +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/KitodoScriptProcessor.java @@ -0,0 +1,71 @@ +/* + * (c) Kitodo. Key to digital objects e. V. + * + * 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 processIds = ticket.getCollectionOfInteger("processes"); + List 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); + } + } +} diff --git a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/MapMessageObjectReader.java b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/MapMessageObjectReader.java index 21837bc7956..c7e128f6772 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/MapMessageObjectReader.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/MapMessageObjectReader.java @@ -11,6 +11,7 @@ package org.kitodo.production.interfaces.activemq; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -18,10 +19,12 @@ 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; @@ -109,6 +112,51 @@ public String getMandatoryString(String key) throws JMSException { return mandatoryString; } + /** + * Fetches a {@code Collection} 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 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} 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 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 diff --git a/Kitodo/src/main/resources/kitodo_config.properties b/Kitodo/src/main/resources/kitodo_config.properties index 0570ff44a0f..22b3a1f0b0a 100644 --- a/Kitodo/src/main/resources/kitodo_config.properties +++ b/Kitodo/src/main/resources/kitodo_config.properties @@ -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 # ----------------------------------- diff --git a/Kitodo/src/test/java/org/kitodo/production/interfaces/activemq/KitodoScriptProcessorIT.java b/Kitodo/src/test/java/org/kitodo/production/interfaces/activemq/KitodoScriptProcessorIT.java new file mode 100644 index 00000000000..4329189a81f --- /dev/null +++ b/Kitodo/src/test/java/org/kitodo/production/interfaces/activemq/KitodoScriptProcessorIT.java @@ -0,0 +1,119 @@ +/* + * (c) Kitodo. Key to digital objects e. V. + * + * 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> processCaptor; + + @Captor + private ArgumentCaptor 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"); + } +} diff --git a/Kitodo/src/test/resources/kitodo_config.properties b/Kitodo/src/test/resources/kitodo_config.properties index 0ab62453b40..7c6bdbfa4a3 100644 --- a/Kitodo/src/test/resources/kitodo_config.properties +++ b/Kitodo/src/test/resources/kitodo_config.properties @@ -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