1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 /* $Id: DocumentNavigationHandler.java 1844636 2018-10-23 11:56:21Z ssteiner $ */
19 
20 package org.apache.fop.render.intermediate.extensions;
21 
22 import java.awt.Point;
23 import java.awt.Rectangle;
24 import java.util.Map;
25 import java.util.Stack;
26 
27 import org.xml.sax.Attributes;
28 import org.xml.sax.SAXException;
29 import org.xml.sax.helpers.DefaultHandler;
30 
31 import org.apache.commons.logging.Log;
32 import org.apache.commons.logging.LogFactory;
33 
34 import org.apache.fop.accessibility.StructureTreeElement;
35 import org.apache.fop.fo.extensions.InternalElementMapping;
36 import org.apache.fop.render.intermediate.IFDocumentNavigationHandler;
37 import org.apache.fop.render.intermediate.IFException;
38 import org.apache.fop.render.intermediate.PageIndexContext;
39 import org.apache.fop.util.XMLUtil;
40 
41 /**
42  * ContentHandler that handles the IF document navigation namespace.
43  */
44 public class DocumentNavigationHandler extends DefaultHandler
45         implements DocumentNavigationExtensionConstants {
46 
47     /** Logger instance */
48     protected static final Log log = LogFactory.getLog(DocumentNavigationHandler.class);
49 
50     private StringBuffer content = new StringBuffer();
51     private Stack objectStack = new Stack();
52 
53     private IFDocumentNavigationHandler navHandler;
54 
55     private StructureTreeElement structureTreeElement;
56 
57     private Map<String, StructureTreeElement> structureTreeElements;
58 
59     /**
60      * Main constructor.
61      * @param navHandler the navigation handler that will receive the events
62      * @param structureTreeElements the elements representing the structure of the document
63      */
DocumentNavigationHandler(IFDocumentNavigationHandler navHandler, Map<String, StructureTreeElement> structureTreeElements)64     public DocumentNavigationHandler(IFDocumentNavigationHandler navHandler,
65             Map<String, StructureTreeElement> structureTreeElements) {
66         this.navHandler = navHandler;
67         assert structureTreeElements != null;
68         this.structureTreeElements = structureTreeElements;
69     }
70 
71     /** {@inheritDoc} */
startElement(String uri, String localName, String qName, Attributes attributes)72     public void startElement(String uri, String localName, String qName, Attributes attributes)
73             throws SAXException {
74         boolean handled = false;
75         if (NAMESPACE.equals(uri)) {
76             if (BOOKMARK_TREE.getLocalName().equals(localName)) {
77                 if (!objectStack.isEmpty()) {
78                     throw new SAXException(localName + " must be the root element!");
79                 }
80                 BookmarkTree bookmarkTree = new BookmarkTree();
81                 objectStack.push(bookmarkTree);
82             } else if (BOOKMARK.getLocalName().equals(localName)) {
83                 String title = attributes.getValue("title");
84                 String s = attributes.getValue("starting-state");
85                 boolean show = !"hide".equals(s);
86                 Bookmark b = new Bookmark(title, show, null);
87                 Object o = objectStack.peek();
88                 if (o instanceof AbstractAction) {
89                     AbstractAction action = (AbstractAction)objectStack.pop();
90                     o = objectStack.peek();
91                     ((Bookmark)o).setAction(action);
92                 }
93                 if (o instanceof BookmarkTree) {
94                     ((BookmarkTree)o).addBookmark(b);
95                 } else {
96                     ((Bookmark)o).addChildBookmark(b);
97                 }
98                 objectStack.push(b);
99             } else if (NAMED_DESTINATION.getLocalName().equals(localName)) {
100                 if (!objectStack.isEmpty()) {
101                     throw new SAXException(localName + " must be the root element!");
102                 }
103                 String name = attributes.getValue("name");
104                 NamedDestination dest = new NamedDestination(name, null);
105                 objectStack.push(dest);
106             } else if (LINK.getLocalName().equals(localName)) {
107                 if (!objectStack.isEmpty()) {
108                     throw new SAXException(localName + " must be the root element!");
109                 }
110                 Rectangle targetRect = XMLUtil.getAttributeAsRectangle(attributes, "rect");
111                 structureTreeElement = structureTreeElements.get(attributes.getValue(
112                         InternalElementMapping.URI, InternalElementMapping.STRUCT_REF));
113                 Link link = new Link(null, targetRect);
114                 objectStack.push(link);
115             } else if (GOTO_XY.getLocalName().equals(localName)) {
116                 String idref = attributes.getValue("idref");
117                 GoToXYAction action;
118                 if (idref != null) {
119                     action = new GoToXYAction(idref);
120                 } else {
121                     String id = attributes.getValue("id");
122                     int pageIndex = XMLUtil.getAttributeAsInt(attributes, "page-index");
123                     int pageIndexRelative = XMLUtil.getAttributeAsInt(attributes, "page-index-relative", 0);
124                     final Point location;
125                     if (pageIndex < 0) {
126                         location = null;
127                     } else {
128                         if (hasNavigation() && !inBookmark() && pageIndexRelative >= 0) {
129                             int currentPageIndex = navHandler.getPageIndex();
130                             if (currentPageIndex >= 0) {
131                                 pageIndex = currentPageIndex;
132                             }
133                         }
134                         final int x = XMLUtil
135                                 .getAttributeAsInt(attributes, "x");
136                         final int y = XMLUtil
137                                 .getAttributeAsInt(attributes, "y");
138                         location = new Point(x, y);
139                     }
140                     action = new GoToXYAction(id, pageIndex, location,
141                             new PageIndexRelative(pageIndex, pageIndexRelative));
142                 }
143                 if (structureTreeElement != null) {
144                     action.setStructureTreeElement(structureTreeElement);
145                 }
146                 objectStack.push(action);
147             } else if (GOTO_URI.getLocalName().equals(localName)) {
148                 String id = attributes.getValue("id");
149                 String gotoURI = attributes.getValue("uri");
150                 String showDestination = attributes.getValue("show-destination");
151                 boolean newWindow = "new".equals(showDestination);
152                 URIAction action = new URIAction(gotoURI, newWindow);
153                 if (id != null) {
154                     action.setID(id);
155                 }
156                 if (structureTreeElement != null) {
157                     action.setStructureTreeElement(structureTreeElement);
158                 }
159                 objectStack.push(action);
160             } else {
161                 throw new SAXException(
162                         "Invalid element '" + localName + "' in namespace: " + uri);
163             }
164             handled = true;
165         }
166         if (!handled) {
167             if (NAMESPACE.equals(uri)) {
168                 throw new SAXException("Unhandled element '" + localName + "' in namespace: "
169                         + uri);
170             } else {
171                 log.warn("Unhandled element '" + localName + "' in namespace: " + uri);
172             }
173         }
174     }
175 
176     static class PageIndexRelative implements PageIndexContext {
177         private int pageIndex;
PageIndexRelative(int pageIndex, int pageIndexRelative)178         PageIndexRelative(int pageIndex, int pageIndexRelative) {
179             this.pageIndex = (pageIndexRelative * -1) + pageIndex;
180         }
getPageIndex()181         public int getPageIndex() {
182             return pageIndex;
183         }
184     }
185 
inBookmark()186     private boolean inBookmark() {
187         return !objectStack.empty() && objectStack.peek() instanceof Bookmark;
188     }
189 
190     /** {@inheritDoc} */
endElement(String uri, String localName, String qName)191     public void endElement(String uri, String localName, String qName) throws SAXException {
192         if (NAMESPACE.equals(uri)) {
193             try {
194                 if (BOOKMARK_TREE.getLocalName().equals(localName)) {
195                     BookmarkTree tree = (BookmarkTree)objectStack.pop();
196                     if (hasNavigation()) {
197                         this.navHandler.renderBookmarkTree(tree);
198                     }
199                 } else if (BOOKMARK.getLocalName().equals(localName)) {
200                     if (objectStack.peek() instanceof AbstractAction) {
201                         AbstractAction action = (AbstractAction)objectStack.pop();
202                         Bookmark b = (Bookmark)objectStack.pop();
203                         b.setAction(action);
204                     } else {
205                         objectStack.pop();
206                     }
207                 } else if (NAMED_DESTINATION.getLocalName().equals(localName)) {
208                     AbstractAction action = (AbstractAction)objectStack.pop();
209                     NamedDestination dest = (NamedDestination)objectStack.pop();
210                     dest.setAction(action);
211                     if (hasNavigation()) {
212                         this.navHandler.renderNamedDestination(dest);
213                     }
214                 } else if (LINK.getLocalName().equals(localName)) {
215                     AbstractAction action = (AbstractAction)objectStack.pop();
216                     Link link = (Link)objectStack.pop();
217                     link.setAction(action);
218                     if (hasNavigation()) {
219                         this.navHandler.renderLink(link);
220                     }
221                 } else if (localName.startsWith("goto-")) {
222                     if (objectStack.size() == 1) {
223                         //Stand-alone action
224                         AbstractAction action = (AbstractAction)objectStack.pop();
225                         if (hasNavigation()) {
226                             this.navHandler.addResolvedAction(action);
227                         }
228                     }
229                 }
230             } catch (IFException ife) {
231                 throw new SAXException(ife);
232             }
233         }
234         content.setLength(0); // Reset text buffer (see characters())
235     }
236 
hasNavigation()237     private boolean hasNavigation() {
238         return this.navHandler != null;
239     }
240 
241     /** {@inheritDoc} */
characters(char[] ch, int start, int length)242     public void characters(char[] ch, int start, int length) throws SAXException {
243         content.append(ch, start, length);
244     }
245 
246     /** {@inheritDoc} */
endDocument()247     public void endDocument() throws SAXException {
248         assert objectStack.isEmpty();
249     }
250 
251 }
252