diff --git a/Jenkinsfile b/Jenkinsfile index 55abed27..d2c40c39 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,4 +4,16 @@ def repoName = "rups" def dependencyRegex = "itextcore" -automaticJavaBuild(repoName, dependencyRegex, 'jdk-17-openjdk') \ No newline at end of file +automaticJavaBuild(repoName, dependencyRegex) + +pipeline { + agent any + + stages { + stage('test') { + steps { + sh 'Xvfb -ac :99 -screen 0 1280x1024x16 & export DISPLAY=:99' + } + } + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0e858285..2617ccc6 100644 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,6 @@ - 3.2.0 2.1.3 7.2.5 5.9.2 @@ -57,8 +56,11 @@ RUPS 11 11 - 2.22.0 + 3.5.0 + 3.1.0 + 3.1.0 0.8.9 + 0.17.2 5.1.4 @@ -93,6 +95,11 @@ dom4j ${dom4j.version} + + org.uispec4j + uispec4j + 17.0-rc1 + com.itextpdf pdftest @@ -152,7 +159,14 @@ ${maven-surefire-plugin.version} false - @{jacoco.agent.argLine} + + @{jacoco.agent.argLine} + --add-opens=java.desktop/java.awt=ALL-UNNAMED + --add-opens=java.desktop/java.awt.peer=ALL-UNNAMED + --add-opens=java.desktop/sun.awt=ALL-UNNAMED + --add-opens=java.desktop/sun.awt.windows=ALL-UNNAMED + --add-opens=java.desktop/sun.awt.windows.WToolkit=ALL-UNNAMED + @@ -197,8 +211,9 @@ ${maven-bundle-plugin.version} + org.apache.maven.plugins maven-assembly-plugin - ${assembly-plugin.version} + ${maven-assembly-plugin.version} make-assembly @@ -229,7 +244,9 @@ + org.apache.maven.plugins maven-jar-plugin + ${maven-jar-plugin.version} @@ -250,6 +267,7 @@ com.github.siom79.japicmp japicmp-maven-plugin + ${japicmp-maven-plugin.version} true diff --git a/src/main/java/com/itextpdf/rups/RupsLauncher.java b/src/main/java/com/itextpdf/rups/RupsLauncher.java index 56954d90..a947b2ac 100644 --- a/src/main/java/com/itextpdf/rups/RupsLauncher.java +++ b/src/main/java/com/itextpdf/rups/RupsLauncher.java @@ -50,7 +50,6 @@ This file is part of the iText (R) project. * of a PDF file. */ public class RupsLauncher { - /** * Main method. Starts the RUPS application. * diff --git a/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java b/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java index edf8fa41..609a165e 100644 --- a/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java +++ b/src/main/java/com/itextpdf/rups/controller/PdfReaderController.java @@ -57,6 +57,8 @@ This file is part of the iText (R) project. import com.itextpdf.rups.event.NodeAddDictChildEvent; import com.itextpdf.rups.event.NodeDeleteArrayChildEvent; import com.itextpdf.rups.event.NodeDeleteDictChildEvent; +import com.itextpdf.rups.event.NodeUpdateArrayChildEvent; +import com.itextpdf.rups.event.NodeUpdateDictChildEvent; import com.itextpdf.rups.event.OpenPlainTextEvent; import com.itextpdf.rups.event.OpenStructureEvent; import com.itextpdf.rups.event.RupsEvent; @@ -95,6 +97,7 @@ This file is part of the iText (R) project. import javax.swing.event.ChangeListener; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.MutableTreeNode; import javax.swing.tree.TreePath; /** @@ -167,7 +170,9 @@ public class PdfReaderController extends Observable implements Observer { */ public PdfReaderController(TreeSelectionListener treeSelectionListener, PageSelectionListener pageSelectionListener) { + pdfTree = new PdfTree(); + pdfTree.setName("pdfTree"); pdfTree.addTreeSelectionListener(treeSelectionListener); JPopupMenu menu = PdfTreeContextMenu.getPopupMenu(pdfTree); @@ -189,6 +194,7 @@ public PdfReaderController(TreeSelectionListener treeSelectionListener, addObserver(text); navigationTabs = new JTabbedPane(); + navigationTabs.setName("navigationTabs"); final String pagesString = Language.PAGES.getString(); navigationTabs.addTab(pagesString, null, new JScrollPane(pages), pagesString); navigationTabs.addTab(Language.OUTLINES.getString(), null, new JScrollPane(outlines), @@ -219,17 +225,19 @@ public void stateChanged(ChangeEvent e) { } }); - objectPanel = new PdfObjectPanel(); + objectPanel = new PdfObjectPanel(this); addObserver(objectPanel); objectPanel.addObserver(this); streamPane = new SyntaxHighlightedStreamPane(this); addObserver(streamPane); JScrollPane debug = new JScrollPane(DebugView.getInstance().getTextArea()); editorTabs = new JTabbedPane(); + editorTabs.setName("editorTabs"); editorTabs.addTab(Language.STREAM.getString(), null, streamPane, Language.STREAM.getString()); editorTabs.addTab(Language.FORM_XFA.getString(), null, form.getXfaTextArea(), Language.FORM_XFA_LONG_FORM.getString()); editorTabs.addTab(Language.DEBUG_INFO.getString(), null, debug, Language.DEBUG_INFO_DESCRIPTION.getString()); + } /** @@ -343,11 +351,24 @@ public void update(Observable observable, Object obj) { case RupsEvent.NODE_ADD_DICT_CHILD_EVENT: addTreeNodeDictChild(((NodeAddDictChildEvent.Content) event.getContent()).parent, ((NodeAddDictChildEvent.Content) event.getContent()).key, - ((NodeAddDictChildEvent.Content) event.getContent()).index); + ((NodeAddDictChildEvent.Content) event.getContent()).index, + ((NodeAddDictChildEvent.Content) event.getContent()).value); break; case RupsEvent.NODE_ADD_ARRAY_CHILD_EVENT: addTreeNodeArrayChild(((NodeAddArrayChildEvent.Content) event.getContent()).parent, - ((NodeAddArrayChildEvent.Content) event.getContent()).index); + ((NodeAddArrayChildEvent.Content) event.getContent()).index, + ((NodeAddArrayChildEvent.Content) event.getContent()).value); + break; + case RupsEvent.NODE_UPDATE_DICT_CHILD_EVENT: + updateTreeNodeDictChild(((NodeUpdateDictChildEvent.Content) event.getContent()).parent, + ((NodeUpdateDictChildEvent.Content) event.getContent()).key, + ((NodeUpdateDictChildEvent.Content) event.getContent()).index, + ((NodeUpdateDictChildEvent.Content) event.getContent()).value); + break; + case RupsEvent.NODE_UPDATE_ARRAY_CHILD_EVENT: + updateTreeNodeArrayChild(((NodeUpdateArrayChildEvent.Content) event.getContent()).parent, + ((NodeUpdateArrayChildEvent.Content) event.getContent()).index, + ((NodeUpdateArrayChildEvent.Content) event.getContent()).value); break; case RupsEvent.NODE_DELETE_ARRAY_CHILD_EVENT: deleteTreeChild(((NodeDeleteArrayChildEvent.Content) event.getContent()).parent, @@ -466,20 +487,54 @@ public int deleteTreeNodeDictChild(PdfObjectTreeNode parent, PdfName key) { } //Returns index of the added child - public int addTreeNodeDictChild(PdfObjectTreeNode parent, PdfName key, int index) { - PdfObjectTreeNode child = PdfObjectTreeNode.getInstance((PdfDictionary) parent.getPdfObject(), key); + public int addTreeNodeDictChild(PdfObjectTreeNode parent, PdfName key, int index, PdfObject childValue) { + PdfObjectTreeNode child = PdfObjectTreeNode.getInstance(childValue, key); return addTreeNodeChild(parent, child, index); } //Returns index of the added child - public int addTreeNodeArrayChild(PdfObjectTreeNode parent, int index) { - PdfObjectTreeNode child = PdfObjectTreeNode.getInstance(((PdfArray) parent.getPdfObject()).get(index, false)); - return addTreeNodeChild(parent, child, index); + public int addTreeNodeArrayChild(PdfObjectTreeNode parent, int index, PdfObject child) { + PdfArray parentArray = (PdfArray) parent.getPdfObject(); + PdfObjectTreeNode childNode = PdfObjectTreeNode.getInstance(child); + return addTreeNodeChild(parent, childNode, index); + } + + //Returns index of the updated child + public int updateTreeNodeDictChild(PdfObjectTreeNode parent, PdfName key, int index, PdfObject child) { + //PdfObjectTreeNode child = PdfObjectTreeNode.getInstance((PdfDictionary) parent.getPdfObject(), key); + return updateTreeChild(parent, PdfObjectTreeNode.getInstance(child), index); + } + + //Returns index of the updated child + public int updateTreeNodeArrayChild(PdfObjectTreeNode parent, int index, PdfObject child) { + return updateTreeChild(parent, PdfObjectTreeNode.getInstance(child), index); } public int deleteTreeChild(PdfObjectTreeNode parent, int index) { parent.remove(index); - ((DefaultTreeModel) pdfTree.getModel()).reload(parent); + deleteObjectChild(parent.getPdfObject(), index); + updateObject(parent); + updateView(parent); + return index; + } + + //Returns index of the updated child + public int updateTreeChild(PdfObjectTreeNode parent, PdfObjectTreeNode child, int index) { + PdfObjectTreeNode oldChild = (PdfObjectTreeNode) parent.getChildAt(index); + int childrenToTransfer = oldChild.getChildCount(); + if (childrenToTransfer > 0){ + for (int childIndex = 0; childIndex < childrenToTransfer; childIndex++){ + PdfObjectTreeNode childToTransfer = (PdfObjectTreeNode) oldChild.getChildAt(childIndex); + addObjectChild(child.getPdfObject(),childToTransfer.getPdfObject(), child.getKey(), childIndex); + child.add((MutableTreeNode) childToTransfer); + } + } + parent.remove(oldChild); + parent.insert(child, index); + updateObjectChild(parent.getPdfObject(), child.getPdfObject(), index); + nodes.expandNode(child); + updateObject(parent); + updateView(parent); return index; } @@ -487,7 +542,50 @@ public int deleteTreeChild(PdfObjectTreeNode parent, int index) { public int addTreeNodeChild(PdfObjectTreeNode parent, PdfObjectTreeNode child, int index) { parent.insert(child, index); nodes.expandNode(child); - ((DefaultTreeModel) pdfTree.getModel()).reload(parent); + addObjectChild(parent.getPdfObject(), child.getPdfObject(), child.getKey(), index); + updateObject(parent); + updateView(parent); return index; } + + private void updateView(PdfObjectTreeNode parent) { + ((DefaultTreeModel) pdfTree.getModel()).reload(parent); + objectPanel.render(parent,getParser()); + } + + private void updateObject (PdfObjectTreeNode parent){ + if (parent.isDictionary()) { + PdfDictionary parentDict = (PdfDictionary) parent.getPdfObject(); + parentDict.keySet(); + }else if(parent.isArray()){ + PdfArray parentArray = (PdfArray)parent.getPdfObject(); + } + } + + protected void addObjectChild(PdfObject parent, PdfObject child, PdfName key, int index){ + if (parent.isDictionary()) { + PdfDictionary parentDict = ((PdfDictionary) parent); + parentDict.put(key, child); + }else if(parent.isArray()){ + PdfArray parentArray = (PdfArray) parent; + parentArray.add(index, child); + } + } + protected void updateObjectChild(PdfObject parent, PdfObject child, int index){ + if (parent.isDictionary()) { + PdfDictionary parentDict = ((PdfDictionary) parent); + parentDict.put((PdfName) parentDict.keySet().toArray()[index], child); + }else if(parent.isArray()){ + PdfArray parentArray = (PdfArray) parent; + parentArray.set(index, child); + } + } + protected void deleteObjectChild(PdfObject parent, int index){ + if (parent.isDictionary()) { + PdfDictionary parentDict = ((PdfDictionary) parent); + parentDict.remove((PdfName) parentDict.keySet().toArray()[index]); + }else if(parent.isArray()){ + ((PdfArray)parent).remove(index); + } + } } diff --git a/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java b/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java index 410f9f27..59a644a0 100644 --- a/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java +++ b/src/main/java/com/itextpdf/rups/controller/RupsInstanceController.java @@ -141,14 +141,17 @@ public RupsInstanceController(Dimension dimension, JPanel owner) { masterComponent.setDividerSize(2); final JSplitPane content = new JSplitPane(); + content.setName("content"); masterComponent.add(content, JSplitPane.TOP); final JSplitPane info = new JSplitPane(); + info.setName("info"); masterComponent.add(info, JSplitPane.BOTTOM); content.setOrientation(JSplitPane.HORIZONTAL_SPLIT); content.setDividerLocation((int) (dimension.getWidth() * .6)); content.setDividerSize(1); JPanel treePanel = new JPanel(new BorderLayout()); + treePanel.setName("treePanel"); treePanel.add(new JScrollPane(readerController.getPdfTree()), BorderLayout.CENTER); content.add(treePanel, JSplitPane.LEFT); content.add(readerController.getNavigationTabs(), JSplitPane.RIGHT); @@ -157,7 +160,9 @@ public RupsInstanceController(Dimension dimension, JPanel owner) { info.setDividerSize(1); info.add(readerController.getObjectPanel(), JSplitPane.LEFT); final JTabbedPane editorPane = readerController.getEditorTabs(); + editorPane.setName("editorPane"); final JScrollPane cons = new JScrollPane(console.getTextArea()); + cons.setName("console"); console.getTextArea().addMouseListener( new ContextMenuMouseListener(ConsoleContextMenu.getPopupMenu(console.getTextArea()), console.getTextArea())); diff --git a/src/main/java/com/itextpdf/rups/event/NodeUpdateArrayChildEvent.java b/src/main/java/com/itextpdf/rups/event/NodeUpdateArrayChildEvent.java new file mode 100644 index 00000000..e6b4b419 --- /dev/null +++ b/src/main/java/com/itextpdf/rups/event/NodeUpdateArrayChildEvent.java @@ -0,0 +1,78 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2022 iText Group NV + Authors: iText Software. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License version 3 + as published by the Free Software Foundation with the addition of the + following permission added to Section 15 as permitted in Section 7(a): + FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY + ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT + OF THIRD PARTY RIGHTS + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses or write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA, 02110-1301 USA, or download the license from the following URL: + http://itextpdf.com/terms-of-use/ + + The interactive user interfaces in modified source and object code versions + of this program must display Appropriate Legal Notices, as required under + Section 5 of the GNU Affero General Public License. + + In accordance with Section 7(b) of the GNU Affero General Public License, + a covered work must retain the producer line in every PDF that is created + or manipulated using iText. + + You can be released from the requirements of the license by purchasing + a commercial license. Buying such a license is mandatory as soon as you + develop commercial activities involving the iText software without + disclosing the source code of your own applications. + These activities include: offering paid services to customers as an ASP, + serving PDFs on the fly in a web application, shipping iText with a closed + source product. + + For more information, please contact iText Software Corp. at this + address: sales@itextpdf.com + */ +package com.itextpdf.rups.event; + +import com.itextpdf.kernel.pdf.PdfObject; +import com.itextpdf.rups.event.RupsEvent; +import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode; + +public class NodeUpdateArrayChildEvent extends RupsEvent { + + Content content; + + public NodeUpdateArrayChildEvent(PdfObject value, PdfObjectTreeNode parent, int index) { + content = new Content(value, parent, index); + } + + @Override + public int getType() { + return NODE_UPDATE_ARRAY_CHILD_EVENT; + } + + @Override + public Object getContent() { + return content; + } + + public class Content { + public PdfObject value; + public PdfObjectTreeNode parent; + public int index; + + public Content(PdfObject value, PdfObjectTreeNode parent, int index) { + this.value = value; + this.parent = parent; + this.index = index; + } + } +} diff --git a/src/main/java/com/itextpdf/rups/event/NodeUpdateDictChildEvent.java b/src/main/java/com/itextpdf/rups/event/NodeUpdateDictChildEvent.java new file mode 100644 index 00000000..321171c0 --- /dev/null +++ b/src/main/java/com/itextpdf/rups/event/NodeUpdateDictChildEvent.java @@ -0,0 +1,81 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2022 iText Group NV + Authors: iText Software. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License version 3 + as published by the Free Software Foundation with the addition of the + following permission added to Section 15 as permitted in Section 7(a): + FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY + ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT + OF THIRD PARTY RIGHTS + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses or write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA, 02110-1301 USA, or download the license from the following URL: + http://itextpdf.com/terms-of-use/ + + The interactive user interfaces in modified source and object code versions + of this program must display Appropriate Legal Notices, as required under + Section 5 of the GNU Affero General Public License. + + In accordance with Section 7(b) of the GNU Affero General Public License, + a covered work must retain the producer line in every PDF that is created + or manipulated using iText. + + You can be released from the requirements of the license by purchasing + a commercial license. Buying such a license is mandatory as soon as you + develop commercial activities involving the iText software without + disclosing the source code of your own applications. + These activities include: offering paid services to customers as an ASP, + serving PDFs on the fly in a web application, shipping iText with a closed + source product. + + For more information, please contact iText Software Corp. at this + address: sales@itextpdf.com + */ +package com.itextpdf.rups.event; + +import com.itextpdf.kernel.pdf.PdfName; +import com.itextpdf.kernel.pdf.PdfObject; +import com.itextpdf.rups.event.RupsEvent; +import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode; + +public class NodeUpdateDictChildEvent extends RupsEvent { + + Content content; + + public NodeUpdateDictChildEvent(PdfName key, PdfObject value, PdfObjectTreeNode parent, int index) { + content = new Content(key, value, parent, index); + } + + @Override + public int getType() { + return NODE_UPDATE_DICT_CHILD_EVENT; + } + + @Override + public Object getContent() { + return content; + } + + public class Content { + public PdfName key; + public PdfObject value; + public PdfObjectTreeNode parent; + public int index; + + public Content(PdfName key, PdfObject value, PdfObjectTreeNode parent, int index) { + this.key = key; + this.value = value; + this.parent = parent; + this.index = index; + } + } +} diff --git a/src/main/java/com/itextpdf/rups/event/RupsEvent.java b/src/main/java/com/itextpdf/rups/event/RupsEvent.java index cbb1f620..58ca6e94 100644 --- a/src/main/java/com/itextpdf/rups/event/RupsEvent.java +++ b/src/main/java/com/itextpdf/rups/event/RupsEvent.java @@ -46,7 +46,8 @@ This file is part of the iText (R) project. * Abstract Event Class that other events should extend from. */ public abstract class RupsEvent { - + // Does this need to remain a set of static bytes for a particular reason? + // It feels like it's reinventing the enum in this use-case. public static final byte OPEN_FILE_EVENT = 0; public static final byte OPEN_DOCUMENT_POST_EVENT = 1; public static final byte CLOSE_DOCUMENT_EVENT = 2; @@ -65,6 +66,8 @@ public abstract class RupsEvent { public static final byte NEW_INDIRECT_OBJECT_EVENT = 15; public static final byte POST_NEW_INDIRECT_OBJECT_EVENT = 16; public static final byte ALL_FILES_CLOSED = 17; + public static final byte NODE_UPDATE_DICT_CHILD_EVENT = 18; + public static final byte NODE_UPDATE_ARRAY_CHILD_EVENT = 19; public abstract int getType(); diff --git a/src/main/java/com/itextpdf/rups/io/PdfObjectTreeEdit.java b/src/main/java/com/itextpdf/rups/io/PdfObjectTreeEdit.java new file mode 100644 index 00000000..f27d01c2 --- /dev/null +++ b/src/main/java/com/itextpdf/rups/io/PdfObjectTreeEdit.java @@ -0,0 +1,368 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2022 iText Group NV + Authors: iText Software. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License version 3 + as published by the Free Software Foundation with the addition of the + following permission added to Section 15 as permitted in Section 7(a): + FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY + ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT + OF THIRD PARTY RIGHTS + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses or write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA, 02110-1301 USA, or download the license from the following URL: + http://itextpdf.com/terms-of-use/ + + The interactive user interfaces in modified source and object code versions + of this program must display Appropriate Legal Notices, as required under + Section 5 of the GNU Affero General Public License. + + In accordance with Section 7(b) of the GNU Affero General Public License, + a covered work must retain the producer line in every PDF that is created + or manipulated using iText. + + You can be released from the requirements of the license by purchasing + a commercial license. Buying such a license is mandatory as soon as you + develop commercial activities involving the iText software without + disclosing the source code of your own applications. + These activities include: offering paid services to customers as an ASP, + serving PDFs on the fly in a web application, shipping iText with a closed + source product. + + For more information, please contact iText Software Corp. at this + address: sales@itextpdf.com + */ +package com.itextpdf.rups.io; + +import com.itextpdf.kernel.pdf.PdfName; +import com.itextpdf.rups.controller.PdfReaderController; +import com.itextpdf.rups.event.NodeAddArrayChildEvent; +import com.itextpdf.rups.event.NodeAddDictChildEvent; +import com.itextpdf.rups.event.NodeDeleteArrayChildEvent; +import com.itextpdf.rups.event.NodeDeleteDictChildEvent; +import com.itextpdf.rups.event.NodeUpdateArrayChildEvent; +import com.itextpdf.rups.event.NodeUpdateDictChildEvent; +import com.itextpdf.rups.event.RupsEvent; +import com.itextpdf.rups.model.ErrorDialogPane; +import com.itextpdf.rups.view.itext.PdfObjectPanel; +import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode; + +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.UndoableEdit; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Stack; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class PdfObjectTreeEdit implements UndoableEdit { + + enum ObjectType { + ARRAY, DICT + } + enum ActionType { + ADD, DELETE, UPDATE + } + + ObjectType objectType; + ActionType actionType; + PdfReaderController controller; + int editType; // The type of edit, as defined by the static bytes within RupsEvent.java (Refactor to Enum?) + PdfObjectTreeNode parent; // Parent Node + PdfObjectTreeNode oldChild; // Child Node + PdfObjectTreeNode child; // Child Node + int index; // Index at which the edit occurred. + PdfName key; // Index at which the edit occurred. + List addedEdits; + boolean dead; + + public PdfObjectTreeEdit(PdfReaderController controller, RupsEvent editEvent) { + this.controller = controller; + this.addedEdits = new LinkedList<>(); + this.dead = false; + extractEditData(editEvent); + } + + private void extractEditData(RupsEvent editEvent) { + // TODO: Maybe we can add some granularity to these events? + // i.e. (abs)RupsEvent>(abs)NodeArrayEvent>NodeAddArrayEvent so the common array interface of Index, Parent and Value are + // always present and (abs)RupsEvent>(abs)NodeDictEvent>NodeAddDictEvent so that Key, Parent and Value are always present? + + editType = editEvent.getType(); + switch (editType) { + case RupsEvent.NODE_ADD_DICT_CHILD_EVENT: + NodeAddDictChildEvent.Content addDictContent = (NodeAddDictChildEvent.Content)editEvent.getContent(); + parent = addDictContent.parent; + child = PdfObjectTreeNode.getInstance(addDictContent.value); + index = addDictContent.index; + key = addDictContent.key; + objectType = ObjectType.DICT; + actionType = ActionType.ADD; + break; + case RupsEvent.NODE_ADD_ARRAY_CHILD_EVENT: + NodeAddArrayChildEvent.Content addArrayContent = (NodeAddArrayChildEvent.Content)editEvent.getContent(); + parent = addArrayContent.parent; + child = PdfObjectTreeNode.getInstance(addArrayContent.value); + index = addArrayContent.index; + objectType = ObjectType.ARRAY; + actionType = ActionType.ADD; + break; + case RupsEvent.NODE_DELETE_DICT_CHILD_EVENT: + NodeDeleteDictChildEvent.Content deleteDictContent = (NodeDeleteDictChildEvent.Content)editEvent.getContent(); + parent = deleteDictContent.parent; + key = deleteDictContent.key; + child = deleteDictContent.parent.getDictionaryChildNode(key); + index = deleteDictContent.parent.getIndex(this.child); + objectType = ObjectType.DICT; + actionType = ActionType.DELETE; + break; + case RupsEvent.NODE_DELETE_ARRAY_CHILD_EVENT: + NodeDeleteArrayChildEvent.Content deleteArrayContent = (NodeDeleteArrayChildEvent.Content)editEvent.getContent(); + parent = deleteArrayContent.parent; + index = deleteArrayContent.index; + child = (PdfObjectTreeNode) parent.getChildAt(index); + objectType = ObjectType.ARRAY; + actionType = ActionType.DELETE; + break; + case RupsEvent.NODE_UPDATE_DICT_CHILD_EVENT: + NodeUpdateDictChildEvent.Content updateDictContent = (NodeUpdateDictChildEvent.Content)editEvent.getContent(); + parent = updateDictContent.parent; + key = updateDictContent.key; + oldChild = parent.getDictionaryChildNode(key); + child = PdfObjectTreeNode.getInstance(updateDictContent.value); + index = updateDictContent.index; + objectType = ObjectType.DICT; + actionType = ActionType.UPDATE; + break; + case RupsEvent.NODE_UPDATE_ARRAY_CHILD_EVENT: + NodeUpdateArrayChildEvent.Content updateArrayContent = (NodeUpdateArrayChildEvent.Content)editEvent.getContent(); + parent = updateArrayContent.parent; + index = updateArrayContent.index; + child = PdfObjectTreeNode.getInstance(updateArrayContent.value); + oldChild = (PdfObjectTreeNode) parent.getChildAt(index); + objectType = ObjectType.ARRAY; + actionType = ActionType.UPDATE; + break; + } + } + + @Override + public void undo() throws CannotUndoException { + if(!canUndo()) throw new CannotUndoException(); + RupsEvent newChild; + switch (this.editType) { + case RupsEvent.NODE_ADD_DICT_CHILD_EVENT: + newChild = new NodeDeleteDictChildEvent(key, parent); + break; + case RupsEvent.NODE_ADD_ARRAY_CHILD_EVENT: + newChild = new NodeDeleteArrayChildEvent(index, parent); + break; + case RupsEvent.NODE_DELETE_DICT_CHILD_EVENT: + newChild = new NodeAddDictChildEvent(key, child.getPdfObject(), parent, index); + break; + case RupsEvent.NODE_DELETE_ARRAY_CHILD_EVENT: + newChild = new NodeAddArrayChildEvent(child.getPdfObject(), parent, index); + break; + case RupsEvent.NODE_UPDATE_DICT_CHILD_EVENT: + newChild = new NodeUpdateDictChildEvent(key, oldChild.getPdfObject(), parent, index); + break; + case RupsEvent.NODE_UPDATE_ARRAY_CHILD_EVENT: + newChild = new NodeUpdateArrayChildEvent(oldChild.getPdfObject(), parent, index); + break; + default: + throw new CannotUndoException(); + } + for (int i = addedEdits.size() - 1; i > -1; i--) { + addedEdits.get(i).undo(); + } + updateController(newChild, CannotRedoException.class); + } + + private void updateController(RupsEvent newChild, Class exceptionClass) { + try { + controller.update(controller, newChild); + controller.render(parent); + } catch (Exception ex) { + ErrorDialogPane.showErrorDialog(controller.getEditorTabs().getParent(), ex); + System.err.print(ex.getLocalizedMessage()); + try { + throw exceptionClass.getDeclaredConstructor().newInstance(ex.getLocalizedMessage()); + } catch (Exception e) { + throw new RuntimeException(ex.getLocalizedMessage()); + } + } + } + + @Override + public boolean canUndo() { + if(!validateSelf()) return false; + return true; + } + + @Override + public void redo() throws CannotRedoException { + if(!canRedo()) throw new CannotRedoException(); + + try { + RupsEvent newChild; + switch (editType) { + case RupsEvent.NODE_ADD_DICT_CHILD_EVENT: + newChild = new NodeAddDictChildEvent(key,child.getPdfObject(),parent,index); + break; + case RupsEvent.NODE_ADD_ARRAY_CHILD_EVENT: + newChild = new NodeAddArrayChildEvent(child.getPdfObject(),parent,index); + break; + case RupsEvent.NODE_DELETE_DICT_CHILD_EVENT: + newChild = new NodeDeleteDictChildEvent(key,parent); + break; + case RupsEvent.NODE_DELETE_ARRAY_CHILD_EVENT: + newChild = new NodeDeleteArrayChildEvent(index,parent); + break; + case RupsEvent.NODE_UPDATE_DICT_CHILD_EVENT: + newChild = new NodeUpdateDictChildEvent(key,child.getPdfObject(),parent,index); + break; + case RupsEvent.NODE_UPDATE_ARRAY_CHILD_EVENT: + newChild = new NodeUpdateArrayChildEvent(child.getPdfObject(),parent,index); + break; + default: + throw new CannotRedoException(); + } + updateController(newChild, CannotRedoException.class); + + for (int i = 0; i < addedEdits.size(); i++) { + addedEdits.get(i).redo(); + } + } catch (CannotRedoException ex) { + ErrorDialogPane.showErrorDialog( controller.getEditorTabs().getParent(), ex); + System.err.print(ex.getLocalizedMessage()); + throw ex; + } catch (Exception ex) { + ErrorDialogPane.showErrorDialog( controller.getEditorTabs().getParent(), ex); + System.err.print(ex.getLocalizedMessage()); + throw new CannotRedoException(); + } + } + + @Override + public boolean canRedo() { + return validateSelf(); + } + + @Override + public void die() { + this.dead = true; + } + + @Override + public boolean addEdit(UndoableEdit anEdit) { + return false; + } + + @Override + public boolean replaceEdit(UndoableEdit anEdit) { + if (!(anEdit instanceof PdfObjectTreeEdit)) { + return false; + } + if (isSignificant()){ + return false; + } + PdfObjectTreeEdit objectTreeEdit = (PdfObjectTreeEdit) anEdit; + + if (!checkParity(objectTreeEdit)){ + return false; + } + + dead = true; + return true; + } + + @Override + public boolean isSignificant() { + return true; + } + + @Override + public String getPresentationName() { + StringBuilder returnString = new StringBuilder(); + switch(actionType){ + case ADD: + returnString.append("Addition"); + break; + case DELETE: + returnString.append("Removal"); + break; + case UPDATE: + returnString.append("Update"); + break; + default: + return "Error"; + } + switch (objectType){ + case DICT: + returnString.append(" of a Dict value"); + break; + case ARRAY: + returnString.append(" of an Array value"); + } + return returnString.toString(); + } + + @Override + public String getUndoPresentationName() { + return "Undo the ".concat(getPresentationName()); + } + + @Override + public String getRedoPresentationName() { + return "Redo the ".concat(getPresentationName()); + } + + private boolean validateSelf() { + if (dead) return false; + + switch(this.editType){ + case RupsEvent.NODE_ADD_DICT_CHILD_EVENT: + case RupsEvent.NODE_ADD_ARRAY_CHILD_EVENT: + case RupsEvent.NODE_UPDATE_DICT_CHILD_EVENT: + case RupsEvent.NODE_UPDATE_ARRAY_CHILD_EVENT: + case RupsEvent.NODE_DELETE_DICT_CHILD_EVENT: + case RupsEvent.NODE_DELETE_ARRAY_CHILD_EVENT: + return true; + default: + return false; + } + } + + private boolean checkParity(PdfObjectTreeEdit objectTreeEdit) { + + if(objectTreeEdit.controller != controller){ + return false; + } + + if(objectTreeEdit.objectType != objectType){ + return false; + } + + if(objectTreeEdit.index != index){ + return false; + } + + if(objectTreeEdit.parent != parent){ + return false; + } + + if(objectTreeEdit.key != key){ + return false; + } + return true; + } +} diff --git a/src/main/java/com/itextpdf/rups/model/TreeNodeFactory.java b/src/main/java/com/itextpdf/rups/model/TreeNodeFactory.java index b161c3d4..e51ef5f3 100644 --- a/src/main/java/com/itextpdf/rups/model/TreeNodeFactory.java +++ b/src/main/java/com/itextpdf/rups/model/TreeNodeFactory.java @@ -133,7 +133,8 @@ public void expandNode(PdfObjectTreeNode node) { break; case PdfObject.ARRAY: final PdfArray array = (PdfArray) object; - for (int i = 0; i < array.size(); ++i) { + int arrayLength = array.size(); + for (int i = 0; i < arrayLength; ++i) { leaf = PdfObjectTreeNode.getInstance(array.get(i, false)); associateIfIndirect(leaf); addNodes(node, leaf); diff --git a/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java b/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java index 713b03be..2df84100 100644 --- a/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java +++ b/src/main/java/com/itextpdf/rups/view/RupsTabbedPane.java @@ -75,8 +75,8 @@ public void openNewFile(File file, Dimension dimension, boolean readonly) { if (this.defaultTab.equals(this.jTabbedPane.getSelectedComponent())) { this.jTabbedPane.removeTabAt(this.jTabbedPane.getSelectedIndex()); } - RupsPanel rupsPanel = new RupsPanel(); + rupsPanel.setName(file.getName()); RupsInstanceController rupsInstanceController = new RupsInstanceController(dimension, rupsPanel); rupsPanel.setRupsInstanceController(rupsInstanceController); rupsInstanceController.loadFile(file, readonly); diff --git a/src/main/java/com/itextpdf/rups/view/contextmenu/SaveToFilePdfTreeAction.java b/src/main/java/com/itextpdf/rups/view/contextmenu/SaveToFilePdfTreeAction.java index baae9387..c232d25c 100644 --- a/src/main/java/com/itextpdf/rups/view/contextmenu/SaveToFilePdfTreeAction.java +++ b/src/main/java/com/itextpdf/rups/view/contextmenu/SaveToFilePdfTreeAction.java @@ -102,7 +102,7 @@ public void actionPerformed(ActionEvent event) { try (FileOutputStream fos = new FileOutputStream(path)) { fos.write(array); } - } catch (IOException e) { // TODO : Catch this exception properly + } catch (IOException e) { LoggerHelper.error(Language.ERROR_WRITING_FILE.getString(), e, getClass()); } } diff --git a/src/main/java/com/itextpdf/rups/view/itext/PdfObjectPanel.java b/src/main/java/com/itextpdf/rups/view/itext/PdfObjectPanel.java index 50621085..402c798a 100644 --- a/src/main/java/com/itextpdf/rups/view/itext/PdfObjectPanel.java +++ b/src/main/java/com/itextpdf/rups/view/itext/PdfObjectPanel.java @@ -52,7 +52,10 @@ This file is part of the iText (R) project. import com.itextpdf.rups.event.NodeAddDictChildEvent; import com.itextpdf.rups.event.NodeDeleteArrayChildEvent; import com.itextpdf.rups.event.NodeDeleteDictChildEvent; +import com.itextpdf.rups.event.NodeUpdateArrayChildEvent; +import com.itextpdf.rups.event.NodeUpdateDictChildEvent; import com.itextpdf.rups.event.RupsEvent; +import com.itextpdf.rups.io.PdfObjectTreeEdit; import com.itextpdf.rups.model.PdfSyntaxParser; import com.itextpdf.rups.view.Language; import com.itextpdf.rups.view.icons.IconFetcher; @@ -63,16 +66,26 @@ This file is part of the iText (R) project. import com.itextpdf.rups.view.models.PdfArrayTableModel; import java.awt.CardLayout; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.util.LinkedList; +import java.util.List; import java.util.Observable; import java.util.Observer; +import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextArea; +import javax.swing.KeyStroke; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.UndoManager; +import javax.swing.undo.UndoableEdit; public class PdfObjectPanel extends Observable implements Observer { @@ -89,11 +102,14 @@ public class PdfObjectPanel extends Observable implements Observer { */ private static final String TEXT = Language.TEXT.getString(); + private final PdfReaderController controller; + /** * The layout that will show the info about the PDF object that is being analyzed. */ protected CardLayout layout = new CardLayout(); + protected UndoManager undoManager; /** * Table with dictionary entries. @@ -112,22 +128,41 @@ public class PdfObjectPanel extends Observable implements Observer { /** * Creates a PDF object panel. */ - public PdfObjectPanel() { + public PdfObjectPanel(PdfReaderController controller) { + + this.controller = controller; + + // panel name + panel.setName("PdfObjectPanel"); + + table.setName("PdfObjectTable"); + // layout panel.setLayout(layout); // dictionary / array / stream final JScrollPane dictScrollPane = new JScrollPane(); dictScrollPane.setViewportView(table); + dictScrollPane.setName("dictScrollPane"); panel.add(dictScrollPane, TABLE); // number / string / ... final JScrollPane textScrollPane = new JScrollPane(); textScrollPane.setViewportView(text); + textScrollPane.setName("textScrollPane"); text.setEditable(false); panel.add(textScrollPane, TEXT); table.addMouseListener(new JTableButtonMouseListener()); + + this.undoManager = new UndoManager(); + this.undoManager.setLimit(8192); + + //TODO: Check if WHEN_IN_FOCUSED_WINDOW bleeds across open docs + panel.registerKeyboardAction(new UndoAction(undoManager), + KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW); + panel.registerKeyboardAction(new RedoAction(undoManager), + KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK), JComponent.WHEN_IN_FOCUSED_WINDOW); } /** @@ -160,8 +195,8 @@ public void update(Observable observable, Object obj) { */ public void render(PdfObjectTreeNode node, PdfSyntaxParser parser) { target = node; - final PdfObject object = node.getPdfObject(); - if (object == null) { + final PdfObject pdfObjectClone = node.getPdfObject().clone(); + if (pdfObjectClone == null) { text.setText(null); layout.show(panel, TEXT); panel.repaint(); @@ -169,34 +204,36 @@ public void render(PdfObjectTreeNode node, PdfSyntaxParser parser) { return; } - switch (object.getType()) { + DictionaryTableModelButton rowButtons = new DictionaryTableModelButton(IconFetcher.getIcon(CROSS_ICON), IconFetcher.getIcon(ADD_ICON)); + rowButtons.setName("rowButton"); + switch (pdfObjectClone.getType()) { case PdfObject.DICTIONARY: case PdfObject.STREAM: final DictionaryTableModel model = - new DictionaryTableModel((PdfDictionary) object, parser, panel); + new DictionaryTableModel((PdfDictionary) pdfObjectClone, parser, panel); model.addTableModelListener(new DictionaryModelListener()); table.setModel(model); - table.getColumn("").setCellRenderer(new DictionaryTableModelButton( - IconFetcher.getIcon(CROSS_ICON), IconFetcher.getIcon(ADD_ICON))); + table.getColumn("").setCellRenderer(rowButtons); layout.show(panel, TABLE); panel.repaint(); + table.revalidate(); break; case PdfObject.ARRAY: final PdfArrayTableModel arrayModel = - new PdfArrayTableModel((PdfArray) object, parser, panel); + new PdfArrayTableModel((PdfArray) pdfObjectClone, parser, panel); arrayModel.addTableModelListener(new ArrayModelListener()); table.setModel(arrayModel); - table.getColumn("").setCellRenderer(new DictionaryTableModelButton(IconFetcher.getIcon(CROSS_ICON), - IconFetcher.getIcon(ADD_ICON))); + table.getColumn("").setCellRenderer(rowButtons); layout.show(panel, TABLE); panel.repaint(); + table.revalidate(); break; case PdfObject.STRING: - text.setText(((PdfString) object).toUnicodeString()); + text.setText(((PdfString) pdfObjectClone).toUnicodeString()); layout.show(panel, TEXT); break; default: - text.setText(object.toString()); + text.setText(pdfObjectClone.toString()); layout.show(panel, TEXT); break; } @@ -236,19 +273,27 @@ public void tableChanged(TableModelEvent e) { return; } final PdfName key = (PdfName) table.getValueAt(row, 0); - final PdfObject value = ((PdfDictionary) target.getPdfObject()).get(key, false); + final PdfObject value; + RupsEvent notification = null; switch (e.getType()) { - case TableModelEvent.UPDATE: - break; case TableModelEvent.DELETE: - PdfObjectPanel.this.setChanged(); - PdfObjectPanel.this.notifyObservers(new NodeDeleteDictChildEvent(key, target)); + notification = new NodeDeleteDictChildEvent(key, target); + break; + case TableModelEvent.UPDATE: + value = ((PdfDictionary) ((DictionaryTableModel) table.getModel()).getPdfObject()).get(key, false); + notification = new NodeUpdateDictChildEvent(key, value, target, row); break; case TableModelEvent.INSERT: - PdfObjectPanel.this.setChanged(); - PdfObjectPanel.this.notifyObservers(new NodeAddDictChildEvent(key, value, target, row)); + value = ((PdfDictionary) ((DictionaryTableModel) table.getModel()).getPdfObject()).get(key, false); + notification = new NodeAddDictChildEvent(key, value, target, row); break; } + if (notification == null) { + return; + } + undoManager.addEdit(new PdfObjectTreeEdit(controller, notification)); + PdfObjectPanel.this.setChanged(); + PdfObjectPanel.this.notifyObservers(notification); } } @@ -262,19 +307,28 @@ public void tableChanged(TableModelEvent e) { if (row != e.getLastRow()) { return; } - final PdfObject value = ((PdfArray) target.getPdfObject()).get(row, false); + // TODO: Maybe abstract the PDF object used for backing the UI elements from those backing the document tree. + RupsEvent notification = null; + final PdfObject value; switch (e.getType()) { - case TableModelEvent.UPDATE: - break; case TableModelEvent.DELETE: - PdfObjectPanel.this.setChanged(); - PdfObjectPanel.this.notifyObservers(new NodeDeleteArrayChildEvent(row, target)); + notification = new NodeDeleteArrayChildEvent(row, target); + break; + case TableModelEvent.UPDATE: + value = ((PdfArray) ((PdfArrayTableModel) table.getModel()).getPdfObject()).get(row, false); + notification = new NodeUpdateArrayChildEvent(value, target, row); break; case TableModelEvent.INSERT: - PdfObjectPanel.this.setChanged(); - PdfObjectPanel.this.notifyObservers(new NodeAddArrayChildEvent(value, target, row)); + value = ((PdfArray) ((PdfArrayTableModel) table.getModel()).getPdfObject()).get(row, false); + notification = new NodeAddArrayChildEvent(value, target, row); break; } + if (notification == null) { + return; + } + undoManager.addEdit(new PdfObjectTreeEdit(controller, notification)); + PdfObjectPanel.this.setChanged(); + PdfObjectPanel.this.notifyObservers(notification); } } } diff --git a/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java b/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java index f107c015..cd83253f 100644 --- a/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java +++ b/src/main/java/com/itextpdf/rups/view/itext/SyntaxHighlightedStreamPane.java @@ -88,8 +88,6 @@ This file is part of the iText (R) project. public class SyntaxHighlightedStreamPane extends JScrollPane implements Observer { - private static final int MAX_NUMBER_OF_EDITS = 8192; - private static Method pdfStreamGetInputStreamMethod; /** @@ -103,6 +101,8 @@ public class SyntaxHighlightedStreamPane extends JScrollPane implements Observer protected UndoManager manager; + protected int MAX_NUMBER_OF_EDITS = 8192; + //Todo: Remove that field after proper application structure will be implemented. private final PdfReaderController controller; @@ -132,8 +132,9 @@ public SyntaxHighlightedStreamPane(PdfReaderController controller) { text.setComponentPopupMenu(popupMenu); text.addMouseListener(new ContextMenuMouseListener(popupMenu, text)); - manager = new UndoManager(); - manager.setLimit(MAX_NUMBER_OF_EDITS); + this.manager = new UndoManager(); + this.manager.setLimit(MAX_NUMBER_OF_EDITS); + text.getDocument().addUndoableEditListener(manager); text.registerKeyboardAction(new UndoAction(manager), KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK), JComponent.WHEN_FOCUSED); diff --git a/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java b/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java index 86ada289..ab3628ca 100644 --- a/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java +++ b/src/main/java/com/itextpdf/rups/view/itext/treenodes/PdfObjectTreeNode.java @@ -192,6 +192,19 @@ public static PdfObjectTreeNode getInstance(PdfDictionary dict, PdfName key) { return node; } + /** + * Creates an instance of a tree node for the object corresponding with a key in a dictionary. + * + * @param obj the pdf object that is represented by this tree node. + * @param key the dictionary key corresponding with the PDF object in this tree node. + * @return a PdfObjectTreeNode + */ + public static PdfObjectTreeNode getInstance(PdfObject obj, PdfName key) { + final PdfObjectTreeNode node = getInstance(obj); + node.key = key; + return node; + } + /** * Getter for the PDF Object. * @@ -201,6 +214,15 @@ public PdfObject getPdfObject() { return object; } + /** + * Getter for the ObjectTree's key. + * + * @return the PDF Name representing this tree node. + */ + public PdfName getKey(){ + return key; + } + /** * Getter for the object number in case the object is indirect or if it is reference. * diff --git a/src/main/java/com/itextpdf/rups/view/models/AbstractPdfObjectPanelTableModel.java b/src/main/java/com/itextpdf/rups/view/models/AbstractPdfObjectPanelTableModel.java index 178d5c7c..86efaa43 100644 --- a/src/main/java/com/itextpdf/rups/view/models/AbstractPdfObjectPanelTableModel.java +++ b/src/main/java/com/itextpdf/rups/view/models/AbstractPdfObjectPanelTableModel.java @@ -42,10 +42,14 @@ This file is part of the iText (R) project. */ package com.itextpdf.rups.view.models; +import com.itextpdf.kernel.pdf.PdfObject; + import javax.swing.table.AbstractTableModel; public abstract class AbstractPdfObjectPanelTableModel extends AbstractTableModel { + public abstract PdfObject getPdfObject(); + public abstract void removeRow(int rowIndex); public abstract void validateTempRow(); diff --git a/src/main/java/com/itextpdf/rups/view/models/DictionaryTableModel.java b/src/main/java/com/itextpdf/rups/view/models/DictionaryTableModel.java index c32ce171..3ba5813b 100644 --- a/src/main/java/com/itextpdf/rups/view/models/DictionaryTableModel.java +++ b/src/main/java/com/itextpdf/rups/view/models/DictionaryTableModel.java @@ -168,8 +168,7 @@ public void setValueAt(Object aValue, int rowIndex, int columnIndex) { final PdfObject newValue = parser.parseString(value, parent); if (newValue != null) { final PdfName oldName = keys.get(rowIndex); - removeRow(rowIndex); - addRow(oldName, newValue); + updateRow(rowIndex, newValue); } } } @@ -249,6 +248,12 @@ private void addRow(PdfName key, PdfObject value) { fireTableRowsInserted(index, index); } + private void updateRow(int index, PdfObject value) { + PdfName key = keys.get(index); + dictionary.put(key, value); + fireTableRowsUpdated(index, index); + } + private PdfName getCorrectKey(String value) { if (!value.startsWith("/")) { value = "/" + value; @@ -262,4 +267,9 @@ private PdfName getCorrectKey(String value) { LoggerHelper.error(Language.ERROR_KEY_IS_NOT_NAME.getString(), getClass()); return null; } + + @Override + public PdfObject getPdfObject() { + return dictionary; + } } diff --git a/src/main/java/com/itextpdf/rups/view/models/PdfArrayTableModel.java b/src/main/java/com/itextpdf/rups/view/models/PdfArrayTableModel.java index cecb6816..21bb3c87 100644 --- a/src/main/java/com/itextpdf/rups/view/models/PdfArrayTableModel.java +++ b/src/main/java/com/itextpdf/rups/view/models/PdfArrayTableModel.java @@ -130,8 +130,9 @@ public void setValueAt(Object aValue, int rowIndex, int columnIndex) { final String value = (String) aValue; final PdfObject newValue = parser.parseString(value, parent); if (newValue != null) { - removeRow(rowIndex); - addRow(rowIndex, newValue); + removeRow(rowIndex, true); + addRow(rowIndex, newValue, true); + fireTableCellUpdated(rowIndex,columnIndex); } } } @@ -153,8 +154,15 @@ public String getColumnName(int columnIndex) { @Override public void removeRow(int rowIndex) { - fireTableRowsDeleted(rowIndex, rowIndex); + removeRow(rowIndex, false); + } + + protected void removeRow(int rowIndex, boolean compoundAction) { array.remove(rowIndex); + if(compoundAction){ + return; + } + fireTableRowsDeleted(rowIndex, rowIndex); fireTableDataChanged(); } @@ -189,7 +197,7 @@ public void validateTempRow() { LoggerHelper.warn(Language.ERROR_INDEX_NOT_IN_RANGE.getString(), getClass()); } } - addRow(index, value); + addRow(index, value,false); tempValue = ""; fireTableDataChanged(); } @@ -200,10 +208,17 @@ public int getButtonColumn() { return 1; } - private void addRow(int index, PdfObject value) { + private void addRow(int index, PdfObject value, boolean compoundAction) { array.add(index, value); + if (compoundAction){ + return; + } fireTableRowsInserted(index, index); } + @Override + public PdfObject getPdfObject() { + return array; + } } diff --git a/src/test/java/com/itextpdf/rups/view/RupsUndoRedoTest.java b/src/test/java/com/itextpdf/rups/view/RupsUndoRedoTest.java new file mode 100644 index 00000000..568335b7 --- /dev/null +++ b/src/test/java/com/itextpdf/rups/view/RupsUndoRedoTest.java @@ -0,0 +1,349 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2022 iText Group NV + Authors: iText Software. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License version 3 + as published by the Free Software Foundation with the addition of the + following permission added to Section 15 as permitted in Section 7(a): + FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY + ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT + OF THIRD PARTY RIGHTS + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses or write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA, 02110-1301 USA, or download the license from the following URL: + http://itextpdf.com/terms-of-use/ + + The interactive user interfaces in modified source and object code versions + of this program must display Appropriate Legal Notices, as required under + Section 5 of the GNU Affero General Public License. + + In accordance with Section 7(b) of the GNU Affero General Public License, + a covered work must retain the producer line in every PDF that is created + or manipulated using iText. + + You can be released from the requirements of the license by purchasing + a commercial license. Buying such a license is mandatory as soon as you + develop commercial activities involving the iText software without + disclosing the source code of your own applications. + These activities include: offering paid services to customers as an ASP, + serving PDFs on the fly in a web application, shipping iText with a closed + source product. + + For more information, please contact iText Software Corp. at this + address: sales@itextpdf.com + */ +package com.itextpdf.rups.view; + +import com.itextpdf.kernel.actions.data.ITextCoreProductData; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.uispec4j.Key; +import org.uispec4j.Panel; +import org.uispec4j.Table; +import org.uispec4j.Tree; +import org.uispec4j.Trigger; +import org.uispec4j.UIComponent; +import org.uispec4j.Window; +import org.uispec4j.interception.WindowHandler; +import org.uispec4j.interception.WindowInterceptor; + +@Tag("Integration") +@Tag("GUI") +public class RupsUndoRedoTest extends RupsWindowTest { + + private static final String INPUT_FILE = "src/test/resources/com/itextpdf/rups/controller/hello_world.pdf"; + + private static Window MAIN_WINDOW; + private static Panel objectPanel; + private static Tree pdfTree; + private static final String TEST_KEY = "/Test"; + private static final String MODIFICATION_KEY = "/Producer"; + private static final String TEST_VALUE = "(Test)"; + + public RupsUndoRedoTest(){ + super("Undo/Redo interaction Test"); + } + private void validateInitialArray(Table infoArray) { + Assertions.assertTrue(infoArray.getHeader().contentEquals("Array", "").isTrue(), "Table Header identifies the Content as an Array: "); + Assertions.assertTrue(infoArray.rowCountEquals(3).isTrue(), "Table is 3 Cells Tall: "); + } + + private void validateInitialDictionary(Table infoArray) { + Assertions.assertTrue(infoArray.getHeader().contentEquals("Key", "Value", "").isTrue(), "Table Header identifies the Content as an Dictionary: "); + Assertions.assertTrue(infoArray.rowCountEquals(4).isTrue(), "Table is 4 Cells Tall: "); + } + + private void clickTree(String targetNode) { + clickTree(targetNode, 1); + } + + private void doubleClickTree(String targetNode) { + clickTree(targetNode, 2); + } + + private void clickTree(String targetNode, int times) { + pdfTree.selectRoot(); + Assertions.assertTrue(pdfTree.contains(targetNode).isTrue(), String.format("PDFTree contains `%s` node: ", targetNode)); + if (times == 2) { + pdfTree.doubleClick(targetNode); + } else { + while (times > 0) { + pdfTree.click(targetNode); + times--; + } + } + } + + private static void UNDO(UIComponent component) { + component.pressKey(Key.control(Key.Z)); + } + + private static void REDO(UIComponent component) { + component.pressKey(Key.control(Key.Y)); + } + + + @BeforeEach + public void before() { + try { + setUp(INPUT_FILE); + } catch (Exception e) { + Assertions.fail(e.getMessage()); + e.printStackTrace(System.err); + } + MAIN_WINDOW = getMainWindow(); + + String windowTitle = String.format(Language.TITLE.getString(), ITextCoreProductData.getInstance().getVersion()); + waitUntil(MAIN_WINDOW.titleEquals(windowTitle), 8000); + String documentPanelName = INPUT_FILE.substring(1 + INPUT_FILE.lastIndexOf('/')); + waitUntil(MAIN_WINDOW.getPanel(documentPanelName).isEnabled(), 8000); + pdfTree = MAIN_WINDOW.getTree("pdfTree"); + objectPanel = MAIN_WINDOW.getPanel("PdfObjectPanel"); + } + + @Test + @Order(1) + public void phase1canUndoArrayAddition() { + + + clickTree("ID"); + Assertions.assertEquals(2,pdfTree.getChildCount("ID"), "Array Tree has 2 children: "); + + Table infoArray = objectPanel.getTable(); + validateInitialArray(infoArray); + + infoArray.editCell(2, 0, TEST_VALUE, true); + + WindowInterceptor.init(infoArray.triggerClick(2, 1, Key.Modifier.NONE)) + .process(new WindowHandler() { + @Override + public Trigger process(Window dialog) throws Exception { + Assertions.assertEquals("Input", dialog.getTitle(), "Dialog Title is 'Input'"); + Assertions.assertEquals("2", dialog.getInputTextBox().getText(), "Row Value defaults to: 2"); + return dialog.getButton("OK").triggerClick(); + } + }).run(); + waitUntil("Table is now 4 Cells Tall: ", infoArray.rowCountEquals(4), 5000); + Assertions.assertEquals(3,pdfTree.getChildCount("ID"), "Array Tree has 3 children: "); + Assertions.assertEquals(TEST_VALUE, (String) infoArray.getContentAt(2, 0), "Value of cell content is equal to the test value provided: "); + UNDO(infoArray); + Assertions.assertTrue(infoArray.rowCountEquals(3).isTrue(), "Table is 3 Cells Tall again: "); + Assertions.assertEquals("", infoArray.getContentAt(2, 0), "Test value is no longer in the table: "); + Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree has 2 children: "); + + + } + + @Test + @Order(1) + public void phase1canUndoArrayUpdate() { + // Click Tree + clickTree("ID"); + Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree has 2 children: "); + Table infoArray = objectPanel.getTable(); + validateInitialArray(infoArray); + // Store Original Value + Assertions.assertEquals(String.class, infoArray.getContentAt(0, 0).getClass(), "Value is a String: "); + String originalValue = (String) infoArray.getContentAt(0, 0); + // Change Value to "(Test)" + infoArray.editCell(0, 0, TEST_VALUE, true); + // Press Enter Key + infoArray.pressKey(Key.ENTER); + // Test Updated value + Assertions.assertEquals(TEST_VALUE, (String) infoArray.getContentAt(0, 0), "Cell Value is equal to Test Value: "); + Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree still has 2 children: "); + // Press Ctrl-Z + UNDO(infoArray); + // Test Updated Value is back to Original + Assertions.assertEquals(originalValue, (String) infoArray.getContentAt(0,0), "Value has reset to original: "); + validateInitialArray(infoArray); + Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree still has 2 children: "); + } + + @Test + @Order(1) + public void phase1canUndoArrayDeletion() { + // Click Tree + clickTree("ID"); + + Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree has 2 children: "); + + Table infoArray = objectPanel.getTable(); + validateInitialArray(infoArray); + // Store Original Value + Assertions.assertEquals(String.class, infoArray.getContentAt(0, 0).getClass(), "Value is a String: "); + String originalValue = (String) infoArray.getContentAt(0, 0); + // Click Table X Button + infoArray.click(0, 1, Key.Modifier.NONE); + // Test Tree Size + Assertions.assertEquals(1, pdfTree.getChildCount("ID"), "Array Tree has 1 child: "); + // Test Table Length + Assertions.assertEquals(2, infoArray.getRowCount(), "Table is now 2 Cells Tall: "); + // Press Ctrl-Z + UNDO(infoArray); + Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree has 2 children again: "); + // Test Table Length + validateInitialArray(infoArray); + // Test Updated Value is back to Original + Assertions.assertEquals(originalValue, (String) infoArray.getContentAt(0, 0), "Value has reset to original: "); + } + + @Test + @Order(1) + public void phase1canUndoDictAddition() { + doubleClickTree("Info"); + Assertions.assertEquals(3,pdfTree.getChildCount("Info/Dictionary"), "Dict Tree has 3 children: "); + Table infoDict = objectPanel.getTable(); + validateInitialDictionary(infoDict); + infoDict.editCell(3,0,TEST_KEY,true); + infoDict.editCell(3,1,TEST_VALUE,true); + infoDict.click(3,2); + Assertions.assertEquals(4,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 4 children: "); + Assertions.assertEquals(5, infoDict.getRowCount(), "Table is now 5 Rows tall: "); + Assertions.assertEquals(TEST_KEY, (String) infoDict.getContentAt(3,0), "New Key is equal to Test Key: "); + Assertions.assertEquals(TEST_VALUE, (String) infoDict.getContentAt(3,1), "New Value is equal to Test Value: "); + UNDO(infoDict); + Assertions.assertEquals(3,pdfTree.getChildCount("Info/Dictionary"), "Dict Tree has 3 children again: "); + validateInitialDictionary(infoDict); + assertFalse("Dict Tree no longer contains the Test Key: ", pdfTree.contains(String.format("Info/Dictionary%s",TEST_KEY))); + } + + @Test + @Order(1) + public void phase1canUndoDictUpdate() { + doubleClickTree("Info"); + Table infoDict = objectPanel.getTable(); + validateInitialDictionary(infoDict); + String originalValue = (String) infoDict.getContentAt(2, 1); + Assertions.assertEquals(MODIFICATION_KEY, (String) infoDict.getContentAt(2,0), "Key to Modify is on the expected row: "); + infoDict.editCell(2,1,TEST_VALUE, true); + Assertions.assertEquals(3,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 3 children: "); + Assertions.assertEquals(MODIFICATION_KEY, (String) infoDict.getContentAt(2,0), "Row Key is Unchanged"); + Assertions.assertEquals(TEST_VALUE, (String) infoDict.getContentAt(2,1), "Modified Cell is set to Test Value"); + UNDO(infoDict); + validateInitialDictionary(infoDict); + Assertions.assertEquals(originalValue, (String) infoDict.getContentAt(2,1), "Value has reset to original: "); + } + + @Test + @Order(1) + public void phase1canUndoDictDeletion() { + doubleClickTree("Info"); + Table infoDict = objectPanel.getTable(); + validateInitialDictionary(infoDict); + infoDict.click(2,2); + Assertions.assertEquals(2,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 2 children: "); + Assertions.assertEquals(3, infoDict.getRowCount(), "Table is now 3 Rows tall: "); + UNDO(infoDict); + Assertions.assertEquals(3,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 3 children: "); + Assertions.assertEquals(4, infoDict.getRowCount(), "Table is now 4 Rows tall: "); + } + + @Test + @Order(2) + public void phase2canRedoArrayAddition() { + + // run canUndo test + phase1canUndoArrayAddition(); + + Table infoArray = objectPanel.getTable(); + // Press Ctrl-Y + REDO(infoArray); + Assertions.assertEquals(3, pdfTree.getChildCount("ID"), "Array Tree has 3 children again: "); + // Test Table Length + Assertions.assertEquals(4, infoArray.getRowCount(), "Table is now 4 Cells Tall again: "); + // Test Table Value + Assertions.assertEquals(TEST_VALUE, (String) infoArray.getContentAt(2, 0), "Value of cell content is equal to the test value provided: "); + } + + @Test + @Order(2) + public void phase2canRedoArrayUpdate() { + // run canUndo test + phase1canUndoArrayUpdate(); + Table infoArray = objectPanel.getTable(); + // Press Ctrl-Y + REDO(infoArray); + Assertions.assertEquals(2, pdfTree.getChildCount("ID"), "Array Tree still has 2 children: "); + // Test Table Value + Assertions.assertEquals(TEST_VALUE, (String) infoArray.getContentAt(0, 0), "Value of cell content is equal to the test value again: "); + } + + @Test + @Order(2) + public void phase2canRedoArrayDeletion() { + // run canUndo test + phase1canUndoArrayDeletion(); + Table infoArray = objectPanel.getTable(); + // Press Ctrl-Y + REDO(infoArray); + Assertions.assertEquals(1, pdfTree.getChildCount("ID"), "Array Tree has 1 child again: "); + // Test Table Length + Assertions.assertEquals(2, infoArray.getRowCount(), "Table is now 2 Cells Tall again: "); + } + + @Test + @Order(2) + public void phase2canRedoDictAddition() { + phase1canUndoDictAddition(); + Table infoDict = objectPanel.getTable(); + REDO(infoDict); + Assertions.assertEquals(4,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 4 children: "); + Assertions.assertEquals(5, infoDict.getRowCount(), "Table is now 5 Rows tall: "); + Assertions.assertEquals(TEST_KEY, (String) infoDict.getContentAt(3,0), "New Key is equal to Test Key: "); + Assertions.assertEquals(TEST_VALUE, (String) infoDict.getContentAt(3,1), "New Value is equal to Test Value: "); + } + + @Test + @Order(2) + public void phase2canRedoDictUpdate() { + phase1canUndoDictUpdate(); + Table infoDict = objectPanel.getTable(); + REDO(infoDict); + Assertions.assertEquals(3,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 3 children: "); + Assertions.assertEquals(4, infoDict.getRowCount(), "Table is still 4 Rows tall: "); + Assertions.assertEquals(MODIFICATION_KEY, (String) infoDict.getContentAt(2,0), "Modification Key is unchanged: "); + Assertions.assertEquals(TEST_VALUE, (String) infoDict.getContentAt(2,1), "New Value is equal to Test Value: "); + } + + @Test + @Order(2) + public void phase2canRedoDictDeletion() { + phase1canUndoDictDeletion(); + Table infoDict = objectPanel.getTable(); + REDO(infoDict); + Assertions.assertEquals(2,pdfTree.getChildCount("Info/Dictionary"), "Dictionary Tree has 2 children: "); + Assertions.assertEquals(3, infoDict.getRowCount(), "Table is now 3 Rows tall: "); + } +} diff --git a/src/test/java/com/itextpdf/rups/view/RupsWindowTest.java b/src/test/java/com/itextpdf/rups/view/RupsWindowTest.java new file mode 100644 index 00000000..60efc4bb --- /dev/null +++ b/src/test/java/com/itextpdf/rups/view/RupsWindowTest.java @@ -0,0 +1,273 @@ +/* + This file is part of the iText (R) project. + Copyright (c) 1998-2022 iText Group NV + Authors: iText Software. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License version 3 + as published by the Free Software Foundation with the addition of the + following permission added to Section 15 as permitted in Section 7(a): + FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY + ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT + OF THIRD PARTY RIGHTS + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses or write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA, 02110-1301 USA, or download the license from the following URL: + http://itextpdf.com/terms-of-use/ + + The interactive user interfaces in modified source and object code versions + of this program must display Appropriate Legal Notices, as required under + Section 5 of the GNU Affero General Public License. + + In accordance with Section 7(b) of the GNU Affero General Public License, + a covered work must retain the producer line in every PDF that is created + or manipulated using iText. + + You can be released from the requirements of the license by purchasing + a commercial license. Buying such a license is mandatory as soon as you + develop commercial activities involving the iText software without + disclosing the source code of your own applications. + These activities include: offering paid services to customers as an ASP, + serving PDFs on the fly in a web application, shipping iText with a closed + source product. + + For more information, please contact iText Software Corp. at this + address: sales@itextpdf.com + */ +package com.itextpdf.rups.view; + +import com.itextpdf.rups.RupsLauncher; +import org.junit.jupiter.api.Tag; +import org.uispec4j.UISpec4J; +import org.uispec4j.UISpecAdapter; +import org.uispec4j.Window; +import org.uispec4j.assertion.Assertion; +import org.uispec4j.assertion.UISpecAssert; +import org.uispec4j.interception.MainClassAdapter; +import org.uispec4j.interception.toolkit.UISpecDisplay; + +@Tag("Integration") +@Tag("GUI") +public abstract class RupsWindowTest{ + static final String ADAPTER_CLASS_PROPERTY = "uispec4j.adapter"; + static final String PROPERTY_NOT_DEFINED; + + private UISpecAdapter adapter; + + static { + PROPERTY_NOT_DEFINED = + "Adapter class not defined - the '" + ADAPTER_CLASS_PROPERTY + + "' property must refer to a class implementing the UISpecAdapter interface"; + UISpec4J.init(); + } + + public RupsWindowTest() { + this("Window Test"); + } + public RupsWindowTest(String testName) { + } + + public void setAdapter(UISpecAdapter adapter) { + this.adapter = adapter; + } + + + protected void setUp() throws Exception { + setUp(""); + } + + /** + * Initializes the resources needed by the test case.
+ * NB: If you provide your own implementation, do not forget to call this one first. + */ + + + protected void setUp(String filePath) throws Exception { + String fileName = filePath.substring(1 + filePath.lastIndexOf('/')); + UISpecDisplay.instance().reset(); + setAdapter(new MainClassAdapter(RupsLauncher.class, new String[]{filePath})); + waitUntil(getMainWindow().containsSwingComponent(RupsPanel.class), 8000); + } + + /** + * Checks whether an unexpected exception had occurred, and releases the test resources. + */ + protected void tearDown() throws Exception { + adapter = null; + UISpecDisplay.instance().rethrowIfNeeded(); + UISpecDisplay.instance().reset(); + } + + private void retrieveAdapter() throws AdapterNotFoundException { + String adapterClassName = System.getProperty(ADAPTER_CLASS_PROPERTY); + if (adapterClassName == null) { + throw new AdapterNotFoundException(); + } + try { + adapter = (UISpecAdapter)Class.forName(adapterClassName).getDeclaredConstructor().newInstance(); + } + catch (Exception e) { + throw new AdapterNotFoundException(adapterClassName, e); + } + } + + /** + * Returns the Window created by the adapter. + * + * @throws AdapterNotFoundException if the uispec4j.adapter property does not refer + * to a valid adapter + */ + public Window getMainWindow() throws AdapterNotFoundException { + // TODO: Find a way to intercept and interact with Modal Dialogs + return getAdapter().getMainWindow(); + } + + /** + * Checks the given assertion. + * This method is equivalent to {@link #assertThat(Assertion)}. + * + * @see UISpecAssert#assertTrue(Assertion) + */ + public void assertTrue(Assertion assertion) { + UISpecAssert.assertTrue(assertion); + } + + /** + * Checks the given assertion. + * If it fails an AssertionError is thrown with the given message. + * This method is equivalent to {@link #assertThat(String,Assertion)}. + * + * @see UISpecAssert#assertTrue(String,Assertion) + */ + public void assertTrue(String message, Assertion assertion) { + UISpecAssert.assertTrue(message, assertion); + } + + /** + * Checks the given assertion. + * This method is equivalent to {@link #assertTrue(Assertion)}. + * + * @see UISpecAssert#assertThat(Assertion) + */ + public void assertThat(Assertion assertion) { + UISpecAssert.assertThat(assertion); + } + + /** + * Checks the given assertion. + * If it fails an AssertionError is thrown with the given message. + * This method is equivalent to {@link #assertTrue(String,Assertion)}. + * + * @see UISpecAssert#assertTrue(String,Assertion) + */ + public void assertThat(String message, Assertion assertion) { + UISpecAssert.assertThat(message, assertion); + } + + /** + * Waits for at most 'waitTimeLimit' ms until the assertion is true. + * + * @see UISpecAssert#waitUntil(Assertion, long) + */ + public void waitUntil(Assertion assertion, long waitTimeLimit) { + UISpecAssert.waitUntil(assertion, waitTimeLimit); + } + + /** + * Checks that the given assertion fails. + * + * @see UISpecAssert#assertFalse(Assertion) + */ + public void assertFalse(Assertion assertion) { + UISpecAssert.assertFalse(assertion); + } + + /** + * Waits for at most 'waitTimeLimit' ms until the assertion is true. + * If it fails an AssertionError is thrown with the given message. + * + * @see UISpecAssert#waitUntil(String,Assertion,long) + */ + public void waitUntil(String message, Assertion assertion, long waitTimeLimit) { + UISpecAssert.waitUntil(message, assertion, waitTimeLimit); + } + + /** + * Checks that the given assertion fails. + * If it succeeds an AssertionError is thrown with the given message. + * + * @see UISpecAssert#assertFalse(String,Assertion) + */ + public void assertFalse(String message, Assertion assertion) { + UISpecAssert.assertFalse(message, assertion); + } + + /** + * Returns a negation of the given assertion. + * + * @see UISpecAssert#not(Assertion) + */ + public Assertion not(Assertion assertion) { + return UISpecAssert.not(assertion); + } + + /** + * Returns the intersection of two assertions. + * + * @see UISpecAssert#and(Assertion[]) + */ + public Assertion and(Assertion... assertions) { + return UISpecAssert.and(assertions); + } + + /** + * Returns the union of two assertions. + * + * @see UISpecAssert#or(Assertion[]) + */ + public Assertion or(Assertion... assertions) { + return UISpecAssert.or(assertions); + } + + /** + * Checks that the given assertion equals the expected parameter. + * + * @see UISpecAssert#assertEquals(boolean,Assertion) + */ + public void assertEquals(boolean expected, Assertion assertion) { + UISpecAssert.assertEquals(expected, assertion); + } + + /** + * Checks that the given assertion equals the expected parameter. + * If it fails an AssertionError is thrown with the given message. + * + * @see UISpecAssert#assertEquals(String,boolean,Assertion) + */ + public void assertEquals(String message, boolean expected, Assertion assertion) { + UISpecAssert.assertEquals(message, expected, assertion); + } + + private UISpecAdapter getAdapter() throws AdapterNotFoundException { + if (adapter == null) { + retrieveAdapter(); + } + return adapter; + } + + static class AdapterNotFoundException extends RuntimeException { + public AdapterNotFoundException() { + super(PROPERTY_NOT_DEFINED); + } + + public AdapterNotFoundException(String adapterClassName, Exception e) { + super("Adapter class '" + adapterClassName + "' not found", e); + } + } +} \ No newline at end of file