diff --git a/Kitodo/pom.xml b/Kitodo/pom.xml index 553293812aa..20aed63d2b0 100644 --- a/Kitodo/pom.xml +++ b/Kitodo/pom.xml @@ -226,6 +226,12 @@ Saxon-HE compile + + com.github.spotbugs + spotbugs-annotations + ${spotbugs-maven-plugin.version} + provided + se.jiderhamn.classloader-leak-prevention classloader-leak-prevention-servlet3 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 29ea0699e9e..c0801b94a0a 100644 --- a/Kitodo/src/main/java/org/kitodo/config/enums/ParameterCore.java +++ b/Kitodo/src/main/java/org/kitodo/config/enums/ParameterCore.java @@ -619,6 +619,8 @@ public enum ParameterCore implements ParameterInterface { ACTIVE_MQ_AUTH_PASSWORD(new Parameter<>("activeMQ.authPassword", "")), + ACTIVE_MQ_CREATE_NEW_PROCESSES_QUEUE(new Parameter("activeMQ.createNewProcesses.queue")), + ACTIVE_MQ_FINALIZE_STEP_QUEUE(new Parameter("activeMQ.finalizeStep.queue")), ACTIVE_MQ_KITODO_SCRIPT_ALLOW(new Parameter("activeMQ.kitodoScript.allow")), diff --git a/Kitodo/src/main/java/org/kitodo/production/helper/TempProcess.java b/Kitodo/src/main/java/org/kitodo/production/helper/TempProcess.java index 6acd5283cf3..bba10c12da1 100644 --- a/Kitodo/src/main/java/org/kitodo/production/helper/TempProcess.java +++ b/Kitodo/src/main/java/org/kitodo/production/helper/TempProcess.java @@ -215,7 +215,7 @@ public void verifyDocType() throws IOException, ProcessGenerationException { if (docTypeMetadata.isPresent() && docTypeMetadata.get() instanceof MetadataEntry) { String docType = ((MetadataEntry)docTypeMetadata.get()).getValue(); if (StringUtils.isNotBlank(docType) - && !this.getWorkpiece().getLogicalStructure().getType().equals(docType)) { + && !Objects.equals(this.getWorkpiece().getLogicalStructure().getType(), docType)) { this.getWorkpiece().getLogicalStructure().setType(docType); } } 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 0bcd1ee4fdf..002dcca9513 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 @@ -54,10 +54,11 @@ public class ActiveMQDirector implements Runnable, ServletContextListener { private static final Logger logger = LogManager.getLogger(ActiveMQDirector.class); // When implementing new services, add them to this list - private static Collection services; + private static Collection services; static { - services = Arrays.asList(new FinalizeStepProcessor(), new TaskActionProcessor(), new KitodoScriptProcessor()); + services = Arrays.asList(new FinalizeStepProcessor(), new TaskActionProcessor(), + new CreateNewProcessesProcessor(), new KitodoScriptProcessor()); } private static Connection connection = null; diff --git a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java new file mode 100644 index 00000000000..73eace87ad3 --- /dev/null +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java @@ -0,0 +1,310 @@ +/* + * (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 edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; + +import javax.jms.JMSException; + +import org.apache.commons.lang3.tuple.Pair; +import org.kitodo.api.MdSec; +import org.kitodo.api.Metadata; +import org.kitodo.api.MetadataEntry; +import org.kitodo.api.MetadataGroup; +import org.kitodo.data.database.beans.ImportConfiguration; +import org.kitodo.data.database.beans.Process; +import org.kitodo.data.database.beans.Template; +import org.kitodo.data.database.exceptions.DAOException; +import org.kitodo.data.exceptions.DataException; +import org.kitodo.exceptions.ProcessorException; +import org.kitodo.production.dto.ProcessDTO; +import org.kitodo.production.services.ServiceManager; +import org.kitodo.production.services.data.ImportConfigurationService; + +/** + * Order to create a new process. This contains all the necessary data. + */ +public class CreateNewProcessOrder { + + /* Catalog imports can be specified (none, one or more). An + * "importconfiguration" and a search "value" must be specified. The search + * is carried out in the default search field. If no hit is found, or more + * than one, the search aborts with an error message. In the case of + * multiple imports, a repeated import is carried out according to the + * procedure specified in the rule set. */ + private static final String FIELD_IMPORT = "import"; + private static final String FIELD_IMPORT_CONFIG = "importconfiguration"; + private static final String FIELD_IMPORT_VALUE = "value"; + + /* Additionally metadata can be passed. Passing multiple metadata or passing + * grouped metadata is also possible. */ + private static final String FIELD_METADATA = "metadata"; + + /* A parent process can optionally be specified. The process ID or the + * process title can be specified. (If the value is all digits, it is + * considered the process ID, else it is considered the process title.) The + * process must be found in the client’s processes. If no parent process is + * specified, but a metadata entry with a use="higherLevelIdentifier" is + * included in the data from the catalog, the parent process is searched for + * using the metadata entry with use="recordIdentifier". It must already + * exist for the client. No parent process is implicitly created. The child + * process is added at the last position in the parent process. */ + private static final String FIELD_PARENT = "parent"; + + // Mandatory information is the project ID. + private static final String FIELD_PROJECT = "project"; + + // Mandatory information is the process template. + private static final String FIELD_TEMPLATE = "template"; + + /* A process title can optionally be specified. If it is specified + * explicitly, exactly this process title is used, otherwise the system + * creates the process title according to the configured rule. The process + * title must still be unused for the client who owns the project. */ + private static final String FIELD_TITLE = "title"; + + private final Integer projectId; + private final Integer templateId; + private final List> imports; + private final Optional title; + private final Optional parentId; + private final Collection metadata; + + /** + * Creates a new CreateNewProcessOrder from an Active MQ message. + * + * @param ticket + * Active MQ message with (hopefully) all the data + * @throws DAOException + * if the ImportConfiguartionDAO is unable to find an import + * configuration with the given ID + * @throws DataException + * if there is an error accessing the search service + * @throws IllegalArgumentException + * If a required field is missing in the Active MQ message + * message, or contains inappropriate values. + * @throws JMSException + * Defined by the JMS API. I have not seen any cases where this + * would actually be thrown in the calls used here. + * @throws ProcessorException + * if the process count for the title is not exactly one + */ + CreateNewProcessOrder(MapMessageObjectReader ticket) throws DAOException, DataException, JMSException, + ProcessorException { + this.projectId = ticket.getMandatoryInteger(FIELD_PROJECT); + this.templateId = ticket.getMandatoryInteger(FIELD_TEMPLATE); + this.imports = convertImports(ticket.getList(FIELD_IMPORT)); + this.title = Optional.ofNullable(ticket.getString(FIELD_TITLE)); + this.parentId = Optional.ofNullable(convertProcessId(ticket.getString(FIELD_PARENT))); + this.metadata = convertMetadata(ticket.getMapOfString(FIELD_METADATA), MdSec.DMD_SEC); + } + + /** + * Converts import details into safe data objects. For {@code null}, it will + * return an empty list, never {@code null}. + * + * @throws IllegalArgumentException + * if a list member is not a map, or one of the mandatory map + * entries is missing or of a wrong type + * @throws DAOException + * if the ImportConfiguartionDAO is unable to find an import + * configuration with that ID + */ + private static final List> convertImports(@Nullable List imports) + throws DAOException { + + if (Objects.isNull(imports) || imports.isEmpty()) { + return Collections.emptyList(); + } + + final ImportConfigurationService importConfigurationService = ServiceManager.getImportConfigurationService(); + List> result = new ArrayList<>(); + for (Object dubious : imports) { + if (!(dubious instanceof Map)) { + throw new IllegalArgumentException("Entry of \"imports\" is not a map"); + } + Map map = (Map) dubious; + ImportConfiguration importconfiguration = importConfigurationService.getById(MapMessageObjectReader + .getMandatoryInteger(map, FIELD_IMPORT_CONFIG)); + String value = MapMessageObjectReader.getMandatoryString(map, FIELD_IMPORT_VALUE); + result.add(Pair.of(importconfiguration, value)); + } + return result; + } + + /** + * Gets the process ID. If the string is an integer, it is used as the + * process ID. Otherwise it is considered a title and searched for. If it is + * a title, there must be exactly one process for it to be converted to an + * ID. + * + * @param processId + * parent process reference + * @return ID of the parent process + * @throws DataException + * if there is an error accessing the search service + * @throws ProcessorException + * if the process count for the title is not exactly one + */ + @CheckForNull + private static final Integer convertProcessId(String processId) throws DataException, ProcessorException { + if (Objects.isNull(processId)) { + return null; + } + if (processId.matches("\\d+")) { + return Integer.valueOf(processId); + } else { + List parents = ServiceManager.getProcessService().findByTitle(processId); + if (parents.size() == 0) { + throw new ProcessorException("Parent process not found"); + } else if (parents.size() > 1) { + throw new ProcessorException("Parent process exists more than one"); + } else { + return parents.get(0).getId(); + } + } + } + + /** + * Converts metadata details into safe data objects. For {@code null}, it + * will return an empty collection, never {@code null}. + */ + private static final HashSet convertMetadata(@Nullable Map metadata, @Nullable MdSec domain) { + + HashSet result = new HashSet<>(); + if (Objects.isNull(metadata)) { + return result; + } + + for (Entry entry : metadata.entrySet()) { + Object dubiousKey = entry.getKey(); + if (!(dubiousKey instanceof String) || ((String) dubiousKey).isEmpty()) { + throw new IllegalArgumentException("Invalid metadata key"); + } + String key = (String) dubiousKey; + + Object dubiousValuesList = entry.getValue(); + if (!(dubiousValuesList instanceof List)) { + dubiousValuesList = Collections.singletonList(dubiousValuesList); + } + for (Object dubiousValue : (List) dubiousValuesList) { + if (dubiousValue instanceof Map) { + MetadataGroup metadataGroup = new MetadataGroup(); + metadataGroup.setKey(key); + metadataGroup.setMetadata(convertMetadata((Map) dubiousValue, null)); + metadataGroup.setDomain(domain); + result.add(metadataGroup); + } else { + MetadataEntry metadataEntry = new MetadataEntry(); + metadataEntry.setKey(key); + metadataEntry.setValue(dubiousValue.toString()); + metadataEntry.setDomain(domain); + result.add(metadataEntry); + } + } + } + return result; + } + + /** + * Returns the project ID. This is a mandatory field and can never be + * {@code null}. + * + * @return the project ID + */ + @NonNull + Integer getProjectId() { + return projectId; + } + + /** + * Returns the production template. + * + * @return the template + * @throws DAOException + * if the template cannot be loaded + */ + Template getTemplate() throws DAOException { + return ServiceManager.getTemplateService().getById(templateId); + } + + /** + * Returns the production template ID. This is a mandatory field and can + * never be {@code null}. + * + * @return the template ID + */ + @NonNull + Integer getTemplateId() { + return templateId; + } + + /** + * Returns import instructions. Each instruction consists of an import + * configuration and a search value to be searched for in the default search + * field. Subsequent search statements must be executed as additive imports. + * Can be empty, but never {@code null}. + * + * @return import instructions + */ + @NonNull + List> getImports() { + return imports; + } + + /** + * Returns an (optional) predefined title. If specified, this title must be + * used. Otherwise, the title must be formed using the formation rule. Can + * be {@code Optional.empty()}, but never {@code null}. + * + * @return the title, if any + */ + @NonNull + Optional getTitle() { + return title; + } + + /** + * Returns the parent process, if any. + * + * @return the parent process, or {@code null} + * @throws DAOException + * if the process cannot be loaded + */ + @CheckForNull + Process getParent() throws DAOException { + return parentId.isPresent() ? ServiceManager.getProcessService().getById(parentId.get()) : null; + } + + /** + * Specifies the metadata for the logical structure root of the process to + * be created. Can be empty, but never {@code null}. + * + * @return the metadata + */ + @NonNull + Collection getMetadata() { + return metadata; + } +} diff --git a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java new file mode 100644 index 00000000000..87a7d2b10cf --- /dev/null +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -0,0 +1,220 @@ +/* + * (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.net.URISyntaxException; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Locale.LanguageRange; +import java.util.Objects; +import java.util.Set; + +import javax.jms.JMSException; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.xpath.XPathExpressionException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.kitodo.api.Metadata; +import org.kitodo.api.dataeditor.rulesetmanagement.FunctionalMetadata; +import org.kitodo.api.dataeditor.rulesetmanagement.RulesetManagementInterface; +import org.kitodo.api.dataformat.Workpiece; +import org.kitodo.config.ConfigCore; +import org.kitodo.config.enums.ParameterCore; +import org.kitodo.data.database.beans.Process; +import org.kitodo.data.database.beans.Task; +import org.kitodo.data.database.exceptions.DAOException; +import org.kitodo.data.elasticsearch.exceptions.CustomResponseException; +import org.kitodo.data.exceptions.DataException; +import org.kitodo.exceptions.CommandException; +import org.kitodo.exceptions.InvalidMetadataValueException; +import org.kitodo.exceptions.NoRecordFoundException; +import org.kitodo.exceptions.NoSuchMetadataFieldException; +import org.kitodo.exceptions.ProcessGenerationException; +import org.kitodo.exceptions.ProcessorException; +import org.kitodo.exceptions.UnsupportedFormatException; +import org.kitodo.production.forms.createprocess.ProcessFieldedMetadata; +import org.kitodo.production.helper.Helper; +import org.kitodo.production.helper.ProcessHelper; +import org.kitodo.production.helper.TempProcess; +import org.kitodo.production.metadata.MetadataEditor; +import org.kitodo.production.process.ProcessGenerator; +import org.kitodo.production.process.ProcessValidator; +import org.kitodo.production.services.ServiceManager; +import org.kitodo.production.services.data.ImportService; +import org.kitodo.production.services.data.ProcessService; +import org.kitodo.production.services.data.RulesetService; +import org.kitodo.production.services.data.TaskService; +import org.kitodo.production.services.dataformat.MetsService; +import org.kitodo.production.services.file.FileService; +import org.xml.sax.SAXException; + +/** + * An Active MQ service interface to create new processes. + */ +public class CreateNewProcessesProcessor extends ActiveMQProcessor { + private static final Logger logger = LogManager.getLogger(CreateNewProcessesProcessor.class); + + private static final String ACQUISITION_STAGE_PROCESS_CREATION = "create"; + private static final int IMPORT_WITHOUT_ANY_HIERARCHY = 1; + private static final String LAST_CHILD = Integer.toString(-1); + private static final List METADATA_LANGUAGE = Locale.LanguageRange.parse("en"); + + private final FileService fileService = ServiceManager.getFileService(); + private final ImportService importService = ServiceManager.getImportService(); + private final MetsService metsService = ServiceManager.getMetsService(); + private final ProcessService processService = ServiceManager.getProcessService(); + private final RulesetService rulesetService = ServiceManager.getRulesetService(); + private final TaskService taskService = ServiceManager.getTaskService(); + + private RulesetManagementInterface rulesetManagement; + + /** + * The default constructor looks up the queue name to use in + * kitodo_config.properties. If that is not configured and “null” is passed + * to the super constructor, this will prevent + * ActiveMQDirector.registerListeners() from starting this service. + */ + public CreateNewProcessesProcessor() { + super(ConfigCore.getOptionalString(ParameterCore.ACTIVE_MQ_CREATE_NEW_PROCESSES_QUEUE).orElse(null)); + } + + /* The main routine processing incoming tickets. The function has been + * divided into three parts so that it is not too long. */ + @Override + protected void process(MapMessageObjectReader ticket) throws ProcessorException, JMSException { + try { + CreateNewProcessOrder order = new CreateNewProcessOrder(ticket); + rulesetManagement = rulesetService.openRuleset(order.getTemplate().getRuleset()); + TempProcess tempProcess = obtainTempProcess(order); + Process process = tempProcess.getProcess(); + Process parentProcess = formProcessTitle(order, tempProcess, process); + createProcess(tempProcess, process, parentProcess); + + } catch (CommandException | CustomResponseException | DataException | DAOException + | InvalidMetadataValueException | IOException | NoRecordFoundException | NoSuchMetadataFieldException + | ParserConfigurationException | ProcessGenerationException | SAXException | TransformerException + | UnsupportedFormatException | URISyntaxException | XPathExpressionException e) { + throw new ProcessorException(e.getMessage()); + } + } + + /* In the first part of the processing routine, the process is either + * created without import, or it is imported. The existing data is then + * added. */ + private TempProcess obtainTempProcess(CreateNewProcessOrder order) throws DAOException, + InvalidMetadataValueException, + IOException, NoRecordFoundException, NoSuchMetadataFieldException, ParserConfigurationException, + ProcessGenerationException, ProcessorException, SAXException, TransformerException, + UnsupportedFormatException, URISyntaxException, XPathExpressionException { + + if (order.getImports().isEmpty()) { + ProcessGenerator processGenerator = new ProcessGenerator(); + processGenerator.generateProcess(order.getTemplateId(), order.getProjectId()); + TempProcess tempProcess = new TempProcess(processGenerator.getGeneratedProcess(), new Workpiece()); + tempProcess.getWorkpiece().getLogicalStructure().getMetadata().addAll(order.getMetadata()); + tempProcess.verifyDocType(); + return tempProcess; + } else { + TempProcess tempProcess = importProcess(order, 0); + tempProcess.getWorkpiece().getLogicalStructure().getMetadata().addAll(order.getMetadata()); + tempProcess.verifyDocType(); + for (int which = 1; which < order.getImports().size(); which++) { + TempProcess repeatedImport = importProcess(order, which); + Set metadata = repeatedImport.getWorkpiece().getLogicalStructure().getMetadata(); + rulesetManagement.updateMetadata(tempProcess.getWorkpiece().getLogicalStructure().getType(), + tempProcess.getWorkpiece().getLogicalStructure().getMetadata(), + ACQUISITION_STAGE_PROCESS_CREATION, metadata); + } + return tempProcess; + } + } + + /** + * Imports a dataset with an import configuration. + * + * @param order + * order for creating the process + * @param which + * which dataset should be imported + * @return the imported dataset + */ + private TempProcess importProcess(CreateNewProcessOrder order, int which) throws DAOException, + InvalidMetadataValueException, IOException, NoRecordFoundException, NoSuchMetadataFieldException, + ParserConfigurationException, ProcessGenerationException, ProcessorException, SAXException, + TransformerException, UnsupportedFormatException, URISyntaxException, XPathExpressionException { + + List processHierarchy = importService.importProcessHierarchy( + order.getImports().get(which).getValue(), order.getImports().get(which).getKey(), + order.getProjectId(), order.getTemplateId(), IMPORT_WITHOUT_ANY_HIERARCHY, + rulesetManagement.getFunctionalKeys(FunctionalMetadata.HIGHERLEVEL_IDENTIFIER)); + if (processHierarchy.size() == 0) { + throw new ProcessorException("Process was not imported"); + } else if (processHierarchy.size() > 1) { + throw new ProcessorException(processHierarchy.size() + " processes were imported"); + } + return processHierarchy.get(0); + } + + /* In the second and middle part of the processing routine, the process + * title is generated. */ + private Process formProcessTitle(CreateNewProcessOrder order, TempProcess tempProcess, Process process) + throws DAOException, ProcessGenerationException, ProcessorException { + + ProcessFieldedMetadata processDetails = ProcessHelper.initializeProcessDetails(tempProcess.getWorkpiece() + .getLogicalStructure(), rulesetManagement, ACQUISITION_STAGE_PROCESS_CREATION, METADATA_LANGUAGE); + Process parentProcess = order.getParent(); + ProcessHelper.generateAtstslFields(tempProcess, processDetails.getRows(), Collections.emptyList(), tempProcess + .getWorkpiece().getLogicalStructure().getType(), rulesetManagement, ACQUISITION_STAGE_PROCESS_CREATION, + METADATA_LANGUAGE, parentProcess, true); + if (order.getTitle().isPresent()) { + process.setTitle(order.getTitle().get()); + } + if (!ProcessValidator.isProcessTitleCorrect(process.getTitle())) { + throw new ProcessorException(Helper.getTranslation("processTitleAlreadyInUse", process.getTitle())); + } + return parentProcess; + } + + /* In the third and final part of the processing routine, the process is + * created and saved. */ + private void createProcess(TempProcess tempProcess, Process process, Process parentProcess) throws DataException, + CustomResponseException, IOException, CommandException { + + saveProcess(process); + fileService.createProcessLocation(process); + metsService.saveWorkpiece(tempProcess.getWorkpiece(), processService.getMetadataFileUri(process)); + if (Objects.nonNull(parentProcess)) { + MetadataEditor.addLink(parentProcess, LAST_CHILD, process.getId()); + process.setParent(parentProcess); + parentProcess.getChildren().add(process); + saveProcess(process); + saveProcess(parentProcess); + } + } + + /** + * When the process is saved, the tasks are also indexed. + * + * @param process + * process to be saved + */ + private void saveProcess(Process process) throws DataException, CustomResponseException, IOException { + processService.save(process, true); + for (Task task : process.getTasks()) { + taskService.saveToIndex(task, true); + } + } +} 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 cc9839613d9..097ee761d01 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,12 +11,16 @@ package org.kitodo.production.interfaces.activemq; +import edu.umd.cs.findbugs.annotations.CheckForNull; + import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -33,6 +37,7 @@ public class MapMessageObjectReader { private MapMessage ticket; private static final Logger logger = LogManager.getLogger(MapMessageObjectReader.class); + private static final String MANDATORY_ARGUMENT = "Mandatory argument "; private static final String MISSING_ARGUMENT = "Missing mandatory argument: \""; private static final String WRONG_TYPE = "\" was not found to be of type "; @@ -102,7 +107,8 @@ public Set getMandatorySetOfString(String key) throws JMSException { * in case that getObject returns null or the returned string is * of length “0”. * @throws JMSException - * can be thrown by MapMessage.getString(String) + * thrown by MapMessage.getString(String) if the string is a + * byte[] */ public String getMandatoryString(String key) throws JMSException { String mandatoryString = ticket.getString(key); @@ -112,6 +118,32 @@ public String getMandatoryString(String key) throws JMSException { return mandatoryString; } + /** + * Fetches a String from a map. This is a strict implementation that + * requires the string not to be null and not to be empty. + * + * @param key + * the name of the string to return + * @return the string requested + * @throws IllegalArgumentException + * in case that get returns null, an inappropriate object, or + * the returned string is of length “0”. + */ + public static String getMandatoryString(Map data, String key) { + Object value = data.get(key); + if (Objects.isNull(value)) { + throw new IllegalArgumentException(MISSING_ARGUMENT + key + "\""); + } + if (!(value instanceof String)) { + throw new IllegalArgumentException(MANDATORY_ARGUMENT + key + " is not a string"); + } + String mandatoryString = (String) value; + if (mandatoryString.isEmpty()) { + throw new IllegalArgumentException(MISSING_ARGUMENT + key + "\""); + } + return mandatoryString; + } + /** * Fetches a {@code Collection} from a MapMessage. This is a loose * implementation for an optional object with optional content. The @@ -169,6 +201,7 @@ public Collection getCollectionOfString(String key) throws JMSException * can be thrown by MapMessage.getString(String) */ + @CheckForNull public String getString(String key) throws JMSException { return ticket.getString(key); } @@ -178,12 +211,12 @@ public String getString(String key) throws JMSException { * implementation that requires the Integer not to be null. * * @param key - * the name of the string to return + * the name of the integer to return * @return the string requested * @throws IllegalArgumentException * in case that getObject returns null * @throws JMSException - * can be thrown by MapMessage.getString(String) + * in case that getObject returns an unmatching object type */ public Integer getMandatoryInteger(String key) throws JMSException { if (!ticket.itemExists(key)) { @@ -192,6 +225,33 @@ public Integer getMandatoryInteger(String key) throws JMSException { return ticket.getInt(key); } + /** + * Fetches an Integer object from a map. This is a strict implementation + * that requires the Integer not to be null. + * + * @param data + * the data map + * @param key + * the name of the integer to return + * @return the string requested + * @throws IllegalArgumentException + * in case that there is no such key, or get returns an + * unmatching object type + */ + public static Integer getMandatoryInteger(Map data, String key) { + if (!data.containsKey(key)) { + throw new IllegalArgumentException(MISSING_ARGUMENT + key + "\""); + } + Object value = data.get(key); + if (value instanceof Integer) { + return (Integer) value; + } + if (value instanceof String) { + return Integer.valueOf((String) value); + } + throw new IllegalArgumentException(MANDATORY_ARGUMENT + key + " is not an integer"); + } + /** * Fetches a {@code Map} from a MapMessage. This is a partly * strict implementation that allows no null element neither as key, nor as @@ -240,6 +300,91 @@ public Map getMapOfStringToString(String key) { return mapOfStringToString; } + /** + * Fetches a {@code List} from a MapMessage. May return {@code null} if + * there is no such object. + * + * @param key + * key for which the list to return + * @return the map, or {@code null} + * @throws IllegalArgumentException + * if the object isn’t a {@code List} + * @throws JMSException + * if an I/O exception occurs during read, i.e. if the map + * message sent is larger than the allowed size + */ + @CheckForNull + public List getList(String key) throws JMSException { + Object valueObject = ticket.getObject(key); + if (Objects.isNull(valueObject)) { + return null; + } + if (!(valueObject instanceof List)) { + throw new IllegalArgumentException(key + " is not a List"); + } + return (List) valueObject; + } + + /** + * Fetches an {@code Integer} from a MapMessage. May return {@code null} if + * there is no such object. + * + * @param key + * key for which the integer to return + * @return the integer, or {@code null} if there isn’t one + * @throws IllegalArgumentException + * if the object is not an Integer + * @throws JMSException + * if an I/O exception occurs during read, i.e. if the map + * message sent is larger than the allowed size + */ + @CheckForNull + public Integer getInteger(String key) throws JMSException { + Object valueObject = ticket.getObject(key); + if (Objects.isNull(valueObject)) { + return null; + } + if (!(valueObject instanceof Integer)) { + throw new IllegalArgumentException(key + " is not an Integer"); + } + return (Integer) valueObject; + } + + /** + * Fetches a {@code Map} from a MapMessage. May return + * {@code null} if there is no such object. + * + * @param key + * key for which the map to return + * @return the map, or {@code null} + * @throws IllegalArgumentException + * if the object isn’t a {@code Map} or one of its keys isn’t a + * {@code String} + * @throws JMSException + * if an I/O exception occurs during read, i.e. if the map + * message sent is larger than the allowed size + */ + @CheckForNull + public Map getMapOfString(String key) throws JMSException { + HashMap mapOfString = new HashMap<>(); + Object mapObject = ticket.getObject(key); + if (Objects.isNull(mapObject)) { + return null; + } + if (!(mapObject instanceof Map)) { + throw new IllegalArgumentException("Incompatible types: \"" + key + WRONG_TYPE + "Map."); + } + for (Entry entry : ((Map) mapObject).entrySet()) { + Object entryKey = entry.getKey(); + if (!(entryKey instanceof String)) { + throw new IllegalArgumentException("Incompatible types: A key element of \"" + key + WRONG_TYPE + + "String."); + } + mapOfString.put(((String) entryKey), entry.getValue()); + } + return mapOfString; + } + /** * Tests whether a field can be obtained from a MapMessage. * diff --git a/Kitodo/src/main/resources/kitodo_config.properties b/Kitodo/src/main/resources/kitodo_config.properties index 22b3a1f0b0a..6c1e4860252 100644 --- a/Kitodo/src/main/resources/kitodo_config.properties +++ b/Kitodo/src/main/resources/kitodo_config.properties @@ -645,6 +645,9 @@ activeMQ.user=testAdmin # The Kitodo Script commands authorized to be executed must be named here: activeMQ.kitodoScript.allow=createFolders&export&searchForMedia +# You can provide a queue from which messages are read to create new processes +#activeMQ.createNewProcesses.queue=KitodoProduction.CreateNewProcesses.Queue + # ----------------------------------- # Elasticsearch properties