From 23c011d57e2ab70bcc23d40162f667072e449855 Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Fri, 27 Sep 2024 13:22:33 +0200 Subject: [PATCH 01/43] Refactor detail view Javascript. Update OpenLayers to v6.14.1. Fix issue #5796. --- .../forms/dataeditor/GalleryPanel.java | 2 +- .../webapp/WEB-INF/resources/css/kitodo.css | 4 + .../WEB-INF/resources/js/metadata_editor.js | 4 +- .../webapp/WEB-INF/resources/js/ol_custom.js | 583 ++++++++++++------ .../webapp/WEB-INF/resources/js/resize.js | 7 +- .../metadataEditor/dialogs/pagination.xhtml | 2 +- .../includes/metadataEditor/gallery.xhtml | 6 +- .../metadataEditor/logicalStructure.xhtml | 4 +- .../metadataEditor/physicalStructure.xhtml | 4 +- pom.xml | 2 +- 10 files changed, 409 insertions(+), 209 deletions(-) diff --git a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java index 3d09eb0bfb3..ada4f3655ee 100644 --- a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java +++ b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java @@ -859,7 +859,7 @@ private void selectMedia(String physicalDivisionOrder, String stripeIndex, Strin String scrollScripts = "scrollToSelectedTreeNode();scrollToSelectedPaginationRow();"; if (GalleryViewMode.PREVIEW.equals(galleryViewMode)) { PrimeFaces.current().executeScript( - "checkScrollPosition();initializeImage();metadataEditor.gallery.mediaView.update();" + scrollScripts); + "checkScrollPosition();metadataEditor.detailMap.update();metadataEditor.gallery.mediaView.update();" + scrollScripts); } else { PrimeFaces.current().executeScript(scrollScripts); } diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css index a5e3a0e66e3..42a7d695a0c 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css +++ b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css @@ -3242,6 +3242,9 @@ Column content left: 16px; position: absolute; top: 16px; + display: flex; + flex-direction: column; + align-items: start; } #map .ol-overlaycontainer-stopevent > .ol-zoom.ol-unselectable.ol-control { @@ -3263,6 +3266,7 @@ Column content } #map .ol-control { + display: inline-block; background: transparent; padding: 0; position: static; diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js index ccd10ae33c0..4cd4f57e078 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js @@ -14,7 +14,7 @@ /*eslint new-cap: ["error", { "capIsNewExceptionPattern": "^PF" }]*/ /*eslint complexity: ["error", 10]*/ -var metadataEditor = {}; +var metadataEditor = metadataEditor || {}; metadataEditor.metadataTree = { @@ -1147,7 +1147,7 @@ metadataEditor.shortcuts = { case "PREVIEW": initialize(); scrollToSelectedThumbnail(); - changeToMapView(); + metadataEditor.detailMap.update(); break; } } diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js index 049d0f7da75..dee541b733c 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js @@ -11,244 +11,441 @@ /* globals ol */ // jshint unused:false -// Kitodo namespace -var kitodo = {}; -kitodo.map = null; - /** - * @param {Object=} options Custom control options for Kitodo in OpenLayers - * @extends {ol.control.Rotate} - * @constructor + * Abstract class describing a custom control button for the OpenLayers map. */ -kitodo.RotateLeftControl = function(options = {}) { - var buttonLeft = document.createElement('button'); - buttonLeft.innerHTML = ""; - buttonLeft.setAttribute("type", "button"); - buttonLeft.setAttribute("title", "Rotate left"); +class CustomControl extends ol.control.Control { + + /** + * Initializes a custom control button with various options. + * + * @param {object} options the custom control options (className, icon, title, other OpenLayer options) + */ + constructor(options) { + const className = options.className; + const icon = options.icon; + const title = options.title; + + const button = document.createElement('button'); + button.innerHTML = ""; + button.setAttribute("type", "button"); + button.setAttribute("title", title); + + const element = document.createElement('div'); + element.className = className + ' ol-unselectable ol-control ol-rotate'; + element.appendChild(button); + + super({ + element: element, + target: options.target + }); + + button.addEventListener('click', this.handleClick.bind(this), false); + } + + /** + * Abstract method that handles a click event on the button. + * + * @param {MouseEvent} event the click event + */ + handleClick(event) { + // not implemented + } +}; - var this_ = this; +/** + * Custom control that rotates the image 90 degrees to the left. + */ +class RotateLeftControl extends CustomControl { + + constructor(options) { + super(Object.assign(options || {}, { + className: "rotate-left", + icon: "fa-undo", + title: "Rotate left", + })); + } - var handleRotateLeft = function() { - var view = this_.getMap().getView(); + handleClick() { + const view = this.getMap().getView(); view.animate({ rotation: view.getRotation() - (90 * (Math.PI / 180)), duration: 100 }); - }; - - buttonLeft.addEventListener('click', handleRotateLeft, false); - - var elementLeft = document.createElement('div'); - elementLeft.className = 'rotate-left ol-unselectable ol-control ol-rotate'; - elementLeft.appendChild(buttonLeft); - - ol.control.Control.call(this, { - element: elementLeft, - target: options.target - }); + } }; /** - * @param {Object=} options Custom control options for Kitodo in OpenLayers - * @extends {ol.control.Rotate} - * @constructor + * Custom control that rotates the image 90 degrees to the right. */ -kitodo.RotateRightControl = function(options = {}) { - var buttonRight = document.createElement('button'); - buttonRight.innerHTML = ""; - buttonRight.setAttribute("type", "button"); - buttonRight.setAttribute("title", "Rotate right"); - - var this_ = this; +class RotateRightControl extends CustomControl { + + constructor(options) { + super(Object.assign(options || {}, { + className: "rotate-right", + icon: "fa-repeat", + title: "Rotate right", + })); + } - var handleRotateRight = function() { - var view = this_.getMap().getView(); + handleClick() { + const view = this.getMap().getView(); view.animate({ - rotation: view.getRotation() + (90 * (Math.PI / 180)), + rotation: view.getRotation() + (90 * (Math.PI / 180)), duration: 100 }); - }; - - buttonRight.addEventListener('click', handleRotateRight, false); - - var elementRight = document.createElement('div'); - elementRight.className = 'rotate-right ol-unselectable ol-control ol-rotate'; - elementRight.appendChild(buttonRight); - - ol.control.Control.call(this, { - element: elementRight, - target: options.target, - duration: 250 - }); + } }; -function resetNorth() { - if (kitodo.map) { - let view = kitodo.map.getView(); - view.animate({ +/** + * Custom control that rotates the image back to default. + */ +class RotateNorthControl extends CustomControl { + + constructor(options) { + super(Object.assign(options || {}, { + className: "rotate-north", + icon: "fa-compass", + title: "Reset orientation", + })); + } + + handleClick() { + this.getMap().getView().animate({ rotation: 0, - duration: 0 + duration: 100 }); } -} +}; /** - * @param {Object=} options Custom control options for Kitodo in OpenLayers - * @extends {ol.control.Rotate} - * @constructor + * Custom control that scales the image back to default. */ -kitodo.ResetNorthControl = function(options = {}) { - let buttonResetNorth = document.createElement("button"); - buttonResetNorth.innerHTML = ""; - buttonResetNorth.setAttribute("type", "button"); - buttonResetNorth.setAttribute("title", "Reset orientation"); - - buttonResetNorth.addEventListener("click", resetNorth, false); - - let elementResetNorth = document.createElement("div"); - elementResetNorth.className = "ol-rotate ol-unselectable ol-control"; /*ol-rotate-reset*/ - elementResetNorth.appendChild(buttonResetNorth); - - ol.control.Control.call(this, { - element: elementResetNorth, - target: options.target, - duration: 250 - }); +class ResetZoomControl extends CustomControl { + + /** The image dimensions as OpenLayers extent */ + #extent; + + /** + * Initialize a custom control button that scales the image back to its default position. + * + * @param {object} options containing the extent (Array) describing the image dimensions + */ + constructor(options) { + super(Object.assign(options, { + className: "reset-zoom", + icon: "fa-expand", + title: "Reset zoom", + })); + this.#extent = options.extent; + } + + handleClick() { + this.getMap().getView().fit(this.#extent, {}); + } }; -ol.inherits(kitodo.RotateLeftControl, ol.control.Rotate); -ol.inherits(kitodo.RotateRightControl, ol.control.Rotate); -ol.inherits(kitodo.ResetNorthControl, ol.control.Rotate); +/** + * Class managing OpenLayers detail map showing images. + */ +class KitodoDetailMap { + + /** + * Remember position, rotation and zoom level such that a new OpenLayers map can be + * initialized with the same position, rotation and zoom level when new images are selected. + */ + #last_view = { + center: null, + zoom: null, + rotation: null, + }; + + /** + * Image properties of the image that is currently shown in OpenLayers. Object with properties + * dimensions (width, height) and path (url). + */ + #image = { + dimensions: null, + path: null, + }; -function random(length) { - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + /** + * The OpenLayers maps instance + */ + #map = null; - for (var i = 0; i < length; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); + /** + * Initialize a new Kitodo detail map + */ + constructor() { + this.registerResizeEvent(); } - return text; -} + /** + * Debounces various event handlers to improve performance, e.g. when resizing. + * + * @param {function} func the function to be debounced + * @param {number} timeout the timeout in milliseconds + * + * @returns {function} the debounced function + */ + static makeDebounced(func, timeout = 100) { + let timer = null; + return function () { + clearTimeout(timer); + timer = setTimeout(func, timeout); + }; + } -function createProjection(extent) { - return new ol.proj.Projection({ - code: 'kitodo-image', - units: 'pixels', - extent: extent - }); -} + /** + * Generate a random string of [a-zA-Z0-9]. + * + * @param {number} length the length of the string to be generated + * @returns the string of random characters + */ + static randomUUID(length) { + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + let text = ""; + for (let i = 0; i < length; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + + return text; + } -function createSource(extent, imagePath, projection) { - return new ol.source.ImageStatic({ - url: imagePath, - projection: projection, - imageExtent: extent - }); -} + /** + * Create an OpenLayers projection given the image extent. + * + * @param {Array} extent the extent describing the image dimensions + * @returns {ol.proj.Projection} the OpenLayers projection + */ + createProjection(extent) { + return new ol.proj.Projection({ + code: 'kitodo-image', + units: 'pixels', + extent: extent + }); + } -function hideCanvas() { - let map = document.querySelector("#map canvas"); - let loadingIcon = document.querySelector("#map > .fa-spinner"); - if (map) { - map.style.opacity = 0; - loadingIcon.style.opacity = 1; + /** + * Create an OpenLayers source object for the image. + * + * @param {Array} extent the extent describing the image dimensions + * @param {string} path the path (url) of the image + * @param {ol.proj.Projection} projection the OpenLayers projection used to map the image to the canvas + * @returns {ol.source.ImageStatic} the OpenLayers image source object + */ + createSource(extent, path, projection) { + return new ol.source.ImageStatic({ + url: path, + projection: projection, + imageExtent: extent, + interpolate: true + }); } -} -function showCanvas() { - let map = document.querySelector("#map canvas"); - let loadingIcon = document.querySelector("#map > .fa-spinner"); - if (map) { - map.style.opacity = 1; - loadingIcon.style.opacity = 0; + /** + * Hide the map canvas (while the image is loading) + */ + hideCanvas() { + let canvas = document.querySelector("#map canvas"); + let loadingIcon = document.querySelector("#map > .fa-spinner"); + if (canvas) { + canvas.style.opacity = 0; + loadingIcon.style.opacity = 1; + } + } + + /** + * Show the map canvas (as soon as the image has finished loading) + */ + showCanvas() { + let canvas = document.querySelector("#map canvas"); + let loadingIcon = document.querySelector("#map > .fa-spinner"); + if (canvas) { + canvas.style.opacity = 1; + loadingIcon.style.opacity = 0; + } } -} -function initializeMap(imageDimensions, imagePath) { - // Map image coordinates to map coordinates to be able to use image extent in pixels. - let extent = [0, 0, imageDimensions[0], imageDimensions[1]]; - let projection = createProjection(extent); - - kitodo.map = new ol.Map({ - controls: ol.control.defaults({ - attributionOptions: { - collapsible: false - }, - zoomOptions: { - delta: 3 - }, - rotate: false - }).extend([ - new kitodo.RotateRightControl(), - new kitodo.RotateLeftControl(), - new kitodo.ResetNorthControl() - ]), - layers: [ - new ol.layer.Image({ - source: createSource(extent, imagePath, projection) - }) - ], - target: 'map', - view: new ol.View({ - projection: projection, - center: ol.extent.getCenter(extent), - zoomFactor: 1.1 - }) - }); - kitodo.map.getView().fit(extent, {}); - kitodo.map.on("rendercomplete", function () { - showCanvas(); - }); -} + /** + * Handler that is called as soon as the image was completely loaded + * @param {*} image the jQuery image dom element + */ + onImageLoad(image) { + this.#image = { + dimensions: [image.width(), image.height()], + path: image[0].src, + }; + this.initializeOpenLayersMap(); + } -function updateMap(imageDimensions, imagePath) { - // Map image coordinates to map coordinates to be able to use image extent in pixels. - let extent = [0, 0, imageDimensions[0], imageDimensions[1]]; - let projection = createProjection(extent); + /** + * Register the load event for the current image. + */ + registerImageLoadEvent() { + this.hideCanvas(); + let image = $("#imagePreviewForm\\:mediaPreviewGraphicImage"); + if (image.length > 0) { + image.on("load", this.onImageLoad.bind(this, image)); + image[0].src = image[0].src.replace(/&uuid=[a-z0-9]+/i, "") + "&uuid=" + KitodoDetailMap.randomUUID(8); + } + } - kitodo.map.getLayers().getArray()[0].setSource(createSource(extent, imagePath, projection)); - kitodo.map.getView().setCenter(ol.extent.getCenter(extent)); - kitodo.map.getView().getProjection().setExtent(extent); - kitodo.map.getView().fit(extent, {}); -} + /** + * Return extent array containg image dimensions. + * + * @param {Array} dimensions dimensions in pixel as [width, height] + * @returns {Array} the extent array + */ + createImageExtent(dimensions) { + return [0, 0, dimensions[0], dimensions[1]]; + } + + /** + * Creates the OpenLayers map object as soon as the image as been loaded. + */ + initializeOpenLayersMap() { + // Map image coordinates to map coordinates to be able to use image extent in pixels. + const extent = this.createImageExtent(this.#image.dimensions); + const projection = this.createProjection(extent); + + if (this.#map) { + // make last OpenLayers map forget canvas target + // (triggers OpenLayers cleanup code and allows garbage collection) + this.#map.setTarget(null); + } -function addListener(element) { - element.on("load", function () { - if (kitodo.map && $("#map .ol-viewport").length) { - updateMap([element.width(), element.height()], element[0].src); - } else { - initializeMap([element.width(), element.height()], element[0].src); + // initialize new OpenLayers map + this.#map = new ol.Map({ + controls: ol.control.defaults({ + attributionOptions: { + collapsible: false + }, + zoomOptions: { + delta: 3 // zoom delta when clicking zoom buttons + }, + rotate: false + }).extend([ + new RotateLeftControl(), + new RotateRightControl(), + new RotateNorthControl(), + new ResetZoomControl({extent: extent}) + ]), + interactions: ol.interaction.defaults({ + zoomDelta: 5, // zoom delta when using mouse wheel + zoomDuration: 100, + }), + layers: [ + new ol.layer.Image({ + source: this.createSource(extent, this.#image.path, projection) + }) + ], + target: 'map', + view: new ol.View({ + projection: projection, + center: this.unnormalizeCenter(this.#last_view.center, extent), + zoom: this.#last_view.zoom, + rotation: this.#last_view.rotation, + zoomFactor: 1.1, + extent: extent, + constrainOnlyCenter: true, + smoothExtentConstraint: true, + showFullExtent: true + }) + }); + if (this.#last_view.center == null) { + // fit image to current viewport unless previous zoom and center position is known + this.#map.getView().fit(extent, {}); } - }); -} + // register various events to make sure that previous view is remembered + this.#map.on("rendercomplete", KitodoDetailMap.makeDebounced(this.onRenderComplete.bind(this))); + this.#map.on("change", KitodoDetailMap.makeDebounced(this.saveCurrentView.bind(this))); + this.#map.on("postrender", KitodoDetailMap.makeDebounced(this.saveCurrentView.bind(this))); + } -function initializeImage() { - resetNorth(); - hideCanvas(); - let image = $("#imagePreviewForm\\:mediaPreviewGraphicImage"); - if (image.length > 0) { - addListener(image); - image[0].src = image[0].src.replace(/&uuid=[a-z0-9]+/i, "") + "&uuid=" + random(8); + /** + * Return unnormalized center coordinates in case previous center is known (not null). Otherwise + * center is calculated from the image extent containing image dimensions. + * + * @param {Array} center the normalized center coordinates [0..1, 0..1] + * @returns {Array} unnormalized center + */ + unnormalizeCenter(center) { + if (center !== null) { + return [ + center[0] * this.#image.dimensions[0], + center[1] * this.#image.dimensions[1], + ] + } + return ol.extent.getCenter(this.createImageExtent(this.#image.dimensions)); } -} -function changeToMapView() { - initializeImage(); - showCanvas(); - if (kitodo.map) { - kitodo.map.handleTargetChanged_(); + /** + * Normalizes the center coordinates from [0..width, 0..height] to [0..1, 0..1] such + * that images with different dimensions are visualized at the same relative position + * in the viewport. + * + * @param {Array} center the current center coordinates as reported by OpenLayers + * @returns {Array} the normalized center coordinates + */ + normalizeCenter(center) { + return [ + center[0] / this.#image.dimensions[0], + center[1] / this.#image.dimensions[1], + ]; + } + + /** + * Remembers current view properties (center, zoom rotation) such that the OpenLayers + * map can be initialized with the same parameters when selecting another image. + */ + saveCurrentView() { + this.#last_view = { + center: this.normalizeCenter(this.#map.getView().getCenter()), + zoom: this.#map.getView().getZoom(), + rotation: this.#map.getView().getRotation(), + }; + } + + /** + * Is called by OpenLayers whenever a canvas rendering has finished. Unless debounced, this + * event is triggered potentially at 60fps. + */ + onRenderComplete() { + this.showCanvas(); + this.saveCurrentView(); + } + + /** + * Registers the resize event for the meta data editor column, such that the image can be + * repositioned appropriately. + */ + registerResizeEvent() { + // reload map if container was resized + $('#thirdColumnWrapper').on('resize', KitodoDetailMap.makeDebounced(this.onResize.bind(this))); } -} -// reload map if container was resized -$('#thirdColumnWrapper').on('resize', function () { - if (kitodo.map) { - // FIXME: This causes lags. It should only be executed *once* after resize. - kitodo.map.updateSize(); + /** + * Is called when a resize event has happened. Unless debounced, this event is triggered potentially + * at 60fps. + */ + onResize() { + if (this.#map) { + this.#map.updateSize(); + } + } + + /** + * Reloads the image. Is called when the detail view is activated, or a new image was selected. + */ + update() { + this.registerImageLoadEvent(); } -}); +} + +// register detail map class with the metadataEditor namespace +var metadataEditor = metadataEditor || {}; +metadataEditor.detailMap = new KitodoDetailMap(); -$(document).ready(function () { - initializeImage(); -}); diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js index 208bb40dcc1..747d6129d20 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js @@ -397,6 +397,7 @@ function toggleThirdColumn() { secondColumn.animate({width: wrapper.width() - firstColumn.width() - COLLAPSED_COL_WIDTH - 2 * SEPARATOR_WIDTH}); } } else { + metadataEditor.detailMap.update(); var neededWidth = thirdColumnWidth - COLLAPSED_COL_WIDTH - (secondColumn.width() - secondColumn.data('min-width')); if (secondColumn.hasClass(COLLAPSED)) { firstColumn.animate({width: wrapper.width() - COLLAPSED_COL_WIDTH - thirdColumnWidth - 2 * SEPARATOR_WIDTH}); @@ -522,16 +523,14 @@ function updateMetadataEditorView(showMetadataColumn) { } expandThirdColumn(); scrollToSelectedThumbnail(); - initializeImage(); + metadataEditor.detailMap.update(); metadataEditor.gallery.mediaView.update(); scrollToSelectedTreeNode(); scrollToSelectedPaginationRow(); } function resizeMap() { - if (kitodo.map) { - kitodo.map.updateSize(); - } + metadataEditor.detailMap.onResize(); } function saveLayout() { diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml index 3b45179b4ac..aca83ac8064 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml @@ -47,7 +47,7 @@ showCheckbox="true"> diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml index 963816c6116..51ae120ad9c 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml @@ -65,7 +65,7 @@ @@ -290,8 +290,8 @@ galleryWrapperPanel"/> - - + + diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml index 4406dbc9572..4b2e5408122 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml @@ -72,7 +72,7 @@ listener="#{DataEditorForm.structurePanel.treeLogicalSelect}" oncomplete="scrollToSelectedThumbnail(); scrollToSelectedPaginationRow(); - changeToMapView(); + metadataEditor.detailMap.update(); metadataEditor.gallery.mediaView.update(); expandMetadata('logical-metadata-tab');" update="galleryHeadingWrapper @@ -87,7 +87,7 @@ onstart="$('#contextMenuLogicalTree .ui-menuitem').addClass('ui-state-disabled')" oncomplete="scrollToSelectedThumbnail(); scrollToSelectedPaginationRow(); - changeToMapView(); + metadataEditor.detailMap.update(); metadataEditor.gallery.mediaView.update(); PF('contextMenuLogicalTree').show(currentEvent)" update="@(.stripe) diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/physicalStructure.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/physicalStructure.xhtml index fa03940d930..6998518509d 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/physicalStructure.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/physicalStructure.xhtml @@ -34,7 +34,7 @@ listener="#{DataEditorForm.structurePanel.treePhysicalSelect}" oncomplete="scrollToSelectedThumbnail(); scrollToSelectedPaginationRow(); - initializeImage(); + metadataEditor.detailMap.update(); metadataEditor.gallery.mediaView.update(); expandMetadata('physical-metadata-tab');" update="galleryHeadingWrapper @@ -44,7 +44,7 @@ div:nth-child(2) .thumbnail-container"; + private static final Double EPSILON = 0.001; + + private static int processId = -1; + + /** + * Prepare tests by inserting dummy processes into database and index for sub-folders of test metadata resources. + * @throws DAOException when saving of dummy or test processes fails. + * @throws DataException when retrieving test project for test processes fails. + * @throws IOException when copying test metadata or image files fails. + */ + @BeforeAll + public static void prepare() throws DAOException, DataException, IOException { + MockDatabase.insertFoldersForSecondProject(); + processId = MockDatabase.insertTestProcessIntoSecondProject(PROCESS_TITLE); + ProcessTestUtils.copyTestFiles(processId, TEST_RENAME_MEDIA_FILE); + } + + /** + * Tests whether the image preview is shown when a user clicks on the image preview button. + * @throws Exception when something fails + */ + @Test + public void imageVisibleTest() throws Exception { + login("kowal"); + Pages.getProcessesPage().goTo().editMetadata(PROCESS_TITLE); + + // check detail view is not yet visible + assertEquals(0, findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).size()); + + // open detail view + Pages.getMetadataEditorPage().openDetailView(); + + // check it is visible now + pollAssertTrue(() -> findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).get(0).isDisplayed()); + } + + /** + * Tests whether the zoom buttons of the image preview (zoom in, out, reset) work as intended. + * @throws Exception when something fails + */ + @Test + public void zoomLevelTest() throws Exception { + login("kowal"); + + // open detail view and wait for openlayers canvas + Pages.getProcessesPage().goTo().editMetadata(PROCESS_TITLE); + Pages.getMetadataEditorPage().openDetailView(); + pollAssertTrue(() -> findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).get(0).isDisplayed()); + + // remember initial zoom + Double initialZoom = getOpenLayersZoom(); + assertTrue(initialZoom > 0); + + // zoom in, and check zoom increases + findElementsByCSS(OPEN_LAYERS_ZOOM_IN_SELECTOR).get(0).click(); + pollAssertTrue(() -> !isOpenLayersAnimating()); + assertTrue(() -> getOpenLayersZoom() > initialZoom); + + // zoom out, and check zoom returns to initial zoom level + findElementsByCSS(OPEN_LAYERS_ZOOM_OUT_SELECTOR).get(0).click(); + pollAssertTrue(() -> !isOpenLayersAnimating()); + assertTrue(() -> Math.abs(getOpenLayersZoom() - initialZoom) < EPSILON); + + // zoom in, and reset zoom, check zoom returns to initial zoom level + findElementsByCSS(OPEN_LAYERS_ZOOM_IN_SELECTOR).get(0).click(); + pollAssertTrue(() -> !isOpenLayersAnimating()); + findElementsByCSS(OPEN_LAYERS_ZOOM_RESET_SELECTOR).get(0).click(); + pollAssertTrue(() -> !isOpenLayersAnimating()); + assertTrue(() -> Math.abs(getOpenLayersZoom() - initialZoom) < EPSILON); + } + + /** + * Tests whether the rotation buttons of the image preview (rotation left, right, north) work as intended. + * @throws Exception when something fails + */ + @Test + public void rotationTest() throws Exception { + login("kowal"); + + // open detail view and wait for openlayers canvas + Pages.getProcessesPage().goTo().editMetadata(PROCESS_TITLE); + Pages.getMetadataEditorPage().openDetailView(); + pollAssertTrue(() -> findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).get(0).isDisplayed()); + + // check initial rotation is zero + assertTrue(Math.abs(getOpenLayersRotation()) < EPSILON); + + // rotate left and check rotation is decreasing + findElementsByCSS(OPEN_LAYERS_ROTATE_LEFT_SELECTOR).get(0).click(); + pollAssertTrue(() -> !isOpenLayersAnimating()); + assertTrue(() -> getOpenLayersRotation() < 0.0); + + // rotate back, and check rotation returns to zero + findElementsByCSS(OPEN_LAYERS_ROTATE_RIGHT_SELECTOR).get(0).click(); + pollAssertTrue(() -> !isOpenLayersAnimating()); + assertTrue(() -> Math.abs(getOpenLayersRotation()) < EPSILON); + + // rotate left and reset to north, check rotation returns to zero + findElementsByCSS(OPEN_LAYERS_ROTATE_LEFT_SELECTOR).get(0).click(); + pollAssertTrue(() -> !isOpenLayersAnimating()); + findElementsByCSS(OPEN_LAYERS_ROTATE_NORTH_SELECTOR).get(0).click(); + pollAssertTrue(() -> !isOpenLayersAnimating()); + assertTrue(() -> Math.abs(getOpenLayersRotation()) < EPSILON); + } + + /** + * Tests that both zoom level and rotation persists when a user clicks on another image + * (which causes OpenLayers to be loaded again). + * @throws Exception when something fails + */ + @Test + public void viewPersistsImageChange() throws Exception { + login("kowal"); + + // open detail view and wait for openlayers canvas + Pages.getProcessesPage().goTo().editMetadata(PROCESS_TITLE); + Pages.getMetadataEditorPage().openDetailView(); + pollAssertTrue(() -> findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).get(0).isDisplayed()); + + // remember initial zoom, rotation + Double initialZoom = getOpenLayersZoom(); + Double initialRotation = getOpenLayersRotation(); + + // rotate left and zoom in + findElementsByCSS(OPEN_LAYERS_ROTATE_LEFT_SELECTOR).get(0).click(); + findElementsByCSS(OPEN_LAYERS_ZOOM_IN_SELECTOR).get(0).click(); + pollAssertTrue(() -> !isOpenLayersAnimating()); + + // remember changed zoom, rotation + Double changedZoom = getOpenLayersZoom(); + Double changedRotation = getOpenLayersRotation(); + + // verify zoom and rotation was applied + assertTrue(Math.abs(initialZoom - changedZoom) > 0); + assertTrue(Math.abs(initialRotation - changedRotation) > 0); + + // change to second image + findElementsByCSS(SECOND_THUMBNAIL_SELECTOR).get(0).click(); + + // wait until second image has been loaded + pollAssertTrue( + () -> "Bild 1, Seite -".equals( + findElementsByCSS(GALLERY_HEADING_WRAPPER_SELECTOR).get(0).getText().strip() + ) + ); + + // wait until OpenLayers canvas is available + pollAssertTrue(() -> findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).get(0).isDisplayed()); + + // check that rotation and zoom was correctly applied to next image (and is not reset) + assertTrue(Math.abs(getOpenLayersZoom() - changedZoom) < EPSILON); + assertTrue(Math.abs(getOpenLayersRotation() - changedRotation) < EPSILON); + } + + /** + * Close metadata editor and logout after every test. + * @throws Exception when page navigation fails + */ + @AfterEach + public void closeEditorAndLogout() throws Exception { + Pages.getMetadataEditorPage().closeEditor(); + Pages.getTopNavigation().logout(); + } + + /** + * Cleanup test environment by removing temporal dummy processes from database and index. + * @throws DAOException when dummy process cannot be removed from database + * @throws CustomResponseException when dummy process cannot be removed from index + * @throws DataException when dummy process cannot be removed from index + * @throws IOException when deleting test files fails. + */ + @AfterAll + public static void cleanup() throws DAOException, CustomResponseException, DataException, IOException { + ProcessService.deleteProcess(processId); + } + + private void login(String username) throws InstantiationException, IllegalAccessException, InterruptedException { + User metadataUser = ServiceManager.getUserService().getByLogin(username); + Pages.getLoginPage().goTo().performLogin(metadataUser); + } + + private List findElementsByCSS(String css) { + return Browser.getDriver().findElements(By.cssSelector(css)); + } + + private void pollAssertTrue(Callable conditionEvaluator) throws Exception { + await().ignoreExceptions().pollInterval(100, TimeUnit.MILLISECONDS).atMost(3, TimeUnit.SECONDS) + .until(conditionEvaluator); + } + + private Boolean isOpenLayersAnimating() { + return (Boolean)Browser.getDriver().executeScript("return metadataEditor.detailMap.getAnimating()"); + } + + private Double getOpenLayersZoom() { + Object result = Browser.getDriver().executeScript("return metadataEditor.detailMap.getZoom()"); + return ((Number)result).doubleValue(); + } + + private Double getOpenLayersRotation() { + Object result = Browser.getDriver().executeScript("return metadataEditor.detailMap.getRotation()"); + return ((Number)result).doubleValue(); + } + +} diff --git a/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java b/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java index e6284c17a9d..131830957e6 100644 --- a/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java +++ b/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java @@ -78,6 +78,9 @@ public class MetadataEditorPage extends Page { @FindBy(id = "renamingMediaResultForm:okSuccess") private WebElement okButtonRenameMediaFiles; + @FindBy(id = "imagePreviewForm:previewButton") + private WebElement imagePreviewButton; + public MetadataEditorPage() { super("metadataEditor.jsf"); } @@ -222,4 +225,12 @@ public long getNumberOfDisplayedStructureElements() { return Browser.getDriver().findElements(By.cssSelector(".ui-treenode")).stream().filter(WebElement::isDisplayed) .count(); } + + /** + * Open detail view by clicking on image preview button. + */ + public void openDetailView() { + imagePreviewButton.click(); + } + } From 54362a14a024ce4945e82fa2922ab0bd7833dad5 Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Tue, 1 Oct 2024 12:37:55 +0200 Subject: [PATCH 05/43] Add missing semicolons. --- Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js index 369dca897c1..69e9a63eb73 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js +++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js @@ -434,7 +434,7 @@ class KitodoDetailMap { */ getZoom() { if (this.#map) { - return this.#map.getView().getZoom() + return this.#map.getView().getZoom(); } return -1; } @@ -456,7 +456,7 @@ class KitodoDetailMap { */ getRotation() { if (this.#map) { - return this.#map.getView().getRotation() + return this.#map.getView().getRotation(); } return 0; } From c7931181b3b06a842801e3f282fe112844543c0c Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Tue, 8 Oct 2024 11:56:35 +0000 Subject: [PATCH 06/43] Allow to link page to next logical structure independent of hierarchy level. --- .../forms/dataeditor/StructurePanel.java | 125 +++++++++++++++--- 1 file changed, 103 insertions(+), 22 deletions(-) diff --git a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java index 74887942d25..fa44e75eb55 100644 --- a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java +++ b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java @@ -1659,7 +1659,7 @@ private boolean physicalNodeStateUnknown(HashMap expa } private LogicalDivision getTreeNodeStructuralElement(TreeNode treeNode) { - if (treeNode.getData() instanceof StructureTreeNode) { + if (Objects.nonNull(treeNode) && treeNode.getData() instanceof StructureTreeNode) { StructureTreeNode structureTreeNode = (StructureTreeNode) treeNode.getData(); if (structureTreeNode.getDataObject() instanceof LogicalDivision) { return (LogicalDivision) structureTreeNode.getDataObject(); @@ -1669,7 +1669,7 @@ private LogicalDivision getTreeNodeStructuralElement(TreeNode treeNode) { } private PhysicalDivision getTreeNodePhysicalDivision(TreeNode treeNode) { - if (treeNode.getData() instanceof StructureTreeNode) { + if (Objects.nonNull(treeNode) && treeNode.getData() instanceof StructureTreeNode) { StructureTreeNode structureTreeNode = (StructureTreeNode) treeNode.getData(); if (structureTreeNode.getDataObject() instanceof PhysicalDivision) { return (PhysicalDivision) structureTreeNode.getDataObject(); @@ -1678,6 +1678,16 @@ private PhysicalDivision getTreeNodePhysicalDivision(TreeNode treeNode) { return null; } + private View getTreeNodeView(TreeNode treeNode) { + if (Objects.nonNull(treeNode) && treeNode.getData() instanceof StructureTreeNode) { + StructureTreeNode structureTreeNode = (StructureTreeNode) treeNode.getData(); + if (structureTreeNode.getDataObject() instanceof View) { + return (View) structureTreeNode.getDataObject(); + } + } + return null; + } + /** * Get List of PhysicalDivisions assigned to multiple LogicalDivisions. * @@ -1740,28 +1750,101 @@ public boolean isAssignedSeveralTimes() { } /** - * Check if the selected Node's PhysicalDivision can be assigned to the next logical element in addition to the current assignment. + * Find the next logical structure node that can be used to create a new link to the currently selected node. + * The node needs to be the last node amongst its siblings. + * + * @param node the tree node of the currently selected physical devision node + * @return the next logical tree node + */ + private TreeNode findNextLogicalNodeForViewAssignment(TreeNode node) { + if (Objects.isNull(getTreeNodeView(node))) { + // node is not a view + return null; + } + + List viewSiblings = node.getParent().getChildren(); + if (viewSiblings.indexOf(node) != viewSiblings.size() - 1) { + // view is not last view amongst siblings + return null; + } + + // pseudo-recursively find next logical node + return findNextLogicalNodeForViewAssignmentRecursive(node.getParent()); + } + + /** + * Find the next logical structure node that can be used to create a link by pseudo-recursively iterating over + * logical parent and logical children nodes. + * + * @param node the tree node of the logical division + * @return the tree node of the next logical division + */ + private TreeNode findNextLogicalNodeForViewAssignmentRecursive(TreeNode node) { + TreeNode current = node; + + while (Objects.nonNull(current)) { + if (Objects.isNull(getTreeNodeStructuralElement(current))) { + // node is not a logical node + return null; + } + + // check whether next sibling is a logical node as well + List currentSiblings = current.getParent().getChildren(); + int currentIndex = currentSiblings.indexOf(current); + + if (currentSiblings.size() > currentIndex + 1) { + TreeNode nextSibling = currentSiblings.get(currentIndex + 1); + if(Objects.isNull(getTreeNodeStructuralElement(nextSibling))) { + // next sibling is not a logical node + return null; + } + + // next sibling is a logical node and potential valid result, unless there are children + TreeNode nextLogical = nextSibling; + + // check sibling has children (with first child being another logical node) + while (!nextLogical.getChildren().isEmpty()) { + TreeNode firstChild = nextLogical.getChildren().get(0); + if(Objects.isNull(getTreeNodeStructuralElement(firstChild))) { + // first child is not a logical node + return nextLogical; + } + // iterate to child node + nextLogical = firstChild; + } + return nextLogical; + } + + // node is last amongst siblings + // iterate to parent node + current = current.getParent(); + } + return null; + } + + /** + * Check if the selected Node's PhysicalDivision can be assigned to the next logical element in addition to the + * current assignment. + * * @return {@code true} if the PhysicalDivision can be assigned to the next LogicalDivision */ public boolean isAssignableSeveralTimes() { - if (Objects.nonNull(selectedLogicalNode) && selectedLogicalNode.getData() instanceof StructureTreeNode) { - StructureTreeNode structureTreeNode = (StructureTreeNode) selectedLogicalNode.getData(); - if (structureTreeNode.getDataObject() instanceof View) { - List logicalNodeSiblings = selectedLogicalNode.getParent().getParent().getChildren(); - int logicalNodeIndex = logicalNodeSiblings.indexOf(selectedLogicalNode.getParent()); - List viewSiblings = selectedLogicalNode.getParent().getChildren(); - // check for selected node's positions and siblings after selected node's parent - if (viewSiblings.indexOf(selectedLogicalNode) == viewSiblings.size() - 1 - && logicalNodeSiblings.size() > logicalNodeIndex + 1) { - TreeNode nextSibling = logicalNodeSiblings.get(logicalNodeIndex + 1); - if (nextSibling.getData() instanceof StructureTreeNode) { - StructureTreeNode structureTreeNodeSibling = (StructureTreeNode) nextSibling.getData(); - return structureTreeNodeSibling.getDataObject() instanceof LogicalDivision; + TreeNode nextLogical = findNextLogicalNodeForViewAssignment(selectedLogicalNode); + if (Objects.nonNull(nextLogical)) { + // check whether first child is already view of current node (too avoid adding views multiple times) + if (!nextLogical.getChildren().isEmpty()) { + TreeNode childNode = nextLogical.getChildren().get(0); + View childNodeView = getTreeNodeView(childNode); + View selectedView = getTreeNodeView(selectedLogicalNode); + if (Objects.nonNull(childNodeView) && Objects.nonNull(selectedView)) { + if (childNodeView.equals(selectedView)) { + // first child is already a view for the currently selected node + return false; } } } + return true; } - return false; } @@ -1769,14 +1852,12 @@ public boolean isAssignableSeveralTimes() { * Assign selected Node's PhysicalDivision to the next LogicalDivision. */ public void assign() { - if (isAssignableSeveralTimes()) { + TreeNode nextLogical = findNextLogicalNodeForViewAssignment(selectedLogicalNode); + if (Objects.nonNull(nextLogical)) { View view = (View) ((StructureTreeNode) selectedLogicalNode.getData()).getDataObject(); View viewToAssign = new View(); viewToAssign.setPhysicalDivision(view.getPhysicalDivision()); - List logicalNodeSiblings = selectedLogicalNode.getParent().getParent().getChildren(); - int logicalNodeIndex = logicalNodeSiblings.indexOf(selectedLogicalNode.getParent()); - TreeNode nextSibling = logicalNodeSiblings.get(logicalNodeIndex + 1); - StructureTreeNode structureTreeNodeSibling = (StructureTreeNode) nextSibling.getData(); + StructureTreeNode structureTreeNodeSibling = (StructureTreeNode) nextLogical.getData(); LogicalDivision logicalDivision = (LogicalDivision) structureTreeNodeSibling.getDataObject(); dataEditor.assignView(logicalDivision, viewToAssign, 0); severalAssignments.add(viewToAssign.getPhysicalDivision()); From dcc8d3bd4888cef10ff2b4642406a8cacfbc84cc Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Tue, 8 Oct 2024 12:16:21 +0000 Subject: [PATCH 07/43] Fix checkstyle issues in StructurePanel. --- .../kitodo/production/forms/dataeditor/StructurePanel.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java index fa44e75eb55..00a66c5c9f1 100644 --- a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java +++ b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/StructurePanel.java @@ -1794,7 +1794,7 @@ private TreeNode findNextLogicalNodeForViewAssignmentRecursive(TreeNode node) { if (currentSiblings.size() > currentIndex + 1) { TreeNode nextSibling = currentSiblings.get(currentIndex + 1); - if(Objects.isNull(getTreeNodeStructuralElement(nextSibling))) { + if (Objects.isNull(getTreeNodeStructuralElement(nextSibling))) { // next sibling is not a logical node return null; } @@ -1805,7 +1805,7 @@ private TreeNode findNextLogicalNodeForViewAssignmentRecursive(TreeNode node) { // check sibling has children (with first child being another logical node) while (!nextLogical.getChildren().isEmpty()) { TreeNode firstChild = nextLogical.getChildren().get(0); - if(Objects.isNull(getTreeNodeStructuralElement(firstChild))) { + if (Objects.isNull(getTreeNodeStructuralElement(firstChild))) { // first child is not a logical node return nextLogical; } From e849fa34d73251078dc074e4deb7cd085d69630b Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Mon, 14 Oct 2024 11:27:08 +0000 Subject: [PATCH 08/43] Add selenium test for linking a page to the following division. --- .../java/org/kitodo/selenium/MetadataST.java | 55 +++++++++++ .../pages/MetadataEditorPage.java | 49 ++++++++++ .../testLinkPageToNextDivisionMeta.xml | 95 +++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 Kitodo/src/test/resources/metadata/metadataFiles/testLinkPageToNextDivisionMeta.xml diff --git a/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java b/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java index 031fc461cb4..e20f3e11a28 100644 --- a/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java +++ b/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java @@ -40,6 +40,7 @@ import org.kitodo.selenium.testframework.BaseTestSelenium; import org.kitodo.selenium.testframework.Browser; import org.kitodo.selenium.testframework.Pages; +import org.kitodo.selenium.testframework.pages.MetadataEditorPage; import org.kitodo.test.utils.ProcessTestUtils; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; @@ -55,15 +56,18 @@ public class MetadataST extends BaseTestSelenium { private static final String TEST_MEDIA_REFERENCES_FILE = "testUpdatedMediaReferencesMeta.xml"; private static final String TEST_METADATA_LOCK_FILE = "testMetadataLockMeta.xml"; private static final String TEST_RENAME_MEDIA_FILE = "testRenameMediaMeta.xml"; + private static final String TEST_LINK_PAGE_TO_NEXT_DIVISION_MEDIA_FILE = "testLinkPageToNextDivisionMeta.xml"; private static int mediaReferencesProcessId = -1; private static int metadataLockProcessId = -1; private static int parentProcessId = -1; private static int renamingMediaProcessId = -1; private static int dragndropProcessId = -1; private static int createStructureProcessId = -1; + private static int linkPageToNextDivisionProcessId = -1; private static final String PARENT_PROCESS_TITLE = "Parent process"; private static final String FIRST_CHILD_PROCESS_TITLE = "First child process"; private static final String SECOND_CHILD_PROCESS_TITLE = "Second child process"; + private static final String LINK_PAGE_TO_NEXT_DIVISION_PROCESS_TITLE = "Link page to next division"; private static final String TEST_PARENT_PROCESS_METADATA_FILE = "testParentProcessMeta.xml"; private static final String FIRST_CHILD_ID = "FIRST_CHILD_ID"; private static final String SECOND_CHILD_ID = "SECOND_CHILD_ID"; @@ -102,6 +106,11 @@ private static void prepareCreateStructureProcess() throws DAOException, DataExc copyTestFilesForCreateStructure(); } + private static void prepareLinkPageToNextDivision() throws DAOException, DataException, IOException { + linkPageToNextDivisionProcessId = MockDatabase.insertTestProcessIntoSecondProject(LINK_PAGE_TO_NEXT_DIVISION_PROCESS_TITLE); + ProcessTestUtils.copyTestFiles(linkPageToNextDivisionProcessId, TEST_LINK_PAGE_TO_NEXT_DIVISION_MEDIA_FILE); + } + /** * Prepare tests by inserting dummy processes into database and index for sub-folders of test metadata resources. * @throws DAOException when saving of dummy or test processes fails. @@ -117,6 +126,7 @@ public static void prepare() throws DAOException, DataException, IOException { prepareMediaRenamingProcess(); prepareDragNDropProcess(); prepareCreateStructureProcess(); + prepareLinkPageToNextDivision(); } /** @@ -353,6 +363,50 @@ public void showPhysicalPageNumberBelowThumbnailTest() throws Exception { assertFalse(Browser.getDriver().findElements(By.cssSelector(".thumbnail-banner")).isEmpty()); } + @Test + public void linkPageToNextDivision() throws Exception { + login("kowal"); + + // open metadata editor + Pages.getProcessesPage().goTo().editMetadata(LINK_PAGE_TO_NEXT_DIVISION_PROCESS_TITLE); + + MetadataEditorPage metaDataEditor = Pages.getMetadataEditorPage(); + + // wait until structure tree is shown + await().ignoreExceptions().pollDelay(100, TimeUnit.MILLISECONDS).atMost(5, TimeUnit.SECONDS) + .until(metaDataEditor::isLogicalTreeVisible); + + // check page "2" is not marked as "linked" + assertFalse(metaDataEditor.isStructureTreeNodeAssignedSeveralTimes("0_0_0_0")); + + // open context menu for page "2" + metaDataEditor.openContextMenuForStructureTreeNode("0_0_0_0"); + + // click on 2nd menu entry "assign to next element" + metaDataEditor.clickStructureTreeContextMenuEntry(2); + + // verify page "2" is now marked as "linked" + assertTrue(metaDataEditor.isStructureTreeNodeAssignedSeveralTimes("0_0_0_0")); + + // verify linked page "2" was created at correct tree position + assertTrue(metaDataEditor.isStructureTreeNodeAssignedSeveralTimes("0_1_0_0")); + + // check page "3" was moved to be 2nd sibling + assertFalse(metaDataEditor.isStructureTreeNodeAssignedSeveralTimes("0_1_0_1")); + + // open context menu for linked page "2" + metaDataEditor.openContextMenuForStructureTreeNode("0_1_0_0"); + + // click on 2nd menu entry "remove assignment" + metaDataEditor.clickStructureTreeContextMenuEntry(2); + + // check page "2" is not marked as "linked" any more + assertFalse(metaDataEditor.isStructureTreeNodeAssignedSeveralTimes("0_0_0_0")); + + // check page "3" is now only child of folder again + assertTrue(Browser.getDriver().findElements(By.cssSelector("#logicalTree\\:0_1_0_1")).isEmpty()); + } + /** * Close metadata editor and logout after every test. * @throws Exception when page navigation fails @@ -380,6 +434,7 @@ public static void cleanup() throws DAOException, CustomResponseException, DataE ProcessService.deleteProcess(renamingMediaProcessId); ProcessService.deleteProcess(dragndropProcessId); ProcessService.deleteProcess(createStructureProcessId); + ProcessService.deleteProcess(linkPageToNextDivisionProcessId); } private void login(String username) throws InstantiationException, IllegalAccessException, InterruptedException { diff --git a/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java b/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java index e6284c17a9d..e19834fc122 100644 --- a/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java +++ b/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java @@ -78,6 +78,9 @@ public class MetadataEditorPage extends Page { @FindBy(id = "renamingMediaResultForm:okSuccess") private WebElement okButtonRenameMediaFiles; + @FindBy(id = "contextMenuLogicalTree") + private WebElement contextMenuLogicalTree; + public MetadataEditorPage() { super("metadataEditor.jsf"); } @@ -91,6 +94,10 @@ public boolean isStructureTreeFormVisible() { return structureTreeForm.isDisplayed(); } + public boolean isLogicalTreeVisible() { + return logicalTree.isDisplayed(); + } + /** * Gets numberOfScans. * @@ -222,4 +229,46 @@ public long getNumberOfDisplayedStructureElements() { return Browser.getDriver().findElements(By.cssSelector(".ui-treenode")).stream().filter(WebElement::isDisplayed) .count(); } + + /** + * Open context menu (right click) for specific structure tree node. + * + * @param nodeId the tree node id describing the node in the tree (e.g., "0_1_0_1") + */ + public void openContextMenuForStructureTreeNode(String nodeId) { + WebElement treeNode = Browser.getDriver().findElement(By.cssSelector( + "#logicalTree\\:" + nodeId + " .ui-treenode-content" + )); + new Actions(Browser.getDriver()).contextClick(treeNode).build().perform(); + await().ignoreExceptions().pollDelay(100, TimeUnit.MILLISECONDS).atMost(5, TimeUnit.SECONDS).until( + () -> contextMenuLogicalTree.isDisplayed() + ); + } + + /** + * Click on a menu entry in the structure tree context menu. + * + * @param menuEntry the menu entry index (starting with 1) + */ + public void clickStructureTreeContextMenuEntry(int menuEntry) { + // click on menu entry + contextMenuLogicalTree.findElement(By.cssSelector( + ".ui-menuitem:nth-child(" + menuEntry + ") .ui-menuitem-link" + )).click(); + // wait for context menu to disappear + await().ignoreExceptions().pollDelay(100, TimeUnit.MILLISECONDS).atMost(5, TimeUnit.SECONDS) + .until(() -> !contextMenuLogicalTree.isDisplayed()); + } + + /** + * Check if a structure tree node is marked as "assigned several times". + * + * @param nodeId the tree node id describing the node in the tree (e.g., "0_1_0_1") + * @return true if "assigned several times" + */ + public Boolean isStructureTreeNodeAssignedSeveralTimes(String nodeId) { + return !Browser.getDriver().findElements(By.cssSelector( + "#logicalTree\\:" + nodeId + " .assigned-several-times" + )).isEmpty(); + } } diff --git a/Kitodo/src/test/resources/metadata/metadataFiles/testLinkPageToNextDivisionMeta.xml b/Kitodo/src/test/resources/metadata/metadataFiles/testLinkPageToNextDivisionMeta.xml new file mode 100644 index 00000000000..efd4575d043 --- /dev/null +++ b/Kitodo/src/test/resources/metadata/metadataFiles/testLinkPageToNextDivisionMeta.xml @@ -0,0 +1,95 @@ + + + + + 12 + + + + + + Test link page to next division + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 709180bfba04e2dbedd068b3c49b1d740fb4e463 Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Sun, 20 Oct 2024 11:17:07 +0000 Subject: [PATCH 09/43] Allow to open page in new window showing media detail preview. --- .../forms/dataeditor/ExternalView.java | 40 +++++++++ .../resources/messages/messages_de.properties | 1 + .../resources/messages/messages_en.properties | 1 + .../resources/messages/messages_es.properties | 2 + .../webapp/WEB-INF/resources/css/kitodo.css | 23 +++++ .../includes/metadataEditor/gallery.xhtml | 11 +++ .../metadataEditor/logicalStructure.xhtml | 11 +++ .../src/main/webapp/pages/externalView.xhtml | 90 +++++++++++++++++++ 8 files changed, 179 insertions(+) create mode 100644 Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/ExternalView.java create mode 100644 Kitodo/src/main/webapp/pages/externalView.xhtml diff --git a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/ExternalView.java b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/ExternalView.java new file mode 100644 index 00000000000..559a3e82774 --- /dev/null +++ b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/ExternalView.java @@ -0,0 +1,40 @@ +/* + * (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.forms.dataeditor; + +import java.util.Objects; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Named; + +/** + * Bean that is used in externalView.xhtml. + */ +@Named("ExternalView") +@RequestScoped +public class ExternalView { + + /** + * Return the shortened ID of the media by removing leading zeros. + * + * @param id the long id of the media file as string + * @return the shortened id of the media file + */ + public static String convertToShortId(String id) { + if (Objects.nonNull(id)) { + return id.replaceFirst("^0+(?!$)", ""); + } else { + return "-"; + } + } + +} diff --git a/Kitodo/src/main/resources/messages/messages_de.properties b/Kitodo/src/main/resources/messages/messages_de.properties index 1dd2fbc31c8..5d933d7d451 100644 --- a/Kitodo/src/main/resources/messages/messages_de.properties +++ b/Kitodo/src/main/resources/messages/messages_de.properties @@ -1197,6 +1197,7 @@ validator={0}-Bilder validieren value=Wert video=Video view=Anzeigen +viewPageInNewWindow=Seite in neuem Browser-Fenster öffnen visible=Sichtbar volume=Band of=von diff --git a/Kitodo/src/main/resources/messages/messages_en.properties b/Kitodo/src/main/resources/messages/messages_en.properties index ffaabc80ab9..ee02599e3fa 100644 --- a/Kitodo/src/main/resources/messages/messages_en.properties +++ b/Kitodo/src/main/resources/messages/messages_en.properties @@ -1198,6 +1198,7 @@ validator=Validate {0} images value=Value video=Video view=View +viewPageInNewWindow=View page in new browser window visible=Visible volume=volume of=of diff --git a/Kitodo/src/main/resources/messages/messages_es.properties b/Kitodo/src/main/resources/messages/messages_es.properties index 32deac1a1b6..f222c67dd90 100644 --- a/Kitodo/src/main/resources/messages/messages_es.properties +++ b/Kitodo/src/main/resources/messages/messages_es.properties @@ -1198,6 +1198,8 @@ validator=Validar {0}-imágenes value=Valor video=Video view=Ver +# please check google translation below and remove comment if translation is acceptable +viewPageInNewWindow=Ver página en una nueva ventana del navegador visible=Visible volume=Banda of=Desde diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css index 0af10140564..576bd3a347c 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css +++ b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css @@ -3697,6 +3697,29 @@ kbd { padding: 0 var(--default-half-size); } +#externalViewTitle { + color: var(--pure-white); + line-height: 1.5em; +} + +#externalViewPanel { + margin: auto 16px; +} + +#externalViewPanel, #externalViewPanel_content { + padding: 0; + height: 100%; + overflow: hidden; +} + +#externalViewPanel #imagePreviewForm\:mediaDetailMediaContainer { + height: 100%; +} + +#externalViewPanel video { + max-height: 90%; +} + /*---------------------------------------------------------------------- Workflow Editor ----------------------------------------------------------------------*/ diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml index 963816c6116..b8576f5b9ea 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml @@ -249,6 +249,17 @@ paginationForm:paginationWrapperPanel metadataAccordion:logicalMetadataWrapperPanel galleryWrapperPanel"/> + + + + + + + + + + + +
diff --git a/Kitodo/src/main/webapp/pages/externalView.xhtml b/Kitodo/src/main/webapp/pages/externalView.xhtml new file mode 100644 index 00000000000..2edabc9bc45 --- /dev/null +++ b/Kitodo/src/main/webapp/pages/externalView.xhtml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + +

