diff --git a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java index 26f473088..a45ed2197 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java +++ b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java @@ -41,6 +41,8 @@ import org.apache.poi.ooxml.POIXMLDocumentPart; import org.apache.poi.ooxml.POIXMLException; import org.apache.poi.ooxml.POIXMLProperties; import org.apache.poi.ooxml.POIXMLRelation; +import org.apache.poi.ooxml.util.IdentifierManager; +import org.apache.poi.ooxml.util.PackageHelper; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.exceptions.OpenXML4JException; import org.apache.poi.openxml4j.opc.OPCPackage; @@ -84,6 +86,7 @@ import org.openxmlformats.schemas.wordprocessingml.x2006.main.STDocProtect; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STHdrFtr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STOnOff; import org.openxmlformats.schemas.wordprocessingml.x2006.main.StylesDocument; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.*; /** *
High(ish) level class for working with .docx files.
@@ -1653,4 +1656,33 @@ public class XWPFDocument extends POIXMLDocument implements Document, IBody { charts.add(xwpfChart); return xwpfChart; } + + /** + * Create a new footnote and add it to the document. + *The new note will have one paragraph with the style "FootnoteText" + * and one run containing the required footnote reference with the + * style "FootnoteReference". + * + * @return New XWPFFootnote. + */ + public XWPFFootnote createFootnote() { + XWPFFootnotes footnotes = this.createFootnotes(); + + XWPFFootnote footnote = footnotes.createFootnote(); + return footnote; + } + + /** + * Remove the specified footnote if present. + * + * @param pos + * @return True if the footnote was removed. + */ + public boolean removeFootnote(int pos) { + if (null != footnotes) { + return footnotes.removeFootnote(pos); + } else { + return false; + } + } } diff --git a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFFootnote.java b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFFootnote.java index a1be6a71a..9444dbf6e 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFFootnote.java +++ b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFFootnote.java @@ -1,4 +1,3 @@ -/* ==================================================================== /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with @@ -17,6 +16,7 @@ ==================================================================== */ package org.apache.poi.xwpf.usermodel; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -25,12 +25,28 @@ import org.apache.poi.ooxml.POIXMLDocumentPart; import org.apache.xmlbeans.XmlCursor; import org.apache.xmlbeans.XmlObject; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFtnEdn; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFtnEdnRef; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSdtBlock; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc; +/** + * Represents a bottom-of-the-page footnote. + *
Create a new footnote using {@link XWPFDocument#createFootnote()} or + * {@link XWPFFootnotes#createFootnote()}.
+ *The first body element of a footnote should (or possibly must) be a paragraph + * with the first run containing a CTFtnEdnRef object. The {@link XWPFFootnote#createParagraph()} + * and {@link XWPFFootnote#createTable()} methods do this for you.
+ *Footnotes have IDs that are unique across all footnotes in the document. You use + * the footnote ID to create a reference to a footnote from within a paragraph.
+ *To create a reference to a footnote within a paragraph you create a run + * with a CTFtnEdnRef that specifies the ID of the target paragraph. + * The {@link XWPFParagraph#addFootnoteReference(XWPFFootnote)} + * method does this for you.
+ */ public class XWPFFootnote implements IterableUse {@link XWPFDocument#createFootnote()} to create new footnotes.
+ * @param footnote The CTFtnEdn object that will underly the footnote. + */ public void setCTFtnEdn(CTFtnEdn footnote) { ctFtnEdn = footnote; } /** + * Gets the {@link XWPFTable} at the specified position from the footnote's table array. * @param pos in table array - * @return The table at position pos + * @return The {@link XWPFTable} at position pos, or null if there is no table at position pos. * @see org.apache.poi.xwpf.usermodel.IBody#getTableArray(int) */ public XWPFTable getTableArray(int pos) { @@ -119,16 +165,16 @@ public class XWPFFootnote implements IterableFootnote IDs are unique across all bottom-of-the-page and + * end note footnotes.
+ * + * @return Footnote ID + */ + public BigInteger getId() { + return this.ctFtnEdn.getId(); + } + + /** + * Appends a new {@link XWPFParagraph} to this footnote. + * + * @return The new {@link XWPFParagraph} + */ + public XWPFParagraph createParagraph() { + XWPFParagraph p = new XWPFParagraph(this.ctFtnEdn.addNewP(), this); + paragraphs.add(p); + bodyElements.add(p); + + // If the paragraph is the first paragraph in the footnote, + // ensure that it has a footnote reference run. + + if (p.equals(getParagraphs().get(0))) { + ensureFootnoteRef(p); + } + return p; + } + + /** + * Ensure that the specified paragraph has a reference marker for this + * footnote by adding a footnote reference if one is not found. + *This method is for the first paragraph in the footnote, not + * paragraphs that will refer to the footnote. For references to + * the footnote, use {@link XWPFParagraph#addFootnoteReference(XWPFFootnote)}. + *
+ *The first run of the first paragraph in a footnote should + * contain a {@link CTFtnEdnRef} object.
+ * + * @param p The {@link XWPFParagraph} to ensure + */ + public void ensureFootnoteRef(XWPFParagraph p) { + + XWPFRun r = null; + if (p.getRuns().size() > 0) { + r = p.getRuns().get(0); + } + if (r == null) { + r = p.createRun(); + } + CTR ctr = r.getCTR(); + boolean foundRef = false; + for (CTFtnEdnRef ref : ctr.getFootnoteReferenceList()) { + if (getId().equals(ref.getId())) { + foundRef = true; + break; + } + } + if (!foundRef) { + ctr.addNewRPr().addNewRStyle().setVal("FootnoteReference"); + ctr.addNewFootnoteRef(); + } + } + + /** + * Appends a new {@link XWPFTable} to this footnote + * + * @return The new {@link XWPFTable} + */ + public XWPFTable createTable() { + XWPFTable table = new XWPFTable(ctFtnEdn.addNewTbl(), this); + if (bodyElements.size() == 0) { + XWPFParagraph p = createParagraph(); + ensureFootnoteRef(p); + } + bodyElements.add(table); + tables.add(table); + return table; + } + + /** + * Appends a new {@link XWPFTable} to this footnote + * @param rows Number of rows to initialize the table with + * @param cols Number of columns to initialize the table with + * @return the new {@link XWPFTable} with the specified number of rows and columns + */ + public XWPFTable createTable(int rows, int cols) { + XWPFTable table = new XWPFTable(ctFtnEdn.addNewTbl(), this, rows, cols); + bodyElements.add(table); + tables.add(table); + return table; + } } diff --git a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFFootnotes.java b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFFootnotes.java index 7d7077b09..a4788673e 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFFootnotes.java +++ b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFFootnotes.java @@ -22,6 +22,7 @@ import static org.apache.poi.ooxml.POIXMLTypeLoader.DEFAULT_XML_OPTIONS; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.math.BigInteger; import java.util.ArrayList; import java.util.List; @@ -35,10 +36,14 @@ import org.apache.xmlbeans.XmlException; import org.apache.xmlbeans.XmlOptions; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFootnotes; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFtnEdn; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.openxmlformats.schemas.wordprocessingml.x2006.main.FootnotesDocument; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STFtnEdn; /** - * Looks after the collection of Footnotes for a document + * Looks after the collection of Footnotes for a document. + * Manages both bottom-of-the-page footnotes and end notes. */ public class XWPFFootnotes extends POIXMLDocumentPart { protected XWPFDocument document; @@ -157,4 +162,39 @@ public class XWPFFootnotes extends POIXMLDocumentPart { public void setXWPFDocument(XWPFDocument doc) { document = doc; } + + /** + * Create a new footnote and add it to the document. + *The new note will have one paragraph with the style "FootnoteText" + * and one run containing the required footnote reference with the + * style "FootnoteReference". + *
+ * @return New XWPFFootnote + */ + public XWPFFootnote createFootnote() { + CTFtnEdn newNote = CTFtnEdn.Factory.newInstance(); + newNote.setType(STFtnEdn.NORMAL); + + XWPFFootnote footnote = addFootnote(newNote); + int id = ctFootnotes.sizeOfFootnoteArray(); + footnote.getCTFtnEdn().setId(BigInteger.valueOf(id)); + return footnote; + + } + + /** + * Remove the specified footnote if present. + * + * @param pos + * @return True if the footnote was removed. + */ + public boolean removeFootnote(int pos) { + if (ctFootnotes.sizeOfFootnoteArray() >= pos - 1) { + ctFootnotes.removeFootnote(pos); + listFootnote.remove(pos); + return true; + } else { + return false; + } + } } diff --git a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java index 1c033f61b..661e1c1ab 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java +++ b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.List; import org.apache.poi.ooxml.POIXMLDocumentPart; +import org.apache.poi.ss.formula.eval.NotImplementedException; import org.apache.poi.util.Internal; import org.apache.poi.wp.usermodel.Paragraph; import org.apache.xmlbeans.XmlCursor; @@ -1667,4 +1668,17 @@ public class XWPFParagraph implements IBodyElement, IRunBody, ISDTContents, Para } return null; } + + /** + * Add a new run with a reference to the specified footnote. + * The footnote reference run will have the style name "FootnoteReference". + * + * @param footnote Footnote to which to add a reference. + */ + public void addFootnoteReference(XWPFFootnote footnote) { + XWPFRun run = createRun(); + CTR ctRun = run.getCTR(); + ctRun.addNewRPr().addNewRStyle().setVal("FootnoteReference"); + ctRun.addNewFootnoteReference().setId(footnote.getId()); + } } diff --git a/src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFFootnote.java b/src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFFootnote.java new file mode 100644 index 000000000..8bb377fe5 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xwpf/usermodel/TestXWPFFootnote.java @@ -0,0 +1,158 @@ +/* ==================================================================== + 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. +==================================================================== */ +package org.apache.poi.xwpf.usermodel; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.List; + +import org.apache.poi.xwpf.XWPFTestDataSamples; +import org.junit.Before; +import org.junit.Test; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFtnEdnRef; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; + +public class TestXWPFFootnote { + + private XWPFDocument docOut; + private String p1Text; + private String p2Text; + private BigInteger footnoteId; + private XWPFFootnote footnote; + + @Before + public void setUp() { + docOut = new XWPFDocument(); + p1Text = "First paragraph in footnote"; + p2Text = "Second paragraph in footnote"; + + // NOTE: XWPFDocument.createFootnote() delegates directly + // to XWPFFootnotes.createFootnote() so this tests + // both creation of new XWPFFootnotes in document + // and XWPFFootnotes.createFootnote(); + + // NOTE: Creating the footnote does not automatically + // create a first paragraph. + footnote = docOut.createFootnote(); + footnoteId = footnote.getId(); + + } + + @Test + public void testAddParagraphsToFootnote() throws IOException { + + // Add a run to the first paragraph: + + XWPFParagraph p1 = footnote.createParagraph(); + p1.createRun().setText(p1Text); + + // Create a second paragraph: + + XWPFParagraph p = footnote.createParagraph(); + assertNotNull("Paragraph is null", p); + p.createRun().setText(p2Text); + + XWPFDocument docIn = XWPFTestDataSamples.writeOutAndReadBack(docOut); + + XWPFFootnote testFootnote = docIn.getFootnoteByID(footnoteId.intValue()); + assertNotNull(testFootnote); + + assertEquals(2, testFootnote.getParagraphs().size()); + XWPFParagraph testP1 = testFootnote.getParagraphs().get(0); + assertEquals(p1Text, testP1.getText()); + + XWPFParagraph testP2 = testFootnote.getParagraphs().get(1); + assertEquals(p2Text, testP2.getText()); + + // The first paragraph added using createParagraph() should + // have the required footnote reference added to the first + // run. + + // Verify that we have a footnote reference in the first paragraph and not + // in the second paragraph. + + XWPFRun r1 = testP1.getRuns().get(0); + assertNotNull(r1); + assertTrue("No footnote reference in testP1", r1.getCTR().getFootnoteRefList().size() > 0); + assertNotNull("No footnote reference in testP1", r1.getCTR().getFootnoteRefArray(0)); + + XWPFRun r2 = testP2.getRuns().get(0); + assertNotNull("Expected a run in testP2", r2); + assertTrue("Found a footnote reference in testP2", r2.getCTR().getFootnoteRefList().size() == 0); + + } + + @Test + public void testAddTableToFootnote() throws IOException { + XWPFTable table = footnote.createTable(); + assertNotNull(table); + + XWPFDocument docIn = XWPFTestDataSamples.writeOutAndReadBack(docOut); + + XWPFFootnote testFootnote = docIn.getFootnoteByID(footnoteId.intValue()); + XWPFTable testTable = testFootnote.getTableArray(0); + assertNotNull(testTable); + + table = footnote.createTable(2, 3); + assertEquals(2, table.getNumberOfRows()); + assertEquals(3, table.getRow(0).getTableCells().size()); + + // If the table is the first body element of the footnote then + // a paragraph with the footnote reference should have been + // added automatically. + + assertEquals("Expected 3 body elements", 3, footnote.getBodyElements().size()); + IBodyElement testP1 = footnote.getBodyElements().get(0); + assertTrue("Expected a paragraph, got " + testP1.getClass().getSimpleName() , testP1 instanceof XWPFParagraph); + XWPFRun r1 = ((XWPFParagraph)testP1).getRuns().get(0); + assertNotNull(r1); + assertTrue("No footnote reference in testP1", r1.getCTR().getFootnoteRefList().size() > 0); + assertNotNull("No footnote reference in testP1", r1.getCTR().getFootnoteRefArray(0)); + + } + + @Test + public void testRemoveFootnote() { + // NOTE: XWPFDocument.removeFootnote() delegates directly to + // XWPFFootnotes. + docOut.createFootnote(); + assertEquals("Expected 2 footnotes", 2, docOut.getFootnotes().size()); + assertNotNull("Didn't get second footnote", docOut.getFootnotes().get(1)); + boolean result = docOut.removeFootnote(0); + assertTrue("Remove footnote did not return true", result); + assertEquals("Expected 1 footnote after removal", 1, docOut.getFootnotes().size()); + } + + @Test + public void testAddFootnoteRefToParagraph() { + XWPFParagraph p = docOut.createParagraph(); + List