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 0c55b5d313f..9e3d3a164f5 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 @@ -1684,7 +1684,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(); @@ -1694,7 +1694,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(); @@ -1703,6 +1703,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. * @@ -1765,28 +1775,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; } @@ -1794,14 +1877,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()); diff --git a/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java b/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java index de1499ada37..118ab2f024b 100644 --- a/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java +++ b/Kitodo/src/test/java/org/kitodo/selenium/MetadataST.java @@ -42,6 +42,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; @@ -57,15 +58,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"; @@ -104,6 +108,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. @@ -119,6 +128,7 @@ public static void prepare() throws DAOException, DataException, IOException { prepareMediaRenamingProcess(); prepareDragNDropProcess(); prepareCreateStructureProcess(); + prepareLinkPageToNextDivision(); } /** @@ -403,6 +413,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()); + } + /** * 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. @@ -487,6 +541,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 131830957e6..9bb7819ff62 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; + @FindBy(id = "imagePreviewForm:previewButton") private WebElement imagePreviewButton; @@ -94,6 +97,10 @@ public boolean isStructureTreeFormVisible() { return structureTreeForm.isDisplayed(); } + public boolean isLogicalTreeVisible() { + return logicalTree.isDisplayed(); + } + /** * Gets numberOfScans. * @@ -227,10 +234,51 @@ public long getNumberOfDisplayedStructureElements() { } /** + * 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(); + } + + /* * Open detail view by clicking on image preview button. */ public void openDetailView() { imagePreviewButton.click(); } - } 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +