/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* $Id$ */ package org.apache.fop.area; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fop.fo.Constants; import org.apache.fop.fo.extensions.ExtensionAttachment; import org.apache.fop.fo.pagination.SimplePageMaster; /** * Page viewport that specifies the viewport area and holds the page contents. * This is the top level object for a page and remains valid for the life * of the document and the area tree. * This object may be used as a key to reference a page. * This is the level that creates the page. * The page (reference area) is then rendered inside the page object */ public class PageViewport extends AreaTreeObject implements Resolvable, Cloneable { private Page page; private Rectangle2D viewArea; private String simplePageMasterName; /** * Unique key to identify the page. pageNumberString and pageIndex are both no option * for this. */ private String pageKey; private int pageNumber = -1; private String pageNumberString = null; private int pageIndex = -1; //-1 = undetermined private boolean blank; private transient PageSequence pageSequence; // list of id references and the rectangle on the page //private Map idReferences = null; // set of IDs that appear first (or exclusively) on this page: private Set idFirsts = new java.util.HashSet(); // this keeps a list of currently unresolved areas or extensions // once an idref is resolved it is removed // when this is empty the page can be rendered private Map unresolvedIDRefs = new java.util.HashMap(); private Map pendingResolved = null; // hashmap of markers for this page // start and end are added by the fo that contains the markers private Map markerFirstStart = null; private Map markerLastStart = null; private Map markerFirstAny = null; private Map markerLastEnd = null; private Map markerLastAny = null; //Arbitrary attachments to the page from extensions that need to pass information //down to the renderers. private List extensionAttachments = null; /** * logging instance */ protected static Log log = LogFactory.getLog(PageViewport.class); /** * Create a page viewport. * @param spm SimplePageMaster indicating the page and region dimensions * @param pageNumber the page number * @param pageStr String representation of the page number * @param blank true if this is a blank page */ public PageViewport(SimplePageMaster spm, int pageNumber, String pageStr, boolean blank) { this.simplePageMasterName = spm.getMasterName(); this.extensionAttachments = spm.getExtensionAttachments(); this.blank = blank; int pageWidth = spm.getPageWidth().getValue(); int pageHeight = spm.getPageHeight().getValue(); this.pageNumber = pageNumber; this.pageNumberString = pageStr; this.viewArea = new Rectangle(0, 0, pageWidth, pageHeight); this.page = new Page(spm); createSpan(false); } /** * Copy constructor. * @param original the original PageViewport to copy from */ public PageViewport(PageViewport original) { if (original.extensionAttachments != null) { this.extensionAttachments = new java.util.ArrayList(original.extensionAttachments); } this.pageIndex = original.pageIndex; this.pageNumber = original.pageNumber; this.pageNumberString = original.pageNumberString; this.page = (Page)original.page.clone(); this.viewArea = (Rectangle2D)original.viewArea.clone(); this.simplePageMasterName = original.simplePageMasterName; this.blank = original.blank; } /** * Constructor used by the area tree parser. * @param viewArea the view area * @param pageNumber the page number * @param pageStr String representation of the page number * @param simplePageMasterName name of the original simple-page-master that generated this page * @param blank true if this is a blank page */ public PageViewport(Rectangle2D viewArea, int pageNumber, String pageStr, String simplePageMasterName, boolean blank) { this.viewArea = viewArea; this.pageNumber = pageNumber; this.pageNumberString = pageStr; this.simplePageMasterName = simplePageMasterName; this.blank = blank; } /** * Sets the page sequence this page belongs to * @param seq the page sequence */ public void setPageSequence(PageSequence seq) { this.pageSequence = seq; } /** @return the page sequence this page belongs to */ public PageSequence getPageSequence() { return this.pageSequence; } /** * Get the view area rectangle of this viewport. * @return the rectangle for this viewport */ public Rectangle2D getViewArea() { return viewArea; } /** * Get the page reference area with the contents. * @return the page reference area */ public Page getPage() { return page; } /** * Sets the page object for this PageViewport. * @param page the page */ public void setPage(Page page) { this.page = page; } /** * Get the page number of this page. * @return the integer value that represents this page */ public int getPageNumber() { return pageNumber; } /** * Get the page number of this page. * @return the string that represents this page */ public String getPageNumberString() { return pageNumberString; } /** * Sets the page index of the page in this rendering run. * (This is not the same as the page number!) * @param index the page index (zero-based), -1 if it is undetermined */ public void setPageIndex(int index) { this.pageIndex = index; } /** * @return the overall page index of the page in this rendering run (zero-based, * -1 if it is undetermined). */ public int getPageIndex() { return this.pageIndex; } /** * Sets the unique key for this PageViewport that will be used to reference this page. * @param key the unique key. */ public void setKey(String key) { this.pageKey = key; } /** * Get the key for this page viewport. * This is used so that a serializable key can be used to * lookup the page or some other reference. * * @return a unique page viewport key for this area tree */ public String getKey() { if (this.pageKey == null) { throw new IllegalStateException("No page key set on the PageViewport: " + toString()); } return this.pageKey; } /** * Add an "ID-first" to this page. * This is typically called by the AreaTreeHandler when associating * an ID with a PageViewport. * * @param id the id to be registered as first appearing on this page */ public void setFirstWithID(String id) { if (id != null) { idFirsts.add(id); } } /** * Check whether a certain id first appears on this page * * @param id the id to be checked * @return true if this page is the first where the id appears */ public boolean isFirstWithID(String id) { return idFirsts.contains(id); } /** * Add an idref to this page. * All idrefs found for child areas of this PageViewport are added * to unresolvedIDRefs, for subsequent resolution by AreaTreeHandler * calls to this object's resolveIDRef(). * * @param idref the idref * @param res the child element of this page that needs this * idref resolved */ public void addUnresolvedIDRef(String idref, Resolvable res) { if (unresolvedIDRefs == null) { unresolvedIDRefs = new HashMap(); } List list = (List)unresolvedIDRefs.get(idref); if (list == null) { list = new ArrayList(); unresolvedIDRefs.put(idref, list); } list.add(res); } /** * Check if this page has been fully resolved. * @return true if the page is resolved and can be rendered */ public boolean isResolved() { return unresolvedIDRefs == null || unresolvedIDRefs.size() == 0; } /** * Get the unresolved idrefs for this page. * @return String array of idref's that still have not been resolved */ public String[] getIDRefs() { return (unresolvedIDRefs == null) ? null : (String[]) unresolvedIDRefs.keySet().toArray(new String[] {}); } /** * {@inheritDoc} */ public void resolveIDRef(String id, List pages) { if (page == null) { if (pendingResolved == null) { pendingResolved = new HashMap(); } pendingResolved.put(id, pages); } else { if (unresolvedIDRefs != null) { List todo = (List)unresolvedIDRefs.get(id); if (todo != null) { for (int count = 0; count < todo.size(); count++) { Resolvable res = (Resolvable)todo.get(count); res.resolveIDRef(id, pages); } } } } if (unresolvedIDRefs != null && pages != null) { unresolvedIDRefs.remove(id); if (unresolvedIDRefs.isEmpty()) { unresolvedIDRefs = null; } } } /** * Add the markers for this page. * Only the required markers are kept. * For "first-starting-within-page" it adds the markers * that are starting only if the marker class name is not * already added. * For "first-including-carryover" it adds any starting marker * if the marker class name is not already added. * For "last-starting-within-page" it adds all marks that * are starting, replacing earlier markers. * For "last-ending-within-page" it adds all markers that * are ending, replacing earlier markers. * * Should this logic be placed in the Page layout manager. * * @param marks the map of markers to add * @param starting if the area being added is starting or ending * @param isfirst if the area being added has is-first trait * @param islast if the area being added has is-last trait */ public void addMarkers(Map marks, boolean starting, boolean isfirst, boolean islast) { if (marks == null) { return; } if (log.isDebugEnabled()) { log.debug("--" + marks.keySet() + ": " + (starting ? "starting" : "ending") + (isfirst ? ", first" : "") + (islast ? ", last" : "")); } // at the start of the area, register is-first and any areas if (starting) { if (isfirst) { if (markerFirstStart == null) { markerFirstStart = new HashMap(); } if (markerFirstAny == null) { markerFirstAny = new HashMap(); } // first on page: only put in new values, leave current for (Iterator iter = marks.keySet().iterator(); iter.hasNext();) { Object key = iter.next(); if (!markerFirstStart.containsKey(key)) { markerFirstStart.put(key, marks.get(key)); if (log.isTraceEnabled()) { log.trace("page " + pageNumberString + ": " + "Adding marker " + key + " to FirstStart"); } } if (!markerFirstAny.containsKey(key)) { markerFirstAny.put(key, marks.get(key)); if (log.isTraceEnabled()) { log.trace("page " + pageNumberString + ": " + "Adding marker " + key + " to FirstAny"); } } } if (markerLastStart == null) { markerLastStart = new HashMap(); } // last on page: replace all markerLastStart.putAll(marks); if (log.isTraceEnabled()) { log.trace("page " + pageNumberString + ": " + "Adding all markers to LastStart"); } } else { if (markerFirstAny == null) { markerFirstAny = new HashMap(); } // first on page: only put in new values, leave current for (Iterator iter = marks.keySet().iterator(); iter.hasNext();) { Object key = iter.next(); if (!markerFirstAny.containsKey(key)) { markerFirstAny.put(key, marks.get(key)); if (log.isTraceEnabled()) { log.trace("page " + pageNumberString + ": " + "Adding marker " + key + " to FirstAny"); } } } } } else { // at the end of the area, register is-last and any areas if (islast) { if (markerLastEnd == null) { markerLastEnd = new HashMap(); } // last on page: replace all markerLastEnd.putAll(marks); if (log.isTraceEnabled()) { log.trace("page " + pageNumberString + ": " + "Adding all markers to LastEnd"); } } if (markerLastAny == null) { markerLastAny = new HashMap(); } // last on page: replace all markerLastAny.putAll(marks); if (log.isTraceEnabled()) { log.trace("page " + pageNumberString + ": " + "Adding all markers to LastAny"); } } } /** * Get a marker from this page. * This will retrieve a marker with the class name * and position. * * @param name The class name of the marker to retrieve * @param pos the position to retrieve * @return Object the marker found or null */ public Object getMarker(String name, int pos) { Object mark = null; String posName = null; switch (pos) { case Constants.EN_FSWP: if (markerFirstStart != null) { mark = markerFirstStart.get(name); posName = "FSWP"; } if (mark == null && markerFirstAny != null) { mark = markerFirstAny.get(name); posName = "FirstAny after " + posName; } break; case Constants.EN_FIC: if (markerFirstAny != null) { mark = markerFirstAny.get(name); posName = "FIC"; } break; case Constants.EN_LSWP: if (markerLastStart != null) { mark = markerLastStart.get(name); posName = "LSWP"; } if (mark == null && markerLastAny != null) { mark = markerLastAny.get(name); posName = "LastAny after " + posName; } break; case Constants.EN_LEWP: if (markerLastEnd != null) { mark = markerLastEnd.get(name); posName = "LEWP"; } if (mark == null && markerLastAny != null) { mark = markerLastAny.get(name); posName = "LastAny after " + posName; } break; default: throw new RuntimeException(); } if (log.isTraceEnabled()) { log.trace("page " + pageNumberString + ": " + "Retrieving marker " + name + " at position " + posName); } return mark; } /** Dumps the current marker data to the logger. */ public void dumpMarkers() { if (log.isTraceEnabled()) { log.trace("FirstAny: " + this.markerFirstAny); log.trace("FirstStart: " + this.markerFirstStart); log.trace("LastAny: " + this.markerLastAny); log.trace("LastEnd: " + this.markerLastEnd); log.trace("LastStart: " + this.markerLastStart); } } /** * Save the page contents to an object stream. * The map of unresolved references are set on the page so that * the resolvers can be properly serialized and reloaded. * @param out the object output stream to write the contents * @throws IOException in case of an I/O error while serializing the page */ public void savePage(ObjectOutputStream out) throws IOException { // set the unresolved references so they are serialized page.setUnresolvedReferences(unresolvedIDRefs); out.writeObject(page); page = null; } /** * Load the page contents from an object stream. * This loads the page contents from the stream and * if there are any unresolved references that were resolved * while saved they will be resolved on the page contents. * @param in the object input stream to read the page from * @throws ClassNotFoundException if a class was not found while loading the page * @throws IOException if an I/O error occurred while loading the page */ public void loadPage(ObjectInputStream in) throws IOException, ClassNotFoundException { page = (Page) in.readObject(); unresolvedIDRefs = page.getUnresolvedReferences(); if (unresolvedIDRefs != null && pendingResolved != null) { for (Iterator iter = pendingResolved.keySet().iterator(); iter.hasNext();) { String id = (String) iter.next(); resolveIDRef(id, (List)pendingResolved.get(id)); } pendingResolved = null; } } /** * Clone this page. * Used by the page master to create a copy of an original page. * @return a copy of this page and associated viewports */ public Object clone() { return new PageViewport(this); } /** * Clear the page contents to save memory. * This object is kept for the life of the area tree since * it holds id and marker information and is used as a key. */ public void clear() { page = null; } /** * {@inheritDoc} */ public String toString() { StringBuffer sb = new StringBuffer(64); sb.append("PageViewport: page="); sb.append(getPageNumberString()); return sb.toString(); } /** @return the name of the simple-page-master that created this page */ public String getSimplePageMasterName() { return this.simplePageMasterName; } /** * Adds a new ExtensionAttachment instance to this page. * @param attachment the ExtensionAttachment */ public void addExtensionAttachment(ExtensionAttachment attachment) { if (this.extensionAttachments == null) { this.extensionAttachments = new java.util.ArrayList(); } extensionAttachments.add(attachment); } /** @return the list of extension attachments for this page */ public List getExtensionAttachments() { if (this.extensionAttachments == null) { return Collections.EMPTY_LIST; } else { return this.extensionAttachments; } } /** @return True if this is a blank page. */ public boolean isBlank() { return this.blank; } /** * Convenience method to get BodyRegion of this PageViewport * @return BodyRegion object */ public BodyRegion getBodyRegion() { return (BodyRegion) getPage().getRegionViewport( Constants.FO_REGION_BODY).getRegionReference(); } /** * Convenience method to create a new Span for this * this PageViewport. * * @param spanAll whether this is a single-column span * @return Span object created */ public Span createSpan(boolean spanAll) { return getBodyRegion().getMainReference().createSpan(spanAll); } /** * Convenience method to get the span-reference-area currently * being processed * * @return span currently being processed. */ public Span getCurrentSpan() { return getBodyRegion().getMainReference().getCurrentSpan(); } /** * Convenience method to get the normal-flow-reference-area * currently being processed * * @return span currently being processed. */ public NormalFlow getCurrentFlow() { return getCurrentSpan().getCurrentFlow(); } /** * Convenience method to increment the Span to the * next NormalFlow to be processed, and to return that flow. * * @return the next NormalFlow in the Span. */ public NormalFlow moveToNextFlow() { return getCurrentSpan().moveToNextFlow(); } /** * Convenience method to return a given region-reference-area, * keyed by the Constants class identifier for the corresponding * formatting object (ie. Constants.FO_REGION_BODY, FO_REGION_START, * etc.) * * @param id the Constants class identifier for the region. * @return the corresponding region-reference-area for this page. */ public RegionReference getRegionReference(int id) { return getPage().getRegionViewport(id).getRegionReference(); } /** @return whether this page viewport has any extension attachments */ public boolean hasExtensionAttachments() { return this.extensionAttachments != null && !this.extensionAttachments.isEmpty(); } }