+ + + + + + +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ + + + +
+ +
\ No newline at end of file From d12b2b3df362f14ea2771298959a4dbb5152f1b8 Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Sun, 20 Oct 2024 11:43:28 +0000 Subject: [PATCH 10/43] Do not show menu option to open new window if preview media is not available. --- .../WEB-INF/templates/includes/metadataEditor/gallery.xhtml | 4 +++- .../templates/includes/metadataEditor/logicalStructure.xhtml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml index b8576f5b9ea..67fe8f13071 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml @@ -250,7 +250,9 @@ metadataAccordion:logicalMetadataWrapperPanel galleryWrapperPanel"/> Date: Sun, 20 Oct 2024 20:14:57 +0000 Subject: [PATCH 11/43] Add selenium test verifying that page can be opened in new browser window. --- .../java/org/kitodo/selenium/MetadataST.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java b/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java index 031fc461cb4..de0a30bc3d5 100644 --- a/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java +++ b/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java @@ -353,6 +353,60 @@ public void showPhysicalPageNumberBelowThumbnailTest() throws Exception { assertFalse(Browser.getDriver().findElements(By.cssSelector(".thumbnail-banner")).isEmpty()); } + /** + * Verifies that an image can be openend in a separate window by clicking on the corresponding + * context menu item of the first logical tree node. + */ + @Test + public void openPageInSeparateWindowTest() throws Exception { + login("kowal"); + + // remember current window handle + String firstWindowHandle = Browser.getDriver().getWindowHandle(); + + // open the metadata editor + Pages.getProcessesPage().goTo().editMetadata(MockDatabase.MEDIA_RENAMING_TEST_PROCESS_TITLE); + + // wait until structure tree is shown + await().ignoreExceptions().pollDelay(100, TimeUnit.MILLISECONDS).atMost(5, TimeUnit.SECONDS) + .until(() -> Browser.getDriver().findElement(By.id("logicalTree")).isDisplayed()); + + // right click on first tree node representing image 2 + WebElement firstTreeNode = Browser.getDriver().findElement( + By.cssSelector("#logicalTree\\:0_0 .ui-treenode-content") + ); + new Actions(Browser.getDriver()).contextClick(firstTreeNode).build().perform(); + + // wait until menu is visible + await().ignoreExceptions().pollDelay(100, TimeUnit.MILLISECONDS).atMost(5, TimeUnit.SECONDS) + .until(() -> Browser.getDriver().findElement(By.id("contextMenuLogicalTree")).isDisplayed()); + + // click second menu entry to open new tab + Browser.getDriver().findElement(By.cssSelector( + "#contextMenuLogicalTree .ui-menuitem:nth-child(2) .ui-menuitem-link" + )).click(); + + // find handle of new tab window + String newWindowHandle = Browser.getDriver().getWindowHandles().stream() + .filter((h) -> !h.equals(firstWindowHandle)).findFirst().get(); + + // switch to new window + Browser.getDriver().switchTo().window(newWindowHandle); + + // wait until preview image is found + await().ignoreExceptions().pollDelay(100, TimeUnit.MILLISECONDS).atMost(5, TimeUnit.SECONDS) + .until(() -> !Browser.getDriver().findElements(By.id("imagePreviewForm:mediaPreviewGraphicImage")).isEmpty()); + + // check that title contains image number + assertEquals("Bild 2", Browser.getDriver().findElement(By.id("externalViewTitle")).getText()); + + // close tab + Browser.getDriver().close(); + + // switch back to previous window + Browser.getDriver().switchTo().window(firstWindowHandle); + } + /** * Close metadata editor and logout after every test. * @throws Exception when page navigation fails From 75d56205da77b9d6fd50dbfaf826294329d4b105 Mon Sep 17 00:00:00 2001 From: Arved Solth Date: Fri, 25 Oct 2024 08:57:48 +0200 Subject: [PATCH 12/43] Adjust titles of some tabs containing lists --- Kitodo/src/main/webapp/pages/tasks.xhtml | 2 +- Kitodo/src/main/webapp/pages/userEdit.xhtml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Kitodo/src/main/webapp/pages/tasks.xhtml b/Kitodo/src/main/webapp/pages/tasks.xhtml index abd3a3d1cea..f97b7f30d60 100644 --- a/Kitodo/src/main/webapp/pages/tasks.xhtml +++ b/Kitodo/src/main/webapp/pages/tasks.xhtml @@ -59,7 +59,7 @@ - + diff --git a/Kitodo/src/main/webapp/pages/userEdit.xhtml b/Kitodo/src/main/webapp/pages/userEdit.xhtml index d6d511ffca7..9120f2fdffd 100644 --- a/Kitodo/src/main/webapp/pages/userEdit.xhtml +++ b/Kitodo/src/main/webapp/pages/userEdit.xhtml @@ -100,14 +100,14 @@ - + - + From 92a4dcc5096a7ad4aeccaaec135b45810c45c354 Mon Sep 17 00:00:00 2001 From: Stefan Weil Date: Sun, 28 Jul 2024 17:47:16 +0200 Subject: [PATCH 13/43] Remove unused dependency org.apache.pdfbox:pdfbox It's only used in Kitodo-Docket which has its own dependency entry. Signed-off-by: Stefan Weil --- Kitodo/pom.xml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Kitodo/pom.xml b/Kitodo/pom.xml index f5fde9e9192..c77fb52864b 100644 --- a/Kitodo/pom.xml +++ b/Kitodo/pom.xml @@ -145,18 +145,6 @@ org.apache.logging.log4j log4j-web - - - org.apache.pdfbox - pdfbox - 2.0.32 - - - commons-logging - commons-logging - - - org.apache.poi poi-ooxml From 300f949ed015e9b9bc4fac9c596d47b7998ec588 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Thu, 31 Oct 2024 17:42:48 +0100 Subject: [PATCH 14/43] Calculate the default timeout correctly Due to a unit mix-up, the default timeout was only set to 1/60 of the intended time period, which occasionally causes errors in large images that you have to find first. --- .../main/java/org/kitodo/imagemanagement/ConvertRunner.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kitodo-ImageManagement/src/main/java/org/kitodo/imagemanagement/ConvertRunner.java b/Kitodo-ImageManagement/src/main/java/org/kitodo/imagemanagement/ConvertRunner.java index 0423b162e5c..22993c63a28 100644 --- a/Kitodo-ImageManagement/src/main/java/org/kitodo/imagemanagement/ConvertRunner.java +++ b/Kitodo-ImageManagement/src/main/java/org/kitodo/imagemanagement/ConvertRunner.java @@ -51,7 +51,7 @@ class ConvertRunner { /** * Default timeout. */ - private static final int DEFAULT_TIMEOUT_MINS = (int) TimeUnit.MINUTES.convert(2, TimeUnit.HOURS); + private static final int DEFAULT_TIMEOUT_SECS = (int) TimeUnit.SECONDS.convert(2, TimeUnit.HOURS); /** * {@code convert} command, optionally with full path. @@ -72,7 +72,7 @@ void run(IMOperation commandLine) throws IOException { OutputStream outAndErr = new ByteArrayOutputStream(); executor.setStreamHandler(new PumpStreamHandler(outAndErr)); - long timeoutMillis = 1000L * KitodoConfig.getIntParameter(ParameterImageManagement.TIMEOUT_SEC, DEFAULT_TIMEOUT_MINS); + long timeoutMillis = 1000L * KitodoConfig.getIntParameter(ParameterImageManagement.TIMEOUT_SEC, DEFAULT_TIMEOUT_SECS); executor.setWatchdog(new ExecuteWatchdog(timeoutMillis)); CommandLine command; From dca883e3f1cf51c0ebab523cb8b57e2b09428a19 Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Tue, 5 Nov 2024 14:01:00 +0000 Subject: [PATCH 15/43] Update references to openlayers 6.14.1 after merging with #6236. --- Kitodo/src/main/webapp/pages/externalView.xhtml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Kitodo/src/main/webapp/pages/externalView.xhtml b/Kitodo/src/main/webapp/pages/externalView.xhtml index 2edabc9bc45..9150bfde4b9 100644 --- a/Kitodo/src/main/webapp/pages/externalView.xhtml +++ b/Kitodo/src/main/webapp/pages/externalView.xhtml @@ -82,9 +82,15 @@ - - + + + + $(document).ready(function () { + // load openlayers and show image + metadataEditor.detailMap.update(); + }); + \ No newline at end of file From a70fa11b7c20647949fe73a682b541a6ffc9422a Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Tue, 5 Nov 2024 14:07:39 +0000 Subject: [PATCH 16/43] Move context menu for opening new window to second position. --- .../includes/metadataEditor/gallery.xhtml | 26 +++++++++---------- .../metadataEditor/logicalStructure.xhtml | 26 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml index dfdb9f11a1f..60086fd5895 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml @@ -201,6 +201,19 @@ update="dialogAddDocStrucTypeDialog"> + + + + + + - - - - - - + + + + + + - - - - - -
From 28292df5320e0b76be017992b00f99e6d8f80a9f Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Tue, 5 Nov 2024 14:08:20 +0000 Subject: [PATCH 17/43] Add check for map canvas to detect potential openlayer problems in separate window. --- Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java b/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java index 58be2c6906a..de1499ada37 100644 --- a/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java +++ b/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java @@ -450,6 +450,9 @@ public void openPageInSeparateWindowTest() throws Exception { // check that title contains image number assertEquals("Bild 2", Browser.getDriver().findElement(By.id("externalViewTitle")).getText()); + // check that canvas is visible + assertTrue(Browser.getDriver().findElement(By.cssSelector("#map canvas")).isDisplayed()); + // close tab Browser.getDriver().close(); From 39edb4a31ca5812bb31502350d06ec45c88e5cfa Mon Sep 17 00:00:00 2001 From: Arved Solth Date: Tue, 5 Nov 2024 16:55:12 +0100 Subject: [PATCH 18/43] Add missing declaration of 'groupDisplayLabel' to test ruleset.xsd --- Kitodo-DataEditor/src/test/resources/ruleset.xsd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Kitodo-DataEditor/src/test/resources/ruleset.xsd b/Kitodo-DataEditor/src/test/resources/ruleset.xsd index 012ee754c96..33560322467 100644 --- a/Kitodo-DataEditor/src/test/resources/ruleset.xsd +++ b/Kitodo-DataEditor/src/test/resources/ruleset.xsd @@ -466,6 +466,9 @@ imported metadata entries from the data source can be updated later. 'title' This field is used as the title to form the author-title key. + + 'groupDisplayLabel' keys of this type are displayed in the metadata update dialog of the metadata + editor. @@ -481,6 +484,7 @@ + From bba19df34c6f4ce9586c608ec51c9e6ace2807cf Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Tue, 16 Jul 2024 10:49:26 +0200 Subject: [PATCH 19/43] Enable Spotbugs annotations --- Kitodo/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) 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 From bd1a05dd4b6c32abc9497092608ea884f3c7e63a Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Tue, 16 Jul 2024 10:50:33 +0200 Subject: [PATCH 20/43] *Start developing --- .../activemq/CreateNewProcessesProcessor.java | 101 ++++++++++++++++++ .../activemq/MapMessageObjectReader.java | 3 + 2 files changed, 104 insertions(+) create mode 100644 Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java 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..c840ea80578 --- /dev/null +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -0,0 +1,101 @@ +/* + * (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.util.Date; +import java.util.List; +import java.util.Map; + +import javax.jms.JMSException; + +import org.kitodo.config.ConfigCore; +import org.kitodo.config.enums.ParameterCore; +import org.kitodo.data.database.beans.Comment; +import org.kitodo.data.database.beans.Property; +import org.kitodo.data.database.enums.CommentType; +import org.kitodo.data.database.exceptions.DAOException; +import org.kitodo.exceptions.ProcessorException; +import org.kitodo.production.forms.CurrentTaskForm; +import org.kitodo.production.services.ServiceManager; + +/** + * An Active MQ service interface to create new processes. + */ +public class CreateNewProcessesProcessor extends ActiveMQProcessor { + + /** + * 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. + */ + @Override + protected void process(MapMessageObjectReader ticket) throws ProcessorException, JMSException { + try { + Integer projectId = ticket.getMandatoryInteger("project"); + Integer templateId = ticket.getMandatoryInteger("template"); + List imports = ticket.getList("import"); // may be null + String title = ticket.getString("title"); // may be null + Integer parent = ticket.getInteger("parent"); // may be null + Map metadata = ticket.getMapOfString("metadata"); // may + // be + // null + +// Integer stepID = ticket.getMandatoryInteger("id"); +// dialog.setCurrentTask(ServiceManager.getTaskService().getById(stepID)); +// +// if (ticket.hasField("properties")) { +// updateProperties(dialog, ticket.getMapOfStringToString("properties")); +// } +// if (ticket.hasField("message")) { +// Comment comment = new Comment(); +// comment.setProcess(dialog.getCurrentTask().getProcess()); +// comment.setMessage(ticket.getString("message")); +// comment.setAuthor(ServiceManager.getUserService().getCurrentUser()); +// comment.setType(CommentType.INFO); +// comment.setCreationDate(new Date()); +// ServiceManager.getCommentService().saveToDatabase(comment); +// } +// dialog.closeTaskByUser(); + } catch (DAOException e) { + throw new ProcessorException(e); + } + } + + /** + * Transfers the properties to set into Production’s data model. + * + * @param dialog + * The CurrentTaskForm that we work with + * @param propertiesToSet + * A Map with the properties to set + */ + private void updateProperties(CurrentTaskForm dialog, Map propertiesToSet) { + List availableProperties = dialog.getProperties(); + for (Property property : availableProperties) { + String key = property.getTitle(); + if (propertiesToSet.containsKey(key)) { + String desiredValue = propertiesToSet.get(key); + property.setValue(desiredValue); + dialog.setProperty(property); + dialog.saveCurrentProperty(); + } + } + } +} 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..8352e508959 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 @@ -29,6 +29,8 @@ import org.apache.logging.log4j.Logger; import org.kitodo.utils.Guard; +import edu.umd.cs.findbugs.annotations.CheckForNull; + public class MapMessageObjectReader { private MapMessage ticket; @@ -169,6 +171,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); } From 98c9d72d9777b0893479c7076786614f84517834 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Thu, 1 Aug 2024 09:58:57 +0200 Subject: [PATCH 21/43] *Create parameter queue --- .../src/main/java/org/kitodo/config/enums/ParameterCore.java | 2 ++ Kitodo/src/main/resources/kitodo_config.properties | 3 +++ 2 files changed, 5 insertions(+) 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/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 From c083105e29c6c93132976ab7e718963dc6536e72 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Thu, 1 Aug 2024 12:33:51 +0200 Subject: [PATCH 22/43] Parse ticket --- .../activemq/CreateNewProcessOrder.java | 215 ++++++++++++++++++ .../activemq/CreateNewProcessesProcessor.java | 62 +---- .../activemq/MapMessageObjectReader.java | 81 ++++++- 3 files changed, 298 insertions(+), 60 deletions(-) create mode 100644 Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java 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..6fe36a1a2a9 --- /dev/null +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java @@ -0,0 +1,215 @@ +/* + * (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.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.Metadata; +import org.kitodo.api.MetadataEntry; +import org.kitodo.api.MetadataGroup; +import org.kitodo.data.database.beans.ImportConfiguration; +import org.kitodo.data.database.exceptions.DAOException; +import org.kitodo.production.services.ServiceManager; +import org.kitodo.production.services.data.ImportConfigurationService; + +import edu.umd.cs.findbugs.annotations.NonNull; +import net.bytebuddy.utility.nullability.MaybeNull; + +/** + * Order to create a new process. This contains all the necessary data. + */ +public class CreateNewProcessOrder { + 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 JMSException + * Defined by the JMS API. I have not seen any cases where this + * would actually be thrown in the calls used here. + * @throws DAOException + * if the ImportConfiguartionDAO is unable to find an import + * configuration with the given ID + * @throws IllegalArgumentException + * If a required field is missing in the Active MQ message + * message, or contains inappropriate values. + */ + public CreateNewProcessOrder(MapMessageObjectReader ticket) throws JMSException, DAOException { + this.projectId = ticket.getMandatoryInteger("project"); + this.templateId = ticket.getMandatoryInteger("template"); + this.imports = convertImports(ticket.getList("import")); + this.title = Optional.ofNullable(ticket.getString("title")); + this.parentId = Optional.ofNullable(ticket.getInteger("parent")); + this.metadata = convertMetadata(ticket.getMapOfString("metadata")); + } + + /** + * 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(@MaybeNull 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, "importconfiguration")); + String value = MapMessageObjectReader.getMandatoryString(map, "value"); + result.add(Pair.of(importconfiguration, value)); + } + return result; + } + + /** + * Converts metadata details into safe data objects. For {@code null}, it + * will return an empty collection, never {@code null}. + */ + private static final HashSet convertMetadata(@MaybeNull Map metadata) { + + 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)); + result.add(metadataGroup); + } else { + MetadataEntry metadataEntry = new MetadataEntry(); + metadataEntry.setKey(key); + metadataEntry.setValue(dubiousValue.toString()); + 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 + public Integer getProjectId() { + return projectId; + } + + /** + * Returns the production template ID. This is a mandatory field and can + * never be {@code null}. + * + * @return the template ID + */ + @NonNull + public 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 + public 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 + public Optional getTitle() { + return title; + } + + /** + * Returns the (optional) parent record ID. If set, the process to be + * created must be created as the new last child under this parent process. + * Otherwise, a standalone process is created. Can be + * {@code Optional.empty()}, but never {@code null}. + * + * @return the title, if any + */ + @NonNull + public Optional getParentId() { + return parentId; + } + + /** + * Specifies the metadata for the logical structure root of the process to + * be created. Can be empty, but never {@code null}. + * + * @return + */ + @NonNull + public 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 index c840ea80578..abaa383ad3f 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -11,21 +11,12 @@ package org.kitodo.production.interfaces.activemq; -import java.util.Date; -import java.util.List; -import java.util.Map; - import javax.jms.JMSException; import org.kitodo.config.ConfigCore; import org.kitodo.config.enums.ParameterCore; -import org.kitodo.data.database.beans.Comment; -import org.kitodo.data.database.beans.Property; -import org.kitodo.data.database.enums.CommentType; import org.kitodo.data.database.exceptions.DAOException; import org.kitodo.exceptions.ProcessorException; -import org.kitodo.production.forms.CurrentTaskForm; -import org.kitodo.production.services.ServiceManager; /** * An Active MQ service interface to create new processes. @@ -48,54 +39,15 @@ public CreateNewProcessesProcessor() { @Override protected void process(MapMessageObjectReader ticket) throws ProcessorException, JMSException { try { - Integer projectId = ticket.getMandatoryInteger("project"); - Integer templateId = ticket.getMandatoryInteger("template"); - List imports = ticket.getList("import"); // may be null - String title = ticket.getString("title"); // may be null - Integer parent = ticket.getInteger("parent"); // may be null - Map metadata = ticket.getMapOfString("metadata"); // may - // be - // null - -// Integer stepID = ticket.getMandatoryInteger("id"); -// dialog.setCurrentTask(ServiceManager.getTaskService().getById(stepID)); -// -// if (ticket.hasField("properties")) { -// updateProperties(dialog, ticket.getMapOfStringToString("properties")); -// } -// if (ticket.hasField("message")) { -// Comment comment = new Comment(); -// comment.setProcess(dialog.getCurrentTask().getProcess()); -// comment.setMessage(ticket.getString("message")); -// comment.setAuthor(ServiceManager.getUserService().getCurrentUser()); -// comment.setType(CommentType.INFO); -// comment.setCreationDate(new Date()); -// ServiceManager.getCommentService().saveToDatabase(comment); -// } -// dialog.closeTaskByUser(); + CreateNewProcessOrder order = new CreateNewProcessOrder(ticket); + + + } catch (IllegalArgumentException e) { + // ticket.getMandatory... value not found (null) + throw new ProcessorException(e); } catch (DAOException e) { + // importconfiguration not found throw new ProcessorException(e); } } - - /** - * Transfers the properties to set into Production’s data model. - * - * @param dialog - * The CurrentTaskForm that we work with - * @param propertiesToSet - * A Map with the properties to set - */ - private void updateProperties(CurrentTaskForm dialog, Map propertiesToSet) { - List availableProperties = dialog.getProperties(); - for (Property property : availableProperties) { - String key = property.getTitle(); - if (propertiesToSet.containsKey(key)) { - String desiredValue = propertiesToSet.get(key); - property.setValue(desiredValue); - dialog.setProperty(property); - dialog.saveCurrentProperty(); - } - } - } } 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 8352e508959..4988f1e8c55 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,11 +11,14 @@ 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.Objects; import java.util.Set; @@ -29,12 +32,11 @@ import org.apache.logging.log4j.Logger; import org.kitodo.utils.Guard; -import edu.umd.cs.findbugs.annotations.CheckForNull; - 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 "; @@ -104,7 +106,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); @@ -159,6 +162,32 @@ public Collection getCollectionOfString(String key) throws JMSException .collect(Collectors.toList()); } + /** + * 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 String from a MapMessage. This is an access forward to the * native function of the MapMessage. You may consider to use @@ -181,12 +210,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)) { @@ -195,6 +224,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 @@ -243,6 +299,21 @@ public Map getMapOfStringToString(String key) { return mapOfStringToString; } + @CheckForNull + public List getList(String key) { + return null; + } + + @CheckForNull + public Integer getInteger(String key) { + return null; + } + + @CheckForNull + public Map getMapOfString(String string) { + return null; + } + /** * Tests whether a field can be obtained from a MapMessage. * From a9dbc1c91f0a8a5e25290fe2e05f78ab90c6fbb2 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Mon, 5 Aug 2024 14:04:00 +0200 Subject: [PATCH 23/43] * changes --- .../activemq/CreateNewProcessesProcessor.java | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) 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 index abaa383ad3f..d1d27f6a66f 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -11,17 +11,30 @@ package org.kitodo.production.interfaces.activemq; +import java.util.Collections; +import java.util.LinkedList; + import javax.jms.JMSException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.kitodo.api.dataformat.Workpiece; import org.kitodo.config.ConfigCore; import org.kitodo.config.enums.ParameterCore; import org.kitodo.data.database.exceptions.DAOException; +import org.kitodo.exceptions.ProcessGenerationException; import org.kitodo.exceptions.ProcessorException; +import org.kitodo.production.helper.TempProcess; +import org.kitodo.production.process.ProcessGenerator; +import org.kitodo.production.services.ServiceManager; +import org.kitodo.production.services.data.ProcessService; /** * An Active MQ service interface to create new processes. */ public class CreateNewProcessesProcessor extends ActiveMQProcessor { + private static final Logger logger = LogManager.getLogger(CreateNewProcessesProcessor.class); + private final ProcessService processService = ServiceManager.getProcessService(); /** * The default constructor looks up the queue name to use in @@ -41,13 +54,22 @@ protected void process(MapMessageObjectReader ticket) throws ProcessorException, try { CreateNewProcessOrder order = new CreateNewProcessOrder(ticket); + // init + ProcessGenerator processGenerator = new ProcessGenerator(); + boolean generated = processGenerator.generateProcess(order.getTemplateId(), order.getProjectId()); + if (!generated) { + throw new ProcessGenerationException("Process was not generated"); + } + Workpiece workpiece = new Workpiece(); + TempProcess tp = new TempProcess(processGenerator.getGeneratedProcess(), workpiece); - } catch (IllegalArgumentException e) { - // ticket.getMandatory... value not found (null) - throw new ProcessorException(e); - } catch (DAOException e) { - // importconfiguration not found - throw new ProcessorException(e); + if (order.getImports().isEmpty()) { + // create process without import + } else { + // create process with import + } + } catch (DAOException /* | DataException */ | ProcessGenerationException e) { + throw new ProcessorException(e.getMessage()); } } } From 0a5cf5aea680dc37c5a164f574df28cebf4ecd8b Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Mon, 12 Aug 2024 14:10:48 +0200 Subject: [PATCH 24/43] Implement get object from Map Message --- .../activemq/MapMessageObjectReader.java | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) 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 4988f1e8c55..b5fa1ece007 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 @@ -20,6 +20,7 @@ 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; @@ -300,18 +301,48 @@ public Map getMapOfStringToString(String key) { } @CheckForNull - public List getList(String key) { - return null; + 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; } @CheckForNull - public Integer getInteger(String key) { - return null; + 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; } @CheckForNull - public Map getMapOfString(String string) { - return null; + 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; } /** From be6b70bbc6f6ac4dbf95633440549ab9baf17b85 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Mon, 12 Aug 2024 16:11:57 +0200 Subject: [PATCH 25/43] Main task of implementing --- .../activemq/CreateNewProcessOrder.java | 79 +++++++++++++++- .../activemq/CreateNewProcessesProcessor.java | 94 ++++++++++++++++--- 2 files changed, 153 insertions(+), 20 deletions(-) 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 index 6fe36a1a2a9..d3460b515b2 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java @@ -20,6 +20,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; +import java.util.regex.Pattern; import javax.jms.JMSException; @@ -28,10 +29,16 @@ 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; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import net.bytebuddy.utility.nullability.MaybeNull; @@ -51,25 +58,31 @@ public class CreateNewProcessOrder { * * @param ticket * Active MQ message with (hopefully) all the data - * @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 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 */ - public CreateNewProcessOrder(MapMessageObjectReader ticket) throws JMSException, DAOException { + public CreateNewProcessOrder(MapMessageObjectReader ticket) throws DAOException, DataException, JMSException, + ProcessorException { this.projectId = ticket.getMandatoryInteger("project"); this.templateId = ticket.getMandatoryInteger("template"); this.imports = convertImports(ticket.getList("import")); this.title = Optional.ofNullable(ticket.getString("title")); - this.parentId = Optional.ofNullable(ticket.getInteger("parent")); + this.parentId = Optional.ofNullable(convertProcessId(ticket.getString("parent"))); this.metadata = convertMetadata(ticket.getMapOfString("metadata")); } + /** * Converts import details into safe data objects. For {@code null}, it will * return an empty list, never {@code null}. @@ -103,6 +116,39 @@ private static final List> convertImports(@May 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}. @@ -153,6 +199,17 @@ public Integer getProjectId() { return projectId; } + /** + * Returns the production template. + * + * @return the template + * @throws DAOException + * if the template cannot be loaded + */ + public 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}. @@ -189,6 +246,18 @@ public 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 + public Process getParent() throws DAOException { + return parentId.isPresent() ? ServiceManager.getProcessService().getById(parentId.get()) : null; + } + /** * Returns the (optional) parent record ID. If set, the process to be * created must be created as the new last child under this parent process. 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 index d1d27f6a66f..64364753abf 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -11,30 +11,60 @@ package org.kitodo.production.interfaces.activemq; +import java.io.IOException; +import java.net.URISyntaxException; import java.util.Collections; -import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Locale.LanguageRange; 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.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.exceptions.DAOException; +import org.kitodo.data.exceptions.DataException; +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.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.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 = "create"; + private static final int IMPORT_DEPTH = 1; + private static final List METADATA_LANGUAGE = Locale.LanguageRange.parse("en"); + + private final ImportService importService = ServiceManager.getImportService(); private final ProcessService processService = ServiceManager.getProcessService(); + private final RulesetService rulesetService = ServiceManager.getRulesetService(); + + private RulesetManagementInterface rulesetManagement; /** * The default constructor looks up the queue name to use in @@ -53,23 +83,57 @@ public CreateNewProcessesProcessor() { protected void process(MapMessageObjectReader ticket) throws ProcessorException, JMSException { try { CreateNewProcessOrder order = new CreateNewProcessOrder(ticket); - - // init - ProcessGenerator processGenerator = new ProcessGenerator(); - boolean generated = processGenerator.generateProcess(order.getTemplateId(), order.getProjectId()); - if (!generated) { - throw new ProcessGenerationException("Process was not generated"); - } - Workpiece workpiece = new Workpiece(); - TempProcess tp = new TempProcess(processGenerator.getGeneratedProcess(), workpiece); - - if (order.getImports().isEmpty()) { - // create process without import + rulesetManagement = rulesetService.openRuleset(order.getTemplate().getRuleset()); + TempProcess tempProcess; + if (!order.getImports().isEmpty()) { + tempProcess = importProcess(order, 0); + tempProcess.getWorkpiece().getLogicalStructure().getMetadata().addAll(order.getMetadata()); + tempProcess.verifyDocType(); + for (int which = 1; which < order.getImports().size(); which++) { + rulesetManagement.updateMetadata(tempProcess.getWorkpiece().getLogicalStructure().getType(), + tempProcess.getWorkpiece().getLogicalStructure().getMetadata(), ACQUISITION_STAGE, + importProcess(order, which).getWorkpiece().getLogicalStructure().getMetadata()); + } } else { - // create process with import + ProcessGenerator processGenerator = new ProcessGenerator(); + processGenerator.generateProcess(order.getTemplateId(), order.getProjectId()); + tempProcess = new TempProcess(processGenerator.getGeneratedProcess(), new Workpiece()); + tempProcess.getWorkpiece().getLogicalStructure().getMetadata().addAll(order.getMetadata()); + tempProcess.verifyDocType(); } - } catch (DAOException /* | DataException */ | ProcessGenerationException e) { + ProcessFieldedMetadata processDetails = ProcessHelper.initializeProcessDetails(tempProcess.getWorkpiece() + .getLogicalStructure(), rulesetManagement, ACQUISITION_STAGE, METADATA_LANGUAGE); + ProcessHelper.generateAtstslFields(tempProcess, processDetails.getRows(), Collections.emptyList(), + tempProcess.getProcess().getBaseType(), rulesetManagement, ACQUISITION_STAGE, METADATA_LANGUAGE, order + .getParent(), true); + if (order.getTitle().isPresent()) { + tempProcess.getProcess().setTitle(order.getTitle().get()); + } + if (!ProcessValidator.isProcessTitleCorrect(tempProcess.getProcess().getTitle())) { + throw new ProcessorException(Helper.getTranslation("processTitleAlreadyInUse", tempProcess.getProcess() + .getTitle())); + } + } catch (DataException | DAOException | InvalidMetadataValueException | IOException | NoRecordFoundException + | NoSuchMetadataFieldException | ParserConfigurationException | ProcessGenerationException + | SAXException | TransformerException | UnsupportedFormatException | URISyntaxException + | XPathExpressionException e) { throw new ProcessorException(e.getMessage()); } } + + 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_DEPTH, 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); + } } From 3019eb24ba5228dcfadf8bfe8ef089d0f13dd227 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Mon, 12 Aug 2024 16:24:39 +0200 Subject: [PATCH 26/43] Use constants for string constants, and add documentation --- .../activemq/CreateNewProcessOrder.java | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) 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 index d3460b515b2..cecf5a8b637 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java @@ -46,6 +46,44 @@ * 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; @@ -74,15 +112,14 @@ public class CreateNewProcessOrder { */ public CreateNewProcessOrder(MapMessageObjectReader ticket) throws DAOException, DataException, JMSException, ProcessorException { - this.projectId = ticket.getMandatoryInteger("project"); - this.templateId = ticket.getMandatoryInteger("template"); - this.imports = convertImports(ticket.getList("import")); - this.title = Optional.ofNullable(ticket.getString("title")); - this.parentId = Optional.ofNullable(convertProcessId(ticket.getString("parent"))); - this.metadata = convertMetadata(ticket.getMapOfString("metadata")); + 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)); } - /** * Converts import details into safe data objects. For {@code null}, it will * return an empty list, never {@code null}. @@ -108,9 +145,9 @@ private static final List> convertImports(@May throw new IllegalArgumentException("Entry of \"imports\" is not a map"); } Map map = (Map) dubious; - ImportConfiguration importconfiguration = importConfigurationService.getById( - MapMessageObjectReader.getMandatoryInteger(map, "importconfiguration")); - String value = MapMessageObjectReader.getMandatoryString(map, "value"); + 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; From 209038a90e94e98916acb1164592a9b3a38efbc3 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Mon, 12 Aug 2024 16:27:39 +0200 Subject: [PATCH 27/43] Fix imports --- .../interfaces/activemq/CreateNewProcessOrder.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 index cecf5a8b637..37128d2aa7c 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java @@ -20,7 +20,6 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; -import java.util.regex.Pattern; import javax.jms.JMSException; @@ -40,7 +39,7 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; -import net.bytebuddy.utility.nullability.MaybeNull; +import edu.umd.cs.findbugs.annotations.Nullable; /** * Order to create a new process. This contains all the necessary data. @@ -131,7 +130,7 @@ public CreateNewProcessOrder(MapMessageObjectReader ticket) throws DAOException, * if the ImportConfiguartionDAO is unable to find an import * configuration with that ID */ - private static final List> convertImports(@MaybeNull List imports) + private static final List> convertImports(@Nullable List imports) throws DAOException { if (Objects.isNull(imports) || imports.isEmpty()) { @@ -190,7 +189,7 @@ private static final Integer convertProcessId(String processId) throws DataExcep * Converts metadata details into safe data objects. For {@code null}, it * will return an empty collection, never {@code null}. */ - private static final HashSet convertMetadata(@MaybeNull Map metadata) { + private static final HashSet convertMetadata(@Nullable Map metadata) { HashSet result = new HashSet<>(); if (Objects.isNull(metadata)) { From c21c7c873561d78b108431178f293605bba395da Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Mon, 12 Aug 2024 16:31:14 +0200 Subject: [PATCH 28/43] Remove unused code and reduce visibility --- .../activemq/CreateNewProcessOrder.java | 29 +++++-------------- .../activemq/CreateNewProcessesProcessor.java | 1 - 2 files changed, 8 insertions(+), 22 deletions(-) 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 index 37128d2aa7c..516c7ecbe0a 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java @@ -109,7 +109,7 @@ public class CreateNewProcessOrder { * @throws ProcessorException * if the process count for the title is not exactly one */ - public CreateNewProcessOrder(MapMessageObjectReader ticket) throws DAOException, DataException, JMSException, + CreateNewProcessOrder(MapMessageObjectReader ticket) throws DAOException, DataException, JMSException, ProcessorException { this.projectId = ticket.getMandatoryInteger(FIELD_PROJECT); this.templateId = ticket.getMandatoryInteger(FIELD_TEMPLATE); @@ -231,7 +231,7 @@ private static final HashSet convertMetadata(@Nullable Map metad * @return the project ID */ @NonNull - public Integer getProjectId() { + Integer getProjectId() { return projectId; } @@ -242,7 +242,7 @@ public Integer getProjectId() { * @throws DAOException * if the template cannot be loaded */ - public Template getTemplate() throws DAOException { + Template getTemplate() throws DAOException { return ServiceManager.getTemplateService().getById(templateId); } @@ -253,7 +253,7 @@ public Template getTemplate() throws DAOException { * @return the template ID */ @NonNull - public Integer getTemplateId() { + Integer getTemplateId() { return templateId; } @@ -266,7 +266,7 @@ public Integer getTemplateId() { * @return import instructions */ @NonNull - public List> getImports() { + List> getImports() { return imports; } @@ -278,7 +278,7 @@ public List> getImports() { * @return the title, if any */ @NonNull - public Optional getTitle() { + Optional getTitle() { return title; } @@ -290,23 +290,10 @@ public Optional getTitle() { * if the process cannot be loaded */ @CheckForNull - public Process getParent() throws DAOException { + Process getParent() throws DAOException { return parentId.isPresent() ? ServiceManager.getProcessService().getById(parentId.get()) : null; } - /** - * Returns the (optional) parent record ID. If set, the process to be - * created must be created as the new last child under this parent process. - * Otherwise, a standalone process is created. Can be - * {@code Optional.empty()}, but never {@code null}. - * - * @return the title, if any - */ - @NonNull - public Optional getParentId() { - return parentId; - } - /** * Specifies the metadata for the logical structure root of the process to * be created. Can be empty, but never {@code null}. @@ -314,7 +301,7 @@ public Optional getParentId() { * @return */ @NonNull - public Collection getMetadata() { + 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 index 64364753abf..267c4e5935d 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -61,7 +61,6 @@ public class CreateNewProcessesProcessor extends ActiveMQProcessor { private static final List METADATA_LANGUAGE = Locale.LanguageRange.parse("en"); private final ImportService importService = ServiceManager.getImportService(); - private final ProcessService processService = ServiceManager.getProcessService(); private final RulesetService rulesetService = ServiceManager.getRulesetService(); private RulesetManagementInterface rulesetManagement; From c01f50fc8e26f6a5bbe51131802e42e221fce2c1 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Mon, 12 Aug 2024 16:39:14 +0200 Subject: [PATCH 29/43] Clean up, and document --- .../activemq/CreateNewProcessesProcessor.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 index 267c4e5935d..7e39361ceb4 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -46,7 +46,6 @@ 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.xml.sax.SAXException; @@ -120,6 +119,15 @@ protected void process(MapMessageObjectReader ticket) throws ProcessorException, } } + /** + * 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, From 8341f6dfb3cf0024c6b6abcbed927ada8b6aec25 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Mon, 12 Aug 2024 16:59:49 +0200 Subject: [PATCH 30/43] Add saving new process --- .../activemq/CreateNewProcessesProcessor.java | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) 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 index 7e39361ceb4..54cb59a1408 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -16,6 +16,7 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Locale.LanguageRange; import javax.jms.JMSException; @@ -30,8 +31,10 @@ 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.exceptions.DAOException; 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; @@ -42,11 +45,14 @@ 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.file.FileService; import org.xml.sax.SAXException; /** @@ -57,9 +63,12 @@ public class CreateNewProcessesProcessor extends ActiveMQProcessor { private static final String ACQUISITION_STAGE = "create"; private static final int IMPORT_DEPTH = 1; + private static final String LAST_CHILD = Integer.toString(Integer.MAX_VALUE); private static final List METADATA_LANGUAGE = Locale.LanguageRange.parse("en"); + private final FileService fileService = ServiceManager.getFileService(); private final ImportService importService = ServiceManager.getImportService(); + private final ProcessService processService = ServiceManager.getProcessService(); private final RulesetService rulesetService = ServiceManager.getRulesetService(); private RulesetManagementInterface rulesetManagement; @@ -101,20 +110,29 @@ protected void process(MapMessageObjectReader ticket) throws ProcessorException, } ProcessFieldedMetadata processDetails = ProcessHelper.initializeProcessDetails(tempProcess.getWorkpiece() .getLogicalStructure(), rulesetManagement, ACQUISITION_STAGE, METADATA_LANGUAGE); + Process process = tempProcess.getProcess(); + Process parent = order.getParent(); ProcessHelper.generateAtstslFields(tempProcess, processDetails.getRows(), Collections.emptyList(), - tempProcess.getProcess().getBaseType(), rulesetManagement, ACQUISITION_STAGE, METADATA_LANGUAGE, order - .getParent(), true); + process.getBaseType(), rulesetManagement, ACQUISITION_STAGE, METADATA_LANGUAGE, parent, true); if (order.getTitle().isPresent()) { - tempProcess.getProcess().setTitle(order.getTitle().get()); + process.setTitle(order.getTitle().get()); } - if (!ProcessValidator.isProcessTitleCorrect(tempProcess.getProcess().getTitle())) { - throw new ProcessorException(Helper.getTranslation("processTitleAlreadyInUse", tempProcess.getProcess() - .getTitle())); + if (!ProcessValidator.isProcessTitleCorrect(process.getTitle())) { + throw new ProcessorException(Helper.getTranslation("processTitleAlreadyInUse", process.getTitle())); } - } catch (DataException | DAOException | InvalidMetadataValueException | IOException | NoRecordFoundException - | NoSuchMetadataFieldException | ParserConfigurationException | ProcessGenerationException - | SAXException | TransformerException | UnsupportedFormatException | URISyntaxException - | XPathExpressionException e) { + processService.save(process); + fileService.createProcessLocation(process); + if (Objects.nonNull(parent)) { + MetadataEditor.addLink(parent, LAST_CHILD, process.getId()); + parent.getChildren().add(process); + process.setParent(parent); + processService.save(process); + processService.save(parent); + } + } catch (CommandException | DataException | DAOException | InvalidMetadataValueException | IOException + | NoRecordFoundException | NoSuchMetadataFieldException | ParserConfigurationException + | ProcessGenerationException | SAXException | TransformerException | UnsupportedFormatException + | URISyntaxException | XPathExpressionException e) { throw new ProcessorException(e.getMessage()); } } From 3689ce5c29d1bb60752ced3f7f41f01ee13a5428 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Tue, 13 Aug 2024 11:28:05 +0200 Subject: [PATCH 31/43] Register listener --- .../production/interfaces/activemq/ActiveMQDirector.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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; From fb840719f45e19220f307a1ae8eb352846aa3724 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Tue, 13 Aug 2024 11:28:33 +0200 Subject: [PATCH 32/43] Fix exception if current type is null --- .../src/main/java/org/kitodo/production/helper/TempProcess.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } } From 9ac51555875a6901db515234327edd198343222f Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Tue, 13 Aug 2024 11:28:57 +0200 Subject: [PATCH 33/43] Rename constants --- .../activemq/CreateNewProcessesProcessor.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) 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 index 54cb59a1408..06f5b2aa716 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -61,8 +61,8 @@ public class CreateNewProcessesProcessor extends ActiveMQProcessor { private static final Logger logger = LogManager.getLogger(CreateNewProcessesProcessor.class); - private static final String ACQUISITION_STAGE = "create"; - private static final int IMPORT_DEPTH = 1; + 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(Integer.MAX_VALUE); private static final List METADATA_LANGUAGE = Locale.LanguageRange.parse("en"); @@ -98,7 +98,8 @@ protected void process(MapMessageObjectReader ticket) throws ProcessorException, tempProcess.verifyDocType(); for (int which = 1; which < order.getImports().size(); which++) { rulesetManagement.updateMetadata(tempProcess.getWorkpiece().getLogicalStructure().getType(), - tempProcess.getWorkpiece().getLogicalStructure().getMetadata(), ACQUISITION_STAGE, + tempProcess.getWorkpiece().getLogicalStructure().getMetadata(), + ACQUISITION_STAGE_PROCESS_CREATION, importProcess(order, which).getWorkpiece().getLogicalStructure().getMetadata()); } } else { @@ -109,11 +110,12 @@ protected void process(MapMessageObjectReader ticket) throws ProcessorException, tempProcess.verifyDocType(); } ProcessFieldedMetadata processDetails = ProcessHelper.initializeProcessDetails(tempProcess.getWorkpiece() - .getLogicalStructure(), rulesetManagement, ACQUISITION_STAGE, METADATA_LANGUAGE); + .getLogicalStructure(), rulesetManagement, ACQUISITION_STAGE_PROCESS_CREATION, METADATA_LANGUAGE); Process process = tempProcess.getProcess(); Process parent = order.getParent(); ProcessHelper.generateAtstslFields(tempProcess, processDetails.getRows(), Collections.emptyList(), - process.getBaseType(), rulesetManagement, ACQUISITION_STAGE, METADATA_LANGUAGE, parent, true); + process.getBaseType(), rulesetManagement, ACQUISITION_STAGE_PROCESS_CREATION, METADATA_LANGUAGE, parent, + true); if (order.getTitle().isPresent()) { process.setTitle(order.getTitle().get()); } @@ -151,9 +153,10 @@ private TempProcess importProcess(CreateNewProcessOrder order, int which) throws 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_DEPTH, rulesetManagement.getFunctionalKeys(FunctionalMetadata.HIGHERLEVEL_IDENTIFIER)); + 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) { From f9dffca19d8db6be4fd0821986a66fddd12aa2bd Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Tue, 13 Aug 2024 13:28:00 +0200 Subject: [PATCH 34/43] Generate correctly --- .../interfaces/activemq/CreateNewProcessesProcessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 06f5b2aa716..535fc3fa6ae 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -114,8 +114,8 @@ protected void process(MapMessageObjectReader ticket) throws ProcessorException, Process process = tempProcess.getProcess(); Process parent = order.getParent(); ProcessHelper.generateAtstslFields(tempProcess, processDetails.getRows(), Collections.emptyList(), - process.getBaseType(), rulesetManagement, ACQUISITION_STAGE_PROCESS_CREATION, METADATA_LANGUAGE, parent, - true); + tempProcess.getWorkpiece().getLogicalStructure().getType(), rulesetManagement, + ACQUISITION_STAGE_PROCESS_CREATION, METADATA_LANGUAGE, parent, true); if (order.getTitle().isPresent()) { process.setTitle(order.getTitle().get()); } From 198046b0474671fc322be0d954be8065815459c8 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Tue, 13 Aug 2024 13:36:40 +0200 Subject: [PATCH 35/43] Save correctly --- .../activemq/CreateNewProcessesProcessor.java | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) 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 index 535fc3fa6ae..a1462a3fb66 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -16,8 +16,9 @@ import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.Locale.LanguageRange; +import java.util.Objects; +import java.util.Set; import javax.jms.JMSException; import javax.xml.parsers.ParserConfigurationException; @@ -26,13 +27,16 @@ 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; @@ -52,6 +56,8 @@ 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; @@ -63,13 +69,15 @@ public class CreateNewProcessesProcessor extends ActiveMQProcessor { 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(Integer.MAX_VALUE); + 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; @@ -97,10 +105,11 @@ protected void process(MapMessageObjectReader ticket) throws ProcessorException, 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, - importProcess(order, which).getWorkpiece().getLogicalStructure().getMetadata()); + ACQUISITION_STAGE_PROCESS_CREATION, metadata); } } else { ProcessGenerator processGenerator = new ProcessGenerator(); @@ -109,32 +118,33 @@ protected void process(MapMessageObjectReader ticket) throws ProcessorException, tempProcess.getWorkpiece().getLogicalStructure().getMetadata().addAll(order.getMetadata()); tempProcess.verifyDocType(); } + Process process = tempProcess.getProcess(); ProcessFieldedMetadata processDetails = ProcessHelper.initializeProcessDetails(tempProcess.getWorkpiece() .getLogicalStructure(), rulesetManagement, ACQUISITION_STAGE_PROCESS_CREATION, METADATA_LANGUAGE); - Process process = tempProcess.getProcess(); - Process parent = order.getParent(); + Process parentProcess = order.getParent(); ProcessHelper.generateAtstslFields(tempProcess, processDetails.getRows(), Collections.emptyList(), tempProcess.getWorkpiece().getLogicalStructure().getType(), rulesetManagement, - ACQUISITION_STAGE_PROCESS_CREATION, METADATA_LANGUAGE, parent, true); + 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())); } - processService.save(process); + saveProcess(process); fileService.createProcessLocation(process); - if (Objects.nonNull(parent)) { - MetadataEditor.addLink(parent, LAST_CHILD, process.getId()); - parent.getChildren().add(process); - process.setParent(parent); - processService.save(process); - processService.save(parent); + 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); } - } catch (CommandException | DataException | DAOException | InvalidMetadataValueException | IOException - | NoRecordFoundException | NoSuchMetadataFieldException | ParserConfigurationException - | ProcessGenerationException | SAXException | TransformerException | UnsupportedFormatException - | URISyntaxException | XPathExpressionException e) { + } catch (CommandException | CustomResponseException | DataException | DAOException + | InvalidMetadataValueException | IOException | NoRecordFoundException | NoSuchMetadataFieldException + | ParserConfigurationException | ProcessGenerationException | SAXException | TransformerException + | UnsupportedFormatException | URISyntaxException | XPathExpressionException e) { throw new ProcessorException(e.getMessage()); } } @@ -164,4 +174,17 @@ private TempProcess importProcess(CreateNewProcessOrder order, int which) throws } return processHierarchy.get(0); } + + /** + * 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); + } + } } From c9b8d63f3cdb8ae0d5c767fd8ae3e6bbc0e471e8 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Fri, 16 Aug 2024 10:23:24 +0200 Subject: [PATCH 36/43] Fix Checkstyle (sizes) MethodLength: Method process length Method process length is 53 lines (max allowed is 50). --- .../activemq/CreateNewProcessesProcessor.java | 118 +++++++++++------- 1 file changed, 74 insertions(+), 44 deletions(-) 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 index a1462a3fb66..87a7d2b10cf 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessesProcessor.java @@ -91,56 +91,18 @@ public CreateNewProcessesProcessor() { super(ConfigCore.getOptionalString(ParameterCore.ACTIVE_MQ_CREATE_NEW_PROCESSES_QUEUE).orElse(null)); } - /* - * The main routine processing incoming tickets. - */ + /* 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; - if (!order.getImports().isEmpty()) { - 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); - } - } else { - ProcessGenerator processGenerator = new ProcessGenerator(); - processGenerator.generateProcess(order.getTemplateId(), order.getProjectId()); - tempProcess = new TempProcess(processGenerator.getGeneratedProcess(), new Workpiece()); - tempProcess.getWorkpiece().getLogicalStructure().getMetadata().addAll(order.getMetadata()); - tempProcess.verifyDocType(); - } + TempProcess tempProcess = obtainTempProcess(order); Process process = tempProcess.getProcess(); - 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())); - } - 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); - } + Process parentProcess = formProcessTitle(order, tempProcess, process); + createProcess(tempProcess, process, parentProcess); + } catch (CommandException | CustomResponseException | DataException | DAOException | InvalidMetadataValueException | IOException | NoRecordFoundException | NoSuchMetadataFieldException | ParserConfigurationException | ProcessGenerationException | SAXException | TransformerException @@ -149,6 +111,37 @@ protected void process(MapMessageObjectReader ticket) throws ProcessorException, } } + /* 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. * @@ -175,6 +168,43 @@ private TempProcess importProcess(CreateNewProcessOrder order, int which) throws 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. * From f56fb5b66c74b101777aeba01bf13aa92a271c0a Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Fri, 16 Aug 2024 10:37:28 +0200 Subject: [PATCH 37/43] Fix Checkstyle (javadoc) --- .../activemq/CreateNewProcessOrder.java | 10 ++--- .../activemq/MapMessageObjectReader.java | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) 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 index 516c7ecbe0a..558e8621248 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java @@ -11,6 +11,10 @@ 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; @@ -37,10 +41,6 @@ import org.kitodo.production.services.ServiceManager; import org.kitodo.production.services.data.ImportConfigurationService; -import edu.umd.cs.findbugs.annotations.CheckForNull; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; - /** * Order to create a new process. This contains all the necessary data. */ @@ -298,7 +298,7 @@ Process getParent() throws DAOException { * Specifies the metadata for the logical structure root of the process to * be created. Can be empty, but never {@code null}. * - * @return + * @return the metadata */ @NonNull Collection getMetadata() { 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 b5fa1ece007..0d596f7f69b 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 @@ -300,6 +300,19 @@ 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); @@ -312,6 +325,19 @@ public List getList(String key) throws JMSException { 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); @@ -324,6 +350,20 @@ public Integer getInteger(String key) throws JMSException { 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<>(); From 8044732f32be3a0b773ad17ec491e511e42af733 Mon Sep 17 00:00:00 2001 From: Matthias Ronge Date: Wed, 2 Oct 2024 12:13:28 +0200 Subject: [PATCH 38/43] Fix checkstyle --- .../activemq/CreateNewProcessOrder.java | 9 ++-- .../activemq/MapMessageObjectReader.java | 52 +++++++++---------- 2 files changed, 32 insertions(+), 29 deletions(-) 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 index 558e8621248..73eace87ad3 100644 --- a/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java +++ b/Kitodo/src/main/java/org/kitodo/production/interfaces/activemq/CreateNewProcessOrder.java @@ -28,6 +28,7 @@ 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; @@ -116,7 +117,7 @@ public class CreateNewProcessOrder { 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)); + this.metadata = convertMetadata(ticket.getMapOfString(FIELD_METADATA), MdSec.DMD_SEC); } /** @@ -189,7 +190,7 @@ private static final Integer convertProcessId(String processId) throws DataExcep * 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) { + private static final HashSet convertMetadata(@Nullable Map metadata, @Nullable MdSec domain) { HashSet result = new HashSet<>(); if (Objects.isNull(metadata)) { @@ -211,12 +212,14 @@ private static final HashSet convertMetadata(@Nullable Map metad if (dubiousValue instanceof Map) { MetadataGroup metadataGroup = new MetadataGroup(); metadataGroup.setKey(key); - metadataGroup.setMetadata(convertMetadata((Map) dubiousValue)); + 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); } } 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 0d596f7f69b..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 @@ -118,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 @@ -163,32 +189,6 @@ public Collection getCollectionOfString(String key) throws JMSException .collect(Collectors.toList()); } - /** - * 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 String from a MapMessage. This is an access forward to the * native function of the MapMessage. You may consider to use From 0d100bcb0ba7e9e955fb5bab8ff40a5b9765efa3 Mon Sep 17 00:00:00 2001 From: Arved Solth Date: Thu, 7 Nov 2024 08:56:47 +0100 Subject: [PATCH 39/43] Add option to set default client for each user --- .../org/kitodo/data/database/beans/User.java | 23 +++++++++ .../V2_136__Add_default_client_to_user.sql | 13 +++++ .../controller/SessionClientController.java | 17 ++++++- .../org/kitodo/production/forms/UserForm.java | 3 ++ .../security/CustomLoginSuccessHandler.java | 3 +- .../resources/messages/messages_de.properties | 1 + .../resources/messages/messages_en.properties | 1 + .../templates/includes/userEdit/details.xhtml | 17 +++++++ .../java/org/kitodo/selenium/LoginST.java | 50 +++++++++++++++---- 9 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 Kitodo-DataManagement/src/main/resources/db/migration/V2_136__Add_default_client_to_user.sql diff --git a/Kitodo-DataManagement/src/main/java/org/kitodo/data/database/beans/User.java b/Kitodo-DataManagement/src/main/java/org/kitodo/data/database/beans/User.java index 2d3535e8a56..f9b38d731f0 100644 --- a/Kitodo-DataManagement/src/main/java/org/kitodo/data/database/beans/User.java +++ b/Kitodo-DataManagement/src/main/java/org/kitodo/data/database/beans/User.java @@ -117,6 +117,10 @@ public class User extends BaseBean { @Column(name = "show_physical_page_number_below_thumbnail") private boolean showPhysicalPageNumberBelowThumbnail; + @ManyToOne + @JoinColumn(name = "default_client_id", foreignKey = @ForeignKey(name = "FK_user_default_client_id")) + private Client defaultClient; + /** * Constructor for User Entity. */ @@ -160,6 +164,7 @@ public User(User user) { this.projects = Objects.isNull(user.projects) ? new ArrayList<>() : user.projects; this.clients = Objects.isNull(user.clients) ? new ArrayList<>() : user.clients; this.filters = Objects.isNull(user.filters) ? new ArrayList<>() : user.filters; + this.defaultClient = Objects.isNull(user.defaultClient) ? null : user.defaultClient; if (Objects.nonNull(user.tableSize)) { this.tableSize = user.tableSize; @@ -517,6 +522,24 @@ public void setShowPhysicalPageNumberBelowThumbnail(boolean showPhysicalPageNumb this.showPhysicalPageNumberBelowThumbnail = showPhysicalPageNumberBelowThumbnail; } + /** + * Get default client. + * + * @return default client + */ + public Client getDefaultClient() { + return defaultClient; + } + + /** + * Set default client. + * + * @param defaultClient default client + */ + public void setDefaultClient(Client defaultClient) { + this.defaultClient = defaultClient; + } + /** * Removes a user from the environment. Since the * user ID may still be referenced somewhere, the user is not hard deleted from diff --git a/Kitodo-DataManagement/src/main/resources/db/migration/V2_136__Add_default_client_to_user.sql b/Kitodo-DataManagement/src/main/resources/db/migration/V2_136__Add_default_client_to_user.sql new file mode 100644 index 00000000000..1a9e0faf855 --- /dev/null +++ b/Kitodo-DataManagement/src/main/resources/db/migration/V2_136__Add_default_client_to_user.sql @@ -0,0 +1,13 @@ +-- +-- (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. +-- + +-- Add column "default_client_id" to "user" table +ALTER TABLE user ADD default_client_id INT; diff --git a/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java b/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java index 9db615e1cc4..6d0fa97d36c 100644 --- a/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java +++ b/Kitodo/src/main/java/org/kitodo/production/controller/SessionClientController.java @@ -80,10 +80,14 @@ public boolean shouldUserChangeSessionClient() { * Display client selection dialog if user is logged in and has multiple clients. */ public void showClientSelectDialog() { - if (Objects.isNull(getCurrentSessionClient()) && !userHasOnlyOneClient()) { - PrimeFaces.current().executeScript("PF('selectClientDialog').show();"); + User currentUser = ServiceManager.getUserService().getCurrentUser(); + Client defaultClient = currentUser.getDefaultClient(); + if (Objects.nonNull(defaultClient)) { + setSessionClient(defaultClient); } else if (userHasOnlyOneClient()) { setSessionClient(getFirstClientOfCurrentUser()); + } else if (Objects.isNull(getCurrentSessionClient()) && !userHasOnlyOneClient()) { + PrimeFaces.current().executeScript("PF('selectClientDialog').show();"); } } @@ -162,6 +166,15 @@ public List getAvailableClientsOfCurrentUser() { return clients; } + /** + * Get default client of current user. + * + * @return default client of current user + */ + public Client getDefaultClientOfCurrentUser() { + return ServiceManager.getUserService().getCurrentUser().getDefaultClient(); + } + /** * Get list of available clients of current user sorted by name. * @return list of available clients of current user sorted by name diff --git a/Kitodo/src/main/java/org/kitodo/production/forms/UserForm.java b/Kitodo/src/main/java/org/kitodo/production/forms/UserForm.java index 570c5fe4c98..55c4c3c2cba 100644 --- a/Kitodo/src/main/java/org/kitodo/production/forms/UserForm.java +++ b/Kitodo/src/main/java/org/kitodo/production/forms/UserForm.java @@ -364,6 +364,9 @@ public String deleteFromClient() { for (Client client : this.userObject.getClients()) { if (client.getId().equals(clientId)) { this.userObject.getClients().remove(client); + if (client.equals(this.userObject.getDefaultClient())) { + this.userObject.setDefaultClient(null); + } break; } } diff --git a/Kitodo/src/main/java/org/kitodo/production/security/CustomLoginSuccessHandler.java b/Kitodo/src/main/java/org/kitodo/production/security/CustomLoginSuccessHandler.java index c8f8dfbf6e3..0ad26640737 100644 --- a/Kitodo/src/main/java/org/kitodo/production/security/CustomLoginSuccessHandler.java +++ b/Kitodo/src/main/java/org/kitodo/production/security/CustomLoginSuccessHandler.java @@ -48,7 +48,8 @@ public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpS try { SessionClientController controller = new SessionClientController(); if (ServiceManager.getIndexingService().isIndexCorrupted() - || controller.getAvailableClientsOfCurrentUser().size() > 1) { + || (controller.getAvailableClientsOfCurrentUser().size() > 1 + && Objects.isNull(controller.getDefaultClientOfCurrentUser()))) { // redirect to empty landing page, where dialogs are displayed depending on both checks! redirectStrategy.sendRedirect(httpServletRequest, httpServletResponse, EMPTY_LANDING_PAGE); } else { diff --git a/Kitodo/src/main/resources/messages/messages_de.properties b/Kitodo/src/main/resources/messages/messages_de.properties index 5cb752b8328..e63c9f3794e 100644 --- a/Kitodo/src/main/resources/messages/messages_de.properties +++ b/Kitodo/src/main/resources/messages/messages_de.properties @@ -363,6 +363,7 @@ databaseStatistic=Datenbankstatistik day=Tag days=Tage deactivatedTemplates=Deaktivierte Produktionsvorlagen +defaultClient=Standardmandant defaults=Vorgaben delete=L\u00F6schen deleteAfterMove=Nach dem Export l\u00F6schen diff --git a/Kitodo/src/main/resources/messages/messages_en.properties b/Kitodo/src/main/resources/messages/messages_en.properties index 426bc1b48ec..9f50b60b7b2 100644 --- a/Kitodo/src/main/resources/messages/messages_en.properties +++ b/Kitodo/src/main/resources/messages/messages_en.properties @@ -363,6 +363,7 @@ databaseStatistic=Database statistics day=day days=days deactivatedTemplates=Deactivated templates +defaultClient=Default client defaults=Defaults delete=Delete deleteAfterMove=Delete after export diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/userEdit/details.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/userEdit/details.xhtml index 1086018ee82..b1a4d70d307 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/userEdit/details.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/userEdit/details.xhtml @@ -123,6 +123,23 @@ onchange="toggleSave()" required="#{empty param['editForm:saveButtonToggler']}" redisplay="true"/> +
+ + + + + + +
diff --git a/Kitodo/src/test/java/org/kitodo/selenium/LoginST.java b/Kitodo/src/test/java/org/kitodo/selenium/LoginST.java index 641fa5bff81..c34f076a11c 100644 --- a/Kitodo/src/test/java/org/kitodo/selenium/LoginST.java +++ b/Kitodo/src/test/java/org/kitodo/selenium/LoginST.java @@ -11,27 +11,26 @@ package org.kitodo.selenium; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.kitodo.data.elasticsearch.exceptions.CustomResponseException; -import org.kitodo.data.exceptions.DataException; +import org.kitodo.data.database.beans.Client; +import org.kitodo.data.database.beans.Process; +import org.kitodo.data.database.beans.User; import org.kitodo.production.services.ServiceManager; import org.kitodo.selenium.testframework.BaseTestSelenium; import org.kitodo.selenium.testframework.Browser; import org.kitodo.selenium.testframework.Pages; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + public class LoginST extends BaseTestSelenium { - @BeforeAll - public static void manipulateIndex() throws DataException, CustomResponseException { + @Test + public void indexWarningTest() throws Exception { // remove one process from index but not from DB to provoke index warning ServiceManager.getProcessService().removeFromIndex(1, true); - } - @Test - public void indexWarningTest() throws Exception { // log into Kitodo with non-admin user to be redirected to 'checks' page Pages.getLoginPage().goTo().performLogin(ServiceManager.getUserService().getById(2)); assertEquals("http://localhost:8080/kitodo/pages/checks.jsf", Browser.getCurrentUrl()); @@ -41,5 +40,36 @@ public void indexWarningTest() throws Exception { Pages.getLoginPage().goTo().performLoginAsAdmin(); assertEquals("http://localhost:8080/kitodo/pages/system.jsf?tabIndex=2", Browser.getCurrentUrl()); Pages.getTopNavigation().logout(); + + // restore deleted process to index + Process unindexedProcess = ServiceManager.getProcessService().getById(1); + ServiceManager.getProcessService().addAllObjectsToIndex(Collections.singletonList(unindexedProcess)); + } + + @Test + public void defaultClientTest() throws Exception { + User userNowak = ServiceManager.getUserService().getByLogin("nowak"); + assertTrue(userNowak.getClients().size() > 1, "Test user should have more than one client"); + Client defaultClient = userNowak.getDefaultClient(); + assertNull(defaultClient, "Default client should be null."); + + Pages.getLoginPage().goTo().performLogin(userNowak); + assertEquals("http://localhost:8080/kitodo/pages/checks.jsf", Browser.getCurrentUrl(), + "User with multiple clients but no default client should get redirected to 'checks' page."); + Pages.getTopNavigation().cancelClientSelection(); + + // set default client of user + Client firstClient = ServiceManager.getClientService().getById(1); + userNowak.setDefaultClient(firstClient); + ServiceManager.getUserService().saveToDatabase(userNowak); + + Pages.getLoginPage().goTo().performLogin(userNowak); + assertEquals("http://localhost:8080/kitodo/pages/desktop.jsf", Browser.getCurrentUrl(), + "User with default client should get redirected to 'desktop' page."); + Pages.getTopNavigation().logout(); + + // restore users original settings + userNowak.setDefaultClient(null); + ServiceManager.getUserService().saveToDatabase(userNowak); } } From 4b405394c6de481bd71f4d1150a56dc8ec002c67 Mon Sep 17 00:00:00 2001 From: Arved Solth Date: Fri, 8 Nov 2024 15:43:54 +0100 Subject: [PATCH 40/43] Hide default client selection in user settings if user has only one client to choose from --- .../WEB-INF/templates/includes/userEdit/details.xhtml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/userEdit/details.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/userEdit/details.xhtml index b1a4d70d307..d191394e069 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/userEdit/details.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/userEdit/details.xhtml @@ -123,7 +123,9 @@ onchange="toggleSave()" required="#{empty param['editForm:saveButtonToggler']}" redisplay="true"/> -
+ @@ -139,7 +141,7 @@ -
+
From 895956eccb974ffc3c6851ee69b900966a18250d Mon Sep 17 00:00:00 2001 From: Danilo Penagos Jaramillo <67697709+danilopenagos@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:19:48 +0100 Subject: [PATCH 41/43] Update messages_es.properties Add missing messages and translating the concept "media" as "archivo multimedia" --- .../resources/messages/messages_es.properties | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/Kitodo/src/main/resources/messages/messages_es.properties b/Kitodo/src/main/resources/messages/messages_es.properties index 3ae2d9de152..1cb5b184f8d 100644 --- a/Kitodo/src/main/resources/messages/messages_es.properties +++ b/Kitodo/src/main/resources/messages/messages_es.properties @@ -54,7 +54,7 @@ archived=Archivado ARCHIVED=Archivado assigned=Asignado assignedTo=En proceso por -assignMedia=y asignar los medios seleccionados +assignMedia=y asignar los archivos multimedia seleccionados assignOcrdWorkflow=Asignar al flujo de trabajo OCR-D assignToNextElement=Enlace al siguiente artículo ats=ATS @@ -75,7 +75,7 @@ batchStep=Paso de lote batches=Lotes batchesForMigration=Seleccione los lotes para la migración edit=Editar -editMediaPartial=Editar medios parciales +editMediaPartial=Editar archivos multimedia parciales releaseTask=Devuelve el procesamiento de esta tarea reallyReleaseTask=¿Realmente quiere enviar el procesamiento de esta tarea y volver a poner el paso en 'abierto'? selectPlease=Por favor, seleccione @@ -298,7 +298,7 @@ dataEditor.comment.type.individual=individual dataEditor.comment.type.organization=organización dataEditor.copy={0} Elementos en el portapapeles dataEditor.dragNDropError=El elemento de tipo „{0}“ no se ha podido mover al elemento de tipo „{1}“. -dataEditor.dragNDropLinkError=Los elementos y los medios no pueden moverse entre diferentes operaciones +dataEditor.dragNDropLinkError=Los elementos y los archivos multimedia no pueden moverse entre diferentes operaciones dataEditor.fileReferencesChanged=Las referencias de archivos en la pieza de trabajo se han actualizado debido a archivos de imagen añadidos o eliminados. dataEditor.fileReferencesChangedTitle=Las referencias de los archivos se han cambiado dataEditor.fileReferencesChangedUnsavedWarning=Las referencias de archivos actualizadas no se guardan automáticamente en el archivo meta.xml de este proceso. ¡Haga clic en el botón "Guardar" para guardarlas manualmente! @@ -308,7 +308,7 @@ dataEditor.galleryStructuredView=Vista estructurada dataEditor.galleryDetailView=Vista detallada dataEditor.invalidMetadataValue=„{0}“ no se puede guardar: El valor no es válido. Valor: {1} dataEditor.invalidStructureField=„{0}“ no se puede guardar: No existe tal campo ({1}). -dataEditor.mediaNotFound=No se encontraron los medios, pero al menos un archivo de referencia está presente. La eleminación de los medios faltantes de la pieza de trabajo fue omitida. +dataEditor.mediaNotFound=No se encontraron los archivos multimedia, pero al menos un archivo de referencia está presente. La eleminación de los archivos multimedia faltantes de la pieza de trabajo fue omitida. dataEditor.multipleMetadataTasksText=Tiene varias tareas de edición de metadatos en curso. Por favor, seleccione una de estas tareas. dataEditor.noParentsError=No se ha encontrado el elemento padre de {0}. dataEditor.numberOfScans=Número de imágenes escaneadas @@ -318,7 +318,7 @@ dataEditor.position.asLastChildOfCurrentElement=Como último subelemento del ele dataEditor.position.asParentOfCurrentElement=Como elemento padre del elemento actual dataEditor.position.beforeCurrentElement=Antes del elemento actual dataEditor.position.currentPosition=Posición actual -dataEditor.removeElement.noConsecutivePagesSelected=Los elementos estructurales sólo pueden crearse a partir de medios continuos. +dataEditor.removeElement.noConsecutivePagesSelected=Los elementos estructurales sólo pueden crearse a partir de archivos multimedia continuos. dataEditor.selectMetadataTask=Seleccionar la tarea # please check google translation below and remove comment if translation is acceptable dataEditor.layoutDeletedSuccessfullyTitle=Diseño personalizado eliminado @@ -343,17 +343,17 @@ dataEditor.layoutSavedSuccessfullyTitle=La plantilla actual se guardó correctam dataEditor.layoutSavedSuccessfullyDefaultText=La configuración actual de la columna del editor se ha guardado correctamente como predeterminada. La próxima vez que abra el editor, el ancho de las tres columnas (datos de estructura, metadatos, galería) se cargará de acuerdo con su configuración actual. # please check google translation below and remove comment if translation is acceptable dataEditor.layoutSavedSuccessfullyForTaskText=La configuración actual de la columna del editor se ha guardado correctamente como predeterminada para el tipo de tarea "{0}". La próxima vez que abra el editor para el mismo tipo de tarea, el ancho de las tres columnas (datos de estructura, metadatos, galería) se cargará de acuerdo con su configuración actual. -dataEditor.renamingMediaComplete=El cambio de nombre de los medios ha finalizado +dataEditor.renamingMediaComplete=El cambio de nombre de los archivos multimedia ha finalizado dataEditor.renamingMediaError=Se produjo un error al cambiar el nombre de los archivos multimedia -dataEditor.renamingMediaText=Se ha cambiado el nombre de {0} archivos de medios en {1} carpeta. Por favor, haga clic en el botón 'Guardar' para mantener los cambios en los nombres de archivo. De lo contrario, el cambio de nombre se revertirá al cerrar el editor. +dataEditor.renamingMediaText=Se ha cambiado el nombre de {0} archivos multimedia en {1} carpeta. Por favor, haga clic en el botón 'Guardar' para mantener los cambios en los nombres de archivo. De lo contrario, el cambio de nombre se revertirá al cerrar el editor. dataEditor.structure.customizeDisplay=Personalizar la pantalla dataEditor.structureTree.collapseAll=Ocultar todo dataEditor.structureTree.expandAll=Expandir todo dataEditor.unableToMoveError=El elemento no pueder ser movido dataEditor.undefinedStructure=La división no se conoce en el conjunto de reglas dataEditor.undefinedKey=La clave no se conoce en el conjunto de reglas -dataEditor.unlinkedMediaTree=Medios de comunicación no vinculados -dataEditor.unstructuredMedia=Medios no estructurados +dataEditor.unlinkedMediaTree=Archivos multimedia no vinculados +dataEditor.unstructuredMedia=Archivos multimedia no estructurados dataEditor.validation.state.error=Error de validación dataEditor.validation.state.warning=Aviso de validación dataEditor.validation.state.success=Validación exitosa @@ -371,8 +371,8 @@ delete=Borrar deleteAfterMove=Borrar después de exportar deleteIndex=Eliminar el índice de ElasticSearch deleteLinkHomeDirectory=Eliminar el enlace del directorio principal -deleteMedia=Borrar los medios de comunicación -deleteMediaPartial=Borrar los medios parciales +deleteMedia=Borrar los archivos multimedia +deleteMediaPartial=Borrar los archivos multimedia parciales deleteFromProject=Eliminar el usuario del proyecto deleteProcessChildren=Esta operación tiene {0} subordinaciones, ¿también las desea eliminar? desktop=Escritorio @@ -491,8 +491,8 @@ folderUse.audioMediaView.disabled=Vista multimedia de los audios desactivada folderUse.generatorSource=Utilizar como fuente para generar contenidos folderUse.generatorSource.disabled=Generación de contenido desactivada folderUse.mediaView=Usar para la vista multimedia -folderUse.mediaView.disabled=Vista de medios desactivada -folderUse.audioMediaViewWaveform=Mostrar la forma de onda en la vista de medios para audio +folderUse.mediaView.disabled=Vista de archivos multimedia desactivada +folderUse.audioMediaViewWaveform=Mostrar la forma de onda en la vista de archivos multimedia para audio folderUse.preview=Utilizar como vista previa folderUse.preview.disabled=Vista previa deshabilitada folderUse.videoPreview=Usar como vista previa para videos @@ -505,7 +505,7 @@ fromFirstSelectedPage=De la primera página marcada gallery=Galería generate=Generar generateImages=Genera {0} -generateMediaFailed=La generación de medios recién cargados no se ha completado con éxito. +generateMediaFailed=La generación de archivos multimedia recién cargados no se ha completado con éxito. generateMissingImages=Empezar a generar todas las imágenes que faltan generateMissingImagesStarted=Todas las imágenes que faltan son generadas por el gestor de tareas generatesNewspaperProcessesThread=Generar periódico @@ -739,7 +739,7 @@ massImport=Importación masiva massImport.addRow=Añadir línea massImport.results=Importación masiva - resultados masterpieceProperties=Propiedad de la pieza de trabajo -media=Medios +media=Archivos multimedia medium=Medium mediaUploaded={0} se ha cargado con éxito messageAdd=Añadir mensaje @@ -857,7 +857,7 @@ no=No noExistingWorkflow=Todavía no hay flujo de trabajo en el sistema noLdapGroupAssignedToUser=No hay ningún grupo LDAP asignado al usuario noLdapServerAssignedToLdapGroup=No hay ningún servidor LDAP asignado al grupo LDAP -noMedia=No hay medios de comunicación +noMedia=No hay archivos multimedia none=No normDataRecord=URI noImageFolderConfiguredInProject=No se ha configurado ninguna carpeta de destino para la generación de imágenes en el proyecto. @@ -1176,8 +1176,8 @@ updateType=Tipo de actualización upload=Subir a uploadFile=Carga de archivos uploadImport=Importación de archivos -uploadMedia=Cargar medios de comunicación -uploadMediaCompleted=La carga y generación de nuevos medios se ha completado con éxito. +uploadMedia=Cargar los archivos de multimedia +uploadMediaCompleted=La carga y generación de nuevos archivos multimedia se ha completado con éxito. uploadMediaFileLimit=El número máximo de archivos ha sido superado uriMalformed=El URI tiene un formato incorrecto. url=URL @@ -1198,7 +1198,7 @@ userEdit.metadataEditorSettings.paginateFromFirstPageByDefault = Paginación pre userEdit.metadataEditorSettings.showCommentsByDefault = Mostrar comentarios predeterminados userEdit.metadataEditorSettings.showPaginationByDefault = Mostrar páginación predeterminada # please check google translation below and remove comment if translation is acceptable -userEdit.metadataEditorSettings.showPhysicalPageNumberBelowThumbnail = Mostrar el número de página física debajo de la miniatura +userEdit.metadataEditorSettings.showPhysicalPageNumberBelowThumbnail = Mostrar el número de la página física debajo de la miniatura userInstruction=Instrucciones de uso userInstructionText=Encontrará instrucciones para Kitodo.Production 3.x en el userList=Lista de usuarios @@ -1329,6 +1329,7 @@ renameMediaThread=Cambiar el nombre de los archivos multimedia renameMediaForProcessesConfirmMessage=Los archivos multimedia de {0} procesos se renombrarán según su orden en los procesos individuales. Este cambio no se puede revertir. ¿Desea continuar? runKitodoScript=Ejecutar KitodoScript linkToProcessesOfUnassignedProjects=Enlace a las operaciones no asignadas de los proyectos +useMassImport=Usar importación masiva viewAllAuthorities=Mostrar todos los permisos viewAllBatches=Mostrar todos los lotes From 9d66d6d569afdfeb39f9c5018b37dedbdbba101e Mon Sep 17 00:00:00 2001 From: Thomas Low Date: Thu, 14 Nov 2024 10:01:05 +0000 Subject: [PATCH 42/43] Identify context menu item by class name instead of occurance order to fix selenium test. --- .../includes/metadataEditor/gallery.xhtml | 6 +++--- .../metadataEditor/logicalStructure.xhtml | 6 +++--- .../metadataEditor/physicalStructure.xhtml | 4 ++-- .../java/org/kitodo/selenium/MetadataST.java | 19 +++++-------------- .../pages/MetadataEditorPage.java | 8 +++----- 5 files changed, 16 insertions(+), 27 deletions(-) diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml index 60086fd5895..47b37d05888 100644 --- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml +++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml @@ -206,7 +206,7 @@ DataEditorForm.structurePanel.selectedLogicalNode.type eq StructurePanel.VIEW_NODE_TYPE and mediaProvider.hasPreviewVariant(DataEditorForm.galleryPanel.getGalleryMediaContent(DataEditorForm.galleryPanel.lastSelection.key))}" icon="fa fa-external-link fa-sm" - styleClass="plain" + styleClass="plain viewPageInNewWindow" outcome="externalView" target="_blank"> @@ -216,7 +216,7 @@ @@ -196,7 +196,7 @@ update="dialogEditDocStrucTypeDialog"/> Browser.getDriver().findElement(By.id("logicalTree")).isDisplayed()); - // right click on first tree node representing image 2 - WebElement firstTreeNode = Browser.getDriver().findElement( - By.cssSelector("#logicalTree\\:0_0 .ui-treenode-content") - ); - new Actions(Browser.getDriver()).contextClick(firstTreeNode).build().perform(); - - // wait until menu is visible - await().ignoreExceptions().pollDelay(100, TimeUnit.MILLISECONDS).atMost(5, TimeUnit.SECONDS) - .until(() -> Browser.getDriver().findElement(By.id("contextMenuLogicalTree")).isDisplayed()); + // open context menu for linked page "2" + Pages.getMetadataEditorPage().openContextMenuForStructureTreeNode("0_0"); // click second menu entry to open new tab - Browser.getDriver().findElement(By.cssSelector( - "#contextMenuLogicalTree .ui-menuitem:nth-child(2) .ui-menuitem-link" - )).click(); + Browser.getDriver().findElement(By.cssSelector("#contextMenuLogicalTree .viewPageInNewWindow")).click(); // find handle of new tab window String newWindowHandle = Browser.getDriver().getWindowHandles().stream() diff --git a/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java b/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java index 9bb7819ff62..a87dea1d367 100644 --- a/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java +++ b/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java @@ -251,13 +251,11 @@ public void openContextMenuForStructureTreeNode(String nodeId) { /** * Click on a menu entry in the structure tree context menu. * - * @param menuEntry the menu entry index (starting with 1) + * @param menuItemClassName the class name of the menu entry */ - public void clickStructureTreeContextMenuEntry(int menuEntry) { + public void clickStructureTreeContextMenuEntry(String menuItemClassName) { // click on menu entry - contextMenuLogicalTree.findElement(By.cssSelector( - ".ui-menuitem:nth-child(" + menuEntry + ") .ui-menuitem-link" - )).click(); + contextMenuLogicalTree.findElement(By.className(menuItemClassName)).click(); // wait for context menu to disappear await().ignoreExceptions().pollDelay(100, TimeUnit.MILLISECONDS).atMost(5, TimeUnit.SECONDS) .until(() -> !contextMenuLogicalTree.isDisplayed()); From 13efa88cdb8c3a6f30f94bb62a42cefa1799e33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20St=C3=B6hr?= Date: Wed, 13 Nov 2024 11:23:26 +0100 Subject: [PATCH 43/43] Fix height of metadata accordion to accommodate new header --- .../src/main/webapp/WEB-INF/resources/css/kitodo.css | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css index 301f0e545be..d466666964b 100644 --- a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css +++ b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css @@ -2711,7 +2711,7 @@ Column content #structureAccordion .scroll-wrapper { box-sizing: border-box; - height: calc(100% - 53px); + height: calc(100% - var(--input-height) - 2 * var(--default-half-size)); overflow: hidden scroll; width: 100%; } @@ -2966,7 +2966,6 @@ Column content #metadataAccordion, #metadataAccordion > .ui-accordion-content, -#metadataAccordion\:logicalMetadataWrapperPanel, #metadataAccordion\:physicalMetadataWrapperPanel, #metadataAccordion\:logicalMetadataWrapperPanel_content, #metadataAccordion\:physicalMetadataWrapperPanel_content, @@ -2977,6 +2976,10 @@ Column content height: 100%; } +#metadataAccordion\:logicalMetadataWrapperPanel { + height: calc(100% - var(--input-height) - 2 * var(--default-half-size)); +} + #structureAccordion .ui-accordion-header, #metadataAccordion .ui-accordion-header { display: none; @@ -3001,10 +3004,6 @@ Column content overflow-y: scroll; } -#metadataAccordion\:metadata\:metadataTable table[role='treegrid'] { - margin-bottom: 42px; -} - #metadataAccordion\:metadata\:metadataTable_data, #metadataAccordion\:physicalMetadata\:metadataTable_data, #metadataAccordion\:metadata\:metadataTable_data tr,